finger protocol
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
9cbc5cdd46
commit
4f6f3dcd4b
|
@ -17,7 +17,7 @@ Gus is carefully structured as composable building blocks. The top-level package
|
||||||
|
|
||||||
## Protocols
|
## Protocols
|
||||||
|
|
||||||
The packages gus/gemini and gus/gopher provide concrete implementations of gus abstractions specific to those protocols.
|
The packages gus/gemini, gus/gopher, and gus/finger provide concrete implementations of gus abstractions specific to those protocols.
|
||||||
* I/O (parsing, formatting) request and responses
|
* I/O (parsing, formatting) request and responses
|
||||||
* constructors for the various kinds of protocol responses
|
* constructors for the various kinds of protocol responses
|
||||||
* helpers for building a protocol-suitable TLS config
|
* helpers for building a protocol-suitable TLS config
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"tildegit.org/tjp/gus/finger"
|
||||||
|
"tildegit.org/tjp/gus/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
_, infoLog, _, errLog := logging.DefaultLoggers()
|
||||||
|
|
||||||
|
fs, err := finger.NewServer(
|
||||||
|
context.Background(),
|
||||||
|
"localhost",
|
||||||
|
"tcp",
|
||||||
|
":79",
|
||||||
|
logging.LogRequests(infoLog)(finger.SystemFinger(false)),
|
||||||
|
errLog,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.Serve()
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package finger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"tildegit.org/tjp/gus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ForwardingDenied is returned in response to requests for forwarding service.
|
||||||
|
var ForwardingDenied = errors.New("Finger forwarding service denied.")
|
||||||
|
|
||||||
|
// InvalidFingerQuery is sent when a client doesn't properly format the query.
|
||||||
|
var InvalidFingerQuery = errors.New("Invalid finger query .")
|
||||||
|
|
||||||
|
// ParseRequest builds a gus.Request by reading a finger protocol request.
|
||||||
|
//
|
||||||
|
// At the time of writing, there is no firm standard on how to represent finger
|
||||||
|
// queries as URLs (the finger protocol itself predates URLs entirely), but there
|
||||||
|
// are a few helpful resources to go from.
|
||||||
|
// - The lynx browser supports finger URLs and documents the forms they may take:
|
||||||
|
// https://lynx.invisible-island.net/lynx_help/lynx_url_support.html#finger_url
|
||||||
|
// - There is an IETF draft:
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-uri-url-finger
|
||||||
|
//
|
||||||
|
// As this function builds a *gus.Request (which is mostly a wrapper around a URL)
|
||||||
|
// from nothing but an io.Reader, it doesn't have the context of the hostname which
|
||||||
|
// the receiving server was hosting. So it only has the host component if it
|
||||||
|
// arrived in the body of the query in the form username@hostname. Bear in mind that
|
||||||
|
// in gus handlers, request objects will also carry a reference to the server so
|
||||||
|
// that hostname is always available as request.Server.Hostname().
|
||||||
|
//
|
||||||
|
// The primary deviation from the IETF draft is that a query-specified host becomes
|
||||||
|
// the Host section of the URL, rather than remaining in the Path. Where the IETF draft
|
||||||
|
// would consider a query of "tjp@ctrl-c.club\r\n" to be "finger:/tjp@ctrl-c.club", this
|
||||||
|
// function will parse it into "finger://ctrl-c.club/tjp". This decision to separate the
|
||||||
|
// query-specified host from the username is intended to make it easier to avoid
|
||||||
|
// inadvertently acting as a jump host for example with:
|
||||||
|
// `exec.Command("/usr/bin/finger", request.Path[1:])`.
|
||||||
|
//
|
||||||
|
// Consistent with the IETF draft, the /W whois switch is dropped and not represented
|
||||||
|
// in the URL at all.
|
||||||
|
//
|
||||||
|
// In accordance with the recommendation of RFC 1288 section 3.2.1
|
||||||
|
// (https://datatracker.ietf.org/doc/html/rfc1288#section-3.2.1), any queries which
|
||||||
|
// include a jump-host (user@host1@host2) are rejected with the ForwardingDenied error.
|
||||||
|
func ParseRequest(rdr io.Reader) (*gus.Request, error) {
|
||||||
|
line, err := bufio.NewReader(rdr).ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if line[len(line)-2] != '\r' {
|
||||||
|
return nil, InvalidFingerQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.TrimSuffix(line, "\r\n")
|
||||||
|
line = strings.TrimPrefix(line, "/W")
|
||||||
|
line = strings.TrimLeft(line, " ")
|
||||||
|
|
||||||
|
username, hostname, _ := strings.Cut(line, "@")
|
||||||
|
if strings.Contains(hostname, "@") {
|
||||||
|
return nil, ForwardingDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
return &gus.Request{URL: &url.URL{
|
||||||
|
Scheme: "finger",
|
||||||
|
Host: hostname,
|
||||||
|
Path: "/" + username,
|
||||||
|
OmitHost: true, //nolint:typecheck
|
||||||
|
// (for some reason typecheck on drone-ci doesn't realize OmitHost is a field in url.URL)
|
||||||
|
}}, nil
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package finger_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"tildegit.org/tjp/gus/finger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseRequest(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
source string
|
||||||
|
host string
|
||||||
|
path string
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
source: "/W tjp\r\n",
|
||||||
|
host: "",
|
||||||
|
path: "/tjp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "tjp@host.com\r\n",
|
||||||
|
host: "host.com",
|
||||||
|
path: "/tjp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "tjp@forwarder.com@host.com\r\n",
|
||||||
|
err: finger.ForwardingDenied,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "tjp\r\n",
|
||||||
|
host: "",
|
||||||
|
path: "/tjp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "\r\n",
|
||||||
|
host: "",
|
||||||
|
path: "/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/W\r\n",
|
||||||
|
host: "",
|
||||||
|
path: "/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "tjp",
|
||||||
|
err: io.EOF,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.source, func(t *testing.T) {
|
||||||
|
request, err := finger.ParseRequest(bytes.NewBufferString(test.source))
|
||||||
|
require.Equal(t, test.err, err)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
assert.Equal(t, "finger", request.Scheme)
|
||||||
|
assert.Equal(t, test.host, request.Host)
|
||||||
|
assert.Equal(t, test.path, request.Path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package finger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"tildegit.org/tjp/gus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error produces a finger Response containing the error message and Status 1.
|
||||||
|
func Error(msg string) *gus.Response {
|
||||||
|
if !strings.HasSuffix(msg, "\r\n") {
|
||||||
|
msg += "\r\n"
|
||||||
|
}
|
||||||
|
return &gus.Response{Body: bytes.NewBufferString(msg), Status: 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success produces a finger response with a Status of 0.
|
||||||
|
func Success(body io.Reader) *gus.Response {
|
||||||
|
return &gus.Response{Body: body}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package finger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"tildegit.org/tjp/gus"
|
||||||
|
"tildegit.org/tjp/gus/internal"
|
||||||
|
"tildegit.org/tjp/gus/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fingerServer struct {
|
||||||
|
internal.Server
|
||||||
|
handler gus.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs fingerServer) Protocol() string { return "FINGER" }
|
||||||
|
|
||||||
|
// NewServer builds a finger server.
|
||||||
|
func NewServer(
|
||||||
|
ctx context.Context,
|
||||||
|
hostname string,
|
||||||
|
network string,
|
||||||
|
address string,
|
||||||
|
handler gus.Handler,
|
||||||
|
errLog logging.Logger,
|
||||||
|
) (gus.Server, error) {
|
||||||
|
fs := &fingerServer{handler: handler}
|
||||||
|
|
||||||
|
if strings.IndexByte(hostname, ':') < 0 {
|
||||||
|
hostname = net.JoinHostPort(hostname, "79")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
fs.Server, err = internal.NewServer(ctx, hostname, network, address, errLog, fs.handleConn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *fingerServer) handleConn(conn net.Conn) {
|
||||||
|
request, err := ParseRequest(conn)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprint(conn, err.Error()+"\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Server = fs
|
||||||
|
request.RemoteAddr = conn.RemoteAddr()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
_ = fs.LogError("msg", "panic in handler", "err", r)
|
||||||
|
_, _ = fmt.Fprint(conn, "Error handling request.\r\n")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
response := fs.handler(fs.Ctx, request)
|
||||||
|
if response == nil {
|
||||||
|
response = Error("No result found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer response.Close()
|
||||||
|
_, _ = io.Copy(conn, response.Body)
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package finger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"tildegit.org/tjp/gus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListingDenied is returned to reject online user listing requests.
|
||||||
|
var ListingDenied = errors.New("Finger online user list denied.")
|
||||||
|
|
||||||
|
// SystemFinger handles finger requests by invoking the finger(1) command-line utility.
|
||||||
|
func SystemFinger(allowListings bool) gus.Handler {
|
||||||
|
return func(ctx context.Context, request *gus.Request) *gus.Response {
|
||||||
|
fingerPath, err := exec.LookPath("finger")
|
||||||
|
if err != nil {
|
||||||
|
_ = request.Server.LogError(
|
||||||
|
"msg", "handler failure",
|
||||||
|
"ctx", "exec.LookPath(\"finger\")",
|
||||||
|
"err", err,
|
||||||
|
)
|
||||||
|
return Error("Could not resolve request.")
|
||||||
|
}
|
||||||
|
|
||||||
|
path := request.Path[1:]
|
||||||
|
|
||||||
|
if len(path) == 0 && !allowListings {
|
||||||
|
return Error(ListingDenied.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
args := make([]string, 0, 1)
|
||||||
|
if len(path) > 0 {
|
||||||
|
args = append(args, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, fingerPath, args...)
|
||||||
|
outbuf := &bytes.Buffer{}
|
||||||
|
cmd.Stdout = outbuf
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return Error(err.Error())
|
||||||
|
}
|
||||||
|
return Success(outbuf)
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,10 +25,10 @@ func ParseRequest(rdr io.Reader) (*gus.Request, error) {
|
||||||
return &gus.Request{
|
return &gus.Request{
|
||||||
URL: &url.URL{
|
URL: &url.URL{
|
||||||
Scheme: "gopher",
|
Scheme: "gopher",
|
||||||
Path: path.Clean(strings.TrimRight(selector, "\r\n")),
|
Path: path.Clean(strings.TrimSuffix(selector, "\r\n")),
|
||||||
OmitHost: true, //nolint:typecheck
|
OmitHost: true, //nolint:typecheck
|
||||||
// (for some reason typecheck on drone-ci doesn't realize OmitHost is a field in url.URL)
|
// (for some reason typecheck on drone-ci doesn't realize OmitHost is a field in url.URL)
|
||||||
RawQuery: url.QueryEscape(strings.TrimRight(search, "\r\n")),
|
RawQuery: url.QueryEscape(strings.TrimSuffix(search, "\r\n")),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
Reference in New Issue