update docs and improve readability
This commit is contained in:
parent
8e66aa4314
commit
f590db1c37
5 changed files with 63 additions and 35 deletions
10
README.md
10
README.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
5
main.go
5
main.go
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue