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 }