Source file src/cmd/go/internal/modfetch/sumdb.go

     1  // Copyright 2019 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Go checksum database lookup
     6  
     7  //go:build !cmd_go_bootstrap
     8  
     9  package modfetch
    10  
    11  import (
    12  	"bytes"
    13  	"errors"
    14  	"fmt"
    15  	"io"
    16  	"io/fs"
    17  	"net/url"
    18  	"os"
    19  	"path/filepath"
    20  	"strings"
    21  	"sync"
    22  	"time"
    23  
    24  	"cmd/go/internal/base"
    25  	"cmd/go/internal/cfg"
    26  	"cmd/go/internal/lockedfile"
    27  	"cmd/go/internal/web"
    28  
    29  	"golang.org/x/mod/module"
    30  	"golang.org/x/mod/sumdb"
    31  	"golang.org/x/mod/sumdb/note"
    32  )
    33  
    34  // useSumDB reports whether to use the Go checksum database for the given module.
    35  func useSumDB(mod module.Version) bool {
    36  	if mod.Path == "golang.org/toolchain" {
    37  		must := true
    38  		// Toolchain downloads must be verified against the checksum database,
    39  		// even if GOSUMDB=off or GONOSUMDB matches the pattern.
    40  		// If GOSUMDB=off, then the eventual lookup will fail
    41  		// with a good error message.
    42  
    43  		// Exception #1: using GOPROXY=file:// to test a distpack.
    44  		if strings.HasPrefix(cfg.GOPROXY, "file://") && !strings.ContainsAny(cfg.GOPROXY, ",|") {
    45  			must = false
    46  		}
    47  		// Exception #2: the Go proxy+checksum database cannot check itself
    48  		// while doing the initial download.
    49  		if strings.Contains(os.Getenv("GIT_HTTP_USER_AGENT"), "proxy.golang.org") {
    50  			must = false
    51  		}
    52  
    53  		// Another potential exception would be GOPROXY=direct,
    54  		// but that would make toolchain downloads only as secure
    55  		// as HTTPS, and in particular they'd be susceptible to MITM
    56  		// attacks on systems with less-than-trustworthy root certificates.
    57  		// The checksum database provides a stronger guarantee,
    58  		// so we don't make that exception.
    59  
    60  		// Otherwise, require the checksum database.
    61  		if must {
    62  			return true
    63  		}
    64  	}
    65  	return cfg.GOSUMDB != "off" && !module.MatchPrefixPatterns(cfg.GONOSUMDB, mod.Path)
    66  }
    67  
    68  // lookupSumDB returns the Go checksum database's go.sum lines for the given module,
    69  // along with the name of the database.
    70  func lookupSumDB(mod module.Version) (dbname string, lines []string, err error) {
    71  	dbOnce.Do(func() {
    72  		dbName, db, dbErr = dbDial()
    73  	})
    74  	if dbErr != nil {
    75  		return "", nil, dbErr
    76  	}
    77  	lines, err = db.Lookup(mod.Path, mod.Version)
    78  	return dbName, lines, err
    79  }
    80  
    81  var (
    82  	dbOnce sync.Once
    83  	dbName string
    84  	db     *sumdb.Client
    85  	dbErr  error
    86  )
    87  
    88  func dbDial() (dbName string, db *sumdb.Client, err error) {
    89  	// $GOSUMDB can be "key" or "key url",
    90  	// and the key can be a full verifier key
    91  	// or a host on our list of known keys.
    92  
    93  	// Special case: sum.golang.google.cn
    94  	// is an alias, reachable inside mainland China,
    95  	// for sum.golang.org. If there are more
    96  	// of these we should add a map like knownGOSUMDB.
    97  	gosumdb := cfg.GOSUMDB
    98  	if gosumdb == "sum.golang.google.cn" {
    99  		gosumdb = "sum.golang.org https://sum.golang.google.cn"
   100  	}
   101  
   102  	if gosumdb == "off" {
   103  		return "", nil, fmt.Errorf("checksum database disabled by GOSUMDB=off")
   104  	}
   105  
   106  	key := strings.Fields(gosumdb)
   107  	if len(key) >= 1 {
   108  		if k := knownGOSUMDB[key[0]]; k != "" {
   109  			key[0] = k
   110  		}
   111  	}
   112  	if len(key) == 0 {
   113  		return "", nil, fmt.Errorf("missing GOSUMDB")
   114  	}
   115  	if len(key) > 2 {
   116  		return "", nil, fmt.Errorf("invalid GOSUMDB: too many fields")
   117  	}
   118  	vkey, err := note.NewVerifier(key[0])
   119  	if err != nil {
   120  		return "", nil, fmt.Errorf("invalid GOSUMDB: %v", err)
   121  	}
   122  	name := vkey.Name()
   123  
   124  	// No funny business in the database name.
   125  	direct, err := url.Parse("https://" + name)
   126  	if err != nil || strings.HasSuffix(name, "/") || *direct != (url.URL{Scheme: "https", Host: direct.Host, Path: direct.Path, RawPath: direct.RawPath}) || direct.RawPath != "" || direct.Host == "" {
   127  		return "", nil, fmt.Errorf("invalid sumdb name (must be host[/path]): %s %+v", name, *direct)
   128  	}
   129  
   130  	// Determine how to get to database.
   131  	var base *url.URL
   132  	if len(key) >= 2 {
   133  		// Use explicit alternate URL listed in $GOSUMDB,
   134  		// bypassing both the default URL derivation and any proxies.
   135  		u, err := url.Parse(key[1])
   136  		if err != nil {
   137  			return "", nil, fmt.Errorf("invalid GOSUMDB URL: %v", err)
   138  		}
   139  		base = u
   140  	}
   141  
   142  	return name, sumdb.NewClient(&dbClient{key: key[0], name: name, direct: direct, base: base}), nil
   143  }
   144  
   145  type dbClient struct {
   146  	key    string
   147  	name   string
   148  	direct *url.URL
   149  
   150  	once    sync.Once
   151  	base    *url.URL
   152  	baseErr error
   153  }
   154  
   155  func (c *dbClient) ReadRemote(path string) ([]byte, error) {
   156  	c.once.Do(c.initBase)
   157  	if c.baseErr != nil {
   158  		return nil, c.baseErr
   159  	}
   160  
   161  	var data []byte
   162  	start := time.Now()
   163  	targ := web.Join(c.base, path)
   164  	data, err := web.GetBytes(targ)
   165  	if false {
   166  		fmt.Fprintf(os.Stderr, "%.3fs %s\n", time.Since(start).Seconds(), targ.Redacted())
   167  	}
   168  	return data, err
   169  }
   170  
   171  // initBase determines the base URL for connecting to the database.
   172  // Determining the URL requires sending network traffic to proxies,
   173  // so this work is delayed until we need to download something from
   174  // the database. If everything we need is in the local cache and
   175  // c.ReadRemote is never called, we will never do this work.
   176  func (c *dbClient) initBase() {
   177  	if c.base != nil {
   178  		return
   179  	}
   180  
   181  	// Try proxies in turn until we find out how to connect to this database.
   182  	//
   183  	// Before accessing any checksum database URL using a proxy, the proxy
   184  	// client should first fetch <proxyURL>/sumdb/<sumdb-name>/supported.
   185  	//
   186  	// If that request returns a successful (HTTP 200) response, then the proxy
   187  	// supports proxying checksum database requests. In that case, the client
   188  	// should use the proxied access method only, never falling back to a direct
   189  	// connection to the database.
   190  	//
   191  	// If the /sumdb/<sumdb-name>/supported check fails with a “not found” (HTTP
   192  	// 404) or “gone” (HTTP 410) response, or if the proxy is configured to fall
   193  	// back on errors, the client will try the next proxy. If there are no
   194  	// proxies left or if the proxy is "direct" or "off", the client should
   195  	// connect directly to that database.
   196  	//
   197  	// Any other response is treated as the database being unavailable.
   198  	//
   199  	// See https://golang.org/design/25530-sumdb#proxying-a-checksum-database.
   200  	err := TryProxies(func(proxy string) error {
   201  		switch proxy {
   202  		case "noproxy":
   203  			return errUseProxy
   204  		case "direct", "off":
   205  			return errProxyOff
   206  		default:
   207  			proxyURL, err := url.Parse(proxy)
   208  			if err != nil {
   209  				return err
   210  			}
   211  			if _, err := web.GetBytes(web.Join(proxyURL, "sumdb/"+c.name+"/supported")); err != nil {
   212  				return err
   213  			}
   214  			// Success! This proxy will help us.
   215  			c.base = web.Join(proxyURL, "sumdb/"+c.name)
   216  			return nil
   217  		}
   218  	})
   219  	if errors.Is(err, fs.ErrNotExist) {
   220  		// No proxies, or all proxies failed (with 404, 410, or were allowed
   221  		// to fall back), or we reached an explicit "direct" or "off".
   222  		c.base = c.direct
   223  	} else if err != nil {
   224  		c.baseErr = err
   225  	}
   226  }
   227  
   228  // ReadConfig reads the key from c.key
   229  // and otherwise reads the config (a latest tree head) from GOPATH/pkg/sumdb/<file>.
   230  func (c *dbClient) ReadConfig(file string) (data []byte, err error) {
   231  	if file == "key" {
   232  		return []byte(c.key), nil
   233  	}
   234  
   235  	if cfg.SumdbDir == "" {
   236  		return nil, fmt.Errorf("could not locate sumdb file: missing $GOPATH: %s",
   237  			cfg.GoPathError)
   238  	}
   239  	targ := filepath.Join(cfg.SumdbDir, file)
   240  	data, err = lockedfile.Read(targ)
   241  	if errors.Is(err, fs.ErrNotExist) {
   242  		// Treat non-existent as empty, to bootstrap the "latest" file
   243  		// the first time we connect to a given database.
   244  		return []byte{}, nil
   245  	}
   246  	return data, err
   247  }
   248  
   249  // WriteConfig rewrites the latest tree head.
   250  func (*dbClient) WriteConfig(file string, old, new []byte) error {
   251  	if file == "key" {
   252  		// Should not happen.
   253  		return fmt.Errorf("cannot write key")
   254  	}
   255  	if cfg.SumdbDir == "" {
   256  		return fmt.Errorf("could not locate sumdb file: missing $GOPATH: %s",
   257  			cfg.GoPathError)
   258  	}
   259  	targ := filepath.Join(cfg.SumdbDir, file)
   260  	os.MkdirAll(filepath.Dir(targ), 0777)
   261  	f, err := lockedfile.Edit(targ)
   262  	if err != nil {
   263  		return err
   264  	}
   265  	defer f.Close()
   266  	data, err := io.ReadAll(f)
   267  	if err != nil {
   268  		return err
   269  	}
   270  	if len(data) > 0 && !bytes.Equal(data, old) {
   271  		return sumdb.ErrWriteConflict
   272  	}
   273  	if _, err := f.Seek(0, 0); err != nil {
   274  		return err
   275  	}
   276  	if err := f.Truncate(0); err != nil {
   277  		return err
   278  	}
   279  	if _, err := f.Write(new); err != nil {
   280  		return err
   281  	}
   282  	return f.Close()
   283  }
   284  
   285  // ReadCache reads cached lookups or tiles from
   286  // GOPATH/pkg/mod/cache/download/sumdb,
   287  // which will be deleted by "go clean -modcache".
   288  func (*dbClient) ReadCache(file string) ([]byte, error) {
   289  	targ := filepath.Join(cfg.GOMODCACHE, "cache/download/sumdb", file)
   290  	data, err := lockedfile.Read(targ)
   291  	// lockedfile.Write does not atomically create the file with contents.
   292  	// There is a moment between file creation and locking the file for writing,
   293  	// during which the empty file can be locked for reading.
   294  	// Treat observing an empty file as file not found.
   295  	if err == nil && len(data) == 0 {
   296  		err = &fs.PathError{Op: "read", Path: targ, Err: fs.ErrNotExist}
   297  	}
   298  	return data, err
   299  }
   300  
   301  // WriteCache updates cached lookups or tiles.
   302  func (*dbClient) WriteCache(file string, data []byte) {
   303  	targ := filepath.Join(cfg.GOMODCACHE, "cache/download/sumdb", file)
   304  	os.MkdirAll(filepath.Dir(targ), 0777)
   305  	lockedfile.Write(targ, bytes.NewReader(data), 0666)
   306  }
   307  
   308  func (*dbClient) Log(msg string) {
   309  	// nothing for now
   310  }
   311  
   312  func (*dbClient) SecurityError(msg string) {
   313  	base.Fatalf("%s", msg)
   314  }
   315  

View as plain text