Serge Bazanski | d4438d6 | 2021-05-23 13:37:30 +0200 | [diff] [blame] | 1 | package ident |
| 2 | |
| 3 | import ( |
| 4 | "bufio" |
| 5 | "context" |
| 6 | "fmt" |
| 7 | "io" |
| 8 | "net" |
| 9 | "strconv" |
| 10 | |
| 11 | "github.com/golang/glog" |
| 12 | ) |
| 13 | |
| 14 | type DialOption func(d *dialOptions) |
| 15 | |
| 16 | type dialOptions struct { |
| 17 | dialer func(context.Context, string, string) (net.Conn, error) |
| 18 | } |
| 19 | |
| 20 | // WithDialer configures a Client to use a given dial function instead of the |
| 21 | // default implementation in net. |
| 22 | func WithDialer(dialer func(context.Context, string, string) (net.Conn, error)) DialOption { |
| 23 | return func(d *dialOptions) { |
| 24 | d.dialer = dialer |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | // parseTarget interprets a target string (ie. the target address of the Dial |
| 29 | // function) as an ident service address, either a host:port pair, or a host |
| 30 | // (in which case the default ident port, 113, is used). |
| 31 | func parseTarget(s string) (string, uint16, error) { |
| 32 | host, portStr, err := net.SplitHostPort(s) |
| 33 | if err == nil { |
| 34 | port, err := strconv.ParseUint(portStr, 10, 16) |
| 35 | if err != nil { |
| 36 | return "", 0, fmt.Errorf("can't parse port %q: %w", portStr, err) |
| 37 | } |
| 38 | return host, uint16(port), nil |
| 39 | } |
| 40 | |
| 41 | // Doesn't look like a host:port pair? Default to port 113. |
| 42 | return s, 113, nil |
| 43 | } |
| 44 | |
| 45 | // Dial sets up an ident protocol Client that will connect to the given target. |
| 46 | // Target can be either a host:port pair, or just a host (in which case the |
| 47 | // default ident port, 113, is used). |
| 48 | // This does not actually connect to identd over TCP - that will be done, as |
| 49 | // necessary, as requests are processed (including reconnections if multiple |
| 50 | // requests are processed on a Client which connects to a server that does not |
| 51 | // support long-standing ident donnections). |
| 52 | func Dial(target string, options ...DialOption) (*Client, error) { |
| 53 | host, port, err := parseTarget(target) |
| 54 | if err != nil { |
| 55 | return nil, fmt.Errorf("invalid target: %v", err) |
| 56 | } |
| 57 | |
| 58 | dialer := net.Dialer{} |
| 59 | opts := dialOptions{ |
| 60 | dialer: dialer.DialContext, |
| 61 | } |
| 62 | for _, opt := range options { |
| 63 | opt(&opts) |
| 64 | } |
| 65 | |
| 66 | return &Client{ |
| 67 | opts: opts, |
| 68 | target: net.JoinHostPort(host, fmt.Sprintf("%d", port)), |
| 69 | conn: nil, |
| 70 | scanner: nil, |
| 71 | }, nil |
| 72 | } |
| 73 | |
| 74 | // Client is an ident protocol client. It maintains a connection to the ident |
| 75 | // server that it's been configured for, reconnecting as necessary. It is not |
| 76 | // safe to be used by multiple goroutines. |
| 77 | type Client struct { |
| 78 | // opts are the dialOptions with which the client has been constructed. |
| 79 | opts dialOptions |
| 80 | // target is the full host:port pair that the client should connect to. |
| 81 | target string |
| 82 | // conn is either nil or an active TCP connection to the ident server. |
| 83 | conn net.Conn |
| 84 | // scannner is either nil or a line-scanner attached to the receive side of |
| 85 | // conn. |
| 86 | scanner *bufio.Scanner |
| 87 | } |
| 88 | |
| 89 | func (c *Client) connect(ctx context.Context) error { |
| 90 | glog.V(1).Infof("Dialing IDENT at %q", c.target) |
| 91 | conn, err := c.opts.dialer(ctx, "tcp", c.target) |
| 92 | if err != nil { |
| 93 | return fmt.Errorf("connecting: %w", err) |
| 94 | } |
| 95 | c.conn = conn |
| 96 | c.scanner = bufio.NewScanner(conn) |
| 97 | return nil |
| 98 | } |
| 99 | |
| 100 | func (c *Client) disconnect() { |
| 101 | if c.conn == nil { |
| 102 | return |
| 103 | } |
| 104 | c.conn.Close() |
| 105 | c.conn = nil |
| 106 | } |
| 107 | |
| 108 | // Do executes the given Request against the server to which the Client is |
| 109 | // connected. |
| 110 | func (c *Client) Do(ctx context.Context, r *Request) (*Response, error) { |
| 111 | glog.V(1).Infof("Do(%+v)", r) |
| 112 | |
| 113 | // Connect if needed. |
| 114 | if c.conn == nil { |
| 115 | if err := c.connect(ctx); err != nil { |
| 116 | return nil, err |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | // Start a goroutine that will perform the actual request/response |
| 121 | // processing to the server. A successful response will land in resC, while |
| 122 | // any protocl-level error will land in errC. |
| 123 | // We make both channels buffered, because if the context expires without a |
| 124 | // response, we want the goroutine to be able to write to them even though |
| 125 | // we're not receiving anymore. The channel will then be garbage collected. |
| 126 | resC := make(chan *Response, 1) |
| 127 | errC := make(chan error, 1) |
| 128 | go func() { |
| 129 | data := r.encode() |
| 130 | glog.V(3).Infof(" -> %q", data) |
| 131 | _, err := c.conn.Write(data) |
| 132 | if err != nil { |
| 133 | errC <- fmt.Errorf("Write: %w", err) |
| 134 | return |
| 135 | } |
| 136 | if !c.scanner.Scan() { |
| 137 | // scanner.Err() returns nil on EOF. We want that EOF, as the ident |
| 138 | // protocol has special meaning for EOF sent by the server |
| 139 | // (indicating either a lack of support for multiple requests per |
| 140 | // connection, or a refusal to serve at an early stage of the |
| 141 | // connection). |
| 142 | if err := c.scanner.Err(); err != nil { |
| 143 | errC <- fmt.Errorf("Read: %w", err) |
| 144 | } else { |
| 145 | errC <- fmt.Errorf("Read: %w", io.EOF) |
| 146 | } |
| 147 | } |
| 148 | data = c.scanner.Bytes() |
| 149 | glog.V(3).Infof(" <- %q", data) |
| 150 | resp, err := decodeResponse(data) |
| 151 | if err != nil { |
| 152 | errC <- err |
| 153 | } else { |
| 154 | resC <- resp |
| 155 | } |
| 156 | }() |
| 157 | |
| 158 | select { |
| 159 | case <-ctx.Done(): |
| 160 | // If the context is closed, fail with the context error and kill the |
| 161 | // connection. The running goroutine will error out on any pending |
| 162 | // network I/O and fail at some later point. |
| 163 | // TODO(q3k): make the communication goroutine long-lived and don't |
| 164 | // kill it here, just let it finish whatever it's doing and ignore the |
| 165 | // result. |
| 166 | c.disconnect() |
| 167 | return nil, ctx.Err() |
| 168 | case res := <-resC: |
| 169 | return res, nil |
| 170 | case err := <-errC: |
| 171 | // TODO(q3k): interpret EOF, which can mean different things at |
| 172 | // different times according to the RFC. |
| 173 | if c.conn != nil { |
| 174 | c.conn.Close() |
| 175 | c.conn = nil |
| 176 | } |
| 177 | return nil, err |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | // Close closes the Client, closing any underlying TCP connection. |
| 182 | func (c *Client) Close() error { |
| 183 | if c.conn == nil { |
| 184 | return nil |
| 185 | } |
| 186 | return c.conn.Close() |
| 187 | } |
Serge Bazanski | 8e603e1 | 2021-05-23 18:24:23 +0200 | [diff] [blame] | 188 | |
| 189 | // Query performs a single ident protocol request to a server running at target |
| 190 | // and returns the received ident response. If the ident server cannot be |
| 191 | // queries, or the ident server returns an ident error, an error is returned. |
| 192 | // |
| 193 | // This a convenience wrapper around Dial/Do. The given target must be either a |
| 194 | // host or host:port pair. If not given, the port defaults to 113. |
| 195 | // |
| 196 | // Returned ident server error resposes are *IdentError, and can be tested for |
| 197 | // using errors.Is/errors.As. See the IdentError type documentation for more |
| 198 | // information. |
| 199 | // |
| 200 | // The given context will be used to time out the request, either at the |
| 201 | // connection or request stage. If the context is canceled/times out, the |
| 202 | // context error will be returned and the query aborted. |
| 203 | // |
| 204 | // Since Query opens a connection to the ident server just for a single query, |
| 205 | // it should not be used if a single server is going to be queries about |
| 206 | // multiple addresses, and instead Dial/Do should be used to keep a |
| 207 | // long-standing connection if possible. |
| 208 | func Query(ctx context.Context, target string, client, server uint16) (*IdentResponse, error) { |
| 209 | cl, err := Dial(target) |
| 210 | if err != nil { |
| 211 | return nil, fmt.Errorf("could not dial: %w", err) |
| 212 | } |
| 213 | defer cl.Close() |
| 214 | resp, err := cl.Do(ctx, &Request{ |
| 215 | ClientPort: client, |
| 216 | ServerPort: server, |
| 217 | }) |
| 218 | if err != nil { |
| 219 | return nil, fmt.Errorf("could not query: %w", err) |
| 220 | } |
| 221 | if resp.Ident != nil { |
| 222 | return resp.Ident, nil |
| 223 | } |
| 224 | return nil, &IdentError{Inner: resp.Error} |
| 225 | } |