gddo-server: redirect based on config

shouldRedirectRequest now uses data from the DynamicConfig to decide
whether a request should be redirected.

Change-Id: I69a3d4df275248003b170c0e48ca6959cad93603
Reviewed-on: https://go-review.googlesource.com/c/gddo/+/285933
Trust: Julie Qiu <julie@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/gddo-server/dynconfig/dynconfig.go b/gddo-server/dynconfig/dynconfig.go
index d75f8bc..5207164 100644
--- a/gddo-server/dynconfig/dynconfig.go
+++ b/gddo-server/dynconfig/dynconfig.go
@@ -26,6 +26,10 @@
 type DynamicConfig struct {
 	// Fields can be added at any time, but removing or changing a field
 	// requires careful coordination with the config file contents.
+	RedirectBadges   bool
+	RedirectHomepage bool
+	RedirectSearch   bool
+	RedirectPaths    []string
 }
 
 // Read reads dynamic configuration from the given location.
diff --git a/gddo-server/pkgsite.go b/gddo-server/pkgsite.go
index 5c883f8..162d6e1 100644
--- a/gddo-server/pkgsite.go
+++ b/gddo-server/pkgsite.go
@@ -17,6 +17,8 @@
 	"strings"
 	"time"
 
+	"github.com/golang/gddo/gddo-server/dynconfig"
+	"github.com/golang/gddo/gddo-server/poller"
 	"golang.org/x/mod/module"
 	"golang.org/x/net/context/ctxhttp"
 )
@@ -185,11 +187,14 @@
 // 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 {
+func pkgGoDevRedirectHandler(configPoller *poller.Poller, f func(http.ResponseWriter, *http.Request) error) func(http.ResponseWriter, *http.Request) error {
 	return func(w http.ResponseWriter, r *http.Request) error {
-		if shouldRedirectRequest(r) {
-			http.Redirect(w, r, pkgGoDevURL(r.URL).String(), http.StatusFound)
-			return nil
+		redirectURL := pkgGoDevURL(r.URL).String()
+		if configPoller != nil {
+			if shouldRedirectURL(r, configPoller) {
+				http.Redirect(w, r, redirectURL, http.StatusFound)
+				return nil
+			}
 		}
 
 		if userReturningFromPkgGoDev(r) {
@@ -207,32 +212,50 @@
 		if !shouldRedirectToPkgGoDev(r) {
 			return f(w, r)
 		}
-		http.Redirect(w, r, pkgGoDevURL(r.URL).String(), http.StatusFound)
+		http.Redirect(w, r, redirectURL, http.StatusFound)
 		return nil
 	}
 }
 
-// alwaysRedirectPaths is a set of paths that are always redirected to
-// pkg.go.dev.
-var alwaysRedirectPaths = map[string]bool{
-	"/-/about": true,
+// shouldRedirectURL reports whether a request to the given URL should be
+// redirected to pkg.go.dev.
+func shouldRedirectURL(r *http.Request, poller *poller.Poller) bool {
+	cfg := poller.Current().(*dynconfig.DynamicConfig)
+	return shouldRedirectURLForSnapshot(r, cfg)
 }
 
-func shouldRedirectRequest(r *http.Request) bool {
+func shouldRedirectURLForSnapshot(r *http.Request, cfg *dynconfig.DynamicConfig) bool {
+	if cfg == nil {
+		log.Printf("shouldRedirectURLForSnapshot(%q): cfg is nil", r.URL.Path)
+		return false
+	}
+
 	// Requests to api.godoc.org and talks.godoc.org are not redirected.
 	if strings.HasPrefix(r.URL.Host, "api") || strings.HasPrefix(r.URL.Host, "talks") {
 		return false
 	}
-	// Badge SVGs will be redirected last.
+
 	_, isSVG := r.URL.Query()["status.svg"]
 	_, isPNG := r.URL.Query()["status.png"]
 	if isSVG || isPNG {
+		return cfg.RedirectBadges
+	}
+	for _, p := range cfg.RedirectPaths {
+		if r.URL.Path == p {
+			return true
+		}
+	}
+	q := strings.TrimSpace(r.Form.Get("q"))
+	if r.URL.Path == "/" || r.URL.Path == "" {
+		if q == "" && cfg.RedirectHomepage {
+			return true
+		}
+		if q != "" && cfg.RedirectSearch {
+			return true
+		}
 		return false
 	}
 
-	if alwaysRedirectPaths[r.URL.Path] {
-		return true
-	}
 	// TODO: redirect based on rollout percentage.
 	return false
 }
diff --git a/gddo-server/pkgsite_test.go b/gddo-server/pkgsite_test.go
index 18ffe08..34ad51b 100644
--- a/gddo-server/pkgsite_test.go
+++ b/gddo-server/pkgsite_test.go
@@ -14,6 +14,7 @@
 	"strings"
 	"testing"
 
+	"github.com/golang/gddo/gddo-server/dynconfig"
 	"github.com/google/go-cmp/cmp"
 )
 
@@ -66,19 +67,13 @@
 			cookie:         &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
 			wantStatusCode: http.StatusOK,
 		},
-		{
-			name:               "always redirect /-/about",
-			url:                "http://godoc.org/-/about",
-			wantLocationHeader: "https://pkg.go.dev/about?utm_source=godoc",
-			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)
 			}
-			handler := pkgGoDevRedirectHandler(func(w http.ResponseWriter, r *http.Request) error {
+			handler := pkgGoDevRedirectHandler(nil, func(w http.ResponseWriter, r *http.Request) error {
 				return nil
 			})
 
@@ -498,3 +493,74 @@
 		}
 	}
 }
+
+func TestShouldRedirectURLForSnapshot(t *testing.T) {
+	var testRequests []*http.Request
+	for _, tu := range []string{
+		"https://api.godoc.org/-/about",
+		"https://api.godoc.org/?q=http",
+		"https://api.godoc.org/-/net/http",
+		"https://godoc.org",
+		"https://godoc.org/-/about",
+		"https://godoc.org/-/bot",
+		"https://godoc.org/-/go",
+		"https://godoc.org/-/subrepo",
+		"https://godoc.org/?q=http",
+		"https://godoc.org/net/http",
+		"https://godoc.org/cloud.google.com/go",
+	} {
+		u, err := url.Parse(tu)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req := httptest.NewRequest("GET", u.String(), nil)
+		req.URL = u
+		req.Form = u.Query()
+		testRequests = append(testRequests, req)
+	}
+
+	for _, test := range []struct {
+		name     string
+		snapshot *dynconfig.DynamicConfig
+		redirect map[string]bool
+	}{
+		{
+			name: "redirect homepage",
+			snapshot: &dynconfig.DynamicConfig{
+				RedirectHomepage: true,
+			},
+			redirect: map[string]bool{"https://godoc.org": true},
+		},
+		{
+			name: "redirect search page",
+			snapshot: &dynconfig.DynamicConfig{
+				RedirectSearch: true,
+			},
+			redirect: map[string]bool{"https://godoc.org/?q=http": true},
+		},
+		{
+			name: "redirect static page",
+			snapshot: &dynconfig.DynamicConfig{
+				RedirectPaths: []string{"/-/about", "/-/subrepo", "/-/go"},
+			},
+			redirect: map[string]bool{
+				"https://godoc.org/-/about":   true,
+				"https://godoc.org/-/go":      true,
+				"https://godoc.org/-/subrepo": true,
+			},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			for _, req := range testRequests {
+				u := req.URL.String()
+				got := shouldRedirectURLForSnapshot(req, test.snapshot)
+				if test.redirect[u] && !got {
+					t.Errorf("%q should redirect but didn't", u)
+				}
+				if !test.redirect[u] && got {
+					t.Errorf("%q should not redirect and did", u)
+				}
+			}
+		})
+	}
+}
diff --git a/gddo-server/server.go b/gddo-server/server.go
index fdf5ce4..40c1954 100644
--- a/gddo-server/server.go
+++ b/gddo-server/server.go
@@ -557,11 +557,9 @@
 		if err != nil {
 			return err
 		}
-
 		return s.templates.execute(resp, "home"+templateExt(req), http.StatusOK, nil,
 			map[string]interface{}{
-				"Popular": pkgs,
-
+				"Popular":                   pkgs,
 				"showPkgGoDevRedirectToast": userReturningFromPkgGoDev(req),
 			})
 	}
@@ -607,8 +605,7 @@
 func (s *server) serveAbout(resp http.ResponseWriter, req *http.Request) error {
 	return s.templates.execute(resp, "about.html", http.StatusOK, nil,
 		map[string]interface{}{
-			"Host": req.Host,
-
+			"Host":                      req.Host,
 			"showPkgGoDevRedirectToast": userReturningFromPkgGoDev(req),
 		})
 }
@@ -886,7 +883,7 @@
 		httpClient:     newHTTPClient(v),
 		importGraphSem: make(chan struct{}, 10),
 	}
-	cfg, err := newConfigPoller(ctx, 1*time.Millisecond)
+	cfg, err := newConfigPoller(ctx, 1*time.Minute)
 	if err != nil {
 		log.Printf("newConfigPoller: %q", err)
 	}
@@ -965,10 +962,10 @@
 		}
 	}
 
-	mux.Handle("/-/about", handler(pkgGoDevRedirectHandler(s.serveAbout)))
-	mux.Handle("/-/bot", handler(pkgGoDevRedirectHandler(s.serveBot)))
-	mux.Handle("/-/go", handler(pkgGoDevRedirectHandler(s.serveGoIndex)))
-	mux.Handle("/-/subrepo", handler(pkgGoDevRedirectHandler(s.serveGoSubrepoIndex)))
+	mux.Handle("/-/about", handler(pkgGoDevRedirectHandler(s.configPoller, s.serveAbout)))
+	mux.Handle("/-/bot", handler(pkgGoDevRedirectHandler(s.configPoller, s.serveBot)))
+	mux.Handle("/-/go", handler(pkgGoDevRedirectHandler(s.configPoller, s.serveGoIndex)))
+	mux.Handle("/-/subrepo", handler(pkgGoDevRedirectHandler(s.configPoller, s.serveGoSubrepoIndex)))
 	mux.Handle("/-/refresh", handler(s.serveRefresh))
 	mux.Handle("/about", http.RedirectHandler("/-/about", http.StatusMovedPermanently))
 	mux.Handle("/favicon.ico", staticServer.FileHandler("favicon.ico"))
@@ -978,7 +975,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(pkgGoDevRedirectHandler(s.serveHome)))
+	mux.Handle("/", handler(pkgGoDevRedirectHandler(s.configPoller, s.serveHome)))
 
 	ahMux := http.NewServeMux()
 	ready := new(health.Handler)
@@ -1080,7 +1077,7 @@
 }
 
 func (s *server) teeRequestToPkgGoDev(r *http.Request, latency time.Duration, status int) {
-	if shouldRedirectRequest(r) {
+	if shouldRedirectURL(r, s.configPoller) {
 		log.Printf("shouldRedirectToPkgGoDev(%q, %q)= true: not teeing request because it is redirected to pkg.go.dev", r.URL.Host, r.URL.Path)
 		return
 	}