package companion import ( "fmt" "io" "net/url" "strconv" "strings" "gopkg.in/yaml.v3" ) const ( ConfigSchemaVersion = 1 PluginID = "game.scum" ProfileKey = "scum-client-manager" ProofEnvironment = "SCUM_COMPONENT_PROOF" SCUMDatabaseFileEnvironment = "SCUM_DB_FILE" TrajectorySourceSCUMSQLite = "scum-sqlite" TrajectoryStoreSharedPlatformMySQL = "shared-platform-mysql" DefaultTrajectoryCollectionIntervalSecs = 3 DefaultTrajectoryCollectionMaxRows = 500 ) var requiredCapabilities = []string{ "component.register", "component.heartbeat", "component.health", "component.control", "game-client.bridge", "logs.stream", } var optionalCapabilities = map[string]struct{}{ "handler.vehicle.spawn": {}, } var requiredCapabilitySet = map[string]struct{}{ "component.register": {}, "component.heartbeat": {}, "component.health": {}, "component.control": {}, "game-client.bridge": {}, "logs.stream": {}, } type Config struct { SchemaVersion int `json:"schemaVersion" yaml:"schemaVersion"` Platform PlatformConfig `json:"platform" yaml:"platform"` Component ComponentConfig `json:"component" yaml:"component"` Proof ProofConfig `json:"proof" yaml:"proof"` Session SessionConfig `json:"session" yaml:"session"` Capabilities []string `json:"capabilities" yaml:"capabilities"` Timing TimingConfig `json:"timing" yaml:"timing"` TLS TransportTLSConfig `json:"tls" yaml:"tls"` Trajectory TrajectoryConfig `json:"trajectory" yaml:"trajectory"` } type PlatformConfig struct { BaseURL string `json:"baseUrl" yaml:"baseUrl"` } type ComponentConfig struct { InstallationID string `json:"installationId" yaml:"installationId"` ServerInstanceID string `json:"serverInstanceId" yaml:"serverInstanceId"` PluginID string `json:"pluginId" yaml:"pluginId"` ProfileKey string `json:"profileKey" yaml:"profileKey"` ArtifactID string `json:"artifactId" yaml:"artifactId"` Version string `json:"version" yaml:"version"` SourceRevision string `json:"sourceRevision" yaml:"sourceRevision"` TargetOS string `json:"targetOs" yaml:"targetOs"` TargetArch string `json:"targetArch" yaml:"targetArch"` KeyGeneration int `json:"keyGeneration" yaml:"keyGeneration"` DeploymentGeneration int `json:"deploymentGeneration" yaml:"deploymentGeneration"` } type ProofConfig struct { Mode string `json:"mode" yaml:"mode"` MaterialEnv string `json:"materialEnv" yaml:"materialEnv"` } type SessionConfig struct { Mode string `json:"mode" yaml:"mode"` } type TimingConfig struct { HeartbeatIntervalSeconds int `json:"heartbeatIntervalSeconds" yaml:"heartbeatIntervalSeconds"` CommandPollIntervalSeconds int `json:"commandPollIntervalSeconds" yaml:"commandPollIntervalSeconds"` RequestTimeoutSeconds int `json:"requestTimeoutSeconds" yaml:"requestTimeoutSeconds"` } type TransportTLSConfig struct { Policy string `json:"policy" yaml:"policy"` } type TrajectoryConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` Source string `json:"source" yaml:"source"` Store string `json:"store" yaml:"store"` FileEnv string `json:"fileEnv" yaml:"fileEnv"` IntervalSeconds int `json:"intervalSeconds" yaml:"intervalSeconds"` MaxRows int `json:"maxRows" yaml:"maxRows"` } func LoadConfig(reader io.Reader) (Config, error) { decoder := yaml.NewDecoder(reader) decoder.KnownFields(true) var config Config if err := decoder.Decode(&config); err != nil { return Config{}, fmt.Errorf("decode companion config: %w", err) } var trailing any if err := decoder.Decode(&trailing); err != io.EOF { if err == nil { return Config{}, fmt.Errorf("decode companion config: multiple documents are not allowed") } return Config{}, fmt.Errorf("decode companion config: %w", err) } config.applyDefaults() if err := config.Validate(); err != nil { return Config{}, err } config.Platform.BaseURL, _ = canonicalPlatformOrigin(config.Platform.BaseURL) config.Capabilities = append([]string(nil), config.Capabilities...) return config, nil } func (config *Config) applyDefaults() { if config.Trajectory.Source == "" { config.Trajectory.Source = TrajectorySourceSCUMSQLite } if config.Trajectory.Store == "" { config.Trajectory.Store = TrajectoryStoreSharedPlatformMySQL } if config.Trajectory.FileEnv == "" { config.Trajectory.FileEnv = SCUMDatabaseFileEnvironment } if config.Trajectory.IntervalSeconds == 0 { config.Trajectory.IntervalSeconds = DefaultTrajectoryCollectionIntervalSecs } if config.Trajectory.MaxRows == 0 { config.Trajectory.MaxRows = DefaultTrajectoryCollectionMaxRows } } func (config Config) Validate() error { if config.SchemaVersion != ConfigSchemaVersion { return fmt.Errorf("companion config schema version is unsupported") } if _, err := canonicalPlatformOrigin(config.Platform.BaseURL); err != nil { return err } component := config.Component if component.InstallationID == "" || component.ServerInstanceID == "" || component.ArtifactID == "" || component.Version == "" || component.SourceRevision == "" { return fmt.Errorf("component identity is incomplete") } if component.PluginID != PluginID || component.ProfileKey != ProfileKey || component.TargetOS != "windows" || component.TargetArch != "amd64" { return fmt.Errorf("component identity does not match the SCUM companion profile") } if component.KeyGeneration <= 0 || component.DeploymentGeneration <= 0 { return fmt.Errorf("component generations must be positive") } if config.Proof.Mode != "hmac-sha256" || config.Proof.MaterialEnv != ProofEnvironment { return fmt.Errorf("component proof policy is unsupported") } if config.Session.Mode != "component-session" { return fmt.Errorf("component session policy is unsupported") } if config.TLS.Policy != "verify-system-roots" { return fmt.Errorf("TLS policy must verify system roots") } if err := validateCapabilities(config.Capabilities); err != nil { return err } if config.Timing.HeartbeatIntervalSeconds < 5 || config.Timing.HeartbeatIntervalSeconds > 300 || config.Timing.CommandPollIntervalSeconds < 1 || config.Timing.CommandPollIntervalSeconds > 60 || config.Timing.RequestTimeoutSeconds < 1 || config.Timing.RequestTimeoutSeconds > 60 { return fmt.Errorf("companion timing policy is invalid") } if err := config.Trajectory.Validate(); err != nil { return err } return nil } func (config TrajectoryConfig) Validate() error { if config.Source != TrajectorySourceSCUMSQLite || config.Store != TrajectoryStoreSharedPlatformMySQL { return fmt.Errorf("SCUM trajectory collection mode is unsupported") } if !validCompanionEnvironmentName(config.FileEnv) { return fmt.Errorf("SCUM database file environment name is invalid") } if config.IntervalSeconds < 1 || config.IntervalSeconds > 3600 || config.MaxRows < 1 || config.MaxRows > 5000 { return fmt.Errorf("SCUM trajectory collection bounds are invalid") } return nil } func validCompanionEnvironmentName(value string) bool { if len(value) < 3 || len(value) > 64 || value[0] < 'A' || value[0] > 'Z' { return false } for _, char := range value[1:] { if char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' || char == '_' { continue } return false } switch value { case "PATH", "LD_PRELOAD", "DYLD_INSERT_LIBRARIES": return false default: return true } } func canonicalPlatformOrigin(value string) (string, error) { parsed, err := url.Parse(strings.TrimSpace(value)) if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.Hostname() == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.Path != "" && parsed.Path != "/" { return "", fmt.Errorf("platform base URL must be a credential-free HTTPS origin") } port := parsed.Port() if strings.HasSuffix(parsed.Host, ":") || port != "" { value, err := strconv.Atoi(port) if err != nil || value < 1 || value > 65535 { return "", fmt.Errorf("platform base URL must use a valid HTTPS port") } } return (&url.URL{Scheme: parsed.Scheme, Host: parsed.Host}).String(), nil } func validateCapabilities(capabilities []string) error { if len(capabilities) < len(requiredCapabilities) || len(capabilities) > len(requiredCapabilities)+len(optionalCapabilities) { return fmt.Errorf("component capabilities do not match the SCUM companion profile") } actual := make(map[string]struct{}, len(capabilities)) for _, capability := range capabilities { if _, exists := actual[capability]; exists { return fmt.Errorf("component capabilities must be unique") } actual[capability] = struct{}{} } for _, capability := range requiredCapabilities { if _, exists := actual[capability]; !exists { return fmt.Errorf("component capabilities do not match the SCUM companion profile") } } for capability := range actual { if _, required := requiredCapabilitySet[capability]; !required { if _, optional := optionalCapabilities[capability]; !optional { return fmt.Errorf("component capabilities do not match the SCUM companion profile") } } } return nil }