github.com/koderover/helm@v2.17.0+incompatible/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 "k8s.io/helm/pkg/plugin/installer" 17 18 import ( 19 "archive/tar" 20 "bytes" 21 "compress/gzip" 22 "errors" 23 "fmt" 24 "io" 25 "os" 26 "path" 27 "path/filepath" 28 "regexp" 29 "strings" 30 31 securejoin "github.com/cyphar/filepath-securejoin" 32 33 "k8s.io/helm/pkg/getter" 34 "k8s.io/helm/pkg/helm/environment" 35 "k8s.io/helm/pkg/helm/helmpath" 36 "k8s.io/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 // NewExtractor creates a new extractor matching the source file name 63 func NewExtractor(source string) (Extractor, error) { 64 for suffix, extractor := range Extractors { 65 if strings.HasSuffix(source, suffix) { 66 return extractor, nil 67 } 68 } 69 return nil, fmt.Errorf("no extractor implemented yet for %s", source) 70 } 71 72 // NewHTTPInstaller creates a new HttpInstaller. 73 func NewHTTPInstaller(source string, home helmpath.Home) (*HTTPInstaller, error) { 74 75 key, err := cache.Key(source) 76 if err != nil { 77 return nil, err 78 } 79 80 extractor, err := NewExtractor(source) 81 if err != nil { 82 return nil, err 83 } 84 85 getConstructor, err := getter.ByScheme("http", environment.EnvSettings{}) 86 if err != nil { 87 return nil, err 88 } 89 90 get, err := getConstructor.New(source, "", "", "") 91 if err != nil { 92 return nil, err 93 } 94 95 i := &HTTPInstaller{ 96 CacheDir: home.Path("cache", "plugins", key), 97 PluginName: stripPluginName(filepath.Base(source)), 98 base: newBase(source, home), 99 extractor: extractor, 100 getter: get, 101 } 102 return i, nil 103 } 104 105 // helper that relies on some sort of convention for plugin name (plugin-name-<version>) 106 func stripPluginName(name string) string { 107 var strippedName string 108 for suffix := range Extractors { 109 if strings.HasSuffix(name, suffix) { 110 strippedName = strings.TrimSuffix(name, suffix) 111 break 112 } 113 } 114 re := regexp.MustCompile(`(.*)-[0-9]+\..*`) 115 return re.ReplaceAllString(strippedName, `$1`) 116 } 117 118 // Install downloads and extracts the tarball into the cache directory and creates a symlink to the plugin directory in $HELM_HOME. 119 // 120 // Implements Installer. 121 func (i *HTTPInstaller) Install() error { 122 123 pluginData, err := i.getter.Get(i.Source) 124 if err != nil { 125 return err 126 } 127 128 err = i.extractor.Extract(pluginData, i.CacheDir) 129 if err != nil { 130 return err 131 } 132 133 if !isPlugin(i.CacheDir) { 134 return ErrMissingMetadata 135 } 136 137 src, err := filepath.Abs(i.CacheDir) 138 if err != nil { 139 return err 140 } 141 142 return i.link(src) 143 } 144 145 // Update updates a local repository 146 // Not implemented for now since tarball most likely will be packaged by version 147 func (i *HTTPInstaller) Update() error { 148 return fmt.Errorf("method Update() not implemented for HttpInstaller") 149 } 150 151 // Override link because we want to use HttpInstaller.Path() not base.Path() 152 func (i *HTTPInstaller) link(from string) error { 153 debug("symlinking %s to %s", from, i.Path()) 154 return os.Symlink(from, i.Path()) 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 filepath.Join(i.base.HelmHome.Plugins(), i.PluginName) 163 } 164 165 // Extract extracts compressed archives 166 // 167 // Implements Extractor. 168 func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error { 169 uncompressedStream, err := gzip.NewReader(buffer) 170 if err != nil { 171 return err 172 } 173 174 tarReader := tar.NewReader(uncompressedStream) 175 176 os.MkdirAll(targetDir, 0755) 177 178 for true { 179 header, err := tarReader.Next() 180 181 if err == io.EOF { 182 break 183 } 184 185 if err != nil { 186 return err 187 } 188 189 path, err := cleanJoin(targetDir, header.Name) 190 if err != nil { 191 return err 192 } 193 194 switch header.Typeflag { 195 case tar.TypeDir: 196 if err := os.Mkdir(path, 0755); err != nil { 197 return err 198 } 199 case tar.TypeReg: 200 outFile, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) 201 if err != nil { 202 return err 203 } 204 if _, err := io.Copy(outFile, tarReader); err != nil { 205 outFile.Close() 206 return err 207 } 208 outFile.Close() 209 default: 210 return fmt.Errorf("unknown type: %b in %s", header.Typeflag, header.Name) 211 } 212 } 213 214 return nil 215 216 } 217 218 // CleanJoin resolves dest as a subpath of root. 219 // 220 // This function runs several security checks on the path, generating an error if 221 // the supplied `dest` looks suspicious or would result in dubious behavior on the 222 // filesystem. 223 // 224 // CleanJoin assumes that any attempt by `dest` to break out of the CWD is an attempt 225 // to be malicious. (If you don't care about this, use the securejoin-filepath library.) 226 // It will emit an error if it detects paths that _look_ malicious, operating on the 227 // assumption that we don't actually want to do anything with files that already 228 // appear to be nefarious. 229 // 230 // - The character `:` is considered illegal because it is a separator on UNIX and a 231 // drive designator on Windows. 232 // - The path component `..` is considered suspicions, and therefore illegal 233 // - The character \ (backslash) is treated as a path separator and is converted to /. 234 // - Beginning a path with a path separator is illegal 235 // - Rudimentary symlink protects are offered by SecureJoin. 236 func cleanJoin(root, dest string) (string, error) { 237 238 // On Windows, this is a drive separator. On UNIX-like, this is the path list separator. 239 // In neither case do we want to trust a TAR that contains these. 240 if strings.Contains(dest, ":") { 241 return "", errors.New("path contains ':', which is illegal") 242 } 243 244 // The Go tar library does not convert separators for us. 245 // We assume here, as we do elsewhere, that `\\` means a Windows path. 246 dest = strings.ReplaceAll(dest, "\\", "/") 247 248 // We want to alert the user that something bad was attempted. Cleaning it 249 // is not a good practice. 250 for _, part := range strings.Split(dest, "/") { 251 if part == ".." { 252 return "", errors.New("path contains '..', which is illegal") 253 } 254 } 255 256 // If a path is absolute, the creator of the TAR is doing something shady. 257 if path.IsAbs(dest) { 258 return "", errors.New("path is absolute, which is illegal") 259 } 260 261 // SecureJoin will do some cleaning, as well as some rudimentary checking of symlinks. 262 newpath, err := securejoin.SecureJoin(root, dest) 263 if err != nil { 264 return "", err 265 } 266 267 return filepath.ToSlash(newpath), nil 268 }