123 lines
2.5 KiB
Go
123 lines
2.5 KiB
Go
package gemini
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"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")
|
|
}
|