rotate log files on SIGHUP
This commit is contained in:
parent
860799291c
commit
e776588e5c
2 changed files with 70 additions and 136 deletions
46
README.md
46
README.md
|
@ -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
160
main.go
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue