rotate log files on SIGHUP

This commit is contained in:
dre 2021-07-08 20:55:09 +08:00
parent 860799291c
commit e776588e5c
2 changed files with 70 additions and 136 deletions

View file

@ -1,38 +1,54 @@
# gmifs
Gemini File Server, short gmifs, is intended to be minimal and serve static files only. It is used
Gemini File Server, short gmifs, is intended to be minimal and serve static files. It is used
to accompany a hugo blog served via httpd and make it available via the [gemini
protocol](https://gemini.circumlunar.space/docs/specification.gmi). Why built yet another gemini
server? Because it's educational and that's the spirit of the protocol.
Features
- zero conf
- zero dependencies (pure go, standard library only)
- only modern tls ciphers (from Mozilla's [TLS ciphers recommendation](https://statics.tls.security.mozilla.org/server-side-tls-conf.json))
- concurrent request limiter
- **zero conf**, if no certificate is available, gmifs can generates self-signed certs
- **zero dependencies**, Go standard library only
- directory listing support
- only modern tls ciphers (from [Mozilla's TLS ciphers recommendations](https://statics.tls.security.mozilla.org/server-side-tls-conf.json))
- concurrent requests limiter
- reloads ssl certs and flushes/reopens log files on SIGHUP
- single file gemini implementation, focus on simplicity, no bells and whistles
This tool is used alongside the markdown to gemtext converter
[md2gmi](https://github.com/n0x1m/md2gmi).
Generate a self-signed server certificate with openssl:
## Usage
### Dev & Tests
Test it locally by serving e.g. a `./public` directory on localhost with directory listing turned on
```
./gmifs -root ./public -host localhost -autoindex
```
### Production
In the real world generate a self-signed server certificate with OpenSSL or use a Let's Encrypt
key pair
```bash
openssl req -x509 -newkey rsa:4096 -keyout key.rsa -out cert.pem \
-days 3650 -nodes -subj "/CN=nox.im"
```
## Usage
locally test it by serving a `./gemini` directory
```
./gmifs -root ./gemini
```
full example
start gmifs with the keypair
```
gmifs -addr 0.0.0.0:1965 -root /var/www/htdocs/nox.im/gemini \
-host nox.im -max-conns 1024 -timeout 5 \
-logs /var/gemini/logs/ \
-cert /etc/ssl/nox.im.fullchain.pem \
-key /etc/ssl/private/nox.im.key
```
if need be, send SIGHUP to reload the certificate without downtime
```
pgrep gmifs | awk '{print "kill -1 " $1}' | sh
```

160
main.go
View file

@ -6,16 +6,14 @@ import (
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"mime"
"os"
"os/signal"
"path"
"path/filepath"
"syscall"
"time"
"github.com/n0x1m/gmifs/fileserver"
"github.com/n0x1m/gmifs/gemini"
"github.com/n0x1m/gmifs/middleware"
)
@ -32,8 +30,6 @@ const (
shutdownTimeout = 10 * time.Second
)
var ErrDirWithoutIndex = errors.New("path is directory without index.gmi")
func main() {
var addr, root, crt, key, host, logs string
var maxconns, timeout int
@ -51,36 +47,20 @@ func main() {
flag.BoolVar(&autoindex, "autoindex", false, "enables or disables the directory listing output")
flag.Parse()
// TODO: rotate on SIGHUP
mlogger := log.New(os.Stdout, "", log.LUTC|log.Ldate|log.Ltime)
if logs != "" {
logpath := filepath.Join(logs, "access.log")
accessLog, err := os.OpenFile(logpath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
var err error
var flogger, dlogger *log.Logger
flogger, err = setupLogger(logs, "access.log")
if err != nil {
log.Fatal(err)
}
if debug {
dlogger, err = setupLogger(logs, "debug.log")
if err != nil {
log.Fatal(err)
}
defer accessLog.Close()
mlogger.SetOutput(accessLog)
}
var dlogger *log.Logger
if debug {
dlogger = log.New(os.Stdout, "", log.LUTC|log.Ldate|log.Ltime)
if logs != "" {
logpath := filepath.Join(logs, "debug.log")
debugLog, err := os.OpenFile(logpath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatal(err)
}
defer debugLog.Close()
dlogger.SetOutput(debugLog)
}
}
var err error
var cert tls.Certificate
if crt != "" && key != "" {
log.Println("loading certificate from", crt)
@ -103,8 +83,8 @@ func main() {
}
mux := gemini.NewMux()
mux.Use(middleware.Logger(mlogger))
mux.Handle(gemini.HandlerFunc(fileserver(root, true)))
mux.Use(middleware.Logger(flogger))
mux.Handle(gemini.HandlerFunc(fileserver.Serve(root, true)))
server := &gemini.Server{
Addr: addr,
@ -131,109 +111,47 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
if err := server.Shutdown(ctx); err != nil {
cancel()
log.Fatal("ListenAndServe shutdown")
log.Fatalf("ListenAndServe shutdown with error: %v", err)
}
<-confirm
cancel()
}
func fileserver(root string, dirlisting bool) func(w io.Writer, r *gemini.Request) {
return func(w io.Writer, r *gemini.Request) {
fullpath, err := fullPath(root, r.URL.Path)
func setupLogger(dir, filename string) (*log.Logger, error) {
logger := log.New(os.Stdout, "", log.LUTC|log.Ldate|log.Ltime)
if dir != "" {
// non 12factor stuff
logpath := filepath.Join(dir, filename)
_, err := setupFileLogging(logger, logpath)
if err != nil {
if err == ErrDirWithoutIndex && dirlisting {
body, mimeType, err := listDirectory(fullpath, r.URL.Path)
log.Fatalf("failed to open log file: %v", err)
}
go func(logger *log.Logger, logpath string) {
hup := make(chan os.Signal, 1)
signal.Notify(hup, syscall.SIGHUP)
for {
<-hup
logger.Println("rotating log file after SIGHUP")
_, err := setupFileLogging(logger, logpath)
if err != nil {
gemini.WriteHeader(w, gemini.StatusNotFound, err.Error())
return
log.Fatalf("failed to rotate log file: %v", err)
}
gemini.WriteHeader(w, gemini.StatusSuccess, mimeType)
gemini.Write(w, body)
return
}
gemini.WriteHeader(w, gemini.StatusNotFound, err.Error())
return
}
body, mimeType, err := readFile(fullpath)
if err != nil {
gemini.WriteHeader(w, gemini.StatusNotFound, err.Error())
return
}
gemini.WriteHeader(w, gemini.StatusSuccess, mimeType)
gemini.Write(w, body)
}(logger, logpath)
}
return logger, nil
}
func fullPath(root, requestPath string) (string, error) {
fullpath := path.Join(root, requestPath)
pathInfo, err := os.Stat(fullpath)
func setupFileLogging(logger *log.Logger, logpath string) (*os.File, error) {
logfile, err := os.OpenFile(logpath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
return "", fmt.Errorf("path: %w", err)
return logfile, err
}
if pathInfo.IsDir() {
subDirIndex := path.Join(fullpath, gemini.IndexFile)
if _, err := os.Stat(subDirIndex); os.IsNotExist(err) {
return fullpath, ErrDirWithoutIndex
}
fullpath = subDirIndex
}
return fullpath, nil
}
func readFile(filepath string) ([]byte, string, error) {
mimeType := getMimeType(filepath)
if mimeType == "" {
return nil, "", errors.New("disabled/unsupported file type")
}
file, err := os.Open(filepath)
if err != nil {
return nil, "", fmt.Errorf("file: %w", err)
}
defer file.Close()
data, err := ioutil.ReadAll(file)
if err != nil {
return nil, "", fmt.Errorf("read: %w", err)
}
return data, mimeType, nil
}
func getMimeType(fullpath string) string {
if ext := path.Ext(fullpath); ext != ".gmi" {
return mime.TypeByExtension(ext)
}
return gemini.MimeType
}
func listDirectory(fullpath, relpath string) ([]byte, string, error) {
files, err := ioutil.ReadDir(fullpath)
if err != nil {
return nil, "", err
}
var out []byte
parent := filepath.Dir(relpath)
if relpath != "/" {
out = append(out, []byte(fmt.Sprintf("Index of %s/\n\n", relpath))...)
out = append(out, []byte(fmt.Sprintf("=> %s ..\n", parent))...)
} else {
out = append(out, []byte(fmt.Sprintf("Index of %s\n\n", relpath))...)
}
for _, f := range files {
if relpath == "/" {
out = append(out, []byte(fmt.Sprintf("=> %s\n", f.Name()))...)
} else {
out = append(out, []byte(fmt.Sprintf("=> %s/%s %s\n", relpath, f.Name(), f.Name()))...)
}
}
return out, gemini.MimeType, nil
logger.SetOutput(logfile)
return logfile, nil
}