internal/proxy: proxy that disables fetch

Add a server that proxies requests to proxy.golang.org but disables
fetches.

We will use this so that `go mod tidy` doesn't download
modules that aren't in the proxy's cache.

Change-Id: I02886681e3eb267913421ac995a6894892cc9e7b
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/493675
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
diff --git a/internal/proxy/server.go b/internal/proxy/server.go
new file mode 100644
index 0000000..3d6fc4a
--- /dev/null
+++ b/internal/proxy/server.go
@@ -0,0 +1,46 @@
+// Copyright 2023 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 proxy
+
+import (
+	"io"
+	"net/http"
+	"net/http/httptest"
+
+	"golang.org/x/pkgsite-metrics/internal/log"
+)
+
+// ServeDisablingFetch returns a server that proxies requests to proxy.golang.org,
+// adding the Disable-Fetch header to prevent the proxy from fetching old modules.
+func ServeDisablingFetch() *httptest.Server {
+	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		resp, err := proxyRequest(r)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		for k, vs := range resp.Header {
+			for _, v := range vs {
+				w.Header().Add(k, v)
+			}
+		}
+		w.WriteHeader(resp.StatusCode)
+		defer resp.Body.Close()
+		_, err = io.Copy(w, resp.Body)
+		if err != nil {
+			log.Errorf(r.Context(), err, "ServeDisablingFetch: io.Copy")
+		}
+	}))
+}
+
+func proxyRequest(r *http.Request) (*http.Response, error) {
+	url := "https://proxy.golang.org" + r.URL.Path
+	req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, url, nil)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set(DisableFetchHeader, "true")
+	return http.DefaultClient.Do(req)
+}
diff --git a/internal/proxy/server_test.go b/internal/proxy/server_test.go
new file mode 100644
index 0000000..edf6248
--- /dev/null
+++ b/internal/proxy/server_test.go
@@ -0,0 +1,55 @@
+// Copyright 2023 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 proxy
+
+import (
+	"bytes"
+	"io"
+	"net/http"
+	"net/url"
+	"testing"
+
+	test "golang.org/x/pkgsite-metrics/internal/testing"
+)
+
+// This module is currently not stored on the proxy.
+const missingModule = "code.haiziwang.com/golang/autil"
+
+func TestServer(t *testing.T) {
+	test.NeedsIntegrationEnv(t)
+	s := ServeDisablingFetch()
+	defer s.Close()
+	serverURL, err := url.Parse(s.URL)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	url := serverURL.JoinPath("github.com/pkg/errors/@v/list")
+	resp, err := http.Get(url.String())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if g, w := resp.StatusCode, 200; g != w {
+		t.Fatalf("status: got %d, want %d", g, w)
+	}
+	defer resp.Body.Close()
+	got, err := io.ReadAll(resp.Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+	want := "v0.8.1\n"
+	if !bytes.Contains(got, []byte(want)) {
+		t.Errorf("got body %q, missing %q", got, want)
+	}
+
+	url = serverURL.JoinPath(missingModule, "/@v/list")
+	resp, err = http.Get(url.String())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if g, w := resp.StatusCode, http.StatusNotFound; g != w {
+		t.Fatalf("status: got %d, want %d", g, w)
+	}
+}