blob: 4d65b283fffb1982b667511a94bca2a368ede049 [file] [log] [blame]
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}
}