package getter import ( "bufio" "bytes" "crypto/md5" "crypto/sha1" "crypto/sha256" "crypto/sha512" "encoding/hex" "fmt" "hash" "io" "net/url" "os" "path/filepath" "strings" urlhelper "github.com/hashicorp/go-getter/helper/url" ) // FileChecksum helps verifying the checksum for a file. type FileChecksum struct { Type string Hash hash.Hash Value []byte Filename string } // A ChecksumError is returned when a checksum differs type ChecksumError struct { Hash hash.Hash Actual []byte Expected []byte File string } func (cerr *ChecksumError) Error() string { if cerr == nil { return "" } return fmt.Sprintf( "Checksums did not match for %s.\nExpected: %s\nGot: %s\n%T", cerr.File, hex.EncodeToString(cerr.Expected), hex.EncodeToString(cerr.Actual), cerr.Hash, // ex: *sha256.digest ) } // checksum is a simple method to compute the checksum of a source file // and compare it to the given expected value. func (c *FileChecksum) checksum(source string) error { f, err := os.Open(source) if err != nil { return fmt.Errorf("Failed to open file for checksum: %s", err) } defer f.Close() c.Hash.Reset() if _, err := io.Copy(c.Hash, f); err != nil { return fmt.Errorf("Failed to hash: %s", err) } if actual := c.Hash.Sum(nil); !bytes.Equal(actual, c.Value) { return &ChecksumError{ Hash: c.Hash, Actual: actual, Expected: c.Value, File: source, } } return nil } // extractChecksum will return a FileChecksum based on the 'checksum' // parameter of u. // ex: // http://hashicorp.com/terraform?checksum= // http://hashicorp.com/terraform?checksum=: // http://hashicorp.com/terraform?checksum=file: // when checksumming from a file, extractChecksum will go get checksum_url // in a temporary directory, parse the content of the file then delete it. // Content of files are expected to be BSD style or GNU style. // // BSD-style checksum: // MD5 (file1) = // MD5 (file2) = // // GNU-style: // file1 // *file2 // // see parseChecksumLine for more detail on checksum file parsing func (c *Client) extractChecksum(u *url.URL) (*FileChecksum, error) { q := u.Query() v := q.Get("checksum") if v == "" { return nil, nil } vs := strings.SplitN(v, ":", 2) switch len(vs) { case 2: break // good default: // here, we try to guess the checksum from it's length // if the type was not passed return newChecksumFromValue(v, filepath.Base(u.EscapedPath())) } checksumType, checksumValue := vs[0], vs[1] switch checksumType { case "file": return c.ChecksumFromFile(checksumValue, u) default: return newChecksumFromType(checksumType, checksumValue, filepath.Base(u.EscapedPath())) } } func newChecksum(checksumValue, filename string) (*FileChecksum, error) { c := &FileChecksum{ Filename: filename, } var err error c.Value, err = hex.DecodeString(checksumValue) if err != nil { return nil, fmt.Errorf("invalid checksum: %s", err) } return c, nil } func newChecksumFromType(checksumType, checksumValue, filename string) (*FileChecksum, error) { c, err := newChecksum(checksumValue, filename) if err != nil { return nil, err } c.Type = strings.ToLower(checksumType) switch c.Type { case "md5": c.Hash = md5.New() case "sha1": c.Hash = sha1.New() case "sha256": c.Hash = sha256.New() case "sha512": c.Hash = sha512.New() default: return nil, fmt.Errorf( "unsupported checksum type: %s", checksumType) } return c, nil } func newChecksumFromValue(checksumValue, filename string) (*FileChecksum, error) { c, err := newChecksum(checksumValue, filename) if err != nil { return nil, err } switch len(c.Value) { case md5.Size: c.Hash = md5.New() c.Type = "md5" case sha1.Size: c.Hash = sha1.New() c.Type = "sha1" case sha256.Size: c.Hash = sha256.New() c.Type = "sha256" case sha512.Size: c.Hash = sha512.New() c.Type = "sha512" default: return nil, fmt.Errorf("Unknown type for checksum %s", checksumValue) } return c, nil } // ChecksumFromFile will return all the FileChecksums found in file // // ChecksumFromFile will try to guess the hashing algorithm based on content // of checksum file // // ChecksumFromFile will only return checksums for files that match file // behind src func (c *Client) ChecksumFromFile(checksumFile string, src *url.URL) (*FileChecksum, error) { checksumFileURL, err := urlhelper.Parse(checksumFile) if err != nil { return nil, err } tempfile, err := tmpFile("", filepath.Base(checksumFileURL.Path)) if err != nil { return nil, err } defer os.Remove(tempfile) c2 := &Client{ Ctx: c.Ctx, Getters: c.Getters, Decompressors: c.Decompressors, Detectors: c.Detectors, Pwd: c.Pwd, Dir: false, Src: checksumFile, Dst: tempfile, ProgressListener: c.ProgressListener, } if err = c2.Get(); err != nil { return nil, fmt.Errorf( "Error downloading checksum file: %s", err) } filename := filepath.Base(src.Path) absPath, err := filepath.Abs(src.Path) if err != nil { return nil, err } checksumFileDir := filepath.Dir(checksumFileURL.Path) relpath, err := filepath.Rel(checksumFileDir, absPath) switch { case err == nil || err.Error() == "Rel: can't make "+absPath+" relative to "+checksumFileDir: // ex: on windows C:\gopath\...\content.txt cannot be relative to \ // which is okay, may be another expected path will work. break default: return nil, err } // possible file identifiers: options := []string{ filename, // ubuntu-14.04.1-server-amd64.iso "*" + filename, // *ubuntu-14.04.1-server-amd64.iso Standard checksum "?" + filename, // ?ubuntu-14.04.1-server-amd64.iso shasum -p relpath, // dir/ubuntu-14.04.1-server-amd64.iso "./" + relpath, // ./dir/ubuntu-14.04.1-server-amd64.iso absPath, // fullpath; set if local } f, err := os.Open(tempfile) if err != nil { return nil, fmt.Errorf( "Error opening downloaded file: %s", err) } defer f.Close() rd := bufio.NewReader(f) for { line, err := rd.ReadString('\n') if err != nil { if err != io.EOF { return nil, fmt.Errorf( "Error reading checksum file: %s", err) } if line == "" { break } // parse the line, if we hit EOF, but the line is not empty } checksum, err := parseChecksumLine(line) if err != nil || checksum == nil { continue } if checksum.Filename == "" { // filename not sure, let's try return checksum, nil } // make sure the checksum is for the right file for _, option := range options { if option != "" && checksum.Filename == option { // any checksum will work so we return the first one return checksum, nil } } } return nil, fmt.Errorf("no checksum found in: %s", checksumFile) } // parseChecksumLine takes a line from a checksum file and returns // checksumType, checksumValue and filename parseChecksumLine guesses the style // of the checksum BSD vs GNU by splitting the line and by counting the parts. // of a line. // for BSD type sums parseChecksumLine guesses the hashing algorithm // by checking the length of the checksum. func parseChecksumLine(line string) (*FileChecksum, error) { parts := strings.Fields(line) switch len(parts) { case 4: // BSD-style checksum: // MD5 (file1) = // MD5 (file2) = if len(parts[1]) <= 2 || parts[1][0] != '(' || parts[1][len(parts[1])-1] != ')' { return nil, fmt.Errorf( "Unexpected BSD-style-checksum filename format: %s", line) } filename := parts[1][1 : len(parts[1])-1] return newChecksumFromType(parts[0], parts[3], filename) case 2: // GNU-style: // file1 // *file2 return newChecksumFromValue(parts[0], parts[1]) case 0: return nil, nil // empty line default: return newChecksumFromValue(parts[0], "") } }