Files
EvoBGP/cmd/evobgp-api/main.go
T
DenozordecandCursor 6c6e76fca3
CI / changes (push) Successful in 7s
CI / openapi (push) Failing after 40s
CI / web (push) Successful in 56s
CI / commitlint (push) Skipped
CI / go (push) Failing after 34s
CI / bird2 (push) Skipped
CI / release (push) Skipped
feat(ops): protect metrics, rate-limit auth, agent secret timing, e2e smoke
Bearer для /metrics (EVOBGP_METRICS_TOKEN); rate limit /v1/auth/config; constant-time agent secret; OTel stub; Playwright smoke; HTTP_PROXY note; checklist обновлён.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-31 12:29:00 +07:00

144 lines
4.6 KiB
Go

package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"evobgp/internal/birdfmt"
"evobgp/internal/config"
"evobgp/internal/dbcli"
"evobgp/internal/httpapi"
"evobgp/internal/logging"
"evobgp/internal/observability"
"evobgp/internal/platform"
"evobgp/internal/version"
)
func main() {
if len(os.Args) > 1 && os.Args[1] == "db" {
os.Exit(dbcli.Run(os.Args[2:]))
}
cfg := config.Load()
if err := config.ValidateProductionEnforce(); err != nil {
log.Fatal(err)
}
seedDemo := os.Getenv("EVOBGP_SEED_DEMO") != "0"
opts := httpapi.Options{
APIKeys: os.Getenv("EVOBGP_API_KEYS"),
DatabaseURL: cfg.DatabaseURL,
InsecureDev: os.Getenv("EVOBGP_DEV_INSECURE") == "1",
SeedDemo: seedDemo,
BundleSeedHex: strings.TrimSpace(os.Getenv("EVOBGP_BUNDLE_SEED_HEX")),
CORSAllowedOrigins: strings.TrimSpace(os.Getenv("EVOBGP_CORS_ORIGINS")),
JWTSecret: firstNonEmpty(os.Getenv("EVOBGP_AUTH_JWT_SECRET"), os.Getenv("AUTH_JWT_SECRET")),
AuthIssuer: firstNonEmpty(os.Getenv("EVOBGP_AUTH_ISSUER"), os.Getenv("AUTH_ISSUER")),
AuthPortalURL: firstNonEmpty(os.Getenv("EVOBGP_AUTH_PORTAL_URL"), os.Getenv("AUTH_PORTAL_URL")),
PortalTenantID: strings.TrimSpace(os.Getenv("EVOBGP_PORTAL_TENANT_ID")),
AuthRequired: boolFromEnv("EVOBGP_AUTH_REQUIRED", "AUTH_REQUIRED"),
AuditIngestSecret: firstNonEmpty(os.Getenv("EVOBGP_AUTH_AUDIT_INGEST_SECRET"), os.Getenv("AUTH_AUDIT_INGEST_SECRET")),
}
srv, err := httpapi.New(opts)
if err != nil {
log.Fatal(err)
}
defer srv.Close()
observability.SetBuildInfo(version.Version, version.GitSHA)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
otelShutdown := observability.StartOTelIfEnabled(ctx)
defer func() { _ = otelShutdown(context.Background()) }()
srv.StartBackground(ctx)
startBirdMetricsPoller(ctx)
httpSrv := &http.Server{
Addr: cfg.HTTPAddr,
Handler: srv.Handler(),
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 60 * time.Second,
WriteTimeout: 120 * time.Second,
IdleTimeout: 120 * time.Second,
}
go func() {
svc := platform.ServiceName("evobgp-api")
logging.Default().Info("listening", "service", svc, "addr", cfg.HTTPAddr)
logging.Default().Info("bundle signing public key (base64, set on evobgp-node)", "key", srv.BundlePublicKeyBase64())
if opts.SeedDemo {
tid, mCDN, mIP, rev, sp := srv.Store().DemoIDs()
logging.Default().Info("demo ids", "tenant", tid, "module_cdn", mCDN, "module_ip_ranges", mIP, "revision", rev, "speaker", sp)
logging.Default().Info("example API keys", "hint", "EVOBGP_API_KEYS=op|"+tid+"|operator,node|"+tid+"|node")
logging.Default().Info("demo auth: Authorization: Bearer dev (operator, demo tenant only)")
}
if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
n := srv.Jobs().RequestCancelAll()
if n > 0 {
logging.Default().Info("draining jobs", "count", n)
if err := srv.Jobs().Drain(shutdownCtx); err != nil {
logging.Default().Warn("job drain", "err", err)
}
}
if err := httpSrv.Shutdown(shutdownCtx); err != nil {
logging.Default().Warn("HTTP shutdown", "err", err)
}
logging.Default().Info("stopped", "service", platform.ServiceName("evobgp-api"))
}
func firstNonEmpty(candidates ...string) string {
for _, c := range candidates {
if v := strings.TrimSpace(c); v != "" {
return v
}
}
return ""
}
func boolFromEnv(keys ...string) bool {
for _, k := range keys {
v := strings.TrimSpace(os.Getenv(k))
if v == "" {
continue
}
switch strings.ToLower(v) {
case "1", "true", "yes", "on":
return true
case "0", "false", "no", "off":
return false
}
}
return false
}
func startBirdMetricsPoller(ctx context.Context) {
sock := strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_SOCKET"))
if sock == "" {
return
}
interval := 30 * time.Second
if d, err := time.ParseDuration(strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_INTERVAL"))); err == nil && d > 0 {
interval = d
}
bin := strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_BIN"))
observability.StartBirdProtocolsPoller(ctx, sock, bin, interval,
func(ctx context.Context, socket, birdcBin string) (string, error) {
return birdfmt.ShowProtocols(ctx, socket, birdcBin)
},
birdfmt.CountEstablishedBGPSessions,
birdfmt.ParseBGPProtocolStates,
)
log.Printf("birdc protocols poller enabled (socket=%s interval=%s)", sock, interval)
}