This commit is contained in:
221
cmd/dweb/main.go
Normal file
221
cmd/dweb/main.go
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user