blob: e41c86628cf7a15a09d1dafb1de1431d10adc325 [file] [log] [blame]
// 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 (
"net/http"
"net/url"
"path/filepath"
"strings"
"time"
"golang.org/x/net/context/ctxhttp"
)
type pkggodevEvent struct {
Host string
Path string
Status int
URL string
Latency time.Duration
Error error
}
func teeRequestToPkgGoDev(godocReq *http.Request, latency time.Duration, isRobot bool, status int) (gddoEvent *gddoEvent, pkgEvent *pkggodevEvent) {
gddoEvent = newGDDOEvent(godocReq, latency, isRobot, status)
u := pkgGoDevURL(godocReq.URL)
// Strip the utm_source from the URL.
vals := u.Query()
vals.Del("utm_source")
u.RawQuery = vals.Encode()
pkgEvent = &pkggodevEvent{
Host: u.Host,
Path: u.Path,
URL: u.String(),
}
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
pkgEvent.Status = http.StatusInternalServerError
pkgEvent.Error = err
return gddoEvent, pkgEvent
}
start := time.Now()
xfwd := req.Header.Get("X-Forwarded-for")
req.Header.Set("X-Godoc-Forwarded-for", xfwd)
resp, err := ctxhttp.Do(godocReq.Context(), http.DefaultClient, req)
if err != nil {
// Use StatusBadGateway to indicate the upstream error.
pkgEvent.Status = http.StatusBadGateway
pkgEvent.Error = err
return gddoEvent, pkgEvent
}
pkgEvent.Status = resp.StatusCode
pkgEvent.Latency = time.Since(start)
return gddoEvent, pkgEvent
}
// doNotTeeURLsToPkgGoDev are paths that should not be teed to pkg.go.dev.
var doNotTeeURLsToPkgGoDev = map[string]bool{
"/-/bot": true,
"/-/refresh": true,
}
// doNotTeeExtsToPkgGoDev are URL extensions that should not be teed to
// pkg.go.dev.
var doNotTeeExtsToPkgGoDev = map[string]bool{
".css": true,
".html": true,
".js": true,
".txt": true,
".xml": true,
".ico": true,
}
// shouldTeeRequest reports whether a request should be teed to pkg.go.dev.
func shouldTeeRequest(u string) bool {
// Don't tee App Engine requests to pkg.go.dev.
if strings.HasPrefix(u, "/_ah/") {
return false
}
ext := filepath.Ext(u)
if doNotTeeExtsToPkgGoDev[ext] {
return false
}
if doNotTeeURLsToPkgGoDev[u] {
return false
}
return true
}
type gddoEvent struct {
Host string
Path string
Status int
URL string
Header http.Header
Latency time.Duration
IsRobot bool
UsePkgGoDev bool
Error error
}
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"
)
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"
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 "/-/subrepo":
u.Path = "/search"
q.Set("q", "golang.org/x")
default:
{
_, isSVG := godocURL.Query()["status.svg"]
_, isPNG := godocURL.Query()["status.png"]
if isSVG || isPNG {
u.Path = "/badge" + godocURL.Path
break
}
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
}