Files
helix-proxy/cmd/helix-proxy/run.go
T
s1d3sw1ped_bot 13070a275d
Format / gofmt (push) Successful in 7s
CI / Build (push) Successful in 14s
Format / gofmt (pull_request) Successful in 7s
CI / Build (pull_request) Successful in 13s
CI / Go Tests (push) Successful in 50s
CI / Go Tests (pull_request) Successful in 48s
Lock bootstrap admin token until password is changed.
Closes #2: bootstrap JWTs cannot mutate admin APIs except change-password, production requires ADMIN_PASSWORD on first boot, admin binds loopback.
2026-09-01 03:54:09 +00:00

140 lines
3.2 KiB
Go

package main
import (
"context"
"errors"
"log/slog"
"net/http"
"os"
"time"
"helix-proxy/internal/auth"
"helix-proxy/internal/certificate"
"helix-proxy/internal/config"
"helix-proxy/internal/proxy"
"helix-proxy/internal/store"
)
// run boots the admin API, proxy engine, HTTPS listener, and renewal loop until ctx is cancelled.
func run(ctx context.Context) error {
applyUmaskFromEnv()
if err := config.EnsureDataDirs(); err != nil {
return err
}
if err := applyPUIDPGID(); err != nil {
slog.Warn("PUID/PGID apply partial", "err", err)
}
dbPath := config.Resolve("db.bolt")
slog.Info("using data dir", "data", config.DataDir(), "db", dbPath)
if config.IsDevelopment() {
slog.Warn("PROXY_MODE=development (all letsencrypt certs will be self-signed test certs; not for production use)")
} else {
slog.Info("production mode (real LE only; no test cert simulation)")
}
st, err := store.New()
if err != nil {
return err
}
if err := applyInitialAdminPassword(st); err != nil {
return err
}
eng := proxy.NewEngine(st)
eng.ReloadFromStore()
cm := certificate.NewManager(st)
jwtSecret, err := auth.LoadOrCreateSecret(config.DataDir())
if err != nil {
return err
}
jwtMgr := auth.NewJWTManager(jwtSecret)
go startRenewalLoop(ctx, cm, eng)
r := newAdminRouter(st, eng, cm, jwtMgr)
adminAddr := adminListenAddr()
srv := &http.Server{
Addr: adminAddr,
Handler: r,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
slog.Info("admin UI + API listening", "addr", adminAddr, "note", "use ADMIN_PORT=81 in privileged env or docker for real port")
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("server error", "err", err)
}
}()
proxyCtx, proxyCancel := context.WithCancel(ctx)
defer proxyCancel()
go func() {
if err := eng.Start(proxyCtx); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("proxy engine stopped", "err", err)
}
}()
go func() {
httpsAddr := proxyHTTPSListenAddr()
slog.Info("starting HTTPS listener for cert-enabled hosts (pure Go tls termination)", "addr", httpsAddr)
if err := serveHTTPS(proxyCtx, eng, httpsAddr); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("https server error", "err", err)
}
}()
<-ctx.Done()
slog.Info("shutting down...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
proxyCancel()
_ = srv.Shutdown(shutdownCtx)
slog.Info("stopped")
return nil
}
// startRenewalLoop periodically renews LE certs until ctx is cancelled.
func startRenewalLoop(ctx context.Context, cm *certificate.Manager, eng *proxy.Engine) {
initialDelay := 5 * time.Second
if d := os.Getenv("RENEWAL_INITIAL_DELAY"); d != "" {
if parsed, err := time.ParseDuration(d); err == nil {
initialDelay = parsed
}
}
renew := func() {
cm.ProcessRenewals()
eng.ReloadFromStore()
}
go func() {
select {
case <-ctx.Done():
return
case <-time.After(initialDelay):
renew()
}
}()
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
renew()
}
}
}