package validator import ( "regexp" "strings" "unicode/utf8" "browser.local/platform/domain" ) const ( maxSourceRCONCommandBytes = 4000 maxSourceRCONChatBytes = 1024 ) var sourceRCONSteamIDPattern = regexp.MustCompile(`^[0-9]{17}$`) func ValidateSourceRCONCommandRequest(request domain.SourceRCONCommandRequest) error { request = domain.CopySourceRCONCommandRequest(request) var violations []string violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID) violations = appendRequired(violations, "idempotencyKey", request.IdempotencyKey) if len([]byte(request.IdempotencyKey)) > 128 || strings.ContainsAny(request.IdempotencyKey, "\x00\r\n") { violations = append(violations, "idempotencyKey is invalid") } switch request.Kind { case domain.SourceRCONCommandKindChat: if request.Command != "" { violations = append(violations, "command must be empty for chat") } violations = append(violations, validateSourceRCONText("message", request.Message, maxSourceRCONChatBytes, true)...) if request.ChatType < 0 || request.ChatType > 7 { violations = append(violations, "chatType must be between 0 and 7") } if request.TargetSteamID != "" && !sourceRCONSteamIDPattern.MatchString(request.TargetSteamID) { violations = append(violations, "targetSteamId must be a 17-digit SteamID64") } case domain.SourceRCONCommandKindCommand: if request.Message != "" || request.TargetSteamID != "" || request.ChatType != 0 { violations = append(violations, "chat fields are not allowed for a raw command") } violations = append(violations, validateSourceRCONText("command", request.Command, maxSourceRCONCommandBytes, true)...) default: violations = append(violations, "kind must be chat or command") } return finish(violations) } func validateRuntimeSourceRCONPlan(prefix string, plan *domain.RuntimeSourceRCONPlan) []string { if plan == nil { return []string{prefix + " is required"} } var violations []string if plan.Protocol != "source-rcon" { violations = append(violations, prefix+".protocol must be source-rcon") } violations = append(violations, validateProfileKey(prefix+".extensionKey", plan.ExtensionKey)...) if !runtimeDLLModKeyPattern.MatchString(plan.ModKey) { violations = append(violations, prefix+".modKey is invalid") } if plan.ConfigRef != "ue4ss/Mods/"+plan.ModKey+"/config.ini" || !validLogicalFileKey(plan.ConfigRef) { violations = append(violations, prefix+".configRef must be the managed UE4SS config path") } if !validSourceRCONDeploymentStateRef(plan.DeploymentStateRef) { violations = append(violations, prefix+".deploymentStateRef must be a managed UE4SS deployment state path") } if plan.Port < 1024 || plan.Port > 65535 { violations = append(violations, prefix+".port must be an unprivileged TCP port") } return violations } func validSourceRCONDeploymentStateRef(value string) bool { const prefix = "runtime/ue4ss-dll/" const suffix = "/release.json" if !strings.HasPrefix(value, prefix) || !strings.HasSuffix(value, suffix) { return false } targetKey := strings.TrimSuffix(strings.TrimPrefix(value, prefix), suffix) return targetKey != "" && validLogicalFileKey(targetKey) } func validateSourceRCONText(field string, value string, maxBytes int, required bool) []string { if required && strings.TrimSpace(value) == "" { return []string{field + " is required"} } if !utf8.ValidString(value) || len([]byte(value)) > maxBytes || strings.ContainsAny(value, "\x00\r\n") { return []string{field + " must be bounded UTF-8 without command framing controls"} } return nil }