diff --git a/.gitignore b/.gitignore index 964f705..302c88f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Binaries bin/ strix +main *.exe *.exe~ *.dll @@ -37,4 +38,9 @@ Thumbs.db # Temporary files tmp/ -temp/ \ No newline at end of file +temp/ +*.dump +*_output.txt + +# Configuration (user-specific) +strix.yaml \ No newline at end of file diff --git a/README.md b/README.md index 48b8e51..cea1815 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,9 @@ make build # Run the application make run + +# The server will start on http://localhost:4567 +# Open your browser and navigate to http://localhost:4567 ``` ## šŸ“” API Endpoints @@ -105,15 +108,54 @@ strix/ ## šŸ› ļø Configuration -Environment variables: +Strix can be configured via `strix.yaml` file or environment variables. + +### Configuration File (strix.yaml) + +Create a `strix.yaml` file in the same directory as the binary: + +```yaml +# API Server Configuration +api: + listen: ":4567" # Format: ":port" or "host:port" +``` + +Examples: +```yaml +api: + listen: ":4567" # All interfaces, port 4567 (default) + # listen: "127.0.0.1:4567" # Localhost only + # listen: ":8080" # Custom port +``` + +### Environment Variables + +Environment variables override config file values: ```bash -STRIX_HOST=0.0.0.0 # Server host (default: 0.0.0.0) -STRIX_PORT=8080 # Server port (default: 8080) +STRIX_API_LISTEN=":4567" # Server listen address (overrides strix.yaml) STRIX_LOG_LEVEL=info # Log level: debug, info, warn, error STRIX_LOG_FORMAT=json # Log format: json, text ``` +### Configuration Priority + +1. **Environment variable** `STRIX_API_LISTEN` (highest priority) +2. **Config file** `strix.yaml` +3. **Default value** `:4567` (lowest priority) + +### Quick Start with Custom Port + +```bash +# Using environment variable +STRIX_API_LISTEN=":8080" ./strix + +# Or using config file +cp strix.yaml.example strix.yaml +# Edit strix.yaml, then: +./strix +``` + ## šŸ“Š Camera Database The system includes a comprehensive database of camera models: diff --git a/bubble_test_output.txt b/bubble_test_output.txt deleted file mode 100644 index 9c952b0..0000000 --- a/bubble_test_output.txt +++ /dev/null @@ -1,57 +0,0 @@ - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed - 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 100 324 0 157 100 167 209 222 --:--:-- --:--:-- --:--:-- 431 100 324 0 157 100 167 89 95 0:00:01 0:00:01 --:--:-- 184 100 324 0 157 100 167 57 60 0:00:02 0:00:02 --:--:-- 117 100 1075 0 908 100 167 242 44 0:00:03 0:00:03 --:--:-- 286 100 1752 0 1585 100 167 296 31 0:00:05 0:00:05 --:--:-- 327 100 1752 0 1585 100 167 249 26 0:00:06 0:00:06 --:--:-- 255 100 1816 0 1649 100 167 244 24 0:00:06 0:00:06 --:--:-- 298 100 1816 0 1649 100 167 212 21 0:00:07 0:00:07 --:--:-- 298 100 2163 0 1996 100 167 228 19 0:00:08 0:00:08 --:--:-- 218 100 2163 0 1996 100 167 204 17 0:00:09 0:00:09 --:--:-- 93 100 2227 0 2060 100 167 191 15 0:00:11 0:00:10 0:00:01 107 100 2227 0 2060 100 167 175 14 0:00:11 0:00:11 --:--:-- 82 100 2291 0 2124 100 167 166 13 0:00:12 0:00:12 --:--:-- 95 100 2291 0 2124 100 167 154 12 0:00:13 0:00:13 --:--:-- 25 100 2291 0 2124 100 167 143 11 0:00:15 0:00:14 0:00:01 25 100 2353 0 2186 100 167 138 10 0:00:16 0:00:15 0:00:01 25 100 2353 0 2186 100 167 130 9 0:00:18 0:00:16 0:00:02 25 100 2353 0 2186 100 167 123 9 0:00:18 0:00:17 0:00:01 12 100 2353 0 2186 100 167 116 8 0:00:20 0:00:18 0:00:02 12 100 2353 0 2186 100 167 110 8 0:00:20 0:00:19 0:00:01 12 100 2353 0 2186 100 167 105 8 0:00:20 0:00:20 --:--:-- 0 100 2353 0 2186 100 167 100 7 0:00:23 0:00:21 0:00:02 0 100 2353 0 2186 100 167 96 7 0:00:23 0:00:22 0:00:01 0 100 2353 0 2186 100 167 92 7 0:00:23 0:00:23 --:--:-- 0 100 2353 0 2186 100 167 88 6 0:00:27 0:00:24 0:00:03 0 100 2353 0 2186 100 167 84 6 0:00:27 0:00:25 0:00:02 0 100 2353 0 2186 100 167 81 6 0:00:27 0:00:26 0:00:01 0 100 2353 0 2186 100 167 78 6 0:00:27 0:00:27 --:--:-- 0 100 2353 0 2186 100 167 76 5 0:00:33 0:00:28 0:00:05 0 100 2353 0 2186 100 167 73 5 0:00:33 0:00:29 0:00:04 0 100 2353 0 2186 100 167 71 5 0:00:33 0:00:30 0:00:03 0 100 2353 0 2186 100 167 68 5 0:00:33 0:00:31 0:00:02 0 100 2353 0 2186 100 167 66 5 0:00:33 0:00:32 0:00:01 0 100 2353 0 2186 100 167 64 4 0:00:41 0:00:33 0:00:08 0 100 2353 0 2186 100 167 64 4 0:00:41 0:00:33 0:00:08 0 -curl: (18) transfer closed with outstanding read data remaining -event: scan_started -data: {"max_streams":5,"model":"NVR","target":"10.0.20.110","timeout":60} - -event: progress -data: {"tested":0,"found":0,"remaining":959} - -event: stream_found -data: {"stream":{"url":"http://admin:5f8a5b7s9m@10.0.20.110/bubble/live?ch=0\u0026stream=1","type":"BUBBLE","protocol":"http","port":0,"working":true,"has_audio":false,"test_time_ms":11294107,"metadata":{"content_type":"video/bubble","stream_type":"main"}}} - -event: progress -data: {"tested":226,"found":1,"remaining":733} - -event: stream_found -data: {"stream":{"url":"http://admin:5f8a5b7s9m@10.0.20.110/bubble/live?ch=0\u0026stream=0","type":"BUBBLE","protocol":"http","port":0,"working":true,"has_audio":false,"test_time_ms":212128072,"metadata":{"content_type":"video/bubble","stream_type":"main"}}} - -event: progress -data: {"tested":232,"found":2,"remaining":727} - -event: progress -data: {"tested":323,"found":2,"remaining":636} - -event: stream_found -data: {"stream":{"url":"http://admin:5f8a5b7s9m@10.0.20.110/cgi-bin/snapshot.cgi?chn=0\u0026p=5f8a5b7s9m\u0026u=admin","type":"JPEG","protocol":"http","port":0,"working":true,"has_audio":false,"test_time_ms":1692728991,"metadata":{"content_type":"image/jpeg"}}} - -event: progress -data: {"tested":334,"found":3,"remaining":625} - -event: stream_found -data: {"stream":{"url":"http://10.0.20.110/cgi-bin/snapshot.cgi?chn=0\u0026p=5f8a5b7s9m\u0026u=admin","type":"JPEG","protocol":"http","port":0,"working":true,"has_audio":false,"test_time_ms":2027069571,"metadata":{"content_type":"image/jpeg"}}} - -event: progress -data: {"tested":357,"found":4,"remaining":602} - -event: progress -data: {"tested":457,"found":4,"remaining":502} - -event: stream_found -data: {"stream":{"url":"http://admin:5f8a5b7s9m@10.0.20.110/cgi-bin/snapshot.cgi?chn=8\u0026p=5f8a5b7s9m\u0026u=admin","type":"JPEG","protocol":"http","port":0,"working":true,"has_audio":false,"test_time_ms":1236955428,"metadata":{"content_type":"image/jpeg"}}} - -event: progress -data: {"tested":631,"found":5,"remaining":328} - -event: progress -data: {"tested":688,"found":5,"remaining":271} - -event: progress -data: {"tested":828,"found":5,"remaining":131} - -event: progress -data: {"tested":950,"found":5,"remaining":9} - -curl: (3) URL using bad/illegal format or missing URL -curl: (3) URL using bad/illegal format or missing URL diff --git a/cmd/strix/main.go b/cmd/strix/main.go index b4a565e..eff8097 100644 --- a/cmd/strix/main.go +++ b/cmd/strix/main.go @@ -14,6 +14,7 @@ import ( "github.com/eduard256/Strix/internal/config" "github.com/eduard256/Strix/internal/utils/logger" "github.com/eduard256/Strix/webui" + "github.com/go-chi/chi/v5" ) const ( @@ -52,8 +53,7 @@ func main() { log.Info("starting Strix", slog.String("version", Version), slog.String("go_version", os.Getenv("GO_VERSION")), - slog.String("host", cfg.Server.Host), - slog.String("port", cfg.Server.Port), + slog.String("listen", cfg.Server.Listen), ) // Check if ffprobe is available @@ -71,51 +71,39 @@ func main() { // Create Web UI server webuiServer := webui.NewServer(log) - // Create API HTTP server + // Create unified router combining API and WebUI + unifiedRouter := chi.NewRouter() + + // Mount API routes at /api/v1/* + unifiedRouter.Mount("/api/v1", apiServer.GetRouter()) + + // Mount WebUI routes at /* (serves everything else including root) + unifiedRouter.Mount("/", webuiServer.GetRouter()) + + // Create unified HTTP server httpServer := &http.Server{ - Addr: fmt.Sprintf("%s:%s", cfg.Server.Host, cfg.Server.Port), - Handler: apiServer, + Addr: cfg.Server.Listen, + Handler: unifiedRouter, ReadTimeout: cfg.Server.ReadTimeout, WriteTimeout: cfg.Server.WriteTimeout, IdleTimeout: 120 * time.Second, } - // Create Web UI HTTP server - webuiHTTPServer := &http.Server{ - Addr: fmt.Sprintf("%s:4567", cfg.Server.Host), - Handler: webuiServer, - ReadTimeout: cfg.Server.ReadTimeout, - WriteTimeout: cfg.Server.WriteTimeout, - IdleTimeout: 120 * time.Second, - } - - // Start API server in goroutine + // Start server in goroutine go func() { - log.Info("API server starting", + log.Info("server starting", slog.String("address", httpServer.Addr), slog.String("api_version", "v1"), ) if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Error("API server failed", err) - os.Exit(1) - } - }() - - // Start Web UI server in goroutine - go func() { - log.Info("Web UI server starting", - slog.String("address", webuiHTTPServer.Addr), - ) - - if err := webuiHTTPServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Error("Web UI server failed", err) + log.Error("server failed", err) os.Exit(1) } }() // Print endpoints - printEndpoints(cfg.Server.Host, cfg.Server.Port) + printEndpoints(cfg.Server.Listen) // Wait for interrupt signal quit := make(chan os.Signal, 1) @@ -128,19 +116,13 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - // Shutdown API server + // Shutdown server if err := httpServer.Shutdown(ctx); err != nil { - log.Error("API server shutdown failed", err) + log.Error("server shutdown failed", err) os.Exit(1) } - // Shutdown Web UI server - if err := webuiHTTPServer.Shutdown(ctx); err != nil { - log.Error("Web UI server shutdown failed", err) - os.Exit(1) - } - - log.Info("servers stopped gracefully") + log.Info("server stopped gracefully") } // checkFFProbe checks if ffprobe is available @@ -167,19 +149,38 @@ func checkFFProbe() error { return fmt.Errorf("ffprobe not found in common locations") } -// printEndpoints prints available API endpoints -func printEndpoints(host, port string) { - if host == "0.0.0.0" || host == "" { - host = "localhost" +// printEndpoints prints available endpoints +func printEndpoints(listen string) { + // Parse listen address to get host and port + host := "localhost" + port := "4567" + + // Extract port from listen address + if len(listen) > 0 { + if listen[0] == ':' { + port = listen[1:] + } else { + // Parse host:port format + for i := len(listen) - 1; i >= 0; i-- { + if listen[i] == ':' { + port = listen[i+1:] + if i > 0 { + host = listen[:i] + if host == "0.0.0.0" || host == "" { + host = "localhost" + } + } + break + } + } + } } baseURL := fmt.Sprintf("http://%s:%s", host, port) - webuiURL := fmt.Sprintf("http://%s:4567", host) - fmt.Println("\n🌐 Web Interface:") fmt.Println("────────────────────────────────────────────────") - fmt.Printf(" Open in browser: %s\n", webuiURL) + fmt.Printf(" Open in browser: %s\n", baseURL) fmt.Println("────────────────────────────────────────────────") fmt.Println("\nšŸš€ API Endpoints:") @@ -215,4 +216,4 @@ func printEndpoints(host, port string) { fmt.Println("\n────────────────────────────────────────────────") fmt.Println("šŸ“š Documentation: https://github.com/eduard256/Strix") fmt.Println("────────────────────────────────────────────────\n") -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index c1de15b..ccb7b9b 100644 --- a/go.mod +++ b/go.mod @@ -23,4 +23,5 @@ require ( golang.org/x/crypto v0.42.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.29.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e0428a9..a65434f 100644 --- a/go.sum +++ b/go.sum @@ -67,5 +67,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/routes.go b/internal/api/routes.go index d211b2f..2f283cf 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -118,31 +118,15 @@ func (s *Server) setupRoutes() { }) }) - // API version 1 routes - s.router.Route("/api/v1", func(r chi.Router) { - // Health check - r.Get("/health", handlers.NewHealthHandler("1.0.0", s.logger).ServeHTTP) + // API routes (mounted at /api/v1 in main.go) + // Health check + s.router.Get("/health", handlers.NewHealthHandler("1.0.0", s.logger).ServeHTTP) - // Camera search - r.Post("/cameras/search", handlers.NewSearchHandler(s.searchEngine, s.logger).ServeHTTP) + // Camera search + s.router.Post("/cameras/search", handlers.NewSearchHandler(s.searchEngine, s.logger).ServeHTTP) - // Stream discovery (SSE) - r.Post("/streams/discover", handlers.NewDiscoverHandler(s.scanner, s.sseServer, s.logger).ServeHTTP) - }) - - // Root health check - s.router.Get("/", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"name":"Strix","version":"1.0.0","api":"v1"}`)) - }) - - // 404 handler - s.router.NotFound(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusNotFound) - w.Write([]byte(`{"error":"Not found"}`)) - }) + // Stream discovery (SSE) + s.router.Post("/streams/discover", handlers.NewDiscoverHandler(s.scanner, s.sseServer, s.logger).ServeHTTP) } // ServeHTTP implements http.Handler diff --git a/internal/config/config.go b/internal/config/config.go index 8a92b6c..961aebe 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,10 +1,15 @@ package config import ( + "fmt" "log/slog" "os" "path/filepath" + "strconv" + "strings" "time" + + "gopkg.in/yaml.v3" ) // Config holds application configuration @@ -17,8 +22,7 @@ type Config struct { // ServerConfig contains HTTP server settings type ServerConfig struct { - Host string - Port string + Listen string // Address to listen on (e.g., ":4567" or "0.0.0.0:4567") ReadTimeout time.Duration WriteTimeout time.Duration } @@ -35,17 +39,17 @@ type DatabaseConfig struct { // ScannerConfig contains stream scanner settings type ScannerConfig struct { - DefaultTimeout time.Duration - MaxStreams int - ModelSearchLimit int - WorkerPoolSize int - FFProbeTimeout time.Duration - RetryAttempts int - RetryDelay time.Duration + DefaultTimeout time.Duration + MaxStreams int + ModelSearchLimit int + WorkerPoolSize int + FFProbeTimeout time.Duration + RetryAttempts int + RetryDelay time.Duration // Validation settings - StrictValidation bool // Enable strict validation mode - MinImageSize int // Minimum bytes for valid image (JPEG/PNG) - MinVideoStreams int // Minimum video streams required + StrictValidation bool // Enable strict validation mode + MinImageSize int // Minimum bytes for valid image (JPEG/PNG) + MinVideoStreams int // Minimum video streams required } // LoggerConfig contains logging settings @@ -54,14 +58,20 @@ type LoggerConfig struct { Format string // "text" or "json" } +// yamlConfig represents the structure of strix.yaml +type yamlConfig struct { + API struct { + Listen string `yaml:"listen"` + } `yaml:"api"` +} + // Load returns configuration with defaults func Load() *Config { dataPath := getEnv("STRIX_DATA_PATH", "./data") - return &Config{ + cfg := &Config{ Server: ServerConfig{ - Host: getEnv("STRIX_HOST", "0.0.0.0"), - Port: getEnv("STRIX_PORT", "8080"), + Listen: ":4567", // Default listen address ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, }, @@ -83,14 +93,90 @@ func Load() *Config { RetryDelay: 500 * time.Millisecond, // Strict validation enabled by default StrictValidation: true, - MinImageSize: 5120, // 5KB minimum for valid images - MinVideoStreams: 1, // At least 1 video stream required + MinImageSize: 5120, // 5KB minimum for valid images + MinVideoStreams: 1, // At least 1 video stream required }, Logger: LoggerConfig{ Level: getEnv("STRIX_LOG_LEVEL", "info"), Format: getEnv("STRIX_LOG_FORMAT", "json"), }, } + + // Load from strix.yaml if exists + configSource := "default" + if err := loadYAML(cfg); err == nil { + configSource = "strix.yaml" + } + + // Environment variable overrides everything + if envListen := os.Getenv("STRIX_API_LISTEN"); envListen != "" { + cfg.Server.Listen = envListen + configSource = "environment variable STRIX_API_LISTEN" + } + + // Validate listen address + if err := validateListen(cfg.Server.Listen); err != nil { + fmt.Fprintf(os.Stderr, "ERROR: Invalid listen address '%s': %v\n", cfg.Server.Listen, err) + fmt.Fprintf(os.Stderr, "Using default: :4567\n") + cfg.Server.Listen = ":4567" + configSource = "default (validation failed)" + } + + // Log configuration source + fmt.Printf("INFO: API listen address '%s' loaded from %s\n", cfg.Server.Listen, configSource) + + return cfg +} + +// loadYAML attempts to load configuration from strix.yaml +func loadYAML(cfg *Config) error { + data, err := os.ReadFile("./strix.yaml") + if err != nil { + return err + } + + var yamlCfg yamlConfig + if err := yaml.Unmarshal(data, &yamlCfg); err != nil { + return fmt.Errorf("failed to parse strix.yaml: %w", err) + } + + // Apply yaml configuration + if yamlCfg.API.Listen != "" { + cfg.Server.Listen = yamlCfg.API.Listen + } + + return nil +} + +// validateListen validates the listen address format and port range +func validateListen(listen string) error { + if listen == "" { + return fmt.Errorf("listen address cannot be empty") + } + + // Parse the listen address + parts := strings.Split(listen, ":") + if len(parts) < 2 { + return fmt.Errorf("invalid format, expected ':port' or 'host:port', got '%s'", listen) + } + + // Get port (last part) + portStr := parts[len(parts)-1] + if portStr == "" { + return fmt.Errorf("port cannot be empty") + } + + // Validate port number + port, err := strconv.Atoi(portStr) + if err != nil { + return fmt.Errorf("invalid port number '%s': %w", portStr, err) + } + + if port < 1 || port > 65535 { + return fmt.Errorf("port %d out of valid range (1-65535)", port) + } + + return nil } // SetupLogger configures the global logger @@ -126,4 +212,4 @@ func getEnv(key, defaultValue string) string { return value } return defaultValue -} \ No newline at end of file +} diff --git a/main b/main deleted file mode 100755 index d98247a..0000000 Binary files a/main and /dev/null differ diff --git a/stream.dump b/stream.dump deleted file mode 100644 index e69de29..0000000 diff --git a/strix.yaml.example b/strix.yaml.example new file mode 100644 index 0000000..8d90a95 --- /dev/null +++ b/strix.yaml.example @@ -0,0 +1,25 @@ +# Strix Configuration Example +# Copy this file to strix.yaml and modify as needed + +# API Server Configuration +api: + # Listen address in format ":port" or "host:port" + # Default: ":4567" + listen: ":4567" + + # Examples: + # listen: ":4567" # Listen on all interfaces, port 4567 (default) + # listen: "0.0.0.0:4567" # Explicitly listen on all interfaces + # listen: "127.0.0.1:4567" # Listen only on localhost (secure local-only access) + # listen: ":8080" # Custom port on all interfaces + +# Configuration Priority (highest to lowest): +# 1. Environment variable: STRIX_API_LISTEN +# 2. This file: strix.yaml +# 3. Default value: :4567 + +# Quick Start: +# 1. Copy this file: cp strix.yaml.example strix.yaml +# 2. Edit the listen address if needed +# 3. Run strix: ./strix +# 4. Or set via environment: STRIX_API_LISTEN=":8080" ./strix diff --git a/webui/web/js/api/camera-search.js b/webui/web/js/api/camera-search.js index 3347385..38f8eaf 100644 --- a/webui/web/js/api/camera-search.js +++ b/webui/web/js/api/camera-search.js @@ -1,9 +1,8 @@ export class CameraSearchAPI { constructor(baseURL = null) { - // Auto-detect API URL based on current host + // Use relative URLs since API and UI are on the same port if (!baseURL) { - const currentHost = window.location.hostname; - this.baseURL = `http://${currentHost}:8080`; + this.baseURL = ''; } else { this.baseURL = baseURL; } diff --git a/webui/web/js/api/stream-discovery.js b/webui/web/js/api/stream-discovery.js index e9464c0..da0ce31 100644 --- a/webui/web/js/api/stream-discovery.js +++ b/webui/web/js/api/stream-discovery.js @@ -1,9 +1,8 @@ export class StreamDiscoveryAPI { constructor(baseURL = null) { - // Auto-detect API URL based on current host + // Use relative URLs since API and UI are on the same port if (!baseURL) { - const currentHost = window.location.hostname; - this.baseURL = `http://${currentHost}:8080`; + this.baseURL = ''; } else { this.baseURL = baseURL; }