blob: 4d65b283fffb1982b667511a94bca2a368ede049 [file] [log] [blame]
Serge Bazanskid4438d62021-05-23 13:37:30 +02001package ident
2
3import (
4 "bufio"
5 "context"
6 "fmt"
7 "io"
8 "net"
9 "strconv"
10
11 "github.com/golang/glog"
12)
13
14type DialOption func(d *dialOptions)
15
16type 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.
22func 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).
31func 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).
52func 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.
77type 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
89func (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
100func (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.
110func (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.
182func (c *Client) Close() error {
183 if c.conn == nil {
184 return nil
185 }
186 return c.conn.Close()
187}
Serge Bazanski8e603e12021-05-23 18:24:23 +0200188
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.
208func 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}