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