Skip to content

Commit

Permalink
feat: add sha256sum check when downloading from hashicorp
Browse files Browse the repository at this point in the history
  • Loading branch information
c4po committed Apr 14, 2022
1 parent d33778d commit f43b75b
Showing 1 changed file with 80 additions and 20 deletions.
100 changes: 80 additions & 20 deletions internal/releaseapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package releaseapi

import (
"archive/zip"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
Expand All @@ -10,6 +12,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"

"github.com/Masterminds/semver/v3"
"github.com/gregjones/httpcache"
Expand All @@ -28,6 +31,7 @@ type ReleaseIndex struct {

type Release struct {
Version *semver.Version `json:"version"`
Shasums string `json:"shasums"`
Builds []Build `json:"builds"`
}

Expand Down Expand Up @@ -87,48 +91,101 @@ func (c *Client) ListReleases() (ReleaseIndex, error) {

func (c *Client) DownloadRelease(r Release, os, arch string) (string, error) {
var matchingBuild Build

var checkSha256Sum string
for _, build := range r.Builds {
if build.OS == os && build.Arch == arch {
matchingBuild = build
break
}
}

checkSums, err := c.getReleaseCheckSums(r)
if err != nil {
return "", errors.Wrap(err, "could not download checksum file")
}

for _, line := range strings.Split(checkSums, "\n") {
checksum := strings.Split(line, " ")
if checksum[1] == matchingBuild.zipFileName() {
checkSha256Sum = checksum[0]
break
}
}

if matchingBuild.URL == "" {
return "", errors.Errorf(
"could not find matching build for OS '%s' and arch '%s'", os, arch,
)
}

return c.downloadBuild(matchingBuild)
build, sha256sum, err := c.downloadBuild(matchingBuild)
if sha256sum != nil { // new download
if checkSha256Sum != hex.EncodeToString(sha256sum) {
return "", errors.Errorf(
"checksum for %s should be %s, got %s", matchingBuild.URL, checkSha256Sum, hex.EncodeToString(sha256sum),
)
} else {
log.Printf("check sum match\n")
}
}

return build, err
}

func (c *Client) downloadBuild(build Build) (string, error) {
func (c *Client) getReleaseCheckSums(release Release) (string, error) {
shaSumFile, _, err := c.downloadReleaseArchive(release.ShaSumsURL())
defer os.Remove(shaSumFile.Name())
defer shaSumFile.Close()
if err != nil {
return "", errors.Wrap(err, "could not download checksum file")
}
checkSums, err := os.ReadFile(shaSumFile.Name())
return string(checkSums), err
}

func (r *Release) ShaSumsURL() string {
releaseUrl := "https://releases.hashicorp.com/terraform"
return fmt.Sprintf("%s/%s/%s", releaseUrl, r.Version, r.Shasums)
}

func (c *Client) downloadBuild(build Build) (string, []byte, error) {
path := cachedExecutablePath(c.cacheDir, build)

if _, err := os.Stat(path); err == nil {
log.Printf("found cached Terraform executable at %s", path)

return path, nil
return path, nil, nil
} else if !os.IsNotExist(err) {
return "", errors.Wrap(err, "could not stat Terraform executable")
return "", nil, errors.Wrap(err, "could not stat Terraform executable")
}

log.Printf("dowloading release archive from %s", build.URL)

zipFile, zipLength, err := c.downloadReleaseArchive(build)
zipFile, zipLength, err := c.downloadReleaseArchive(build.URL)
defer os.Remove(zipFile.Name())
defer zipFile.Close()

if err != nil {
return "", err
return "", nil, err
}

defer zipFile.Close()
f, err := os.Open(zipFile.Name())
if err != nil {
return "", nil, errors.Wrap(err, "could not open zip archive")
}
defer f.Close()

h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", nil, errors.Wrap(err, "could not check sha256sum for zip archive")
}

sha256Sum := h.Sum(nil)

zipReader, err := zip.NewReader(zipFile, zipLength)

if err != nil {
return "", errors.Wrap(err, "could not unzip release archive")
return "", nil, errors.Wrap(err, "could not unzip release archive")
}

for _, f := range zipReader.File {
Expand All @@ -139,27 +196,27 @@ func (c *Client) downloadBuild(build Build) (string, error) {
source, err := f.Open()

if err != nil {
return "", errors.Wrap(err, "could not read binary in release archive")
return "", nil, errors.Wrap(err, "could not read binary in release archive")
}

defer source.Close()

if err := atomic.WriteFile(path, source); err != nil {
return "", errors.Wrap(err, "could not write binary to the cache directory")
return "", nil, errors.Wrap(err, "could not write binary to the cache directory")
}

if err := os.Chmod(path, 0700); err != nil {
return "", errors.Wrap(err, "could not make binary executable")
return "", nil, errors.Wrap(err, "could not make binary executable")
}

return path, nil
return path, sha256Sum, nil
}

return "", errors.New("could not find executable named 'terraform' in release archive")
return "", nil, errors.New("could not find executable named 'terraform' in release archive")
}

func (c *Client) downloadReleaseArchive(build Build) (*os.File, int64, error) {
request, err := http.NewRequest("GET", build.URL, nil)
func (c *Client) downloadReleaseArchive(url string) (*os.File, int64, error) {
request, err := http.NewRequest("GET", url, nil)

if err != nil {
return nil, 0, errors.Wrap(err, "could not create request for release archive")
Expand All @@ -179,7 +236,7 @@ func (c *Client) downloadReleaseArchive(build Build) (*os.File, int64, error) {
return nil, 0, errors.Errorf("unexpected status code '%s' in response", response.StatusCode)
}

tmp, err := ioutil.TempFile("", filepath.Base(build.URL))
tmp, err := ioutil.TempFile("", filepath.Base(url))

if err != nil {
return nil, 0, errors.Wrap(err, "could not create temporary file for release archive")
Expand All @@ -188,14 +245,17 @@ func (c *Client) downloadReleaseArchive(build Build) (*os.File, int64, error) {
if _, err := io.Copy(tmp, response.Body); err != nil {
return nil, 0, errors.Wrap(err, "could not copy release archive to temporary file")
}

return tmp, response.ContentLength, nil
}

func cachedExecutablePath(cacheDir string, b Build) string {
return filepath.Join(cacheDir, executableName(b))
return filepath.Join(cacheDir, b.executableName())
}

func executableName(b Build) string {
func (b *Build) executableName() string {
return fmt.Sprintf("terraform_%s_%s_%s", b.Version.String(), b.OS, b.Arch)
}

func (b *Build) zipFileName() string {
return filepath.Base(b.URL)
}

0 comments on commit f43b75b

Please sign in to comment.