521 lines
22 KiB
Go
521 lines
22 KiB
Go
package protocol
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
const maxRunLogicalFileKeyLength = 160
|
|
const maxRunExecutionContentBytes = 64 * 1024
|
|
const maxRunDLLExtensionBytes = int64(128 * 1024 * 1024)
|
|
const maxSourceRCONTimeoutSeconds = 60
|
|
const maxProtectedRequestTimeoutSeconds = 120
|
|
const maxProtectedRequestTextBytes = 16 * 1024
|
|
const (
|
|
maxSQLiteSchemaProbeObjects = 512
|
|
maxSQLiteSchemaProbeColumnsPerObject = 256
|
|
maxSQLiteSchemaProbeIndexesPerObject = 128
|
|
maxSQLiteSchemaProbeForeignKeys = 128
|
|
maxSQLiteSchemaProbeCardinalityReads = 512
|
|
maxSQLiteSchemaProbeSamples = 3
|
|
maxSQLiteSchemaProbeTimeoutMS = 10000
|
|
maxSQLiteSchemaProbeResultBytes = 1024 * 1024
|
|
)
|
|
|
|
func ValidateRunJobAssignment(assignment RunJobAssignment) error {
|
|
if assignment.JobID == "" || assignment.RunEndpointID == "" || assignment.Capability == "" {
|
|
return ValidationError("jobId, runEndpointId, and capability are required")
|
|
}
|
|
switch assignment.Capability {
|
|
case RunCapabilityConfigWrite, RunCapabilityFilesList, RunCapabilityFilesRead, RunCapabilityFilesWrite:
|
|
if assignment.ServerInstanceID == "" {
|
|
return ValidationError("serverInstanceId is required for scoped file jobs")
|
|
}
|
|
if !ValidLogicalFileKey(assignment.TargetKey) {
|
|
return ValidationError("targetKey is not allowed")
|
|
}
|
|
}
|
|
switch assignment.Capability {
|
|
case RunCapabilityConfigWrite, RunCapabilityFilesWrite:
|
|
if !ValidScopedInputRef(assignment.InputRef) {
|
|
return ValidationError("inputRef is not allowed")
|
|
}
|
|
}
|
|
if len([]byte(assignment.ExecutionInput.Content)) > maxRunExecutionContentBytes {
|
|
return ValidationError("execution input content is too large")
|
|
}
|
|
if assignment.ExecutionInput.MaxReadBytes < 0 || assignment.ExecutionInput.MaxReadBytes > maxRunExecutionContentBytes {
|
|
return ValidationError("execution input maxReadBytes is out of bounds")
|
|
}
|
|
if assignment.ExecutionInput.WorkspaceScope != "" && !ValidLogicalFileKey(assignment.ExecutionInput.WorkspaceScope) {
|
|
return ValidationError("execution input workspaceScope is not allowed")
|
|
}
|
|
if assignment.ExecutionInput.ServerDeploymentPlan != nil {
|
|
return ValidationError("game-specific server deployment plans are legacy unsupported input")
|
|
}
|
|
if len(assignment.ExecutionInput.DLLExtensions) > 0 {
|
|
if assignment.Capability != RunCapabilityProcessStart || assignment.ServerInstanceID == "" || assignment.ExecutionInput.WorkspaceScope == "" {
|
|
return ValidationError("DLL extensions are allowed only for scoped process.start jobs")
|
|
}
|
|
if len(assignment.ExecutionInput.DLLExtensions) > 16 {
|
|
return ValidationError("too many DLL extensions are declared")
|
|
}
|
|
keys := make(map[string]struct{}, len(assignment.ExecutionInput.DLLExtensions))
|
|
targets := make(map[string]struct{}, len(assignment.ExecutionInput.DLLExtensions))
|
|
for _, plan := range assignment.ExecutionInput.DLLExtensions {
|
|
if err := validateRuntimeDLLExtensionPlan(plan); err != nil {
|
|
return err
|
|
}
|
|
if _, exists := keys[plan.Key]; exists {
|
|
return ValidationError("DLL extension key is duplicated")
|
|
}
|
|
if _, exists := targets[plan.TargetKey]; exists {
|
|
return ValidationError("DLL extension target is duplicated")
|
|
}
|
|
keys[plan.Key] = struct{}{}
|
|
targets[plan.TargetKey] = struct{}{}
|
|
}
|
|
}
|
|
if assignment.ExecutionInput.LogSource != nil {
|
|
if assignment.Capability != RunCapabilityLogsBackfill || assignment.ServerInstanceID == "" {
|
|
return ValidationError("log source is allowed only for logs.backfill jobs")
|
|
}
|
|
if err := validateRuntimeLogSourcePlan(*assignment.ExecutionInput.LogSource); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(assignment.ExecutionInput.LogSources) > 0 {
|
|
if assignment.Capability != RunCapabilityProcessStart || assignment.ServerInstanceID == "" {
|
|
return ValidationError("process log sources are allowed only for process.start jobs")
|
|
}
|
|
seen := map[string]struct{}{}
|
|
for _, source := range assignment.ExecutionInput.LogSources {
|
|
if err := validateRuntimeProcessLogSourcePlan(source); err != nil {
|
|
return err
|
|
}
|
|
if _, exists := seen[source.Kind]; exists {
|
|
return ValidationError("process log source kind is duplicated")
|
|
}
|
|
seen[source.Kind] = struct{}{}
|
|
}
|
|
}
|
|
if assignment.ExecutionInput.SourceRCON != nil {
|
|
isSourceCommand := assignment.Capability == RunCapabilityRemoteRunRCONCommand
|
|
isProtectedRCON := assignment.Capability == RunCapabilityRemoteRunProtectedRCON
|
|
if (!isSourceCommand && !isProtectedRCON) || assignment.ServerInstanceID == "" || assignment.ExecutionInput.WorkspaceScope == "" {
|
|
return ValidationError("Source RCON is allowed only for scoped remote.run.rcon.command or remote.run.protected.rcon jobs")
|
|
}
|
|
if assignment.MaxAttempts != 1 {
|
|
return ValidationError("Source RCON jobs must have exactly one attempt")
|
|
}
|
|
if isSourceCommand && !strings.HasPrefix(assignment.InputRef, "input://source-rcon/") {
|
|
return ValidationError("Source RCON inputRef must be a source-rcon input ref")
|
|
}
|
|
if isProtectedRCON && !strings.HasPrefix(assignment.InputRef, "input://protected-request/") {
|
|
return ValidationError("protected Source RCON inputRef must be a protected-request input ref")
|
|
}
|
|
wantAdapterKind := "rcon"
|
|
if isProtectedRCON {
|
|
wantAdapterKind = protectedRequestAdapterKind(assignment.Capability)
|
|
}
|
|
if assignment.ExecutionInput.RemoteAdapterKind != "" && assignment.ExecutionInput.RemoteAdapterKind != wantAdapterKind {
|
|
return ValidationError("Source RCON requires the rcon adapter kind")
|
|
}
|
|
maxTimeout := maxSourceRCONTimeoutSeconds
|
|
if isProtectedRCON {
|
|
maxTimeout = maxProtectedRequestTimeoutSeconds
|
|
}
|
|
if assignment.ExecutionInput.TimeoutSeconds < 1 || assignment.ExecutionInput.TimeoutSeconds > maxTimeout {
|
|
return ValidationError("Source RCON timeout is out of bounds")
|
|
}
|
|
if err := validateRuntimeSourceRCONPlan(*assignment.ExecutionInput.SourceRCON); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if assignment.ExecutionInput.SQLiteSchemaProbe != nil {
|
|
if assignment.Capability != RunCapabilityRemoteRunDBSQLiteProbe || assignment.ServerInstanceID == "" || assignment.ExecutionInput.WorkspaceScope == "" || assignment.MaxAttempts != 1 || assignment.FencingToken == 0 {
|
|
return ValidationError("SQLite schema probe requires a scoped single fenced probe job")
|
|
}
|
|
if assignment.InputRef != "" || assignment.ExecutionInput.RemoteAdapterKey != "" || assignment.ExecutionInput.RemoteAdapterKind != "" || assignment.ExecutionInput.Content != "" || len(assignment.ExecutionInput.Inputs) != 0 {
|
|
return ValidationError("SQLite schema probe must not carry adapter input or content")
|
|
}
|
|
if !ValidLogicalFileKey(assignment.TargetKey) || !strings.HasPrefix(assignment.TargetKey, "databases/") {
|
|
return ValidationError("SQLite schema probe target must be a logical database target")
|
|
}
|
|
if err := validateSQLiteSchemaProbeRequest(*assignment.ExecutionInput.SQLiteSchemaProbe, assignment); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if isProtectedRequestCapability(assignment.Capability) {
|
|
if assignment.ServerInstanceID == "" || assignment.MaxAttempts != 1 || assignment.FencingToken == 0 {
|
|
return ValidationError("protected requests require a scoped single fenced attempt")
|
|
}
|
|
if !strings.HasPrefix(assignment.InputRef, "input://protected-request/") {
|
|
return ValidationError("protected request inputRef is not allowed")
|
|
}
|
|
if !ValidLogicalFileKey(assignment.TargetKey) || !ValidLogicalFileKey(assignment.ExecutionInput.RemoteAdapterKey) {
|
|
return ValidationError("protected request logical binding is not allowed")
|
|
}
|
|
if assignment.ExecutionInput.RemoteAdapterKind != protectedRequestAdapterKind(assignment.Capability) {
|
|
return ValidationError("protected request adapter kind does not match capability")
|
|
}
|
|
if assignment.ExecutionInput.TimeoutSeconds < 1 || assignment.ExecutionInput.TimeoutSeconds > maxProtectedRequestTimeoutSeconds {
|
|
return ValidationError("protected request timeout is out of bounds")
|
|
}
|
|
if assignment.ExecutionInput.Content != "" {
|
|
return ValidationError("protected request text must not be in a job assignment")
|
|
}
|
|
}
|
|
if strings.HasPrefix(assignment.InputRef, "input://source-rcon/") && assignment.ExecutionInput.SourceRCON == nil {
|
|
return ValidationError("source-rcon inputRef requires a Source RCON plan")
|
|
}
|
|
if IsRemoteCapability(assignment.Capability) {
|
|
if assignment.ServerInstanceID == "" {
|
|
return ValidationError("serverInstanceId is required for remote jobs")
|
|
}
|
|
if RemoteCapabilityRequiresTargetKey(assignment.Capability) && !ValidLogicalFileKey(assignment.TargetKey) {
|
|
return ValidationError("targetKey is not allowed")
|
|
}
|
|
if RemoteCapabilityRequiresInputRef(assignment.Capability) && !ValidScopedInputRef(assignment.InputRef) {
|
|
return ValidationError("inputRef is not allowed")
|
|
}
|
|
if assignment.ExecutionInput.RemoteAdapterKey != "" && !ValidLogicalFileKey(assignment.ExecutionInput.RemoteAdapterKey) {
|
|
return ValidationError("remoteAdapterKey is not allowed")
|
|
}
|
|
if assignment.ExecutionInput.RemoteAdapterKind != "" && !validRemoteAdapterKind(assignment.ExecutionInput.RemoteAdapterKind) {
|
|
return ValidationError("remoteAdapterKind is not allowed")
|
|
}
|
|
if assignment.ExecutionInput.TimeoutSeconds < 0 || assignment.ExecutionInput.TimeoutSeconds > 300 {
|
|
return ValidationError("remote adapter timeout is out of bounds")
|
|
}
|
|
}
|
|
if assignment.Capability == RunCapabilityRemoteRunDBSQLiteProbe && assignment.ExecutionInput.SQLiteSchemaProbe == nil {
|
|
return ValidationError("SQLite schema probe request is required")
|
|
}
|
|
switch assignment.Capability {
|
|
case RunCapabilityDistributionBuild:
|
|
if assignment.ServerInstanceID == "" {
|
|
return ValidationError("serverInstanceId is required for distribution build jobs")
|
|
}
|
|
if !ValidLogicalFileKey(assignment.TargetKey) || !strings.HasPrefix(assignment.TargetKey, "distribution/") {
|
|
return ValidationError("targetKey is not allowed for distribution build jobs")
|
|
}
|
|
if !ValidScopedInputRef(assignment.InputRef) || !strings.HasPrefix(assignment.InputRef, "input://distribution-build/") {
|
|
return ValidationError("inputRef must be a distribution build input ref")
|
|
}
|
|
case RunCapabilityRunSelfUpdate:
|
|
if assignment.ServerInstanceID == "" {
|
|
return ValidationError("serverInstanceId is required for self-update jobs")
|
|
}
|
|
if assignment.TargetKey != "run/update" {
|
|
return ValidationError("targetKey must be run/update")
|
|
}
|
|
if !ValidScopedInputRef(assignment.InputRef) || !strings.HasPrefix(assignment.InputRef, "artifact://") {
|
|
return ValidationError("inputRef must be an artifact ref for self-update")
|
|
}
|
|
case RunCapabilityDependenciesCheck, RunCapabilityDependenciesInstall:
|
|
if assignment.ServerInstanceID == "" {
|
|
return ValidationError("serverInstanceId is required for dependency jobs")
|
|
}
|
|
if !ValidLogicalFileKey(assignment.TargetKey) || !strings.HasPrefix(assignment.TargetKey, "dependencies/") {
|
|
return ValidationError("targetKey is not allowed for dependency jobs")
|
|
}
|
|
if assignment.InputRef != "" {
|
|
return ValidationError("dependency jobs must not carry arbitrary input refs")
|
|
}
|
|
case RunCapabilityLogsBackfill:
|
|
if assignment.ServerInstanceID == "" {
|
|
return ValidationError("serverInstanceId is required for log backfill jobs")
|
|
}
|
|
if !ValidLogicalFileKey(assignment.TargetKey) || !strings.HasPrefix(assignment.TargetKey, "logs/") {
|
|
return ValidationError("targetKey is not allowed for log backfill jobs")
|
|
}
|
|
if assignment.InputRef != "" && !ValidScopedInputRef(assignment.InputRef) {
|
|
return ValidationError("inputRef is not allowed for log backfill jobs")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidProtectedRequestExecutionInput(value ProtectedRequestExecutionInputResponse) bool {
|
|
return value.JobID != "" && value.ServerInstanceID != "" && value.RunEndpointID != "" && value.FencingToken != 0 && value.Authorized && value.ApprovalState == "approved" && value.QueueState == "claimed" && !value.ExpiresAt.IsZero() && time.Now().UTC().Before(value.ExpiresAt) && validProtectedRequestKind(value.Kind) && ValidLogicalFileKey(value.TransportKey) && ValidLogicalFileKey(value.TargetKey) && validProtectedRequestText(value.RequestText)
|
|
}
|
|
|
|
func validProtectedRequestText(value string) bool {
|
|
return strings.TrimSpace(value) != "" && utf8.ValidString(value) && len([]byte(value)) <= maxProtectedRequestTextBytes && !strings.ContainsRune(value, '\x00')
|
|
}
|
|
|
|
func isProtectedRequestCapability(capability string) bool {
|
|
return protectedRequestAdapterKind(capability) != ""
|
|
}
|
|
|
|
func IsProtectedRequestCapability(capability string) bool {
|
|
return isProtectedRequestCapability(capability)
|
|
}
|
|
|
|
func protectedRequestAdapterKind(capability string) string {
|
|
switch capability {
|
|
case RunCapabilityRemoteRunProtectedSQL:
|
|
return "protected-sql"
|
|
case RunCapabilityRemoteRunProtectedRCON:
|
|
return "protected-rcon"
|
|
case RunCapabilityRemoteRunProgram:
|
|
return "protected-program"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func validProtectedRequestKind(kind string) bool {
|
|
return kind == "sql" || kind == "rcon" || kind == "program"
|
|
}
|
|
|
|
func validRemoteAdapterKind(kind string) bool {
|
|
switch kind {
|
|
case "ftp", "rsync", "run-file", "run-process", "database", "rcon", "log-transfer", "protected-sql", "protected-rcon", "protected-program":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func validateSQLiteSchemaProbeRequest(request SQLiteSchemaProbeRequest, assignment RunJobAssignment) error {
|
|
if !validProbeIdentifier(request.RequestID) {
|
|
return ValidationError("SQLite schema probe requestId is not allowed")
|
|
}
|
|
binding := request.Binding
|
|
if binding.ServerInstanceID != assignment.ServerInstanceID || binding.RunEndpointID != assignment.RunEndpointID || !validProbeIdentifier(binding.RunBindingID) || !validProbeIdentifier(binding.PluginID) || !validProbeIdentifier(binding.PluginVersion) || !validProbeIdentifier(binding.AdapterVersion) || !validProbeIdentifier(binding.DatabaseIdentity) {
|
|
return ValidationError("SQLite schema probe binding is invalid")
|
|
}
|
|
if binding.GameVersion != "" && !validProbeIdentifier(binding.GameVersion) {
|
|
return ValidationError("SQLite schema probe gameVersion is invalid")
|
|
}
|
|
if err := validateSQLiteSchemaProbeLimits(request.Limits); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateSQLiteSchemaProbeLimits(limits SQLiteSchemaProbeLimits) error {
|
|
if limits.MaxObjects < 1 || limits.MaxObjects > maxSQLiteSchemaProbeObjects || limits.MaxColumnsPerObject < 1 || limits.MaxColumnsPerObject > maxSQLiteSchemaProbeColumnsPerObject || limits.MaxIndexesPerObject < 0 || limits.MaxIndexesPerObject > maxSQLiteSchemaProbeIndexesPerObject || limits.MaxForeignKeys < 0 || limits.MaxForeignKeys > maxSQLiteSchemaProbeForeignKeys || limits.MaxCardinalityReads < 0 || limits.MaxCardinalityReads > maxSQLiteSchemaProbeCardinalityReads || limits.MaxSampleRows < 0 || limits.MaxSampleRows > maxSQLiteSchemaProbeSamples || limits.TimeoutMS < 1 || limits.TimeoutMS > maxSQLiteSchemaProbeTimeoutMS || limits.MaxResultBytes < 1 || limits.MaxResultBytes > maxSQLiteSchemaProbeResultBytes {
|
|
return ValidationError("SQLite schema probe limits are out of bounds")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validProbeIdentifier(value string) bool {
|
|
return ValidLogicalFileKey(value) && !strings.Contains(value, "/")
|
|
}
|
|
|
|
func validateRuntimeDLLExtensionPlan(plan RuntimeDLLExtensionPlan) error {
|
|
if !ValidLogicalFileKey(plan.Key) || !ValidLogicalFileKey(plan.TargetKey) || !validExtensionVersion(plan.Version) {
|
|
return ValidationError("DLL extension identity is not allowed")
|
|
}
|
|
if !validRuntimeDLLURL(plan.ReleaseURL) || !validSHA256(plan.Checksum) || !validSHA256(plan.SCUMExecutableChecksum) || plan.SizeBytes < 1 || plan.SizeBytes > maxRunDLLExtensionBytes {
|
|
return ValidationError("DLL extension release integrity is not allowed")
|
|
}
|
|
if !validDLLModKey(plan.ModKey) || plan.DLLRef != "ue4ss/Mods/"+plan.ModKey+"/dlls/main.dll" {
|
|
return ValidationError("DLL extension deployment path is not allowed")
|
|
}
|
|
if !validUE4SSABI(plan.UE4SSABI) || plan.RCONPort < 1024 || plan.RCONPort > 65535 {
|
|
return ValidationError("DLL extension compatibility metadata is not allowed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateRuntimeLogSourcePlan(plan RuntimeLogSourcePlan) error {
|
|
if !ValidLogicalFileKey(plan.Key) || !ValidLogicalFileKey(plan.StreamKey) || !ValidLogicalFileKey(plan.TargetKey) {
|
|
return ValidationError("log source identity is not allowed")
|
|
}
|
|
if plan.Kind != "file.tail" {
|
|
return ValidationError("log source kind is unsupported")
|
|
}
|
|
switch plan.CursorKind {
|
|
case "", "offset", "fingerprint":
|
|
default:
|
|
return ValidationError("log source cursor kind is unsupported")
|
|
}
|
|
if plan.RetentionDays < 0 || plan.RetentionDays > 365 {
|
|
return ValidationError("log source retention is out of bounds")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateRuntimeProcessLogSourcePlan(plan RuntimeLogSourcePlan) error {
|
|
if !ValidLogicalFileKey(plan.Key) || !ValidLogicalFileKey(plan.StreamKey) {
|
|
return ValidationError("process log source identity is not allowed")
|
|
}
|
|
switch plan.Kind {
|
|
case "process.stdout", "process.stderr":
|
|
default:
|
|
return ValidationError("process log source kind is unsupported")
|
|
}
|
|
if plan.TargetKey != "" && !ValidLogicalFileKey(plan.TargetKey) {
|
|
return ValidationError("process log source target is not allowed")
|
|
}
|
|
switch plan.CursorKind {
|
|
case "", "sequence":
|
|
default:
|
|
return ValidationError("process log source cursor kind is unsupported")
|
|
}
|
|
if plan.RetentionDays < 0 || plan.RetentionDays > 365 {
|
|
return ValidationError("process log source retention is out of bounds")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateRuntimeSourceRCONPlan(plan RuntimeSourceRCONPlan) error {
|
|
if plan.Protocol != "source-rcon" || !ValidLogicalFileKey(plan.ExtensionKey) || !validDLLModKey(plan.ModKey) {
|
|
return ValidationError("Source RCON identity is not allowed")
|
|
}
|
|
if plan.ConfigRef != "ue4ss/Mods/"+plan.ModKey+"/config.ini" || !ValidLogicalFileKey(plan.ConfigRef) {
|
|
return ValidationError("Source RCON config reference is not allowed")
|
|
}
|
|
if !validSourceRCONDeploymentStateRef(plan.DeploymentStateRef) {
|
|
return ValidationError("Source RCON deployment state reference is not allowed")
|
|
}
|
|
if plan.Port < 1024 || plan.Port > 65535 {
|
|
return ValidationError("Source RCON port is not allowed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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 validRuntimeDLLURL(value string) bool {
|
|
parsed, err := url.ParseRequestURI(value)
|
|
if err != nil || parsed.Scheme != "https" || parsed.Hostname() == "" || parsed.User != nil || parsed.Fragment != "" || parsed.Port() != "" && parsed.Port() != "443" || parsed.RawQuery != "" || !strings.HasSuffix(strings.ToLower(parsed.Path), ".dll") {
|
|
return false
|
|
}
|
|
host := strings.ToLower(parsed.Hostname())
|
|
if host == "localhost" || strings.HasSuffix(host, ".localhost") || strings.HasSuffix(host, ".local") {
|
|
return false
|
|
}
|
|
if ip := net.ParseIP(host); ip != nil && (ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() || ip.IsLinkLocalUnicast()) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func validSHA256(value string) bool {
|
|
if len(value) != len("sha256:")+64 || !strings.HasPrefix(value, "sha256:") {
|
|
return false
|
|
}
|
|
_, err := hex.DecodeString(strings.TrimPrefix(value, "sha256:"))
|
|
return err == nil
|
|
}
|
|
|
|
func validExtensionVersion(value string) bool {
|
|
if len(value) == 0 || len(value) > 80 {
|
|
return false
|
|
}
|
|
for _, char := range value {
|
|
if char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' || char == '.' || char == '-' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func validDLLModKey(value string) bool {
|
|
if len(value) == 0 || len(value) > 80 {
|
|
return false
|
|
}
|
|
for index, char := range value {
|
|
if char >= 'a' && char <= 'z' || char >= '0' && char <= '9' || char == '_' || char == '-' {
|
|
if index > 0 || char != '_' && char != '-' {
|
|
continue
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func validUE4SSABI(value string) bool {
|
|
if len(value) == 0 || len(value) > 80 {
|
|
return false
|
|
}
|
|
for _, char := range value {
|
|
if char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' || char == '.' || char == '_' || char == '-' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
type ValidationError string
|
|
|
|
func (err ValidationError) Error() string { return string(err) }
|
|
|
|
func ValidLogicalFileKey(key string) bool {
|
|
trimmed := strings.TrimSpace(key)
|
|
if trimmed == "" || trimmed != key || len([]rune(key)) > maxRunLogicalFileKeyLength {
|
|
return false
|
|
}
|
|
lower := strings.ToLower(key)
|
|
if strings.HasPrefix(key, "/") || strings.Contains(key, "..") || strings.Contains(key, `\`) || strings.Contains(key, "://") || strings.Contains(lower, "/users/") || strings.Contains(lower, "password=") || strings.Contains(lower, "secret=") || strings.Contains(lower, "sk-") || strings.Contains(lower, "bearer ") {
|
|
return false
|
|
}
|
|
for _, char := range key {
|
|
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_' || char == '-' || char == '.' || char == '/' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func ValidScopedInputRef(ref string) bool {
|
|
trimmed := strings.TrimSpace(ref)
|
|
lower := strings.ToLower(ref)
|
|
if trimmed == "" || trimmed != ref || strings.Contains(lower, "/users/") || strings.Contains(lower, "password=") || strings.Contains(lower, "secret=") || strings.Contains(lower, "sk-") || strings.Contains(lower, "bearer ") {
|
|
return false
|
|
}
|
|
return strings.HasPrefix(ref, "input://") || strings.HasPrefix(ref, "artifact://")
|
|
}
|
|
|
|
func IsRemoteCapability(capability string) bool {
|
|
return strings.HasPrefix(capability, "remote.")
|
|
}
|
|
|
|
func RemoteCapabilityRequiresTargetKey(capability string) bool {
|
|
switch capability {
|
|
case RunCapabilityRemoteRunProcessStart, RunCapabilityRemoteRunProcessStop:
|
|
return false
|
|
default:
|
|
return IsRemoteCapability(capability)
|
|
}
|
|
}
|
|
|
|
func RemoteCapabilityRequiresInputRef(capability string) bool {
|
|
switch capability {
|
|
case RunCapabilityRemoteFTPWrite,
|
|
RunCapabilityRemoteRsyncWrite,
|
|
RunCapabilityRemoteRunFilesWrite,
|
|
RunCapabilityRemoteRunDBMySQLQuery,
|
|
RunCapabilityRemoteRunDBSQLiteQuery,
|
|
RunCapabilityRemoteRunRCONCommand,
|
|
RunCapabilityRemoteRunProtectedSQL,
|
|
RunCapabilityRemoteRunProtectedRCON,
|
|
RunCapabilityRemoteRunProgram:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|