feat: add UE4SS DLL runtime extension

This commit is contained in:
npc0-hue
2026-07-23 09:49:14 +08:00
parent 8e34c8762a
commit 1caf40565c
30 changed files with 1179 additions and 70 deletions
+9
View File
@@ -1012,6 +1012,15 @@ func ValidateJob(job domain.Job) error {
if job.ExecutionInput.ExpectedChecksum != "" && !validSHA256Checksum(job.ExecutionInput.ExpectedChecksum) {
violations = append(violations, "executionInput.expectedChecksum must be sha256:<hex>")
}
if len(job.ExecutionInput.DLLExtensions) > 16 {
violations = append(violations, "executionInput.dllExtensions must not exceed 16")
}
if len(job.ExecutionInput.DLLExtensions) > 0 && (job.Capability != domain.LifecycleCapabilityStart || job.ExecutionInput.LifecycleOperation != "start" || job.ServerInstanceID == "") {
violations = append(violations, "executionInput.dllExtensions are allowed only for scoped process.start jobs")
}
for i, plan := range job.ExecutionInput.DLLExtensions {
violations = append(violations, validateRuntimeDLLExtensionPlan(fmt.Sprintf("executionInput.dllExtensions[%d]", i), plan)...)
}
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>")
@@ -0,0 +1,71 @@
package validator
import (
"strings"
"testing"
"browser.local/platform/domain"
)
func TestValidateGamePluginRuntimeProfilesAcceptsReadyUE4SSDLLExtension(t *testing.T) {
profiles := validRuntimeDLLExtensionProfiles()
if err := ValidateGamePluginRuntimeProfiles(profiles); err != nil {
t.Fatalf("expected ready DLL extension profile to validate: %v", err)
}
}
func TestValidateGamePluginRuntimeProfilesRejectsUnsafeOrUnpublishedDLLExtension(t *testing.T) {
cases := []struct {
name string
mutate func(*domain.GamePluginRuntimeProfiles)
want string
}{
{name: "linux target", mutate: func(profiles *domain.GamePluginRuntimeProfiles) {
profiles.DLLExtensions[0].SupportedTargets = []domain.RuntimeTarget{{OS: "linux", Arch: "amd64"}}
}, want: "windows/amd64"},
{name: "unsafe URL", mutate: func(profiles *domain.GamePluginRuntimeProfiles) {
profiles.DLLExtensions[0].ReleaseURL = "https://127.0.0.1/plugin.dll"
}, want: "public credential-free HTTPS DLL URL"},
{name: "query URL", mutate: func(profiles *domain.GamePluginRuntimeProfiles) {
profiles.DLLExtensions[0].ReleaseURL = "https://cdn.npc0.com/plugin.dll?release=1"
}, want: "public credential-free HTTPS DLL URL"},
{name: "unsafe path", mutate: func(profiles *domain.GamePluginRuntimeProfiles) {
profiles.DLLExtensions[0].DLLRef = "ue4ss/Mods/scum_simple_rcon/dlls/plugin.exe"
}, want: "main.dll path"},
{name: "missing pin", mutate: func(profiles *domain.GamePluginRuntimeProfiles) { profiles.DLLExtensions[0].Checksum = "" }, want: "SHA-256"},
{name: "unpublished reference", mutate: func(profiles *domain.GamePluginRuntimeProfiles) {
profiles.DLLExtensions[0].ReleaseState = "unpublished"
}, want: "unpublished DLL extension"},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
profiles := validRuntimeDLLExtensionProfiles()
testCase.mutate(&profiles)
err := ValidateGamePluginRuntimeProfiles(profiles)
if err == nil || !strings.Contains(err.Error(), testCase.want) {
t.Fatalf("expected %q validation error, got %v", testCase.want, err)
}
})
}
}
func TestValidateJobRejectsDLLPlanOutsideProcessStart(t *testing.T) {
profiles := validRuntimeDLLExtensionProfiles()
extension := profiles.DLLExtensions[0]
job := domain.Job{ID: "dll-job", ServerInstanceID: "server-1", RunEndpointID: "run-1", Capability: domain.LifecycleCapabilityStop, IdempotencyKey: "dll-stop", State: domain.JobStateQueued, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{LifecycleOperation: "stop", DLLExtensions: []domain.RuntimeDLLExtensionPlan{{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, SCUMExecutableChecksum: extension.SCUMExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort}}}}
if err := ValidateJob(job); err == nil || !strings.Contains(err.Error(), "process.start") {
t.Fatalf("expected process.start plan restriction, got %v", err)
}
}
func validRuntimeDLLExtensionProfiles() domain.GamePluginRuntimeProfiles {
return domain.GamePluginRuntimeProfiles{
LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "run-local", Mode: "local-process", Capabilities: []string{domain.LifecycleCapabilityStart}, Platforms: []string{"windows"}, DLLExtensionRefs: []string{"scum-simple-rcon"}}},
DLLExtensions: []domain.RuntimeDLLExtensionProfile{{
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
SCUMExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
}},
}
}
+114
View File
@@ -14,6 +14,8 @@ import (
var (
runtimeLogEventSchemaRefPattern = regexp.MustCompile(`^[A-Za-z0-9_./-]+\.json$`)
runtimeLogEventTypePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,119}$`)
runtimeDLLModKeyPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,79}$`)
runtimeDLLABIPattern = regexp.MustCompile(`^[A-Za-z0-9._-]{1,80}$`)
)
func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles) error {
@@ -22,6 +24,8 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
lifecycleKeys := map[string]struct{}{}
transportKeys := map[string]struct{}{}
managerKeys := map[string]struct{}{}
dllExtensionKeys := map[string]struct{}{}
dllExtensionStates := map[string]string{}
discoveryKeys := map[string]struct{}{}
dependencyKeys := map[string]struct{}{}
installPlanKeys := map[string]struct{}{}
@@ -65,6 +69,10 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
if profile.ClientManagerRef != "" {
violations = append(violations, validateProfileKey(prefix+".clientManagerRef", profile.ClientManagerRef)...)
}
for j, key := range profile.DLLExtensionRefs {
violations = append(violations, validateProfileKey(fmt.Sprintf("%s.dllExtensionRefs[%d]", prefix, j), key)...)
}
violations = append(violations, duplicateViolations(prefix+".dllExtensionRefs", profile.DLLExtensionRefs)...)
violations = append(violations, validateRuntimePlatforms(prefix+".platforms", profile.Platforms)...)
}
for i, probe := range profiles.DependencyProbes {
@@ -349,6 +357,15 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
}
}
}
for i, extension := range profiles.DLLExtensions {
prefix := fmt.Sprintf("runtimeProfiles.dllExtensions[%d]", i)
violations = append(violations, validateProfileKey(prefix+".key", extension.Key)...)
violations = append(violations, recordRuntimeProfileKey(dllExtensionKeys, prefix+".key", extension.Key)...)
if extension.Key != "" {
dllExtensionStates[extension.Key] = extension.ReleaseState
}
violations = append(violations, validateRuntimeDLLExtensionProfile(prefix, extension)...)
}
for i, profile := range profiles.LifecycleProfiles {
for _, key := range profile.TransportKeys {
if _, ok := transportKeys[key]; !ok {
@@ -360,10 +377,107 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
violations = append(violations, fmt.Sprintf("runtimeProfiles.lifecycleProfiles[%d].clientManagerRef references undeclared client manager", i))
}
}
if len(profile.DLLExtensionRefs) > 0 {
if profile.Mode != "local-process" || !containsString(profile.Capabilities, domain.LifecycleCapabilityStart) || len(profile.Platforms) != 1 || profile.Platforms[0] != "windows" {
violations = append(violations, fmt.Sprintf("runtimeProfiles.lifecycleProfiles[%d] DLL extensions require a windows local-process start profile", i))
}
for _, key := range profile.DLLExtensionRefs {
state, exists := dllExtensionStates[key]
if !exists {
violations = append(violations, fmt.Sprintf("runtimeProfiles.lifecycleProfiles[%d].dllExtensionRefs references undeclared DLL extension %q", i, key))
continue
}
if state != "ready" {
violations = append(violations, fmt.Sprintf("runtimeProfiles.lifecycleProfiles[%d].dllExtensionRefs references unpublished DLL extension %q", i, key))
}
}
}
}
return finish(violations)
}
func validateRuntimeDLLExtensionProfile(prefix string, extension domain.RuntimeDLLExtensionProfile) []string {
var violations []string
if extension.Kind != "ue4ss-dll" || extension.Activation != "server-start" {
violations = append(violations, prefix+".kind and activation must be ue4ss-dll/server-start")
}
if !validSemanticVersion(extension.Version) {
violations = append(violations, prefix+".version must be semantic")
}
violations = append(violations, validateSafeRuntimeValue(prefix+".displayName", extension.DisplayName)...)
violations = append(violations, validateProfileKey(prefix+".targetKey", extension.TargetKey)...)
if !runtimeDLLModKeyPattern.MatchString(extension.ModKey) {
violations = append(violations, prefix+".modKey is invalid")
}
if extension.DLLRef != "ue4ss/Mods/"+extension.ModKey+"/dlls/main.dll" {
violations = append(violations, prefix+".dllRef must be the declared UE4SS main.dll path")
}
if extension.UpdateOnStart != true {
violations = append(violations, prefix+".updateOnStart must be true")
}
if extension.RCONPort < 1024 || extension.RCONPort > 65535 {
violations = append(violations, prefix+".rconPort must be an unprivileged port")
}
if len(extension.SupportedTargets) != 1 || extension.SupportedTargets[0].OS != "windows" || extension.SupportedTargets[0].Arch != "amd64" {
violations = append(violations, prefix+".supportedTargets must contain only windows/amd64")
}
if extension.ReleaseState != "ready" && extension.ReleaseState != "unpublished" {
violations = append(violations, prefix+".releaseState is invalid")
}
if extension.ReleaseURL != "" {
violations = append(violations, validateRuntimeDLLReleaseURL(prefix+".releaseUrl", extension.ReleaseURL)...)
}
if extension.ReleaseState == "ready" {
if extension.ReleaseURL == "" {
violations = append(violations, prefix+".releaseUrl is required for a ready release")
}
if !validSHA256Checksum(extension.Checksum) || !validSHA256Checksum(extension.SCUMExecutableChecksum) {
violations = append(violations, prefix+".checksum and scumExecutableChecksum must be SHA-256")
}
if extension.SizeBytes < 1 || extension.SizeBytes > 128*1024*1024 {
violations = append(violations, prefix+".sizeBytes is out of bounds")
}
if !runtimeDLLABIPattern.MatchString(extension.UE4SSABI) {
violations = append(violations, prefix+".ue4ssAbi is invalid")
}
}
return violations
}
func validateRuntimeDLLReleaseURL(field string, value string) []string {
parsed, err := url.Parse(value)
host := ""
if parsed != nil {
host = strings.ToLower(parsed.Hostname())
}
ip := net.ParseIP(host)
if err != nil || parsed == nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || parsed.Fragment != "" || parsed.RawQuery != "" || parsed.Port() != "" && parsed.Port() != "443" || host == "localhost" || strings.HasSuffix(host, ".localhost") || strings.HasSuffix(host, ".local") || ip != nil && (ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() || ip.IsLinkLocalUnicast()) || !strings.HasSuffix(strings.ToLower(parsed.Path), ".dll") {
return []string{field + " must be a public credential-free HTTPS DLL URL"}
}
return nil
}
func validateRuntimeDLLExtensionPlan(prefix string, plan domain.RuntimeDLLExtensionPlan) []string {
return validateRuntimeDLLExtensionProfile(prefix, domain.RuntimeDLLExtensionProfile{
Key: plan.Key,
Kind: "ue4ss-dll",
Activation: "server-start",
Version: plan.Version,
ReleaseState: "ready",
ReleaseURL: plan.ReleaseURL,
Checksum: plan.Checksum,
SizeBytes: plan.SizeBytes,
TargetKey: plan.TargetKey,
ModKey: plan.ModKey,
DLLRef: plan.DLLRef,
SCUMExecutableChecksum: plan.SCUMExecutableChecksum,
UE4SSABI: plan.UE4SSABI,
SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}},
UpdateOnStart: true,
RCONPort: plan.RCONPort,
})
}
func validateProfileKey(field, value string) []string {
if strings.TrimSpace(value) == "" {
return []string{field + " is required"}