github.com/hashicorp/go-getter/v2@v2.2.2/client.go (about)

     1  package getter
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"strconv"
    11  	"strings"
    12  
    13  	urlhelper "github.com/hashicorp/go-getter/v2/helper/url"
    14  	"github.com/hashicorp/go-multierror"
    15  	safetemp "github.com/hashicorp/go-safetemp"
    16  )
    17  
    18  // ErrSymlinkCopy means that a copy of a symlink was encountered on a request with DisableSymlinks enabled.
    19  var ErrSymlinkCopy = errors.New("copying of symlinks has been disabled")
    20  
    21  // Client is a client for downloading things.
    22  //
    23  // Top-level functions such as Get are shortcuts for interacting with a client.
    24  // Using a client directly allows more fine-grained control over how downloading
    25  // is done, as well as customizing the protocols supported.
    26  type Client struct {
    27  	// Decompressors is the map of decompressors supported by this client.
    28  	// If this is nil, then the default value is the Decompressors global.
    29  	Decompressors map[string]Decompressor
    30  
    31  	// Getters is the list of protocols supported by this client. If this
    32  	// is nil, then the default Getters variable will be used.
    33  	Getters []Getter
    34  
    35  	// Disable symlinks is used to prevent copying or writing files through symlinks for Get requests.
    36  	// When set to true any copying or writing through symlinks will result in a ErrSymlinkCopy error.
    37  	DisableSymlinks bool
    38  }
    39  
    40  // GetResult is the result of a Client.Get
    41  type GetResult struct {
    42  	// Local destination of the gotten object.
    43  	Dst string
    44  }
    45  
    46  // Get downloads the configured source to the destination.
    47  func (c *Client) Get(ctx context.Context, req *Request) (*GetResult, error) {
    48  	if err := c.configure(); err != nil {
    49  		return nil, err
    50  	}
    51  
    52  	// Pass along the configured Getter client in the context for usage with the X-Terraform-Get feature.
    53  	ctx = NewContextWithClient(ctx, c)
    54  
    55  	// Store this locally since there are cases we swap this
    56  	if req.GetMode == ModeInvalid {
    57  		req.GetMode = ModeAny
    58  	}
    59  
    60  	// Client setting takes precedence for all requests
    61  	if c.DisableSymlinks {
    62  		req.DisableSymlinks = true
    63  	}
    64  
    65  	// If there is a subdir component, then we download the root separately
    66  	// and then copy over the proper subdir.
    67  	req.Src, req.subDir = SourceDirSubdir(req.Src)
    68  
    69  	if req.subDir != "" {
    70  		// Check if the subdirectory is attempting to traverse upwards, outside of
    71  		// the cloned repository path.
    72  		req.subDir = filepath.Clean(req.subDir)
    73  		if containsDotDot(req.subDir) {
    74  			return nil, fmt.Errorf("subdirectory component contain path traversal out of the repository")
    75  		}
    76  
    77  		// Prevent absolute paths, remove a leading path separator from the subdirectory
    78  		if req.subDir[0] == os.PathSeparator {
    79  			req.subDir = req.subDir[1:]
    80  		}
    81  
    82  		td, tdcloser, err := safetemp.Dir("", "getter")
    83  		if err != nil {
    84  			return nil, err
    85  		}
    86  		defer tdcloser.Close()
    87  
    88  		req.realDst = req.Dst
    89  		req.Dst = td
    90  	}
    91  
    92  	var multierr []error
    93  	for _, g := range c.Getters {
    94  		shouldDownload, err := Detect(req, g)
    95  		if err != nil {
    96  			return nil, err
    97  		}
    98  		if !shouldDownload {
    99  			// the request should not be processed by that getter
   100  			continue
   101  		}
   102  
   103  		result, getErr := c.get(ctx, req, g)
   104  		if getErr != nil {
   105  			if getErr.Fatal {
   106  				return nil, getErr.Err
   107  			}
   108  			multierr = append(multierr, getErr.Err)
   109  			continue
   110  		}
   111  
   112  		return result, nil
   113  	}
   114  
   115  	if len(multierr) == 1 {
   116  		// This is for keeping the error original format
   117  		return nil, multierr[0]
   118  	}
   119  
   120  	if multierr != nil {
   121  		var result *multierror.Error
   122  		result = multierror.Append(result, multierr...)
   123  		return nil, fmt.Errorf("error downloading '%s': %s", req.Src, result.Error())
   124  	}
   125  
   126  	return nil, fmt.Errorf("error downloading '%s'", req.Src)
   127  }
   128  
   129  // getError is the Error response object returned by get(context.Context, *Request, Getter)
   130  // to tell the client whether to halt (Fatal) Get or to keep trying to get an artifact.
   131  type getError struct {
   132  	// When Fatal is true something went wrong with get(context.Context, *Request, Getter)
   133  	// and the client should halt and return the Err.
   134  	Fatal bool
   135  	Err   error
   136  }
   137  
   138  func (ge *getError) Error() string {
   139  	return ge.Err.Error()
   140  }
   141  
   142  func (c *Client) get(ctx context.Context, req *Request, g Getter) (*GetResult, *getError) {
   143  	u, err := urlhelper.Parse(req.Src)
   144  	req.u = u
   145  	if err != nil {
   146  		return nil, &getError{true, err}
   147  	}
   148  
   149  	// We have magic query parameters that we use to signal different features
   150  	q := req.u.Query()
   151  
   152  	// Determine if we have an archive type
   153  	archiveV := q.Get("archive")
   154  	if archiveV != "" {
   155  		// Delete the parameter since it is a magic parameter we don't
   156  		// want to pass on to the Getter
   157  		q.Del("archive")
   158  		req.u.RawQuery = q.Encode()
   159  
   160  		// If we can parse the value as a bool and it is false, then
   161  		// set the archive to "-" which should never map to a decompressor
   162  		if b, err := strconv.ParseBool(archiveV); err == nil && !b {
   163  			archiveV = "-"
   164  		}
   165  	} else {
   166  		// We don't appear to... but is it part of the filename?
   167  		matchingLen := 0
   168  		for k := range c.Decompressors {
   169  			if strings.HasSuffix(req.u.Path, "."+k) && len(k) > matchingLen {
   170  				archiveV = k
   171  				matchingLen = len(k)
   172  			}
   173  		}
   174  	}
   175  
   176  	// If we have a decompressor, then we need to change the destination
   177  	// to download to a temporary path. We unarchive this into the final,
   178  	// real path.
   179  	var decompressDst string
   180  	var decompressDir bool
   181  	decompressor := c.Decompressors[archiveV]
   182  	if decompressor != nil {
   183  		// Create a temporary directory to store our archive. We delete
   184  		// this at the end of everything.
   185  		td, err := ioutil.TempDir("", "getter")
   186  		if err != nil {
   187  			return nil, &getError{true, fmt.Errorf(
   188  				"Error creating temporary directory for archive: %s", err)}
   189  		}
   190  		defer os.RemoveAll(td)
   191  
   192  		// Swap the download directory to be our temporary path and
   193  		// store the old values.
   194  		decompressDst = req.Dst
   195  		decompressDir = req.GetMode != ModeFile
   196  		req.Dst = filepath.Join(td, "archive")
   197  		req.GetMode = ModeFile
   198  	}
   199  
   200  	// Determine checksum if we have one
   201  	checksum, err := c.GetChecksum(ctx, req)
   202  	if err != nil {
   203  		return nil, &getError{true, fmt.Errorf("invalid checksum: %s", err)}
   204  	}
   205  
   206  	// Delete the query parameter if we have it.
   207  	q.Del("checksum")
   208  	req.u.RawQuery = q.Encode()
   209  
   210  	if req.GetMode == ModeAny {
   211  		// Ask the getter which client mode to use
   212  		req.GetMode, err = g.Mode(ctx, req.u)
   213  		if err != nil {
   214  			return nil, &getError{false, err}
   215  		}
   216  
   217  		// Destination is the base name of the URL path in "any" mode when
   218  		// a file source is detected.
   219  		if req.GetMode == ModeFile {
   220  			filename := filepath.Base(req.u.Path)
   221  
   222  			// Determine if we have a custom file name
   223  			if v := q.Get("filename"); v != "" {
   224  				// Delete the query parameter if we have it.
   225  				q.Del("filename")
   226  				req.u.RawQuery = q.Encode()
   227  
   228  				filename = v
   229  			}
   230  
   231  			if containsDotDot(filename) {
   232  				return nil, &getError{true, fmt.Errorf("filename query parameter contain path traversal")}
   233  			}
   234  
   235  			req.Dst = filepath.Join(req.Dst, filename)
   236  		}
   237  	}
   238  
   239  	// If we're not downloading a directory, then just download the file
   240  	// and return.
   241  	if req.GetMode == ModeFile {
   242  		getFile := true
   243  		if checksum != nil {
   244  			if err := checksum.Checksum(req.Dst); err == nil {
   245  				// don't get the file if the checksum of dst is correct
   246  				getFile = false
   247  			}
   248  		}
   249  		if getFile {
   250  			if err := g.GetFile(ctx, req); err != nil {
   251  				return nil, &getError{false, err}
   252  			}
   253  
   254  			if checksum != nil {
   255  				if err := checksum.Checksum(req.Dst); err != nil {
   256  					return nil, &getError{true, err}
   257  				}
   258  			}
   259  		}
   260  
   261  		if decompressor != nil {
   262  			// We have a decompressor, so decompress the current destination
   263  			// into the final destination with the proper mode.
   264  			err := decompressor.Decompress(decompressDst, req.Dst, decompressDir, req.umask())
   265  			if err != nil {
   266  				return nil, &getError{true, err}
   267  			}
   268  
   269  			// Swap the information back
   270  			req.Dst = decompressDst
   271  			if decompressDir {
   272  				req.GetMode = ModeAny
   273  			} else {
   274  				req.GetMode = ModeFile
   275  			}
   276  		}
   277  
   278  		// We check the dir value again because it can be switched back
   279  		// if we were unarchiving. If we're still only Get-ing a file, then
   280  		// we're done.
   281  		if req.GetMode == ModeFile {
   282  			return &GetResult{req.Dst}, nil
   283  		}
   284  	}
   285  
   286  	// If we're at this point we're either downloading a directory or we've
   287  	// downloaded and unarchived a directory and we're just checking subdir.
   288  	// In the case we have a decompressor we don't Get because it was Get
   289  	// above.
   290  	if decompressor == nil {
   291  		// If we're getting a directory, then this is an error. You cannot
   292  		// checksum a directory. TODO: test
   293  		if checksum != nil {
   294  			return nil, &getError{true, fmt.Errorf(
   295  				"checksum cannot be specified for directory download")}
   296  		}
   297  
   298  		// We're downloading a directory, which might require a bit more work
   299  		// if we're specifying a subdir.
   300  		if err := g.Get(ctx, req); err != nil {
   301  			return nil, &getError{false, err}
   302  		}
   303  	}
   304  
   305  	// If we have a subdir, copy that over
   306  	if req.subDir != "" {
   307  		if err := os.RemoveAll(req.realDst); err != nil {
   308  			return nil, &getError{true, err}
   309  		}
   310  		if err := os.MkdirAll(req.realDst, req.Mode(0755)); err != nil {
   311  			return nil, &getError{true, err}
   312  		}
   313  
   314  		// Process any globs
   315  		subDir, err := SubdirGlob(req.Dst, req.subDir)
   316  		if err != nil {
   317  			return nil, &getError{true, err}
   318  		}
   319  
   320  		err = copyDir(ctx, req.realDst, subDir, false, req.DisableSymlinks, req.umask())
   321  		if err != nil {
   322  			return nil, &getError{false, err}
   323  		}
   324  		return &GetResult{req.realDst}, nil
   325  	}
   326  
   327  	return &GetResult{req.Dst}, nil
   328  
   329  }
   330  
   331  func (c *Client) checkArchive(req *Request) string {
   332  	q := req.u.Query()
   333  	archiveV := q.Get("archive")
   334  	if archiveV != "" {
   335  		// Delete the paramter since it is a magic parameter we don't
   336  		// want to pass on to the Getter
   337  		q.Del("archive")
   338  		req.u.RawQuery = q.Encode()
   339  
   340  		// If we can parse the value as a bool and it is false, then
   341  		// set the archive to "-" which should never map to a decompressor
   342  		if b, err := strconv.ParseBool(archiveV); err == nil && !b {
   343  			archiveV = "-"
   344  		}
   345  	}
   346  	if archiveV == "" {
   347  		// We don't appear to... but is it part of the filename?
   348  		matchingLen := 0
   349  		for k := range c.Decompressors {
   350  			if strings.HasSuffix(req.u.Path, "."+k) && len(k) > matchingLen {
   351  				archiveV = k
   352  				matchingLen = len(k)
   353  			}
   354  		}
   355  	}
   356  	return archiveV
   357  }