8cf05a1576
Add a secret-masking slog.Handler that automatically replaces registered passwords with "***" in all log output. Secrets are registered per-scan when a discovery request arrives and unregistered when it completes. This approach masks credentials everywhere they appear in logs — URL userinfo, query parameters, path segments, and Go HTTP error messages — without modifying any business logic in scanner, builder, tester, or ONVIF components. API responses are unaffected and still return full URLs with credentials for frontend use.
168 lines
4.8 KiB
Go
168 lines
4.8 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/eduard256/Strix/internal/api/handlers"
|
|
"github.com/eduard256/Strix/internal/camera/database"
|
|
"github.com/eduard256/Strix/internal/camera/discovery"
|
|
"github.com/eduard256/Strix/internal/camera/stream"
|
|
"github.com/eduard256/Strix/internal/config"
|
|
logutil "github.com/eduard256/Strix/internal/utils/logger"
|
|
"github.com/eduard256/Strix/pkg/sse"
|
|
)
|
|
|
|
// Server represents the API server
|
|
type Server struct {
|
|
router chi.Router
|
|
config *config.Config
|
|
loader *database.Loader
|
|
searchEngine *database.SearchEngine
|
|
scanner *discovery.Scanner
|
|
probeService *discovery.ProbeService
|
|
sseServer *sse.Server
|
|
secrets *logutil.SecretStore
|
|
logger interface{ Debug(string, ...any); Error(string, error, ...any); Info(string, ...any) }
|
|
}
|
|
|
|
// NewServer creates a new API server
|
|
func NewServer(
|
|
cfg *config.Config,
|
|
secrets *logutil.SecretStore,
|
|
logger interface{ Debug(string, ...any); Error(string, error, ...any); Info(string, ...any) },
|
|
) (*Server, error) {
|
|
// Initialize database loader
|
|
loader := database.NewLoader(
|
|
cfg.Database.BrandsPath,
|
|
cfg.Database.PatternsPath,
|
|
cfg.Database.ParametersPath,
|
|
logger,
|
|
)
|
|
|
|
// Load query parameters for URL builder
|
|
queryParams, err := loader.LoadQueryParameters()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Initialize search engine
|
|
searchEngine := database.NewSearchEngine(loader, logger)
|
|
|
|
// Initialize stream components
|
|
builder := stream.NewBuilder(queryParams, logger)
|
|
tester := stream.NewTester(cfg.Scanner.FFProbeTimeout, logger)
|
|
|
|
// Initialize ONVIF discovery
|
|
onvif := discovery.NewONVIFDiscovery(logger)
|
|
|
|
// Initialize scanner
|
|
scannerConfig := discovery.ScannerConfig{
|
|
WorkerPoolSize: cfg.Scanner.WorkerPoolSize,
|
|
DefaultTimeout: cfg.Scanner.DefaultTimeout,
|
|
MaxStreams: cfg.Scanner.MaxStreams,
|
|
ModelSearchLimit: cfg.Scanner.ModelSearchLimit,
|
|
FFProbeTimeout: cfg.Scanner.FFProbeTimeout,
|
|
}
|
|
|
|
scanner := discovery.NewScanner(
|
|
loader,
|
|
searchEngine,
|
|
builder,
|
|
tester,
|
|
onvif,
|
|
scannerConfig,
|
|
logger,
|
|
)
|
|
|
|
// Initialize SSE server
|
|
sseServer := sse.NewServer(logger)
|
|
|
|
// Initialize OUI database for vendor identification
|
|
ouiDB := discovery.NewOUIDatabase()
|
|
if err := ouiDB.LoadFromFile(cfg.Database.OUIPath); err != nil {
|
|
logger.Error("failed to load OUI database, vendor lookup will be unavailable", err)
|
|
} else {
|
|
logger.Info("OUI database loaded", "entries", ouiDB.Size())
|
|
}
|
|
|
|
// Initialize ProbeService with all probers
|
|
probers := []discovery.Prober{
|
|
&discovery.DNSProber{},
|
|
discovery.NewARPProber(ouiDB),
|
|
&discovery.MDNSProber{},
|
|
&discovery.HTTPProber{},
|
|
}
|
|
probeService := discovery.NewProbeService(probers, logger)
|
|
|
|
// Create server
|
|
server := &Server{
|
|
router: chi.NewRouter(),
|
|
config: cfg,
|
|
loader: loader,
|
|
searchEngine: searchEngine,
|
|
scanner: scanner,
|
|
probeService: probeService,
|
|
sseServer: sseServer,
|
|
secrets: secrets,
|
|
logger: logger,
|
|
}
|
|
|
|
// Setup routes
|
|
server.setupRoutes()
|
|
|
|
return server, nil
|
|
}
|
|
|
|
// setupRoutes configures all routes and middleware
|
|
func (s *Server) setupRoutes() {
|
|
// Global middleware
|
|
s.router.Use(middleware.RequestID)
|
|
s.router.Use(middleware.RealIP)
|
|
s.router.Use(middleware.Logger)
|
|
s.router.Use(middleware.Recoverer)
|
|
// Note: No global timeout middleware - endpoints use context-based timeouts from request parameters
|
|
|
|
// CORS middleware
|
|
s.router.Use(func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type, X-Request-ID")
|
|
w.Header().Set("Access-Control-Max-Age", "3600")
|
|
|
|
if r.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
})
|
|
|
|
// API routes (mounted at /api/v1 in main.go)
|
|
// Health check (GET + HEAD for Docker/CasaOS healthcheck compatibility)
|
|
healthHandler := handlers.NewHealthHandler(s.config.Version, s.logger).ServeHTTP
|
|
s.router.Get("/health", healthHandler)
|
|
s.router.Head("/health", healthHandler)
|
|
|
|
// Camera search
|
|
s.router.Post("/cameras/search", handlers.NewSearchHandler(s.searchEngine, s.logger).ServeHTTP)
|
|
|
|
// Stream discovery (SSE)
|
|
s.router.Post("/streams/discover", handlers.NewDiscoverHandler(s.scanner, s.sseServer, s.secrets, s.logger).ServeHTTP)
|
|
|
|
// Device probe (ping + DNS + ARP/OUI + mDNS)
|
|
s.router.Get("/probe", handlers.NewProbeHandler(s.probeService, s.logger).ServeHTTP)
|
|
}
|
|
|
|
// ServeHTTP implements http.Handler
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
s.router.ServeHTTP(w, r)
|
|
}
|
|
|
|
// GetRouter returns the chi router
|
|
func (s *Server) GetRouter() chi.Router {
|
|
return s.router
|
|
} |