Add SCUM Source RCON transport

This commit is contained in:
npc0-hue
2026-07-23 10:55:31 +08:00
parent ec96c22e4c
commit 742f96ea02
33 changed files with 1297 additions and 19 deletions
+4
View File
@@ -64,6 +64,10 @@ func ValidateDependencyExecutionInputRequest(request domain.DependencyExecutionI
return finish(appendLeaseFields(nil, request.RunEndpointID, request.SessionToken, request.JobID, request.LeaseToken, request.Attempt))
}
func ValidateSourceRCONExecutionInputRequest(request domain.SourceRCONExecutionInputRequest) error {
return finish(appendLeaseFields(nil, request.RunEndpointID, request.SessionToken, request.JobID, request.LeaseToken, request.Attempt))
}
func ValidateRunUpdateInputRequest(request domain.RunUpdateInputRequest) error {
return finish(appendLeaseFields(nil, request.RunEndpointID, request.SessionToken, request.JobID, request.LeaseToken, request.Attempt))
}
+12
View File
@@ -1021,6 +1021,18 @@ func ValidateJob(job domain.Job) error {
for i, plan := range job.ExecutionInput.DLLExtensions {
violations = append(violations, validateRuntimeDLLExtensionPlan(fmt.Sprintf("executionInput.dllExtensions[%d]", i), plan)...)
}
if job.ExecutionInput.SourceRCON != nil {
violations = append(violations, validateRuntimeSourceRCONPlan("executionInput.sourceRcon", job.ExecutionInput.SourceRCON)...)
if job.Capability != domain.JobCapabilityRemoteRunRCONCommand || job.ExecutionInput.RemoteAdapterKind != "rcon" {
violations = append(violations, "executionInput.sourceRcon is allowed only for rcon jobs")
}
if job.RetryPolicy.MaxAttempts != 1 {
violations = append(violations, "executionInput.sourceRcon jobs must have one attempt")
}
if len(job.ExecutionInput.Inputs) != 0 {
violations = append(violations, "executionInput.sourceRcon must not persist adapter inputs")
}
}
violations = append(violations, validateRemoteAdapterInputs("executionInput.inputs", job.ExecutionInput.Inputs)...)
if job.ExecutionResult.Checksum != "" && !validSHA256Checksum(job.ExecutionResult.Checksum) {
violations = append(violations, "executionResult.checksum must be sha256:<hex>")
+91
View File
@@ -0,0 +1,91 @@
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
}