4d6c2fd878
Fast (~1-3s) endpoint that gathers network info about a device before full stream discovery. Runs ping first, then parallel probes. Features: - Ping with ICMP + TCP fallback (works without root) - Reverse DNS hostname lookup - ARP table MAC address + OUI vendor identification (2403 entries, 51 camera vendors) - mDNS HomeKit detection (camera/doorbell, paired status) - Extensible Prober interface for adding new probe types - 3-second overall timeout, parallel execution Response includes "type" field: - "unreachable" - device not responding - "standard" - normal IP camera (RTSP/HTTP/ONVIF flow) - "homekit" - Apple HomeKit camera (PIN pairing flow)
218 lines
5.3 KiB
Go
218 lines
5.3 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Config holds application configuration
|
|
type Config struct {
|
|
Server ServerConfig
|
|
Database DatabaseConfig
|
|
Scanner ScannerConfig
|
|
Logger LoggerConfig
|
|
}
|
|
|
|
// ServerConfig contains HTTP server settings
|
|
type ServerConfig struct {
|
|
Listen string // Address to listen on (e.g., ":4567" or "0.0.0.0:4567")
|
|
ReadTimeout time.Duration
|
|
WriteTimeout time.Duration
|
|
}
|
|
|
|
// DatabaseConfig contains database settings
|
|
type DatabaseConfig struct {
|
|
DataPath string
|
|
BrandsPath string
|
|
PatternsPath string
|
|
ParametersPath string
|
|
OUIPath string
|
|
CacheEnabled bool
|
|
CacheTTL time.Duration
|
|
}
|
|
|
|
// 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
|
|
// Validation settings
|
|
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
|
|
type LoggerConfig struct {
|
|
Level string
|
|
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")
|
|
|
|
cfg := &Config{
|
|
Server: ServerConfig{
|
|
Listen: ":4567", // Default listen address
|
|
ReadTimeout: 30 * time.Second,
|
|
WriteTimeout: 5 * time.Minute, // Increased for SSE long-polling
|
|
},
|
|
Database: DatabaseConfig{
|
|
DataPath: dataPath,
|
|
BrandsPath: filepath.Join(dataPath, "brands"),
|
|
PatternsPath: filepath.Join(dataPath, "popular_stream_patterns.json"),
|
|
ParametersPath: filepath.Join(dataPath, "query_parameters.json"),
|
|
OUIPath: filepath.Join(dataPath, "camera_oui.json"),
|
|
CacheEnabled: true,
|
|
CacheTTL: 5 * time.Minute,
|
|
},
|
|
Scanner: ScannerConfig{
|
|
DefaultTimeout: 4 * time.Minute,
|
|
MaxStreams: 10,
|
|
ModelSearchLimit: 6,
|
|
WorkerPoolSize: 20,
|
|
FFProbeTimeout: 30 * time.Second,
|
|
RetryAttempts: 2,
|
|
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
|
|
},
|
|
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
|
|
func (c *Config) SetupLogger() *slog.Logger {
|
|
var level slog.Level
|
|
switch c.Logger.Level {
|
|
case "debug":
|
|
level = slog.LevelDebug
|
|
case "warn":
|
|
level = slog.LevelWarn
|
|
case "error":
|
|
level = slog.LevelError
|
|
default:
|
|
level = slog.LevelInfo
|
|
}
|
|
|
|
opts := &slog.HandlerOptions{
|
|
Level: level,
|
|
}
|
|
|
|
var handler slog.Handler
|
|
if c.Logger.Format == "json" {
|
|
handler = slog.NewJSONHandler(os.Stdout, opts)
|
|
} else {
|
|
handler = slog.NewTextHandler(os.Stdout, opts)
|
|
}
|
|
|
|
return slog.New(handler)
|
|
}
|
|
|
|
func getEnv(key, defaultValue string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|