Files
Strix/internal/config/config.go
T
eduard256 4d6c2fd878 Add GET /api/v1/probe endpoint for device inspection
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)
2026-03-16 13:57:41 +00:00

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
}