summaryrefslogtreecommitdiff
path: root/internal/image/download_filesystem.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/image/download_filesystem.go')
-rw-r--r--internal/image/download_filesystem.go200
1 files changed, 200 insertions, 0 deletions
diff --git a/internal/image/download_filesystem.go b/internal/image/download_filesystem.go
new file mode 100644
index 0000000..7d0c854
--- /dev/null
+++ b/internal/image/download_filesystem.go
@@ -0,0 +1,200 @@
+package image
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "golang.org/x/sys/unix"
+)
+
+type secureOutputStore struct {
+ options *DownloadOptions
+}
+
+func newSecureOutputStore(options *DownloadOptions) secureOutputStore {
+ return secureOutputStore{options: options}
+}
+
+func (s secureOutputStore) openSecureOutputFile(outputPath string) (*os.File, int, string, error) {
+ baseAbs, err := s.baseAbs()
+ if err != nil {
+ return nil, 0, "", err
+ }
+
+ targetAbs, err := filepath.Abs(outputPath)
+ if err != nil {
+ return nil, 0, "", fmt.Errorf("resolve output path: %w", err)
+ }
+ relPath, err := filepath.Rel(baseAbs, targetAbs)
+ if err != nil {
+ return nil, 0, "", fmt.Errorf("relativize output path: %w", err)
+ }
+ if relPath == "." || relPath == ".." || strings.HasPrefix(relPath, ".."+string(os.PathSeparator)) {
+ return nil, 0, "", fmt.Errorf("path escapes base dir")
+ }
+
+ relPath = filepath.Clean(relPath)
+ parentRel := filepath.Dir(relPath)
+ finalName := filepath.Base(relPath)
+
+ parentFD, err := openDirPathNoFollow(baseAbs, parentRel, s.createDirs())
+ if err != nil {
+ return nil, 0, "", err
+ }
+
+ flags := unix.O_WRONLY | unix.O_CREAT | unix.O_CLOEXEC | unix.O_NOFOLLOW
+ if s.options != nil && s.options.OverwriteExisting {
+ flags |= unix.O_TRUNC
+ } else {
+ flags |= unix.O_EXCL
+ }
+
+ fd, err := unix.Openat(parentFD, finalName, flags, 0o644)
+ if err != nil {
+ _ = unix.Close(parentFD)
+ return nil, 0, "", fmt.Errorf("open output file: %w", err)
+ }
+
+ return os.NewFile(uintptr(fd), targetAbs), parentFD, finalName, nil
+}
+
+func (s secureOutputStore) writeSecureRelativeFile(outputPath, relPath string, content []byte, perm os.FileMode) error {
+ file, parentFD, finalName, err := s.openSecureRelativeFile(outputPath, relPath, perm)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ _ = unix.Close(parentFD)
+ }()
+ defer func() {
+ _ = file.Close()
+ }()
+
+ if _, err := file.Write(content); err != nil {
+ _ = unix.Unlinkat(parentFD, finalName, 0)
+ return fmt.Errorf("write attribution file: %w", err)
+ }
+ if err := file.Sync(); err != nil {
+ _ = unix.Unlinkat(parentFD, finalName, 0)
+ return fmt.Errorf("sync attribution file: %w", err)
+ }
+ return nil
+}
+
+func (s secureOutputStore) openSecureRelativeFile(outputPath, relPath string, perm os.FileMode) (*os.File, int, string, error) {
+ baseAbs, err := s.baseAbs()
+ if err != nil {
+ return nil, 0, "", err
+ }
+
+ targetAbs, err := filepath.Abs(outputPath)
+ if err != nil {
+ return nil, 0, "", fmt.Errorf("resolve output path: %w", err)
+ }
+ baseRelPath, err := filepath.Rel(baseAbs, targetAbs)
+ if err != nil {
+ return nil, 0, "", fmt.Errorf("relativize output path: %w", err)
+ }
+ if baseRelPath == "." || baseRelPath == ".." || strings.HasPrefix(baseRelPath, ".."+string(os.PathSeparator)) {
+ return nil, 0, "", fmt.Errorf("path escapes base dir")
+ }
+
+ parentRel := filepath.Dir(filepath.Clean(baseRelPath))
+ parentFD, err := openDirPathNoFollow(baseAbs, parentRel, true)
+ if err != nil {
+ return nil, 0, "", err
+ }
+
+ flags := unix.O_WRONLY | unix.O_CREAT | unix.O_CLOEXEC | unix.O_NOFOLLOW | unix.O_TRUNC
+ fd, err := unix.Openat(parentFD, relPath, flags, uint32(perm.Perm()))
+ if err != nil {
+ _ = unix.Close(parentFD)
+ return nil, 0, "", fmt.Errorf("open attribution file: %w", err)
+ }
+
+ return os.NewFile(uintptr(fd), targetAbs), parentFD, relPath, nil
+}
+
+func (s secureOutputStore) baseAbs() (string, error) {
+ baseDir := "./images"
+ if s.options != nil && s.options.OutputDir != "" {
+ baseDir = s.options.OutputDir
+ }
+
+ baseAbs, err := filepath.Abs(baseDir)
+ if err != nil {
+ return "", fmt.Errorf("resolve base dir: %w", err)
+ }
+ return baseAbs, nil
+}
+
+func (s secureOutputStore) createDirs() bool {
+ return s.options != nil && s.options.CreateDir
+}
+
+func openDirPathNoFollow(baseAbs, relPath string, createDirs bool) (int, error) {
+ dirFD, err := openPathDirNoFollow(baseAbs, createDirs)
+ if err != nil {
+ return 0, err
+ }
+
+ relPath = filepath.Clean(relPath)
+ if relPath == "." {
+ return dirFD, nil
+ }
+
+ parts := strings.Split(relPath, string(os.PathSeparator))
+ for _, part := range parts {
+ if part == "" || part == "." {
+ continue
+ }
+ if createDirs {
+ if err := unix.Mkdirat(dirFD, part, 0o755); err != nil && !errors.Is(err, unix.EEXIST) {
+ _ = unix.Close(dirFD)
+ return 0, fmt.Errorf("create dir %q: %w", part, err)
+ }
+ }
+
+ nextFD, err := unix.Openat(dirFD, part, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
+ if err != nil {
+ _ = unix.Close(dirFD)
+ return 0, fmt.Errorf("open dir %q: %w", part, err)
+ }
+ _ = unix.Close(dirFD)
+ dirFD = nextFD
+ }
+
+ return dirFD, nil
+}
+
+func openPathDirNoFollow(absPath string, createDirs bool) (int, error) {
+ absPath = filepath.Clean(absPath)
+ if absPath == string(os.PathSeparator) {
+ return unix.Open(absPath, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
+ }
+
+ parentAbs := filepath.Dir(absPath)
+ leaf := filepath.Base(absPath)
+ parentFD, err := openPathDirNoFollow(parentAbs, createDirs)
+ if err != nil {
+ return 0, err
+ }
+
+ if createDirs {
+ if err := unix.Mkdirat(parentFD, leaf, 0o755); err != nil && !errors.Is(err, unix.EEXIST) {
+ _ = unix.Close(parentFD)
+ return 0, fmt.Errorf("create dir %q: %w", leaf, err)
+ }
+ }
+
+ dirFD, err := unix.Openat(parentFD, leaf, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
+ _ = unix.Close(parentFD)
+ if err != nil {
+ return 0, fmt.Errorf("open dir %q: %w", leaf, err)
+ }
+
+ return dirFD, nil
+}