diff options
Diffstat (limited to 'src/cmd/go/proxy_test.go')
-rw-r--r-- | src/cmd/go/proxy_test.go | 492 |
1 files changed, 492 insertions, 0 deletions
diff --git a/src/cmd/go/proxy_test.go b/src/cmd/go/proxy_test.go new file mode 100644 index 0000000..e390c73 --- /dev/null +++ b/src/cmd/go/proxy_test.go @@ -0,0 +1,492 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main_test + +import ( + "archive/zip" + "bytes" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "io/fs" + "log" + "net" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "testing" + + "cmd/go/internal/modfetch" + "cmd/go/internal/modfetch/codehost" + "cmd/go/internal/par" + "cmd/go/internal/txtar" + + "golang.org/x/mod/module" + "golang.org/x/mod/semver" + "golang.org/x/mod/sumdb" + "golang.org/x/mod/sumdb/dirhash" +) + +var ( + proxyAddr = flag.String("proxy", "", "run proxy on this network address instead of running any tests") + proxyURL string +) + +var proxyOnce sync.Once + +// StartProxy starts the Go module proxy running on *proxyAddr (like "localhost:1234") +// and sets proxyURL to the GOPROXY setting to use to access the proxy. +// Subsequent calls are no-ops. +// +// The proxy serves from testdata/mod. See testdata/mod/README. +func StartProxy() { + proxyOnce.Do(func() { + readModList() + addr := *proxyAddr + if addr == "" { + addr = "localhost:0" + } + l, err := net.Listen("tcp", addr) + if err != nil { + log.Fatal(err) + } + *proxyAddr = l.Addr().String() + proxyURL = "http://" + *proxyAddr + "/mod" + fmt.Fprintf(os.Stderr, "go test proxy running at GOPROXY=%s\n", proxyURL) + go func() { + log.Fatalf("go proxy: http.Serve: %v", http.Serve(l, http.HandlerFunc(proxyHandler))) + }() + + // Prepopulate main sumdb. + for _, mod := range modList { + sumdbOps.Lookup(nil, mod) + } + }) +} + +var modList []module.Version + +func readModList() { + files, err := os.ReadDir("testdata/mod") + if err != nil { + log.Fatal(err) + } + for _, f := range files { + name := f.Name() + if !strings.HasSuffix(name, ".txt") { + continue + } + name = strings.TrimSuffix(name, ".txt") + i := strings.LastIndex(name, "_v") + if i < 0 { + continue + } + encPath := strings.ReplaceAll(name[:i], "_", "/") + path, err := module.UnescapePath(encPath) + if err != nil { + if testing.Verbose() && encPath != "example.com/invalidpath/v1" { + fmt.Fprintf(os.Stderr, "go proxy_test: %v\n", err) + } + continue + } + encVers := name[i+1:] + vers, err := module.UnescapeVersion(encVers) + if err != nil { + fmt.Fprintf(os.Stderr, "go proxy_test: %v\n", err) + continue + } + modList = append(modList, module.Version{Path: path, Version: vers}) + } +} + +var zipCache par.Cache + +const ( + testSumDBName = "localhost.localdev/sumdb" + testSumDBVerifierKey = "localhost.localdev/sumdb+00000c67+AcTrnkbUA+TU4heY3hkjiSES/DSQniBqIeQ/YppAUtK6" + testSumDBSignerKey = "PRIVATE+KEY+localhost.localdev/sumdb+00000c67+AXu6+oaVaOYuQOFrf1V59JK1owcFlJcHwwXHDfDGxSPk" +) + +var ( + sumdbOps = sumdb.NewTestServer(testSumDBSignerKey, proxyGoSum) + sumdbServer = sumdb.NewServer(sumdbOps) + + sumdbWrongOps = sumdb.NewTestServer(testSumDBSignerKey, proxyGoSumWrong) + sumdbWrongServer = sumdb.NewServer(sumdbWrongOps) +) + +// proxyHandler serves the Go module proxy protocol. +// See the proxy section of https://research.swtch.com/vgo-module. +func proxyHandler(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, "/mod/") { + http.NotFound(w, r) + return + } + path := r.URL.Path[len("/mod/"):] + + // /mod/invalid returns faulty responses. + if strings.HasPrefix(path, "invalid/") { + w.Write([]byte("invalid")) + return + } + + // Next element may opt into special behavior. + if j := strings.Index(path, "/"); j >= 0 { + n, err := strconv.Atoi(path[:j]) + if err == nil && n >= 200 { + w.WriteHeader(n) + return + } + if strings.HasPrefix(path, "sumdb-") { + n, err := strconv.Atoi(path[len("sumdb-"):j]) + if err == nil && n >= 200 { + if strings.HasPrefix(path[j:], "/sumdb/") { + w.WriteHeader(n) + return + } + path = path[j+1:] + } + } + } + + // Request for $GOPROXY/sumdb-direct is direct sumdb access. + // (Client thinks it is talking directly to a sumdb.) + if strings.HasPrefix(path, "sumdb-direct/") { + r.URL.Path = path[len("sumdb-direct"):] + sumdbServer.ServeHTTP(w, r) + return + } + + // Request for $GOPROXY/sumdb-wrong is direct sumdb access + // but all the hashes are wrong. + // (Client thinks it is talking directly to a sumdb.) + if strings.HasPrefix(path, "sumdb-wrong/") { + r.URL.Path = path[len("sumdb-wrong"):] + sumdbWrongServer.ServeHTTP(w, r) + return + } + + // Request for $GOPROXY/redirect/<count>/... goes to redirects. + if strings.HasPrefix(path, "redirect/") { + path = path[len("redirect/"):] + if j := strings.Index(path, "/"); j >= 0 { + count, err := strconv.Atoi(path[:j]) + if err != nil { + return + } + + // The last redirect. + if count <= 1 { + http.Redirect(w, r, fmt.Sprintf("/mod/%s", path[j+1:]), 302) + return + } + http.Redirect(w, r, fmt.Sprintf("/mod/redirect/%d/%s", count-1, path[j+1:]), 302) + return + } + } + + // Request for $GOPROXY/sumdb/<name>/supported + // is checking whether it's OK to access sumdb via the proxy. + if path == "sumdb/"+testSumDBName+"/supported" { + w.WriteHeader(200) + return + } + + // Request for $GOPROXY/sumdb/<name>/... goes to sumdb. + if sumdbPrefix := "sumdb/" + testSumDBName + "/"; strings.HasPrefix(path, sumdbPrefix) { + r.URL.Path = path[len(sumdbPrefix)-1:] + sumdbServer.ServeHTTP(w, r) + return + } + + // Module proxy request: /mod/path/@latest + // Rewrite to /mod/path/@v/<latest>.info where <latest> is the semantically + // latest version, including pseudo-versions. + if i := strings.LastIndex(path, "/@latest"); i >= 0 { + enc := path[:i] + modPath, err := module.UnescapePath(enc) + if err != nil { + if testing.Verbose() { + fmt.Fprintf(os.Stderr, "go proxy_test: %v\n", err) + } + http.NotFound(w, r) + return + } + + // Imitate what "latest" does in direct mode and what proxy.golang.org does. + // Use the latest released version. + // If there is no released version, use the latest prereleased version. + // Otherwise, use the latest pseudoversion. + var latestRelease, latestPrerelease, latestPseudo string + for _, m := range modList { + if m.Path != modPath { + continue + } + if modfetch.IsPseudoVersion(m.Version) && (latestPseudo == "" || semver.Compare(latestPseudo, m.Version) > 0) { + latestPseudo = m.Version + } else if semver.Prerelease(m.Version) != "" && (latestPrerelease == "" || semver.Compare(latestPrerelease, m.Version) > 0) { + latestPrerelease = m.Version + } else if latestRelease == "" || semver.Compare(latestRelease, m.Version) > 0 { + latestRelease = m.Version + } + } + var latest string + if latestRelease != "" { + latest = latestRelease + } else if latestPrerelease != "" { + latest = latestPrerelease + } else if latestPseudo != "" { + latest = latestPseudo + } else { + http.NotFound(w, r) + return + } + + encVers, err := module.EscapeVersion(latest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + path = fmt.Sprintf("%s/@v/%s.info", enc, encVers) + } + + // Module proxy request: /mod/path/@v/version[.suffix] + i := strings.Index(path, "/@v/") + if i < 0 { + http.NotFound(w, r) + return + } + enc, file := path[:i], path[i+len("/@v/"):] + path, err := module.UnescapePath(enc) + if err != nil { + if testing.Verbose() { + fmt.Fprintf(os.Stderr, "go proxy_test: %v\n", err) + } + http.NotFound(w, r) + return + } + if file == "list" { + // list returns a list of versions, not including pseudo-versions. + // If the module has no tagged versions, we should serve an empty 200. + // If the module doesn't exist, we should serve 404 or 410. + found := false + for _, m := range modList { + if m.Path != path { + continue + } + found = true + if !modfetch.IsPseudoVersion(m.Version) { + if err := module.Check(m.Path, m.Version); err == nil { + fmt.Fprintf(w, "%s\n", m.Version) + } + } + } + if !found { + http.NotFound(w, r) + } + return + } + + i = strings.LastIndex(file, ".") + if i < 0 { + http.NotFound(w, r) + return + } + encVers, ext := file[:i], file[i+1:] + vers, err := module.UnescapeVersion(encVers) + if err != nil { + fmt.Fprintf(os.Stderr, "go proxy_test: %v\n", err) + http.NotFound(w, r) + return + } + + if codehost.AllHex(vers) { + var best string + // Convert commit hash (only) to known version. + // Use latest version in semver priority, to match similar logic + // in the repo-based module server (see modfetch.(*codeRepo).convert). + for _, m := range modList { + if m.Path == path && semver.Compare(best, m.Version) < 0 { + var hash string + if modfetch.IsPseudoVersion(m.Version) { + hash = m.Version[strings.LastIndex(m.Version, "-")+1:] + } else { + hash = findHash(m) + } + if strings.HasPrefix(hash, vers) || strings.HasPrefix(vers, hash) { + best = m.Version + } + } + } + if best != "" { + vers = best + } + } + + a, err := readArchive(path, vers) + if err != nil { + if testing.Verbose() { + fmt.Fprintf(os.Stderr, "go proxy: no archive %s %s: %v\n", path, vers, err) + } + if errors.Is(err, fs.ErrNotExist) { + http.NotFound(w, r) + } else { + http.Error(w, "cannot load archive", 500) + } + return + } + + switch ext { + case "info", "mod": + want := "." + ext + for _, f := range a.Files { + if f.Name == want { + w.Write(f.Data) + return + } + } + + case "zip": + type cached struct { + zip []byte + err error + } + c := zipCache.Do(a, func() interface{} { + var buf bytes.Buffer + z := zip.NewWriter(&buf) + for _, f := range a.Files { + if strings.HasPrefix(f.Name, ".") { + continue + } + var zipName string + if strings.HasPrefix(f.Name, "/") { + zipName = f.Name[1:] + } else { + zipName = path + "@" + vers + "/" + f.Name + } + zf, err := z.Create(zipName) + if err != nil { + return cached{nil, err} + } + if _, err := zf.Write(f.Data); err != nil { + return cached{nil, err} + } + } + if err := z.Close(); err != nil { + return cached{nil, err} + } + return cached{buf.Bytes(), nil} + }).(cached) + + if c.err != nil { + if testing.Verbose() { + fmt.Fprintf(os.Stderr, "go proxy: %v\n", c.err) + } + http.Error(w, c.err.Error(), 500) + return + } + w.Write(c.zip) + return + + } + http.NotFound(w, r) +} + +func findHash(m module.Version) string { + a, err := readArchive(m.Path, m.Version) + if err != nil { + return "" + } + var data []byte + for _, f := range a.Files { + if f.Name == ".info" { + data = f.Data + break + } + } + var info struct{ Short string } + json.Unmarshal(data, &info) + return info.Short +} + +var archiveCache par.Cache + +var cmdGoDir, _ = os.Getwd() + +func readArchive(path, vers string) (*txtar.Archive, error) { + enc, err := module.EscapePath(path) + if err != nil { + return nil, err + } + encVers, err := module.EscapeVersion(vers) + if err != nil { + return nil, err + } + + prefix := strings.ReplaceAll(enc, "/", "_") + name := filepath.Join(cmdGoDir, "testdata/mod", prefix+"_"+encVers+".txt") + a := archiveCache.Do(name, func() interface{} { + a, err := txtar.ParseFile(name) + if err != nil { + if testing.Verbose() || !os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "go proxy: %v\n", err) + } + a = nil + } + return a + }).(*txtar.Archive) + if a == nil { + return nil, fs.ErrNotExist + } + return a, nil +} + +// proxyGoSum returns the two go.sum lines for path@vers. +func proxyGoSum(path, vers string) ([]byte, error) { + a, err := readArchive(path, vers) + if err != nil { + return nil, err + } + var names []string + files := make(map[string][]byte) + var gomod []byte + for _, f := range a.Files { + if strings.HasPrefix(f.Name, ".") { + if f.Name == ".mod" { + gomod = f.Data + } + continue + } + name := path + "@" + vers + "/" + f.Name + names = append(names, name) + files[name] = f.Data + } + h1, err := dirhash.Hash1(names, func(name string) (io.ReadCloser, error) { + data := files[name] + return io.NopCloser(bytes.NewReader(data)), nil + }) + if err != nil { + return nil, err + } + h1mod, err := dirhash.Hash1([]string{"go.mod"}, func(string) (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(gomod)), nil + }) + if err != nil { + return nil, err + } + data := []byte(fmt.Sprintf("%s %s %s\n%s %s/go.mod %s\n", path, vers, h1, path, vers, h1mod)) + return data, nil +} + +// proxyGoSumWrong returns the wrong lines. +func proxyGoSumWrong(path, vers string) ([]byte, error) { + data := []byte(fmt.Sprintf("%s %s %s\n%s %s/go.mod %s\n", path, vers, "h1:wrong", path, vers, "h1:wrong")) + return data, nil +} |