2021-05-23 11:37:30 +00:00
|
|
|
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()
|
|
|
|
}
|
2021-05-23 16:24:23 +00:00
|
|
|
|
|
|
|
// 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}
|
|
|
|
}
|