feat: initialize nuntius project with full server implementation
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
//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:])
|
||||
}
|
||||
Reference in New Issue
Block a user