1package getter
2
3import (
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/hashicorp/go-getter/helper/url"
20)
21
22// FileChecksum helps verifying the checksum for a file.
23type 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
31type ChecksumError struct {
32	Hash     hash.Hash
33	Actual   []byte
34	Expected []byte
35	File     string
36}
37
38func (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.
53func (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
96func (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
124func 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
136func 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
160func 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
193func (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			break
263		}
264		checksum, err := parseChecksumLine(line)
265		if err != nil || checksum == nil {
266			continue
267		}
268		if checksum.Filename == "" {
269			// filename not sure, let's try
270			return checksum, nil
271		}
272		// make sure the checksum is for the right file
273		for _, option := range options {
274			if option != "" && checksum.Filename == option {
275				// any checksum will work so we return the first one
276				return checksum, nil
277			}
278		}
279	}
280	return nil, fmt.Errorf("no checksum found in: %s", checksumFile)
281}
282
283// parseChecksumLine takes a line from a checksum file and returns
284// checksumType, checksumValue and filename parseChecksumLine guesses the style
285// of the checksum BSD vs GNU by splitting the line and by counting the parts.
286// of a line.
287// for BSD type sums parseChecksumLine guesses the hashing algorithm
288// by checking the length of the checksum.
289func parseChecksumLine(line string) (*FileChecksum, error) {
290	parts := strings.Fields(line)
291
292	switch len(parts) {
293	case 4:
294		// BSD-style checksum:
295		//  MD5 (file1) = <checksum>
296		//  MD5 (file2) = <checksum>
297		if len(parts[1]) <= 2 ||
298			parts[1][0] != '(' || parts[1][len(parts[1])-1] != ')' {
299			return nil, fmt.Errorf(
300				"Unexpected BSD-style-checksum filename format: %s", line)
301		}
302		filename := parts[1][1 : len(parts[1])-1]
303		return newChecksumFromType(parts[0], parts[3], filename)
304	case 2:
305		// GNU-style:
306		//  <checksum>  file1
307		//  <checksum> *file2
308		return newChecksumFromValue(parts[0], parts[1])
309	case 0:
310		return nil, nil // empty line
311	default:
312		return newChecksumFromValue(parts[0], "")
313	}
314}
315