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 }