package main import ( "context" "log" "os" "os/signal" "strings" "syscall" "time" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/recover" "github.com/gosec/gsc-shell-api/internal/auth" "github.com/gosec/gsc-shell-api/internal/config" "github.com/gosec/gsc-shell-api/internal/db" "github.com/gosec/gsc-shell-api/internal/handlers" ) func main() { cfg, err := config.Load() if err != nil { log.Fatalf("config: %v", err) } ctx := context.Background() database, err := db.New(ctx, cfg.DatabaseURL) if err != nil { log.Fatalf("db: %v", err) } defer database.Close() verifier, err := auth.NewVerifier(ctx, cfg.KeycloakIssuer, cfg.KeycloakDiscovery, cfg.KeycloakAudCSV) if err != nil { log.Fatalf("oidc: %v", err) } app := fiber.New(fiber.Config{ AppName: "gsc-shell-api", DisableStartupMessage: true, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, }) app.Use(recover.New()) app.Use(logger.New(logger.Config{ Format: `{"time":"${time}","status":${status},"method":"${method}","path":"${path}","latency":"${latency}","ip":"${ip}"}` + "\n", })) // CORS — apps run on different hostnames (crm.gosec.internal, // chronos.gosec.internal, etc.) so the browser fetch is cross-origin. // Allow gosec.internal app origins; AllowOrigins is comma-separated // via env (CORS_ORIGINS). Default permits any *.gosec.internal / // *.gosec.cloud origin via the AllowOriginsFunc fallback. corsConfig := cors.Config{ AllowMethods: "GET,OPTIONS", AllowHeaders: "Authorization,Content-Type,If-None-Match", ExposeHeaders: "ETag", AllowCredentials: false, MaxAge: 600, } if cfg.CORSAllowOrigins != "" { corsConfig.AllowOrigins = cfg.CORSAllowOrigins } else { corsConfig.AllowOriginsFunc = func(origin string) bool { return strings.HasSuffix(origin, ".gosec.internal") || strings.HasSuffix(origin, ".gosec.cloud") } } app.Use(cors.New(corsConfig)) health := &handlers.HealthHandlers{DB: database} app.Get("/healthz", health.Live) app.Get("/readyz", health.Ready) shell := &handlers.ShellHandlers{ DB: database, CacheTTLSeconds: cfg.CacheTTLSeconds, } api := app.Group("/api/v1", verifier.Middleware()) api.Get("/shell/:appKey", shell.GetShell) api.Get("/apps", shell.ListApps) addr := ":" + itoa(cfg.Port) log.Printf("gsc-shell-api listening on %s (issuer=%s)", addr, cfg.KeycloakIssuer) go func() { if err := app.Listen(addr); err != nil { log.Fatalf("listen: %v", err) } }() stop := make(chan os.Signal, 1) signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) <-stop log.Println("shutting down") shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = app.ShutdownWithContext(shutdownCtx) } func itoa(n int) string { // avoid pulling strconv just for this if n == 0 { return "0" } neg := n < 0 if neg { n = -n } buf := [20]byte{} i := len(buf) for n > 0 { i-- buf[i] = byte('0' + n%10) n /= 10 } if neg { i-- buf[i] = '-' } return string(buf[i:]) }