func renderSumForHeader(v Version, h *tar.Header, data []byte) (string, error) { buf := bytes.NewBuffer(nil) // first build our test tar tw := tar.NewWriter(buf) if err := tw.WriteHeader(h); err != nil { return "", err } if _, err := tw.Write(data); err != nil { return "", err } tw.Close() ts, err := NewTarSum(buf, true, v) if err != nil { return "", err } tr := tar.NewReader(ts) for { hdr, err := tr.Next() if hdr == nil || err == io.EOF { // Signals the end of the archive. break } if err != nil { return "", err } if _, err = io.Copy(ioutil.Discard, tr); err != nil { return "", err } } return ts.Sum(nil), nil }
func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) { log.Debugf("CopyFileWithTar(%s, %s)", src, dst) srcSt, err := os.Stat(src) if err != nil { return err } if srcSt.IsDir() { return fmt.Errorf("Can't copy a directory") } // Clean up the trailing / if dst[len(dst)-1] == '/' { dst = path.Join(dst, filepath.Base(src)) } // Create the holding directory if necessary if err := os.MkdirAll(filepath.Dir(dst), 0700); err != nil && !os.IsExist(err) { return err } r, w := io.Pipe() errC := promise.Go(func() error { defer w.Close() srcF, err := os.Open(src) if err != nil { return err } defer srcF.Close() hdr, err := tar.FileInfoHeader(srcSt, "") if err != nil { return err } hdr.Name = filepath.Base(dst) hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode))) tw := tar.NewWriter(w) defer tw.Close() if err := tw.WriteHeader(hdr); err != nil { return err } if _, err := io.Copy(tw, srcF); err != nil { return err } return nil }) defer func() { if er := <-errC; err != nil { err = er } }() return archiver.Untar(r, filepath.Dir(dst), nil) }
func emptyTarSum(gzip bool) (TarSum, error) { reader, writer := io.Pipe() tarWriter := tar.NewWriter(writer) // Immediately close tarWriter and write-end of the // Pipe in a separate goroutine so we don't block. go func() { tarWriter.Close() writer.Close() }() return NewTarSum(reader, !gzip, Version0) }
// ExportChanges produces an Archive from the provided changes, relative to dir. func ExportChanges(dir string, changes []Change) (Archive, error) { reader, writer := io.Pipe() go func() { ta := &tarAppender{ TarWriter: tar.NewWriter(writer), Buffer: pools.BufioWriter32KPool.Get(nil), SeenFiles: make(map[uint64]string), } // this buffer is needed for the duration of this piped stream defer pools.BufioWriter32KPool.Put(ta.Buffer) sort.Sort(changesByPath(changes)) // In general we log errors here but ignore them because // during e.g. a diff operation the container can continue // mutating the filesystem and we can see transient errors // from this for _, change := range changes { if change.Kind == ChangeDelete { whiteOutDir := filepath.Dir(change.Path) whiteOutBase := filepath.Base(change.Path) whiteOut := filepath.Join(whiteOutDir, ".wh."+whiteOutBase) timestamp := time.Now() hdr := &tar.Header{ Name: whiteOut[1:], Size: 0, ModTime: timestamp, AccessTime: timestamp, ChangeTime: timestamp, } if err := ta.TarWriter.WriteHeader(hdr); err != nil { log.Debugf("Can't write whiteout header: %s", err) } } else { path := filepath.Join(dir, change.Path) if err := ta.addTarFile(path, change.Path[1:]); err != nil { log.Debugf("Can't add file %s to tar: %s", path, err) } } } // Make sure to check the error on Close. if err := ta.TarWriter.Close(); err != nil { log.Debugf("Can't close layer: %s", err) } if err := writer.Close(); err != nil { log.Debugf("failed close Changes writer: %s", err) } }() return reader, nil }
// make a tar: // * num is the number of files the tar should have // * size is the bytes per file // * isRand is whether the contents of the files should be a random chunk (otherwise it's all zeros) // * realFile will write to a TempFile, instead of an in memory buffer func sizedTar(opts sizedOptions) io.Reader { var ( fh io.ReadWriter err error ) if opts.realFile { fh, err = ioutil.TempFile("", "tarsum") if err != nil { return nil } } else { fh = bytes.NewBuffer([]byte{}) } tarW := tar.NewWriter(fh) defer tarW.Close() for i := int64(0); i < opts.num; i++ { err := tarW.WriteHeader(&tar.Header{ Name: fmt.Sprintf("/testdata%d", i), Mode: 0755, Uid: 0, Gid: 0, Size: opts.size, }) if err != nil { return nil } var rBuf []byte if opts.isRand { rBuf = make([]byte, 8) _, err = rand.Read(rBuf) if err != nil { return nil } } else { rBuf = []byte{0, 0, 0, 0, 0, 0, 0, 0} } for i := int64(0); i < opts.size/int64(8); i++ { tarW.Write(rBuf) } } return fh }
func (ts *tarSum) initTarSum() error { ts.bufTar = bytes.NewBuffer([]byte{}) ts.bufWriter = bytes.NewBuffer([]byte{}) ts.tarR = tar.NewReader(ts.Reader) ts.tarW = tar.NewWriter(ts.bufTar) if !ts.DisableCompression { ts.writer = gzip.NewWriter(ts.bufWriter) } else { ts.writer = &nopCloseFlusher{Writer: ts.bufWriter} } if ts.tHash == nil { ts.tHash = DefaultTHash } ts.h = ts.tHash.Hash() ts.h.Reset() ts.first = true ts.sums = FileInfoSums{} return nil }
func ExportChanges(dir string, changes []Change) (Archive, error) { reader, writer := io.Pipe() tw := tar.NewWriter(writer) go func() { twBuf := bufio.NewWriterSize(nil, twBufSize) // In general we log errors here but ignore them because // during e.g. a diff operation the container can continue // mutating the filesystem and we can see transient errors // from this for _, change := range changes { if change.Kind == ChangeDelete { whiteOutDir := filepath.Dir(change.Path) whiteOutBase := filepath.Base(change.Path) whiteOut := filepath.Join(whiteOutDir, ".wh."+whiteOutBase) timestamp := time.Now() hdr := &tar.Header{ Name: whiteOut[1:], Size: 0, ModTime: timestamp, AccessTime: timestamp, ChangeTime: timestamp, } if err := tw.WriteHeader(hdr); err != nil { utils.Debugf("Can't write whiteout header: %s\n", err) } } else { path := filepath.Join(dir, change.Path) if err := addTarFile(path, change.Path[1:], tw, twBuf); err != nil { utils.Debugf("Can't add file %s to tar: %s\n", path, err) } } } // Make sure to check the error on Close. if err := tw.Close(); err != nil { utils.Debugf("Can't close layer: %s\n", err) } writer.Close() }() return reader, nil }
// Generate generates a new archive from the content provided // as input. // // `files` is a sequence of path/content pairs. A new file is // added to the archive for each pair. // If the last pair is incomplete, the file is created with an // empty content. For example: // // Generate("foo.txt", "hello world", "emptyfile") // // The above call will return an archive with 2 files: // * ./foo.txt with content "hello world" // * ./empty with empty content // // FIXME: stream content instead of buffering // FIXME: specify permissions and other archive metadata func Generate(input ...string) (Archive, error) { files := parseStringPairs(input...) buf := new(bytes.Buffer) tw := tar.NewWriter(buf) for _, file := range files { name, content := file[0], file[1] hdr := &tar.Header{ Name: name, Size: int64(len(content)), } if err := tw.WriteHeader(hdr); err != nil { return nil, err } if _, err := tw.Write([]byte(content)); err != nil { return nil, err } } if err := tw.Close(); err != nil { return nil, err } return ioutil.NopCloser(buf), nil }
func fakeTar() (io.Reader, error) { uid := os.Getuid() gid := os.Getgid() content := []byte("Hello world!\n") buf := new(bytes.Buffer) tw := tar.NewWriter(buf) for _, name := range []string{"/etc/postgres/postgres.conf", "/etc/passwd", "/var/log/postgres/postgres.conf"} { hdr := new(tar.Header) // Leaving these fields blank requires root privileges hdr.Uid = uid hdr.Gid = gid hdr.Size = int64(len(content)) hdr.Name = name if err := tw.WriteHeader(hdr); err != nil { return nil, err } tw.Write([]byte(content)) } tw.Close() return buf, nil }
// TarWithOptions creates an archive from the directory at `path`, only including files whose relative // paths are included in `options.Includes` (if non-nil) or not in `options.Excludes`. func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) { pipeReader, pipeWriter := io.Pipe() compressWriter, err := CompressStream(pipeWriter, options.Compression) if err != nil { return nil, err } tw := tar.NewWriter(compressWriter) go func() { // In general we log errors here but ignore them because // during e.g. a diff operation the container can continue // mutating the filesystem and we can see transient errors // from this if options.Includes == nil { options.Includes = []string{"."} } twBuf := bufio.NewWriterSize(nil, twBufSize) for _, include := range options.Includes { filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error { if err != nil { utils.Debugf("Tar: Can't stat file %s to tar: %s\n", srcPath, err) return nil } relFilePath, err := filepath.Rel(srcPath, filePath) if err != nil { return nil } for _, exclude := range options.Excludes { matched, err := filepath.Match(exclude, relFilePath) if err != nil { utils.Errorf("Error matching: %s (pattern: %s)", relFilePath, exclude) return err } if matched { if filepath.Clean(relFilePath) == "." { utils.Errorf("Can't exclude whole path, excluding pattern: %s", exclude) continue } utils.Debugf("Skipping excluded path: %s", relFilePath) if f.IsDir() { return filepath.SkipDir } return nil } } if err := addTarFile(filePath, relFilePath, tw, twBuf); err != nil { utils.Debugf("Can't add file %s to tar: %s\n", srcPath, err) } return nil }) } // Make sure to check the error on Close. if err := tw.Close(); err != nil { utils.Debugf("Can't close tar writer: %s\n", err) } if err := compressWriter.Close(); err != nil { utils.Debugf("Can't close compress writer: %s\n", err) } if err := pipeWriter.Close(); err != nil { utils.Debugf("Can't close pipe writer: %s\n", err) } }() return pipeReader, nil }
// TarWithOptions creates an archive from the directory at `path`, only including files whose relative // paths are included in `options.IncludeFiles` (if non-nil) or not in `options.ExcludePatterns`. func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) { pipeReader, pipeWriter := io.Pipe() compressWriter, err := CompressStream(pipeWriter, options.Compression) if err != nil { return nil, err } go func() { ta := &tarAppender{ TarWriter: tar.NewWriter(compressWriter), Buffer: pools.BufioWriter32KPool.Get(nil), SeenFiles: make(map[uint64]string), } // this buffer is needed for the duration of this piped stream defer pools.BufioWriter32KPool.Put(ta.Buffer) // In general we log errors here but ignore them because // during e.g. a diff operation the container can continue // mutating the filesystem and we can see transient errors // from this if options.IncludeFiles == nil { options.IncludeFiles = []string{"."} } seen := make(map[string]bool) var renamedRelFilePath string // For when tar.Options.Name is set for _, include := range options.IncludeFiles { filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error { if err != nil { log.Debugf("Tar: Can't stat file %s to tar: %s", srcPath, err) return nil } relFilePath, err := filepath.Rel(srcPath, filePath) if err != nil || (relFilePath == "." && f.IsDir()) { // Error getting relative path OR we are looking // at the root path. Skip in both situations. return nil } skip := false // If "include" is an exact match for the current file // then even if there's an "excludePatterns" pattern that // matches it, don't skip it. IOW, assume an explicit 'include' // is asking for that file no matter what - which is true // for some files, like .dockerignore and Dockerfile (sometimes) if include != relFilePath { skip, err = fileutils.Matches(relFilePath, options.ExcludePatterns) if err != nil { log.Debugf("Error matching %s", relFilePath, err) return err } } if skip { if f.IsDir() { return filepath.SkipDir } return nil } if seen[relFilePath] { return nil } seen[relFilePath] = true // Rename the base resource if options.Name != "" && filePath == srcPath+"/"+filepath.Base(relFilePath) { renamedRelFilePath = relFilePath } // Set this to make sure the items underneath also get renamed if options.Name != "" { relFilePath = strings.Replace(relFilePath, renamedRelFilePath, options.Name, 1) } if err := ta.addTarFile(filePath, relFilePath); err != nil { log.Debugf("Can't add file %s to tar: %s", filePath, err) } return nil }) } // Make sure to check the error on Close. if err := ta.TarWriter.Close(); err != nil { log.Debugf("Can't close tar writer: %s", err) } if err := compressWriter.Close(); err != nil { log.Debugf("Can't close compress writer: %s", err) } if err := pipeWriter.Close(); err != nil { log.Debugf("Can't close pipe writer: %s", err) } }() return pipeReader, nil }
// testBreakout is a helper function that, within the provided `tmpdir` directory, // creates a `victim` folder with a generated `hello` file in it. // `untar` extracts to a directory named `dest`, the tar file created from `headers`. // // Here are the tested scenarios: // - removed `victim` folder (write) // - removed files from `victim` folder (write) // - new files in `victim` folder (write) // - modified files in `victim` folder (write) // - file in `dest` with same content as `victim/hello` (read) // // When using testBreakout make sure you cover one of the scenarios listed above. func testBreakout(untarFn string, tmpdir string, headers []*tar.Header) error { tmpdir, err := ioutil.TempDir("", tmpdir) if err != nil { return err } defer os.RemoveAll(tmpdir) dest := filepath.Join(tmpdir, "dest") if err := os.Mkdir(dest, 0755); err != nil { return err } victim := filepath.Join(tmpdir, "victim") if err := os.Mkdir(victim, 0755); err != nil { return err } hello := filepath.Join(victim, "hello") helloData, err := time.Now().MarshalText() if err != nil { return err } if err := ioutil.WriteFile(hello, helloData, 0644); err != nil { return err } helloStat, err := os.Stat(hello) if err != nil { return err } reader, writer := io.Pipe() go func() { t := tar.NewWriter(writer) for _, hdr := range headers { t.WriteHeader(hdr) } t.Close() }() untar := testUntarFns[untarFn] if untar == nil { return fmt.Errorf("could not find untar function %q in testUntarFns", untarFn) } if err := untar(dest, reader); err != nil { if _, ok := err.(breakoutError); !ok { // If untar returns an error unrelated to an archive breakout, // then consider this an unexpected error and abort. return err } // Here, untar detected the breakout. // Let's move on verifying that indeed there was no breakout. fmt.Printf("breakoutError: %v\n", err) } // Check victim folder f, err := os.Open(victim) if err != nil { // codepath taken if victim folder was removed return fmt.Errorf("archive breakout: error reading %q: %v", victim, err) } defer f.Close() // Check contents of victim folder // // We are only interested in getting 2 files from the victim folder, because if all is well // we expect only one result, the `hello` file. If there is a second result, it cannot // hold the same name `hello` and we assume that a new file got created in the victim folder. // That is enough to detect an archive breakout. names, err := f.Readdirnames(2) if err != nil { // codepath taken if victim is not a folder return fmt.Errorf("archive breakout: error reading directory content of %q: %v", victim, err) } for _, name := range names { if name != "hello" { // codepath taken if new file was created in victim folder return fmt.Errorf("archive breakout: new file %q", name) } } // Check victim/hello f, err = os.Open(hello) if err != nil { // codepath taken if read permissions were removed return fmt.Errorf("archive breakout: could not lstat %q: %v", hello, err) } defer f.Close() b, err := ioutil.ReadAll(f) if err != nil { return err } fi, err := f.Stat() if err != nil { return err } if helloStat.IsDir() != fi.IsDir() || // TODO: cannot check for fi.ModTime() change helloStat.Mode() != fi.Mode() || helloStat.Size() != fi.Size() || !bytes.Equal(helloData, b) { // codepath taken if hello has been modified return fmt.Errorf("archive breakout: file %q has been modified. Contents: expected=%q, got=%q. FileInfo: expected=%#v, got=%#v.", hello, helloData, b, helloStat, fi) } // Check that nothing in dest/ has the same content as victim/hello. // Since victim/hello was generated with time.Now(), it is safe to assume // that any file whose content matches exactly victim/hello, managed somehow // to access victim/hello. return filepath.Walk(dest, func(path string, info os.FileInfo, err error) error { if info.IsDir() { if err != nil { // skip directory if error return filepath.SkipDir } // enter directory return nil } if err != nil { // skip file if error return nil } b, err := ioutil.ReadFile(path) if err != nil { // Houston, we have a problem. Aborting (space)walk. return err } if bytes.Equal(helloData, b) { return fmt.Errorf("archive breakout: file %q has been accessed via %q", hello, path) } return nil }) }
func (ts *TarSum) Read(buf []byte) (int, error) { if ts.gz == nil { ts.bufTar = bytes.NewBuffer([]byte{}) ts.bufGz = bytes.NewBuffer([]byte{}) ts.tarR = tar.NewReader(ts.Reader) ts.tarW = tar.NewWriter(ts.bufTar) if !ts.DisableCompression { ts.gz = gzip.NewWriter(ts.bufGz) } else { ts.gz = &nopCloseFlusher{Writer: ts.bufGz} } ts.h = sha256.New() ts.h.Reset() ts.first = true ts.sums = make(map[string]string) } if ts.finished { return ts.bufGz.Read(buf) } buf2 := make([]byte, len(buf), cap(buf)) n, err := ts.tarR.Read(buf2) if err != nil { if err == io.EOF { if _, err := ts.h.Write(buf2[:n]); err != nil { return 0, err } if !ts.first { ts.sums[ts.currentFile] = hex.EncodeToString(ts.h.Sum(nil)) ts.h.Reset() } else { ts.first = false } currentHeader, err := ts.tarR.Next() if err != nil { if err == io.EOF { if err := ts.gz.Close(); err != nil { return 0, err } ts.finished = true return n, nil } return n, err } ts.currentFile = strings.TrimSuffix(strings.TrimPrefix(currentHeader.Name, "./"), "/") if err := ts.encodeHeader(currentHeader); err != nil { return 0, err } if err := ts.tarW.WriteHeader(currentHeader); err != nil { return 0, err } if _, err := ts.tarW.Write(buf2[:n]); err != nil { return 0, err } ts.tarW.Flush() if _, err := io.Copy(ts.gz, ts.bufTar); err != nil { return 0, err } ts.gz.Flush() return ts.bufGz.Read(buf) } return n, err } // Filling the hash buffer if _, err = ts.h.Write(buf2[:n]); err != nil { return 0, err } // Filling the tar writter if _, err = ts.tarW.Write(buf2[:n]); err != nil { return 0, err } ts.tarW.Flush() // Filling the gz writter if _, err = io.Copy(ts.gz, ts.bufTar); err != nil { return 0, err } ts.gz.Flush() return ts.bufGz.Read(buf) }