185 lines
5.7 KiB
Go
185 lines
5.7 KiB
Go
package updater
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
appDirectoryName = "app"
|
|
stagingDirectoryName = "staging"
|
|
backupsDirectoryName = "backups"
|
|
transactionFileName = "self-update-transaction.json"
|
|
healthFileName = "self-update-health.json"
|
|
transactionSchemaVersion = 1
|
|
)
|
|
|
|
type updateLayout struct {
|
|
root string
|
|
target string
|
|
staging string
|
|
backup string
|
|
transaction string
|
|
health string
|
|
requestID string
|
|
}
|
|
|
|
func inspectRequest(request Request) (updateLayout, error) {
|
|
if request.ParentPID <= 0 {
|
|
return updateLayout{}, fmt.Errorf("%w: parent PID must be positive", ErrInvalidRequest)
|
|
}
|
|
if !validRequestID(request.RequestID) {
|
|
return updateLayout{}, fmt.Errorf("%w: unsafe request ID", ErrInvalidRequest)
|
|
}
|
|
layout, err := inspectTarget(request.TargetDir)
|
|
if err != nil {
|
|
return updateLayout{}, err
|
|
}
|
|
if err := requireRealDirectory(layout.target, "target"); err != nil {
|
|
return updateLayout{}, err
|
|
}
|
|
layout.requestID = request.RequestID
|
|
expectedStaging := filepath.Join(layout.root, stagingDirectoryName, request.RequestID)
|
|
if !filepath.IsAbs(request.StagingDir) || !samePath(request.StagingDir, expectedStaging) {
|
|
return updateLayout{}, fmt.Errorf("%w: staging path does not match fixed layout", ErrUnsafeLayout)
|
|
}
|
|
layout.staging = expectedStaging
|
|
layout.backup = filepath.Join(layout.root, backupsDirectoryName, request.RequestID)
|
|
if err := requireRealDirectory(filepath.Join(layout.root, stagingDirectoryName), "staging parent"); err != nil {
|
|
return updateLayout{}, err
|
|
}
|
|
if err := requireRealDirectory(layout.staging, "staging"); err != nil {
|
|
if os.IsNotExist(unwrapPathError(err)) {
|
|
return updateLayout{}, ErrStagingMissing
|
|
}
|
|
return updateLayout{}, err
|
|
}
|
|
return layout, nil
|
|
}
|
|
|
|
func inspectTarget(targetDir string) (updateLayout, error) {
|
|
if !filepath.IsAbs(targetDir) {
|
|
return updateLayout{}, fmt.Errorf("%w: target path must be absolute", ErrUnsafeLayout)
|
|
}
|
|
target := filepath.Clean(targetDir)
|
|
if filepath.Base(target) != appDirectoryName {
|
|
return updateLayout{}, fmt.Errorf("%w: target must be root/app", ErrUnsafeLayout)
|
|
}
|
|
root := filepath.Dir(target)
|
|
if filepath.Base(root) == "." || root == target {
|
|
return updateLayout{}, fmt.Errorf("%w: target root is invalid", ErrUnsafeLayout)
|
|
}
|
|
if err := requireRealDirectory(root, "root"); err != nil {
|
|
return updateLayout{}, err
|
|
}
|
|
if exists, err := realDirectoryState(target, "target"); err != nil {
|
|
return updateLayout{}, err
|
|
} else if !exists {
|
|
// target can be absent while recovery restores a backed-up app.
|
|
return updateLayout{root: root, target: target, transaction: filepath.Join(root, transactionFileName), health: filepath.Join(root, healthFileName)}, nil
|
|
}
|
|
return updateLayout{root: root, target: target, transaction: filepath.Join(root, transactionFileName), health: filepath.Join(root, healthFileName)}, nil
|
|
}
|
|
|
|
func (layout updateLayout) validateReady(syncer DirectorySyncer) error {
|
|
if err := requireRealDirectory(layout.target, "target"); err != nil {
|
|
return err
|
|
}
|
|
if err := requireRealDirectory(layout.staging, "staging"); err != nil {
|
|
return err
|
|
}
|
|
if err := verifyTreeNoLinks(layout.staging); err != nil {
|
|
return err
|
|
}
|
|
if err := validateRegularFile(filepath.Join(layout.staging, ProductExecutableName)); err != nil {
|
|
return fmt.Errorf("%w: staged %s: %v", ErrUnsafeLayout, ProductExecutableName, err)
|
|
}
|
|
backups := filepath.Dir(layout.backup)
|
|
if info, err := os.Lstat(backups); os.IsNotExist(err) {
|
|
if err := os.Mkdir(backups, 0o700); err != nil {
|
|
return fmt.Errorf("create backups directory: %w", err)
|
|
}
|
|
if err := syncer.SyncDirectory(layout.root); err != nil {
|
|
return fmt.Errorf("sync root after creating backups directory: %w", err)
|
|
}
|
|
} else if err != nil {
|
|
return fmt.Errorf("%w: inspect backups parent: %v", ErrUnsafeLayout, err)
|
|
} else if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
|
return fmt.Errorf("%w: backups parent is not a real directory", ErrUnsafeLayout)
|
|
}
|
|
if exists, err := realDirectoryState(layout.backup, "backup"); err != nil {
|
|
return err
|
|
} else if exists {
|
|
return ErrBackupExists
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (layout updateLayout) entrypoint() string {
|
|
return filepath.Join(layout.target, ProductExecutableName)
|
|
}
|
|
|
|
func requireRealDirectory(path string, description string) error {
|
|
exists, err := realDirectoryState(path, description)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exists {
|
|
return &os.PathError{Op: "lstat", Path: path, Err: os.ErrNotExist}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func realDirectoryState(path string, description string) (bool, error) {
|
|
info, err := os.Lstat(path)
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, fmt.Errorf("%w: inspect %s: %v", ErrUnsafeLayout, description, err)
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
|
return false, fmt.Errorf("%w: %s is not a real directory", ErrUnsafeLayout, description)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func validateRegularFile(path string) error {
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
|
return fmt.Errorf("file is not a regular non-symlink file")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validRequestID(value string) bool {
|
|
if len(value) < 8 || len(value) > 64 || strings.HasPrefix(value, "-") || strings.HasSuffix(value, "-") {
|
|
return false
|
|
}
|
|
for _, char := range value {
|
|
if (char < 'a' || char > 'z') && (char < '0' || char > '9') && char != '-' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func samePath(left string, right string) bool {
|
|
return filepath.Clean(left) == filepath.Clean(right)
|
|
}
|
|
|
|
func unwrapPathError(err error) error {
|
|
for {
|
|
pathErr, ok := err.(*os.PathError)
|
|
if !ok {
|
|
return err
|
|
}
|
|
err = pathErr.Err
|
|
}
|
|
}
|