aboutsummaryrefslogtreecommitdiff
path: root/pkg/copy/copy.go
blob: 3993b532e5e7391330dbeedf53848fcc75b84ef5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package copy

import (
	"io"
	"os"
	"path/filepath"
	"strings"

	buildahCopiah "github.com/containers/buildah/copier"
	"github.com/containers/storage/pkg/archive"
	securejoin "github.com/cyphar/filepath-securejoin"
	"github.com/pkg/errors"
)

// ********************************* NOTE *************************************
//
// Most security bugs are caused by attackers playing around with symlinks
// trying to escape from the container onto the host and/or trick into data
// corruption on the host.  Hence, file operations on containers (including
// *stat) should always be handled by `github.com/containers/buildah/copier`
// which makes sure to evaluate files in a chroot'ed environment.
//
// Please make sure to add verbose comments when changing code to make the
// lives of future readers easier.
//
// ****************************************************************************

// Copy the source item to destination.  Use extract to untar the source if
// it's a tar archive.
func Copy(source *CopyItem, destination *CopyItem, extract bool) error {
	// First, do the man-page dance.  See podman-cp(1) for details.
	if err := enforceCopyRules(source, destination); err != nil {
		return err
	}

	// Destination is a stream (e.g., stdout or an http body).
	if destination.info.IsStream {
		// Source is a stream (e.g., stdin or an http body).
		if source.info.IsStream {
			_, err := io.Copy(destination.writer, source.reader)
			return err
		}
		root, glob, err := source.buildahGlobs()
		if err != nil {
			return err
		}
		return buildahCopiah.Get(root, "", source.getOptions(), []string{glob}, destination.writer)
	}

	// Destination is either a file or a directory.
	if source.info.IsStream {
		return buildahCopiah.Put(destination.root, destination.resolved, source.putOptions(), source.reader)
	}

	tarOptions := &archive.TarOptions{
		Compression: archive.Uncompressed,
		CopyPass:    true,
	}

	root := destination.root
	dir := destination.resolved
	if !source.info.IsDir {
		// When copying a file, make sure to rename the
		// destination base path.
		nameMap := make(map[string]string)
		nameMap[filepath.Base(source.resolved)] = filepath.Base(destination.resolved)
		tarOptions.RebaseNames = nameMap
		dir = filepath.Dir(dir)
	}

	var tarReader io.ReadCloser
	if extract && archive.IsArchivePath(source.resolved) {
		if !destination.info.IsDir {
			return errors.Errorf("cannot extract archive %q to file %q", source.original, destination.original)
		}

		reader, err := os.Open(source.resolved)
		if err != nil {
			return err
		}
		defer reader.Close()

		// The stream from stdin may be compressed (e.g., via gzip).
		decompressedStream, err := archive.DecompressStream(reader)
		if err != nil {
			return err
		}

		defer decompressedStream.Close()
		tarReader = decompressedStream
	} else {
		reader, err := archive.TarWithOptions(source.resolved, tarOptions)
		if err != nil {
			return err
		}
		defer reader.Close()
		tarReader = reader
	}

	return buildahCopiah.Put(root, dir, source.putOptions(), tarReader)
}

// enforceCopyRules enforces the rules for copying from a source to a
// destination as mentioned in the podman-cp(1) man page.  Please refer to the
// man page and/or the inline comments for further details.  Note that source
// and destination are passed by reference and the their data may be changed.
func enforceCopyRules(source, destination *CopyItem) error {
	if source.statError != nil {
		return source.statError
	}

	// We can copy everything to a stream.
	if destination.info.IsStream {
		return nil
	}

	if source.info.IsStream {
		if !(destination.info.IsDir || destination.info.IsStream) {
			return errors.New("destination must be a directory or stream when copying from a stream")
		}
		return nil
	}

	// Source is a *directory*.
	if source.info.IsDir {
		if destination.statError != nil {
			// It's okay if the destination does not exist.  We
			// made sure before that it's parent exists, so it
			// would be created while copying.
			if os.IsNotExist(destination.statError) {
				return nil
			}
			// Could be a permission error.
			return destination.statError
		}

		// If the destination exists and is not a directory, we have a
		// problem.
		if !destination.info.IsDir {
			return errors.Errorf("cannot copy directory %q to file %q", source.original, destination.original)
		}

		// If the destination exists and is a directory, we need to
		// append the source base directory to it.  This makes sure
		// that copying "/foo/bar" "/tmp" will copy to "/tmp/bar" (and
		// not "/tmp").
		newDestination, err := securejoin.SecureJoin(destination.resolved, filepath.Base(source.resolved))
		if err != nil {
			return err
		}
		destination.resolved = newDestination
		return nil
	}

	// Source is a *file*.
	if destination.statError != nil {
		// It's okay if the destination does not exist, unless it ends
		// with "/".
		if !os.IsNotExist(destination.statError) {
			return destination.statError
		} else if strings.HasSuffix(destination.resolved, "/") {
			// Note: this is practically unreachable code as the
			// existence of parent directories is enforced early
			// on.  It's left here as an extra security net.
			return errors.Errorf("destination directory %q must exist (trailing %q)", destination.original, "/")
		}
		// Does not exist and does not end with "/".
		return nil
	}

	// If the destination is a file, we're good.  We will overwrite the
	// contents while copying.
	if !destination.info.IsDir {
		return nil
	}

	// If the destination exists and is a directory, we need to append the
	// source base directory to it.  This makes sure that copying
	// "/foo/bar" "/tmp" will copy to "/tmp/bar" (and not "/tmp").
	newDestination, err := securejoin.SecureJoin(destination.resolved, filepath.Base(source.resolved))
	if err != nil {
		return err
	}

	destination.resolved = newDestination
	return nil
}