2026-10-10 09:57:43 +02:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"golang.org/x/mod/sumdb"
|
|
|
|
|
"golang.org/x/mod/sumdb/note"
|
2026-10-10 10:19:39 +02:00
|
|
|
"golang.org/x/mod/sumdb/tlog"
|
2026-10-10 09:57:43 +02:00
|
|
|
)
|
|
|
|
|
|
2026-10-10 11:31:01 +02:00
|
|
|
const (
|
|
|
|
|
testSumDBName = "sum.example"
|
|
|
|
|
// testSumDBTree looks like a signed tree as far as the proxy checks.
|
|
|
|
|
testSumDBTree = "go.sum database tree\n1\nhash\n\n— sum.example signature\n"
|
|
|
|
|
// testSumDBRecord looks like a lookup answer as far as the proxy checks.
|
|
|
|
|
testSumDBRecord = "1\ngolang.org/x/text v0.14.0 h1:hash=\ngolang.org/x/text v0.14.0/go.mod h1:hash=\n\n" + testSumDBTree
|
|
|
|
|
)
|
2026-10-10 09:57:43 +02:00
|
|
|
|
2026-10-10 10:19:39 +02:00
|
|
|
// hashTile returns a hash tile body of the given width.
|
|
|
|
|
func hashTile(width int, fill string) string {
|
|
|
|
|
return strings.Repeat(fill, width*tlog.HashSize)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 22:35:20 +02:00
|
|
|
// dataTile returns a data tile body holding width records, laid out the way
|
|
|
|
|
// sum.golang.org serves it: a blank line between records, none after the
|
|
|
|
|
// last.
|
|
|
|
|
func dataTile(width int) string {
|
|
|
|
|
records := make([]string, width)
|
|
|
|
|
for i := range records {
|
|
|
|
|
records[i] = fmt.Sprintf("example.com/m%d v1.0.0 h1:hash=\nexample.com/m%d v1.0.0/go.mod h1:hash=\n", i, i)
|
|
|
|
|
}
|
|
|
|
|
return strings.Join(records, "\n")
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 09:57:43 +02:00
|
|
|
// fakeSumDB is a checksum database upstream serving fixed files. Unknown
|
|
|
|
|
// paths get 404 with the body from notFound, or a default text.
|
|
|
|
|
type fakeSumDB struct {
|
2026-10-10 10:19:39 +02:00
|
|
|
server *httptest.Server
|
|
|
|
|
|
|
|
|
|
mu sync.Mutex
|
2026-10-10 09:57:43 +02:00
|
|
|
files map[string]string
|
|
|
|
|
notFound map[string]string
|
2026-10-10 10:19:39 +02:00
|
|
|
hits map[string]int
|
|
|
|
|
down atomic.Bool
|
2026-10-10 09:57:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newFakeSumDB(t *testing.T, files map[string]string) *fakeSumDB {
|
|
|
|
|
t.Helper()
|
2026-10-10 10:19:39 +02:00
|
|
|
if files == nil {
|
|
|
|
|
files = map[string]string{}
|
|
|
|
|
}
|
2026-10-10 09:57:43 +02:00
|
|
|
db := &fakeSumDB{files: files, notFound: map[string]string{}, hits: map[string]int{}}
|
|
|
|
|
db.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
db.mu.Lock()
|
|
|
|
|
db.hits[r.URL.Path]++
|
2026-10-10 10:19:39 +02:00
|
|
|
body, ok := db.files[r.URL.Path]
|
|
|
|
|
text, custom := db.notFound[r.URL.Path]
|
2026-10-10 09:57:43 +02:00
|
|
|
db.mu.Unlock()
|
|
|
|
|
if db.down.Load() {
|
|
|
|
|
http.Error(w, "unavailable", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !ok {
|
|
|
|
|
if !custom {
|
|
|
|
|
text = "not found: " + r.URL.Path
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
|
_, _ = io.WriteString(w, text)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
|
|
|
_, _ = io.WriteString(w, body)
|
|
|
|
|
}))
|
|
|
|
|
t.Cleanup(db.server.Close)
|
|
|
|
|
return db
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 10:19:39 +02:00
|
|
|
func (db *fakeSumDB) setFile(path, body string) {
|
|
|
|
|
db.mu.Lock()
|
|
|
|
|
defer db.mu.Unlock()
|
|
|
|
|
db.files[path] = body
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (db *fakeSumDB) setNotFound(path, text string) {
|
|
|
|
|
db.mu.Lock()
|
|
|
|
|
defer db.mu.Unlock()
|
|
|
|
|
db.notFound[path] = text
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 09:57:43 +02:00
|
|
|
func (db *fakeSumDB) requests(path string) int {
|
|
|
|
|
db.mu.Lock()
|
|
|
|
|
defer db.mu.Unlock()
|
|
|
|
|
return db.hits[path]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (db *fakeSumDB) totalRequests() int {
|
|
|
|
|
db.mu.Lock()
|
|
|
|
|
defer db.mu.Unlock()
|
|
|
|
|
total := 0
|
|
|
|
|
for _, n := range db.hits {
|
|
|
|
|
total += n
|
|
|
|
|
}
|
|
|
|
|
return total
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newSumDBTestHandler(t *testing.T, sumDBURL string, cacheMetadata bool, ttl time.Duration) *GoHandler {
|
|
|
|
|
t.Helper()
|
|
|
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
|
|
|
proxy.HTTPClient = http.DefaultClient
|
|
|
|
|
proxy.CacheMetadata = cacheMetadata
|
|
|
|
|
proxy.MetadataTTL = ttl
|
|
|
|
|
h := NewGoHandler(proxy, "http://proxy.example")
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
h.SetSumDBs(map[string]string{testSumDBName: sumDBURL}, false)
|
2026-10-10 09:57:43 +02:00
|
|
|
return h
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func serveGoRequest(h *GoHandler, target string) *httptest.ResponseRecorder {
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
h.Routes().ServeHTTP(w, httptest.NewRequest(http.MethodGet, target, nil))
|
|
|
|
|
return w
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 10:18:40 +02:00
|
|
|
func TestGoSumDB_DefaultsToSumGolangOrg(t *testing.T) {
|
|
|
|
|
h := NewGoHandler(nil, "http://proxy.example")
|
|
|
|
|
if got := h.sumDBs[defaultGoSumDBName]; got != defaultGoSumDBURL {
|
|
|
|
|
t.Errorf("default checksum database = %q, want %q", got, defaultGoSumDBURL)
|
|
|
|
|
}
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
h.SetSumDBs(nil, false)
|
2026-10-10 10:18:40 +02:00
|
|
|
if got := h.sumDBs[defaultGoSumDBName]; got != defaultGoSumDBURL || len(h.sumDBs) != 1 {
|
|
|
|
|
t.Errorf("SetSumDBs(nil) = %v, want only the default", h.sumDBs)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-10 09:57:43 +02:00
|
|
|
|
2026-10-10 10:18:40 +02:00
|
|
|
func TestGoSumDB_SupportedProbe(t *testing.T) {
|
2026-10-10 11:31:01 +02:00
|
|
|
db := newFakeSumDB(t, map[string]string{"/latest": testSumDBTree})
|
2026-10-10 10:18:40 +02:00
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Hour)
|
2026-10-10 09:57:43 +02:00
|
|
|
|
2026-10-10 10:18:40 +02:00
|
|
|
for range 2 {
|
2026-10-10 09:57:43 +02:00
|
|
|
for target, want := range map[string]int{
|
|
|
|
|
"/sumdb/sum.example/supported": http.StatusOK,
|
|
|
|
|
"/sumdb/sum.golang.org/supported": http.StatusNotFound,
|
|
|
|
|
"/sumdb/other.example/supported": http.StatusNotFound,
|
|
|
|
|
} {
|
|
|
|
|
if w := serveGoRequest(h, target); w.Code != want {
|
|
|
|
|
t.Errorf("%s: status = %d, want %d", target, w.Code, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-10 10:18:40 +02:00
|
|
|
}
|
|
|
|
|
if n := db.totalRequests(); n != 1 {
|
|
|
|
|
t.Errorf("upstream requests = %d, want 1: the probe fetches latest once, then uses the cache", n)
|
|
|
|
|
}
|
2026-10-10 11:31:01 +02:00
|
|
|
if h.sumDBProbes.recentlyFailed(testSumDBName) {
|
|
|
|
|
t.Error("a probe served from the fresh cache was recorded as a failure")
|
|
|
|
|
}
|
2026-10-10 10:18:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestGoSumDB_SupportedFallsBackWhenUnreachable covers the go command's only
|
|
|
|
|
// way back to the database: once supported answers 200 it never contacts the
|
|
|
|
|
// database directly, so a database the proxy cannot reach must get 404.
|
|
|
|
|
func TestGoSumDB_SupportedFallsBackWhenUnreachable(t *testing.T) {
|
|
|
|
|
for _, cacheMetadata := range []bool{true, false} {
|
|
|
|
|
t.Run(fmt.Sprintf("cache_metadata=%v", cacheMetadata), func(t *testing.T) {
|
2026-10-10 11:31:01 +02:00
|
|
|
db := newFakeSumDB(t, map[string]string{"/latest": testSumDBTree})
|
2026-10-10 10:18:40 +02:00
|
|
|
db.down.Store(true)
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, cacheMetadata, time.Hour)
|
|
|
|
|
|
|
|
|
|
for i := range 2 {
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/supported"); w.Code != http.StatusNotFound {
|
|
|
|
|
t.Fatalf("request %d: status = %d, want 404", i, w.Code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if n := db.requests("/latest"); n != 1 {
|
|
|
|
|
t.Errorf("probes = %d, want 1: a failed probe is not repeated within %s", n, goSumDBProbeBackoff)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestGoSumDB_SupportedWithCachedTreeWhileUnreachable keeps the offline case:
|
|
|
|
|
// a proxy that has served the database before can still serve it from cache.
|
|
|
|
|
func TestGoSumDB_SupportedWithCachedTreeWhileUnreachable(t *testing.T) {
|
2026-10-10 11:31:01 +02:00
|
|
|
t.Run("fresh tree", func(t *testing.T) {
|
|
|
|
|
db := newFakeSumDB(t, map[string]string{"/latest": testSumDBTree})
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Hour)
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/latest"); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("latest: status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
db.down.Store(true)
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/supported"); w.Code != http.StatusOK {
|
|
|
|
|
t.Errorf("supported with a fresh cached tree: status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
// A stale tree only counts with go_sumdbs_offline. Without it, clients
|
|
|
|
|
// that can reach the database themselves should go there.
|
|
|
|
|
for _, offline := range []bool{true, false} {
|
|
|
|
|
t.Run(fmt.Sprintf("stale tree offline=%v", offline), func(t *testing.T) {
|
|
|
|
|
db := newFakeSumDB(t, map[string]string{"/latest": testSumDBTree})
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Nanosecond)
|
|
|
|
|
h.sumDBOffline = offline
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/latest"); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("latest: status = %d, want 200", w.Code)
|
2026-10-10 11:31:01 +02:00
|
|
|
}
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
db.down.Store(true)
|
|
|
|
|
want := http.StatusNotFound
|
|
|
|
|
if offline {
|
|
|
|
|
want = http.StatusOK
|
|
|
|
|
}
|
|
|
|
|
for i := range 2 { // second probe hits the remembered failure
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/supported"); w.Code != want {
|
|
|
|
|
t.Errorf("probe %d: status = %d, want %d", i, w.Code, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if n := db.requests("/latest"); n != 2 {
|
|
|
|
|
t.Errorf("upstream requests = %d, want 2: the failure is remembered", n)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-10-10 11:31:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestGoSumDB_SupportedGivesUpOnSilentDatabase: a database that accepts the
|
|
|
|
|
// connection but never answers (a firewall dropping packets) must not hold
|
|
|
|
|
// the go command, which waits for supported without a timeout of its own.
|
|
|
|
|
func TestGoSumDB_SupportedGivesUpOnSilentDatabase(t *testing.T) {
|
|
|
|
|
release := make(chan struct{})
|
|
|
|
|
db := newFakeSumDB(t, nil)
|
|
|
|
|
t.Cleanup(func() { close(release) }) // runs before the server closes
|
|
|
|
|
db.server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
<-release
|
|
|
|
|
})
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Hour)
|
|
|
|
|
h.sumDBProbes.timeout = 50 * time.Millisecond
|
|
|
|
|
|
|
|
|
|
answered := make(chan *httptest.ResponseRecorder, 1)
|
|
|
|
|
go func() { answered <- serveGoRequest(h, "/sumdb/sum.example/supported") }()
|
|
|
|
|
select {
|
|
|
|
|
case w := <-answered:
|
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
|
|
|
t.Fatalf("status = %d, want 404", w.Code)
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(time.Second):
|
|
|
|
|
t.Fatal("supported still waiting for the database after a second")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
// TestGoSumDB_SupportedWithStaleTreeWhileSilent: the fetch only falls back
|
|
|
|
|
// to a stale cached tree once upstream has failed, which a silent database
|
|
|
|
|
// delays past the probe timeout. The proxy can still serve from cache, so
|
|
|
|
|
// supported must say so.
|
2026-10-10 11:31:01 +02:00
|
|
|
func TestGoSumDB_SupportedWithStaleTreeWhileSilent(t *testing.T) {
|
|
|
|
|
release := make(chan struct{})
|
|
|
|
|
db := newFakeSumDB(t, map[string]string{"/latest": testSumDBTree})
|
|
|
|
|
t.Cleanup(func() { close(release) })
|
2026-10-10 10:18:40 +02:00
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Nanosecond)
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
h.sumDBOffline = true
|
2026-10-10 11:31:01 +02:00
|
|
|
h.sumDBProbes.timeout = 50 * time.Millisecond
|
2026-10-10 10:18:40 +02:00
|
|
|
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/latest"); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("latest: status = %d, want 200", w.Code)
|
|
|
|
|
}
|
2026-10-10 11:31:01 +02:00
|
|
|
db.server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
<-release
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
for i := range 2 { // second probe hits the remembered failure
|
|
|
|
|
answered := make(chan *httptest.ResponseRecorder, 1)
|
|
|
|
|
go func() { answered <- serveGoRequest(h, "/sumdb/sum.example/supported") }()
|
|
|
|
|
select {
|
|
|
|
|
case w := <-answered:
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("probe %d: status = %d, want 200 from the cached tree", i, w.Code)
|
2026-10-10 11:31:01 +02:00
|
|
|
}
|
|
|
|
|
case <-time.After(time.Second):
|
|
|
|
|
t.Fatalf("probe %d: supported still waiting after a second", i)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestGoSumDB_SlowProbeSuccessClearsFailure: a database that answers after
|
|
|
|
|
// the probe timeout gets a 404 the first time, but once its answer is in
|
|
|
|
|
// the cache, supported says yes without waiting for the next backoff.
|
|
|
|
|
func TestGoSumDB_SlowProbeSuccessClearsFailure(t *testing.T) {
|
|
|
|
|
db := newFakeSumDB(t, map[string]string{"/latest": testSumDBTree})
|
|
|
|
|
inner := db.server.Config.Handler
|
|
|
|
|
db.server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
|
inner.ServeHTTP(w, r)
|
|
|
|
|
})
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Hour)
|
|
|
|
|
h.sumDBProbes.timeout = 10 * time.Millisecond
|
|
|
|
|
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/supported"); w.Code != http.StatusNotFound {
|
|
|
|
|
t.Fatalf("first probe: status = %d, want 404", w.Code)
|
|
|
|
|
}
|
|
|
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
|
|
|
for h.sumDBProbes.recentlyFailed(testSumDBName) {
|
|
|
|
|
if time.Now().After(deadline) {
|
|
|
|
|
t.Fatal("the late fetch never cleared the failure")
|
|
|
|
|
}
|
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
|
}
|
2026-10-10 10:18:40 +02:00
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/supported"); w.Code != http.StatusOK {
|
2026-10-10 11:31:01 +02:00
|
|
|
t.Errorf("after the late answer: status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if n := db.requests("/latest"); n != 1 {
|
|
|
|
|
t.Errorf("upstream requests = %d, want 1: the second probe is a cache hit", n)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestGoSumDB_StaleFallbackIsNotAnAnswer: the fetch reports no error when
|
|
|
|
|
// it falls back to the cached tree after the database failed. That must
|
|
|
|
|
// not count as the database answering, or the failure would be forgotten
|
|
|
|
|
// and every probe would wait for the timeout again.
|
|
|
|
|
func TestGoSumDB_StaleFallbackIsNotAnAnswer(t *testing.T) {
|
|
|
|
|
db := newFakeSumDB(t, map[string]string{"/latest": testSumDBTree})
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Nanosecond)
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
h.sumDBOffline = true
|
2026-10-10 11:31:01 +02:00
|
|
|
h.sumDBProbes.timeout = 10 * time.Millisecond
|
|
|
|
|
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/latest"); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("latest: status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
inner := db.server.Config.Handler
|
|
|
|
|
db.server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
|
inner.ServeHTTP(w, r)
|
|
|
|
|
})
|
|
|
|
|
db.down.Store(true)
|
|
|
|
|
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/supported"); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200 from the cached tree", w.Code)
|
2026-10-10 11:31:01 +02:00
|
|
|
}
|
|
|
|
|
time.Sleep(300 * time.Millisecond) // the fetch has failed and fallen back by now
|
|
|
|
|
if !h.sumDBProbes.recentlyFailed(testSumDBName) {
|
|
|
|
|
t.Error("the stale fallback cleared the remembered failure")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestGoSumDB_AnswerCountsWhenCacheWriteFails: an answer from the database
|
|
|
|
|
// is an answer even when it could not be cached afterwards, whether or not
|
|
|
|
|
// a stale tree was cached before.
|
|
|
|
|
func TestGoSumDB_AnswerCountsWhenCacheWriteFails(t *testing.T) {
|
|
|
|
|
for _, staleTree := range []bool{false, true} {
|
|
|
|
|
t.Run(fmt.Sprintf("stale tree cached=%v", staleTree), func(t *testing.T) {
|
|
|
|
|
db := newFakeSumDB(t, map[string]string{"/latest": testSumDBTree})
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Nanosecond)
|
|
|
|
|
if staleTree {
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/latest"); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("latest: status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
h.proxy.Storage.(*mockStorage).storeErr = errors.New("disk full")
|
|
|
|
|
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/supported"); w.Code != http.StatusOK {
|
|
|
|
|
t.Errorf("status = %d, want 200: the database answered", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if h.sumDBProbes.recentlyFailed(testSumDBName) {
|
|
|
|
|
t.Error("an answered probe was recorded as a failure")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
// TestGoSumDB_OfflineServesCacheWithoutWaiting: in offline mode, once the
|
|
|
|
|
// database is known to be unreachable, stale lookups must come from the
|
|
|
|
|
// cache at once instead of waiting for the upstream timeout each time.
|
|
|
|
|
func TestGoSumDB_OfflineServesCacheWithoutWaiting(t *testing.T) {
|
|
|
|
|
const lookup = "/lookup/golang.org/x/text@v0.14.0"
|
|
|
|
|
release := make(chan struct{})
|
|
|
|
|
db := newFakeSumDB(t, map[string]string{"/latest": testSumDBTree, lookup: testSumDBRecord})
|
|
|
|
|
t.Cleanup(func() { close(release) })
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Nanosecond)
|
|
|
|
|
h.sumDBOffline = true
|
|
|
|
|
h.sumDBProbes.timeout = 50 * time.Millisecond
|
|
|
|
|
|
|
|
|
|
for _, path := range []string{"/latest", lookup} {
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example"+path); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("%s: status = %d, want 200", path, w.Code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
db.server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
<-release
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
for _, path := range []string{"/supported", "/latest", lookup} {
|
|
|
|
|
answered := make(chan *httptest.ResponseRecorder, 1)
|
|
|
|
|
go func() { answered <- serveGoRequest(h, "/sumdb/sum.example"+path) }()
|
|
|
|
|
select {
|
|
|
|
|
case w := <-answered:
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Errorf("%s: status = %d, want 200 from the cache", path, w.Code)
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(time.Second):
|
|
|
|
|
t.Fatalf("%s: still waiting for the database after a second", path)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestGoSumDB_StaleLookupRemembersFailure: the go command asks supported
|
|
|
|
|
// only once per process. A lookup that falls back to the cache must renew
|
|
|
|
|
// the remembered failure itself, or offline mode would wait for the upstream
|
|
|
|
|
// timeout on every lookup a minute into a long build.
|
|
|
|
|
func TestGoSumDB_StaleLookupRemembersFailure(t *testing.T) {
|
|
|
|
|
const lookup = "/lookup/golang.org/x/text@v0.14.0"
|
|
|
|
|
db := newFakeSumDB(t, map[string]string{lookup: testSumDBRecord})
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Nanosecond)
|
|
|
|
|
h.sumDBOffline = true
|
|
|
|
|
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example"+lookup); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("online: status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if h.sumDBProbes.recentlyFailed(testSumDBName) {
|
|
|
|
|
t.Fatal("an answered lookup was recorded as a failure")
|
|
|
|
|
}
|
|
|
|
|
db.down.Store(true)
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example"+lookup); w.Code != http.StatusOK || w.Body.String() != testSumDBRecord {
|
|
|
|
|
t.Fatalf("outage: status = %d, body = %q, want the cached record", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if !h.sumDBProbes.recentlyFailed(testSumDBName) {
|
|
|
|
|
t.Error("a lookup served from the cache after the database failed did not remember the failure")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestGoSumDB_LateStaleLookupKeepsNewerSuccess: a lookup sent while the
|
|
|
|
|
// database was failing, which falls back to the cache only after a probe has
|
|
|
|
|
// reached the database again, must not mark it unreachable.
|
|
|
|
|
func TestGoSumDB_LateStaleLookupKeepsNewerSuccess(t *testing.T) {
|
|
|
|
|
const lookup = "/lookup/golang.org/x/text@v0.14.0"
|
|
|
|
|
db := newFakeSumDB(t, map[string]string{"/latest": testSumDBTree, lookup: testSumDBRecord})
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Nanosecond)
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example"+lookup); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("lookup: status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var sentOnce sync.Once
|
|
|
|
|
sent := make(chan struct{})
|
|
|
|
|
release := make(chan struct{})
|
|
|
|
|
inner := db.server.Config.Handler
|
|
|
|
|
db.server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.URL.Path == lookup {
|
|
|
|
|
sentOnce.Do(func() { close(sent) })
|
|
|
|
|
<-release
|
|
|
|
|
http.Error(w, "unavailable", http.StatusServiceUnavailable)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
inner.ServeHTTP(w, r)
|
|
|
|
|
})
|
|
|
|
|
done := make(chan *httptest.ResponseRecorder, 1)
|
|
|
|
|
go func() { done <- serveGoRequest(h, "/sumdb/sum.example"+lookup) }()
|
|
|
|
|
<-sent
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/supported"); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("supported: status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
close(release)
|
|
|
|
|
if w := <-done; w.Code != http.StatusOK || w.Body.String() != testSumDBRecord {
|
|
|
|
|
t.Fatalf("lookup: status = %d, body = %q, want the cached record", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if h.sumDBProbes.recentlyFailed(testSumDBName) {
|
|
|
|
|
t.Error("a lookup that fell back after the probe succeeded marked the database unreachable")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestGoSumDB_ShortcutNeedsOfflineMode: without go_sumdbs_offline, a
|
|
|
|
|
// remembered failure does not keep cached lookups from asking the database,
|
|
|
|
|
// which may well be back.
|
|
|
|
|
func TestGoSumDB_ShortcutNeedsOfflineMode(t *testing.T) {
|
|
|
|
|
const lookup = "/lookup/golang.org/x/text@v0.14.0"
|
|
|
|
|
db := newFakeSumDB(t, map[string]string{lookup: testSumDBRecord})
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Nanosecond)
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example"+lookup); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("lookup: status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h.sumDBProbes.record(testSumDBName, false)
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example"+lookup); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("lookup with a remembered failure: status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if n := db.requests(lookup); n != 2 {
|
|
|
|
|
t.Errorf("database requests = %d, want 2: the cached lookup was served without asking", n)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 22:35:23 +02:00
|
|
|
// TestGoSumDB_FreshTreeIsNotAnAnswer: a supported check answered from a
|
|
|
|
|
// fresh cached tree did not reach the database, so it must not cancel a
|
|
|
|
|
// failure that a lookup started earlier reports afterwards.
|
|
|
|
|
func TestGoSumDB_FreshTreeIsNotAnAnswer(t *testing.T) {
|
|
|
|
|
db := newFakeSumDB(t, map[string]string{"/latest": testSumDBTree})
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Hour)
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/supported"); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("supported: status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 23:08:08 +02:00
|
|
|
// Coarse clocks (Windows) can give the first probe's success and start
|
|
|
|
|
// the same time; start must come after it.
|
|
|
|
|
h.sumDBProbes.mu.Lock()
|
|
|
|
|
succeeded := h.sumDBProbes.succeeded[testSumDBName]
|
|
|
|
|
h.sumDBProbes.mu.Unlock()
|
2026-10-12 22:35:23 +02:00
|
|
|
start := time.Now()
|
2026-10-12 23:08:08 +02:00
|
|
|
for !start.After(succeeded) {
|
|
|
|
|
start = time.Now()
|
|
|
|
|
}
|
2026-10-12 22:35:23 +02:00
|
|
|
db.down.Store(true)
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/supported"); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("supported with a fresh cached tree: status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
h.sumDBProbes.recordFailure(testSumDBName, start)
|
|
|
|
|
if !h.sumDBProbes.recentlyFailed(testSumDBName) {
|
|
|
|
|
t.Error("a supported check answered from the cache counted as reaching the database")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 11:31:01 +02:00
|
|
|
func TestGoSumDBProbes_TimeoutDoesNotOverwriteLaterSuccess(t *testing.T) {
|
|
|
|
|
var probes goSumDBProbes
|
|
|
|
|
start := time.Now()
|
|
|
|
|
probes.record(testSumDBName, true)
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
probes.recordFailure(testSumDBName, start)
|
2026-10-10 11:31:01 +02:00
|
|
|
if probes.recentlyFailed(testSumDBName) {
|
|
|
|
|
t.Error("a success recorded after the probe started was overwritten by its timeout")
|
|
|
|
|
}
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
probes.recordFailure(testSumDBName, time.Now().Add(time.Second))
|
2026-10-10 11:31:01 +02:00
|
|
|
if !probes.recentlyFailed(testSumDBName) {
|
|
|
|
|
t.Error("a timeout with no later success was not recorded")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestGoSumDB_SupportedRefusesBlockPage: egress proxies answer blocked hosts
|
|
|
|
|
// with a 200 page. That is not a reachable database, and must not be cached
|
|
|
|
|
// as its signed tree either.
|
|
|
|
|
func TestGoSumDB_SupportedRefusesBlockPage(t *testing.T) {
|
|
|
|
|
db := newFakeSumDB(t, map[string]string{"/latest": "<html>access to this site is blocked</html>"})
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Hour)
|
|
|
|
|
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/supported"); w.Code != http.StatusNotFound {
|
|
|
|
|
t.Errorf("supported: status = %d, want 404", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example/latest"); w.Code != http.StatusBadGateway {
|
|
|
|
|
t.Errorf("latest: status = %d, want 502 for a block page", w.Code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The page was not cached: once the database answers, its tree is served.
|
|
|
|
|
db.setFile("/latest", testSumDBTree)
|
|
|
|
|
w := serveGoRequest(h, "/sumdb/sum.example/latest")
|
|
|
|
|
if w.Code != http.StatusOK || w.Body.String() != testSumDBTree {
|
|
|
|
|
t.Errorf("latest after recovery: status = %d, body = %q, want the signed tree", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestGoSumDB_LookupRefusesBlockPage: a block page in place of a lookup must
|
|
|
|
|
// not be cached, where it would replace the copy served during an outage.
|
|
|
|
|
// That includes a page that happens to start like a record.
|
|
|
|
|
func TestGoSumDB_LookupRefusesBlockPage(t *testing.T) {
|
|
|
|
|
const lookup = "/lookup/golang.org/x/text@v0.14.0"
|
|
|
|
|
pages := map[string]string{
|
|
|
|
|
"html": "<html>access to this site is blocked</html>",
|
|
|
|
|
"record without tree": "1\ngolang.org/x/text v0.14.0 h1:hash=\n\n<html>access to this site is blocked</html>",
|
|
|
|
|
}
|
|
|
|
|
for name, page := range pages {
|
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
|
db := newFakeSumDB(t, map[string]string{lookup: testSumDBRecord})
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Nanosecond)
|
|
|
|
|
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example"+lookup); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("lookup: status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
db.setFile(lookup, page)
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example"+lookup); w.Code != http.StatusOK || w.Body.String() != testSumDBRecord {
|
|
|
|
|
t.Errorf("behind a block page: status = %d, body = %q, want the cached record", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The page did not replace the record: it is still there in an outage.
|
|
|
|
|
db.down.Store(true)
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example"+lookup); w.Code != http.StatusOK || w.Body.String() != testSumDBRecord {
|
|
|
|
|
t.Errorf("in an outage: status = %d, body = %q, want the cached record", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-10-10 10:18:40 +02:00
|
|
|
}
|
2026-10-10 09:57:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGoSumDB_RequestsGoToTheDatabase(t *testing.T) {
|
|
|
|
|
var moduleUpstreamRequests atomic.Int32
|
|
|
|
|
moduleUpstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
moduleUpstreamRequests.Add(1)
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
}))
|
|
|
|
|
defer moduleUpstream.Close()
|
|
|
|
|
|
|
|
|
|
const lookup = "/lookup/golang.org/x/text@v0.14.0"
|
2026-10-10 11:31:01 +02:00
|
|
|
db := newFakeSumDB(t, map[string]string{lookup: testSumDBRecord})
|
2026-10-10 09:57:43 +02:00
|
|
|
|
|
|
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
|
|
|
proxy.HTTPClient = http.DefaultClient
|
|
|
|
|
h := NewGoHandlerWithUpstream(proxy, "http://proxy.example", moduleUpstream.URL)
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
h.SetSumDBs(map[string]string{testSumDBName: db.server.URL + "/"}, false)
|
2026-10-10 09:57:43 +02:00
|
|
|
|
|
|
|
|
w := serveGoRequest(h, "/sumdb/sum.example"+lookup)
|
2026-10-10 11:31:01 +02:00
|
|
|
if w.Code != http.StatusOK || w.Body.String() != testSumDBRecord {
|
2026-10-10 09:57:43 +02:00
|
|
|
t.Fatalf("status = %d, body = %q; want 200 with the database's response", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if n := db.requests(lookup); n != 1 {
|
|
|
|
|
t.Errorf("database requests for %s = %d, want 1", lookup, n)
|
|
|
|
|
}
|
|
|
|
|
if n := moduleUpstreamRequests.Load(); n != 0 {
|
|
|
|
|
t.Errorf("module upstream requests = %d, want 0", n)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGoSumDB_LookupCachedWithinTTL(t *testing.T) {
|
|
|
|
|
const lookup = "/lookup/golang.org/x/text@v0.14.0"
|
2026-10-10 11:31:01 +02:00
|
|
|
db := newFakeSumDB(t, map[string]string{lookup: testSumDBRecord})
|
2026-10-10 09:57:43 +02:00
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Hour)
|
|
|
|
|
|
|
|
|
|
for i := range 2 {
|
|
|
|
|
w := serveGoRequest(h, "/sumdb/sum.example"+lookup)
|
2026-10-10 11:31:01 +02:00
|
|
|
if w.Code != http.StatusOK || w.Body.String() != testSumDBRecord {
|
2026-10-10 09:57:43 +02:00
|
|
|
t.Fatalf("request %d: status = %d, body = %q", i, w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if n := db.requests(lookup); n != 1 {
|
|
|
|
|
t.Errorf("database requests = %d, want 1: the second lookup is a cache hit", n)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGoSumDB_CompleteTilesSkipRevalidation(t *testing.T) {
|
|
|
|
|
files := map[string]string{
|
2026-10-13 16:23:12 +02:00
|
|
|
"/tile/8/0/000": hashTile(256, "a"),
|
|
|
|
|
"/tile/8/0/001.p/5": hashTile(5, "b"),
|
|
|
|
|
"/latest": testSumDBTree,
|
2026-10-10 09:57:43 +02:00
|
|
|
}
|
|
|
|
|
wantRequests := map[string]int{
|
2026-10-13 16:23:12 +02:00
|
|
|
"/tile/8/0/000": 1,
|
|
|
|
|
"/tile/8/0/001.p/5": 2,
|
|
|
|
|
"/latest": 2,
|
2026-10-10 09:57:43 +02:00
|
|
|
}
|
|
|
|
|
db := newFakeSumDB(t, files)
|
|
|
|
|
// Every cached entry is past its TTL by the second request.
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Nanosecond)
|
|
|
|
|
|
|
|
|
|
for range 2 {
|
|
|
|
|
for path, want := range files {
|
|
|
|
|
w := serveGoRequest(h, "/sumdb/sum.example"+path)
|
|
|
|
|
if w.Code != http.StatusOK || w.Body.String() != want {
|
|
|
|
|
t.Fatalf("%s: status = %d, body = %q, want %q", path, w.Code, w.Body.String(), want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for path, want := range wantRequests {
|
|
|
|
|
if n := db.requests(path); n != want {
|
|
|
|
|
t.Errorf("%s: database requests = %d, want %d", path, n, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 16:23:12 +02:00
|
|
|
// TestGoSumDB_DataTilesAreNotStored: the go command never fetches data
|
|
|
|
|
// tiles, and stored they would add up to the whole log.
|
|
|
|
|
func TestGoSumDB_DataTilesAreNotStored(t *testing.T) {
|
|
|
|
|
files := map[string]string{
|
|
|
|
|
"/tile/8/data/000": dataTile(256),
|
|
|
|
|
"/tile/8/data/001.p/5": dataTile(5),
|
|
|
|
|
}
|
|
|
|
|
db := newFakeSumDB(t, files)
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Hour)
|
|
|
|
|
|
|
|
|
|
for range 2 {
|
|
|
|
|
for path, want := range files {
|
|
|
|
|
w := serveGoRequest(h, "/sumdb/sum.example"+path)
|
|
|
|
|
if w.Code != http.StatusOK || w.Body.String() != want {
|
|
|
|
|
t.Fatalf("%s: status = %d, body length %d, want the %d-byte tile", path, w.Code, w.Body.Len(), len(want))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for path := range files {
|
|
|
|
|
if n := db.requests(path); n != 2 {
|
|
|
|
|
t.Errorf("%s: database requests = %d, want 2", path, n)
|
|
|
|
|
}
|
|
|
|
|
key := goSumDBCacheKey(testSumDBName, db.server.URL, strings.TrimPrefix(path, "/"))
|
|
|
|
|
if entry, _ := h.proxy.DB.GetMetadataCache(goEcosystem, key); entry != nil {
|
|
|
|
|
t.Errorf("%s: stored in the metadata cache", path)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 10:19:39 +02:00
|
|
|
// TestGoSumDB_BadTilesAreNotCached guards complete tiles, which are never
|
|
|
|
|
// revalidated once cached: a response of the wrong size must not be stored.
|
|
|
|
|
func TestGoSumDB_BadTilesAreNotCached(t *testing.T) {
|
|
|
|
|
const tile = "/tile/8/0/000"
|
|
|
|
|
db := newFakeSumDB(t, map[string]string{
|
2026-10-13 16:23:12 +02:00
|
|
|
tile: "<html>please log in</html>",
|
2026-10-10 10:19:39 +02:00
|
|
|
})
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Hour)
|
|
|
|
|
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example"+tile); w.Code != http.StatusBadGateway {
|
|
|
|
|
t.Fatalf("wrong-size tile: status = %d, want 502", w.Code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
want := hashTile(256, "a")
|
|
|
|
|
db.setFile(tile, want)
|
|
|
|
|
w := serveGoRequest(h, "/sumdb/sum.example"+tile)
|
|
|
|
|
if w.Code != http.StatusOK || w.Body.String() != want {
|
|
|
|
|
t.Fatalf("after upstream recovered: status = %d, body length %d, want the %d-byte tile", w.Code, w.Body.Len(), len(want))
|
|
|
|
|
}
|
|
|
|
|
if n := db.requests(tile); n != 2 {
|
|
|
|
|
t.Errorf("upstream requests = %d, want 2: the bad response was not cached", n)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 11:31:01 +02:00
|
|
|
// TestGoSumDB_DamagedCachedTileIsFetchedAgain guards the check on the cached
|
|
|
|
|
// copy: a complete tile is never revalidated, so a damaged blob would
|
|
|
|
|
// otherwise be served for good. The database sends an ETag, so past the TTL
|
|
|
|
|
// a request that asks whether the tile changed would get 304 and the damaged
|
|
|
|
|
// copy back.
|
|
|
|
|
func TestGoSumDB_DamagedCachedTileIsFetchedAgain(t *testing.T) {
|
|
|
|
|
const tile = "/tile/8/0/000"
|
|
|
|
|
want := hashTile(256, "a")
|
|
|
|
|
for _, ttl := range []time.Duration{time.Hour, time.Nanosecond} {
|
|
|
|
|
t.Run(fmt.Sprintf("ttl=%s", ttl), func(t *testing.T) {
|
|
|
|
|
db := newFakeSumDB(t, map[string]string{tile: want})
|
|
|
|
|
inner := db.server.Config.Handler
|
|
|
|
|
db.server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("ETag", `"tile"`)
|
|
|
|
|
if r.Header.Get("If-None-Match") == `"tile"` {
|
|
|
|
|
w.WriteHeader(http.StatusNotModified)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
inner.ServeHTTP(w, r)
|
|
|
|
|
})
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, ttl)
|
|
|
|
|
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example"+tile); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
store := h.proxy.Storage.(*mockStorage)
|
|
|
|
|
blob := metadataStoragePath(goEcosystem, goSumDBCacheKey(testSumDBName, db.server.URL, "tile/8/0/000"))
|
|
|
|
|
store.mu.Lock()
|
|
|
|
|
if _, ok := store.files[blob]; !ok {
|
|
|
|
|
store.mu.Unlock()
|
|
|
|
|
t.Fatalf("tile not stored at %s", blob)
|
|
|
|
|
}
|
|
|
|
|
store.files[blob] = []byte(want[:100])
|
|
|
|
|
store.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
w := serveGoRequest(h, "/sumdb/sum.example"+tile)
|
|
|
|
|
if w.Code != http.StatusOK || w.Body.String() != want {
|
|
|
|
|
t.Fatalf("after damage: status = %d, body length %d, want the %d-byte tile", w.Code, w.Body.Len(), len(want))
|
|
|
|
|
}
|
|
|
|
|
if n := db.requests(tile); n != 2 {
|
|
|
|
|
t.Errorf("upstream requests = %d, want 2: the damaged copy was fetched again", n)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 09:57:43 +02:00
|
|
|
func TestGoSumDB_CompleteTilesNotStoredWithoutMetadataCache(t *testing.T) {
|
|
|
|
|
const tile = "/tile/8/0/000"
|
2026-10-10 10:19:39 +02:00
|
|
|
db := newFakeSumDB(t, map[string]string{tile: hashTile(256, "a")})
|
2026-10-10 09:57:43 +02:00
|
|
|
h := newSumDBTestHandler(t, db.server.URL, false, time.Hour)
|
|
|
|
|
|
|
|
|
|
for range 2 {
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example"+tile); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if n := db.requests(tile); n != 2 {
|
|
|
|
|
t.Errorf("database requests = %d, want 2 with cache_metadata off", n)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGoSumDB_OfflineFallback(t *testing.T) {
|
|
|
|
|
files := map[string]string{
|
2026-10-10 11:31:01 +02:00
|
|
|
"/lookup/golang.org/x/text@v0.14.0": testSumDBRecord,
|
|
|
|
|
"/latest": testSumDBTree,
|
2026-10-10 10:19:39 +02:00
|
|
|
"/tile/8/0/000": hashTile(256, "a"),
|
|
|
|
|
"/tile/8/0/001.p/5": hashTile(5, "b"),
|
2026-10-10 09:57:43 +02:00
|
|
|
}
|
|
|
|
|
db := newFakeSumDB(t, files)
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Nanosecond)
|
|
|
|
|
|
|
|
|
|
for path := range files {
|
|
|
|
|
if w := serveGoRequest(h, "/sumdb/sum.example"+path); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("%s online: status = %d, want 200", path, w.Code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db.down.Store(true)
|
|
|
|
|
for path, want := range files {
|
|
|
|
|
w := serveGoRequest(h, "/sumdb/sum.example"+path)
|
|
|
|
|
if w.Code != http.StatusOK || w.Body.String() != want {
|
|
|
|
|
t.Errorf("%s offline: status = %d, body = %q, want 200 with %q", path, w.Code, w.Body.String(), want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGoSumDB_NotFoundRelaysUpstreamText(t *testing.T) {
|
|
|
|
|
const lookup = "/lookup/github.com/google/uuid@v9.9.9"
|
|
|
|
|
const reason = "not found: github.com/google/uuid@v9.9.9: invalid version: unknown revision v9.9.9"
|
|
|
|
|
|
|
|
|
|
for _, cacheMetadata := range []bool{true, false} {
|
|
|
|
|
t.Run(fmt.Sprintf("cache_metadata=%v", cacheMetadata), func(t *testing.T) {
|
|
|
|
|
db := newFakeSumDB(t, nil)
|
2026-10-10 10:19:39 +02:00
|
|
|
db.setNotFound(lookup, reason)
|
2026-10-10 09:57:43 +02:00
|
|
|
h := newSumDBTestHandler(t, db.server.URL, cacheMetadata, time.Hour)
|
|
|
|
|
|
|
|
|
|
w := serveGoRequest(h, "/sumdb/sum.example"+lookup)
|
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
|
|
|
t.Fatalf("status = %d, want 404", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if got := strings.TrimSpace(w.Body.String()); got != reason {
|
|
|
|
|
t.Errorf("body = %q, want the database's text %q", got, reason)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.Run("long text is cut", func(t *testing.T) {
|
|
|
|
|
db := newFakeSumDB(t, nil)
|
2026-10-10 10:19:39 +02:00
|
|
|
db.setNotFound(lookup, strings.Repeat("x", upstreamNotFoundBodyLimit+1000))
|
2026-10-10 09:57:43 +02:00
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Hour)
|
|
|
|
|
|
|
|
|
|
w := serveGoRequest(h, "/sumdb/sum.example"+lookup)
|
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
|
|
|
t.Fatalf("status = %d, want 404", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if got := w.Body.Len(); got != upstreamNotFoundBodyLimit {
|
|
|
|
|
t.Errorf("body length = %d, want %d", got, upstreamNotFoundBodyLimit)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGoSumDB_InvalidPathsNeverReachUpstream(t *testing.T) {
|
|
|
|
|
db := newFakeSumDB(t, nil)
|
|
|
|
|
h := newSumDBTestHandler(t, db.server.URL, true, time.Hour)
|
|
|
|
|
|
|
|
|
|
for _, target := range []string{
|
|
|
|
|
"/sumdb/sum.example",
|
|
|
|
|
"/sumdb/sum.example/",
|
|
|
|
|
"/sumdb/sum.example/other",
|
|
|
|
|
"/sumdb/sum.example/latest/extra",
|
|
|
|
|
"/sumdb/sum.example/lookup/",
|
|
|
|
|
"/sumdb/sum.example/lookup/golang.org/x/text",
|
|
|
|
|
"/sumdb/sum.example/lookup/golang.org/x/text@",
|
|
|
|
|
"/sumdb/sum.example/lookup/golang.org/x/text@master",
|
|
|
|
|
"/sumdb/sum.example/lookup/github.com/Google/uuid@v1.6.0",
|
|
|
|
|
"/sumdb/sum.example/lookup/../../etc/passwd@v1.0.0",
|
|
|
|
|
"/sumdb/sum.example/tile/x/0/000",
|
|
|
|
|
"/sumdb/sum.example/tile/8/0/000.p/0",
|
|
|
|
|
"/sumdb/sum.example/tile/8/0/000.p/256",
|
|
|
|
|
"/sumdb/sum.example/tile/8/0/../../../latest",
|
|
|
|
|
"/sumdb/unknown.example/latest",
|
|
|
|
|
} {
|
|
|
|
|
if w := serveGoRequest(h, target); w.Code != http.StatusNotFound {
|
|
|
|
|
t.Errorf("%s: status = %d, want 404", target, w.Code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if n := db.totalRequests(); n != 0 {
|
|
|
|
|
t.Errorf("upstream requests = %d, want 0", n)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sumDBClientOps lets the x/mod checksum database client read through the
|
|
|
|
|
// proxy, keeping its configuration and cache in memory.
|
|
|
|
|
type sumDBClientOps struct {
|
|
|
|
|
t *testing.T
|
|
|
|
|
remote string
|
|
|
|
|
key string
|
|
|
|
|
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
config map[string][]byte
|
|
|
|
|
cache map[string][]byte
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newSumDBClientOps(t *testing.T, remote, key string) *sumDBClientOps {
|
|
|
|
|
return &sumDBClientOps{t: t, remote: remote, key: key, config: map[string][]byte{}, cache: map[string][]byte{}}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *sumDBClientOps) ReadRemote(path string) ([]byte, error) {
|
|
|
|
|
resp, err := http.Get(o.remote + path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
return nil, fmt.Errorf("GET %s: %s: %s", path, resp.Status, body)
|
|
|
|
|
}
|
|
|
|
|
return body, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *sumDBClientOps) ReadConfig(file string) ([]byte, error) {
|
|
|
|
|
if file == "key" {
|
|
|
|
|
return []byte(o.key), nil
|
|
|
|
|
}
|
|
|
|
|
o.mu.Lock()
|
|
|
|
|
defer o.mu.Unlock()
|
|
|
|
|
return o.config[file], nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *sumDBClientOps) WriteConfig(file string, old, data []byte) error {
|
|
|
|
|
o.mu.Lock()
|
|
|
|
|
defer o.mu.Unlock()
|
|
|
|
|
if !bytes.Equal(o.config[file], old) {
|
|
|
|
|
return sumdb.ErrWriteConflict
|
|
|
|
|
}
|
|
|
|
|
o.config[file] = data
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *sumDBClientOps) ReadCache(file string) ([]byte, error) {
|
|
|
|
|
o.mu.Lock()
|
|
|
|
|
defer o.mu.Unlock()
|
|
|
|
|
data, ok := o.cache[file]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, errors.New("not cached")
|
|
|
|
|
}
|
|
|
|
|
return data, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *sumDBClientOps) WriteCache(file string, data []byte) {
|
|
|
|
|
o.mu.Lock()
|
|
|
|
|
defer o.mu.Unlock()
|
|
|
|
|
o.cache[file] = data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *sumDBClientOps) Log(msg string) { o.t.Log(msg) }
|
|
|
|
|
|
|
|
|
|
func (o *sumDBClientOps) SecurityError(msg string) {
|
|
|
|
|
o.t.Errorf("checksum database security error: %s", msg)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestGoSumDB_ClientRoundTrip runs the go command's checksum database client
|
|
|
|
|
// against a real database implementation through the proxy, so signatures,
|
|
|
|
|
// record proofs and tile hashes are checked end to end. A second client then
|
|
|
|
|
// verifies the same lookup from the proxy's cache with the database down.
|
|
|
|
|
func TestGoSumDB_ClientRoundTrip(t *testing.T) {
|
|
|
|
|
signerKey, verifierKey, err := note.GenerateKey(rand.Reader, testSumDBName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
gosum := func(path, version string) ([]byte, error) {
|
|
|
|
|
return fmt.Appendf(nil, "%s %s h1:Y29udGVudA==\n%s %s/go.mod h1:Z28ubW9k\n", path, version, path, version), nil
|
|
|
|
|
}
|
|
|
|
|
database := sumdb.NewServer(sumdb.NewTestServer(signerKey, gosum))
|
|
|
|
|
var down atomic.Bool
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if down.Load() {
|
|
|
|
|
http.Error(w, "unavailable", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
database.ServeHTTP(w, r)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
h := newSumDBTestHandler(t, upstream.URL, true, time.Hour)
|
|
|
|
|
proxy := httptest.NewServer(h.Routes())
|
|
|
|
|
defer proxy.Close()
|
|
|
|
|
remote := proxy.URL + "/sumdb/" + testSumDBName
|
|
|
|
|
|
|
|
|
|
client := sumdb.NewClient(newSumDBClientOps(t, remote, verifierKey))
|
|
|
|
|
for _, m := range [][2]string{{"rsc.io/quote", "v1.5.2"}, {"golang.org/x/text", "v0.3.0"}, {"github.com/Google/UUID", "v1.6.0"}} {
|
|
|
|
|
lines, err := client.Lookup(m[0], m[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("lookup %s@%s through the proxy: %v", m[0], m[1], err)
|
|
|
|
|
}
|
|
|
|
|
if want := m[0] + " " + m[1] + " h1:Y29udGVudA=="; len(lines) != 1 || lines[0] != want {
|
|
|
|
|
t.Errorf("lookup %s@%s = %q, want [%q]", m[0], m[1], lines, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
down.Store(true)
|
|
|
|
|
offline := sumdb.NewClient(newSumDBClientOps(t, remote, verifierKey))
|
|
|
|
|
if _, err := offline.Lookup("rsc.io/quote", "v1.5.2"); err != nil {
|
|
|
|
|
t.Fatalf("lookup from the proxy cache with the database down: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|