You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
terraformDummyRepo2/vendor/github.com/hashicorp/hc-install/internal/releasesjson/downloader.go

193 lines
4.7 KiB
Go

package releasesjson
import (
"archive/zip"
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/hashicorp/hc-install/internal/httpclient"
)
type Downloader struct {
Logger *log.Logger
VerifyChecksum bool
ArmoredPublicKey string
BaseURL string
}
func (d *Downloader) DownloadAndUnpack(ctx context.Context, pv *ProductVersion, dstDir string) (zipFilePath string, err error) {
if len(pv.Builds) == 0 {
return "", fmt.Errorf("no builds found for %s %s", pv.Name, pv.Version)
}
pb, ok := pv.Builds.FilterBuild(runtime.GOOS, runtime.GOARCH, "zip")
if !ok {
return "", fmt.Errorf("no ZIP archive found for %s %s %s/%s",
pv.Name, pv.Version, runtime.GOOS, runtime.GOARCH)
}
var verifiedChecksum HashSum
if d.VerifyChecksum {
v := &ChecksumDownloader{
BaseURL: d.BaseURL,
ProductVersion: pv,
Logger: d.Logger,
ArmoredPublicKey: d.ArmoredPublicKey,
}
verifiedChecksums, err := v.DownloadAndVerifyChecksums(ctx)
if err != nil {
return "", err
}
var ok bool
verifiedChecksum, ok = verifiedChecksums[pb.Filename]
if !ok {
return "", fmt.Errorf("no checksum found for %q", pb.Filename)
}
}
client := httpclient.NewHTTPClient()
archiveURL := pb.URL
if d.BaseURL != "" {
// ensure that absolute download links from mocked responses
// are still pointing to the mock server if one is set
baseURL, err := url.Parse(d.BaseURL)
if err != nil {
return "", err
}
u, err := url.Parse(archiveURL)
if err != nil {
return "", err
}
u.Scheme = baseURL.Scheme
u.Host = baseURL.Host
archiveURL = u.String()
}
d.Logger.Printf("downloading archive from %s", archiveURL)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, archiveURL, nil)
if err != nil {
return "", fmt.Errorf("failed to create request for %q: %w", archiveURL, err)
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("failed to download ZIP archive from %q: %s", archiveURL, resp.Status)
}
defer resp.Body.Close()
var pkgReader io.Reader
pkgReader = resp.Body
contentType := resp.Header.Get("content-type")
if !contentTypeIsZip(contentType) {
return "", fmt.Errorf("unexpected content-type: %s (expected any of %q)",
contentType, zipMimeTypes)
}
expectedSize := resp.ContentLength
if d.VerifyChecksum {
d.Logger.Printf("verifying checksum of %q", pb.Filename)
// provide extra reader to calculate & compare checksum
var buf bytes.Buffer
r := io.TeeReader(resp.Body, &buf)
pkgReader = &buf
err := compareChecksum(d.Logger, r, verifiedChecksum, pb.Filename, expectedSize)
if err != nil {
return "", err
}
}
pkgFile, err := ioutil.TempFile("", pb.Filename)
if err != nil {
return "", err
}
defer pkgFile.Close()
pkgFilePath, err := filepath.Abs(pkgFile.Name())
d.Logger.Printf("copying %q (%d bytes) to %s", pb.Filename, expectedSize, pkgFile.Name())
// Unless the bytes were already downloaded above for checksum verification
// this may take a while depending on network connection as the io.Reader
// is expected to be http.Response.Body which streams the bytes
// on demand over the network.
bytesCopied, err := io.Copy(pkgFile, pkgReader)
if err != nil {
return pkgFilePath, err
}
d.Logger.Printf("copied %d bytes to %s", bytesCopied, pkgFile.Name())
if expectedSize != 0 && bytesCopied != int64(expectedSize) {
return pkgFilePath, fmt.Errorf("unexpected size (downloaded: %d, expected: %d)",
bytesCopied, expectedSize)
}
r, err := zip.OpenReader(pkgFile.Name())
if err != nil {
return pkgFilePath, err
}
defer r.Close()
for _, f := range r.File {
if strings.Contains(f.Name, "..") {
// While we generally trust the source ZIP file
// we still reject path traversal attempts as a precaution.
continue
}
srcFile, err := f.Open()
if err != nil {
return pkgFilePath, err
}
d.Logger.Printf("unpacking %s to %s", f.Name, dstDir)
dstPath := filepath.Join(dstDir, f.Name)
dstFile, err := os.Create(dstPath)
if err != nil {
return pkgFilePath, err
}
_, err = io.Copy(dstFile, srcFile)
if err != nil {
return pkgFilePath, err
}
srcFile.Close()
dstFile.Close()
}
return pkgFilePath, nil
}
// The production release site uses consistent single mime type
// but mime types are platform-dependent
// and we may use different OS under test
var zipMimeTypes = []string{
"application/x-zip-compressed", // Windows
"application/zip", // Unix
}
func contentTypeIsZip(contentType string) bool {
for _, mt := range zipMimeTypes {
if mt == contentType {
return true
}
}
return false
}