- LifecycleActionTemplate 新增 gracefulStop:stop 动作可声明插件自有的优雅关闭命令、 参数、环境、超时与 fallback;超时且 fallback=report 时任务失败,避免默认杀进程。 - 依赖探针新增 steam.update:通过 steamcmd +app_info_print 读取公开分支 buildid, 与本地 appmanifest_<appid>.acf 比较,输出 installed/latest/update=yes|no|unknown。 - 依赖执行输入新增 ServerRoot,供探针定位插件声明的服务器安装目录。
276 lines
11 KiB
Go
276 lines
11 KiB
Go
package protocol
|
|
|
|
import "strings"
|
|
|
|
type RunAutonomousLifecyclePlan struct {
|
|
SchemaVersion string `json:"schemaVersion"`
|
|
ServerInstanceID string `json:"serverInstanceId"`
|
|
PluginID string `json:"pluginId"`
|
|
PluginVersion string `json:"pluginVersion"`
|
|
RunEndpointID string `json:"runEndpointId"`
|
|
ProfileKey string `json:"profileKey,omitempty"`
|
|
TargetOS string `json:"targetOs"`
|
|
TargetArch string `json:"targetArch"`
|
|
TargetRelease string `json:"targetRelease"`
|
|
DeploymentRevision int `json:"deploymentRevision,omitempty"`
|
|
Bootstrap *RunAutonomousLifecycleAction `json:"bootstrap,omitempty"`
|
|
Actions []RunAutonomousLifecycleAction `json:"actions,omitempty"`
|
|
DependencyProbes []DependencyProbe `json:"dependencyProbes,omitempty"`
|
|
InstallPlans []DependencyInstallPlan `json:"installPlans,omitempty"`
|
|
LogSources []RuntimeLogSourcePlan `json:"logSources,omitempty"`
|
|
DLLExtensions []RuntimeDLLExtensionPlan `json:"dllExtensions,omitempty"`
|
|
DataTargets []RunAutonomousDataTarget `json:"dataTargets,omitempty"`
|
|
RuntimeBindings map[string]string `json:"runtimeBindings,omitempty"`
|
|
Deployment *RunAutonomousDeployment `json:"deployment,omitempty"`
|
|
}
|
|
|
|
type RunAutonomousLifecycleAction struct {
|
|
Action string `json:"action"`
|
|
Operation string `json:"operation"`
|
|
Capability string `json:"capability"`
|
|
TargetKey string `json:"targetKey"`
|
|
}
|
|
|
|
type RunAutonomousDeployment struct {
|
|
SchemaVersion string `json:"schemaVersion"`
|
|
Mode string `json:"mode"`
|
|
ProfileKey string `json:"profileKey,omitempty"`
|
|
RuntimeBindings map[string]string `json:"runtimeBindings,omitempty"`
|
|
CreateInputs map[string]string `json:"createInputs,omitempty"`
|
|
ServerRoot string `json:"serverRoot,omitempty"`
|
|
WorkingDirectory string `json:"workingDirectory,omitempty"`
|
|
InstallCommand string `json:"installCommand,omitempty"`
|
|
StartCommand string `json:"startCommand,omitempty"`
|
|
StopCommand string `json:"stopCommand,omitempty"`
|
|
StatusCommand string `json:"statusCommand,omitempty"`
|
|
Shell string `json:"shell,omitempty"`
|
|
Revision int `json:"revision,omitempty"`
|
|
}
|
|
|
|
type RunAutonomousDataTarget struct {
|
|
Key string `json:"key"`
|
|
Kind string `json:"kind"`
|
|
TransportKey string `json:"transportKey"`
|
|
SourceRootKey string `json:"sourceRootKey"`
|
|
SourcePath string `json:"sourcePath"`
|
|
WorkspaceKey string `json:"workspaceKey"`
|
|
RefreshPolicy string `json:"refreshPolicy"`
|
|
MaxBytes int64 `json:"maxBytes,omitempty"`
|
|
Platforms []string `json:"platforms,omitempty"`
|
|
}
|
|
|
|
const maxAutonomousDataTargetBytes = int64(1024 * 1024 * 1024)
|
|
|
|
func ValidateRunAutonomousLifecyclePlan(plan RunAutonomousLifecyclePlan) error {
|
|
if plan.SchemaVersion != "1" {
|
|
return ValidationError("autonomous lifecycle plan schema is unsupported")
|
|
}
|
|
if !ValidLogicalFileKey(plan.ServerInstanceID) || !ValidLogicalFileKey(plan.RunEndpointID) || !validAutonomousPluginID(plan.PluginID) {
|
|
return ValidationError("autonomous lifecycle plan identity is invalid")
|
|
}
|
|
if plan.ProfileKey != "" && !ValidLogicalFileKey(plan.ProfileKey) {
|
|
return ValidationError("autonomous lifecycle profile key is invalid")
|
|
}
|
|
if !validAutonomousTarget(plan.TargetOS, plan.TargetArch) {
|
|
return ValidationError("autonomous lifecycle target platform is invalid")
|
|
}
|
|
if plan.TargetRelease != "" && !validAutonomousToken(plan.TargetRelease, 240) {
|
|
return ValidationError("autonomous lifecycle target release is invalid")
|
|
}
|
|
if plan.PluginVersion != "" && !validAutonomousToken(plan.PluginVersion, 120) {
|
|
return ValidationError("autonomous lifecycle plugin version is invalid")
|
|
}
|
|
if plan.Bootstrap != nil {
|
|
if err := validateAutonomousLifecycleAction(*plan.Bootstrap); err != nil {
|
|
return err
|
|
}
|
|
if plan.Bootstrap.Capability != RunCapabilityProcessInstall && plan.Bootstrap.Capability != RunCapabilityProcessStart {
|
|
return ValidationError("autonomous lifecycle bootstrap action is invalid")
|
|
}
|
|
}
|
|
if len(plan.Actions) > 16 || len(plan.DependencyProbes) > 64 || len(plan.InstallPlans) > 64 || len(plan.LogSources) > 16 || len(plan.DLLExtensions) > 16 || len(plan.DataTargets) > 16 {
|
|
return ValidationError("autonomous lifecycle plan is too large")
|
|
}
|
|
for _, action := range plan.Actions {
|
|
if err := validateAutonomousLifecycleAction(action); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, probe := range plan.DependencyProbes {
|
|
if !ValidLogicalFileKey(probe.Key) || !ValidLogicalFileKey(probe.TargetKey) || !validAutonomousToken(probe.Kind, 80) || probe.MinimumVersion != "" && !validAutonomousToken(probe.MinimumVersion, 80) || probe.SteamAppID != "" && !validSteamAppID(probe.SteamAppID) {
|
|
return ValidationError("autonomous dependency probe is invalid")
|
|
}
|
|
}
|
|
for _, installPlan := range plan.InstallPlans {
|
|
if !ValidLogicalFileKey(installPlan.Key) || len(installPlan.Steps) > 64 {
|
|
return ValidationError("autonomous install plan is invalid")
|
|
}
|
|
for _, step := range installPlan.Steps {
|
|
if !validAutonomousInstallStepType(step.Type) || !ValidLogicalFileKey(step.TargetKey) || step.PackageManager != "" && !validAutonomousToken(step.PackageManager, 80) || step.PackageName != "" && !validAutonomousToken(step.PackageName, 160) || step.Version != "" && !validAutonomousToken(step.Version, 120) {
|
|
return ValidationError("autonomous install step is invalid")
|
|
}
|
|
}
|
|
}
|
|
for _, source := range plan.LogSources {
|
|
if err := validateRuntimeProcessLogSourcePlan(source); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
seenDataTargets := map[string]struct{}{}
|
|
seenWorkspaceTargets := map[string]struct{}{}
|
|
for _, target := range plan.DataTargets {
|
|
if err := validateAutonomousDataTarget(target); err != nil {
|
|
return err
|
|
}
|
|
if _, exists := seenDataTargets[target.Key]; exists {
|
|
return ValidationError("autonomous data target key is duplicated")
|
|
}
|
|
if _, exists := seenWorkspaceTargets[target.WorkspaceKey]; exists {
|
|
return ValidationError("autonomous data target workspace is duplicated")
|
|
}
|
|
seenDataTargets[target.Key] = struct{}{}
|
|
seenWorkspaceTargets[target.WorkspaceKey] = struct{}{}
|
|
}
|
|
if plan.Deployment != nil {
|
|
if err := validateAutonomousDeployment(*plan.Deployment); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validAutonomousInstallStepType(value string) bool {
|
|
switch value {
|
|
case "package", "verified-download", "manual":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func validateAutonomousDataTarget(target RunAutonomousDataTarget) error {
|
|
if !ValidLogicalFileKey(target.Key) || !ValidLogicalFileKey(target.TransportKey) || !ValidLogicalFileKey(target.SourceRootKey) || !ValidLogicalFileKey(target.SourcePath) || !ValidLogicalFileKey(target.WorkspaceKey) {
|
|
return ValidationError("autonomous data target is invalid")
|
|
}
|
|
if target.Kind != "sqlite.snapshot" || target.RefreshPolicy != "on-demand-snapshot" || !strings.HasPrefix(target.WorkspaceKey, "databases/") {
|
|
return ValidationError("autonomous data target is invalid")
|
|
}
|
|
if target.MaxBytes <= 0 || target.MaxBytes > maxAutonomousDataTargetBytes {
|
|
return ValidationError("autonomous data target byte limit is invalid")
|
|
}
|
|
for _, platform := range target.Platforms {
|
|
if platform != "windows" && platform != "linux" && platform != "darwin" {
|
|
return ValidationError("autonomous data target platform is invalid")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateAutonomousLifecycleAction(action RunAutonomousLifecycleAction) error {
|
|
if !ValidLogicalFileKey(action.TargetKey) || !validAutonomousLifecycleCapability(action.Capability) || !validAutonomousLifecycleName(action.Action) || !validAutonomousLifecycleName(action.Operation) {
|
|
return ValidationError("autonomous lifecycle action is invalid")
|
|
}
|
|
if expected := autonomousCapabilityForAction(action.Action); expected != "" && expected != action.Capability {
|
|
return ValidationError("autonomous lifecycle action capability mismatch")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func autonomousCapabilityForAction(action string) string {
|
|
switch action {
|
|
case "create":
|
|
return RunCapabilityProcessInstall
|
|
case "start":
|
|
return RunCapabilityProcessStart
|
|
case "stop":
|
|
return RunCapabilityProcessStop
|
|
case "status":
|
|
return RunCapabilityProcessStatus
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func validAutonomousLifecycleCapability(capability string) bool {
|
|
switch capability {
|
|
case RunCapabilityProcessInstall, RunCapabilityProcessStart, RunCapabilityProcessStop, RunCapabilityProcessStatus:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func validAutonomousLifecycleName(value string) bool {
|
|
switch value {
|
|
case "create", "install", "start", "stop", "status":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func validateAutonomousDeployment(deployment RunAutonomousDeployment) error {
|
|
if deployment.SchemaVersion != "1" || deployment.Mode == "" || deployment.Revision < 0 || deployment.ProfileKey != "" && !ValidLogicalFileKey(deployment.ProfileKey) {
|
|
return ValidationError("autonomous deployment is invalid")
|
|
}
|
|
for key := range deployment.RuntimeBindings {
|
|
if !ValidLogicalFileKey(key) {
|
|
return ValidationError("autonomous deployment binding is invalid")
|
|
}
|
|
}
|
|
for key := range deployment.CreateInputs {
|
|
if !ValidLogicalFileKey(key) {
|
|
return ValidationError("autonomous deployment input is invalid")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validAutonomousPluginID(value string) bool {
|
|
return ValidLogicalFileKey(value)
|
|
}
|
|
|
|
func validAutonomousTarget(osName string, arch string) bool {
|
|
switch osName {
|
|
case "windows", "linux", "darwin":
|
|
default:
|
|
return false
|
|
}
|
|
switch arch {
|
|
case "amd64", "arm64":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func validSteamAppID(value string) bool {
|
|
if value == "" || len(value) > 10 {
|
|
return false
|
|
}
|
|
for _, char := range value {
|
|
if char < '0' || char > '9' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func validAutonomousToken(value string, maxRunes int) bool {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" || trimmed != value || len([]rune(value)) > maxRunes {
|
|
return false
|
|
}
|
|
lower := strings.ToLower(value)
|
|
if strings.Contains(value, "..") || strings.Contains(value, `\`) || strings.Contains(value, "://") || strings.Contains(lower, "/users/") || strings.Contains(lower, "password=") || strings.Contains(lower, "secret=") || strings.Contains(lower, "bearer ") || strings.Contains(lower, "sk-") {
|
|
return false
|
|
}
|
|
for _, char := range value {
|
|
if char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' || char == '_' || char == '-' || char == '.' || char == '/' || char == ':' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|