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  }