github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/plugin/installer/http_installer.go (about) 1 /* 2 Copyright The Helm Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package installer // import "github.com/stefanmcshane/helm/pkg/plugin/installer" 17 18 import ( 19 "archive/tar" 20 "bytes" 21 "compress/gzip" 22 "io" 23 "os" 24 "path" 25 "path/filepath" 26 "regexp" 27 "strings" 28 29 securejoin "github.com/cyphar/filepath-securejoin" 30 "github.com/pkg/errors" 31 32 "github.com/stefanmcshane/helm/internal/third_party/dep/fs" 33 "github.com/stefanmcshane/helm/pkg/cli" 34 "github.com/stefanmcshane/helm/pkg/getter" 35 "github.com/stefanmcshane/helm/pkg/helmpath" 36 "github.com/stefanmcshane/helm/pkg/plugin/cache" 37 ) 38 39 // HTTPInstaller installs plugins from an archive served by a web server. 40 type HTTPInstaller struct { 41 CacheDir string 42 PluginName string 43 base 44 extractor Extractor 45 getter getter.Getter 46 } 47 48 // TarGzExtractor extracts gzip compressed tar archives 49 type TarGzExtractor struct{} 50 51 // Extractor provides an interface for extracting archives 52 type Extractor interface { 53 Extract(buffer *bytes.Buffer, targetDir string) error 54 } 55 56 // Extractors contains a map of suffixes and matching implementations of extractor to return 57 var Extractors = map[string]Extractor{ 58 ".tar.gz": &TarGzExtractor{}, 59 ".tgz": &TarGzExtractor{}, 60 } 61 62 // Convert a media type to an extractor extension. 63 // 64 // This should be refactored in Helm 4, combined with the extension-based mechanism. 65 func mediaTypeToExtension(mt string) (string, bool) { 66 switch strings.ToLower(mt) { 67 case "application/gzip", "application/x-gzip", "application/x-tgz", "application/x-gtar": 68 return ".tgz", true 69 default: 70 return "", false 71 } 72 } 73 74 // NewExtractor creates a new extractor matching the source file name 75 func NewExtractor(source string) (Extractor, error) { 76 for suffix, extractor := range Extractors { 77 if strings.HasSuffix(source, suffix) { 78 return extractor, nil 79 } 80 } 81 return nil, errors.Errorf("no extractor implemented yet for %s", source) 82 } 83 84 // NewHTTPInstaller creates a new HttpInstaller. 85 func NewHTTPInstaller(source string) (*HTTPInstaller, error) { 86 key, err := cache.Key(source) 87 if err != nil { 88 return nil, err 89 } 90 91 extractor, err := NewExtractor(source) 92 if err != nil { 93 return nil, err 94 } 95 96 get, err := getter.All(new(cli.EnvSettings)).ByScheme("http") 97 if err != nil { 98 return nil, err 99 } 100 101 i := &HTTPInstaller{ 102 CacheDir: helmpath.CachePath("plugins", key), 103 PluginName: stripPluginName(filepath.Base(source)), 104 base: newBase(source), 105 extractor: extractor, 106 getter: get, 107 } 108 return i, nil 109 } 110 111 // helper that relies on some sort of convention for plugin name (plugin-name-<version>) 112 func stripPluginName(name string) string { 113 var strippedName string 114 for suffix := range Extractors { 115 if strings.HasSuffix(name, suffix) { 116 strippedName = strings.TrimSuffix(name, suffix) 117 break 118 } 119 } 120 re := regexp.MustCompile(`(.*)-[0-9]+\..*`) 121 return re.ReplaceAllString(strippedName, `$1`) 122 } 123 124 // Install downloads and extracts the tarball into the cache directory 125 // and installs into the plugin directory. 126 // 127 // Implements Installer. 128 func (i *HTTPInstaller) Install() error { 129 pluginData, err := i.getter.Get(i.Source) 130 if err != nil { 131 return err 132 } 133 134 if err := i.extractor.Extract(pluginData, i.CacheDir); err != nil { 135 return errors.Wrap(err, "extracting files from archive") 136 } 137 138 if !isPlugin(i.CacheDir) { 139 return ErrMissingMetadata 140 } 141 142 src, err := filepath.Abs(i.CacheDir) 143 if err != nil { 144 return err 145 } 146 147 debug("copying %s to %s", src, i.Path()) 148 return fs.CopyDir(src, i.Path()) 149 } 150 151 // Update updates a local repository 152 // Not implemented for now since tarball most likely will be packaged by version 153 func (i *HTTPInstaller) Update() error { 154 return errors.Errorf("method Update() not implemented for HttpInstaller") 155 } 156 157 // Path is overridden because we want to join on the plugin name not the file name 158 func (i HTTPInstaller) Path() string { 159 if i.base.Source == "" { 160 return "" 161 } 162 return helmpath.DataPath("plugins", i.PluginName) 163 } 164 165 // cleanJoin resolves dest as a subpath of root. 166 // 167 // This function runs several security checks on the path, generating an error if 168 // the supplied `dest` looks suspicious or would result in dubious behavior on the 169 // filesystem. 170 // 171 // cleanJoin assumes that any attempt by `dest` to break out of the CWD is an attempt 172 // to be malicious. (If you don't care about this, use the securejoin-filepath library.) 173 // It will emit an error if it detects paths that _look_ malicious, operating on the 174 // assumption that we don't actually want to do anything with files that already 175 // appear to be nefarious. 176 // 177 // - The character `:` is considered illegal because it is a separator on UNIX and a 178 // drive designator on Windows. 179 // - The path component `..` is considered suspicions, and therefore illegal 180 // - The character \ (backslash) is treated as a path separator and is converted to /. 181 // - Beginning a path with a path separator is illegal 182 // - Rudimentary symlink protects are offered by SecureJoin. 183 func cleanJoin(root, dest string) (string, error) { 184 185 // On Windows, this is a drive separator. On UNIX-like, this is the path list separator. 186 // In neither case do we want to trust a TAR that contains these. 187 if strings.Contains(dest, ":") { 188 return "", errors.New("path contains ':', which is illegal") 189 } 190 191 // The Go tar library does not convert separators for us. 192 // We assume here, as we do elsewhere, that `\\` means a Windows path. 193 dest = strings.ReplaceAll(dest, "\\", "/") 194 195 // We want to alert the user that something bad was attempted. Cleaning it 196 // is not a good practice. 197 for _, part := range strings.Split(dest, "/") { 198 if part == ".." { 199 return "", errors.New("path contains '..', which is illegal") 200 } 201 } 202 203 // If a path is absolute, the creator of the TAR is doing something shady. 204 if path.IsAbs(dest) { 205 return "", errors.New("path is absolute, which is illegal") 206 } 207 208 // SecureJoin will do some cleaning, as well as some rudimentary checking of symlinks. 209 newpath, err := securejoin.SecureJoin(root, dest) 210 if err != nil { 211 return "", err 212 } 213 214 return filepath.ToSlash(newpath), nil 215 } 216 217 // Extract extracts compressed archives 218 // 219 // Implements Extractor. 220 func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error { 221 uncompressedStream, err := gzip.NewReader(buffer) 222 if err != nil { 223 return err 224 } 225 226 if err := os.MkdirAll(targetDir, 0755); err != nil { 227 return err 228 } 229 230 tarReader := tar.NewReader(uncompressedStream) 231 for { 232 header, err := tarReader.Next() 233 if err == io.EOF { 234 break 235 } 236 if err != nil { 237 return err 238 } 239 240 path, err := cleanJoin(targetDir, header.Name) 241 if err != nil { 242 return err 243 } 244 245 switch header.Typeflag { 246 case tar.TypeDir: 247 if err := os.Mkdir(path, 0755); err != nil { 248 return err 249 } 250 case tar.TypeReg: 251 outFile, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) 252 if err != nil { 253 return err 254 } 255 if _, err := io.Copy(outFile, tarReader); err != nil { 256 outFile.Close() 257 return err 258 } 259 outFile.Close() 260 // We don't want to process these extension header files. 261 case tar.TypeXGlobalHeader, tar.TypeXHeader: 262 continue 263 default: 264 return errors.Errorf("unknown type: %b in %s", header.Typeflag, header.Name) 265 } 266 } 267 return nil 268 }