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  }