Files
nuntius/cmd/server/main.go
T

161 lines
3.4 KiB
Go

//go:build linux || freebsd
// Command nuntius runs the contact form backend server.
//
// Configuration is loaded from a TOML file (default: /etc/nuntius/config.toml).
// The TOML file may reference environment variables for secrets using
// ${VAR_NAME} or $VAR_NAME syntax.
package main
import (
"context"
"errors"
"flag"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"codeberg.org/petrbalvin/nuntius/internal/config"
"codeberg.org/petrbalvin/nuntius/internal/handler"
"codeberg.org/petrbalvin/nuntius/internal/version"
)
func main() {
showVersion := flag.Bool("version", false, "Print version and exit")
flag.Parse()
if *showVersion {
fmt.Printf("%s version %s\n", version.Name, version.Version)
return
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
slog.SetDefault(logger)
cfgPath := config.ConfigPath()
cfg, err := config.Load(cfgPath)
if err != nil {
logger.Error("config load failed", "path", cfgPath, "err", err)
os.Exit(1)
}
h := handler.New(cfg)
mux := http.NewServeMux()
h.Register(mux)
srv := &http.Server{
Addr: ":" + itoa(cfg.Server.Port),
Handler: withRequestLog(mux, logger),
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
logger.Info("nuntius starting",
"version", version.Name+" "+version.Version,
"addr", srv.Addr,
"config", cfgPath,
"forms", len(cfg.Forms),
)
for _, f := range cfg.Forms {
logger.Info("form registered",
"name", f.Name,
"path", f.Path,
"to", f.To,
"smtp", f.SMTP.Host,
)
}
// Graceful shutdown.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Error("server error", "err", err)
os.Exit(1)
}
}()
<-ctx.Done()
logger.Info("shutdown signal received")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
logger.Error("shutdown error", "err", err)
os.Exit(1)
}
logger.Info("nuntius stopped cleanly")
}
// withRequestLog logs each HTTP request method, path, status, and duration.
func withRequestLog(next http.Handler, logger *slog.Logger) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rw, r)
logger.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", rw.status,
"duration_ms", time.Since(start).Milliseconds(),
"ip", clientIP(r),
)
})
}
type statusRecorder struct {
http.ResponseWriter
status int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
func clientIP(r *http.Request) string {
host := r.RemoteAddr
if i := last(host, ':'); i >= 0 {
return host[:i]
}
return host
}
func last(s string, c byte) int {
for i := len(s) - 1; i >= 0; i-- {
if s[i] == c {
return i
}
}
return -1
}
func itoa(n int) string {
if n == 0 {
return "0"
}
neg := n < 0
if neg {
n = -n
}
var buf [20]byte
i := len(buf)
for n > 0 {
i--
buf[i] = byte('0' + n%10)
n /= 10
}
if neg {
i--
buf[i] = '-'
}
return string(buf[i:])
}