This repository has been archived on 2023-05-01. You can view files and clone it, but cannot push or open issues or pull requests.
gus/gemini/serve.go

139 lines
3.1 KiB
Go

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(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 func(ctx context.Context, request *gus.Request) *gus.Response {
if request.Scheme == "gemini" || (allowTitan && request.Scheme == "titan") {
return inner(ctx, request)
}
return RefuseProxy("Non-gemini protocol requests are not supported.")
}
}
}