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>
183 lines
5.9 KiB
Go
183 lines
5.9 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"evobgp/internal/audit"
|
|
"evobgp/internal/jobs"
|
|
"evobgp/internal/maintenance"
|
|
"evobgp/internal/pgmonitor"
|
|
"evobgp/internal/repository"
|
|
"evobgp/internal/runtimelogs"
|
|
"evobgp/internal/store"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// Server implements EvoBGP control-plane HTTP API.
|
|
type Server struct {
|
|
store store.Backend
|
|
pgPool *pgxpool.Pool
|
|
pgMonitor *pgmonitor.Service
|
|
maintConfig *maintenance.ConfigProvider
|
|
maintStats *maintenance.DBStatsProvider
|
|
jobs *jobs.Registry
|
|
authLimiter *authRateLimiter
|
|
bundlePriv ed25519.PrivateKey
|
|
keyResolver *apiKeyResolver
|
|
firewallResolver *firewallTokenResolver
|
|
bundleSeedHex string
|
|
corsOrigins []string
|
|
cdnHTTP *http.Client
|
|
runtimeLogs *runtimelogs.Service
|
|
runtimeLogsPolicyTenant string
|
|
mux *http.ServeMux
|
|
|
|
// Portal / dual-auth (JWT) configuration.
|
|
jwtSecret string
|
|
authIssuer string
|
|
authPortalURL string
|
|
portalTenantID string
|
|
authRequired bool
|
|
auditIngestSecret string
|
|
auditPusher *audit.PortalPusher
|
|
}
|
|
|
|
// Options configures the API server.
|
|
type Options struct {
|
|
APIKeys string
|
|
// DatabaseURL enables PostgreSQL-backed store (migrations applied on connect).
|
|
DatabaseURL string
|
|
InsecureDev bool
|
|
SeedDemo bool
|
|
BundleSeedHex string
|
|
CORSAllowedOrigins string
|
|
// RuntimeLogsPolicyTenant overrides tenant for auto-cleanup scheduler settings (optional).
|
|
RuntimeLogsPolicyTenant string
|
|
|
|
// Portal / dual-auth (JWT) — leave empty to disable JWT path.
|
|
JWTSecret string // AUTH_JWT_SECRET / EVOBGP_AUTH_JWT_SECRET (HS256 shared secret)
|
|
AuthIssuer string // AUTH_ISSUER (expected iss claim; default https://auth.shnt.top)
|
|
AuthPortalURL string // AUTH_PORTAL_URL (returned by /v1/auth/config for the UI)
|
|
PortalTenantID string // fallback when JWT has no bgp_tenant_id / tenants.bgp
|
|
AuthRequired bool // AUTH_REQUIRED / EVOBGP_AUTH_REQUIRED (surfaced via /v1/auth/config)
|
|
// AuditIngestSecret — AUTH_AUDIT_INGEST_SECRET for portal push (optional).
|
|
AuditIngestSecret string
|
|
}
|
|
|
|
// New constructs Server and wiring for async jobs.
|
|
func New(opts Options) (*Server, error) {
|
|
backend, reg, pool, err := BootstrapWorkers(context.Background(), opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var priv ed25519.PrivateKey
|
|
if strings.TrimSpace(opts.BundleSeedHex) != "" {
|
|
seed, err := hex.DecodeString(strings.TrimSpace(opts.BundleSeedHex))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(seed) != ed25519.SeedSize {
|
|
return nil, errors.New("httpapi: BundleSeedHex must decode to 32 bytes")
|
|
}
|
|
priv = ed25519.NewKeyFromSeed(seed)
|
|
} else {
|
|
_, priv, _ = ed25519.GenerateKey(rand.Reader)
|
|
}
|
|
|
|
resolver, err := newAPIKeyResolver(opts.APIKeys, backend)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fwResolver, err := newFirewallTokenResolver(backend)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var pgMon *pgmonitor.Service
|
|
var maintCfg *maintenance.ConfigProvider
|
|
var maintStats *maintenance.DBStatsProvider
|
|
if pool != nil {
|
|
pgMon = pgmonitor.NewService(pool)
|
|
maintCfg = maintenance.NewConfigProvider(backend)
|
|
_ = maintCfg.Reload(context.Background())
|
|
maintStats = maintenance.NewDBStatsProvider(pgMon)
|
|
}
|
|
s := &Server{
|
|
store: backend,
|
|
pgPool: pool,
|
|
pgMonitor: pgMon,
|
|
maintConfig: maintCfg,
|
|
maintStats: maintStats,
|
|
jobs: reg,
|
|
bundlePriv: priv,
|
|
keyResolver: resolver,
|
|
firewallResolver: fwResolver,
|
|
bundleSeedHex: strings.TrimSpace(opts.BundleSeedHex),
|
|
corsOrigins: parseCORSOrigins(opts.CORSAllowedOrigins),
|
|
cdnHTTP: NewCDNHTTPClient(),
|
|
runtimeLogs: runtimelogs.NewService(runtimelogs.ConfigFromEnv()),
|
|
runtimeLogsPolicyTenant: strings.TrimSpace(opts.RuntimeLogsPolicyTenant),
|
|
jwtSecret: strings.TrimSpace(opts.JWTSecret),
|
|
authIssuer: strings.TrimSpace(opts.AuthIssuer),
|
|
authPortalURL: strings.TrimSpace(opts.AuthPortalURL),
|
|
portalTenantID: strings.TrimSpace(opts.PortalTenantID),
|
|
authRequired: opts.AuthRequired,
|
|
auditIngestSecret: strings.TrimSpace(opts.AuditIngestSecret),
|
|
}
|
|
if s.authIssuer == "" {
|
|
s.authIssuer = "https://auth.shnt.top"
|
|
}
|
|
s.initAuditPusher(s.authPortalURL, s.auditIngestSecret)
|
|
s.mux = http.NewServeMux()
|
|
s.registerRoutes()
|
|
return s, nil
|
|
}
|
|
|
|
// Close releases database resources.
|
|
func (s *Server) Close() {
|
|
if s.pgPool != nil {
|
|
s.pgPool.Close()
|
|
}
|
|
}
|
|
|
|
// Store exposes the backing store (for operators / tests).
|
|
func (s *Server) Store() store.Backend { return s.store }
|
|
|
|
// Jobs exposes the in-process async job registry (for scheduler / evobgp-all).
|
|
func (s *Server) Jobs() *jobs.Registry { return s.jobs }
|
|
|
|
// StartBackground starts PostgreSQL monitoring and maintenance schedulers until ctx is cancelled.
|
|
func (s *Server) StartBackground(ctx context.Context) {
|
|
if s != nil && s.pgPool != nil {
|
|
pgmonitor.StartScheduler(ctx, s.pgPool)
|
|
if s.jobs != nil {
|
|
jobs.StartDurableQueueLoop(ctx, s.jobs, repository.NewJobAuditWriter(s.pgPool))
|
|
}
|
|
}
|
|
if s != nil && s.maintConfig != nil && s.jobs != nil {
|
|
maintenance.StartScheduler(ctx, s.maintConfig, func(policyID string, dryRun bool, idem string) {
|
|
key := idem
|
|
_, _, _ = s.jobs.Enqueue("", jobs.KindMaintenancePolicyRun, &key, nil, map[string]any{
|
|
"policy_id": policyID,
|
|
"dry_run": dryRun,
|
|
"trigger": "scheduler",
|
|
})
|
|
}, 30*time.Second)
|
|
}
|
|
if s != nil && s.runtimeLogs != nil && s.store != nil {
|
|
runtimelogs.StartAutoCleanupScheduler(ctx, runtimelogs.SchedulerDeps{
|
|
Service: s.runtimeLogs,
|
|
Store: s.store,
|
|
PolicyTenant: s.runtimeLogsPolicyTenant,
|
|
}, 30*time.Second)
|
|
}
|
|
}
|