init
Some checks failed
release-tag / release-image (push) Failing after 1m28s

This commit is contained in:
2025-09-24 10:32:22 +02:00
parent b851b57e28
commit dc3abf661f
17 changed files with 1008 additions and 0 deletions

221
cmd/dweb/main.go Normal file
View File

@@ -0,0 +1,221 @@
package main
import (
"context"
"crypto/tls"
"encoding/json"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"git.send.nrw/sendnrw/decent-websrv/internal/beacon"
"git.send.nrw/sendnrw/decent-websrv/internal/cas"
"git.send.nrw/sendnrw/decent-websrv/internal/mesh"
"git.send.nrw/sendnrw/decent-websrv/internal/security"
)
// --- Config from ENV ---
type Config struct {
Addr string // public listener, e.g. :8080
TLSCertFile string // optional for public TLS
TLSKeyFile string
MeshAddr string // private mesh listener, e.g. :8443
TLSCAFile string // CA for mTLS client verify (mesh)
BaseURL string // public base URL of THIS node (for info pages)
MeshURL string // mesh base URL of THIS node (used by peers)
NodeID string // unique ID
AuthMode string // "ed25519" | "hmac"
SigPrivPath string // ed25519 private key (this node)
SigPubDir string // directory with <nodeID>.pub (peer pubkeys)
MeshSecret string // hmac secret (if AuthMode=hmac)
ConfigDir string // state dir (CAS, nonces)
MaxBodyBytes int64 // e.g. 8<<20
BeaconMode bool // run introducer
BeaconAddr string // introducer listener (public)
BeaconURL string // where to register/poll (client)
BeaconToken string // optional shared token for register/poll
}
func env(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
return def
}
func envBool(k string) bool {
v := strings.ToLower(os.Getenv(k))
return v == "1" || v == "true" || v == "yes"
}
func envInt64(k string, def int64) int64 {
if v := os.Getenv(k); v != "" {
var x int64
_, _ = fmtSscanf(v, &x)
return x
}
return def
}
func fmtSscanf(s string, p *int64) (int, error) { return fmtSscanfImpl(s, p) }
// tiny wrapper to avoid importing fmt at top-level; keeps imports tidy
func fmtSscanfImpl(s string, p *int64) (int, error) { return fmtSscanfReal(s, p) }
//go:linkname fmtSscanfReal fmt.Sscanf
func fmtSscanfReal(s string, p *int64) (int, error)
func loadConfig() Config {
cfg := Config{
Addr: env("ADDR", ":8080"),
TLSCertFile: env("TLS_CERT", ""),
TLSKeyFile: env("TLS_KEY", ""),
MeshAddr: env("MESH_ADDR", ":8443"),
TLSCAFile: env("TLS_CA", ""),
BaseURL: env("BASE_URL", "http://127.0.0.1:8080"),
MeshURL: env("MESH_URL", "https://127.0.0.1:8443"),
NodeID: env("NODE_ID", "node-A"),
AuthMode: env("AUTH_MODE", "ed25519"),
SigPrivPath: env("SIG_PRIV", ""),
SigPubDir: env("SIG_PUB_DIR", "./peers"),
MeshSecret: env("MESH_SECRET", ""),
ConfigDir: env("CONFIG_DIR", "./data"),
MaxBodyBytes: envInt64("MAX_BODY_BYTES", 8<<20),
BeaconMode: envBool("BEACON_MODE"),
BeaconAddr: env("BEACON_ADDR", ":9443"),
BeaconURL: env("BEACON_URL", ""),
BeaconToken: env("BEACON_TOKEN", ""),
}
return cfg
}
func main() {
cfg := loadConfig()
_ = os.MkdirAll(cfg.ConfigDir, 0o755)
casStore := cas.New(filepath.Join(cfg.ConfigDir, "cas"))
nonceStore := security.NewNonceStore(2 * time.Minute)
// --- Mesh auth setup ---
verifier, _ := security.NewMeshAuth(cfg.AuthMode, cfg.SigPrivPath, cfg.SigPubDir, cfg.MeshSecret)
// --- Catalog & Rendezvous ---
catalog := mesh.NewCatalog()
ring := mesh.NewRendezvous()
self := mesh.NodeInfo{NodeID: cfg.NodeID, PublicURL: cfg.BaseURL, MeshURL: cfg.MeshURL}
catalog.Set(self)
updateRing := func() { ring.Set(catalog.IDs()) }
updateRing()
// --- Beacon (introducer) ---
if cfg.BeaconMode {
go runBeaconServer(cfg)
}
// --- Beacon client loops ---
if cfg.BeaconURL != "" {
bcl := &beacon.Client{URL: cfg.BeaconURL, Token: cfg.BeaconToken, HTTP: &http.Client{Timeout: 5 * time.Second}}
bcl.LoopRegister(self)
go func() {
for {
peers := bcl.PollPeers()
catalog.Replace(peers)
updateRing()
time.Sleep(20 * time.Second)
}
}()
}
// --- Public handler ---
casHTTP := &cas.HTTP{S: casStore}
fetcher := &cas.MeshFetcher{Ring: ring, HTTP: &http.Client{Timeout: 10 * time.Second}, Verifier: verifier, Self: self}
casHTTP.Fetcher = fetcher
publicMux := http.NewServeMux()
publicMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]any{
"node": self,
"peers": catalog.All(),
"hint": "GET /c/<sha256> to fetch content; use mesh PUT to write",
})
})
publicMux.HandleFunc("/c/", casHTTP.Serve)
// --- Mesh handler (private) ---
meshMux := http.NewServeMux()
meshMux.HandleFunc("/_mesh/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })
meshMux.HandleFunc("/_mesh/cas/", casHTTP.MeshGet)
meshMux.HandleFunc("/_mesh/cas/put", casHTTP.MeshPut)
meshHandler := security.MeshAuthMiddleware(verifier, nonceStore, cfg.MaxBodyBytes)(meshMux)
// --- Rate limiter & global security wrappers ---
rl := security.NewLimiter(5, 20) // 5rps per IP+path, burst 20
publicHandler := security.SecurityHeaders(security.MaxBody(cfg.MaxBodyBytes, rl.Middleware(publicMux)))
// --- Servers ---
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
publicSrv := &http.Server{
Addr: cfg.Addr,
Handler: publicHandler,
ReadHeaderTimeout: 5 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 8 << 10,
}
meshSrv := &http.Server{
Addr: cfg.MeshAddr,
Handler: meshHandler,
ReadHeaderTimeout: 5 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 8 << 10,
}
if cfg.TLSCAFile != "" {
meshSrv.TLSConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: security.LoadCertPool(cfg.TLSCAFile),
}
}
go func() {
log.Println("public listening on", cfg.Addr)
if cfg.TLSCertFile != "" {
_ = publicSrv.ListenAndServeTLS(cfg.TLSCertFile, cfg.TLSKeyFile)
} else {
_ = publicSrv.ListenAndServe()
}
}()
go func() {
log.Println("mesh listening on", cfg.MeshAddr)
if cfg.TLSCertFile != "" {
_ = meshSrv.ListenAndServeTLS(cfg.TLSCertFile, cfg.TLSKeyFile)
} else {
_ = meshSrv.ListenAndServe()
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = publicSrv.Shutdown(shutdownCtx)
_ = meshSrv.Shutdown(shutdownCtx)
}
func runBeaconServer(cfg Config) {
s := beacon.NewServer(cfg.BeaconAddr, cfg.BeaconToken)
log.Println("beacon listening on", cfg.BeaconAddr)
if cfg.TLSCertFile != "" {
_ = s.ListenAndServeTLS(cfg.TLSCertFile, cfg.TLSKeyFile)
} else {
_ = s.ListenAndServe()
}
}