feat(sense): implement Control API v1 [T-011]
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -19,20 +20,26 @@ const (
|
||||
defaultReconcilePeriod = 5 * time.Second
|
||||
defaultProbePeriod = 10 * time.Second
|
||||
defaultONVIFMode = "disabled"
|
||||
defaultControlAuthMode = "static-sha256"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
HTTPAddress string
|
||||
AllowNonLoopback bool
|
||||
DatabaseDriver string
|
||||
DatabaseDSN string
|
||||
MediaMTXURL string
|
||||
ReconcileInterval time.Duration
|
||||
ProbeInterval time.Duration
|
||||
ONVIFMode string
|
||||
RTSPRewriteHost string
|
||||
RTSPRewritePort int
|
||||
RTSPStripQuery bool
|
||||
HTTPAddress string
|
||||
AllowNonLoopback bool
|
||||
DatabaseDriver string
|
||||
DatabaseDSN string
|
||||
MediaMTXURL string
|
||||
ReconcileInterval time.Duration
|
||||
ProbeInterval time.Duration
|
||||
ONVIFMode string
|
||||
RTSPRewriteHost string
|
||||
RTSPRewritePort int
|
||||
RTSPStripQuery bool
|
||||
ControlAPIEnabled bool
|
||||
ControlAuthMode string
|
||||
ControlAuthFile string
|
||||
ControlCursorKeyFile string
|
||||
ControlAllowInsecureHTTP bool
|
||||
}
|
||||
|
||||
func Load() (Config, error) {
|
||||
@@ -56,19 +63,32 @@ func Load() (Config, error) {
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
controlEnabled, err := boolEnv("SENSE_CONTROL_API_ENABLED", false)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
controlAllowInsecure, err := boolEnv("SENSE_CONTROL_ALLOW_INSECURE_HTTP", false)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
HTTPAddress: stringEnv("SENSE_HTTP_ADDR", defaultHTTPAddress),
|
||||
AllowNonLoopback: allow,
|
||||
DatabaseDriver: stringEnv("SENSE_DB_DRIVER", defaultDatabaseDriver),
|
||||
DatabaseDSN: stringEnv("SENSE_DB_DSN", defaultDatabaseDSN),
|
||||
MediaMTXURL: stringEnv("SENSE_MEDIAMTX_URL", defaultMediaMTXURL),
|
||||
ReconcileInterval: reconcilePeriod,
|
||||
ProbeInterval: probePeriod,
|
||||
ONVIFMode: stringEnv("SENSE_ONVIF_MODE", defaultONVIFMode),
|
||||
RTSPRewriteHost: stringEnv("SENSE_ONVIF_RTSP_REWRITE_HOST", ""),
|
||||
RTSPRewritePort: rewritePort,
|
||||
RTSPStripQuery: stripQuery,
|
||||
HTTPAddress: stringEnv("SENSE_HTTP_ADDR", defaultHTTPAddress),
|
||||
AllowNonLoopback: allow,
|
||||
DatabaseDriver: stringEnv("SENSE_DB_DRIVER", defaultDatabaseDriver),
|
||||
DatabaseDSN: stringEnv("SENSE_DB_DSN", defaultDatabaseDSN),
|
||||
MediaMTXURL: stringEnv("SENSE_MEDIAMTX_URL", defaultMediaMTXURL),
|
||||
ReconcileInterval: reconcilePeriod,
|
||||
ProbeInterval: probePeriod,
|
||||
ONVIFMode: stringEnv("SENSE_ONVIF_MODE", defaultONVIFMode),
|
||||
RTSPRewriteHost: stringEnv("SENSE_ONVIF_RTSP_REWRITE_HOST", ""),
|
||||
RTSPRewritePort: rewritePort,
|
||||
RTSPStripQuery: stripQuery,
|
||||
ControlAPIEnabled: controlEnabled,
|
||||
ControlAuthMode: stringEnv("SENSE_CONTROL_AUTH_MODE", defaultControlAuthMode),
|
||||
ControlAuthFile: stringEnv("SENSE_CONTROL_AUTH_FILE", ""),
|
||||
ControlCursorKeyFile: stringEnv("SENSE_CONTROL_CURSOR_KEY_FILE", ""),
|
||||
ControlAllowInsecureHTTP: controlAllowInsecure,
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return Config{}, err
|
||||
@@ -121,6 +141,23 @@ func (c Config) Validate() error {
|
||||
return fmt.Errorf("invalid SENSE_ONVIF_RTSP_REWRITE_HOST")
|
||||
}
|
||||
}
|
||||
if c.ControlAPIEnabled {
|
||||
if databaseDriver != "postgres" {
|
||||
return fmt.Errorf("Sense Control API requires SENSE_DB_DRIVER=postgres")
|
||||
}
|
||||
if c.ControlAuthMode != defaultControlAuthMode {
|
||||
return fmt.Errorf("SENSE_CONTROL_AUTH_MODE must be static-sha256")
|
||||
}
|
||||
if c.ControlAuthFile == "" || !filepath.IsAbs(c.ControlAuthFile) {
|
||||
return fmt.Errorf("SENSE_CONTROL_AUTH_FILE must be an absolute external path")
|
||||
}
|
||||
if c.ControlCursorKeyFile == "" || !filepath.IsAbs(c.ControlCursorKeyFile) {
|
||||
return fmt.Errorf("SENSE_CONTROL_CURSOR_KEY_FILE must be an absolute external path")
|
||||
}
|
||||
if !isLoopback && !c.ControlAllowInsecureHTTP {
|
||||
return fmt.Errorf("non-loopback Control API requires SENSE_CONTROL_ALLOW_INSECURE_HTTP=true")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateRejectsNonLoopbackByDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -93,3 +96,43 @@ func TestValidateDatabaseDriver(t *testing.T) {
|
||||
t.Fatal("unknown database driver must be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateControlAPIRequiresPostgresAndExternalSecurityFiles(t *testing.T) {
|
||||
cfg := Config{
|
||||
HTTPAddress: "127.0.0.1:8080", DatabaseDriver: "sqlite", DatabaseDSN: "file:test.db",
|
||||
MediaMTXURL: "http://127.0.0.1:9997", ReconcileInterval: 1, ProbeInterval: 1,
|
||||
ControlAPIEnabled: true, ControlAuthMode: "static-sha256",
|
||||
ControlAuthFile: filepath.Join(t.TempDir(), "sense-auth.json"),
|
||||
ControlCursorKeyFile: filepath.Join(t.TempDir(), "sense-cursor.key"),
|
||||
}
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("Control API was accepted on SQLite")
|
||||
}
|
||||
cfg.DatabaseDriver = "postgres"
|
||||
cfg.DatabaseDSN = "postgres://sense-runtime@127.0.0.1/yovision?sslmode=disable"
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("valid Control API configuration failed: %v", err)
|
||||
}
|
||||
cfg.ControlAuthFile = "relative-auth.json"
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("repository-relative authentication file was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateControlAPINonLoopbackNeedsSeparateRiskAcceptance(t *testing.T) {
|
||||
cfg := Config{
|
||||
HTTPAddress: "0.0.0.0:8080", AllowNonLoopback: true,
|
||||
DatabaseDriver: "postgres", DatabaseDSN: "postgres://sense-runtime@127.0.0.1/yovision?sslmode=disable",
|
||||
MediaMTXURL: "http://127.0.0.1:9997", ReconcileInterval: 1, ProbeInterval: 1,
|
||||
ControlAPIEnabled: true, ControlAuthMode: "static-sha256",
|
||||
ControlAuthFile: filepath.Join(t.TempDir(), "sense-auth.json"),
|
||||
ControlCursorKeyFile: filepath.Join(t.TempDir(), "sense-cursor.key"),
|
||||
}
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("non-loopback plaintext Control API was accepted without explicit risk acceptance")
|
||||
}
|
||||
cfg.ControlAllowInsecureHTTP = true
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("explicit non-loopback Control API risk acceptance failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user