...

Source file src/golang.org/x/mod/sumdb/client_test.go

Documentation: golang.org/x/mod/sumdb

     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  package sumdb
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"strings"
    11  	"sync"
    12  	"testing"
    13  
    14  	"golang.org/x/mod/sumdb/note"
    15  	"golang.org/x/mod/sumdb/tlog"
    16  )
    17  
    18  const (
    19  	testName        = "localhost.localdev/sumdb"
    20  	testVerifierKey = "localhost.localdev/sumdb+00000c67+AcTrnkbUA+TU4heY3hkjiSES/DSQniBqIeQ/YppAUtK6"
    21  	testSignerKey   = "PRIVATE+KEY+localhost.localdev/sumdb+00000c67+AXu6+oaVaOYuQOFrf1V59JK1owcFlJcHwwXHDfDGxSPk"
    22  )
    23  
    24  func TestClientLookup(t *testing.T) {
    25  	tc := newTestClient(t)
    26  	tc.mustHaveLatest(1)
    27  
    28  	// Basic lookup.
    29  	tc.mustLookup("rsc.io/sampler", "v1.3.0", "rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=")
    30  	tc.mustHaveLatest(3)
    31  
    32  	// Everything should now be cached, both for the original package and its /go.mod.
    33  	tc.getOK = false
    34  	tc.mustLookup("rsc.io/sampler", "v1.3.0", "rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=")
    35  	tc.mustLookup("rsc.io/sampler", "v1.3.0/go.mod", "rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=")
    36  	tc.mustHaveLatest(3)
    37  	tc.getOK = true
    38  	tc.getTileOK = false // the cache has what we need
    39  
    40  	// Lookup with multiple returned lines.
    41  	tc.mustLookup("rsc.io/quote", "v1.5.2", "rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=\nrsc.io/quote v1.5.2 h2:xyzzy")
    42  	tc.mustHaveLatest(3)
    43  
    44  	// Lookup with need for !-encoding.
    45  	// rsc.io/Quote is the only record written after rsc.io/samper,
    46  	// so it is the only one that should need more tiles.
    47  	tc.getTileOK = true
    48  	tc.mustLookup("rsc.io/Quote", "v1.5.2", "rsc.io/Quote v1.5.2 h1:uppercase!=")
    49  	tc.mustHaveLatest(4)
    50  }
    51  
    52  func TestClientBadTiles(t *testing.T) {
    53  	tc := newTestClient(t)
    54  
    55  	flipBits := func() {
    56  		for url, data := range tc.remote {
    57  			if strings.Contains(url, "/tile/") {
    58  				for i := range data {
    59  					data[i] ^= 0x80
    60  				}
    61  			}
    62  		}
    63  	}
    64  
    65  	// Bad tiles in initial download.
    66  	tc.mustHaveLatest(1)
    67  	flipBits()
    68  	_, err := tc.client.Lookup("rsc.io/sampler", "v1.3.0")
    69  	tc.mustError(err, "rsc.io/sampler@v1.3.0: initializing sumdb.Client: checking tree#1: downloaded inconsistent tile")
    70  	flipBits()
    71  	tc.newClient()
    72  	tc.mustLookup("rsc.io/sampler", "v1.3.0", "rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=")
    73  
    74  	// Bad tiles after initial download.
    75  	flipBits()
    76  	_, err = tc.client.Lookup("rsc.io/Quote", "v1.5.2")
    77  	tc.mustError(err, "rsc.io/Quote@v1.5.2: checking tree#3 against tree#4: downloaded inconsistent tile")
    78  	flipBits()
    79  	tc.newClient()
    80  	tc.mustLookup("rsc.io/Quote", "v1.5.2", "rsc.io/Quote v1.5.2 h1:uppercase!=")
    81  
    82  	// Bad starting tree hash looks like bad tiles.
    83  	tc.newClient()
    84  	text := tlog.FormatTree(tlog.Tree{N: 1, Hash: tlog.Hash{}})
    85  	data, err := note.Sign(&note.Note{Text: string(text)}, tc.signer)
    86  	if err != nil {
    87  		tc.t.Fatal(err)
    88  	}
    89  	tc.config[testName+"/latest"] = data
    90  	_, err = tc.client.Lookup("rsc.io/sampler", "v1.3.0")
    91  	tc.mustError(err, "rsc.io/sampler@v1.3.0: initializing sumdb.Client: checking tree#1: downloaded inconsistent tile")
    92  }
    93  
    94  func TestClientFork(t *testing.T) {
    95  	tc := newTestClient(t)
    96  	tc2 := tc.fork()
    97  
    98  	tc.addRecord("rsc.io/pkg1@v1.5.2", `rsc.io/pkg1 v1.5.2 h1:hash!=
    99  `)
   100  	tc.addRecord("rsc.io/pkg1@v1.5.4", `rsc.io/pkg1 v1.5.4 h1:hash!=
   101  `)
   102  	tc.mustLookup("rsc.io/pkg1", "v1.5.2", "rsc.io/pkg1 v1.5.2 h1:hash!=")
   103  
   104  	tc2.addRecord("rsc.io/pkg1@v1.5.3", `rsc.io/pkg1 v1.5.3 h1:hash!=
   105  `)
   106  	tc2.addRecord("rsc.io/pkg1@v1.5.4", `rsc.io/pkg1 v1.5.4 h1:hash!=
   107  `)
   108  	tc2.mustLookup("rsc.io/pkg1", "v1.5.4", "rsc.io/pkg1 v1.5.4 h1:hash!=")
   109  
   110  	key := "/lookup/rsc.io/pkg1@v1.5.2"
   111  	tc2.remote[key] = tc.remote[key]
   112  	_, err := tc2.client.Lookup("rsc.io/pkg1", "v1.5.2")
   113  	tc2.mustError(err, ErrSecurity.Error())
   114  
   115  	/*
   116  	   SECURITY ERROR
   117  	   go.sum database server misbehavior detected!
   118  
   119  	   old database:
   120  	   	go.sum database tree!
   121  	   	5
   122  	   	nWzN20+pwMt62p7jbv1/NlN95ePTlHijabv5zO/s36w=
   123  
   124  	   	— localhost.localdev/sumdb AAAMZ5/2FVAdMH58kmnz/0h299pwyskEbzDzoa2/YaPdhvLya4YWDFQQxu2TQb5GpwAH4NdWnTwuhILafisyf3CNbgg=
   125  
   126  	   new database:
   127  	   	go.sum database tree
   128  	   	6
   129  	   	wc4SkQt52o5W2nQ8To2ARs+mWuUJjss+sdleoiqxMmM=
   130  
   131  	   	— localhost.localdev/sumdb AAAMZ6oRNswlEZ6ZZhxrCvgl1MBy+nusq4JU+TG6Fe2NihWLqOzb+y2c2kzRLoCr4tvw9o36ucQEnhc20e4nA4Qc/wc=
   132  
   133  	   proof of misbehavior:
   134  	   	T7i+H/8ER4nXOiw4Bj0koZOkGjkxoNvlI34GpvhHhQg=
   135  	   	Nsuejv72de9hYNM5bqFv8rv3gm3zJQwv/DT/WNbLDLA=
   136  	   	mOmqqZ1aI/lzS94oq/JSbj7pD8Rv9S+xDyi12BtVSHo=
   137  	   	/7Aw5jVSMM9sFjQhaMg+iiDYPMk6decH7QLOGrL9Lx0=
   138  	*/
   139  
   140  	wants := []string{
   141  		"SECURITY ERROR",
   142  		"go.sum database server misbehavior detected!",
   143  		"old database:\n\tgo.sum database tree\n\t5\n",
   144  		"— localhost.localdev/sumdb AAAMZ5/2FVAd",
   145  		"new database:\n\tgo.sum database tree\n\t6\n",
   146  		"— localhost.localdev/sumdb AAAMZ6oRNswl",
   147  		"proof of misbehavior:\n\tT7i+H/8ER4nXOiw4Bj0k",
   148  	}
   149  	text := tc2.security.String()
   150  	for _, want := range wants {
   151  		if !strings.Contains(text, want) {
   152  			t.Fatalf("cannot find %q in security text:\n%s", want, text)
   153  		}
   154  	}
   155  }
   156  
   157  func TestClientGONOSUMDB(t *testing.T) {
   158  	tc := newTestClient(t)
   159  	tc.client.SetGONOSUMDB("p,*/q")
   160  	tc.client.Lookup("rsc.io/sampler", "v1.3.0") // initialize before we turn off network
   161  	tc.getOK = false
   162  
   163  	ok := []string{
   164  		"abc",
   165  		"a/p",
   166  		"pq",
   167  		"q",
   168  		"n/o/p/q",
   169  	}
   170  	skip := []string{
   171  		"p",
   172  		"p/x",
   173  		"x/q",
   174  		"x/q/z",
   175  	}
   176  
   177  	for _, path := range ok {
   178  		_, err := tc.client.Lookup(path, "v1.0.0")
   179  		if err == ErrGONOSUMDB {
   180  			t.Errorf("Lookup(%q): ErrGONOSUMDB, wanted failed actual lookup", path)
   181  		}
   182  	}
   183  	for _, path := range skip {
   184  		_, err := tc.client.Lookup(path, "v1.0.0")
   185  		if err != ErrGONOSUMDB {
   186  			t.Errorf("Lookup(%q): %v, wanted ErrGONOSUMDB", path, err)
   187  		}
   188  	}
   189  }
   190  
   191  // A testClient is a self-contained client-side testing environment.
   192  type testClient struct {
   193  	t          *testing.T // active test
   194  	client     *Client    // client being tested
   195  	tileHeight int        // tile height to use (default 2)
   196  	getOK      bool       // should tc.GetURL succeed?
   197  	getTileOK  bool       // should tc.GetURL of tiles succeed?
   198  	treeSize   int64
   199  	hashes     []tlog.Hash
   200  	remote     map[string][]byte
   201  	signer     note.Signer
   202  
   203  	// mu protects config, cache, log, security
   204  	// during concurrent use of the exported methods
   205  	// by the client itself (testClient is the Client's ClientOps,
   206  	// and the Client methods can both read and write these fields).
   207  	// Unexported methods invoked directly by the test
   208  	// (for example, addRecord) need not hold the mutex:
   209  	// for proper test execution those methods should only
   210  	// be called when the Client is idle and not using its ClientOps.
   211  	// Not holding the mutex in those methods ensures
   212  	// that if a mistake is made, go test -race will report it.
   213  	// (Holding the mutex would eliminate the race report but
   214  	// not the underlying problem.)
   215  	// Similarly, the get map is not protected by the mutex,
   216  	// because the Client methods only read it.
   217  	mu       sync.Mutex // prot
   218  	config   map[string][]byte
   219  	cache    map[string][]byte
   220  	security bytes.Buffer
   221  }
   222  
   223  // newTestClient returns a new testClient that will call t.Fatal on error
   224  // and has a few records already available on the remote server.
   225  func newTestClient(t *testing.T) *testClient {
   226  	tc := &testClient{
   227  		t:          t,
   228  		tileHeight: 2,
   229  		getOK:      true,
   230  		getTileOK:  true,
   231  		config:     make(map[string][]byte),
   232  		cache:      make(map[string][]byte),
   233  		remote:     make(map[string][]byte),
   234  	}
   235  
   236  	tc.config["key"] = []byte(testVerifierKey + "\n")
   237  	var err error
   238  	tc.signer, err = note.NewSigner(testSignerKey)
   239  	if err != nil {
   240  		t.Fatal(err)
   241  	}
   242  
   243  	tc.newClient()
   244  
   245  	tc.addRecord("rsc.io/quote@v1.5.2", `rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
   246  rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
   247  rsc.io/quote v1.5.2 h2:xyzzy
   248  `)
   249  
   250  	tc.addRecord("golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c", `golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
   251  golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
   252  `)
   253  	tc.addRecord("rsc.io/sampler@v1.3.0", `rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
   254  rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
   255  `)
   256  	tc.config[testName+"/latest"] = tc.signTree(1)
   257  
   258  	tc.addRecord("rsc.io/!quote@v1.5.2", `rsc.io/Quote v1.5.2 h1:uppercase!=
   259  `)
   260  	return tc
   261  }
   262  
   263  // newClient resets the Client associated with tc.
   264  // This clears any in-memory cache from the Client
   265  // but not tc's on-disk cache.
   266  func (tc *testClient) newClient() {
   267  	tc.client = NewClient(tc)
   268  	tc.client.SetTileHeight(tc.tileHeight)
   269  }
   270  
   271  // mustLookup does a lookup for path@vers and checks that the lines that come back match want.
   272  func (tc *testClient) mustLookup(path, vers, want string) {
   273  	tc.t.Helper()
   274  	lines, err := tc.client.Lookup(path, vers)
   275  	if err != nil {
   276  		tc.t.Fatal(err)
   277  	}
   278  	if strings.Join(lines, "\n") != want {
   279  		tc.t.Fatalf("Lookup(%q, %q):\n\t%s\nwant:\n\t%s", path, vers, strings.Join(lines, "\n\t"), strings.Replace(want, "\n", "\n\t", -1))
   280  	}
   281  }
   282  
   283  // mustHaveLatest checks that the on-disk configuration
   284  // for latest is a tree of size n.
   285  func (tc *testClient) mustHaveLatest(n int64) {
   286  	tc.t.Helper()
   287  
   288  	latest := tc.config[testName+"/latest"]
   289  	lines := strings.Split(string(latest), "\n")
   290  	if len(lines) < 2 || lines[1] != fmt.Sprint(n) {
   291  		tc.t.Fatalf("/latest should have tree %d, but has:\n%s", n, latest)
   292  	}
   293  }
   294  
   295  // mustError checks that err's error string contains the text.
   296  func (tc *testClient) mustError(err error, text string) {
   297  	tc.t.Helper()
   298  	if err == nil || !strings.Contains(err.Error(), text) {
   299  		tc.t.Fatalf("err = %v, want %q", err, text)
   300  	}
   301  }
   302  
   303  // fork returns a copy of tc.
   304  // Changes made to the new copy or to tc are not reflected in the other.
   305  func (tc *testClient) fork() *testClient {
   306  	tc2 := &testClient{
   307  		t:          tc.t,
   308  		getOK:      tc.getOK,
   309  		getTileOK:  tc.getTileOK,
   310  		tileHeight: tc.tileHeight,
   311  		treeSize:   tc.treeSize,
   312  		hashes:     append([]tlog.Hash{}, tc.hashes...),
   313  		signer:     tc.signer,
   314  		config:     copyMap(tc.config),
   315  		cache:      copyMap(tc.cache),
   316  		remote:     copyMap(tc.remote),
   317  	}
   318  	tc2.newClient()
   319  	return tc2
   320  }
   321  
   322  func copyMap(m map[string][]byte) map[string][]byte {
   323  	m2 := make(map[string][]byte)
   324  	for k, v := range m {
   325  		m2[k] = v
   326  	}
   327  	return m2
   328  }
   329  
   330  // ReadHashes is tc's implementation of tlog.HashReader, for use with
   331  // tlog.TreeHash and so on.
   332  func (tc *testClient) ReadHashes(indexes []int64) ([]tlog.Hash, error) {
   333  	var list []tlog.Hash
   334  	for _, id := range indexes {
   335  		list = append(list, tc.hashes[id])
   336  	}
   337  	return list, nil
   338  }
   339  
   340  // addRecord adds a log record using the given (!-encoded) key and data.
   341  func (tc *testClient) addRecord(key, data string) {
   342  	tc.t.Helper()
   343  
   344  	// Create record, add hashes to log tree.
   345  	id := tc.treeSize
   346  	tc.treeSize++
   347  	rec, err := tlog.FormatRecord(id, []byte(data))
   348  	if err != nil {
   349  		tc.t.Fatal(err)
   350  	}
   351  	hashes, err := tlog.StoredHashesForRecordHash(id, tlog.RecordHash([]byte(data)), tc)
   352  	if err != nil {
   353  		tc.t.Fatal(err)
   354  	}
   355  	tc.hashes = append(tc.hashes, hashes...)
   356  
   357  	// Create lookup result.
   358  	tc.remote["/lookup/"+key] = append(rec, tc.signTree(tc.treeSize)...)
   359  
   360  	// Create new tiles.
   361  	tiles := tlog.NewTiles(tc.tileHeight, id, tc.treeSize)
   362  	for _, tile := range tiles {
   363  		data, err := tlog.ReadTileData(tile, tc)
   364  		if err != nil {
   365  			tc.t.Fatal(err)
   366  		}
   367  		tc.remote["/"+tile.Path()] = data
   368  		// TODO delete old partial tiles
   369  	}
   370  }
   371  
   372  // signTree returns the signed head for the tree of the given size.
   373  func (tc *testClient) signTree(size int64) []byte {
   374  	h, err := tlog.TreeHash(size, tc)
   375  	if err != nil {
   376  		tc.t.Fatal(err)
   377  	}
   378  	text := tlog.FormatTree(tlog.Tree{N: size, Hash: h})
   379  	data, err := note.Sign(&note.Note{Text: string(text)}, tc.signer)
   380  	if err != nil {
   381  		tc.t.Fatal(err)
   382  	}
   383  	return data
   384  }
   385  
   386  // ReadRemote is for tc's implementation of Client.
   387  func (tc *testClient) ReadRemote(path string) ([]byte, error) {
   388  	// No mutex here because only the Client should be running
   389  	// and the Client cannot change tc.get.
   390  	if !tc.getOK {
   391  		return nil, fmt.Errorf("disallowed remote read %s", path)
   392  	}
   393  	if strings.Contains(path, "/tile/") && !tc.getTileOK {
   394  		return nil, fmt.Errorf("disallowed remote tile read %s", path)
   395  	}
   396  
   397  	data, ok := tc.remote[path]
   398  	if !ok {
   399  		return nil, fmt.Errorf("no remote path %s", path)
   400  	}
   401  	return data, nil
   402  }
   403  
   404  // ReadConfig is for tc's implementation of Client.
   405  func (tc *testClient) ReadConfig(file string) ([]byte, error) {
   406  	tc.mu.Lock()
   407  	defer tc.mu.Unlock()
   408  
   409  	data, ok := tc.config[file]
   410  	if !ok {
   411  		return nil, fmt.Errorf("no config %s", file)
   412  	}
   413  	return data, nil
   414  }
   415  
   416  // WriteConfig is for tc's implementation of Client.
   417  func (tc *testClient) WriteConfig(file string, old, new []byte) error {
   418  	tc.mu.Lock()
   419  	defer tc.mu.Unlock()
   420  
   421  	data := tc.config[file]
   422  	if !bytes.Equal(old, data) {
   423  		return ErrWriteConflict
   424  	}
   425  	tc.config[file] = new
   426  	return nil
   427  }
   428  
   429  // ReadCache is for tc's implementation of Client.
   430  func (tc *testClient) ReadCache(file string) ([]byte, error) {
   431  	tc.mu.Lock()
   432  	defer tc.mu.Unlock()
   433  
   434  	data, ok := tc.cache[file]
   435  	if !ok {
   436  		return nil, fmt.Errorf("no cache %s", file)
   437  	}
   438  	return data, nil
   439  }
   440  
   441  // WriteCache is for tc's implementation of Client.
   442  func (tc *testClient) WriteCache(file string, data []byte) {
   443  	tc.mu.Lock()
   444  	defer tc.mu.Unlock()
   445  
   446  	tc.cache[file] = data
   447  }
   448  
   449  // Log is for tc's implementation of Client.
   450  func (tc *testClient) Log(msg string) {
   451  	tc.t.Log(msg)
   452  }
   453  
   454  // SecurityError is for tc's implementation of Client.
   455  func (tc *testClient) SecurityError(msg string) {
   456  	tc.mu.Lock()
   457  	defer tc.mu.Unlock()
   458  
   459  	fmt.Fprintf(&tc.security, "%s\n", strings.TrimRight(msg, "\n"))
   460  }
   461  

View as plain text