feat(scum-companion): validate typed adapters
This commit is contained in:
@@ -20,6 +20,7 @@ type SafeAdapter interface {
|
||||
}
|
||||
|
||||
type HandlerAvailability struct {
|
||||
BoundServerID string
|
||||
ServerVersion string
|
||||
Capabilities map[string]bool
|
||||
Approved bool
|
||||
@@ -66,7 +67,7 @@ func (registry *HandlerRegistry) Execute(ctx context.Context, command ClaimedCom
|
||||
if err := validateDeclaredCommandAt(command, time.Now); err != nil {
|
||||
return unsupportedResult("validation-failed"), nil
|
||||
}
|
||||
if !registry.availability.Approved || !registry.availability.Capabilities[command.CommandType] {
|
||||
if strings.TrimSpace(registry.availability.BoundServerID) == "" || !registry.availability.Approved || !registry.availability.Capabilities[command.CommandType] {
|
||||
return unsupportedResult("unsupported"), nil
|
||||
}
|
||||
handler, exists := registry.handlers[command.CommandType]
|
||||
@@ -93,7 +94,104 @@ func validateDeclaredCommandAt(command ClaimedCommand, now func() time.Time) err
|
||||
return fmt.Errorf("unsafe payload")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return validateCommandPayload(command.CommandType, command.Payload)
|
||||
}
|
||||
|
||||
func validateCommandPayload(commandType string, payload map[string]any) error {
|
||||
require := func(keys ...string) error {
|
||||
for _, key := range keys {
|
||||
if _, ok := payload[key]; !ok {
|
||||
return fmt.Errorf("payload is incomplete")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
noUnknown := func(keys ...string) error {
|
||||
allowed := map[string]bool{}
|
||||
for _, key := range keys {
|
||||
allowed[key] = true
|
||||
}
|
||||
for key := range payload {
|
||||
if !allowed[key] {
|
||||
return fmt.Errorf("payload has unsupported field")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
switch commandType {
|
||||
case "config.read":
|
||||
return noUnknown()
|
||||
case "config.patch":
|
||||
if err := require("revision", "fields"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := noUnknown("revision", "fields"); err != nil {
|
||||
return err
|
||||
}
|
||||
_, revisionOK := payload["revision"].(string)
|
||||
fields, fieldsOK := payload["fields"].([]any)
|
||||
if !revisionOK || !fieldsOK || len(fields) == 0 || len(fields) > 32 {
|
||||
return fmt.Errorf("config patch payload is invalid")
|
||||
}
|
||||
return nil
|
||||
case "companion.diagnostics":
|
||||
if err := noUnknown("includeWindowState", "maxEntries"); err != nil {
|
||||
return err
|
||||
}
|
||||
if value, ok := payload["includeWindowState"]; ok {
|
||||
if _, valid := value.(bool); !valid {
|
||||
return fmt.Errorf("diagnostics payload is invalid")
|
||||
}
|
||||
}
|
||||
if value, ok := payload["maxEntries"]; ok {
|
||||
if !boundedDiagnosticsEntries(value) {
|
||||
return fmt.Errorf("diagnostics payload is invalid")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case "game-state.patch":
|
||||
if err := require("playerId", "gameVersion", "expectedStateVersion", "safetyWindow", "reason", "changes"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := noUnknown("playerId", "gameVersion", "expectedStateVersion", "safetyWindow", "reason", "changes"); err != nil {
|
||||
return err
|
||||
}
|
||||
version, ok := payload["gameVersion"].(string)
|
||||
changes, changesOK := payload["changes"].([]any)
|
||||
if !ok || version == "" || !changesOK || len(changes) == 0 || len(changes) > 8 {
|
||||
return fmt.Errorf("state patch payload is invalid")
|
||||
}
|
||||
return nil
|
||||
case "reward.deliver":
|
||||
if err := require("grantId", "playerId", "items"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := noUnknown("grantId", "playerId", "items"); err != nil {
|
||||
return err
|
||||
}
|
||||
_, grantOK := payload["grantId"].(string)
|
||||
_, playerOK := payload["playerId"].(string)
|
||||
items, itemsOK := payload["items"].([]any)
|
||||
if !grantOK || !playerOK || !itemsOK || len(items) == 0 || len(items) > 8 {
|
||||
return fmt.Errorf("reward payload is invalid")
|
||||
}
|
||||
return nil
|
||||
case "player.notify":
|
||||
if err := require("playerId", "message"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := noUnknown("playerId", "message"); err != nil {
|
||||
return err
|
||||
}
|
||||
_, playerOK := payload["playerId"].(string)
|
||||
message, messageOK := payload["message"].(string)
|
||||
if !playerOK || !messageOK || strings.TrimSpace(message) == "" || len(message) > 200 {
|
||||
return fmt.Errorf("notification payload is invalid")
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("command type is not declared")
|
||||
}
|
||||
}
|
||||
|
||||
func safeCommandField(key string, value any) bool {
|
||||
|
||||
Reference in New Issue
Block a user