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 }