github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/chart/loader/archive.go (about)

     1  /*
     2  Copyright The Helm Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package loader
    18  
    19  import (
    20  	"archive/tar"
    21  	"bytes"
    22  	"compress/gzip"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"os"
    27  	"path"
    28  	"regexp"
    29  	"strings"
    30  
    31  	"github.com/pkg/errors"
    32  
    33  	"github.com/stefanmcshane/helm/pkg/chart"
    34  )
    35  
    36  var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`)
    37  
    38  // FileLoader loads a chart from a file
    39  type FileLoader string
    40  
    41  // Load loads a chart
    42  func (l FileLoader) Load() (*chart.Chart, error) {
    43  	return LoadFile(string(l))
    44  }
    45  
    46  // LoadFile loads from an archive file.
    47  func LoadFile(name string) (*chart.Chart, error) {
    48  	if fi, err := os.Stat(name); err != nil {
    49  		return nil, err
    50  	} else if fi.IsDir() {
    51  		return nil, errors.New("cannot load a directory")
    52  	}
    53  
    54  	raw, err := os.Open(name)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  	defer raw.Close()
    59  
    60  	err = ensureArchive(name, raw)
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	c, err := LoadArchive(raw)
    66  	if err != nil {
    67  		if err == gzip.ErrHeader {
    68  			return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err)
    69  		}
    70  	}
    71  	return c, err
    72  }
    73  
    74  // ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive.
    75  //
    76  // Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence
    77  // of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error
    78  // if we didn't check for this.
    79  func ensureArchive(name string, raw *os.File) error {
    80  	defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed.
    81  
    82  	// Check the file format to give us a chance to provide the user with more actionable feedback.
    83  	buffer := make([]byte, 512)
    84  	_, err := raw.Read(buffer)
    85  	if err != nil && err != io.EOF {
    86  		return fmt.Errorf("file '%s' cannot be read: %s", name, err)
    87  	}
    88  	if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" {
    89  		// TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide
    90  		//       variety of content (Makefile, .zshrc) as valid YAML without errors.
    91  
    92  		// Wrong content type. Let's check if it's yaml and give an extra hint?
    93  		if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
    94  			return fmt.Errorf("file '%s' seems to be a YAML file, but expected a gzipped archive", name)
    95  		}
    96  		return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType)
    97  	}
    98  	return nil
    99  }
   100  
   101  // LoadArchiveFiles reads in files out of an archive into memory. This function
   102  // performs important path security checks and should always be used before
   103  // expanding a tarball
   104  func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
   105  	unzipped, err := gzip.NewReader(in)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  	defer unzipped.Close()
   110  
   111  	files := []*BufferedFile{}
   112  	tr := tar.NewReader(unzipped)
   113  	for {
   114  		b := bytes.NewBuffer(nil)
   115  		hd, err := tr.Next()
   116  		if err == io.EOF {
   117  			break
   118  		}
   119  		if err != nil {
   120  			return nil, err
   121  		}
   122  
   123  		if hd.FileInfo().IsDir() {
   124  			// Use this instead of hd.Typeflag because we don't have to do any
   125  			// inference chasing.
   126  			continue
   127  		}
   128  
   129  		switch hd.Typeflag {
   130  		// We don't want to process these extension header files.
   131  		case tar.TypeXGlobalHeader, tar.TypeXHeader:
   132  			continue
   133  		}
   134  
   135  		// Archive could contain \ if generated on Windows
   136  		delimiter := "/"
   137  		if strings.ContainsRune(hd.Name, '\\') {
   138  			delimiter = "\\"
   139  		}
   140  
   141  		parts := strings.Split(hd.Name, delimiter)
   142  		n := strings.Join(parts[1:], delimiter)
   143  
   144  		// Normalize the path to the / delimiter
   145  		n = strings.ReplaceAll(n, delimiter, "/")
   146  
   147  		if path.IsAbs(n) {
   148  			return nil, errors.New("chart illegally contains absolute paths")
   149  		}
   150  
   151  		n = path.Clean(n)
   152  		if n == "." {
   153  			// In this case, the original path was relative when it should have been absolute.
   154  			return nil, errors.Errorf("chart illegally contains content outside the base directory: %q", hd.Name)
   155  		}
   156  		if strings.HasPrefix(n, "..") {
   157  			return nil, errors.New("chart illegally references parent directory")
   158  		}
   159  
   160  		// In some particularly arcane acts of path creativity, it is possible to intermix
   161  		// UNIX and Windows style paths in such a way that you produce a result of the form
   162  		// c:/foo even after all the built-in absolute path checks. So we explicitly check
   163  		// for this condition.
   164  		if drivePathPattern.MatchString(n) {
   165  			return nil, errors.New("chart contains illegally named files")
   166  		}
   167  
   168  		if parts[0] == "Chart.yaml" {
   169  			return nil, errors.New("chart yaml not in base directory")
   170  		}
   171  
   172  		if _, err := io.Copy(b, tr); err != nil {
   173  			return nil, err
   174  		}
   175  
   176  		data := bytes.TrimPrefix(b.Bytes(), utf8bom)
   177  
   178  		files = append(files, &BufferedFile{Name: n, Data: data})
   179  		b.Reset()
   180  	}
   181  
   182  	if len(files) == 0 {
   183  		return nil, errors.New("no files in chart archive")
   184  	}
   185  	return files, nil
   186  }
   187  
   188  // LoadArchive loads from a reader containing a compressed tar archive.
   189  func LoadArchive(in io.Reader) (*chart.Chart, error) {
   190  	files, err := LoadArchiveFiles(in)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  
   195  	return LoadFiles(files)
   196  }