update docs and improve readability

This commit is contained in:
dre 2021-07-10 12:28:24 +08:00
parent 8e66aa4314
commit f590db1c37
5 changed files with 63 additions and 35 deletions

View file

@ -11,7 +11,7 @@ server? Because it's educational and that's the spirit of the protocol.
- directory listing support through the auto index flag - directory listing support through the auto index flag
- reloads ssl certs and reopens log files on SIGHUP, e.g. after Let's Encrypt renewal - reloads ssl certs and reopens log files on SIGHUP, e.g. after Let's Encrypt renewal
- response writer interceptor and middleware support - response writer interceptor and middleware support
- simple middleware for lru document cache - simple middleware for fifo document cache
- concurrent request limiter - concurrent request limiter
- KISS, single file gemini implementation, handler func in main - KISS, single file gemini implementation, handler func in main
- modern tls ciphers (from [Mozilla's TLS ciphers recommendations](https://statics.tls.security.mozilla.org/server-side-tls-conf.json)) - modern tls ciphers (from [Mozilla's TLS ciphers recommendations](https://statics.tls.security.mozilla.org/server-side-tls-conf.json))
@ -26,7 +26,7 @@ Currently only supported through the go toolchain, either check out the repot an
go install github.com/n0x1m/gmifs go install github.com/n0x1m/gmifs
``` ```
### Dev & Tests ### Development
Test it locally by serving e.g. a `./public` directory on localhost with directory listing turned on Test it locally by serving e.g. a `./public` directory on localhost with directory listing turned on
@ -68,15 +68,15 @@ If debug logs are enabled, the certificate rotation will be confirmed.
### Supported flags ### Supported flags
``` ```
Usage of ./gmifs: sage of ./gmifs:
-addr string -addr string
address to listen on, e.g. 127.0.0.1:1965 (default ":1965") address to listen on, e.g. 127.0.0.1:1965 (default ":1965")
-autocertvalidity int -autocertvalidity int
valid days when using a gmifs auto provisioned self-signed certificate (default 1) valid days when using a gmifs provisioned certificate (default 1)
-autoindex -autoindex
enables auto indexing, directory listings enables auto indexing, directory listings
-cache int -cache int
simple lru document cache for n items. Disabled when zero. simple fifo document cache for n items. Disabled when zero.
-cert string -cert string
TLS chain of one or more certificates TLS chain of one or more certificates
-debug -debug

View file

@ -56,8 +56,8 @@ var (
ErrEmptyRequestURL = errors.New("gemini: empty request URL") ErrEmptyRequestURL = errors.New("gemini: empty request URL")
ErrInvalidPath = errors.New("gemini: path error") ErrInvalidPath = errors.New("gemini: path error")
ErrInvalidHost = errors.New("gemini: empty host") ErrInvalidHost = errors.New("gemini: empty host")
ErrInvalidUtf8 = errors.New("empty request URL") ErrInvalidUtf8 = errors.New("gemini: empty request URL")
ErrUnknownProtocol = fmt.Errorf("unknown protocol scheme") ErrUnknownProtocol = fmt.Errorf("gemini: unknown protocol scheme")
) )
type Request struct { type Request struct {
@ -108,9 +108,10 @@ type Server struct {
MaxOpenConns int MaxOpenConns int
// internal // internal
listener net.Listener listener net.Listener
shutdown bool shutdown bool
closed chan struct{} closed chan struct{}
sighupListener chan struct{}
} }
func (s *Server) log(v string) { func (s *Server) log(v string) {
@ -134,18 +135,13 @@ func (s *Server) loadTLS() (err error) {
return err return err
} }
func (s *Server) ListenAndServe() error { func (s *Server) reloadTLSConfigOnSighup() {
err := s.loadTLS()
if err != nil {
return err
}
hup := make(chan os.Signal, 1) hup := make(chan os.Signal, 1)
signal.Notify(hup, syscall.SIGHUP) signal.Notify(hup, syscall.SIGHUP)
go func() { for {
for { select {
<-hup case <-hup:
s.log("reloading certificate") s.log("reloading certificate")
if s.listener != nil { if s.listener != nil {
@ -157,8 +153,21 @@ func (s *Server) ListenAndServe() error {
s.listener.Close() s.listener.Close()
} }
case <-s.closed:
close(s.sighupListener)
return
} }
}() }
}
func (s *Server) ListenAndServe() error {
err := s.loadTLS()
if err != nil {
return err
}
s.sighupListener = make(chan struct{})
go s.reloadTLSConfigOnSighup()
// outer for loop, if listener closes we will restart it. This may be useful if we switch out // outer for loop, if listener closes we will restart it. This may be useful if we switch out
// TLSConfig. // TLSConfig.
@ -166,6 +175,7 @@ func (s *Server) ListenAndServe() error {
s.closed = make(chan struct{}) s.closed = make(chan struct{})
var err error var err error
s.listener, err = tls.Listen("tcp", s.Addr, s.TLSConfig) s.listener, err = tls.Listen("tcp", s.Addr, s.TLSConfig)
if err != nil { if err != nil {
return fmt.Errorf("gemini server listen: %w", err) return fmt.Errorf("gemini server listen: %w", err)
@ -218,8 +228,10 @@ func (s *Server) handleConnection(conn net.Conn, sem chan struct{}) {
conn.Close() conn.Close()
<-sem // release <-sem // release
}() }()
reqChan := make(chan request) reqChan := make(chan request)
w := &writer{conn} w := &writer{conn}
// push job for which we allocated a sem slot and wait // push job for which we allocated a sem slot and wait
go requestChannel(conn, reqChan) go requestChannel(conn, reqChan)
select { select {
@ -229,6 +241,7 @@ func (s *Server) handleConnection(conn net.Conn, sem chan struct{}) {
return return
} }
ctx := context.Background() ctx := context.Background()
r := &Request{ r := &Request{
ctx: ctx, ctx: ctx,
@ -236,6 +249,7 @@ func (s *Server) handleConnection(conn net.Conn, sem chan struct{}) {
RequestURI: header.rawuri, RequestURI: header.rawuri,
RemoteAddr: conn.RemoteAddr().String(), RemoteAddr: conn.RemoteAddr().String(),
} }
s.Handler.ServeGemini(w, r) s.Handler.ServeGemini(w, r)
case <-time.After(s.ReadTimeout): case <-time.After(s.ReadTimeout):
@ -283,10 +297,12 @@ type request struct {
func requestChannel(c net.Conn, rsp chan request) { func requestChannel(c net.Conn, rsp chan request) {
req := &request{} req := &request{}
r, err := readHeader(c) r, err := readHeader(c)
if r != nil { if r != nil {
req = r req = r
} }
req.err = err req.err = err
rsp <- *req rsp <- *req
} }
@ -294,6 +310,7 @@ func requestChannel(c net.Conn, rsp chan request) {
func readHeader(c net.Conn) (*request, error) { func readHeader(c net.Conn) (*request, error) {
req, err := bufio.NewReader(c).ReadString('\r') req, err := bufio.NewReader(c).ReadString('\r')
if err != nil { if err != nil {
// not sure this is the right response
return nil, Error(StatusTemporaryFailure, ErrEmptyRequest) return nil, Error(StatusTemporaryFailure, ErrEmptyRequest)
} }
@ -315,19 +332,22 @@ func readHeader(c net.Conn) (*request, error) {
} }
r.URL = parsedURL r.URL = parsedURL
return validateRequest(r)
}
if parsedURL.Scheme != "" && parsedURL.Scheme != "gemini" { func validateRequest(r *request) (*request, error) {
if r.URL.Scheme != "" && r.URL.Scheme != "gemini" {
return r, Error(StatusProxyRequestRefused, ErrUnknownProtocol) return r, Error(StatusProxyRequestRefused, ErrUnknownProtocol)
} else if parsedURL.Host == "" { } else if r.URL.Host == "" {
return r, Error(StatusBadRequest, ErrInvalidHost) return r, Error(StatusBadRequest, ErrInvalidHost)
} }
if parsedURL.Path == "" { if r.URL.Path == "" {
// This error is a redirect path. // This error is a redirect path.
return r, Error(StatusRedirectPermanent, errors.New("./"+parsedURL.Path)) return r, Error(StatusRedirectPermanent, errors.New("./"+r.URL.Path))
} else if cleaned := path.Clean(parsedURL.Path); cleaned != parsedURL.Path { } else if cleaned := path.Clean(r.URL.Path); cleaned != r.URL.Path {
// check valid alternative if unclean for directories // check valid alternative if unclean for directories
if cleaned != strings.TrimRight(parsedURL.Path, "/") { if cleaned != strings.TrimRight(r.URL.Path, "/") {
return r, Error(StatusBadRequest, ErrInvalidPath) return r, Error(StatusBadRequest, ErrInvalidPath)
} }
} }
@ -362,6 +382,8 @@ func (s *Server) Shutdown(ctx context.Context) error {
s.logf("error while closing listener %v", err) s.logf("error while closing listener %v", err)
} }
} }
// confirm sighup listener for cert reloading exited
<-s.sighupListener
return nil return nil
} }
@ -381,7 +403,6 @@ func (w *writer) WriteHeader(code int, message string) (int, error) {
func (w *writer) Write(body []byte) (int, error) { func (w *writer) Write(body []byte) (int, error) {
reader := bytes.NewReader(body) reader := bytes.NewReader(body)
n, err := io.Copy(w.w, reader) n, err := io.Copy(w.w, reader)
return int(n), err return int(n), err
} }

View file

@ -12,7 +12,7 @@ import (
// Note that the body being written two times and the complete caching of the body in the memory. // Note that the body being written two times and the complete caching of the body in the memory.
type Interceptor struct { type Interceptor struct {
// ResponseWriter is the underlying response writer that is wrapped by Interceptor // ResponseWriter is the underlying response writer that is wrapped by Interceptor
w ResponseWriter responseWriter ResponseWriter
// Interceptor is the underlying io.Writer that buffers the response body // Interceptor is the underlying io.Writer that buffers the response body
Body bytes.Buffer Body bytes.Buffer
@ -30,9 +30,10 @@ type Interceptor struct {
} }
// NewInterceptor creates a new Interceptor by wrapping the given response writer. // NewInterceptor creates a new Interceptor by wrapping the given response writer.
func NewInterceptor(w ResponseWriter) (m *Interceptor) { func NewInterceptor(responseWriter ResponseWriter) (m *Interceptor) {
m = &Interceptor{} m = &Interceptor{}
m.w = w m.responseWriter = responseWriter
return return
} }
@ -66,10 +67,10 @@ func (m *Interceptor) Flush() {
// FlushBody flushes to the underlying responsewriter. // FlushBody flushes to the underlying responsewriter.
func (m *Interceptor) FlushBody() { func (m *Interceptor) FlushBody() {
m.w.Write(m.Body.Bytes()) m.responseWriter.Write(m.Body.Bytes())
} }
// FlushHeader writes the header to the underlying ResponseWriter. // FlushHeader writes the header to the underlying ResponseWriter.
func (m *Interceptor) FlushHeader() { func (m *Interceptor) FlushHeader() {
m.w.WriteHeader(m.Code, m.Meta) m.responseWriter.WriteHeader(m.Code, m.Meta)
} }

View file

@ -41,12 +41,12 @@ func main() {
flag.StringVar(&addr, "addr", defaultAddress, "address to listen on, e.g. 127.0.0.1:1965") flag.StringVar(&addr, "addr", defaultAddress, "address to listen on, e.g. 127.0.0.1:1965")
flag.IntVar(&maxconns, "max-conns", defaultMaxConns, "maximum number of concurrently open connections") flag.IntVar(&maxconns, "max-conns", defaultMaxConns, "maximum number of concurrently open connections")
flag.IntVar(&timeout, "timeout", defaultTimeout, "connection timeout in seconds") flag.IntVar(&timeout, "timeout", defaultTimeout, "connection timeout in seconds")
flag.IntVar(&cache, "cache", defaultCacheObjects, "simple lru document cache for n items. Disabled when zero.") flag.IntVar(&cache, "cache", defaultCacheObjects, "simple fifo document cache for n items. Disabled when zero.")
flag.StringVar(&root, "root", defaultRootPath, "server root directory to serve from") flag.StringVar(&root, "root", defaultRootPath, "server root directory to serve from")
flag.StringVar(&host, "host", defaultHost, "hostname for sni and x509 CN when using temporary self-signed certs") flag.StringVar(&host, "host", defaultHost, "hostname for sni and x509 CN when using temporary self-signed certs")
flag.StringVar(&crt, "cert", defaultCertPath, "TLS chain of one or more certificates") flag.StringVar(&crt, "cert", defaultCertPath, "TLS chain of one or more certificates")
flag.StringVar(&key, "key", defaultKeyPath, "TLS private key") flag.StringVar(&key, "key", defaultKeyPath, "TLS private key")
flag.IntVar(&autocertvalidity, "autocertvalidity", defaultAutoCertValidity, "valid days when using a gmifs auto provisioned self-signed certificate") flag.IntVar(&autocertvalidity, "autocertvalidity", defaultAutoCertValidity, "valid days when using a gmifs provisioned certificate")
flag.StringVar(&logs, "logs", defaultLogsDir, "enables file based logging and specifies the directory") flag.StringVar(&logs, "logs", defaultLogsDir, "enables file based logging and specifies the directory")
flag.BoolVar(&debug, "debug", defaultDebugMode, "enable verbose logging of the gemini server") flag.BoolVar(&debug, "debug", defaultDebugMode, "enable verbose logging of the gemini server")
flag.BoolVar(&autoindex, "autoindex", defaultAutoIndex, "enables auto indexing, directory listings") flag.BoolVar(&autoindex, "autoindex", defaultAutoIndex, "enables auto indexing, directory listings")
@ -54,6 +54,7 @@ func main() {
var err error var err error
var flogger, dlogger *log.Logger var flogger, dlogger *log.Logger
flogger, err = setupLogger(logs, "access.log") flogger, err = setupLogger(logs, "access.log")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View file

@ -26,6 +26,7 @@ func (c *cache) Read(key string) ([]byte, string, bool) {
func (c *cache) housekeeping(key string) { func (c *cache) housekeeping(key string) {
// we enter locked and can modify // we enter locked and can modify
// check if we need to free a slot.
if len(c.tracker) >= c.size { if len(c.tracker) >= c.size {
overflow := c.index overflow := c.index
expired := c.tracker[overflow] expired := c.tracker[overflow]
@ -33,9 +34,10 @@ func (c *cache) housekeeping(key string) {
delete(c.mimeTypes, expired) delete(c.mimeTypes, expired)
delete(c.tracker, overflow) delete(c.tracker, overflow)
} }
c.tracker[c.index] = key c.tracker[c.index] = key
c.index++ c.index++
c.index = c.index % (c.size) c.index %= c.size
} }
func (c *cache) Write(key string, mimeType string, doc []byte) { func (c *cache) Write(key string, mimeType string, doc []byte) {
@ -43,6 +45,7 @@ func (c *cache) Write(key string, mimeType string, doc []byte) {
if c.size <= 0 { if c.size <= 0 {
return return
} }
c.Lock() c.Lock()
c.housekeeping(key) c.housekeeping(key)
c.documents[key] = doc c.documents[key] = doc
@ -65,6 +68,7 @@ func (c *cache) middleware(next gemini.Handler) gemini.Handler {
if body, mimeType, hit := c.Read(key); hit { if body, mimeType, hit := c.Read(key); hit {
w.WriteHeader(gemini.StatusSuccess, mimeType) w.WriteHeader(gemini.StatusSuccess, mimeType)
w.Write(body) w.Write(body)
return return
} }
@ -75,6 +79,7 @@ func (c *cache) middleware(next gemini.Handler) gemini.Handler {
if ri.HasHeader() && ri.Code == gemini.StatusSuccess { if ri.HasHeader() && ri.Code == gemini.StatusSuccess {
c.Write(key, ri.Meta, ri.Body.Bytes()) c.Write(key, ri.Meta, ri.Body.Bytes())
} }
ri.Flush() ri.Flush()
} }
return gemini.HandlerFunc(fn) return gemini.HandlerFunc(fn)