104 lines
3.0 KiB
Go
104 lines
3.0 KiB
Go
package platform
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"git.casaderoll.de/michael/urbm/internal/model"
|
|
)
|
|
|
|
func ValidateRsyncPaths(job model.Job) error {
|
|
target, err := filepath.EvalSymlinks(job.Rsync.Target)
|
|
if err != nil {
|
|
return errors.New("validation: resolve rsync target: " + err.Error())
|
|
}
|
|
for _, source := range job.Sources {
|
|
resolved, err := filepath.EvalSymlinks(source.Path)
|
|
if err != nil {
|
|
return errors.New("validation: resolve rsync source: " + err.Error())
|
|
}
|
|
if pathsContainEachOther(resolved, target) {
|
|
return errors.New("validation: resolved rsync source and target must not contain each other")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func pathsContainEachOther(first, second string) bool {
|
|
within := func(path, root string) bool {
|
|
rel, err := filepath.Rel(filepath.Clean(root), filepath.Clean(path))
|
|
return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
|
|
}
|
|
return within(first, second) || within(second, first)
|
|
}
|
|
|
|
func ValidateRestoreTarget(target, restoreRoot string, inPlace, confirmed bool) (string, error) {
|
|
if inPlace {
|
|
if !confirmed {
|
|
return "", errors.New("validation: in-place restore requires explicit confirmation")
|
|
}
|
|
return string(os.PathSeparator), nil
|
|
}
|
|
if !filepath.IsAbs(target) {
|
|
return "", errors.New("validation: restore target must be absolute")
|
|
}
|
|
clean := filepath.Clean(target)
|
|
if clean == "/" || clean == "/boot" || clean == "/etc" || clean == "/usr" || clean == "/var" {
|
|
return "", errors.New("validation: protected system restore target")
|
|
}
|
|
if !inPlace {
|
|
if err := validateStagingRestoreTarget(clean, restoreRoot); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
return clean, nil
|
|
}
|
|
|
|
func validateStagingRestoreTarget(target, restoreRoot string) error {
|
|
_ = restoreRoot // Configuration validation keeps this below one of the storage roots.
|
|
allowedRoots := []string{"/mnt/user", "/mnt/disks", "/mnt/remotes"}
|
|
for _, root := range allowedRoots {
|
|
if root == "." || root == "" {
|
|
continue
|
|
}
|
|
if target == root || strings.HasPrefix(target, root+string(os.PathSeparator)) {
|
|
return rejectSymlinks(root, target)
|
|
}
|
|
}
|
|
return errors.New("validation: staging restore target must be below restoreRoot, /mnt/user, /mnt/disks, or /mnt/remotes")
|
|
}
|
|
|
|
func rejectSymlinks(root, target string) error {
|
|
if info, err := os.Lstat(root); err == nil {
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
return errors.New("validation: restore target traverses a symlink")
|
|
}
|
|
} else if !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
relative, err := filepath.Rel(root, target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
current := root
|
|
for _, component := range strings.Split(relative, string(os.PathSeparator)) {
|
|
if component == "." || component == "" {
|
|
continue
|
|
}
|
|
current = filepath.Join(current, component)
|
|
info, err := os.Lstat(current)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
return errors.New("validation: restore target traverses a symlink")
|
|
}
|
|
}
|
|
return nil
|
|
}
|