github.com/ethereum/go-ethereum@v1.16.1/internal/download/download.go (about) 1 // Copyright 2019 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 // Package download implements checksum-verified file downloads. 18 package download 19 20 import ( 21 "bufio" 22 "bytes" 23 "crypto/sha256" 24 "encoding/hex" 25 "fmt" 26 "io" 27 "iter" 28 "net/http" 29 "net/url" 30 "os" 31 "path/filepath" 32 "strings" 33 ) 34 35 // ChecksumDB keeps file checksums and tool versions. 36 type ChecksumDB struct { 37 hashes []hashEntry 38 versions []versionEntry 39 } 40 41 type versionEntry struct { 42 name string 43 version string 44 } 45 46 type hashEntry struct { 47 hash string 48 file string 49 url *url.URL 50 } 51 52 // MustLoadChecksums loads a file containing checksums. 53 func MustLoadChecksums(file string) *ChecksumDB { 54 content, err := os.ReadFile(file) 55 if err != nil { 56 panic("can't load checksum file: " + err.Error()) 57 } 58 db, err := ParseChecksums(content) 59 if err != nil { 60 panic(fmt.Sprintf("invalid checksums in %s: %v", file, err)) 61 } 62 return db 63 } 64 65 // ParseChecksums parses a checksum database. 66 func ParseChecksums(input []byte) (*ChecksumDB, error) { 67 var ( 68 csdb = new(ChecksumDB) 69 rd = bytes.NewBuffer(input) 70 lastURL *url.URL 71 ) 72 for lineNum := 1; ; lineNum++ { 73 line, err := rd.ReadString('\n') 74 if err == io.EOF { 75 break 76 } 77 line = strings.TrimSpace(line) 78 switch { 79 case line == "": 80 // Blank lines are allowed, and they reset the current urlEntry. 81 lastURL = nil 82 83 case strings.HasPrefix(line, "#"): 84 // It's a comment. Some comments have special meaning. 85 content := strings.TrimLeft(line, "# ") 86 switch { 87 case strings.HasPrefix(content, "version:"): 88 // Version comments define the version of a tool. 89 v := strings.Split(content, ":")[1] 90 parts := strings.Split(v, " ") 91 if len(parts) != 2 { 92 return nil, fmt.Errorf("line %d: invalid version string: %q", lineNum, v) 93 } 94 csdb.versions = append(csdb.versions, versionEntry{parts[0], parts[1]}) 95 96 case strings.HasPrefix(content, "https://") || strings.HasPrefix(content, "http://"): 97 // URL comments define the URL where the following files are found. Here 98 // we keep track of the last found urlEntry and attach it to each file later. 99 u, err := url.Parse(content) 100 if err != nil { 101 return nil, fmt.Errorf("line %d: invalid URL: %v", lineNum, err) 102 } 103 lastURL = u 104 } 105 106 default: 107 // It's a file hash entry. 108 fields := strings.Fields(line) 109 if len(fields) != 2 { 110 return nil, fmt.Errorf("line %d: invalid number of space-separated fields (%d)", lineNum, len(fields)) 111 } 112 csdb.hashes = append(csdb.hashes, hashEntry{fields[0], fields[1], lastURL}) 113 } 114 } 115 return csdb, nil 116 } 117 118 // Files returns an iterator over all file names. 119 func (db *ChecksumDB) Files() iter.Seq[string] { 120 return func(yield func(string) bool) { 121 for _, e := range db.hashes { 122 if !yield(e.file) { 123 return 124 } 125 } 126 } 127 } 128 129 // DownloadAndVerifyAll downloads all files and checks that they match the checksum given in 130 // the database. This task can be used to sanity-check new checksums. 131 func (db *ChecksumDB) DownloadAndVerifyAll() { 132 var tmp = os.TempDir() 133 for _, e := range db.hashes { 134 if e.url == nil { 135 fmt.Printf("Skipping verification of %s: no URL defined in checksum database", e.file) 136 continue 137 } 138 url := e.url.JoinPath(e.file).String() 139 dst := filepath.Join(tmp, e.file) 140 if err := db.DownloadFile(url, dst); err != nil { 141 fmt.Println("error:", err) 142 } 143 } 144 } 145 146 // verifyHash checks that the file at 'path' has the expected hash. 147 func verifyHash(path, expectedHash string) error { 148 fd, err := os.Open(path) 149 if err != nil { 150 return err 151 } 152 defer fd.Close() 153 154 h := sha256.New() 155 if _, err := io.Copy(h, bufio.NewReader(fd)); err != nil { 156 return err 157 } 158 fileHash := hex.EncodeToString(h.Sum(nil)) 159 if fileHash != expectedHash { 160 return fmt.Errorf("invalid file hash: %s %s", fileHash, filepath.Base(path)) 161 } 162 return nil 163 } 164 165 // DownloadFileFromKnownURL downloads a file from the URL defined in the checksum database. 166 func (db *ChecksumDB) DownloadFileFromKnownURL(dstPath string) error { 167 base := filepath.Base(dstPath) 168 url, err := db.FindURL(base) 169 if err != nil { 170 return err 171 } 172 return db.DownloadFile(url, dstPath) 173 } 174 175 // DownloadFile downloads a file and verifies its checksum. 176 func (db *ChecksumDB) DownloadFile(url, dstPath string) error { 177 basename := filepath.Base(dstPath) 178 hash := db.findHash(basename) 179 if hash == "" { 180 return fmt.Errorf("no known hash for file %q", basename) 181 } 182 // Shortcut if already downloaded. 183 if verifyHash(dstPath, hash) == nil { 184 fmt.Printf("%s is up-to-date\n", dstPath) 185 return nil 186 } 187 188 fmt.Printf("%s is stale\n", dstPath) 189 fmt.Printf("downloading from %s\n", url) 190 resp, err := http.Get(url) 191 if err != nil { 192 return fmt.Errorf("download error: %v", err) 193 } 194 defer resp.Body.Close() 195 if resp.StatusCode != http.StatusOK { 196 return fmt.Errorf("download error: status %d", resp.StatusCode) 197 } 198 if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil { 199 return err 200 } 201 202 // Download to a temporary file. 203 tmpfile := dstPath + ".tmp" 204 fd, err := os.OpenFile(tmpfile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 205 if err != nil { 206 return err 207 } 208 dst := newDownloadWriter(fd, resp.ContentLength) 209 _, err = io.Copy(dst, resp.Body) 210 dst.Close() 211 if err != nil { 212 os.Remove(tmpfile) 213 return err 214 } 215 if err := verifyHash(tmpfile, hash); err != nil { 216 os.Remove(tmpfile) 217 return err 218 } 219 // It's valid, rename to dstPath to complete the download. 220 return os.Rename(tmpfile, dstPath) 221 } 222 223 // findHash returns the known hash of a file. 224 func (db *ChecksumDB) findHash(basename string) string { 225 for _, e := range db.hashes { 226 if e.file == basename { 227 return e.hash 228 } 229 } 230 return "" 231 } 232 233 // FindVersion returns the current known version of a tool, if it is defined in the file. 234 func (db *ChecksumDB) FindVersion(tool string) (string, error) { 235 for _, e := range db.versions { 236 if e.name == tool { 237 return e.version, nil 238 } 239 } 240 return "", fmt.Errorf("tool version %q not defined in checksum database", tool) 241 } 242 243 // FindURL gets the URL for a file. 244 func (db *ChecksumDB) FindURL(basename string) (string, error) { 245 for _, e := range db.hashes { 246 if e.file == basename { 247 if e.url == nil { 248 return "", fmt.Errorf("file %q has no URL defined", e.file) 249 } 250 return e.url.JoinPath(e.file).String(), nil 251 } 252 } 253 return "", fmt.Errorf("file %q does not exist in checksum database", basename) 254 } 255 256 type downloadWriter struct { 257 file *os.File 258 dstBuf *bufio.Writer 259 size int64 260 written int64 261 lastpct int64 262 } 263 264 func newDownloadWriter(dst *os.File, size int64) *downloadWriter { 265 return &downloadWriter{ 266 file: dst, 267 dstBuf: bufio.NewWriter(dst), 268 size: size, 269 } 270 } 271 272 func (w *downloadWriter) Write(buf []byte) (int, error) { 273 n, err := w.dstBuf.Write(buf) 274 275 // Report progress. 276 w.written += int64(n) 277 pct := w.written * 10 / w.size * 10 278 if pct != w.lastpct { 279 if w.lastpct != 0 { 280 fmt.Print("...") 281 } 282 fmt.Print(pct, "%") 283 w.lastpct = pct 284 } 285 return n, err 286 } 287 288 func (w *downloadWriter) Close() error { 289 if w.lastpct > 0 { 290 fmt.Println() // Finish the progress line. 291 } 292 flushErr := w.dstBuf.Flush() 293 closeErr := w.file.Close() 294 if flushErr != nil { 295 return flushErr 296 } 297 return closeErr 298 }