feat(scum-companion): validate typed adapters
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
package companion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AuthorizedConfigPort is supplied by a version-bound Companion integration.
|
||||
// It exposes logical configuration values only: never a host path, connection
|
||||
// string, credential, arbitrary command, or direct database handle.
|
||||
type AuthorizedConfigPort interface {
|
||||
ReadConfig(context.Context) (map[string]string, error)
|
||||
ApplyConfigPatch(ctx context.Context, revision string, fields []ConfigFieldPatch) (map[string]string, error)
|
||||
}
|
||||
type ConfigFieldPatch struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
type VersionedAdapter struct {
|
||||
ServerVersion string
|
||||
Config AuthorizedConfigPort
|
||||
DiagnosticsState map[string]string
|
||||
}
|
||||
|
||||
func (adapter VersionedAdapter) ReadConfiguration(ctx context.Context) (map[string]any, error) {
|
||||
if !supportedAdapterVersion(adapter.ServerVersion) || adapter.Config == nil {
|
||||
return nil, fmt.Errorf("configuration adapter is unsupported")
|
||||
}
|
||||
fields, err := adapter.Config.ReadConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"version": adapter.ServerVersion, "fields": redactConfigValues(fields)}, nil
|
||||
}
|
||||
func (adapter VersionedAdapter) PatchConfiguration(ctx context.Context, payload map[string]any) (map[string]any, error) {
|
||||
if !supportedAdapterVersion(adapter.ServerVersion) || adapter.Config == nil {
|
||||
return nil, fmt.Errorf("configuration adapter is unsupported")
|
||||
}
|
||||
revision, _ := payload["revision"].(string)
|
||||
raw, _ := payload["fields"].([]any)
|
||||
fields := make([]ConfigFieldPatch, 0, len(raw))
|
||||
for _, value := range raw {
|
||||
item, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("configuration patch payload is invalid")
|
||||
}
|
||||
key, keyOK := item["key"].(string)
|
||||
fieldValue, valueOK := item["value"].(string)
|
||||
if !keyOK || !valueOK || !supportedConfigKey(key) {
|
||||
return nil, fmt.Errorf("configuration patch field is unsupported")
|
||||
}
|
||||
fields = append(fields, ConfigFieldPatch{Key: key, Value: fieldValue})
|
||||
}
|
||||
applied, err := adapter.Config.ApplyConfigPatch(ctx, revision, fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"version": adapter.ServerVersion, "appliedFields": redactConfigValues(applied)}, nil
|
||||
}
|
||||
func (adapter VersionedAdapter) Diagnostics(context.Context) (map[string]any, error) {
|
||||
state := map[string]any{"version": adapter.ServerVersion, "adapter": "version-bound", "configuration": supportedAdapterVersion(adapter.ServerVersion)}
|
||||
for key, value := range adapter.DiagnosticsState {
|
||||
if safeDiagnosticField(key, value) {
|
||||
state[key] = value
|
||||
}
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
func (VersionedAdapter) PatchGameState(context.Context, map[string]any) (map[string]any, error) {
|
||||
return nil, fmt.Errorf("state adapter is unsupported")
|
||||
}
|
||||
func (VersionedAdapter) DeliverReward(context.Context, map[string]any) (map[string]any, error) {
|
||||
return nil, fmt.Errorf("reward adapter is unsupported")
|
||||
}
|
||||
func (VersionedAdapter) NotifyPlayer(context.Context, map[string]any) (map[string]any, error) {
|
||||
return nil, fmt.Errorf("notification adapter is unsupported")
|
||||
}
|
||||
|
||||
func supportedAdapterVersion(version string) bool { return version == "0.9.700.90357" }
|
||||
func supportedConfigKey(key string) bool {
|
||||
return map[string]bool{"ServerName": true, "GamePort": true, "QueryPort": true, "MaxPlayers": true, "WelcomeMessage": true}[key]
|
||||
}
|
||||
func redactConfigValues(values map[string]string) map[string]string {
|
||||
result := map[string]string{}
|
||||
keys := make([]string, 0, len(values))
|
||||
for key := range values {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
if supportedConfigKey(key) && safeDiagnosticField(key, values[key]) {
|
||||
result[key] = values[key]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
func safeDiagnosticField(key, value string) bool {
|
||||
lowered := strings.ToLower(key + "=" + value)
|
||||
return !strings.Contains(lowered, "path") && !strings.Contains(lowered, "credential") && !strings.Contains(lowered, "password") && !strings.Contains(lowered, "bearer ") && !strings.Contains(lowered, "rcon") && !strings.Contains(lowered, "sql") && !strings.Contains(lowered, "://")
|
||||
}
|
||||
Reference in New Issue
Block a user