557 lines
21 KiB
Go
557 lines
21 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
pathpkg "path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"browser.local/run/protocol"
|
|
)
|
|
|
|
const (
|
|
ue4ssExtensionStateRoot = "runtime/ue4ss-dll"
|
|
ue4ssExtensionMarkerVersion = 2
|
|
maxUE4SSMetadataBytes int64 = 16 * 1024
|
|
maxUE4SSDLLBytes int64 = 128 * 1024 * 1024
|
|
maxSCUMExecutableBytes int64 = 2 * 1024 * 1024 * 1024
|
|
managedRCONConfigMarker = "; managed by Run UE4SS DLL extension"
|
|
)
|
|
|
|
type dllExtensionError struct {
|
|
code string
|
|
message string
|
|
}
|
|
|
|
func (err dllExtensionError) Error() string { return err.message }
|
|
|
|
type managedDLLExtensionMarker struct {
|
|
Version int `json:"version"`
|
|
ReleaseVersion string `json:"releaseVersion"`
|
|
Checksum string `json:"checksum"`
|
|
SizeBytes int64 `json:"sizeBytes"`
|
|
ExtensionKey string `json:"extensionKey"`
|
|
ModKey string `json:"modKey"`
|
|
ConfigRef string `json:"configRef"`
|
|
RCONPort int `json:"rconPort"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
func (executor LifecycleExecutor) synchronizeUE4SSDLLExtensions(ctx context.Context, assignment protocol.RunJobAssignment, template LifecycleActionTemplate, scope string) error {
|
|
if executor.runtimeTargetOS != "windows" || executor.runtimeTargetArch != "amd64" {
|
|
return dllExtensionError{code: "unsupported_extension_platform", message: "UE4SS DLL extensions require Windows amd64"}
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
executableKey := template.TargetExecutableKey
|
|
// Older generated plugin packages do not have targetExecutableKey yet,
|
|
// but SCUM's existing start action already carries the same logical path
|
|
// in SERVER_EXECUTABLE_REF. Keep those packages forward-compatible.
|
|
if executableKey == "" && template.Environment != nil {
|
|
executableKey = template.Environment["SERVER_EXECUTABLE_REF"]
|
|
}
|
|
if executableKey == "" && template.Env != nil {
|
|
executableKey = template.Env["SERVER_EXECUTABLE_REF"]
|
|
}
|
|
if executableKey == "" && strings.HasSuffix(strings.ToLower(template.ExecutableKey), ".exe") {
|
|
executableKey = template.ExecutableKey
|
|
}
|
|
if executableKey == "" || !strings.HasSuffix(strings.ToLower(executableKey), ".exe") {
|
|
return dllExtensionError{code: "extension_scum_executable_invalid", message: "UE4SS DLL extensions require a declared SCUM executable"}
|
|
}
|
|
|
|
resolver := NewWorkspaceResolver(executor.workspaceRoot)
|
|
targetResolver, targetScope := resolver, scope
|
|
if deployment := assignment.ExecutionInput.Deployment; deployment != nil && deployment.ServerRoot != "" {
|
|
root := filepath.Clean(deployment.ServerRoot)
|
|
if root == "." || !filepath.IsAbs(root) {
|
|
return dllExtensionError{code: "extension_scum_executable_invalid", message: "declared executable root is unsafe"}
|
|
}
|
|
targetResolver = NewWorkspaceResolver(filepath.Dir(root))
|
|
targetScope = root
|
|
}
|
|
executable, err := declaredLifecycleExecutable(targetResolver, targetScope, executableKey)
|
|
if err != nil {
|
|
return dllExtensionError{code: "extension_scum_executable_invalid", message: "declared SCUM executable is unavailable"}
|
|
}
|
|
executableChecksum, _, err := checksumRegularFile(executable, maxSCUMExecutableBytes)
|
|
if err != nil {
|
|
return dllExtensionError{code: "extension_scum_executable_invalid", message: "declared SCUM executable cannot be verified"}
|
|
}
|
|
for _, plan := range assignment.ExecutionInput.DLLExtensions {
|
|
if !strings.EqualFold(executableChecksum, plan.SCUMExecutableChecksum) {
|
|
return dllExtensionError{code: "extension_scum_checksum_mismatch", message: "declared SCUM executable does not match the extension release"}
|
|
}
|
|
}
|
|
|
|
gameRootKey, err := gameRootKeyForExecutable(template.ExecutableKey)
|
|
if err != nil {
|
|
return dllExtensionError{code: "extension_scum_executable_invalid", message: "declared SCUM executable location is unsafe"}
|
|
}
|
|
if err := verifyUE4SSBootstrap(targetResolver, targetScope, gameRootKey); err != nil {
|
|
return err
|
|
}
|
|
for _, plan := range assignment.ExecutionInput.DLLExtensions {
|
|
if err := executor.synchronizeUE4SSDLLExtension(ctx, targetResolver, targetScope, gameRootKey, plan); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func declaredLifecycleExecutable(resolver WorkspaceResolver, scope string, executableKey string) (string, error) {
|
|
if !protocol.ValidLogicalFileKey(filepath.ToSlash(executableKey)) {
|
|
return "", fmt.Errorf("declared executable key is unsafe")
|
|
}
|
|
return resolver.ExistingTarget(scope, executableKey)
|
|
}
|
|
|
|
func (executor LifecycleExecutor) synchronizeUE4SSDLLExtension(ctx context.Context, resolver WorkspaceResolver, scope string, gameRootKey string, plan protocol.RuntimeDLLExtensionPlan) error {
|
|
activeKey := gameRelativeKey(gameRootKey, plan.DLLRef)
|
|
configRef := managedRCONConfigRef(gameRootKey, plan.ModKey)
|
|
activePath, _, err := resolver.WritableTarget(scope, activeKey)
|
|
if err != nil {
|
|
return dllExtensionError{code: "dll_extension_workspace_failed", message: "extension deployment workspace is unavailable"}
|
|
}
|
|
markerPath, stagePath, previousPath, err := extensionStatePaths(resolver, scope, plan)
|
|
if err != nil {
|
|
return dllExtensionError{code: "dll_extension_workspace_failed", message: "extension state workspace is unavailable"}
|
|
}
|
|
marker, markerFound, err := loadManagedDLLExtensionMarker(markerPath)
|
|
if err != nil {
|
|
return dllExtensionError{code: "dll_extension_state_failed", message: "extension release state cannot be read"}
|
|
}
|
|
unchanged := markerFound && markerMatchesDeployment(marker, plan, configRef) && managedDLLMatchesPlan(activePath, plan)
|
|
if !unchanged {
|
|
_ = os.Remove(stagePath)
|
|
defer os.Remove(stagePath)
|
|
downloadedSize, downloadedChecksum, downloadErr := executor.dependencyDownloader.Download(ctx, plan.ReleaseURL, stagePath, plan.SizeBytes)
|
|
if downloadErr != nil {
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
return dllExtensionError{code: "dll_extension_download_failed", message: "declared DLL download failed"}
|
|
}
|
|
verifiedChecksum, verifiedSize, verifyErr := checksumRegularFile(stagePath, plan.SizeBytes)
|
|
if verifyErr != nil || downloadedSize != plan.SizeBytes || verifiedSize != plan.SizeBytes || !strings.EqualFold(downloadedChecksum, plan.Checksum) || !strings.EqualFold(verifiedChecksum, plan.Checksum) {
|
|
return dllExtensionError{code: "dll_extension_verify_failed", message: "declared DLL did not match its fixed release checksum"}
|
|
}
|
|
}
|
|
if err := executor.ensureLoopbackRCONConfig(resolver, scope, gameRootKey, plan); err != nil {
|
|
return err
|
|
}
|
|
if err := executor.ensureUE4SSModsIndex(resolver, scope, gameRootKey, plan.ModKey); err != nil {
|
|
return err
|
|
}
|
|
if unchanged {
|
|
return nil
|
|
}
|
|
if err := executor.activateManagedDLLExtension(activePath, stagePath, previousPath, markerPath, plan, configRef); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func verifyUE4SSBootstrap(resolver WorkspaceResolver, scope string, gameRootKey string) error {
|
|
for _, filename := range []string{"dwmapi.dll", "UE4SS.dll"} {
|
|
if _, err := resolver.ExistingTarget(scope, gameRelativeKey(gameRootKey, filename)); err != nil {
|
|
return dllExtensionError{code: "ue4ss_bootstrap_missing", message: "required UE4SS bootstrap files are not installed"}
|
|
}
|
|
}
|
|
if err := existingRuntimeDirectory(scope, gameRelativeKey(gameRootKey, "ue4ss")); err != nil {
|
|
return dllExtensionError{code: "ue4ss_bootstrap_missing", message: "required UE4SS bootstrap files are not installed"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func gameRootKeyForExecutable(executableKey string) (string, error) {
|
|
normalized := filepath.ToSlash(executableKey)
|
|
if !protocol.ValidLogicalFileKey(normalized) || strings.HasPrefix(normalized, "/") || strings.Contains(normalized, `\`) {
|
|
return "", fmt.Errorf("executable key is unsafe")
|
|
}
|
|
parent := pathpkg.Dir(normalized)
|
|
if parent == "." {
|
|
return "", nil
|
|
}
|
|
return parent, nil
|
|
}
|
|
|
|
func gameRelativeKey(gameRootKey string, relativeKey string) string {
|
|
if gameRootKey == "" {
|
|
return relativeKey
|
|
}
|
|
return gameRootKey + "/" + relativeKey
|
|
}
|
|
|
|
func extensionStatePaths(resolver WorkspaceResolver, scope string, plan protocol.RuntimeDLLExtensionPlan) (string, string, string, error) {
|
|
baseKey := ue4ssExtensionStateRoot + "/" + plan.TargetKey
|
|
markerPath, _, err := resolver.WritableTarget(scope, baseKey+"/release.json")
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
stagePath, _, err := resolver.WritableTarget(scope, baseKey+"/download.staged")
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
previousPath, _, err := resolver.WritableTarget(scope, baseKey+"/previous.dll")
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
return markerPath, stagePath, previousPath, nil
|
|
}
|
|
|
|
func loadManagedDLLExtensionMarker(path string) (managedDLLExtensionMarker, bool, error) {
|
|
body, found, err := readBoundedRegularFile(path, maxUE4SSMetadataBytes)
|
|
if err != nil || !found {
|
|
return managedDLLExtensionMarker{}, found, err
|
|
}
|
|
var marker managedDLLExtensionMarker
|
|
if err := json.Unmarshal(body, &marker); err != nil {
|
|
return managedDLLExtensionMarker{}, false, nil
|
|
}
|
|
if marker.Version != ue4ssExtensionMarkerVersion || !protocol.ValidLogicalFileKey(marker.ExtensionKey) || !protocol.ValidLogicalFileKey(marker.ModKey) || !managedRCONConfigRefForMod(marker.ConfigRef, marker.ModKey) || !protocolValidSHA256(marker.Checksum) || marker.SizeBytes < 1 || marker.ReleaseVersion == "" || marker.RCONPort < 1024 || marker.RCONPort > 65535 {
|
|
return managedDLLExtensionMarker{}, false, nil
|
|
}
|
|
return marker, true, nil
|
|
}
|
|
|
|
func markerMatchesPlan(marker managedDLLExtensionMarker, plan protocol.RuntimeDLLExtensionPlan) bool {
|
|
return marker.Version == ue4ssExtensionMarkerVersion && marker.ReleaseVersion == plan.Version && strings.EqualFold(marker.Checksum, plan.Checksum) && marker.SizeBytes == plan.SizeBytes && marker.ExtensionKey == plan.Key && marker.ModKey == plan.ModKey && marker.RCONPort == plan.RCONPort
|
|
}
|
|
|
|
func markerMatchesDeployment(marker managedDLLExtensionMarker, plan protocol.RuntimeDLLExtensionPlan, configRef string) bool {
|
|
return markerMatchesPlan(marker, plan) && marker.ConfigRef == configRef
|
|
}
|
|
|
|
func managedDLLMatchesPlan(path string, plan protocol.RuntimeDLLExtensionPlan) bool {
|
|
checksum, size, err := checksumRegularFile(path, plan.SizeBytes)
|
|
return err == nil && size == plan.SizeBytes && strings.EqualFold(checksum, plan.Checksum)
|
|
}
|
|
|
|
func (executor LifecycleExecutor) activateManagedDLLExtension(activePath string, stagePath string, previousPath string, markerPath string, plan protocol.RuntimeDLLExtensionPlan, configRef string) error {
|
|
if _, _, err := checksumRegularFile(stagePath, plan.SizeBytes); err != nil {
|
|
return dllExtensionError{code: "dll_extension_verify_failed", message: "staged DLL cannot be verified"}
|
|
}
|
|
previousMarker, previousMarkerFound, markerErr := readBoundedRegularFile(markerPath, maxUE4SSMetadataBytes)
|
|
if markerErr != nil {
|
|
return dllExtensionError{code: "dll_extension_state_failed", message: "extension release state cannot be read"}
|
|
}
|
|
activeExists := false
|
|
if _, _, err := checksumRegularFile(activePath, maxUE4SSDLLBytes); err == nil {
|
|
activeExists = true
|
|
if err := copyRegularFileAtomic(activePath, previousPath, maxUE4SSDLLBytes, 0o600); err != nil {
|
|
return dllExtensionError{code: "dll_extension_activation_failed", message: "previous DLL could not be retained"}
|
|
}
|
|
} else if !errors.Is(err, os.ErrNotExist) {
|
|
return dllExtensionError{code: "dll_extension_activation_failed", message: "current DLL cannot be safely replaced"}
|
|
}
|
|
if err := os.Rename(stagePath, activePath); err != nil {
|
|
return dllExtensionError{code: "dll_extension_activation_failed", message: "verified DLL could not be activated"}
|
|
}
|
|
if err := os.Chmod(activePath, 0o600); err != nil {
|
|
rollbackManagedDLLExtension(activePath, previousPath, activeExists)
|
|
return dllExtensionError{code: "dll_extension_activation_failed", message: "activated DLL permissions could not be secured"}
|
|
}
|
|
marker := managedDLLExtensionMarker{Version: ue4ssExtensionMarkerVersion, ReleaseVersion: plan.Version, Checksum: strings.ToLower(plan.Checksum), SizeBytes: plan.SizeBytes, ExtensionKey: plan.Key, ModKey: plan.ModKey, ConfigRef: configRef, RCONPort: plan.RCONPort, UpdatedAt: time.Now().UTC()}
|
|
body, err := json.Marshal(marker)
|
|
if err != nil || executor.writeRuntimeFile(markerPath, body, 0o600) != nil {
|
|
rollbackManagedDLLExtension(activePath, previousPath, activeExists)
|
|
if previousMarkerFound {
|
|
_ = executor.writeRuntimeFile(markerPath, previousMarker, 0o600)
|
|
} else {
|
|
_ = os.Remove(markerPath)
|
|
}
|
|
return dllExtensionError{code: "dll_extension_activation_failed", message: "extension release state could not be activated"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func rollbackManagedDLLExtension(activePath string, previousPath string, activeExists bool) {
|
|
if activeExists {
|
|
_ = copyRegularFileAtomic(previousPath, activePath, maxUE4SSDLLBytes, 0o600)
|
|
return
|
|
}
|
|
_ = os.Remove(activePath)
|
|
}
|
|
|
|
func (executor LifecycleExecutor) ensureLoopbackRCONConfig(resolver WorkspaceResolver, scope string, gameRootKey string, plan protocol.RuntimeDLLExtensionPlan) error {
|
|
configKey := managedRCONConfigRef(gameRootKey, plan.ModKey)
|
|
configPath, _, err := resolver.WritableTarget(scope, configKey)
|
|
if err != nil {
|
|
return dllExtensionError{code: "dll_extension_config_failed", message: "loopback RCON configuration cannot be prepared"}
|
|
}
|
|
if managedLoopbackRCONConfigMatches(configPath, plan.RCONPort) {
|
|
return nil
|
|
}
|
|
password, err := randomRCONPassword()
|
|
if err != nil {
|
|
return dllExtensionError{code: "dll_extension_config_failed", message: "loopback RCON configuration cannot be secured"}
|
|
}
|
|
body := fmt.Sprintf("%s\n[rcon]\nbind_address=127.0.0.1\nport=%d\npassword=%s\n", managedRCONConfigMarker, plan.RCONPort, password)
|
|
if err := executor.writeRuntimeFile(configPath, []byte(body), 0o600); err != nil {
|
|
return dllExtensionError{code: "dll_extension_config_failed", message: "loopback RCON configuration cannot be written"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func managedRCONConfigRef(gameRootKey string, modKey string) string {
|
|
return gameRelativeKey(gameRootKey, "ue4ss/Mods/"+modKey+"/config.ini")
|
|
}
|
|
|
|
func managedRCONConfigRefForMod(configRef string, modKey string) bool {
|
|
baseRef := "ue4ss/Mods/" + modKey + "/config.ini"
|
|
return protocol.ValidLogicalFileKey(configRef) && (configRef == baseRef || strings.HasSuffix(configRef, "/"+baseRef))
|
|
}
|
|
|
|
func managedLoopbackRCONConfigMatches(path string, port int) bool {
|
|
body, found, err := readBoundedRegularFile(path, maxUE4SSMetadataBytes)
|
|
if err != nil || !found {
|
|
return false
|
|
}
|
|
content := strings.ReplaceAll(string(body), "\r\n", "\n")
|
|
if !strings.Contains(content, managedRCONConfigMarker) {
|
|
return false
|
|
}
|
|
values := map[string]string{}
|
|
inRCON := false
|
|
for _, rawLine := range strings.Split(content, "\n") {
|
|
line := strings.TrimSpace(rawLine)
|
|
if line == "[rcon]" {
|
|
inRCON = true
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "[") {
|
|
inRCON = false
|
|
continue
|
|
}
|
|
if !inRCON || line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
key, value, ok := strings.Cut(line, "=")
|
|
if !ok {
|
|
continue
|
|
}
|
|
key = strings.TrimSpace(key)
|
|
if key != "bind_address" && key != "port" && key != "password" {
|
|
continue
|
|
}
|
|
if _, duplicate := values[key]; duplicate {
|
|
return false
|
|
}
|
|
values[key] = strings.TrimSpace(value)
|
|
}
|
|
configuredPort, err := strconv.Atoi(values["port"])
|
|
if err != nil || values["bind_address"] != "127.0.0.1" || configuredPort != port || len(values["password"]) != 64 {
|
|
return false
|
|
}
|
|
_, err = hex.DecodeString(values["password"])
|
|
return err == nil
|
|
}
|
|
|
|
func randomRCONPassword() (string, error) {
|
|
bytes := make([]byte, 32)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(bytes), nil
|
|
}
|
|
|
|
func (executor LifecycleExecutor) ensureUE4SSModsIndex(resolver WorkspaceResolver, scope string, gameRootKey string, modKey string) error {
|
|
modsKey := gameRelativeKey(gameRootKey, "ue4ss/Mods/mods.txt")
|
|
modsPath, _, err := resolver.WritableTarget(scope, modsKey)
|
|
if err != nil {
|
|
return dllExtensionError{code: "dll_extension_mods_failed", message: "UE4SS mods index cannot be prepared"}
|
|
}
|
|
body, found, err := readBoundedRegularFile(modsPath, maxUE4SSMetadataBytes)
|
|
if err != nil {
|
|
return dllExtensionError{code: "dll_extension_mods_failed", message: "UE4SS mods index cannot be read"}
|
|
}
|
|
content := ""
|
|
if found {
|
|
content = strings.ReplaceAll(string(body), "\r\n", "\n")
|
|
}
|
|
lines := strings.Split(content, "\n")
|
|
if content == "" {
|
|
lines = nil
|
|
}
|
|
updated := make([]string, 0, len(lines)+1)
|
|
declared := false
|
|
for _, line := range lines {
|
|
if modsIndexLineKey(line) == modKey {
|
|
if !declared {
|
|
updated = append(updated, modKey+" : 1")
|
|
declared = true
|
|
}
|
|
continue
|
|
}
|
|
updated = append(updated, line)
|
|
}
|
|
if !declared {
|
|
updated = append(updated, modKey+" : 1")
|
|
}
|
|
next := strings.Join(updated, "\n")
|
|
if !strings.HasSuffix(next, "\n") {
|
|
next += "\n"
|
|
}
|
|
if content == next {
|
|
return nil
|
|
}
|
|
if err := executor.writeRuntimeFile(modsPath, []byte(next), 0o600); err != nil {
|
|
return dllExtensionError{code: "dll_extension_mods_failed", message: "UE4SS mods index cannot be updated"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func modsIndexLineKey(line string) string {
|
|
withoutComment := strings.SplitN(line, "#", 2)[0]
|
|
parts := strings.SplitN(strings.TrimSpace(withoutComment), ":", 2)
|
|
if len(parts) != 2 {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(parts[0])
|
|
}
|
|
|
|
func existingRuntimeDirectory(scope string, key string) error {
|
|
if !protocol.ValidLogicalFileKey(key) || filepath.IsAbs(key) || strings.Contains(key, `\`) {
|
|
return fmt.Errorf("directory key is unsafe")
|
|
}
|
|
cleanScope, err := filepath.Abs(scope)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
current := cleanScope
|
|
for _, part := range strings.Split(filepath.ToSlash(key), "/") {
|
|
current = filepath.Join(current, part)
|
|
info, err := os.Lstat(current)
|
|
if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
|
return fmt.Errorf("required directory is unavailable")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checksumRegularFile(path string, maxBytes int64) (string, int64, error) {
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
|
return "", 0, fmt.Errorf("file is not regular")
|
|
}
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
defer file.Close()
|
|
hash := sha256.New()
|
|
size, err := io.Copy(hash, io.LimitReader(file, maxBytes+1))
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
if size > maxBytes {
|
|
return "", size, fmt.Errorf("file exceeds maximum size")
|
|
}
|
|
return "sha256:" + hex.EncodeToString(hash.Sum(nil)), size, nil
|
|
}
|
|
|
|
func copyRegularFileAtomic(source string, destination string, maxBytes int64, mode os.FileMode) error {
|
|
checksum, size, err := checksumRegularFile(source, maxBytes)
|
|
if err != nil || checksum == "" || size < 1 {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return fmt.Errorf("source file is empty")
|
|
}
|
|
input, err := os.Open(source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer input.Close()
|
|
temporary := destination + ".copying"
|
|
output, err := os.OpenFile(temporary, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
remove := true
|
|
defer func() {
|
|
_ = output.Close()
|
|
if remove {
|
|
_ = os.Remove(temporary)
|
|
}
|
|
}()
|
|
written, err := io.Copy(output, io.LimitReader(input, maxBytes+1))
|
|
if err != nil || written != size || written > maxBytes {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return fmt.Errorf("source file changed during copy")
|
|
}
|
|
if err := output.Sync(); err != nil {
|
|
return err
|
|
}
|
|
if err := output.Close(); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(temporary, destination); err != nil {
|
|
return err
|
|
}
|
|
remove = false
|
|
return os.Chmod(destination, mode)
|
|
}
|
|
|
|
func readBoundedRegularFile(path string, maxBytes int64) ([]byte, bool, error) {
|
|
info, err := os.Lstat(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil, false, nil
|
|
}
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() || info.Size() > maxBytes {
|
|
return nil, false, fmt.Errorf("file is not a bounded regular file")
|
|
}
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
defer file.Close()
|
|
body, err := io.ReadAll(io.LimitReader(file, maxBytes+1))
|
|
if err != nil || int64(len(body)) > maxBytes {
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
return nil, false, fmt.Errorf("file exceeds maximum size")
|
|
}
|
|
return body, true, nil
|
|
}
|
|
|
|
func protocolValidSHA256(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 dllExtensionLifecycleFailure(err error) LifecycleExecutionResult {
|
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
return LifecycleExecutionResult{State: lifecycleResultStateCancelled, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "DLL extension synchronization cancelled"}, Message: "DLL extension synchronization cancelled", ErrorCode: "dll_extension_cancelled"}
|
|
}
|
|
var extensionErr dllExtensionError
|
|
if errors.As(err, &extensionErr) {
|
|
return lifecycleFailure(extensionErr.code, extensionErr.message)
|
|
}
|
|
return lifecycleFailure("dll_extension_sync_failed", "DLL extension synchronization failed")
|
|
}
|