package app import ( "fmt" "net" "net/http" "strings" "github.com/go-sql-driver/mysql" "github.com/taosdata/driver-go/v3/taosRestful" "lingniu/vehicle-data-platform/apps/api/internal/config" ) // ValidateLocalIsolation runs before constructing any database, external client, // scheduler or listener. It is an opt-in development boundary, not an auth bypass. func validOneOSDevBaseURL(value string) bool { return value == "https://dev-oneos.lnh2e.com/prod-api" || value == "https://dev-oneos.lnh2e.com/prod-api/" || value == "https://lnoneos.com/prod-api" || value == "https://lnoneos.com/prod-api/" } func ValidateLocalIsolation(cfg config.Config) error { if err := validateOneOSDevDataProxy(cfg); err != nil { return err } if cfg.OneOSLocalOnly && cfg.OneOSDevIntegration { return fmt.Errorf("ONEOS_LOCAL_ONLY and ONEOS_DEV_INTEGRATION are mutually exclusive") } if cfg.OneOSLocalBackend && !cfg.OneOSDevIntegration { return fmt.Errorf("local backend requires dev integration") } if !cfg.OneOSLocalOnly && !cfg.OneOSDevIntegration { return nil } if cfg.OneOSDevIntegration { if !validOneOSDevBaseURL(cfg.OneOSProfileBaseURL) { return fmt.Errorf("ONEOS_DEV_INTEGRATION requires an explicitly allowed OneOS HTTPS origin") } if !validOneOSOrigin(cfg.OneOSPublicOrigin, true) { return fmt.Errorf("ONEOS_DEV_INTEGRATION requires a literal loopback public origin") } } host, _, err := net.SplitHostPort(cfg.HTTPAddr) if err != nil || net.ParseIP(host) == nil || !net.ParseIP(host).IsLoopback() { return fmt.Errorf("local OneOS testing requires an explicit loopback HTTP_ADDR") } if cfg.OneOSLocalBackend { if !cfg.OneOSDevIntegration || cfg.OneOSLocalOnly || !cfg.OneOSAuthEnabled || !cfg.OneOSVehicleQueryScope || cfg.DataMode != "production" || cfg.AuthMode != "enforce" || cfg.OneOSDevDataProxyURL != "" { return fmt.Errorf("local backend requires dev OneOS, vehicle-query scope, production store and enforced authentication") } db, err := mysql.ParseDSN(cfg.MySQLDSN) if err != nil || db.Net != "tcp" || !loopbackAddress(db.Addr) { return fmt.Errorf("local backend requires a loopback MySQL tunnel") } if cfg.RedisAddr != "" && !loopbackAddress(cfg.RedisAddr) { return fmt.Errorf("local backend requires a loopback Redis tunnel") } if cfg.TDengineDSN != "" { if cfg.TDengineDriver == "taosRestful" { rest, err := taosRestful.ParseDSN(cfg.TDengineDSN) if err != nil || rest.Net != "http" || rest.Port < 1 || rest.Port > 65535 || !loopbackAddress(net.JoinHostPort(rest.Addr, fmt.Sprint(rest.Port))) { return fmt.Errorf("local backend requires a loopback TDengine REST tunnel") } } else { if cfg.TDengineDriver != "" && cfg.TDengineDriver != "taosWS" { return fmt.Errorf("local backend requires a supported TDengine driver") } marker := strings.LastIndex(cfg.TDengineDSN, "@ws(") if marker < 0 { return fmt.Errorf("local backend requires a TDengine websocket tunnel") } address := strings.SplitN(cfg.TDengineDSN[marker+4:], ")", 2) if len(address) != 2 || !loopbackAddress(address[0]) { return fmt.Errorf("local backend requires a loopback TDengine tunnel") } } } if cfg.DailyGeographyEnabled || cfg.HistoryCleanupAutomation || cfg.OneOSSSOEnabled || cfg.CapacityCheckBin != "" || cfg.AlertStreamMode != "disabled" || len(cfg.AlertStreamKafkaBrokers) != 0 || cfg.AlertNotificationSMSURL != "" || cfg.AlertNotificationEmailURL != "" || cfg.AlertNotificationWeComURL != "" { return fmt.Errorf("local backend must not start production workers or notification delivery") } return nil } if cfg.DataMode != "mock" || cfg.AuthMode != "enforce" { return fmt.Errorf("local OneOS testing requires DATA_MODE=mock and AUTH_MODE=enforce") } values := []string{ cfg.MySQLDSN, cfg.RedisAddr, cfg.TDengineDSN, cfg.CapacityCheckBin, cfg.AMapAPIKey, cfg.AMapSecurityCode, cfg.AuthToken, cfg.AuthTokensJSON, cfg.BootstrapAdminPassword, cfg.AlertNotificationSMSURL, cfg.AlertNotificationEmailURL, cfg.AlertNotificationWeComURL, cfg.OneOSIntrospectionURL, } if cfg.OneOSDevDataProxyURL == "" && (cfg.AMapWebJSKey != "" || cfg.AMapServiceHost != "") { return fmt.Errorf("isolated OneOS testing rejects external map configuration") } if cfg.OneOSDevDataProxyURL != "" && cfg.AMapServiceHost != "" && cfg.AMapServiceHost != "/_AMapService" { return fmt.Errorf("real-data development requires the local map proxy path") } for _, value := range values { if strings.TrimSpace(value) != "" { return fmt.Errorf("local OneOS testing rejects external data, legacy auth and notification configuration") } } if cfg.DailyGeographyEnabled || cfg.HistoryCleanupAutomation || cfg.OneOSSSOEnabled || (cfg.AlertStreamMode != "" && cfg.AlertStreamMode != "disabled") || len(cfg.AlertStreamKafkaBrokers) != 0 { return fmt.Errorf("local OneOS testing rejects external workers and legacy SSO") } return nil } func loopbackAddress(address string) bool { host, _, err := net.SplitHostPort(address) ip := net.ParseIP(host) return err == nil && ip != nil && ip.IsLoopback() } func withOneOSLocalIsolationHeaders(next http.Handler, realData ...bool) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // The local UI must not load map SDKs, analytics, APIs or images remotely. // Existing index.html contains an inline boot/error indicator. Inline code // remains local; every external resource and connection is still excluded. w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; worker-src 'self' blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'") if len(realData) > 0 && realData[0] { w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://webapi.amap.com https://jsapi-service.amap.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://*.amap.com https://*.autonavi.com; font-src 'self' data:; connect-src 'self' https://*.amap.com https://*.autonavi.com; worker-src 'self' blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'") } w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("X-Content-Type-Options", "nosniff") next.ServeHTTP(w, r) }) }