gddo: add option to redirect requests to pkg.go.dev

Updates golang/go#37099

Change-Id: Ibaf46b9182d761d56d8dc6770259ab4bc3703d59
Reviewed-on: https://go-review.googlesource.com/c/gddo/+/218697
Reviewed-by: Andrew Bonventre <andybons@golang.org>
diff --git a/gddo-server/main.go b/gddo-server/main.go
index 52186dc..ea34868 100644
--- a/gddo-server/main.go
+++ b/gddo-server/main.go
@@ -19,6 +19,7 @@
 	"io"
 	"log"
 	"net/http"
+	"net/url"
 	"os"
 	"path"
 	"regexp"
@@ -926,9 +927,10 @@
 			trustProxyHeaders: v.GetBool(ConfigTrustProxyHeaders),
 		}
 	}
-	mux.Handle("/-/about", handler(s.serveAbout))
+
+	mux.Handle("/-/about", handler(pkgGoDevRedirectHandler(s.serveAbout)))
 	mux.Handle("/-/bot", handler(s.serveBot))
-	mux.Handle("/-/go", handler(s.serveGoIndex))
+	mux.Handle("/-/go", handler(pkgGoDevRedirectHandler(s.serveGoIndex)))
 	mux.Handle("/-/subrepo", handler(s.serveGoSubrepoIndex))
 	mux.Handle("/-/refresh", handler(s.serveRefresh))
 	mux.Handle("/about", http.RedirectHandler("/-/about", http.StatusMovedPermanently))
@@ -939,7 +941,7 @@
 	mux.Handle("/BingSiteAuth.xml", staticServer.FileHandler("BingSiteAuth.xml"))
 	mux.Handle("/C", http.RedirectHandler("http://golang.org/doc/articles/c_go_cgo.html", http.StatusMovedPermanently))
 	mux.Handle("/code.jquery.com/", http.NotFoundHandler())
-	mux.Handle("/", handler(s.serveHome))
+	mux.Handle("/", handler(pkgGoDevRedirectHandler(s.serveHome)))
 
 	ahMux := http.NewServeMux()
 	ready := new(health.Handler)
@@ -1017,6 +1019,64 @@
 	})
 }
 
+const (
+	pkgGoDevRedirectCookie = "pkggodev-redirect"
+	pkgGoDevRedirectParam  = "redirect"
+	pkgGoDevRedirectOn     = "on"
+	pkgGoDevRedirectOff    = "off"
+	pkgGoDevHost           = "pkg.go.dev"
+)
+
+var gddoToPkgGoDevRequest = map[string]string{
+	"/-/about": "/about",
+	"/-/go":    "/std",
+}
+
+// pkgGoDevRedirectHandler redirects requests from godoc.org to pkg.go.dev,
+// based on whether a cookie is set for pkggodev-redirect. The cookie
+// can be turned on/off using a query param. It determines which path to
+// direct to by checking if a path is mapped in gddoToPkgGoDevRequest, and
+// if not redirecting to the same path that was used for the godoc.org request.
+func pkgGoDevRedirectHandler(f func(http.ResponseWriter, *http.Request) error) func(http.ResponseWriter, *http.Request) error {
+	return func(w http.ResponseWriter, r *http.Request) error {
+		redirectParam := r.FormValue(pkgGoDevRedirectParam)
+
+		if redirectParam == pkgGoDevRedirectOn {
+			cookie := &http.Cookie{Name: pkgGoDevRedirectCookie, Value: redirectParam, Path: "/"}
+			http.SetCookie(w, cookie)
+		}
+		if redirectParam == pkgGoDevRedirectOff {
+			cookie := &http.Cookie{Name: pkgGoDevRedirectCookie, Value: "", MaxAge: -1, Path: "/"}
+			http.SetCookie(w, cookie)
+		}
+
+		var shouldRedirect bool
+		if redirectParam == pkgGoDevRedirectOn || redirectParam == pkgGoDevRedirectOff {
+			shouldRedirect = redirectParam == pkgGoDevRedirectOn
+		} else {
+			for _, v := range r.Cookies() {
+				if v.Name == pkgGoDevRedirectCookie {
+					shouldRedirect = v.Value == pkgGoDevRedirectOn
+					break
+				}
+			}
+		}
+
+		if !shouldRedirect {
+			return f(w, r)
+		}
+
+		path, ok := gddoToPkgGoDevRequest[r.URL.Path]
+		if !ok {
+			path = r.URL.Path
+		}
+
+		nextUrl := url.URL{Scheme: "https", Host: pkgGoDevHost, Path: path}
+		http.Redirect(w, r, nextUrl.String(), http.StatusFound)
+		return nil
+	}
+}
+
 func main() {
 	ctx := context.Background()
 	v, err := loadConfig(ctx, os.Args)
diff --git a/gddo-server/main_test.go b/gddo-server/main_test.go
index 7c4bc78..8dddb8e 100644
--- a/gddo-server/main_test.go
+++ b/gddo-server/main_test.go
@@ -7,6 +7,8 @@
 package main
 
 import (
+	"net/http"
+	"net/http/httptest"
 	"testing"
 )
 
@@ -33,3 +35,79 @@
 		}
 	}
 }
+
+func TestHandlePkgGoDevRedirect(t *testing.T) {
+	handler := pkgGoDevRedirectHandler(func(w http.ResponseWriter, r *http.Request) error {
+		return nil
+	})
+
+	for _, test := range []struct {
+		name, url, wantLocationHeader, wantSetCookieHeader string
+		wantStatusCode                                     int
+		cookie                                             *http.Cookie
+	}{
+		{
+			name:                "test pkggodev-redirect param is on",
+			url:                 "http://godoc.org/net/http?redirect=on",
+			wantLocationHeader:  "https://pkg.go.dev/net/http",
+			wantSetCookieHeader: "pkggodev-redirect=on; Path=/",
+			wantStatusCode:      http.StatusFound,
+		},
+		{
+			name:                "test pkggodev-redirect param is off",
+			url:                 "http://godoc.org/net/http?redirect=off",
+			wantLocationHeader:  "",
+			wantSetCookieHeader: "pkggodev-redirect=; Path=/; Max-Age=0",
+			wantStatusCode:      http.StatusOK,
+		},
+		{
+			name:                "test pkggodev-redirect param is unset",
+			url:                 "http://godoc.org/net/http",
+			wantLocationHeader:  "",
+			wantSetCookieHeader: "",
+			wantStatusCode:      http.StatusOK,
+		},
+		{
+			name:                "toggle enabled pkggodev-redirect cookie",
+			url:                 "http://godoc.org/net/http?redirect=off",
+			cookie:              &http.Cookie{Name: "pkggodev-redirect", Value: "true"},
+			wantLocationHeader:  "",
+			wantSetCookieHeader: "pkggodev-redirect=; Path=/; Max-Age=0",
+			wantStatusCode:      http.StatusOK,
+		},
+		{
+			name:                "pkggodev-redirect enabled cookie should redirect",
+			url:                 "http://godoc.org/net/http",
+			cookie:              &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
+			wantLocationHeader:  "https://pkg.go.dev/net/http",
+			wantSetCookieHeader: "",
+			wantStatusCode:      http.StatusFound,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			req := httptest.NewRequest("GET", test.url, nil)
+			if test.cookie != nil {
+				req.AddCookie(test.cookie)
+			}
+
+			w := httptest.NewRecorder()
+			err := handler(w, req)
+			if err != nil {
+				t.Fatal(err)
+			}
+			resp := w.Result()
+
+			if got, want := resp.Header.Get("Location"), test.wantLocationHeader; got != want {
+				t.Errorf("Location header mismatch: got %q; want %q", got, want)
+			}
+
+			if got, want := resp.Header.Get("Set-Cookie"), test.wantSetCookieHeader; got != want {
+				t.Errorf("Set-Cookie header mismatch: got %q; want %q", got, want)
+			}
+
+			if got, want := resp.StatusCode, test.wantStatusCode; got != want {
+				t.Errorf("Status code mismatch: got %q; want %q", got, want)
+			}
+		})
+	}
+}