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
- reloads ssl certs and reopens log files on SIGHUP, e.g. after Let's Encrypt renewal
- response writer interceptor and middleware support
- simple middleware for lru document cache
- simple middleware for fifo document cache
- concurrent request limiter
- 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))
@ -26,7 +26,7 @@ Currently only supported through the go toolchain, either check out the repot an
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
@ -68,15 +68,15 @@ If debug logs are enabled, the certificate rotation will be confirmed.
### Supported flags
```
Usage of ./gmifs:
sage of ./gmifs:
-addr string
address to listen on, e.g. 127.0.0.1:1965 (default ":1965")
-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
enables auto indexing, directory listings
-cache int
simple lru document cache for n items. Disabled when zero.
simple fifo document cache for n items. Disabled when zero.
-cert string
TLS chain of one or more certificates
-debug

View file

@ -56,8 +56,8 @@ var (
ErrEmptyRequestURL = errors.New("gemini: empty request URL")
ErrInvalidPath = errors.New("gemini: path error")
ErrInvalidHost = errors.New("gemini: empty host")
ErrInvalidUtf8 = errors.New("empty request URL")
ErrUnknownProtocol = fmt.Errorf("unknown protocol scheme")
ErrInvalidUtf8 = errors.New("gemini: empty request URL")
ErrUnknownProtocol = fmt.Errorf("gemini: unknown protocol scheme")
)
type Request struct {
@ -108,9 +108,10 @@ type Server struct {
MaxOpenConns int
// internal
listener net.Listener
shutdown bool
closed chan struct{}
listener net.Listener
shutdown bool
closed chan struct{}
sighupListener chan struct{}
}
func (s *Server) log(v string) {
@ -134,18 +135,13 @@ func (s *Server) loadTLS() (err error) {
return err
}
func (s *Server) ListenAndServe() error {
err := s.loadTLS()
if err != nil {
return err
}
func (s *Server) reloadTLSConfigOnSighup() {
hup := make(chan os.Signal, 1)
signal.Notify(hup, syscall.SIGHUP)
go func() {
for {
<-hup
for {
select {
case <-hup:
s.log("reloading certificate")
if s.listener != nil {
@ -157,8 +153,21 @@ func (s *Server) ListenAndServe() error {
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
// TLSConfig.
@ -166,6 +175,7 @@ func (s *Server) ListenAndServe() error {
s.closed = make(chan struct{})
var err error
s.listener, err = tls.Listen("tcp", s.Addr, s.TLSConfig)
if err != nil {
return fmt.Errorf("gemini server listen: %w", err)
@ -218,8 +228,10 @@ func (s *Server) handleConnection(conn net.Conn, sem chan struct{}) {
conn.Close()
<-sem // release
}()
reqChan := make(chan request)
w := &writer{conn}
// push job for which we allocated a sem slot and wait
go requestChannel(conn, reqChan)
select {
@ -229,6 +241,7 @@ func (s *Server) handleConnection(conn net.Conn, sem chan struct{}) {
return
}
ctx := context.Background()
r := &Request{
ctx: ctx,
@ -236,6 +249,7 @@ func (s *Server) handleConnection(conn net.Conn, sem chan struct{}) {
RequestURI: header.rawuri,
RemoteAddr: conn.RemoteAddr().String(),
}
s.Handler.ServeGemini(w, r)
case <-time.After(s.ReadTimeout):
@ -283,10 +297,12 @@ type request struct {
func requestChannel(c net.Conn, rsp chan request) {
req := &request{}
r, err := readHeader(c)
if r != nil {
req = r
}
req.err = err
rsp <- *req
}
@ -294,6 +310,7 @@ func requestChannel(c net.Conn, rsp chan request) {
func readHeader(c net.Conn) (*request, error) {
req, err := bufio.NewReader(c).ReadString('\r')
if err != nil {
// not sure this is the right response
return nil, Error(StatusTemporaryFailure, ErrEmptyRequest)
}
@ -315,19 +332,22 @@ func readHeader(c net.Conn) (*request, error) {
}
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)
} else if parsedURL.Host == "" {
} else if r.URL.Host == "" {
return r, Error(StatusBadRequest, ErrInvalidHost)
}
if parsedURL.Path == "" {
if r.URL.Path == "" {
// This error is a redirect path.
return r, Error(StatusRedirectPermanent, errors.New("./"+parsedURL.Path))
} else if cleaned := path.Clean(parsedURL.Path); cleaned != parsedURL.Path {
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
if cleaned != strings.TrimRight(parsedURL.Path, "/") {
if cleaned != strings.TrimRight(r.URL.Path, "/") {
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)
}
}
// confirm sighup listener for cert reloading exited
<-s.sighupListener
return nil
}
@ -381,7 +403,6 @@ func (w *writer) WriteHeader(code int, message string) (int, error) {
func (w *writer) Write(body []byte) (int, error) {
reader := bytes.NewReader(body)
n, err := io.Copy(w.w, reader)
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.
type Interceptor struct {
// 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
Body bytes.Buffer
@ -30,9 +30,10 @@ type Interceptor struct {
}
// 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.w = w
m.responseWriter = responseWriter
return
}
@ -66,10 +67,10 @@ func (m *Interceptor) Flush() {
// FlushBody flushes to the underlying responsewriter.
func (m *Interceptor) FlushBody() {
m.w.Write(m.Body.Bytes())
m.responseWriter.Write(m.Body.Bytes())
}
// FlushHeader writes the header to the underlying ResponseWriter.
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.IntVar(&maxconns, "max-conns", defaultMaxConns, "maximum number of concurrently open connections")
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(&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(&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.BoolVar(&debug, "debug", defaultDebugMode, "enable verbose logging of the gemini server")
flag.BoolVar(&autoindex, "autoindex", defaultAutoIndex, "enables auto indexing, directory listings")
@ -54,6 +54,7 @@ func main() {
var err error
var flogger, dlogger *log.Logger
flogger, err = setupLogger(logs, "access.log")
if err != nil {
log.Fatal(err)

View file

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