summaryrefslogtreecommitdiff
path: root/gemini/serve.go
blob: a439ce0778436cfdbfb09e69becca84f4614a7db (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
package gemini

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

	sr "tildegit.org/tjp/sliderule"
	"tildegit.org/tjp/sliderule/internal"
	"tildegit.org/tjp/sliderule/logging"
)

type titanRequestBodyKey struct{}

type server struct {
	internal.Server

	handler sr.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 sr.Handler,
	errorLog logging.Logger,
	tlsConfig *tls.Config,
) (sr.Server, error) {
	s := &server{handler: handler}

	hostname = internal.JoinDefaultPort(hostname, "1965")
	address = internal.JoinDefaultPort(address, "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 *sr.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 {
				request.Meta = 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) sr.Middleware {
	return func(inner sr.Handler) sr.Handler {
		return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
			if request.Scheme == "gemini" || (allowTitan && request.Scheme == "titan") {
				return inner.Handle(ctx, request)
			}

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