- server.Config + DefaultConfig + LoadConfig/ParseConfig (yaml.v3) - all operator knobs now configurable: listen addr, audience, data dir, log level, per-IP rate limits, per-subject quota, challenge/session TTLs, max body bytes - server.New takes Config; flags (-addr/-audience/-data/-config) override the file; missing config.yaml falls back to defaults - config.yaml.example committed as template; config.yaml git-ignored - tests for config defaults/parsing/partial override
119 lines
3.7 KiB
Go
119 lines
3.7 KiB
Go
// Command server runs the trust relay over HTTP using the JSON transport.
|
|
//
|
|
// It holds no signing key: it stores and serves signed TCE objects and brokers
|
|
// authentication, but never forges (INV-1) and never decides authorization
|
|
// (INV-5).
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"git.n1ko.dev/Niko/niko_trust/internal/server"
|
|
)
|
|
|
|
func main() {
|
|
configPath := flag.String("config", "config.yaml", "path to YAML config (missing file => defaults)")
|
|
addr := flag.String("addr", "", "listen address (overrides config)")
|
|
audience := flag.String("audience", "", "server audience bound into auth assertions (overrides config)")
|
|
data := flag.String("data", "", "directory to persist objects (overrides config)")
|
|
powPutBits := flag.Int("pow-put-bits", 22, "proof-of-work difficulty for storing objects, leading zero bits (0 = off)")
|
|
powAuthBits := flag.Int("pow-auth-bits", 18, "proof-of-work difficulty for auth challenge issuance, leading zero bits (0 = off)")
|
|
ckptInterval := flag.Duration("ckpt-interval", time.Minute, "maximum time between signed checkpoints when the object set changed")
|
|
ckptEvery := flag.Int("ckpt-every", 128, "sign a checkpoint after this many new objects (0 = interval only)")
|
|
bftValidators := flag.String("bft-validators", "", "comma-separated validator public keys (hex); enables BFT finality")
|
|
bftURLs := flag.String("bft-urls", "", "comma-separated validator base URLs, aligned with keys")
|
|
bftTimeout := flag.Duration("bft-round-timeout", 2*time.Second, "BFT round timeout")
|
|
flag.Parse()
|
|
|
|
cfg, err := server.LoadConfig(*configPath)
|
|
if err != nil {
|
|
// A missing default config is fine; only a present-but-unreadable or
|
|
// malformed file is fatal.
|
|
if *configPath != "config.yaml" || !os.IsNotExist(err) {
|
|
slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError})).
|
|
Error("load config", "path", *configPath, "err", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// Flags override the file.
|
|
if *addr != "" {
|
|
cfg.ListenAddr = *addr
|
|
}
|
|
if *audience != "" {
|
|
cfg.Audience = *audience
|
|
}
|
|
if *data != "" {
|
|
cfg.DataDir = *data
|
|
}
|
|
|
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel(cfg.LogLevel)}))
|
|
srv := server.New(cfg,
|
|
server.WithPow(*powPutBits, *powAuthBits),
|
|
server.WithCheckpoints(cfg.DataDir, server.CheckpointConfig{Interval: *ckptInterval, EveryN: *ckptEvery}),
|
|
)
|
|
if *bftValidators != "" && *bftURLs != "" {
|
|
srv.SetBFT(server.BFTConfig{
|
|
ValidatorKeys: splitCSV(*bftValidators),
|
|
ValidatorURLs: splitCSV(*bftURLs),
|
|
RoundTimeout: *bftTimeout,
|
|
})
|
|
}
|
|
|
|
ctx, stop := context.WithCancel(context.Background())
|
|
srv.StartCheckpoints(ctx)
|
|
srv.StartBFT(ctx)
|
|
defer stop()
|
|
h := &http.Server{
|
|
Addr: cfg.ListenAddr,
|
|
Handler: srv.Handler(),
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
}
|
|
|
|
go func() {
|
|
logger.Info("trust relay listening", "addr", cfg.ListenAddr, "audience", cfg.Audience, "data_dir", cfg.DataDir)
|
|
if err := h.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
logger.Error("listen", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
}()
|
|
|
|
sig := make(chan os.Signal, 1)
|
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
|
<-sig
|
|
stop()
|
|
|
|
logger.Info("shutting down")
|
|
ctx2, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
if err := h.Shutdown(ctx2); err != nil {
|
|
logger.Error("shutdown", "err", err)
|
|
}
|
|
logger.Info("stopped")
|
|
}
|
|
|
|
func splitCSV(s string) []string {
|
|
var out []string
|
|
for _, part := range strings.Split(s, ",") {
|
|
if part != "" {
|
|
out = append(out, part)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func logLevel(s string) slog.Level {
|
|
var l slog.Level
|
|
if err := l.UnmarshalText([]byte(s)); err != nil {
|
|
return slog.LevelInfo
|
|
}
|
|
return l
|
|
}
|