mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-21 07:02:27 +02:00
- Update Next.js generated chunk hashes and build IDs reflecting latest dashboard build - Update CSS stylesheet references in workflow upload page metadata - Add comprehensive cloud setup E2E test suite (cloud_setup_test.go) with SSH password/key auth, post-command variable expansion, and Ansible integration - Fix API priority levels to include 'medium' priority in test coverage - Add agent-sdk test workflows (minimal, config, codex, multi-agent, session variants) - Update E2E test utilities with runCLIInBase helper for multi-step cloud config tests - Fix stderr/stdout capture in dependencies_target_types_test assertions
674 lines
23 KiB
Go
674 lines
23 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gofiber/adaptor/v2"
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
|
"github.com/gofiber/fiber/v2/middleware/favicon"
|
|
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
|
"github.com/gofiber/fiber/v2/middleware/recover"
|
|
"github.com/gofiber/swagger"
|
|
"github.com/j3ssie/osmedeus/v5/internal/config"
|
|
"github.com/j3ssie/osmedeus/v5/internal/core"
|
|
"github.com/j3ssie/osmedeus/v5/internal/database"
|
|
"github.com/j3ssie/osmedeus/v5/internal/distributed"
|
|
oslogger "github.com/j3ssie/osmedeus/v5/internal/logger"
|
|
"github.com/j3ssie/osmedeus/v5/internal/metrics"
|
|
"github.com/j3ssie/osmedeus/v5/internal/terminal"
|
|
"github.com/j3ssie/osmedeus/v5/pkg/server/handlers"
|
|
"github.com/j3ssie/osmedeus/v5/pkg/server/middleware"
|
|
"github.com/j3ssie/osmedeus/v5/public"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
"go.uber.org/zap"
|
|
|
|
_ "github.com/j3ssie/osmedeus/v5/docs/api-swagger" // swagger docs
|
|
)
|
|
|
|
// Options contains server configuration options
|
|
type Options struct {
|
|
NoAuth bool // Disable authentication when true
|
|
Master *distributed.Master // Master node for distributed mode (nil if not in master mode)
|
|
Debug bool // Enable debug mode (log request bodies, detailed errors)
|
|
HotReload bool // Enable config hot reload (watches osm-settings.yaml for changes)
|
|
EnableEventReceiver bool // Enable event receiver for event-triggered workflows (default: true)
|
|
EnableScheduler bool // Enable scheduler for cron, watch, and event triggers (default: true)
|
|
}
|
|
|
|
// cachedServerInfo holds server info read once at startup to avoid reading config on every request
|
|
var cachedServerInfo struct {
|
|
License string
|
|
Version string
|
|
Binary string
|
|
Repo string
|
|
Author string
|
|
Docs string
|
|
Build string
|
|
Commit string
|
|
}
|
|
|
|
// Server represents the web server
|
|
type Server struct {
|
|
app *fiber.App
|
|
config *config.Config
|
|
configProvider handlers.ConfigProvider
|
|
hotConfig *config.HotReloadableConfig // nil if hot reload is disabled
|
|
options *Options
|
|
eventReceiver *EventReceiver // nil if event receiver is disabled
|
|
}
|
|
|
|
// New creates a new server instance
|
|
func New(cfg *config.Config, opts *Options) (*Server, error) {
|
|
if opts == nil {
|
|
opts = &Options{}
|
|
}
|
|
|
|
// Initialize database connection once at startup
|
|
_, err := database.Connect(cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
|
}
|
|
|
|
// Run migrations once at startup
|
|
ctx := context.Background()
|
|
if err := database.Migrate(ctx); err != nil {
|
|
return nil, fmt.Errorf("failed to run database migrations: %w", err)
|
|
}
|
|
oslogger.Get().Debug("Database initialized", zap.String("engine", cfg.Database.DBEngine))
|
|
|
|
// Index workflows from filesystem to database at startup
|
|
if cfg.WorkflowsPath != "" {
|
|
result, err := database.IndexWorkflowsFromFilesystem(ctx, cfg.WorkflowsPath, false)
|
|
if err != nil {
|
|
oslogger.Get().Warn("Failed to index workflows at startup", zap.Error(err))
|
|
} else {
|
|
oslogger.Get().Info("Workflows indexed at startup",
|
|
zap.Int("added", result.Added),
|
|
zap.Int("updated", result.Updated),
|
|
zap.Int("removed", result.Removed),
|
|
)
|
|
}
|
|
}
|
|
|
|
// Cache server info at startup (read once, not on every request)
|
|
license := cfg.Server.License
|
|
if license == "" {
|
|
license = core.LICENSE // fallback to constant
|
|
}
|
|
cachedServerInfo.License = license
|
|
cachedServerInfo.Version = core.VERSION
|
|
cachedServerInfo.Binary = core.BINARY
|
|
cachedServerInfo.Repo = core.REPO_URL
|
|
cachedServerInfo.Author = core.AUTHOR
|
|
cachedServerInfo.Docs = core.DOCS
|
|
cachedServerInfo.Build = core.BuildTime
|
|
cachedServerInfo.Commit = core.CommitHash
|
|
|
|
// Set cached info for handlers to use
|
|
handlers.SetServerInfo(&handlers.ServerInfoData{
|
|
License: cachedServerInfo.License,
|
|
Version: cachedServerInfo.Version,
|
|
Binary: cachedServerInfo.Binary,
|
|
Repo: cachedServerInfo.Repo,
|
|
Author: cachedServerInfo.Author,
|
|
Docs: cachedServerInfo.Docs,
|
|
Build: cachedServerInfo.Build,
|
|
Commit: cachedServerInfo.Commit,
|
|
})
|
|
|
|
// Select error handler based on debug mode
|
|
var errHandler fiber.ErrorHandler
|
|
if opts.Debug {
|
|
errHandler = middleware.DebugErrorHandler
|
|
} else {
|
|
errHandler = errorHandler
|
|
}
|
|
|
|
app := fiber.New(fiber.Config{
|
|
AppName: "Osmedeus API Server",
|
|
ServerHeader: fmt.Sprintf("%s %s (%s)", core.BINARY, core.VERSION, license),
|
|
ErrorHandler: errHandler,
|
|
DisableStartupMessage: true,
|
|
})
|
|
|
|
// Apply middleware
|
|
app.Use(recover.New())
|
|
app.Use(logger.New(logger.Config{
|
|
Next: func(c *fiber.Ctx) bool {
|
|
// Skip logging for Next.js static routes
|
|
return strings.Contains(c.Path(), "_next")
|
|
},
|
|
}))
|
|
app.Use(cors.New(cors.Config{
|
|
AllowOriginsFunc: func(origin string) bool {
|
|
// Reflect all origins - returns true to allow and echo back the origin
|
|
return true
|
|
},
|
|
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS,HEAD",
|
|
AllowHeaders: "Origin,Content-Type,Accept,Authorization",
|
|
AllowCredentials: true,
|
|
}))
|
|
|
|
// Apply Prometheus metrics middleware (conditional)
|
|
if cfg.Server.IsMetricsEnabled() {
|
|
app.Use(middleware.PrometheusMetrics())
|
|
}
|
|
|
|
// Apply debug middleware if debug mode is enabled
|
|
if opts.Debug {
|
|
app.Use(middleware.BodyReusable())
|
|
app.Use(middleware.DebugRequestBody())
|
|
app.Use(middleware.DebugResponseLogger())
|
|
}
|
|
|
|
s := &Server{
|
|
app: app,
|
|
config: cfg,
|
|
options: opts,
|
|
}
|
|
|
|
// Initialize config provider (hot reload or static)
|
|
if opts.HotReload && cfg.BaseFolder != "" {
|
|
hotCfg, err := config.NewHotReloadableConfig(cfg.BaseFolder)
|
|
if err != nil {
|
|
oslogger.Get().Warn("Failed to initialize hot reload config, falling back to static",
|
|
zap.Error(err),
|
|
)
|
|
s.configProvider = handlers.NewStaticConfigProvider(cfg)
|
|
} else {
|
|
s.hotConfig = hotCfg
|
|
s.configProvider = handlers.NewHotReloadConfigProvider(hotCfg)
|
|
|
|
// Start watching for config changes
|
|
if err := hotCfg.Watch(); err != nil {
|
|
oslogger.Get().Warn("Failed to start config watcher",
|
|
zap.Error(err),
|
|
)
|
|
} else {
|
|
oslogger.Get().Debug("Config hot reload enabled",
|
|
zap.String("config_path", hotCfg.GetConfigPath()),
|
|
)
|
|
}
|
|
}
|
|
} else {
|
|
s.configProvider = handlers.NewStaticConfigProvider(cfg)
|
|
}
|
|
|
|
// Initialize event receiver if enabled (default: true when not explicitly disabled)
|
|
// Event receiver requires scheduler to be enabled (it contains the scheduler)
|
|
if opts.EnableEventReceiver && opts.EnableScheduler {
|
|
er, err := NewEventReceiver(cfg)
|
|
if err != nil {
|
|
oslogger.Get().Warn("Failed to initialize event receiver", zap.Error(err))
|
|
} else {
|
|
s.eventReceiver = er
|
|
oslogger.Get().Debug("Event receiver initialized")
|
|
}
|
|
}
|
|
|
|
// Setup routes
|
|
s.setupRoutes()
|
|
|
|
// Start memory metrics collector if metrics are enabled
|
|
if cfg.Server.IsMetricsEnabled() {
|
|
startMemoryMetricsCollector(context.Background(), 15*time.Second)
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// setupRoutes configures all API routes
|
|
func (s *Server) setupRoutes() {
|
|
// Favicon from embedded filesystem
|
|
s.app.Use(favicon.New(favicon.Config{
|
|
File: "favicon.ico",
|
|
FileSystem: http.FS(public.EmbedFS),
|
|
}))
|
|
|
|
// Health checks
|
|
s.app.Get("/health", handlers.HealthCheck)
|
|
s.app.Get("/health/ready", handlers.ReadinessCheck)
|
|
|
|
// Prometheus metrics endpoint (conditional)
|
|
if s.config.Server.IsMetricsEnabled() {
|
|
s.app.Get("/metrics", adaptor.HTTPHandler(promhttp.Handler()))
|
|
}
|
|
|
|
// Server info (JSON version info)
|
|
s.app.Get("/server-info", handlers.ServerInfo(s.config))
|
|
s.app.Get("/status", handlers.ServerInfo(s.config)) // Alias for /server-info
|
|
s.app.Get("/api/info", handlers.ServerInfo(s.config)) // Alternative endpoint for server info
|
|
|
|
// Swagger documentation
|
|
s.app.Get("/swagger/*", swagger.HandlerDefault)
|
|
|
|
// API routes
|
|
api := s.app.Group("/osm/api")
|
|
|
|
// Login - always accessible
|
|
api.Post("/login", handlers.Login(s.config, s.options.NoAuth))
|
|
|
|
// Logout - always accessible (clears session cookie)
|
|
api.Post("/logout", handlers.Logout())
|
|
|
|
// Webhook trigger endpoint (no auth - uses webhook UUID + optional key)
|
|
if s.config.Server.EnableTriggerViaWebhook {
|
|
api.Get("/webhook-runs/:uuid/trigger", handlers.TriggerWebhookRun(s.config))
|
|
api.Post("/webhook-runs/:uuid/trigger", handlers.TriggerWebhookRun(s.config))
|
|
}
|
|
|
|
// Apply auth middleware conditionally
|
|
if !s.options.NoAuth {
|
|
if s.config.Server.EnabledAuthAPI {
|
|
// Support both API key and JWT auth
|
|
api.Use(middleware.CombinedAuth(s.config))
|
|
} else {
|
|
api.Use(middleware.JWTAuth(s.config))
|
|
}
|
|
}
|
|
|
|
// Webhook management (authenticated)
|
|
api.Get("/webhook-runs/list", handlers.ListWebhookRuns(s.config))
|
|
|
|
// Workflows
|
|
api.Get("/workflows", handlers.ListWorkflowsVerbose(s.config))
|
|
api.Get("/workflows/tags", handlers.GetAllWorkflowTags(s.config))
|
|
api.Post("/workflows/refresh", handlers.RefreshWorkflowIndex(s.config))
|
|
api.Get("/workflows/:name", handlers.GetWorkflowVerbose(s.config))
|
|
|
|
// Runs
|
|
api.Post("/runs", handlers.CreateRun(s.config, s.options.Master))
|
|
api.Get("/runs", handlers.ListRuns(s.config))
|
|
api.Get("/runs/:id", handlers.GetRun(s.config))
|
|
api.Delete("/runs/:id", handlers.CancelRun(s.config))
|
|
api.Post("/runs/:id/duplicate", handlers.DuplicateRun(s.config))
|
|
api.Post("/runs/:id/start", handlers.StartRun(s.config))
|
|
api.Get("/runs/:id/steps", handlers.GetRunSteps)
|
|
api.Get("/runs/:id/artifacts", handlers.GetRunArtifacts)
|
|
|
|
// Jobs (group of runs from same request)
|
|
api.Get("/jobs/:id", handlers.GetJobStatus(s.config))
|
|
|
|
// File uploads
|
|
api.Post("/upload-file", handlers.UploadFile(s.config))
|
|
api.Post("/workflow-upload", handlers.UploadWorkflow(s.config))
|
|
|
|
// Snapshots (legacy endpoint for backward compatibility)
|
|
api.Get("/snapshot-download/:workspace_name", handlers.SnapshotDownload(s.config))
|
|
|
|
// Snapshots (new endpoints)
|
|
api.Get("/snapshots", handlers.ListSnapshots(s.config))
|
|
api.Post("/snapshots/export", handlers.SnapshotExport(s.config))
|
|
api.Post("/snapshots/import", handlers.SnapshotImport(s.config))
|
|
api.Delete("/snapshots/:name", handlers.DeleteSnapshot(s.config))
|
|
|
|
// Workspaces
|
|
api.Get("/workspaces", handlers.ListWorkspaces(s.config))
|
|
api.Get("/workspace-names", handlers.ListWorkspaceNames(s.config))
|
|
|
|
// Artifacts
|
|
api.Get("/artifacts", handlers.ListArtifacts(s.config))
|
|
api.Get("/artifacts/:workspace_name", handlers.DownloadWorkspaceArtifact(s.config))
|
|
|
|
// Step Results (global listing)
|
|
api.Get("/step-results", handlers.ListStepResults(s.config))
|
|
|
|
// Assets
|
|
api.Get("/assets", handlers.ListAssets(s.config))
|
|
api.Get("/assets/diff", handlers.GetAssetDiff(s.config))
|
|
api.Get("/assets/diffs", handlers.ListAssetDiffSnapshots(s.config))
|
|
|
|
// Vulnerabilities
|
|
api.Get("/vulnerabilities", handlers.ListVulnerabilities(s.config))
|
|
api.Get("/vulnerabilities/diff", handlers.GetVulnerabilityDiff(s.config))
|
|
api.Get("/vulnerabilities/diffs", handlers.ListVulnDiffSnapshots(s.config))
|
|
api.Get("/vulnerabilities/summary", handlers.GetVulnerabilitySummary(s.config))
|
|
api.Get("/vulnerabilities/:id", handlers.GetVulnerability(s.config))
|
|
api.Post("/vulnerabilities", handlers.CreateVulnerability(s.config))
|
|
api.Delete("/vulnerabilities/:id", handlers.DeleteVulnerability(s.config))
|
|
|
|
// Stats
|
|
api.Get("/stats", handlers.GetSystemStats(s.config))
|
|
api.Get("/asset-stats", handlers.GetAssetStats(s.config))
|
|
|
|
// Install - registry info and installation endpoints
|
|
api.Get("/registry-info", handlers.GetRegistryInfo(s.config))
|
|
api.Post("/registry-install", handlers.RegistryInstall(s.config))
|
|
|
|
// Schedules
|
|
api.Get("/schedules", handlers.ListSchedules(s.config))
|
|
api.Post("/schedules", handlers.CreateSchedule(s.config, s.eventReceiver))
|
|
api.Get("/schedules/:id", handlers.GetSchedule(s.config))
|
|
api.Put("/schedules/:id", handlers.UpdateSchedule(s.config))
|
|
api.Delete("/schedules/:id", handlers.DeleteSchedule(s.config))
|
|
api.Post("/schedules/:id/enable", handlers.EnableSchedule(s.config))
|
|
api.Post("/schedules/:id/disable", handlers.DisableSchedule(s.config))
|
|
api.Post("/schedules/:id/trigger", handlers.TriggerSchedule(s.config))
|
|
|
|
// Event logs
|
|
api.Get("/event-logs", handlers.ListEventLogs(s.config))
|
|
|
|
// Database management
|
|
api.Get("/database/tables", handlers.ListDatabaseTables(s.config))
|
|
api.Post("/database/tables/:table/clear", handlers.ClearDatabaseTable(s.config))
|
|
|
|
// Functions
|
|
api.Post("/functions/eval", handlers.FunctionEval(s.config))
|
|
api.Get("/functions/list", handlers.FunctionList(s.config))
|
|
|
|
// Settings API
|
|
api.Get("/settings/yaml", handlers.GetSettingsYAML(s.config))
|
|
api.Get("/settings/yaml/", handlers.GetSettingsYAML(s.config))
|
|
api.Post("/settings/reload", handlers.ReloadConfig(s.hotConfig))
|
|
api.Get("/settings/status", handlers.GetConfigStatus(s.hotConfig))
|
|
|
|
// LLM endpoints (OpenAI-compatible)
|
|
api.Post("/llm/v1/chat/completions", handlers.LLMChat(s.config))
|
|
api.Post("/llm/v1/embeddings", handlers.LLMEmbedding(s.config))
|
|
|
|
// Agent ACP endpoints (OpenAI-compatible)
|
|
api.Post("/agent/chat/completions", handlers.AgentChat(s.config))
|
|
|
|
// Distributed endpoints (only available when running in master mode)
|
|
if s.options.Master != nil {
|
|
api.Get("/workers", handlers.ListWorkers(s.options.Master))
|
|
api.Get("/workers/:id", handlers.GetWorker(s.options.Master))
|
|
api.Get("/tasks", handlers.ListTasks(s.options.Master))
|
|
api.Get("/tasks/:id", handlers.GetTask(s.options.Master))
|
|
api.Post("/tasks", handlers.SubmitTask(s.options.Master))
|
|
}
|
|
|
|
// Cloud infrastructure endpoints
|
|
api.Get("/cloud/providers", handlers.ListCloudProviders(s.config))
|
|
api.Post("/cloud/providers/:name/validate", handlers.ValidateCloudProvider(s.config))
|
|
api.Post("/cloud/instances", handlers.CreateCloudInstances(s.config))
|
|
api.Get("/cloud/instances", handlers.ListCloudInstances(s.config))
|
|
api.Get("/cloud/instances/:id", handlers.GetCloudInstance(s.config))
|
|
api.Get("/cloud/instances/:id/status", handlers.GetCloudInstanceStatus(s.config))
|
|
api.Delete("/cloud/instances/:id", handlers.DestroyCloudInstance(s.config))
|
|
api.Post("/cloud/estimate", handlers.EstimateCloudCost(s.config))
|
|
|
|
// Event receiver endpoints (only available when event receiver is enabled)
|
|
if s.eventReceiver != nil {
|
|
api.Get("/event-receiver/status", handlers.GetEventReceiverStatus(s.eventReceiver))
|
|
api.Get("/event-receiver/workflows", handlers.ListEventReceiverWorkflows(s.eventReceiver))
|
|
api.Post("/events/emit", handlers.EmitEvent(s.eventReceiver))
|
|
}
|
|
|
|
// Serve workspace files under /ws/{workspace_prefix_key}/
|
|
// Allows direct access to run outputs in workspaces directory (no auth required)
|
|
if s.config.Server.WorkspacePrefixKey != "" && s.config.WorkspacesPath != "" {
|
|
wsPath := fmt.Sprintf("/ws/%s", s.config.Server.WorkspacePrefixKey)
|
|
s.app.Static(wsPath, s.config.WorkspacesPath, fiber.Static{
|
|
Browse: true, // Enable directory listing
|
|
})
|
|
}
|
|
|
|
// Handle HEAD requests for UI routes (filesystem middleware only handles GET)
|
|
// This middleware intercepts HEAD requests to non-API paths and returns 200 OK
|
|
s.app.Use(func(c *fiber.Ctx) error {
|
|
if c.Method() == fiber.MethodHead {
|
|
// For HEAD requests to non-API paths, return 200 OK
|
|
// This handles UI routes that the filesystem middleware would skip
|
|
if !strings.HasPrefix(c.Path(), "/osm/api") &&
|
|
!strings.HasPrefix(c.Path(), "/health") &&
|
|
!strings.HasPrefix(c.Path(), "/metrics") &&
|
|
!strings.HasPrefix(c.Path(), "/swagger") {
|
|
c.Set("Content-Type", "text/html; charset=utf-8")
|
|
return c.SendStatus(fiber.StatusOK)
|
|
}
|
|
}
|
|
return c.Next()
|
|
})
|
|
|
|
// Serve UI at root /
|
|
// Priority: external UI path > embedded UI
|
|
if s.config.UIPath != "" {
|
|
if _, err := os.Stat(s.config.UIPath); err == nil {
|
|
s.app.Use("/", filesystem.New(filesystem.Config{
|
|
Root: http.Dir(s.config.UIPath),
|
|
Index: "index.html",
|
|
Browse: false,
|
|
NotFoundFile: "index.html", // SPA fallback: serve index.html for client-side routes
|
|
}))
|
|
} else {
|
|
// Fallback to embedded UI if external path doesn't exist
|
|
s.serveEmbeddedUI()
|
|
}
|
|
} else {
|
|
// Use embedded UI when no external path configured
|
|
s.serveEmbeddedUI()
|
|
}
|
|
}
|
|
|
|
// serveEmbeddedUI serves the embedded UI files at root /
|
|
func (s *Server) serveEmbeddedUI() {
|
|
uiFS, err := public.GetUIFS()
|
|
if err != nil {
|
|
return
|
|
}
|
|
s.app.Use("/", filesystem.New(filesystem.Config{
|
|
Root: http.FS(uiFS),
|
|
Index: "index.html",
|
|
Browse: false,
|
|
PathPrefix: "",
|
|
NotFoundFile: "index.html", // SPA fallback: serve index.html for client-side routes
|
|
}))
|
|
}
|
|
|
|
// StartEventReceiver starts the event receiver and scheduler.
|
|
// This should be called before PrintStartupInfo to ensure triggers are registered.
|
|
func (s *Server) StartEventReceiver() {
|
|
if s.eventReceiver != nil {
|
|
if err := s.eventReceiver.Start(context.Background()); err != nil {
|
|
oslogger.Get().Warn("Failed to start event receiver", zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
// StartListener starts only the HTTP listener (event receiver already started).
|
|
func (s *Server) StartListener(addr string) error {
|
|
return s.app.Listen(addr)
|
|
}
|
|
|
|
// Start starts the server (event receiver + HTTP listener).
|
|
// For finer control, use StartEventReceiver() + StartListener() instead.
|
|
func (s *Server) Start(addr string) error {
|
|
s.StartEventReceiver()
|
|
return s.StartListener(addr)
|
|
}
|
|
|
|
// Shutdown gracefully shuts down the server
|
|
func (s *Server) Shutdown() error {
|
|
// Stop event receiver if running
|
|
if s.eventReceiver != nil {
|
|
_ = s.eventReceiver.Stop()
|
|
}
|
|
// Stop hot config watcher if enabled
|
|
if s.hotConfig != nil {
|
|
_ = s.hotConfig.Stop()
|
|
}
|
|
return s.app.Shutdown()
|
|
}
|
|
|
|
// ShutdownWithContext gracefully shuts down the server with a context for timeout
|
|
func (s *Server) ShutdownWithContext(ctx context.Context) error {
|
|
// Stop event receiver if running
|
|
if s.eventReceiver != nil {
|
|
_ = s.eventReceiver.Stop()
|
|
}
|
|
// Stop hot config watcher if enabled
|
|
if s.hotConfig != nil {
|
|
_ = s.hotConfig.Stop()
|
|
}
|
|
return s.app.ShutdownWithContext(ctx)
|
|
}
|
|
|
|
// GetConfigProvider returns the config provider used by this server.
|
|
// This can be used by handlers that need access to fresh configuration.
|
|
func (s *Server) GetConfigProvider() handlers.ConfigProvider {
|
|
return s.configProvider
|
|
}
|
|
|
|
// GetHotConfig returns the hot reloadable config, or nil if hot reload is disabled.
|
|
func (s *Server) GetHotConfig() *config.HotReloadableConfig {
|
|
return s.hotConfig
|
|
}
|
|
|
|
// IsHotReloadEnabled returns true if config hot reload is enabled.
|
|
func (s *Server) IsHotReloadEnabled() bool {
|
|
return s.hotConfig != nil
|
|
}
|
|
|
|
// errorHandler handles errors globally
|
|
func errorHandler(c *fiber.Ctx, err error) error {
|
|
code := fiber.StatusInternalServerError
|
|
|
|
if e, ok := err.(*fiber.Error); ok {
|
|
code = e.Code
|
|
}
|
|
|
|
// Custom handling for 403 Forbidden
|
|
if code == fiber.StatusForbidden {
|
|
path := c.Path()
|
|
// Redirect to root for UI routes that return 403
|
|
if path == "/login" ||
|
|
strings.HasPrefix(path, "/events") ||
|
|
strings.HasPrefix(path, "/inventory") ||
|
|
strings.HasPrefix(path, "/llm") ||
|
|
strings.HasPrefix(path, "/registry") ||
|
|
strings.HasPrefix(path, "/scans") ||
|
|
strings.HasPrefix(path, "/schedules") ||
|
|
strings.HasPrefix(path, "/utilities") ||
|
|
strings.HasPrefix(path, "/vuln") ||
|
|
strings.HasPrefix(path, "/workflow") {
|
|
return c.Redirect("/", fiber.StatusFound)
|
|
}
|
|
return c.Status(code).JSON(fiber.Map{
|
|
"error": true,
|
|
"message": "Oh dear! It seems you've wandered off the path. If you'd like to see the UI page, please pop back root route at /",
|
|
})
|
|
}
|
|
|
|
return c.Status(code).JSON(fiber.Map{
|
|
"error": true,
|
|
"message": err.Error(),
|
|
})
|
|
}
|
|
|
|
// startMemoryMetricsCollector starts a background goroutine that periodically
|
|
// collects memory statistics and updates the Prometheus metrics.
|
|
func startMemoryMetricsCollector(ctx context.Context, interval time.Duration) {
|
|
go func() {
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
|
|
var m runtime.MemStats
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
runtime.ReadMemStats(&m)
|
|
metrics.UpdateMemoryMetrics(&m)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// PrintStartupInfo prints colorized server startup information to stdout.
|
|
// This replaces Fiber's default banner with custom messaging.
|
|
func (s *Server) PrintStartupInfo(addr string) {
|
|
p := terminal.NewPrinter()
|
|
|
|
// Banner line
|
|
fmt.Printf("%s Initiating Osmedeus %s - Crafted with %s by %s\n",
|
|
terminal.Yellow(terminal.SymbolLightning),
|
|
terminal.Cyan(core.VERSION),
|
|
terminal.Red("<3"),
|
|
terminal.Yellow(core.AUTHOR))
|
|
|
|
// Starting server - show both addresses when binding to 0.0.0.0
|
|
if port, found := strings.CutPrefix(addr, "0.0.0.0:"); found {
|
|
p.Info("Starting Osmedeus server %s and %s",
|
|
terminal.Cyan("http://"+addr),
|
|
terminal.Cyan("http://localhost:"+port))
|
|
} else {
|
|
p.Info("Starting Osmedeus server %s", terminal.Cyan("http://"+addr))
|
|
}
|
|
|
|
// Master mode info
|
|
if s.options.Master != nil {
|
|
p.Info("Master mode %s", terminal.Cyan("enabled"))
|
|
p.Info("Redis connected at %s",
|
|
terminal.Cyan(fmt.Sprintf("%s:%d", s.config.Redis.Host, s.config.Redis.Port)))
|
|
}
|
|
|
|
// Database info
|
|
p.Info("Database initialized %s", terminal.Cyan(s.config.Database.DBEngine))
|
|
|
|
// Hot reload status
|
|
if s.hotConfig != nil {
|
|
p.Info("Started config hot reload watcher %s", terminal.Yellow(s.hotConfig.GetConfigPath()))
|
|
}
|
|
|
|
// Workflows path (always show, in yellow)
|
|
p.Info("Workflows loaded from %s", terminal.Yellow(s.config.GetWorkflowsDir()))
|
|
|
|
// Event receiver info with detailed triggers
|
|
if s.eventReceiver != nil {
|
|
triggers := s.eventReceiver.GetRegisteredTriggersInfo()
|
|
|
|
if len(triggers) > 0 {
|
|
p.Info("Event receiver initialized")
|
|
|
|
// Show each registered trigger with tree formatting
|
|
for i, t := range triggers {
|
|
prefix := "├─"
|
|
if i == len(triggers)-1 {
|
|
prefix = "└─"
|
|
}
|
|
|
|
// Format trigger details based on type
|
|
var detail string
|
|
if t.Topic != "" {
|
|
detail = fmt.Sprintf("%s: %s", terminal.Blue("event"), terminal.Blue(t.Topic))
|
|
} else {
|
|
detail = terminal.Blue(t.Type)
|
|
}
|
|
|
|
fmt.Printf(" %s Registered trigger: %s (%s)\n",
|
|
prefix,
|
|
terminal.Yellow(t.WorkflowName),
|
|
detail)
|
|
}
|
|
|
|
// Scheduler started message
|
|
p.Info("Scheduler started with %s triggers", terminal.Cyan(fmt.Sprintf("%d", len(triggers))))
|
|
} else {
|
|
p.Info("Event receiver initialized %s", terminal.Gray("(no event triggers)"))
|
|
p.Info("Scheduler started")
|
|
}
|
|
} else if !s.options.EnableScheduler {
|
|
// Scheduler explicitly disabled
|
|
p.Info("Scheduler %s", terminal.Gray("(disabled)"))
|
|
} else {
|
|
// Event receiver disabled but scheduler not explicitly disabled
|
|
p.Info("Scheduler started")
|
|
}
|
|
|
|
// Print credentials tip
|
|
fmt.Println()
|
|
fmt.Printf("💡 %s\n", terminal.Gray("Tip: View your login credentials with:"))
|
|
fmt.Printf(" %s\n", terminal.Cyan("osmedeus config view server.username"))
|
|
fmt.Printf(" %s\n", terminal.Cyan("osmedeus config view server.password"))
|
|
}
|