cmd/golangorg: add support for module form of Go distributions

Add https://golang.org/toolchain serving appropriate meta tags
with mod redirect to https://go.dev/dl/mod.

Add https://go.dev/dl/mod/golang.org/toolchain/@v/ redirecting
to files in https://dl.google.com/go/.

Add https://go.dev/dl/mod/golang.org/toolchain/@v/list listing
stable toolchain versions.

For golang/go#57001.

Change-Id: Ib3283cab1d8ead0373ca7549f0e17ccba7cfaa22
Reviewed-on: https://go-review.googlesource.com/c/website/+/480840
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
Auto-Submit: Russ Cox <rsc@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/cmd/golangorg/server.go b/cmd/golangorg/server.go
index 239831a..488e838 100644
--- a/cmd/golangorg/server.go
+++ b/cmd/golangorg/server.go
@@ -222,6 +222,7 @@
 
 	// Note: Only golang.org/x/, no go.dev/x/.
 	mux.Handle("golang.org/x/", http.HandlerFunc(xHandler))
+	mux.Handle("golang.org/toolchain", http.HandlerFunc(toolchainHandler))
 
 	redirect.Register(mux)
 
@@ -573,8 +574,8 @@
 }
 
 var xTemplate = template.Must(template.New("x").Parse(`<!DOCTYPE html>
-<html>
-<head>
+<html lang="en">
+<title>The Go Programming Language</title>
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
 <meta name="go-import" content="golang.org/x/{{.Proj}} git https://go.googlesource.com/{{.Proj}}">
 <meta name="go-source" content="golang.org/x/{{.Proj}} https://github.com/golang/{{.Proj}}/ https://github.com/golang/{{.Proj}}/tree/master{/dir} https://github.com/golang/{{.Proj}}/blob/master{/dir}/{file}#L{line}">
@@ -586,6 +587,30 @@
 </html>
 `))
 
+func toolchainHandler(w http.ResponseWriter, r *http.Request) {
+	if r.URL.Path != "/toolchain" {
+		// Shouldn't happen if handler is registered correctly.
+		http.NotFound(w, r)
+		return
+	}
+	w.Write(toolchainPage)
+}
+
+var toolchainPage = []byte(`<!DOCTYPE html>
+<html lang="en">
+<head>
+<title>The Go Programming Language</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+<meta name="go-import" content="golang.org/toolchain mod https://go.dev/dl/mod">
+<meta http-equiv="refresh" content="0; url=https://go.dev/dl/">
+</head>
+<body>
+golang.org/toolchain is the module form of the Go toolchain releases.
+<a href="https://go.dev/dl/">Redirecting to Go toolchain download page...</a>
+</body>
+</html>
+`)
+
 var _ fs.ReadDirFS = unionFS{}
 
 // A unionFS is an FS presenting the union of the file systems in the slice.
diff --git a/cmd/golangorg/testdata/web.txt b/cmd/golangorg/testdata/web.txt
index 68bcfba..bdff81d 100644
--- a/cmd/golangorg/testdata/web.txt
+++ b/cmd/golangorg/testdata/web.txt
@@ -2,6 +2,11 @@
 code == 301
 redirect == https://go.dev/
 
+GET https://golang.org/toolchain
+code == 200
+body contains <meta name="go-import" content="golang.org/toolchain mod https://go.dev/dl/mod">
+body contains <meta http-equiv="refresh" content="0; url=https://go.dev/dl/">
+
 GET http://localhost:6060/
 redirect == /go.dev/
 
@@ -419,6 +424,24 @@
 body contains .windows-amd64.msi
 body !contains UA-
 
+GET https://go.dev/dl/go1.10.darwin-amd64.tar.gz
+redirect == https://dl.google.com/go/go1.10.darwin-amd64.tar.gz
+
+GET https://go.dev/dl/mod/golang.org/toolchain/@v/v0.0.1-go1.20.2.darwin-amd64.zip
+redirect == https://dl.google.com/go/v0.0.1-go1.20.2.darwin-amd64.zip
+
+GET https://go.dev/dl/mod/golang.org/toolchain/@v/v0.0.1-go1.20.2.darwin-amd64.mod
+redirect == https://dl.google.com/go/v0.0.1-go1.20.2.darwin-amd64.mod
+
+GET https://go.dev/dl/mod/golang.org/toolchain/@v/v0.0.1-go1.20.2.darwin-amd64.info
+redirect == https://dl.google.com/go/v0.0.1-go1.20.2.darwin-amd64.info
+
+GET https://go.dev/dl/mod/golang.org/toolchain/@v/list
+body ~ (?m)^v0\.0\.1-go1\.\d+\.\d+.darwin-arm64$
+body ~ (?m)^v0\.0\.1-go1\.\d+\.\d+.darwin-amd64$
+body ~ (?m)^v0\.0\.1-go1\.\d+\.\d+.linux-386$
+body ~ (?m)^v0\.0\.1-go1\.\d+\.\d+.windows-amd64$
+
 GET https://go.dev/ref
 redirect == /doc/#references
 
diff --git a/internal/dl/server.go b/internal/dl/server.go
index 101fc4b..8dc1c7d 100644
--- a/internal/dl/server.go
+++ b/internal/dl/server.go
@@ -42,6 +42,8 @@
 	s := server{site, dc, gob}
 	mux.HandleFunc(host+"/dl", s.getHandler)
 	mux.HandleFunc(host+"/dl/", s.getHandler) // also serves listHandler
+	mux.HandleFunc(host+"/dl/mod/golang.org/toolchain/@v/", s.toolchainRedirect)
+	mux.HandleFunc(host+"/dl/mod/golang.org/toolchain/@v/list", s.toolchainList)
 	mux.HandleFunc(host+"/dl/upload", s.uploadHandler)
 
 	// NOTE(cbro): this only needs to be run once per project,
@@ -78,6 +80,38 @@
 	})
 }
 
+// toolchainList serves the toolchain module version list.
+// We only list the stable releases, even though older releases are available as well.
+func (h server) toolchainList(w http.ResponseWriter, r *http.Request) {
+	d, err := h.listData(r.Context())
+	if err != nil {
+		log.Printf("ERROR listing downloads: %v", err)
+		http.Error(w, "Could not get module list. Try again in a few minutes.", 500)
+		return
+	}
+
+	var buf bytes.Buffer
+	for _, r := range d.Stable {
+		for _, f := range r.Files {
+			if f.Kind != "archive" {
+				continue
+			}
+			buf.WriteString("v0.0.1-")
+			buf.WriteString(f.Version)
+			buf.WriteString(".")
+			buf.WriteString(f.OS)
+			buf.WriteString("-")
+			arch := f.Arch
+			if arch == "armv6l" {
+				arch = "arm"
+			}
+			buf.WriteString(arch)
+			buf.WriteString("\n")
+		}
+	}
+	w.Write(buf.Bytes())
+}
+
 // dl.gob was generated 2021-11-08 from the live server data, for offline testing.
 //
 //go:embed dl.gob
@@ -254,6 +288,22 @@
 `, html.EscapeString(redirectURL), html.EscapeString(redirectURL))
 }
 
+// toolchainRedirect redirects /dl/mod/golang.org/toolchain/@v/v___ to https://dl.google.com/go/v___.
+func (server) toolchainRedirect(w http.ResponseWriter, r *http.Request) {
+	if r.Method != "GET" && r.Method != "HEAD" && r.Method != "OPTIONS" {
+		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+		return
+	}
+
+	_, file, _ := strings.Cut(r.URL.Path, "/@v/")
+	if (!strings.HasPrefix(file, "v0.") && !strings.HasPrefix(file, "v1.")) || strings.Contains(file, "/") {
+		http.NotFound(w, r)
+		return
+	}
+
+	http.Redirect(w, r, "https://dl.google.com/go/"+file, http.StatusFound)
+}
+
 func (h server) initHandler(w http.ResponseWriter, r *http.Request) {
 	var fileRoot struct {
 		Root string