164 lines
4.7 KiB
Go
164 lines
4.7 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/api/handlers"
|
|
"github.com/Wikid82/charon/backend/internal/config"
|
|
"github.com/Wikid82/charon/backend/internal/logger"
|
|
)
|
|
|
|
// EmergencyServer provides a minimal HTTP server for emergency operations.
|
|
// This server runs on a separate port with minimal security for failsafe access.
|
|
//
|
|
// Security Philosophy:
|
|
// - Separate port bypasses Caddy/CrowdSec/WAF entirely
|
|
// - Optional Basic Auth (configurable via env)
|
|
// - Should ONLY be accessible via VPN/SSH tunnel
|
|
// - Default bind to localhost (127.0.0.1) for safety
|
|
//
|
|
// Use Cases:
|
|
// - Layer 7 reverse proxy blocking requests (CrowdSec bouncer at Caddy)
|
|
// - Caddy itself is down or misconfigured
|
|
// - Emergency access when main application port is unreachable
|
|
type EmergencyServer struct {
|
|
server *http.Server
|
|
listener net.Listener
|
|
db *gorm.DB
|
|
cfg config.EmergencyConfig
|
|
}
|
|
|
|
// NewEmergencyServer creates a new emergency server instance
|
|
func NewEmergencyServer(db *gorm.DB, cfg config.EmergencyConfig) *EmergencyServer {
|
|
return &EmergencyServer{
|
|
db: db,
|
|
cfg: cfg,
|
|
}
|
|
}
|
|
|
|
// Start initializes and starts the emergency server
|
|
func (s *EmergencyServer) Start() error {
|
|
if !s.cfg.Enabled {
|
|
logger.Log().Info("Emergency server disabled (CHARON_EMERGENCY_SERVER_ENABLED=false)")
|
|
return nil
|
|
}
|
|
|
|
// Security warning if no authentication configured
|
|
if s.cfg.BasicAuthUsername == "" || s.cfg.BasicAuthPassword == "" {
|
|
logger.Log().Warn("⚠️ SECURITY WARNING: Emergency server has NO authentication configured")
|
|
logger.Log().Warn("⚠️ Ensure port is accessible ONLY via VPN/SSH tunnel")
|
|
logger.Log().Warn("⚠️ Set CHARON_EMERGENCY_USERNAME and CHARON_EMERGENCY_PASSWORD")
|
|
}
|
|
|
|
// Configure Gin for minimal logging (not production mode to preserve logs)
|
|
router := gin.New()
|
|
|
|
// Middleware 1: Recovery (panic handler)
|
|
router.Use(gin.Recovery())
|
|
|
|
// Middleware 2: Simple request logging (minimal)
|
|
router.Use(func(c *gin.Context) {
|
|
start := time.Now()
|
|
path := c.Request.URL.Path
|
|
method := c.Request.Method
|
|
|
|
c.Next()
|
|
|
|
latency := time.Since(start).Milliseconds()
|
|
status := c.Writer.Status()
|
|
|
|
logger.Log().WithFields(map[string]interface{}{
|
|
"server": "emergency",
|
|
"method": method,
|
|
"path": path,
|
|
"status": status,
|
|
"latency": fmt.Sprintf("%dms", latency),
|
|
"ip": c.ClientIP(),
|
|
}).Info("Emergency server request")
|
|
})
|
|
|
|
// Middleware 3: Basic Auth (if configured)
|
|
if s.cfg.BasicAuthUsername != "" && s.cfg.BasicAuthPassword != "" {
|
|
accounts := gin.Accounts{
|
|
s.cfg.BasicAuthUsername: s.cfg.BasicAuthPassword,
|
|
}
|
|
router.Use(gin.BasicAuth(accounts))
|
|
logger.Log().WithField("username", s.cfg.BasicAuthUsername).Info("Emergency server Basic Auth enabled")
|
|
}
|
|
|
|
// Emergency endpoints only
|
|
emergencyHandler := handlers.NewEmergencyHandler(s.db)
|
|
|
|
// POST /emergency/security-reset - Disable all security modules
|
|
router.POST("/emergency/security-reset", emergencyHandler.SecurityReset)
|
|
|
|
// GET /health - Health check endpoint
|
|
router.GET("/health", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"status": "ok",
|
|
"server": "emergency",
|
|
"time": time.Now().UTC().Format(time.RFC3339),
|
|
})
|
|
})
|
|
|
|
// Create HTTP server with sensible timeouts
|
|
s.server = &http.Server{
|
|
Handler: router,
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
IdleTimeout: 30 * time.Second,
|
|
}
|
|
|
|
// Create listener (this allows us to get the actual port when using :0 for testing)
|
|
listener, err := net.Listen("tcp", s.cfg.BindAddress)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create listener: %w", err)
|
|
}
|
|
s.listener = listener
|
|
|
|
// Start server in goroutine
|
|
go func() {
|
|
logger.Log().WithFields(map[string]interface{}{
|
|
"address": listener.Addr().String(),
|
|
"auth": s.cfg.BasicAuthUsername != "",
|
|
"endpoint": "/emergency/security-reset",
|
|
}).Info("Starting emergency server (Tier 2 break glass)")
|
|
|
|
if err := s.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
|
logger.Log().WithError(err).Error("Emergency server failed")
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop gracefully shuts down the emergency server
|
|
func (s *EmergencyServer) Stop(ctx context.Context) error {
|
|
if s.server == nil {
|
|
return nil
|
|
}
|
|
|
|
logger.Log().Info("Stopping emergency server")
|
|
if err := s.server.Shutdown(ctx); err != nil {
|
|
return fmt.Errorf("emergency server shutdown: %w", err)
|
|
}
|
|
|
|
logger.Log().Info("Emergency server stopped")
|
|
return nil
|
|
}
|
|
|
|
// GetAddr returns the actual bind address (useful for tests with :0)
|
|
func (s *EmergencyServer) GetAddr() string {
|
|
if s.listener == nil {
|
|
return ""
|
|
}
|
|
return s.listener.Addr().String()
|
|
}
|