| package ident |
| |
| import ( |
| "bufio" |
| "context" |
| "fmt" |
| "io" |
| "net" |
| "strconv" |
| |
| "github.com/golang/glog" |
| ) |
| |
| type DialOption func(d *dialOptions) |
| |
| type dialOptions struct { |
| dialer func(context.Context, string, string) (net.Conn, error) |
| } |
| |
| // WithDialer configures a Client to use a given dial function instead of the |
| // default implementation in net. |
| func WithDialer(dialer func(context.Context, string, string) (net.Conn, error)) DialOption { |
| return func(d *dialOptions) { |
| d.dialer = dialer |
| } |
| } |
| |
| // parseTarget interprets a target string (ie. the target address of the Dial |
| // function) as an ident service address, either a host:port pair, or a host |
| // (in which case the default ident port, 113, is used). |
| func parseTarget(s string) (string, uint16, error) { |
| host, portStr, err := net.SplitHostPort(s) |
| if err == nil { |
| port, err := strconv.ParseUint(portStr, 10, 16) |
| if err != nil { |
| return "", 0, fmt.Errorf("can't parse port %q: %w", portStr, err) |
| } |
| return host, uint16(port), nil |
| } |
| |
| // Doesn't look like a host:port pair? Default to port 113. |
| return s, 113, nil |
| } |
| |
| // Dial sets up an ident protocol Client that will connect to the given target. |
| // Target can be either a host:port pair, or just a host (in which case the |
| // default ident port, 113, is used). |
| // This does not actually connect to identd over TCP - that will be done, as |
| // necessary, as requests are processed (including reconnections if multiple |
| // requests are processed on a Client which connects to a server that does not |
| // support long-standing ident donnections). |
| func Dial(target string, options ...DialOption) (*Client, error) { |
| host, port, err := parseTarget(target) |
| if err != nil { |
| return nil, fmt.Errorf("invalid target: %v", err) |
| } |
| |
| dialer := net.Dialer{} |
| opts := dialOptions{ |
| dialer: dialer.DialContext, |
| } |
| for _, opt := range options { |
| opt(&opts) |
| } |
| |
| return &Client{ |
| opts: opts, |
| target: net.JoinHostPort(host, fmt.Sprintf("%d", port)), |
| conn: nil, |
| scanner: nil, |
| }, nil |
| } |
| |
| // Client is an ident protocol client. It maintains a connection to the ident |
| // server that it's been configured for, reconnecting as necessary. It is not |
| // safe to be used by multiple goroutines. |
| type Client struct { |
| // opts are the dialOptions with which the client has been constructed. |
| opts dialOptions |
| // target is the full host:port pair that the client should connect to. |
| target string |
| // conn is either nil or an active TCP connection to the ident server. |
| conn net.Conn |
| // scannner is either nil or a line-scanner attached to the receive side of |
| // conn. |
| scanner *bufio.Scanner |
| } |
| |
| func (c *Client) connect(ctx context.Context) error { |
| glog.V(1).Infof("Dialing IDENT at %q", c.target) |
| conn, err := c.opts.dialer(ctx, "tcp", c.target) |
| if err != nil { |
| return fmt.Errorf("connecting: %w", err) |
| } |
| c.conn = conn |
| c.scanner = bufio.NewScanner(conn) |
| return nil |
| } |
| |
| func (c *Client) disconnect() { |
| if c.conn == nil { |
| return |
| } |
| c.conn.Close() |
| c.conn = nil |
| } |
| |
| // Do executes the given Request against the server to which the Client is |
| // connected. |
| func (c *Client) Do(ctx context.Context, r *Request) (*Response, error) { |
| glog.V(1).Infof("Do(%+v)", r) |
| |
| // Connect if needed. |
| if c.conn == nil { |
| if err := c.connect(ctx); err != nil { |
| return nil, err |
| } |
| } |
| |
| // Start a goroutine that will perform the actual request/response |
| // processing to the server. A successful response will land in resC, while |
| // any protocl-level error will land in errC. |
| // We make both channels buffered, because if the context expires without a |
| // response, we want the goroutine to be able to write to them even though |
| // we're not receiving anymore. The channel will then be garbage collected. |
| resC := make(chan *Response, 1) |
| errC := make(chan error, 1) |
| go func() { |
| data := r.encode() |
| glog.V(3).Infof(" -> %q", data) |
| _, err := c.conn.Write(data) |
| if err != nil { |
| errC <- fmt.Errorf("Write: %w", err) |
| return |
| } |
| if !c.scanner.Scan() { |
| // scanner.Err() returns nil on EOF. We want that EOF, as the ident |
| // protocol has special meaning for EOF sent by the server |
| // (indicating either a lack of support for multiple requests per |
| // connection, or a refusal to serve at an early stage of the |
| // connection). |
| if err := c.scanner.Err(); err != nil { |
| errC <- fmt.Errorf("Read: %w", err) |
| } else { |
| errC <- fmt.Errorf("Read: %w", io.EOF) |
| } |
| } |
| data = c.scanner.Bytes() |
| glog.V(3).Infof(" <- %q", data) |
| resp, err := decodeResponse(data) |
| if err != nil { |
| errC <- err |
| } else { |
| resC <- resp |
| } |
| }() |
| |
| select { |
| case <-ctx.Done(): |
| // If the context is closed, fail with the context error and kill the |
| // connection. The running goroutine will error out on any pending |
| // network I/O and fail at some later point. |
| // TODO(q3k): make the communication goroutine long-lived and don't |
| // kill it here, just let it finish whatever it's doing and ignore the |
| // result. |
| c.disconnect() |
| return nil, ctx.Err() |
| case res := <-resC: |
| return res, nil |
| case err := <-errC: |
| // TODO(q3k): interpret EOF, which can mean different things at |
| // different times according to the RFC. |
| if c.conn != nil { |
| c.conn.Close() |
| c.conn = nil |
| } |
| return nil, err |
| } |
| } |
| |
| // Close closes the Client, closing any underlying TCP connection. |
| func (c *Client) Close() error { |
| if c.conn == nil { |
| return nil |
| } |
| return c.conn.Close() |
| } |
| |
| // Query performs a single ident protocol request to a server running at target |
| // and returns the received ident response. If the ident server cannot be |
| // queries, or the ident server returns an ident error, an error is returned. |
| // |
| // This a convenience wrapper around Dial/Do. The given target must be either a |
| // host or host:port pair. If not given, the port defaults to 113. |
| // |
| // Returned ident server error resposes are *IdentError, and can be tested for |
| // using errors.Is/errors.As. See the IdentError type documentation for more |
| // information. |
| // |
| // The given context will be used to time out the request, either at the |
| // connection or request stage. If the context is canceled/times out, the |
| // context error will be returned and the query aborted. |
| // |
| // Since Query opens a connection to the ident server just for a single query, |
| // it should not be used if a single server is going to be queries about |
| // multiple addresses, and instead Dial/Do should be used to keep a |
| // long-standing connection if possible. |
| func Query(ctx context.Context, target string, client, server uint16) (*IdentResponse, error) { |
| cl, err := Dial(target) |
| if err != nil { |
| return nil, fmt.Errorf("could not dial: %w", err) |
| } |
| defer cl.Close() |
| resp, err := cl.Do(ctx, &Request{ |
| ClientPort: client, |
| ServerPort: server, |
| }) |
| if err != nil { |
| return nil, fmt.Errorf("could not query: %w", err) |
| } |
| if resp.Ident != nil { |
| return resp.Ident, nil |
| } |
| return nil, &IdentError{Inner: resp.Error} |
| } |