//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:]) }