gmifs/gemini/gemini.go

409 lines
9.5 KiB
Go
Raw Normal View History

2021-07-07 19:39:16 +03:00
package gemini
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"log"
"net"
"net/url"
2021-07-08 14:31:05 +03:00
"os"
"os/signal"
2021-07-07 19:39:16 +03:00
"path"
"strings"
2021-07-08 14:31:05 +03:00
"syscall"
2021-07-07 19:39:16 +03:00
"time"
"unicode/utf8"
)
const (
StatusInput = 10
StatusSensitiveInput = 11
StatusSuccess = 20
StatusRedirectTemporary = 30
StatusRedirectPermanent = 31
StatusTemporaryFailure = 40
StatusServerUnavailable = 41
StatusCgiError = 42
StatusProxyError = 43
StatusSlowDown = 44
StatusPermanentFailure = 50
StatusNotFound = 51
StatusGone = 52
StatusProxyRequestRefused = 53
StatusBadRequest = 59
StatusClientCertificateRequired = 60
StatusCertificateNotAuthorized = 61
StatusCertificateNotValid = 62
)
const (
Termination = "\r\n"
URLMaxBytes = 1024
IndexFile = "index.gmi"
MimeType = "text/gemini; charset=utf-8"
)
var (
2021-07-08 16:44:18 +03:00
ErrServerClosed = errors.New("gemini: server closed")
ErrHeaderTooLong = errors.New("gemini: header too long")
ErrMissingFile = errors.New("gemini: no such file")
ErrEmptyRequest = errors.New("gemini: empty request")
ErrEmptyRequestURL = errors.New("gemini: empty request URL")
ErrInvalidPath = errors.New("gemini: path error")
ErrInvalidHost = errors.New("gemini: empty host")
2021-07-10 07:28:24 +03:00
ErrInvalidUtf8 = errors.New("gemini: empty request URL")
ErrUnknownProtocol = fmt.Errorf("gemini: unknown protocol scheme")
2021-07-07 19:39:16 +03:00
)
type Request struct {
2021-07-08 04:34:32 +03:00
ctx context.Context
URL *url.URL
RemoteAddr string
// RequestURI is the unmodified request-target of the Request-Line as sent by the client
// to a server. Usually the URL field should be used instead.
RequestURI string
2021-07-07 19:39:16 +03:00
}
type ResponseWriter interface {
WriteHeader(code int, message string) (int, error)
Write(body []byte) (int, error)
}
2021-07-07 19:39:16 +03:00
type Handler interface {
ServeGemini(ResponseWriter, *Request)
2021-07-07 19:39:16 +03:00
}
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as Gemini handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)
2021-07-07 19:39:16 +03:00
// ServeGemini calls f(w, r).
func (f HandlerFunc) ServeGemini(w ResponseWriter, r *Request) {
2021-07-07 19:39:16 +03:00
f(w, r)
}
type Server struct {
// Addr is the address the server is listening on.
Addr string
// Hostname or common name of the server. This is used for absolute redirects.
Hostname string
2021-07-08 09:31:22 +03:00
// Logger enables logging of the gemini server for debugging purposes.
Logger *log.Logger
2021-07-08 16:16:17 +03:00
TLSConfig *tls.Config
TLSConfigLoader func() (*tls.Config, error)
2021-07-07 19:39:16 +03:00
Handler Handler // handler to invoke
ReadTimeout time.Duration
MaxOpenConns int
// internal
2021-07-10 07:28:24 +03:00
listener net.Listener
shutdown bool
closed chan struct{}
sighupListener chan struct{}
2021-07-07 19:39:16 +03:00
}
2021-07-08 09:31:22 +03:00
func (s *Server) log(v string) {
if s.Logger == nil {
return
}
2021-07-08 16:44:18 +03:00
2021-07-08 15:04:33 +03:00
s.Logger.Println("gmifs: " + v)
2021-07-08 09:31:22 +03:00
}
func (s *Server) logf(format string, v ...interface{}) {
if s.Logger == nil {
return
}
2021-07-08 16:44:18 +03:00
2021-07-08 09:31:22 +03:00
s.log(fmt.Sprintf(format, v...))
}
2021-07-08 16:16:17 +03:00
func (s *Server) loadTLS() (err error) {
s.TLSConfig, err = s.TLSConfigLoader()
return err
}
2021-07-10 07:28:24 +03:00
func (s *Server) reloadTLSConfigOnSighup() {
2021-07-08 14:31:05 +03:00
hup := make(chan os.Signal, 1)
signal.Notify(hup, syscall.SIGHUP)
2021-07-08 16:44:18 +03:00
2021-07-10 07:28:24 +03:00
for {
select {
case <-hup:
2021-07-08 16:44:18 +03:00
2021-07-08 16:16:17 +03:00
s.log("reloading certificate")
2021-07-08 14:31:05 +03:00
if s.listener != nil {
2021-07-08 16:16:17 +03:00
err := s.loadTLS()
if err != nil {
fmt.Fprintf(os.Stderr, "critical: failed to load tls certs: %v", err)
os.Exit(1)
}
2021-07-08 14:31:05 +03:00
s.listener.Close()
}
2021-07-10 07:28:24 +03:00
case <-s.closed:
close(s.sighupListener)
return
2021-07-08 14:31:05 +03:00
}
2021-07-10 07:28:24 +03:00
}
}
func (s *Server) ListenAndServe() error {
err := s.loadTLS()
if err != nil {
return err
}
s.sighupListener = make(chan struct{})
go s.reloadTLSConfigOnSighup()
2021-07-07 19:39:16 +03:00
// outer for loop, if listener closes we will restart it. This may be useful if we switch out
// TLSConfig.
for {
2021-07-08 14:31:05 +03:00
s.closed = make(chan struct{})
2021-07-08 16:44:18 +03:00
2021-07-08 14:31:05 +03:00
var err error
2021-07-10 07:28:24 +03:00
2021-07-08 14:31:05 +03:00
s.listener, err = tls.Listen("tcp", s.Addr, s.TLSConfig)
2021-07-07 19:39:16 +03:00
if err != nil {
2021-07-08 14:31:05 +03:00
return fmt.Errorf("gemini server listen: %w", err)
2021-07-07 19:39:16 +03:00
}
2021-07-08 14:31:05 +03:00
queue := make(chan net.Conn, s.MaxOpenConns)
go s.handleConnectionQueue(queue)
s.logf("Accepting new connections on %v", s.listener.Addr())
for {
conn, err := s.listener.Accept()
if err != nil {
s.logf("server accept error: %v", err)
2021-07-08 16:44:18 +03:00
2021-07-08 14:31:05 +03:00
break
}
queue <- conn
// un-stuck call after shutdown will trigger a drop here
if s.shutdown {
break
}
}
2021-07-08 16:44:18 +03:00
2021-07-08 14:31:05 +03:00
// closed confirms the accept call stopped
close(s.closed)
if s.shutdown {
break
}
2021-07-07 19:39:16 +03:00
}
2021-07-08 16:44:18 +03:00
s.log("closing listener gracefully")
return s.listener.Close()
2021-07-07 19:39:16 +03:00
}
func (s *Server) handleConnectionQueue(queue chan net.Conn) {
// semaphore for connection limiter
type semaphore chan struct{}
sem := make(semaphore, s.MaxOpenConns)
for {
// for each connection we receive
conn := <-queue
sem <- struct{}{} // acquire
go s.handleConnection(conn, sem)
}
}
func (s *Server) handleConnection(conn net.Conn, sem chan struct{}) {
defer func() {
conn.Close()
<-sem // release
}()
2021-07-10 07:28:24 +03:00
2021-07-07 19:39:16 +03:00
reqChan := make(chan request)
w := &writer{conn}
2021-07-10 07:28:24 +03:00
2021-07-07 19:39:16 +03:00
// push job for which we allocated a sem slot and wait
go requestChannel(conn, reqChan)
select {
case header := <-reqChan:
if header.err != nil {
s.handleRequestError(conn, w, header)
2021-07-08 16:44:18 +03:00
2021-07-07 19:39:16 +03:00
return
}
2021-07-10 07:28:24 +03:00
2021-07-07 19:39:16 +03:00
ctx := context.Background()
2021-07-08 04:34:32 +03:00
r := &Request{
ctx: ctx,
URL: header.URL,
RequestURI: header.rawuri,
RemoteAddr: conn.RemoteAddr().String(),
}
2021-07-10 07:28:24 +03:00
s.Handler.ServeGemini(w, r)
2021-07-07 19:39:16 +03:00
case <-time.After(s.ReadTimeout):
2021-07-08 09:31:22 +03:00
s.logf("server read timeout, request queue length %v/%v", len(sem), s.MaxOpenConns)
w.WriteHeader(StatusServerUnavailable, "")
2021-07-07 19:39:16 +03:00
}
}
func (s *Server) handleRequestError(conn net.Conn, w ResponseWriter, req request) {
if errors.Is(req.err, ErrEmptyRequest) {
// in debug mode we log these too
s.logf("empty request ignored - %v", conn.RemoteAddr().String())
return
}
2021-07-07 19:39:16 +03:00
var gmierr *GmiError
if errors.As(req.err, &gmierr) {
// notify if error or redirect
if gmierr.Code == StatusRedirectPermanent || gmierr.Code == StatusRedirectTemporary {
s.logf("redirect '%s' -> '%s' %d - %s",
strings.TrimSpace(req.URL.Path), req.err, gmierr.Code, conn.RemoteAddr().String())
} else {
s.logf("read request error: '%s' %v %d - %s",
strings.TrimSpace(req.rawuri), req.err, gmierr.Code, conn.RemoteAddr().String())
}
w.WriteHeader(gmierr.Code, gmierr.Error())
2021-07-08 16:44:18 +03:00
2021-07-07 19:39:16 +03:00
return
}
// this path doesn't exist currently.
s.logf("unexpected error: '%s' %v - %s",
strings.TrimSpace(req.rawuri), req.err, conn.RemoteAddr().String())
w.WriteHeader(StatusTemporaryFailure, "internal")
2021-07-07 19:39:16 +03:00
}
// conn handler
type request struct {
2021-07-08 04:34:32 +03:00
rawuri string
URL *url.URL
err error
2021-07-07 19:39:16 +03:00
}
func requestChannel(c net.Conn, rsp chan request) {
req := &request{}
2021-07-10 07:28:24 +03:00
2021-07-08 04:34:32 +03:00
r, err := readHeader(c)
if r != nil {
req = r
}
2021-07-10 07:28:24 +03:00
req.err = err
rsp <- *req
2021-07-07 19:39:16 +03:00
}
2021-07-08 04:34:32 +03:00
func readHeader(c net.Conn) (*request, error) {
2021-07-07 19:39:16 +03:00
req, err := bufio.NewReader(c).ReadString('\r')
if err != nil {
2021-07-10 07:28:24 +03:00
// not sure this is the right response
return nil, Error(StatusTemporaryFailure, ErrEmptyRequest)
2021-07-07 19:39:16 +03:00
}
r := &request{}
2021-07-08 04:34:32 +03:00
r.rawuri = req
2021-07-07 19:39:16 +03:00
requestURL := strings.TrimSpace(req)
if requestURL == "" {
2021-07-08 16:44:18 +03:00
return r, Error(StatusBadRequest, ErrEmptyRequestURL)
2021-07-07 19:39:16 +03:00
} else if len(requestURL) > URLMaxBytes {
2021-07-08 04:34:32 +03:00
return r, Error(StatusBadRequest, ErrHeaderTooLong)
2021-07-08 16:44:18 +03:00
} else if !utf8.ValidString(requestURL) {
return r, Error(StatusBadRequest, ErrInvalidUtf8)
2021-07-07 19:39:16 +03:00
}
parsedURL, err := url.Parse(requestURL)
if err != nil {
2021-07-08 04:34:32 +03:00
return r, Error(StatusBadRequest, err)
2021-07-07 19:39:16 +03:00
}
2021-07-08 04:34:32 +03:00
r.URL = parsedURL
2021-07-10 07:28:24 +03:00
return validateRequest(r)
}
2021-07-08 04:34:32 +03:00
2021-07-10 07:28:24 +03:00
func validateRequest(r *request) (*request, error) {
if r.URL.Scheme != "" && r.URL.Scheme != "gemini" {
2021-07-08 16:44:18 +03:00
return r, Error(StatusProxyRequestRefused, ErrUnknownProtocol)
2021-07-10 07:28:24 +03:00
} else if r.URL.Host == "" {
2021-07-08 16:44:18 +03:00
return r, Error(StatusBadRequest, ErrInvalidHost)
2021-07-07 19:39:16 +03:00
}
2021-07-10 07:28:24 +03:00
if r.URL.Path == "" {
2021-07-08 16:44:18 +03:00
// This error is a redirect path.
2021-07-10 07:28:24 +03:00
return r, Error(StatusRedirectPermanent, errors.New("./"+r.URL.Path))
} else if cleaned := path.Clean(r.URL.Path); cleaned != r.URL.Path {
// check valid alternative if unclean for directories
2021-07-10 07:28:24 +03:00
if cleaned != strings.TrimRight(r.URL.Path, "/") {
return r, Error(StatusBadRequest, ErrInvalidPath)
}
2021-07-07 19:39:16 +03:00
}
2021-07-08 04:34:32 +03:00
return r, nil
2021-07-07 19:39:16 +03:00
}
2021-07-08 15:04:33 +03:00
// Shutdown uses the self-pipe trick to gracefully allow the accept handler to exit and the listener
// to close within the given context deadline. If unsuccessful the listener is forcefully
// terminated.
2021-07-07 19:39:16 +03:00
func (s *Server) Shutdown(ctx context.Context) error {
s.log("shutdown request received")
t := time.Now()
go func() {
s.shutdown = true
// un-stuck call to self
2021-07-08 16:44:18 +03:00
conn, err := tls.Dial("tcp", s.Addr, &tls.Config{InsecureSkipVerify: true})
if err != nil {
2021-07-08 16:44:18 +03:00
s.logf("un-stuck call failed (ok): %v", err)
return
}
defer conn.Close()
}()
2021-07-07 19:39:16 +03:00
select {
case <-s.closed:
s.log("all clients exited")
case <-ctx.Done():
s.logf("shutdown: context deadline exceeded after %v, terminating listener", time.Since(t))
if err := s.listener.Close(); err != nil {
s.logf("error while closing listener %v", err)
}
}
2021-07-10 07:28:24 +03:00
// confirm sighup listener for cert reloading exited
<-s.sighupListener
2021-07-08 16:44:18 +03:00
2021-07-07 19:39:16 +03:00
return nil
}
type writer struct {
w io.Writer
}
func (w *writer) WriteHeader(code int, message string) (int, error) {
2021-07-07 19:39:16 +03:00
// <STATUS><SPACE><META><CR><LF>
if len(message) == 0 {
return w.Write([]byte(fmt.Sprintf("%d%s", code, Termination)))
2021-07-07 19:39:16 +03:00
}
2021-07-08 16:44:18 +03:00
return w.Write([]byte(fmt.Sprintf("%d %s%s", code, message, Termination)))
2021-07-07 19:39:16 +03:00
}
func (w *writer) Write(body []byte) (int, error) {
2021-07-07 19:39:16 +03:00
reader := bytes.NewReader(body)
n, err := io.Copy(w.w, reader)
return int(n), err
2021-07-07 19:39:16 +03:00
}