summaryrefslogtreecommitdiff
path: root/gemini/serve.go
blob: 2f93153fa83be99790b61a441ebe67a5f66ee185 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package gemini

import (
	"bufio"
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"io"
	"net"
	"strconv"
	"strings"

	"tildegit.org/tjp/gus"
	"tildegit.org/tjp/gus/internal"
	"tildegit.org/tjp/gus/logging"
)

type titanRequestBodyKey struct{}

// TitanRequestBody is the key set in a handler's context for titan requests.
//
// When this key is present in the context (request.URL.Scheme will be "titan"), the
// corresponding value is a *bufio.Reader from which the request body can be read.
var TitanRequestBody = titanRequestBodyKey{}

type server struct {
	internal.Server

	handler gus.Handler
}

func (s server) Protocol() string { return "GEMINI" }

// NewServer builds a gemini server.
func NewServer(
	ctx context.Context,
	hostname string,
	network string,
	address string,
	handler gus.Handler,
	errorLog logging.Logger,
	tlsConfig *tls.Config,
) (gus.Server, error) {
	s := &server{handler: handler}

	if strings.IndexByte(hostname, ':') < 0 {
		hostname = net.JoinHostPort(hostname, "1965")
	}

	internalServer, err := internal.NewServer(ctx, hostname, network, address, errorLog, s.handleConn)
	if err != nil {
		return nil, err
	}
	s.Server = internalServer

	s.Listener = tls.NewListener(s.Listener, tlsConfig)

	return s, nil
}

func (s *server) handleConn(conn net.Conn) {
	buf := bufio.NewReader(conn)

	var response *gus.Response
	request, err := ParseRequest(buf)
	if err != nil {
		response = BadRequest(err.Error())
	} else {
		request.Server = s
		request.RemoteAddr = conn.RemoteAddr()

		if tlsconn, ok := conn.(*tls.Conn); ok {
			state := tlsconn.ConnectionState()
			request.TLSState = &state
		}

		ctx := s.Ctx
		if request.Scheme == "titan" {
			len, err := sizeParam(request.Path)
			if err == nil {
				ctx = context.WithValue(
					ctx,
					TitanRequestBody,
					io.LimitReader(buf, int64(len)),
				)
			}
		}

		defer func() {
			if r := recover(); r != nil {
				err := fmt.Errorf("%s", r)
				_ = s.LogError("msg", "panic in handler", "err", err)
				_, _ = io.Copy(conn, NewResponseReader(Failure(err)))
			}
		}()
		response = s.handler.Handle(ctx, request)
		if response == nil {
			response = NotFound("Resource does not exist.")
		}
	}

	defer response.Close()
	_, _ = io.Copy(conn, NewResponseReader(response))
}

func sizeParam(path string) (int, error) {
	_, rest, found := strings.Cut(path, ";")
	if !found {
		return 0, errors.New("no params in path")
	}

	for _, piece := range strings.Split(rest, ";") {
		key, val, _ := strings.Cut(piece, "=")
		if key == "size" {
			return strconv.Atoi(val)
		}
	}

	return 0, errors.New("no size param found")
}

// GeminiOnly filters requests down to just those on the gemini:// protocol.
//
// Optionally, it will also allow through titan:// requests.
//
// Filtered requests will be turned away with a 53 response "proxy request refused".
func GeminiOnly(allowTitan bool) gus.Middleware {
	return func(inner gus.Handler) gus.Handler {
		return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
			if request.Scheme == "gemini" || (allowTitan && request.Scheme == "titan") {
				return inner.Handle(ctx, request)
			}

			return RefuseProxy("Non-gemini protocol requests are not supported.")
		})
	}
}