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

     1  package getter
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"crypto/md5"
     8  	"crypto/sha1"
     9  	"crypto/sha256"
    10  	"crypto/sha512"
    11  	"encoding/hex"
    12  	"fmt"
    13  	"hash"
    14  	"io"
    15  	"os"
    16  	"path/filepath"
    17  	"strings"
    18  
    19  	urlhelper "github.com/hashicorp/go-getter/v2/helper/url"
    20  )
    21  
    22  // FileChecksum helps verifying the checksum for a file.
    23  type FileChecksum struct {
    24  	Type     string
    25  	Hash     hash.Hash
    26  	Value    []byte
    27  	Filename string
    28  }
    29  
    30  // String returns the hash type and the hash separated by a colon, for example:
    31  //  "md5:090992ba9fd140077b0661cb75f7ce13"
    32  //  "sha1:ebfb681885ddf1234c18094a45bbeafd91467911"
    33  func (c *FileChecksum) String() string {
    34  	return c.Type + ":" + hex.EncodeToString(c.Value)
    35  }
    36  
    37  // A ChecksumError is returned when a checksum differs
    38  type ChecksumError struct {
    39  	Hash     hash.Hash
    40  	Actual   []byte
    41  	Expected []byte
    42  	File     string
    43  }
    44  
    45  func (cerr *ChecksumError) Error() string {
    46  	if cerr == nil {
    47  		return "<nil>"
    48  	}
    49  	return fmt.Sprintf(
    50  		"Checksums did not match for %s.\nExpected: %s\nGot: %s\n%T",
    51  		cerr.File,
    52  		hex.EncodeToString(cerr.Expected),
    53  		hex.EncodeToString(cerr.Actual),
    54  		cerr.Hash, // ex: *sha256.digest
    55  	)
    56  }
    57  
    58  // Checksum computes the Checksum for filePath using the hashing algorithm from
    59  // c.Hash and compares it to c.Value. If those values differ a ChecksumError
    60  // will be returned.
    61  func (c *FileChecksum) Checksum(filePath string) error {
    62  	f, err := os.Open(filePath)
    63  	if err != nil {
    64  		return fmt.Errorf("Failed to open file for checksum: %s", err)
    65  	}
    66  	defer f.Close()
    67  
    68  	c.Hash.Reset()
    69  	if _, err := io.Copy(c.Hash, f); err != nil {
    70  		return fmt.Errorf("Failed to hash: %s", err)
    71  	}
    72  
    73  	if actual := c.Hash.Sum(nil); !bytes.Equal(actual, c.Value) {
    74  		return &ChecksumError{
    75  			Hash:     c.Hash,
    76  			Actual:   actual,
    77  			Expected: c.Value,
    78  			File:     filePath,
    79  		}
    80  	}
    81  
    82  	return nil
    83  }
    84  
    85  // GetChecksum extracts the checksum from the `checksum` parameter
    86  // of the src of the Request
    87  // ex:
    88  //  http://hashicorp.com/terraform?checksum=<checksumValue>
    89  //  http://hashicorp.com/terraform?checksum=<checksumType>:<checksumValue>
    90  //  http://hashicorp.com/terraform?checksum=file:<checksum_url>
    91  // when the checksum is in a file, GetChecksum will first client.Get it
    92  // in a temporary directory, parse the content of the file and finally delete it.
    93  // The content of a checksum file is expected to be BSD style or GNU style.
    94  // For security reasons GetChecksum does not try to get the current working directory
    95  // and as a result, relative files will only be found when Request.Pwd is set.
    96  //
    97  // BSD-style checksum:
    98  //  MD5 (file1) = <checksum>
    99  //  MD5 (file2) = <checksum>
   100  //
   101  // GNU-style:
   102  //  <checksum>  file1
   103  //  <checksum> *file2
   104  func (c *Client) GetChecksum(ctx context.Context, req *Request) (*FileChecksum, error) {
   105  	var err error
   106  	if req.u == nil {
   107  		req.u, err = urlhelper.Parse(req.Src)
   108  		if err != nil {
   109  			return nil, err
   110  		}
   111  	}
   112  	q := req.u.Query()
   113  	v := q.Get("checksum")
   114  
   115  	if v == "" {
   116  		return nil, nil
   117  	}
   118  
   119  	vs := strings.SplitN(v, ":", 2)
   120  	switch len(vs) {
   121  	case 2:
   122  		break // good
   123  	default:
   124  		// here, we try to guess the checksum from it's length
   125  		// if the type was not passed
   126  		return newChecksumFromValue(v, filepath.Base(req.u.EscapedPath()))
   127  	}
   128  
   129  	checksumType, checksumValue := vs[0], vs[1]
   130  
   131  	switch checksumType {
   132  	case "file":
   133  		return c.checksumFromFile(ctx, checksumValue, req.u.Path, req.Pwd)
   134  	default:
   135  		return newChecksumFromType(checksumType, checksumValue, filepath.Base(req.u.EscapedPath()))
   136  	}
   137  }
   138  
   139  func newChecksum(checksumValue, filename string) (*FileChecksum, error) {
   140  	c := &FileChecksum{
   141  		Filename: filename,
   142  	}
   143  	var err error
   144  	c.Value, err = hex.DecodeString(checksumValue)
   145  	if err != nil {
   146  		return nil, fmt.Errorf("invalid checksum: %s", err)
   147  	}
   148  	return c, nil
   149  }
   150  
   151  func newChecksumFromType(checksumType, checksumValue, filename string) (*FileChecksum, error) {
   152  	c, err := newChecksum(checksumValue, filename)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	c.Type = strings.ToLower(checksumType)
   158  	switch c.Type {
   159  	case "md5":
   160  		c.Hash = md5.New()
   161  	case "sha1":
   162  		c.Hash = sha1.New()
   163  	case "sha256":
   164  		c.Hash = sha256.New()
   165  	case "sha512":
   166  		c.Hash = sha512.New()
   167  	default:
   168  		return nil, fmt.Errorf(
   169  			"unsupported checksum type: %s", checksumType)
   170  	}
   171  
   172  	return c, nil
   173  }
   174  
   175  func newChecksumFromValue(checksumValue, filename string) (*FileChecksum, error) {
   176  	c, err := newChecksum(checksumValue, filename)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  
   181  	switch len(c.Value) {
   182  	case md5.Size:
   183  		c.Hash = md5.New()
   184  		c.Type = "md5"
   185  	case sha1.Size:
   186  		c.Hash = sha1.New()
   187  		c.Type = "sha1"
   188  	case sha256.Size:
   189  		c.Hash = sha256.New()
   190  		c.Type = "sha256"
   191  	case sha512.Size:
   192  		c.Hash = sha512.New()
   193  		c.Type = "sha512"
   194  	default:
   195  		return nil, fmt.Errorf("Unknown type for checksum %s", checksumValue)
   196  	}
   197  
   198  	return c, nil
   199  }
   200  
   201  // checksumFromFile will return the first file checksum found in the
   202  // `checksumURL` file that corresponds to the `checksummedPath` path.
   203  //
   204  // checksumFromFile will infer the hashing algorithm based on the checksumURL
   205  // file content.
   206  //
   207  // checksumFromFile will only return checksums for files that match
   208  // checksummedPath, which is the object being checksummed.
   209  func (c *Client) checksumFromFile(ctx context.Context, checksumURL string, checksummedPath string, pwd string) (*FileChecksum, error) {
   210  	checksumFileURL, err := urlhelper.Parse(checksumURL)
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  
   215  	tempfile, err := tmpFile("", filepath.Base(checksumFileURL.Path))
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  	defer os.Remove(tempfile)
   220  
   221  	req := &Request{
   222  		Pwd:     pwd,
   223  		GetMode: ModeFile,
   224  		Src:     checksumURL,
   225  		Dst:     tempfile,
   226  		// ProgressListener: c.ProgressListener, TODO(adrien): pass progress bar ?
   227  	}
   228  
   229  	if _, err = c.Get(ctx, req); err != nil {
   230  		return nil, fmt.Errorf(
   231  			"Error downloading checksum file: %s", err)
   232  	}
   233  
   234  	filename := filepath.Base(checksummedPath)
   235  	absPath, err := filepath.Abs(checksummedPath)
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  	checksumFileDir := filepath.Dir(checksumFileURL.Path)
   240  	relpath, err := filepath.Rel(checksumFileDir, absPath)
   241  	switch {
   242  	case err == nil ||
   243  		err.Error() == "Rel: can't make "+absPath+" relative to "+checksumFileDir:
   244  		// ex: on windows C:\gopath\...\content.txt cannot be relative to \
   245  		// which is okay, may be another expected path will work.
   246  		break
   247  	default:
   248  		return nil, err
   249  	}
   250  
   251  	// possible file identifiers:
   252  	options := []string{
   253  		filename,       // ubuntu-14.04.1-server-amd64.iso
   254  		"*" + filename, // *ubuntu-14.04.1-server-amd64.iso  Standard checksum
   255  		"?" + filename, // ?ubuntu-14.04.1-server-amd64.iso  shasum -p
   256  		relpath,        // dir/ubuntu-14.04.1-server-amd64.iso
   257  		"./" + relpath, // ./dir/ubuntu-14.04.1-server-amd64.iso
   258  		absPath,        // fullpath; set if local
   259  	}
   260  
   261  	f, err := os.Open(tempfile)
   262  	if err != nil {
   263  		return nil, fmt.Errorf(
   264  			"Error opening downloaded file: %s", err)
   265  	}
   266  	defer f.Close()
   267  	rd := bufio.NewReader(f)
   268  	for {
   269  		line, err := rd.ReadString('\n')
   270  		if err != nil {
   271  			if err != io.EOF {
   272  				return nil, fmt.Errorf(
   273  					"Error reading checksum file: %s", err)
   274  			}
   275  			break
   276  		}
   277  		checksum, err := parseChecksumLine(line)
   278  		if err != nil || checksum == nil {
   279  			continue
   280  		}
   281  		if checksum.Filename == "" {
   282  			// filename not sure, let's try
   283  			return checksum, nil
   284  		}
   285  		// make sure the checksum is for the right file
   286  		for _, option := range options {
   287  			if option != "" && checksum.Filename == option {
   288  				// any checksum will work so we return the first one
   289  				return checksum, nil
   290  			}
   291  		}
   292  		// The checksum filename can contain a sub folder to differ versions.
   293  		// e.g. ./netboot/mini.iso and ./hwe-netboot/mini.iso
   294  		// In this case we remove root folder characters to compare with the checksummed path
   295  		fn := strings.TrimLeft(checksum.Filename, "./")
   296  		if strings.Contains(checksummedPath, fn) {
   297  			return checksum, nil
   298  		}
   299  	}
   300  	return nil, fmt.Errorf("no checksum found in: %s", checksumURL)
   301  }
   302  
   303  // parseChecksumLine takes a line from a checksum file and returns
   304  // checksumType, checksumValue and filename parseChecksumLine guesses the style
   305  // of the checksum BSD vs GNU by splitting the line and by counting the parts.
   306  // of a line.
   307  // for BSD type sums parseChecksumLine guesses the hashing algorithm
   308  // by checking the length of the checksum.
   309  func parseChecksumLine(line string) (*FileChecksum, error) {
   310  	switch line[0] {
   311  	case '#', '/', '-':
   312  		return nil, nil // skip
   313  	}
   314  	//TODO: this function will fail if we pass a checksum for a path with spaces
   315  	parts := strings.Fields(line)
   316  
   317  	switch len(parts) {
   318  	case 4:
   319  		// BSD-style checksum:
   320  		//  MD5 (file1) = <checksum>
   321  		//  MD5 (file2) = <checksum>
   322  		if len(parts[1]) <= 2 ||
   323  			parts[1][0] != '(' || parts[1][len(parts[1])-1] != ')' {
   324  			return nil, fmt.Errorf(
   325  				"Unexpected BSD-style-checksum filename format: %s", line)
   326  		}
   327  		filename := parts[1][1 : len(parts[1])-1]
   328  		return newChecksumFromType(parts[0], parts[3], filename)
   329  	case 2:
   330  		// GNU-style:
   331  		//  <checksum>  file1
   332  		//  <checksum> *file2
   333  		return newChecksumFromValue(parts[0], parts[1])
   334  	case 0:
   335  		return nil, nil // empty line
   336  	default:
   337  		return newChecksumFromValue(parts[0], "")
   338  	}
   339  }