Files
soft_quay/core/updater/layout.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
}
}