427 lines
13 KiB
Go
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()
|
|
}
|