mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-20 22:52:32 +02:00
334 lines
10 KiB
Go
334 lines
10 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
|
|
"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/prometheus/client_golang/prometheus/promhttp"
|
|
"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/pkg/server/handlers"
|
|
"github.com/j3ssie/osmedeus/v5/pkg/server/middleware"
|
|
"github.com/j3ssie/osmedeus/v5/public"
|
|
"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)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Server represents the web server
|
|
type Server struct {
|
|
app *fiber.App
|
|
config *config.Config
|
|
options *Options
|
|
}
|
|
|
|
// 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().Info("Database initialized", zap.String("engine", cfg.Database.DBEngine))
|
|
|
|
// 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
|
|
|
|
// 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,
|
|
})
|
|
|
|
// 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,
|
|
})
|
|
|
|
// Apply middleware
|
|
app.Use(recover.New())
|
|
app.Use(logger.New())
|
|
app.Use(cors.New(cors.Config{
|
|
AllowOrigins: "*",
|
|
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
|
|
AllowHeaders: "Origin,Content-Type,Accept,Authorization",
|
|
}))
|
|
|
|
// Apply Prometheus metrics middleware
|
|
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,
|
|
}
|
|
|
|
// Setup routes
|
|
s.setupRoutes()
|
|
|
|
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
|
|
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))
|
|
|
|
// Apply auth middleware conditionally
|
|
if s.config.Server.EnabledAuthAPI {
|
|
api.Use(middleware.APIKeyAuth(s.config))
|
|
} else if !s.options.NoAuth {
|
|
api.Use(middleware.JWTAuth(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))
|
|
api.Get("/runs", handlers.ListRuns(s.config))
|
|
api.Get("/runs/:id", handlers.GetRun(s.config))
|
|
api.Delete("/runs/:id", handlers.CancelRun(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))
|
|
|
|
// Assets
|
|
api.Get("/assets", handlers.ListAssets(s.config))
|
|
|
|
// Vulnerabilities
|
|
api.Get("/vulnerabilities", handlers.ListVulnerabilities(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))
|
|
|
|
// 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))
|
|
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))
|
|
|
|
// 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))
|
|
|
|
// LLM endpoints (OpenAI-compatible)
|
|
api.Post("/llm/v1/chat/completions", handlers.LLMChat(s.config))
|
|
api.Post("/llm/v1/embeddings", handlers.LLMEmbedding(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))
|
|
}
|
|
|
|
// 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
|
|
})
|
|
}
|
|
|
|
// 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.Static("/", s.config.UIPath, fiber.Static{
|
|
Index: "index.html",
|
|
})
|
|
} 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: "",
|
|
}))
|
|
}
|
|
|
|
// Start starts the server
|
|
func (s *Server) Start(addr string) error {
|
|
return s.app.Listen(addr)
|
|
}
|
|
|
|
// Shutdown gracefully shuts down the server
|
|
func (s *Server) Shutdown() error {
|
|
return s.app.Shutdown()
|
|
}
|
|
|
|
// ShutdownWithContext gracefully shuts down the server with a context for timeout
|
|
func (s *Server) ShutdownWithContext(ctx context.Context) error {
|
|
return s.app.ShutdownWithContext(ctx)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
return c.Status(code).JSON(fiber.Map{
|
|
"error": true,
|
|
"message": err.Error(),
|
|
})
|
|
}
|