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 }