├── LICENSE.md ├── README.md ├── handler.go ├── query.go ├── query_test.go ├── server.go └── server_test.go /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 Mitchell Hashimoto 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-finger 2 | 3 | go-finger is a [finger](https://en.wikipedia.org/wiki/Finger_protocol) 4 | library written in Go. This contains both a client and server implementation. 5 | 6 | The [finger protocol](https://tools.ietf.org/html/rfc1288) is an extremely 7 | simple TCP protocol. It can be implemented without a library cleanly in only 8 | a few dozen lines of code but a library helps ensure correctness and handles 9 | RFC-compliant request parsing automatically. 10 | 11 | ## Example 12 | 13 | ```go 14 | import "github.com/mitchellh/go-finger" 15 | ``` 16 | 17 | ### Server 18 | 19 | ```go 20 | go finger.Serve(finger.HandlerFunc(func(ctx context.Context, w io.Writer, q *finger.Query) { 21 | w.Write([]byte(fmt.Sprintf("Hello %q", q.Username))) 22 | })) 23 | ``` 24 | 25 | You can also set more detailed configurations by creating a `Server` 26 | structure directly. The top-level `Serve` function sets reasonable defaults. 27 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package finger 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | // Handler responds to a Finger query. 9 | // 10 | // ServerFinger should respond to a finger query and write the response 11 | // to the given io.Writer. 12 | type Handler interface { 13 | ServeFinger(context.Context, io.Writer, *Query) 14 | } 15 | 16 | // HandlerFunc allows use of ordinary functions as finger handlers. 17 | type HandlerFunc func(context.Context, io.Writer, *Query) 18 | 19 | func (f HandlerFunc) ServeFinger(ctx context.Context, w io.Writer, q *Query) { 20 | f(ctx, w, q) 21 | } 22 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | package finger 2 | 3 | import ( 4 | "net" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | // Query is a valid query a finger server can receive or a client can send. 10 | type Query struct { 11 | Username string // Username, can be blank 12 | Hostname []string // Hostname (zero or more) 13 | 14 | RemoteAddr net.Addr // RemoteAddr set by Server, no effect on clients 15 | } 16 | 17 | // ParseQuery parses the line to determine the finger query. According to 18 | // the spec, the line should end with a newline. The input here can omit this. 19 | // If the newline exists, it will still parse. 20 | func ParseQuery(line string) (*Query, error) { 21 | values := lineRegexp.FindStringSubmatch(line) 22 | if values == nil { 23 | return nil, nil 24 | } 25 | 26 | // The username is always available at index 1, even if blank 27 | var result Query 28 | result.Username = values[1] 29 | 30 | // If we have non-empty host text, then parse those 31 | if values[2] != "" { 32 | parts := strings.Split(values[2], "@") 33 | result.Hostname = parts[1:] 34 | } 35 | 36 | return &result, nil 37 | } 38 | 39 | /* 40 | From RFC 1288 the BNF is as follows. This is matchable via a regexp. 41 | 42 | {Q1} ::= [{W}|{W}{S}{U}]{C} 43 | {Q2} ::= [{W}{S}][{U}]{H}{C} 44 | {U} ::= username 45 | {H} ::= @hostname | @hostname{H} 46 | {W} ::= /W 47 | {S} ::= | {S} 48 | {C} ::= 49 | */ 50 | var lineRegexp = regexp.MustCompile(`` + // This just forces alignment below 51 | `\s*` + // [{S}] 52 | `(?P[\w-]+)?` + // [{U}] 53 | `(?P(@[\w-]+)+)*`, // {H} 54 | ) 55 | -------------------------------------------------------------------------------- /query_test.go: -------------------------------------------------------------------------------- 1 | package finger 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParseQuery(t *testing.T) { 9 | cases := []struct { 10 | Input string 11 | Expected *Query 12 | }{ 13 | { 14 | "", 15 | &Query{Username: ""}, 16 | }, 17 | 18 | { 19 | "foo", 20 | &Query{Username: "foo"}, 21 | }, 22 | 23 | { 24 | "foo@host", 25 | &Query{ 26 | Username: "foo", 27 | Hostname: []string{"host"}, 28 | }, 29 | }, 30 | 31 | { 32 | "foo@jump@host", 33 | &Query{ 34 | Username: "foo", 35 | Hostname: []string{"jump", "host"}, 36 | }, 37 | }, 38 | 39 | { 40 | "@host", 41 | &Query{ 42 | Username: "", 43 | Hostname: []string{"host"}, 44 | }, 45 | }, 46 | 47 | { 48 | "foo@host\n", 49 | &Query{ 50 | Username: "foo", 51 | Hostname: []string{"host"}, 52 | }, 53 | }, 54 | } 55 | 56 | for _, tc := range cases { 57 | t.Run(tc.Input, func(t *testing.T) { 58 | actual, err := ParseQuery(tc.Input) 59 | if err != nil { 60 | t.Fatalf("err: %s", err) 61 | } 62 | 63 | if !reflect.DeepEqual(actual, tc.Expected) { 64 | t.Fatalf("bad: %#v", actual) 65 | } 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package finger 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "io" 7 | "net" 8 | "time" 9 | ) 10 | 11 | // Server defines parameters for running a Finger server. The zero value 12 | // of a Server is a valid configuration, though every request will be 13 | // closed immediately since no handler is set. 14 | type Server struct { 15 | Addr string 16 | Handler Handler 17 | 18 | // ReadTimeout is the maximum duration before timing out reads of the 19 | // response. This sets a deadline on the connection and isn't a handler 20 | // timeout. 21 | ReadTimeout time.Duration 22 | 23 | // WriteTimeout is the maximum duration before timing out writes of the 24 | // response. This sets a deadline on the connection and isn't a handler 25 | // timeout. 26 | WriteTimeout time.Duration 27 | 28 | // MaxQueryBytes is the maximum amount of bytes that will be read from 29 | // the connection to determine the query. 30 | MaxQueryBytes int 31 | } 32 | 33 | // Serve listens on the finger port and serves connections with the given 34 | // handler. This sets reasonable read/write timeouts. Please see the source 35 | // for the exact timeouts which should be generously high. 36 | func Serve(h Handler) error { 37 | s := &Server{ 38 | Handler: h, 39 | ReadTimeout: 5 * time.Minute, 40 | WriteTimeout: 5 * time.Minute, 41 | MaxQueryBytes: 4096, 42 | } 43 | 44 | return s.ListenAndServe() 45 | } 46 | 47 | // ListenAndServe listens on the TCP network address s.Addr and then 48 | // calls Serve to handle incoming connections. If s.Addr is blank, 49 | // ":finger" is used. 50 | func (s *Server) ListenAndServe() error { 51 | addr := s.Addr 52 | if addr == "" { 53 | addr = ":finger" 54 | } 55 | 56 | ln, err := net.Listen("tcp", addr) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return s.Serve(ln) 62 | } 63 | 64 | // Serve accepts incoming connections on the listener l, creating a new 65 | // service goroutine for each. 66 | // 67 | // The listener is closed when this function returns. 68 | func (s *Server) Serve(l net.Listener) error { 69 | defer l.Close() 70 | 71 | for { 72 | c, err := l.Accept() 73 | if err != nil { 74 | return err 75 | } 76 | 77 | s.setupConn(c) 78 | go s.ServeConn(context.Background(), c) 79 | } 80 | } 81 | 82 | // ServeConn serves a single connection, blocking until the request is complete. 83 | func (s *Server) ServeConn(ctx context.Context, conn io.ReadWriteCloser) error { 84 | // Close the connection once we're done in any case 85 | defer conn.Close() 86 | 87 | // If we have a maximum amount to read, then setup a limit 88 | var r io.Reader = conn 89 | if s.MaxQueryBytes > 0 { 90 | r = io.LimitReader(conn, int64(s.MaxQueryBytes)) 91 | } 92 | 93 | // Read the query line 94 | buf := bufio.NewReader(r) 95 | line, err := buf.ReadString('\n') 96 | if err != nil { 97 | return err 98 | } 99 | 100 | // Parse the query 101 | query, err := ParseQuery(line) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | // If we have a net conn then setup the remote addr 107 | if nc, ok := conn.(net.Conn); ok { 108 | query.RemoteAddr = nc.RemoteAddr() 109 | } 110 | 111 | if s.Handler != nil { 112 | s.Handler.ServeFinger(ctx, conn, query) 113 | } 114 | 115 | return nil 116 | } 117 | 118 | func (s *Server) setupConn(c net.Conn) { 119 | t0 := time.Now() 120 | if s.ReadTimeout > 0 { 121 | c.SetReadDeadline(t0.Add(s.ReadTimeout)) 122 | } 123 | 124 | if s.WriteTimeout > 0 { 125 | c.SetWriteDeadline(t0.Add(s.WriteTimeout)) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package finger 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "net" 8 | "os/exec" 9 | "testing" 10 | ) 11 | 12 | func TestServerServeConn(t *testing.T) { 13 | ln := testFingerLn(t) 14 | defer ln.Close() 15 | 16 | // Setup the handler 17 | handler := func(ctx context.Context, w io.Writer, q *Query) { 18 | w.Write([]byte("received!")) 19 | } 20 | 21 | // Serve 22 | s := &Server{Handler: HandlerFunc(handler)} 23 | go s.Serve(ln) 24 | 25 | // Use the finger command to run it 26 | actual := testFinger(t, "foo@127.0.0.1") 27 | if !bytes.Contains(actual, []byte("received!")) { 28 | t.Fatalf("bad: %s", actual) 29 | } 30 | } 31 | 32 | func TestServerServeConn_maxQueryBytes(t *testing.T) { 33 | ln := testFingerLn(t) 34 | defer ln.Close() 35 | 36 | // Setup the handler 37 | handler := func(ctx context.Context, w io.Writer, q *Query) { 38 | panic("don't call me!") 39 | } 40 | 41 | // Serve 42 | s := &Server{Handler: HandlerFunc(handler)} 43 | s.MaxQueryBytes = 4 44 | go s.Serve(ln) 45 | 46 | // Use the finger command to run it 47 | testFinger(t, "toomanybytes@127.0.0.1") 48 | } 49 | 50 | func testFingerLn(t *testing.T) net.Listener { 51 | ln, err := net.Listen("tcp", ":finger") 52 | if err != nil { 53 | t.Skipf("can't listen on finger port: %s", err) 54 | t.SkipNow() 55 | } 56 | 57 | return ln 58 | } 59 | 60 | func testFinger(t *testing.T, query string) []byte { 61 | if _, err := exec.LookPath("finger"); err != nil { 62 | t.Skipf("finger not found") 63 | t.SkipNow() 64 | } 65 | 66 | actual, err := exec.Command("finger", query).Output() 67 | if err != nil { 68 | t.Fatalf("err: %s", err) 69 | } 70 | 71 | return actual 72 | } 73 | --------------------------------------------------------------------------------