gddo-server: move code related to pkgsite to pkgsite.go

Pure code in motion.

Change-Id: If883c14b0d7ca1ced8298980111e11cea9614510
Reviewed-on: https://go-review.googlesource.com/c/gddo/+/274696
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/gddo-server/main.go b/gddo-server/main.go
index f8f7b4b..47ca9e0 100644
--- a/gddo-server/main.go
+++ b/gddo-server/main.go
@@ -19,7 +19,6 @@
 	"io"
 	"log"
 	"net/http"
-	"net/url"
 	"os"
 	"path"
 	"regexp"
@@ -1020,15 +1019,6 @@
 	return s, nil
 }
 
-const (
-	pkgGoDevRedirectCookie = "pkggodev-redirect"
-	pkgGoDevRedirectParam  = "redirect"
-	pkgGoDevRedirectOn     = "on"
-	pkgGoDevRedirectOff    = "off"
-	pkgGoDevHost           = "pkg.go.dev"
-	teeproxyHost           = "teeproxy-dot-go-discovery.appspot.com"
-)
-
 type responseWriter struct {
 	http.ResponseWriter
 	status int
@@ -1065,54 +1055,6 @@
 	}
 }
 
-func makePkgGoDevRequest(r *http.Request, latency time.Duration, isRobot bool, status int) error {
-	event := newGDDOEvent(r, latency, isRobot, status)
-	b, err := json.Marshal(event)
-	if err != nil {
-		return fmt.Errorf("json.Marshal(%v): %v", event, err)
-	}
-
-	teeproxyURL := url.URL{Scheme: "https", Host: teeproxyHost}
-	if _, err := http.Post(teeproxyURL.String(), jsonMIMEType, bytes.NewReader(b)); err != nil {
-		return fmt.Errorf("http.Post(%q, %q, %v): %v", teeproxyURL.String(), jsonMIMEType, event, err)
-	}
-	log.Printf("makePkgGoDevRequest: request made to %q for %+v", teeproxyURL.String(), event)
-	return nil
-}
-
-type gddoEvent struct {
-	Host        string
-	Path        string
-	Status      int
-	URL         string
-	Header      http.Header
-	Latency     time.Duration
-	IsRobot     bool
-	UsePkgGoDev bool
-}
-
-func newGDDOEvent(r *http.Request, latency time.Duration, isRobot bool, status int) *gddoEvent {
-	targetURL := url.URL{
-		Scheme:   "https",
-		Host:     r.URL.Host,
-		Path:     r.URL.Path,
-		RawQuery: r.URL.RawQuery,
-	}
-	if targetURL.Host == "" && r.Host != "" {
-		targetURL.Host = r.Host
-	}
-	return &gddoEvent{
-		Host:        targetURL.Host,
-		Path:        r.URL.Path,
-		Status:      status,
-		URL:         targetURL.String(),
-		Header:      r.Header,
-		Latency:     latency,
-		IsRobot:     isRobot,
-		UsePkgGoDev: shouldRedirectToPkgGoDev(r),
-	}
-}
-
 func (s *server) logRequestStart(req *http.Request) {
 	if s.gceLogger == nil {
 		return
@@ -1138,92 +1080,6 @@
 	})
 }
 
-func userReturningFromPkgGoDev(req *http.Request) bool {
-	return req.FormValue("utm_source") == "backtogodoc"
-}
-
-func shouldRedirectToPkgGoDev(req *http.Request) bool {
-	// API requests are not redirected.
-	if strings.HasPrefix(req.URL.Host, "api") {
-		return false
-	}
-	redirectParam := req.FormValue(pkgGoDevRedirectParam)
-	if redirectParam == pkgGoDevRedirectOn || redirectParam == pkgGoDevRedirectOff {
-		return redirectParam == pkgGoDevRedirectOn
-	}
-	cookie, err := req.Cookie(pkgGoDevRedirectCookie)
-	return (err == nil && cookie.Value == pkgGoDevRedirectOn)
-}
-
-// 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.
-func pkgGoDevRedirectHandler(f func(http.ResponseWriter, *http.Request) error) func(http.ResponseWriter, *http.Request) error {
-	return func(w http.ResponseWriter, r *http.Request) error {
-		if userReturningFromPkgGoDev(r) {
-			return f(w, r)
-		}
-
-		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)
-		}
-
-		if !shouldRedirectToPkgGoDev(r) {
-			return f(w, r)
-		}
-
-		http.Redirect(w, r, pkgGoDevURL(r.URL).String(), http.StatusFound)
-		return nil
-	}
-}
-
-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
-	}
-
-	switch godocURL.Path {
-	case "/-/go":
-		u.Path = "/std"
-		q.Add("tab", "packages")
-	case "/-/about":
-		u.Path = "/about"
-	case "/":
-		if qparam := godocURL.Query().Get("q"); qparam != "" {
-			u.Path = "/search"
-			q.Set("q", qparam)
-		} else {
-			u.Path = "/"
-		}
-	default:
-		{
-			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")
-			} else {
-				q.Set("tab", "doc")
-			}
-		}
-	}
-
-	u.RawQuery = q.Encode()
-	return u
-}
-
 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 9267349..7c4bc78 100644
--- a/gddo-server/main_test.go
+++ b/gddo-server/main_test.go
@@ -7,14 +7,7 @@
 package main
 
 import (
-	"bufio"
-	"net/http"
-	"net/http/httptest"
-	"net/url"
-	"strings"
 	"testing"
-
-	"github.com/google/go-cmp/cmp"
 )
 
 var robotTests = []string{
@@ -40,416 +33,3 @@
 		}
 	}
 }
-
-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?tab=doc&utm_source=godoc",
-			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?tab=doc&utm_source=godoc",
-			wantSetCookieHeader: "",
-			wantStatusCode:      http.StatusFound,
-		},
-		{
-			name:           "do not redirect if user is returning from pkg.go.dev",
-			url:            "http://godoc.org/net/http?utm_source=backtogodoc",
-			cookie:         &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
-			wantStatusCode: http.StatusOK,
-		},
-	} {
-		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 %d; want %d", got, want)
-			}
-		})
-	}
-}
-
-func TestGodoc(t *testing.T) {
-	testCases := []struct {
-		from, to string
-	}{
-		{
-			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?tab=packages&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?tab=doc&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/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",
-		},
-	}
-
-	for _, tc := range testCases {
-		u, err := url.Parse(tc.from)
-		if err != nil {
-			t.Errorf("url.Parse(%q): %v", tc.from, err)
-			continue
-		}
-		to := pkgGoDevURL(u)
-		if got, want := to.String(), tc.to; got != want {
-			t.Errorf("pkgGoDevURL(%q) = %q; want %q", u, got, want)
-		}
-	}
-}
-
-func TestNewGDDOEvent(t *testing.T) {
-	for _, test := range []struct {
-		name   string
-		url    string
-		cookie *http.Cookie
-		want   *gddoEvent
-	}{
-		{
-			name: "home page request",
-			url:  "https://godoc.org",
-			want: &gddoEvent{
-				Host:        "godoc.org",
-				Path:        "",
-				UsePkgGoDev: false,
-			},
-		},
-		{
-			name:   "home page request with cookie on should redirect",
-			url:    "https://godoc.org",
-			cookie: &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
-			want: &gddoEvent{
-				Host:        "godoc.org",
-				Path:        "",
-				UsePkgGoDev: true,
-			},
-		},
-		{
-			name: "about page request",
-			url:  "https://godoc.org/-/about",
-			want: &gddoEvent{
-				Host:        "godoc.org",
-				Path:        "/-/about",
-				UsePkgGoDev: false,
-			},
-		},
-		{
-			name: "request with search query parameter",
-			url:  "https://godoc.org/?q=test",
-			want: &gddoEvent{
-				Host:        "godoc.org",
-				Path:        "/",
-				UsePkgGoDev: false,
-			},
-		},
-		{
-			name: "package page request",
-			url:  "https://godoc.org/net/http",
-			want: &gddoEvent{
-				Host:        "godoc.org",
-				Path:        "/net/http",
-				UsePkgGoDev: false,
-			},
-		},
-		{
-			name:   "package page request with wrong cookie on should not redirect",
-			url:    "https://godoc.org/net/http",
-			cookie: &http.Cookie{Name: "bogus-cookie", Value: "on"},
-			want: &gddoEvent{
-				Host:        "godoc.org",
-				Path:        "/net/http",
-				UsePkgGoDev: false,
-			},
-		},
-		{
-			name:   "package page request with query parameter off should not redirect",
-			url:    "https://godoc.org/net/http?redirect=off",
-			cookie: &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
-			want: &gddoEvent{
-				Host:        "godoc.org",
-				Path:        "/net/http",
-				UsePkgGoDev: false,
-			},
-		},
-		{
-			name:   "package page request with cookie on should redirect",
-			url:    "https://godoc.org/net/http",
-			cookie: &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
-			want: &gddoEvent{
-				Host:        "godoc.org",
-				Path:        "/net/http",
-				UsePkgGoDev: true,
-			},
-		},
-		{
-			name:   "package page request with query parameter on should redirect",
-			url:    "https://godoc.org/net/http?redirect=on",
-			cookie: &http.Cookie{Name: "pkggodev-redirect", Value: ""},
-			want: &gddoEvent{
-				Host:        "godoc.org",
-				Path:        "/net/http",
-				UsePkgGoDev: true,
-			},
-		},
-		{
-			name: "api request",
-			url:  "https://api.godoc.org/imports/net/http",
-			want: &gddoEvent{
-				Host:        "api.godoc.org",
-				Path:        "/imports/net/http",
-				UsePkgGoDev: false,
-			},
-		},
-		{
-			name:   "api requests should never redirect, even with cookie on",
-			url:    "https://api.godoc.org/imports/net/http",
-			cookie: &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
-			want: &gddoEvent{
-				Host:        "api.godoc.org",
-				Path:        "/imports/net/http",
-				UsePkgGoDev: false,
-			},
-		},
-		{
-			name:   "api requests should never redirect, even with query parameter on",
-			url:    "https://api.godoc.org/imports/net/http?redirect=on",
-			cookie: &http.Cookie{Name: "pkggodev-redirect", Value: ""},
-			want: &gddoEvent{
-				Host:        "api.godoc.org",
-				Path:        "/imports/net/http",
-				UsePkgGoDev: false,
-			},
-		},
-	} {
-		t.Run(test.name, func(t *testing.T) {
-			want := test.want
-			want.Latency = 100
-			want.URL = test.url
-			want.Header = http.Header{}
-			want.IsRobot = true
-			r := httptest.NewRequest("GET", test.url, nil)
-			if test.cookie != nil {
-				r.AddCookie(test.cookie)
-				want.Header.Add("Cookie", test.cookie.String())
-			}
-			got := newGDDOEvent(r, want.Latency, want.IsRobot, http.StatusOK)
-			want.Status = http.StatusOK
-			if diff := cmp.Diff(want, got); diff != "" {
-				t.Fatalf("mismatch (-want +got):\n%s", diff)
-			}
-		})
-	}
-}
-
-func TestNewGDDOEventFromRequests(t *testing.T) {
-	for _, test := range []struct {
-		name       string
-		requestURI string
-		host       string
-		want       *gddoEvent
-	}{
-		{
-			name:       "absolute index path",
-			requestURI: "https://godoc.org",
-			host:       "godoc.org",
-			want: &gddoEvent{
-				Host: "godoc.org",
-				Path: "",
-				URL:  "https://godoc.org",
-			},
-		},
-		{
-			name:       "absolute index path with trailing slash",
-			requestURI: "https://godoc.org/",
-			host:       "godoc.org",
-			want: &gddoEvent{
-				Host: "godoc.org",
-				Path: "/",
-				URL:  "https://godoc.org/",
-			},
-		},
-		{
-			name:       "relative index path",
-			requestURI: "/",
-			host:       "godoc.org",
-			want: &gddoEvent{
-				Host: "godoc.org",
-				Path: "/",
-				URL:  "https://godoc.org/",
-			},
-		},
-		{
-			name:       "absolute about path",
-			requestURI: "https://godoc.org/-/about",
-			host:       "godoc.org",
-			want: &gddoEvent{
-				Host: "godoc.org",
-				Path: "/-/about",
-				URL:  "https://godoc.org/-/about",
-			},
-		},
-		{
-			name:       "relative about path",
-			requestURI: "/-/about",
-			host:       "godoc.org",
-			want: &gddoEvent{
-				Host: "godoc.org",
-				Path: "/-/about",
-				URL:  "https://godoc.org/-/about",
-			},
-		},
-		{
-			name:       "absolute package path",
-			requestURI: "https://godoc.org/net/http",
-			host:       "godoc.org",
-			want: &gddoEvent{
-				Host: "godoc.org",
-				Path: "/net/http",
-				URL:  "https://godoc.org/net/http",
-			},
-		},
-		{
-			name:       "relative package path",
-			requestURI: "/net/http",
-			host:       "godoc.org",
-			want: &gddoEvent{
-				Host: "godoc.org",
-				Path: "/net/http",
-				URL:  "https://godoc.org/net/http",
-			},
-		},
-		{
-			name:       "absolute path with query parameters",
-			requestURI: "https://godoc.org/net/http?q=test",
-			host:       "godoc.org",
-			want: &gddoEvent{
-				Host: "godoc.org",
-				Path: "/net/http",
-				URL:  "https://godoc.org/net/http?q=test",
-			},
-		},
-		{
-			name:       "relative path with query parameters",
-			requestURI: "/net/http?q=test",
-			host:       "godoc.org",
-			want: &gddoEvent{
-				Host: "godoc.org",
-				Path: "/net/http",
-				URL:  "https://godoc.org/net/http?q=test",
-			},
-		},
-		{
-			name:       "absolute api path",
-			requestURI: "https://api.godoc.org/imports/net/http",
-			host:       "api.godoc.org",
-			want: &gddoEvent{
-				Host: "api.godoc.org",
-				Path: "/imports/net/http",
-				URL:  "https://api.godoc.org/imports/net/http",
-			},
-		},
-		{
-			name:       "relative api path",
-			requestURI: "/imports/net/http",
-			host:       "api.godoc.org",
-			want: &gddoEvent{
-				Host: "api.godoc.org",
-				Path: "/imports/net/http",
-				URL:  "https://api.godoc.org/imports/net/http",
-			},
-		},
-	} {
-		t.Run(test.name, func(t *testing.T) {
-			want := test.want
-			want.Latency = 100
-			want.Header = http.Header{}
-			want.IsRobot = false
-			requestLine := "GET " + test.requestURI + " HTTP/1.1\r\nHost: " + test.host + "\r\n\r\n"
-			req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(requestLine)))
-			if err != nil {
-				t.Fatal("invalid NewRequest arguments; " + err.Error())
-			}
-			got := newGDDOEvent(req, want.Latency, want.IsRobot, http.StatusOK)
-			want.Status = http.StatusOK
-			if diff := cmp.Diff(want, got); diff != "" {
-				t.Fatalf("mismatch (-want +got):\n%s", diff)
-			}
-		})
-	}
-}
diff --git a/gddo-server/pkgsite.go b/gddo-server/pkgsite.go
new file mode 100644
index 0000000..d5ead96
--- /dev/null
+++ b/gddo-server/pkgsite.go
@@ -0,0 +1,161 @@
+// 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 or at
+// https://developers.google.com/open-source/licenses/bsd.
+
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+)
+
+func makePkgGoDevRequest(r *http.Request, latency time.Duration, isRobot bool, status int) error {
+	event := newGDDOEvent(r, latency, isRobot, status)
+	b, err := json.Marshal(event)
+	if err != nil {
+		return fmt.Errorf("json.Marshal(%v): %v", event, err)
+	}
+
+	teeproxyURL := url.URL{Scheme: "https", Host: teeproxyHost}
+	if _, err := http.Post(teeproxyURL.String(), jsonMIMEType, bytes.NewReader(b)); err != nil {
+		return fmt.Errorf("http.Post(%q, %q, %v): %v", teeproxyURL.String(), jsonMIMEType, event, err)
+	}
+	log.Printf("makePkgGoDevRequest: request made to %q for %+v", teeproxyURL.String(), event)
+	return nil
+}
+
+type gddoEvent struct {
+	Host        string
+	Path        string
+	Status      int
+	URL         string
+	Header      http.Header
+	Latency     time.Duration
+	IsRobot     bool
+	UsePkgGoDev bool
+}
+
+func newGDDOEvent(r *http.Request, latency time.Duration, isRobot bool, status int) *gddoEvent {
+	targetURL := url.URL{
+		Scheme:   "https",
+		Host:     r.URL.Host,
+		Path:     r.URL.Path,
+		RawQuery: r.URL.RawQuery,
+	}
+	if targetURL.Host == "" && r.Host != "" {
+		targetURL.Host = r.Host
+	}
+	return &gddoEvent{
+		Host:        targetURL.Host,
+		Path:        r.URL.Path,
+		Status:      status,
+		URL:         targetURL.String(),
+		Header:      r.Header,
+		Latency:     latency,
+		IsRobot:     isRobot,
+		UsePkgGoDev: shouldRedirectToPkgGoDev(r),
+	}
+}
+
+func userReturningFromPkgGoDev(req *http.Request) bool {
+	return req.FormValue("utm_source") == "backtogodoc"
+}
+
+const (
+	pkgGoDevRedirectCookie = "pkggodev-redirect"
+	pkgGoDevRedirectParam  = "redirect"
+	pkgGoDevRedirectOn     = "on"
+	pkgGoDevRedirectOff    = "off"
+	pkgGoDevHost           = "pkg.go.dev"
+	teeproxyHost           = "teeproxy-dot-go-discovery.appspot.com"
+)
+
+func shouldRedirectToPkgGoDev(req *http.Request) bool {
+	// API requests are not redirected.
+	if strings.HasPrefix(req.URL.Host, "api") {
+		return false
+	}
+	redirectParam := req.FormValue(pkgGoDevRedirectParam)
+	if redirectParam == pkgGoDevRedirectOn || redirectParam == pkgGoDevRedirectOff {
+		return redirectParam == pkgGoDevRedirectOn
+	}
+	cookie, err := req.Cookie(pkgGoDevRedirectCookie)
+	return (err == nil && cookie.Value == pkgGoDevRedirectOn)
+}
+
+// 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.
+func pkgGoDevRedirectHandler(f func(http.ResponseWriter, *http.Request) error) func(http.ResponseWriter, *http.Request) error {
+	return func(w http.ResponseWriter, r *http.Request) error {
+		if userReturningFromPkgGoDev(r) {
+			return f(w, r)
+		}
+
+		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)
+		}
+
+		if !shouldRedirectToPkgGoDev(r) {
+			return f(w, r)
+		}
+
+		http.Redirect(w, r, pkgGoDevURL(r.URL).String(), http.StatusFound)
+		return nil
+	}
+}
+
+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
+	}
+
+	switch godocURL.Path {
+	case "/-/go":
+		u.Path = "/std"
+		q.Add("tab", "packages")
+	case "/-/about":
+		u.Path = "/about"
+	case "/":
+		if qparam := godocURL.Query().Get("q"); qparam != "" {
+			u.Path = "/search"
+			q.Set("q", qparam)
+		} else {
+			u.Path = "/"
+		}
+	default:
+		{
+			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")
+			} else {
+				q.Set("tab", "doc")
+			}
+		}
+	}
+
+	u.RawQuery = q.Encode()
+	return u
+}
diff --git a/gddo-server/pkgsite_test.go b/gddo-server/pkgsite_test.go
new file mode 100644
index 0000000..ba2394d
--- /dev/null
+++ b/gddo-server/pkgsite_test.go
@@ -0,0 +1,431 @@
+// 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 or at
+// https://developers.google.com/open-source/licenses/bsd.
+
+package main
+
+import (
+	"bufio"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+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?tab=doc&utm_source=godoc",
+			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?tab=doc&utm_source=godoc",
+			wantSetCookieHeader: "",
+			wantStatusCode:      http.StatusFound,
+		},
+		{
+			name:           "do not redirect if user is returning from pkg.go.dev",
+			url:            "http://godoc.org/net/http?utm_source=backtogodoc",
+			cookie:         &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
+			wantStatusCode: http.StatusOK,
+		},
+	} {
+		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 %d; want %d", got, want)
+			}
+		})
+	}
+}
+
+func TestGodoc(t *testing.T) {
+	testCases := []struct {
+		from, to string
+	}{
+		{
+			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?tab=packages&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?tab=doc&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/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",
+		},
+	}
+
+	for _, tc := range testCases {
+		u, err := url.Parse(tc.from)
+		if err != nil {
+			t.Errorf("url.Parse(%q): %v", tc.from, err)
+			continue
+		}
+		to := pkgGoDevURL(u)
+		if got, want := to.String(), tc.to; got != want {
+			t.Errorf("pkgGoDevURL(%q) = %q; want %q", u, got, want)
+		}
+	}
+}
+
+func TestNewGDDOEvent(t *testing.T) {
+	for _, test := range []struct {
+		name   string
+		url    string
+		cookie *http.Cookie
+		want   *gddoEvent
+	}{
+		{
+			name: "home page request",
+			url:  "https://godoc.org",
+			want: &gddoEvent{
+				Host:        "godoc.org",
+				Path:        "",
+				UsePkgGoDev: false,
+			},
+		},
+		{
+			name:   "home page request with cookie on should redirect",
+			url:    "https://godoc.org",
+			cookie: &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
+			want: &gddoEvent{
+				Host:        "godoc.org",
+				Path:        "",
+				UsePkgGoDev: true,
+			},
+		},
+		{
+			name: "about page request",
+			url:  "https://godoc.org/-/about",
+			want: &gddoEvent{
+				Host:        "godoc.org",
+				Path:        "/-/about",
+				UsePkgGoDev: false,
+			},
+		},
+		{
+			name: "request with search query parameter",
+			url:  "https://godoc.org/?q=test",
+			want: &gddoEvent{
+				Host:        "godoc.org",
+				Path:        "/",
+				UsePkgGoDev: false,
+			},
+		},
+		{
+			name: "package page request",
+			url:  "https://godoc.org/net/http",
+			want: &gddoEvent{
+				Host:        "godoc.org",
+				Path:        "/net/http",
+				UsePkgGoDev: false,
+			},
+		},
+		{
+			name:   "package page request with wrong cookie on should not redirect",
+			url:    "https://godoc.org/net/http",
+			cookie: &http.Cookie{Name: "bogus-cookie", Value: "on"},
+			want: &gddoEvent{
+				Host:        "godoc.org",
+				Path:        "/net/http",
+				UsePkgGoDev: false,
+			},
+		},
+		{
+			name:   "package page request with query parameter off should not redirect",
+			url:    "https://godoc.org/net/http?redirect=off",
+			cookie: &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
+			want: &gddoEvent{
+				Host:        "godoc.org",
+				Path:        "/net/http",
+				UsePkgGoDev: false,
+			},
+		},
+		{
+			name:   "package page request with cookie on should redirect",
+			url:    "https://godoc.org/net/http",
+			cookie: &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
+			want: &gddoEvent{
+				Host:        "godoc.org",
+				Path:        "/net/http",
+				UsePkgGoDev: true,
+			},
+		},
+		{
+			name:   "package page request with query parameter on should redirect",
+			url:    "https://godoc.org/net/http?redirect=on",
+			cookie: &http.Cookie{Name: "pkggodev-redirect", Value: ""},
+			want: &gddoEvent{
+				Host:        "godoc.org",
+				Path:        "/net/http",
+				UsePkgGoDev: true,
+			},
+		},
+		{
+			name: "api request",
+			url:  "https://api.godoc.org/imports/net/http",
+			want: &gddoEvent{
+				Host:        "api.godoc.org",
+				Path:        "/imports/net/http",
+				UsePkgGoDev: false,
+			},
+		},
+		{
+			name:   "api requests should never redirect, even with cookie on",
+			url:    "https://api.godoc.org/imports/net/http",
+			cookie: &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
+			want: &gddoEvent{
+				Host:        "api.godoc.org",
+				Path:        "/imports/net/http",
+				UsePkgGoDev: false,
+			},
+		},
+		{
+			name:   "api requests should never redirect, even with query parameter on",
+			url:    "https://api.godoc.org/imports/net/http?redirect=on",
+			cookie: &http.Cookie{Name: "pkggodev-redirect", Value: ""},
+			want: &gddoEvent{
+				Host:        "api.godoc.org",
+				Path:        "/imports/net/http",
+				UsePkgGoDev: false,
+			},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			want := test.want
+			want.Latency = 100
+			want.URL = test.url
+			want.Header = http.Header{}
+			want.IsRobot = true
+			r := httptest.NewRequest("GET", test.url, nil)
+			if test.cookie != nil {
+				r.AddCookie(test.cookie)
+				want.Header.Add("Cookie", test.cookie.String())
+			}
+			got := newGDDOEvent(r, want.Latency, want.IsRobot, http.StatusOK)
+			want.Status = http.StatusOK
+			if diff := cmp.Diff(want, got); diff != "" {
+				t.Fatalf("mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestNewGDDOEventFromRequests(t *testing.T) {
+	for _, test := range []struct {
+		name       string
+		requestURI string
+		host       string
+		want       *gddoEvent
+	}{
+		{
+			name:       "absolute index path",
+			requestURI: "https://godoc.org",
+			host:       "godoc.org",
+			want: &gddoEvent{
+				Host: "godoc.org",
+				Path: "",
+				URL:  "https://godoc.org",
+			},
+		},
+		{
+			name:       "absolute index path with trailing slash",
+			requestURI: "https://godoc.org/",
+			host:       "godoc.org",
+			want: &gddoEvent{
+				Host: "godoc.org",
+				Path: "/",
+				URL:  "https://godoc.org/",
+			},
+		},
+		{
+			name:       "relative index path",
+			requestURI: "/",
+			host:       "godoc.org",
+			want: &gddoEvent{
+				Host: "godoc.org",
+				Path: "/",
+				URL:  "https://godoc.org/",
+			},
+		},
+		{
+			name:       "absolute about path",
+			requestURI: "https://godoc.org/-/about",
+			host:       "godoc.org",
+			want: &gddoEvent{
+				Host: "godoc.org",
+				Path: "/-/about",
+				URL:  "https://godoc.org/-/about",
+			},
+		},
+		{
+			name:       "relative about path",
+			requestURI: "/-/about",
+			host:       "godoc.org",
+			want: &gddoEvent{
+				Host: "godoc.org",
+				Path: "/-/about",
+				URL:  "https://godoc.org/-/about",
+			},
+		},
+		{
+			name:       "absolute package path",
+			requestURI: "https://godoc.org/net/http",
+			host:       "godoc.org",
+			want: &gddoEvent{
+				Host: "godoc.org",
+				Path: "/net/http",
+				URL:  "https://godoc.org/net/http",
+			},
+		},
+		{
+			name:       "relative package path",
+			requestURI: "/net/http",
+			host:       "godoc.org",
+			want: &gddoEvent{
+				Host: "godoc.org",
+				Path: "/net/http",
+				URL:  "https://godoc.org/net/http",
+			},
+		},
+		{
+			name:       "absolute path with query parameters",
+			requestURI: "https://godoc.org/net/http?q=test",
+			host:       "godoc.org",
+			want: &gddoEvent{
+				Host: "godoc.org",
+				Path: "/net/http",
+				URL:  "https://godoc.org/net/http?q=test",
+			},
+		},
+		{
+			name:       "relative path with query parameters",
+			requestURI: "/net/http?q=test",
+			host:       "godoc.org",
+			want: &gddoEvent{
+				Host: "godoc.org",
+				Path: "/net/http",
+				URL:  "https://godoc.org/net/http?q=test",
+			},
+		},
+		{
+			name:       "absolute api path",
+			requestURI: "https://api.godoc.org/imports/net/http",
+			host:       "api.godoc.org",
+			want: &gddoEvent{
+				Host: "api.godoc.org",
+				Path: "/imports/net/http",
+				URL:  "https://api.godoc.org/imports/net/http",
+			},
+		},
+		{
+			name:       "relative api path",
+			requestURI: "/imports/net/http",
+			host:       "api.godoc.org",
+			want: &gddoEvent{
+				Host: "api.godoc.org",
+				Path: "/imports/net/http",
+				URL:  "https://api.godoc.org/imports/net/http",
+			},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			want := test.want
+			want.Latency = 100
+			want.Header = http.Header{}
+			want.IsRobot = false
+			requestLine := "GET " + test.requestURI + " HTTP/1.1\r\nHost: " + test.host + "\r\n\r\n"
+			req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(requestLine)))
+			if err != nil {
+				t.Fatal("invalid NewRequest arguments; " + err.Error())
+			}
+			got := newGDDOEvent(req, want.Latency, want.IsRobot, http.StatusOK)
+			want.Status = http.StatusOK
+			if diff := cmp.Diff(want, got); diff != "" {
+				t.Fatalf("mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}