Files
browser/plugins/examples/scum-server-plugin/companion/config.go
T

248 lines
9.2 KiB
Go

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
}