internal/frontend: copy pkgsite redirect logic from gddo

Code in motion. Copies pkgsite redirect from github.com/golang/gddo
and adds it as a middleware to pkgsite requests. Moving this redirect
to the pkgsite service will allow us to turn down the legacy godoc-org
project. After this CL is merged, the next step is to transfer the
godoc.org domain to pkgsite GCP project.

Change-Id: I322e9e6332dc5ee6dbbdf1e640ab3fdde5f15caa
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/425000
Run-TryBot: Jamal Carvalho <jamal@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
Reviewed-by: Julie Qiu <julieqiu@google.com>
diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go
index d197198..82db064 100644
--- a/cmd/frontend/main.go
+++ b/cmd/frontend/main.go
@@ -181,6 +181,7 @@
 		middleware.RequestLog(cmdconfig.Logger(ctx, cfg, "frontend-log")),
 		middleware.AcceptRequests(http.MethodGet, http.MethodPost, http.MethodHead), // accept only GETs, POSTs and HEADs
 		middleware.BetaPkgGoDevRedirect(),
+		middleware.GodocOrgRedirect(),
 		middleware.Quota(cfg.Quota, cacheClient),
 		middleware.SecureHeaders(!*disableCSP), // must come before any caching for nonces to work
 		middleware.Experiment(experimenter),
diff --git a/internal/middleware/godocredirect.go b/internal/middleware/godocredirect.go
new file mode 100644
index 0000000..d8fd23c
--- /dev/null
+++ b/internal/middleware/godocredirect.go
@@ -0,0 +1,103 @@
+// Copyright 2020 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 middleware
+
+import (
+	"net/http"
+	"net/url"
+	"strings"
+
+	"golang.org/x/mod/module"
+)
+
+const (
+	goGithubRepoURLPath = "/github.com/golang/go"
+	pkgGoDevHost        = "pkg.go.dev"
+)
+
+// GodocOrgRedirect redirects requests from godoc.org to pkg.go.dev.
+func GodocOrgRedirect() Middleware {
+	return func(h http.Handler) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			if !strings.HasSuffix(r.Host, "godoc.org") {
+				h.ServeHTTP(w, r)
+				return
+			}
+			http.Redirect(w, r, pkgGoDevURL(r.URL).String(), http.StatusMovedPermanently)
+		})
+	}
+}
+
+func pkgGoDevURL(godocURL *url.URL) *url.URL {
+	u := &url.URL{Scheme: "https", Host: pkgGoDevHost}
+	q := url.Values{"utm_source": []string{"godoc"}}
+
+	if strings.Contains(godocURL.Path, "/vendor/") || strings.HasSuffix(godocURL.Path, "/vendor") {
+		u.Path = "/"
+		u.RawQuery = q.Encode()
+		return u
+	}
+
+	if strings.HasPrefix(godocURL.Path, goGithubRepoURLPath) ||
+		strings.HasPrefix(godocURL.Path, goGithubRepoURLPath+"/src") {
+		u.Path = strings.TrimPrefix(strings.TrimPrefix(godocURL.Path, goGithubRepoURLPath), "/src")
+		if u.Path == "" {
+			u.Path = "/std"
+		}
+		u.RawQuery = q.Encode()
+		return u
+	}
+
+	_, isSVG := godocURL.Query()["status.svg"]
+	_, isPNG := godocURL.Query()["status.png"]
+	if isSVG || isPNG {
+		u.Path = "/badge" + godocURL.Path
+		u.RawQuery = q.Encode()
+		return u
+	}
+
+	switch godocURL.Path {
+	case "/-/go":
+		u.Path = "/std"
+	case "/-/about":
+		u.Path = "/about"
+	case "/C":
+		u.Path = "/C"
+	case "/":
+		if qparam := godocURL.Query().Get("q"); qparam != "" {
+			u.Path = "/search"
+			q.Set("q", qparam)
+		} else {
+			u.Path = "/"
+		}
+	case "":
+		u.Path = ""
+	case "/-/subrepo":
+		u.Path = "/search"
+		q.Set("q", "golang.org/x")
+	default:
+		{
+			godocURL.Path = strings.TrimSuffix(godocURL.Path, "/")
+			// If the import path is invalid, redirect to
+			// https://golang.org/issue/43036, so that the users has more context
+			// on why this path does not work on pkg.go.dev.
+			if err := module.CheckImportPath(strings.TrimPrefix(godocURL.Path, "/")); err != nil && strings.Contains(err.Error(), "invalid char") {
+				u.Host = "golang.org"
+				u.Path = "/issue/43036"
+				return u
+			}
+
+			u.Path = godocURL.Path
+			if _, ok := godocURL.Query()["imports"]; ok {
+				q.Set("tab", "imports")
+			} else if _, ok := godocURL.Query()["importers"]; ok {
+				q.Set("tab", "importedby")
+			}
+		}
+	}
+
+	u.RawQuery = q.Encode()
+	return u
+}
diff --git a/internal/middleware/godocredirect_test.go b/internal/middleware/godocredirect_test.go
new file mode 100644
index 0000000..9653639
--- /dev/null
+++ b/internal/middleware/godocredirect_test.go
@@ -0,0 +1,107 @@
+// Copyright 2022 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 middleware
+
+import (
+	"net/url"
+	"strings"
+	"testing"
+)
+
+func TestPkgGoDevURL(t *testing.T) {
+	testCases := []struct {
+		from, to string
+	}{
+		{
+			from: "https://godoc.org",
+			to:   "https://pkg.go.dev?utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/-/about",
+			to:   "https://pkg.go.dev/about?utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/-/go",
+			to:   "https://pkg.go.dev/std?utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/-/subrepo",
+			to:   "https://pkg.go.dev/search?q=golang.org%2Fx&utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/C",
+			to:   "https://pkg.go.dev/C?utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/?q=foo",
+			to:   "https://pkg.go.dev/search?q=foo&utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/cloud.google.com/go/storage",
+			to:   "https://pkg.go.dev/cloud.google.com/go/storage?utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/cloud.google.com/go/storage?imports",
+			to:   "https://pkg.go.dev/cloud.google.com/go/storage?tab=imports&utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/cloud.google.com/go/storage?importers",
+			to:   "https://pkg.go.dev/cloud.google.com/go/storage?tab=importedby&utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/cloud.google.com/go/storage?status.svg",
+			to:   "https://pkg.go.dev/badge/cloud.google.com/go/storage?utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/cloud.google.com/go/storage?status.png",
+			to:   "https://pkg.go.dev/badge/cloud.google.com/go/storage?utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/github.com/golang/go",
+			to:   "https://pkg.go.dev/std?utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/github.com/golang/go/src",
+			to:   "https://pkg.go.dev/std?utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/github.com/golang/go/src/cmd/vet",
+			to:   "https://pkg.go.dev/cmd/vet?utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/golang.org/x/vgo/vendor/cmd/go/internal/modfile",
+			to:   "https://pkg.go.dev/?utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/golang.org/x/vgo/vendor",
+			to:   "https://pkg.go.dev/?utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/cryptoscope.co/go/specialκ",
+			to:   "https://golang.org/issue/43036",
+		},
+		{
+			from: "https://godoc.org/github.com/badimportpath//doubleslash",
+			to:   "https://pkg.go.dev/github.com/badimportpath//doubleslash?utm_source=godoc",
+		},
+		{
+			from: "https://godoc.org/github.com/google/go-containerregistry/",
+			to:   "https://pkg.go.dev/github.com/google/go-containerregistry?utm_source=godoc",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(strings.ReplaceAll(tc.from, "/", " "), func(t *testing.T) {
+			u, err := url.Parse(tc.from)
+			if err != nil {
+				t.Fatalf("url.Parse(%q): %v", tc.from, err)
+			}
+			to := pkgGoDevURL(u)
+			if got, want := to.String(), tc.to; got != want {
+				t.Errorf("pkgGoDevURL(%q) = %q; want %q", u, got, want)
+			}
+		})
+	}
+}