Files

92 lines
3.5 KiB
Go

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
}