Files
old-ibcetvorke/vendor/github.com/a-h/gemini/server.go
2023-07-30 19:21:16 +02:00

427 lines
13 KiB
Go

package gemini
import (
"context"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/url"
"reflect"
"strings"
"time"
"github.com/a-h/gemini/log"
)
// Handler of Gemini content.
type Handler interface {
ServeGemini(w ResponseWriter, r *Request)
}
// HandlerFunc handles a Gemini request and returns a response.
type HandlerFunc func(ResponseWriter, *Request)
// ServeGemini implements the Handler interface.
func (f HandlerFunc) ServeGemini(w ResponseWriter, r *Request) {
f(w, r)
}
// DefaultMIMEType for Gemini responses.
const DefaultMIMEType = "text/gemini; charset=utf-8"
// Request from the client. A Gemini request contains only the
// URL, the Certificates field is populated by the TLS certificates
// presented by the client.
type Request struct {
Context context.Context
URL *url.URL
Certificate Certificate
}
// Certificate information provided to the server by the client.
type Certificate struct {
// ID is the base64-encoded SHA256 hash of the key.
ID string
// Key is the user public key in PKIX, ASN.1 DER form.
Key string
// Error is an error message related to any failures in handling the client certificate.
Error string
}
// ResponseWriter used by handlers to send a response to the client.
type ResponseWriter interface {
io.Writer
SetHeader(code Code, meta string) error
}
// Code returned as part of the Gemini response (see https://gemini.circumlunar.space/docs/specification.html).
type Code string
const (
CodeInput Code = "10"
CodeInputSensitive = "11"
CodeSuccess = "20"
CodeRedirect = "30"
CodeRedirectTemporary = CodeRedirect
CodeRedirectPermanent = "31"
CodeTemporaryFailure = "40"
CodeServerUnavailable = "41"
CodeCGIError = "42"
CodeProxyError = "43"
CodeSlowDown = "44"
CodePermanentFailure = "50"
CodeNotFound = "51"
CodeGone = "52"
CodeProxyRequestRefused = "53"
CodeBadRequest = "59"
CodeClientCertificateRequired = "60"
CodeClientCertificateNotAuthorised = "61"
CodeClientCertificateNotValid = "62"
)
// IsErrorCode returns true if the code is invalid, or starts with 4, 5 or 6.
func IsErrorCode(code Code) bool {
if !isValidCode(code) || len(code) != 2 {
return false
}
return code[0] == '4' || code[0] == '5' || code[0] == '6'
}
// NewServer creates a new Gemini server.
// addr is in the form "<optional_ip>:<port>", e.g. ":1965". If left empty, it will default to ":1965".
// domainToHandler is a map of the server name (domain) to the certificate key pair and the Gemini handler used to serve content.
func NewServer(ctx context.Context, addr string, domainToHandler map[string]*DomainHandler) *Server {
for k, v := range domainToHandler {
domainToHandler[strings.ToLower(k)] = v
}
return &Server{
Context: ctx,
Addr: addr,
DomainToHandler: domainToHandler,
ReadTimeout: time.Second * 5,
WriteTimeout: time.Second * 10,
HandlerTimeout: time.Second * 30,
}
}
// Server hosts Gemini content.
type Server struct {
Context context.Context
Addr string
Insecure bool
DomainToHandler map[string]*DomainHandler
ReadTimeout time.Duration
WriteTimeout time.Duration
HandlerTimeout time.Duration
}
// Set the server listening on the specified port.
func (srv *Server) ListenAndServe() error {
// Don't start if the server is already closing down.
if srv.Context.Err() != nil {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":1965"
}
log.Info("gemini: starting", log.String("addr", addr))
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
defer ln.Close()
if srv.Insecure {
err = srv.serveInsecure(ln)
if err != nil {
log.Error("gemini: serveInsecure failure", err, log.String("addr", addr))
}
} else {
err = srv.serveTLS(ln)
if err != nil {
log.Error("gemini: serveTLS failure", err, log.String("addr", addr))
}
}
log.Info("gemini: stopped")
return err
}
// ErrServerClosed is returned when a server is attempted to start up when it's already shutting down.
var ErrServerClosed = errors.New("gemini: server closed")
func (srv *Server) serveInsecure(l net.Listener) (err error) {
if len(srv.DomainToHandler) > 1 {
return fmt.Errorf("gemini: cannot start insecure mode for more than one domain")
}
var handler *DomainHandler
for _, handler = range srv.DomainToHandler {
break
}
for {
if err = srv.Context.Err(); err != nil {
log.Error("gemini: context caused shutdown", err)
return err
}
rw, err := l.Accept()
if err != nil {
log.Error("gemini: insecure listener error", err)
continue
}
go func() {
defer rw.Close()
srv.handle(handler, Certificate{}, rw)
}()
}
}
func (srv *Server) serveTLS(l net.Listener) (err error) {
config := &tls.Config{
MinVersion: tls.VersionTLS12,
ClientAuth: tls.RequestClientCert,
InsecureSkipVerify: true,
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
dh, ok := srv.DomainToHandler[strings.ToLower(hello.ServerName)]
if !ok {
return nil, fmt.Errorf("gemini: certificate not found for %q", hello.ServerName)
}
return &dh.KeyPair, nil
},
}
if err != nil {
return err
}
tlsListener := tls.NewListener(l, config)
for {
if err = srv.Context.Err(); err != nil {
log.Error("gemini: context caused shutdown", err)
return err
}
conn, err := tlsListener.Accept()
if err != nil {
log.Error("gemini: tls listener error", err)
continue
}
tlsConn, ok := conn.(*tls.Conn)
if !ok {
panic("gemini: tls.Listener did not return TLS connection")
}
go srv.handleTLS(tlsConn)
}
}
func (srv *Server) handleTLS(conn *tls.Conn) {
defer conn.Close()
if err := conn.Handshake(); err != nil {
log.Info("gemini: failed TLS handshake", log.String("remote", conn.RemoteAddr().String()), log.String("reason", err.Error()))
return
}
var certificate Certificate
peerCerts := conn.ConnectionState().PeerCertificates
if len(peerCerts) > 0 {
now := time.Now()
cert := peerCerts[0]
certificate.ID = base64.StdEncoding.EncodeToString(sha256.New().Sum(cert.Raw))
certificate.Key = string(cert.Raw)
if now.Before(cert.NotBefore) {
certificate.Error = "certificate not yet valid"
}
if now.After(cert.NotAfter) {
certificate.Error = "certificate expired"
}
}
serverName := conn.ConnectionState().ServerName
dh, ok := srv.DomainToHandler[strings.ToLower(serverName)]
if !ok {
log.Warn("gemini: failed to find domain handler", log.String("serverName", serverName))
}
srv.handle(dh, certificate, conn)
}
// while this function could be inlined, exposing it makes it easier to test in isolation.
func (srv *Server) handle(dh *DomainHandler, certificate Certificate, conn net.Conn) {
start := time.Now()
conn.SetReadDeadline(time.Now().Add(srv.ReadTimeout))
r, ok, err := srv.parseRequest(conn)
if err != nil {
log.Info("gemini: failed to parse request", log.String("reason", err.Error()))
return
}
if !ok {
return
}
r.Certificate = certificate
ctx, cancel := context.WithTimeout(srv.Context, srv.HandlerTimeout)
defer cancel()
r.Context = ctx
conn.SetWriteDeadline(time.Now().Add(srv.WriteTimeout))
w := NewWriter(conn)
defer func() {
if p := recover(); p != nil {
log.Error("gemini: server error", nil, log.String("url", r.URL.String()), log.Interface("recover", p))
w.SetHeader(CodeCGIError, "internal error")
}
}()
if certificate.Error != "" {
w.SetHeader(CodeClientCertificateNotValid, certificate.Error)
return
}
dh.Handler.ServeGemini(w, r)
if w.Code == "" {
log.Error("gemini: handler resulted in empty response", nil, log.String("url", r.URL.String()), log.String("handlerType", reflect.TypeOf(dh.Handler).PkgPath()))
w.SetHeader(CodeCGIError, "empty response")
}
duration := time.Now().Sub(start)
log.Info("gemini: response",
log.String("url", r.URL.String()),
log.String("path", r.URL.Path),
log.String("code", w.Code),
log.String("handlerType", reflect.TypeOf(dh.Handler).PkgPath()),
log.Int64("ms", duration.Milliseconds()),
log.Int64("us", int64(duration.Microseconds())),
log.Int64("lenBody", w.WrittenBody),
log.Int("lenHeader", w.WrittenHeader),
log.Int64("len", int64(w.WrittenHeader)+w.WrittenBody),
)
}
func (srv *Server) parseRequest(rw io.ReadWriter) (r *Request, ok bool, err error) {
request, ok, err := readUntilCrLf(rw, 1026)
if err != nil && err != io.EOF {
writeHeaderToWriter(CodeBadRequest, fmt.Sprintf("error reading request: %v", err), rw)
return
}
if !ok {
log.Info("gemini: request too long or malformed", log.String("request", string(request)))
writeHeaderToWriter(CodeBadRequest, "request too long or malformed", rw)
return
}
ok = false
url, err := url.Parse(strings.TrimSpace(string(request)))
if err != nil {
log.Info("gemini: malformed request", log.String("request", string(request)))
writeHeaderToWriter(CodeBadRequest, "request malformed", rw)
return
}
log.Info("gemini: received request", log.String("request", url.String()))
r = &Request{
URL: url,
}
return r, true, err
}
// Writer passed to Gemini handlers.
type Writer struct {
Code string
Writer io.Writer
WrittenHeader int
WrittenBody int64
}
// NewWriter creates a new Gemini writer.
func NewWriter(w io.Writer) *Writer {
return &Writer{
Writer: w,
}
}
var ErrCannotWriteBodyWithoutSuccessCode = errors.New("gemini: cannot write body without success code")
func (gw *Writer) Write(p []byte) (n int, err error) {
if gw.Code == "" {
// Section 3.3
gw.SetHeader(CodeSuccess, DefaultMIMEType)
gw.Code = CodeSuccess
}
if !isSuccessCode(Code(gw.Code)) {
err = ErrCannotWriteBodyWithoutSuccessCode
return
}
n, err = gw.Writer.Write(p)
gw.WrittenBody += int64(n)
return
}
func isSuccessCode(code Code) bool {
return len(code) == 2 && code[0] == '2'
}
// ErrHeaderAlreadyWritten is returned by SetHeader when the Gemini header has already been written to the response.
var ErrHeaderAlreadyWritten = errors.New("gemini: header already written")
func (gw *Writer) SetHeader(code Code, meta string) (err error) {
if gw.Code != "" {
return ErrHeaderAlreadyWritten
}
gw.Code = string(code)
var n int
n, err = writeHeaderToWriter(code, meta, gw.Writer)
gw.WrittenHeader += n
return
}
func writeHeaderToWriter(code Code, meta string, w io.Writer) (n int, err error) {
// <STATUS><SPACE><META><CR><LF>
// Set default meta if required.
if meta == "" && isSuccessCode(code) {
meta = DefaultMIMEType
}
if len(meta) > 1024 {
meta = meta[:1024]
}
return w.Write([]byte(string(code) + " " + meta + "\r\n"))
}
// DomainHandler handles incoming requests for the ServerName using the provided KeyPair certificate
// and Handler to process the request.
type DomainHandler struct {
ServerName string
KeyPair tls.Certificate
Handler Handler
}
// NewDomainHandler creates a new handler to listen for Gemini requests using TLS.
// The cert can be generated by the github.com/a-h/gemini/cert.Generate package,
// or can generated using openssl:
// keyFile:
// openssl ecparam -genkey -name secp384r1 -out server.key
// certFile:
// openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
func NewDomainHandler(serverName string, cert tls.Certificate, handler Handler) *DomainHandler {
return &DomainHandler{
ServerName: serverName,
KeyPair: cert,
Handler: handler,
}
}
// NewDomainHandlerFromFiles creates a new handler to listen for Gemini requests using TLS.
// certFile / keyFile are links to the X509 keypair. This can be generated using openssl:
// keyFile:
// openssl ecparam -genkey -name secp384r1 -out server.key
// certFile:
// openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
func NewDomainHandlerFromFiles(serverName, certFile, keyFile string, handler Handler) (*DomainHandler, error) {
keyPair, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
return NewDomainHandler(serverName, keyPair, handler), nil
}
// ListenAndServe starts up a new server to handle multiple domains with a specific certFile, keyFile and handler.
func ListenAndServe(ctx context.Context, addr string, domains ...*DomainHandler) (err error) {
if len(domains) == 0 {
return fmt.Errorf("gemini: no default handler provided")
}
domainToHandler := make(map[string]*DomainHandler, len(domains))
for i := 0; i < len(domains); i++ {
domainToHandler[domains[i].ServerName] = domains[i]
}
server := NewServer(ctx, addr, domainToHandler)
return server.ListenAndServe()
}