init
This commit is contained in:
@@ -0,0 +1,556 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user