github.com/weiwenhao/getter@v1.30.1/checksum.go (about)

     1  package getter
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"crypto/md5"
     7  	"crypto/sha1"
     8  	"crypto/sha256"
     9  	"crypto/sha512"
    10  	"encoding/hex"
    11  	"fmt"
    12  	"hash"
    13  	"io"
    14  	"net/url"
    15  	"os"
    16  	"path/filepath"
    17  	"strings"
    18  
    19  	urlhelper "github.com/weiwenhao/getter/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  // A ChecksumError is returned when a checksum differs
    31  type ChecksumError struct {
    32  	Hash     hash.Hash
    33  	Actual   []byte
    34  	Expected []byte
    35  	File     string
    36  }
    37  
    38  func (cerr *ChecksumError) Error() string {
    39  	if cerr == nil {
    40  		return "<nil>"
    41  	}
    42  	return fmt.Sprintf(
    43  		"Checksums did not match for %s.\nExpected: %s\nGot: %s\n%T",
    44  		cerr.File,
    45  		hex.EncodeToString(cerr.Expected),
    46  		hex.EncodeToString(cerr.Actual),
    47  		cerr.Hash, // ex: *sha256.digest
    48  	)
    49  }
    50  
    51  // checksum is a simple method to compute the checksum of a source file
    52  // and compare it to the given expected value.
    53  func (c *FileChecksum) checksum(source string) error {
    54  	f, err := os.Open(source)
    55  	if err != nil {
    56  		return fmt.Errorf("Failed to open file for checksum: %s", err)
    57  	}
    58  	defer f.Close()
    59  
    60  	c.Hash.Reset()
    61  	if _, err := io.Copy(c.Hash, f); err != nil {
    62  		return fmt.Errorf("Failed to hash: %s", err)
    63  	}
    64  
    65  	if actual := c.Hash.Sum(nil); !bytes.Equal(actual, c.Value) {
    66  		return &ChecksumError{
    67  			Hash:     c.Hash,
    68  			Actual:   actual,
    69  			Expected: c.Value,
    70  			File:     source,
    71  		}
    72  	}
    73  
    74  	return nil
    75  }
    76  
    77  // extractChecksum will return a FileChecksum based on the 'checksum'
    78  // parameter of u.
    79  // ex:
    80  //  http://hashicorp.com/terraform?checksum=<checksumValue>
    81  //  http://hashicorp.com/terraform?checksum=<checksumType>:<checksumValue>
    82  //  http://hashicorp.com/terraform?checksum=file:<checksum_url>
    83  // when checksumming from a file, extractChecksum will go get checksum_url
    84  // in a temporary directory, parse the content of the file then delete it.
    85  // Content of files are expected to be BSD style or GNU style.
    86  //
    87  // BSD-style checksum:
    88  //  MD5 (file1) = <checksum>
    89  //  MD5 (file2) = <checksum>
    90  //
    91  // GNU-style:
    92  //  <checksum>  file1
    93  //  <checksum> *file2
    94  //
    95  // see parseChecksumLine for more detail on checksum file parsing
    96  func (c *Client) extractChecksum(u *url.URL) (*FileChecksum, error) {
    97  	q := u.Query()
    98  	v := q.Get("checksum")
    99  
   100  	if v == "" {
   101  		return nil, nil
   102  	}
   103  
   104  	vs := strings.SplitN(v, ":", 2)
   105  	switch len(vs) {
   106  	case 2:
   107  		break // good
   108  	default:
   109  		// here, we try to guess the checksum from it's length
   110  		// if the type was not passed
   111  		return newChecksumFromValue(v, filepath.Base(u.EscapedPath()))
   112  	}
   113  
   114  	checksumType, checksumValue := vs[0], vs[1]
   115  
   116  	switch checksumType {
   117  	case "file":
   118  		return c.ChecksumFromFile(checksumValue, u)
   119  	default:
   120  		return newChecksumFromType(checksumType, checksumValue, filepath.Base(u.EscapedPath()))
   121  	}
   122  }
   123  
   124  func newChecksum(checksumValue, filename string) (*FileChecksum, error) {
   125  	c := &FileChecksum{
   126  		Filename: filename,
   127  	}
   128  	var err error
   129  	c.Value, err = hex.DecodeString(checksumValue)
   130  	if err != nil {
   131  		return nil, fmt.Errorf("invalid checksum: %s", err)
   132  	}
   133  	return c, nil
   134  }
   135  
   136  func newChecksumFromType(checksumType, checksumValue, filename string) (*FileChecksum, error) {
   137  	c, err := newChecksum(checksumValue, filename)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	c.Type = strings.ToLower(checksumType)
   143  	switch c.Type {
   144  	case "md5":
   145  		c.Hash = md5.New()
   146  	case "sha1":
   147  		c.Hash = sha1.New()
   148  	case "sha256":
   149  		c.Hash = sha256.New()
   150  	case "sha512":
   151  		c.Hash = sha512.New()
   152  	default:
   153  		return nil, fmt.Errorf(
   154  			"unsupported checksum type: %s", checksumType)
   155  	}
   156  
   157  	return c, nil
   158  }
   159  
   160  func newChecksumFromValue(checksumValue, filename string) (*FileChecksum, error) {
   161  	c, err := newChecksum(checksumValue, filename)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	switch len(c.Value) {
   167  	case md5.Size:
   168  		c.Hash = md5.New()
   169  		c.Type = "md5"
   170  	case sha1.Size:
   171  		c.Hash = sha1.New()
   172  		c.Type = "sha1"
   173  	case sha256.Size:
   174  		c.Hash = sha256.New()
   175  		c.Type = "sha256"
   176  	case sha512.Size:
   177  		c.Hash = sha512.New()
   178  		c.Type = "sha512"
   179  	default:
   180  		return nil, fmt.Errorf("Unknown type for checksum %s", checksumValue)
   181  	}
   182  
   183  	return c, nil
   184  }
   185  
   186  // ChecksumFromFile will return all the FileChecksums found in file
   187  //
   188  // ChecksumFromFile will try to guess the hashing algorithm based on content
   189  // of checksum file
   190  //
   191  // ChecksumFromFile will only return checksums for files that match file
   192  // behind src
   193  func (c *Client) ChecksumFromFile(checksumFile string, src *url.URL) (*FileChecksum, error) {
   194  	checksumFileURL, err := urlhelper.Parse(checksumFile)
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  
   199  	tempfile, err := tmpFile("", filepath.Base(checksumFileURL.Path))
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  	defer os.Remove(tempfile)
   204  
   205  	c2 := &Client{
   206  		Ctx:              c.Ctx,
   207  		Getters:          c.Getters,
   208  		Decompressors:    c.Decompressors,
   209  		Detectors:        c.Detectors,
   210  		Pwd:              c.Pwd,
   211  		Dir:              false,
   212  		Src:              checksumFile,
   213  		Dst:              tempfile,
   214  		ProgressListener: c.ProgressListener,
   215  	}
   216  	if err = c2.Get(); err != nil {
   217  		return nil, fmt.Errorf(
   218  			"Error downloading checksum file: %s", err)
   219  	}
   220  
   221  	filename := filepath.Base(src.Path)
   222  	absPath, err := filepath.Abs(src.Path)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	checksumFileDir := filepath.Dir(checksumFileURL.Path)
   227  	relpath, err := filepath.Rel(checksumFileDir, absPath)
   228  	switch {
   229  	case err == nil ||
   230  		err.Error() == "Rel: can't make "+absPath+" relative to "+checksumFileDir:
   231  		// ex: on windows C:\gopath\...\content.txt cannot be relative to \
   232  		// which is okay, may be another expected path will work.
   233  		break
   234  	default:
   235  		return nil, err
   236  	}
   237  
   238  	// possible file identifiers:
   239  	options := []string{
   240  		filename,       // ubuntu-14.04.1-server-amd64.iso
   241  		"*" + filename, // *ubuntu-14.04.1-server-amd64.iso  Standard checksum
   242  		"?" + filename, // ?ubuntu-14.04.1-server-amd64.iso  shasum -p
   243  		relpath,        // dir/ubuntu-14.04.1-server-amd64.iso
   244  		"./" + relpath, // ./dir/ubuntu-14.04.1-server-amd64.iso
   245  		absPath,        // fullpath; set if local
   246  	}
   247  
   248  	f, err := os.Open(tempfile)
   249  	if err != nil {
   250  		return nil, fmt.Errorf(
   251  			"Error opening downloaded file: %s", err)
   252  	}
   253  	defer f.Close()
   254  	rd := bufio.NewReader(f)
   255  	for {
   256  		line, err := rd.ReadString('\n')
   257  		if err != nil {
   258  			if err != io.EOF {
   259  				return nil, fmt.Errorf(
   260  					"Error reading checksum file: %s", err)
   261  			}
   262  			if line == "" {
   263  				break
   264  			}
   265  			// parse the line, if we hit EOF, but the line is not empty
   266  		}
   267  		checksum, err := parseChecksumLine(line)
   268  		if err != nil || checksum == nil {
   269  			continue
   270  		}
   271  		if checksum.Filename == "" {
   272  			// filename not sure, let's try
   273  			return checksum, nil
   274  		}
   275  		// make sure the checksum is for the right file
   276  		for _, option := range options {
   277  			if option != "" && checksum.Filename == option {
   278  				// any checksum will work so we return the first one
   279  				return checksum, nil
   280  			}
   281  		}
   282  	}
   283  	return nil, fmt.Errorf("no checksum found in: %s", checksumFile)
   284  }
   285  
   286  // parseChecksumLine takes a line from a checksum file and returns
   287  // checksumType, checksumValue and filename parseChecksumLine guesses the style
   288  // of the checksum BSD vs GNU by splitting the line and by counting the parts.
   289  // of a line.
   290  // for BSD type sums parseChecksumLine guesses the hashing algorithm
   291  // by checking the length of the checksum.
   292  func parseChecksumLine(line string) (*FileChecksum, error) {
   293  	parts := strings.Fields(line)
   294  
   295  	switch len(parts) {
   296  	case 4:
   297  		// BSD-style checksum:
   298  		//  MD5 (file1) = <checksum>
   299  		//  MD5 (file2) = <checksum>
   300  		if len(parts[1]) <= 2 ||
   301  			parts[1][0] != '(' || parts[1][len(parts[1])-1] != ')' {
   302  			return nil, fmt.Errorf(
   303  				"Unexpected BSD-style-checksum filename format: %s", line)
   304  		}
   305  		filename := parts[1][1 : len(parts[1])-1]
   306  		return newChecksumFromType(parts[0], parts[3], filename)
   307  	case 2:
   308  		// GNU-style:
   309  		//  <checksum>  file1
   310  		//  <checksum> *file2
   311  		return newChecksumFromValue(parts[0], parts[1])
   312  	case 0:
   313  		return nil, nil // empty line
   314  	default:
   315  		return newChecksumFromValue(parts[0], "")
   316  	}
   317  }