diff --git a/internal/dl/dl.go b/internal/dl/dl.go
new file mode 100644
index 00000000..aaa7af41
--- /dev/null
+++ b/internal/dl/dl.go
@@ -0,0 +1,353 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+// Package dl implements a simple downloads frontend server.
+//
+// It accepts HTTP POST requests to create a new download metadata entity, and
+// lists entities with sorting and filtering.
+// It is designed to run only on the instance of godoc that serves golang.org.
+package dl
+
+import (
+ "fmt"
+ "html/template"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+)
+
+const (
+ downloadBaseURL = "https://dl.google.com/go/"
+ cacheKey = "download_list_3" // increment if listTemplateData changes
+ cacheDuration = time.Hour
+)
+
+// File represents a file on the golang.org downloads page.
+// It should be kept in sync with the upload code in x/build/cmd/release.
+type File struct {
+ Filename string `json:"filename"`
+ OS string `json:"os"`
+ Arch string `json:"arch"`
+ Version string `json:"version"`
+ Checksum string `json:"-" datastore:",noindex"` // SHA1; deprecated
+ ChecksumSHA256 string `json:"sha256" datastore:",noindex"`
+ Size int64 `json:"size" datastore:",noindex"`
+ Kind string `json:"kind"` // "archive", "installer", "source"
+ Uploaded time.Time `json:"-"`
+}
+
+func (f File) ChecksumType() string {
+ if f.ChecksumSHA256 != "" {
+ return "SHA256"
+ }
+ return "SHA1"
+}
+
+func (f File) PrettyChecksum() string {
+ if f.ChecksumSHA256 != "" {
+ return f.ChecksumSHA256
+ }
+ return f.Checksum
+}
+
+func (f File) PrettyOS() string {
+ if f.OS == "darwin" {
+ switch {
+ case strings.Contains(f.Filename, "osx10.8"):
+ return "OS X 10.8+"
+ case strings.Contains(f.Filename, "osx10.6"):
+ return "OS X 10.6+"
+ }
+ }
+ return pretty(f.OS)
+}
+
+func (f File) PrettySize() string {
+ const mb = 1 << 20
+ if f.Size == 0 {
+ return ""
+ }
+ if f.Size < mb {
+ // All Go releases are >1mb, but handle this case anyway.
+ return fmt.Sprintf("%v bytes", f.Size)
+ }
+ return fmt.Sprintf("%.0fMB", float64(f.Size)/mb)
+}
+
+var primaryPorts = map[string]bool{
+ "darwin/amd64": true,
+ "linux/386": true,
+ "linux/amd64": true,
+ "linux/armv6l": true,
+ "windows/386": true,
+ "windows/amd64": true,
+}
+
+func (f File) PrimaryPort() bool {
+ if f.Kind == "source" {
+ return true
+ }
+ return primaryPorts[f.OS+"/"+f.Arch]
+}
+
+func (f File) Highlight() bool {
+ switch {
+ case f.Kind == "source":
+ return true
+ case f.Arch == "amd64" && f.OS == "linux":
+ return true
+ case f.Arch == "amd64" && f.Kind == "installer":
+ switch f.OS {
+ case "windows":
+ return true
+ case "darwin":
+ if !strings.Contains(f.Filename, "osx10.6") {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func (f File) URL() string {
+ return downloadBaseURL + f.Filename
+}
+
+type Release struct {
+ Version string `json:"version"`
+ Stable bool `json:"stable"`
+ Files []File `json:"files"`
+ Visible bool `json:"-"` // show files on page load
+ SplitPortTable bool `json:"-"` // whether files should be split by primary/other ports.
+}
+
+type Feature struct {
+ // The File field will be filled in by the first stable File
+ // whose name matches the given fileRE.
+ File
+ fileRE *regexp.Regexp
+
+ Platform string // "Microsoft Windows", "Apple macOS", "Linux"
+ Requirements string // "Windows XP and above, 64-bit Intel Processor"
+}
+
+// featuredFiles lists the platforms and files to be featured
+// at the top of the downloads page.
+var featuredFiles = []Feature{
+ {
+ Platform: "Microsoft Windows",
+ Requirements: "Windows 7 or later, Intel 64-bit processor",
+ fileRE: regexp.MustCompile(`\.windows-amd64\.msi$`),
+ },
+ {
+ Platform: "Apple macOS",
+ Requirements: "macOS 10.10 or later, Intel 64-bit processor",
+ fileRE: regexp.MustCompile(`\.darwin-amd64(-osx10\.8)?\.pkg$`),
+ },
+ {
+ Platform: "Linux",
+ Requirements: "Linux 2.6.23 or later, Intel 64-bit processor",
+ fileRE: regexp.MustCompile(`\.linux-amd64\.tar\.gz$`),
+ },
+ {
+ Platform: "Source",
+ fileRE: regexp.MustCompile(`\.src\.tar\.gz$`),
+ },
+}
+
+// data to send to the template; increment cacheKey if you change this.
+type listTemplateData struct {
+ Featured []Feature
+ Stable, Unstable, Archive []Release
+}
+
+var (
+ listTemplate = template.Must(template.New("").Funcs(templateFuncs).Parse(templateHTML))
+ templateFuncs = template.FuncMap{"pretty": pretty}
+)
+
+func filesToFeatured(fs []File) (featured []Feature) {
+ for _, feature := range featuredFiles {
+ for _, file := range fs {
+ if feature.fileRE.MatchString(file.Filename) {
+ feature.File = file
+ featured = append(featured, feature)
+ break
+ }
+ }
+ }
+ return
+}
+
+func filesToReleases(fs []File) (stable, unstable, archive []Release) {
+ sort.Sort(fileOrder(fs))
+
+ var r *Release
+ var stableMaj, stableMin int
+ add := func() {
+ if r == nil {
+ return
+ }
+ if !r.Stable {
+ if len(unstable) != 0 {
+ // Only show one (latest) unstable version.
+ return
+ }
+ maj, min, _ := parseVersion(r.Version)
+ if maj < stableMaj || maj == stableMaj && min <= stableMin {
+ // Display unstable version only if newer than the
+ // latest stable release.
+ return
+ }
+ unstable = append(unstable, *r)
+ return
+ }
+
+ // Reports whether the release is the most recent minor version of the
+ // two most recent major versions.
+ shouldAddStable := func() bool {
+ if len(stable) >= 2 {
+ // Show up to two stable versions.
+ return false
+ }
+ if len(stable) == 0 {
+ // Most recent stable version.
+ stableMaj, stableMin, _ = parseVersion(r.Version)
+ return true
+ }
+ if maj, _, _ := parseVersion(r.Version); maj == stableMaj {
+ // Older minor version of most recent major version.
+ return false
+ }
+ // Second most recent stable version.
+ return true
+ }
+ if !shouldAddStable() {
+ archive = append(archive, *r)
+ return
+ }
+
+ // Split the file list into primary/other ports for the stable releases.
+ // NOTE(cbro): This is only done for stable releases because maintaining the historical
+ // nature of primary/other ports for older versions is infeasible.
+ // If freebsd is considered primary some time in the future, we'd not want to
+ // mark all of the older freebsd binaries as "primary".
+ // It might be better if we set that as a flag when uploading.
+ r.SplitPortTable = true
+ r.Visible = true // Toggle open all stable releases.
+ stable = append(stable, *r)
+ }
+ for _, f := range fs {
+ if r == nil || f.Version != r.Version {
+ add()
+ r = &Release{
+ Version: f.Version,
+ Stable: isStable(f.Version),
+ }
+ }
+ r.Files = append(r.Files, f)
+ }
+ add()
+ return
+}
+
+// isStable reports whether the version string v is a stable version.
+func isStable(v string) bool {
+ return !strings.Contains(v, "beta") && !strings.Contains(v, "rc")
+}
+
+type fileOrder []File
+
+func (s fileOrder) Len() int { return len(s) }
+func (s fileOrder) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s fileOrder) Less(i, j int) bool {
+ a, b := s[i], s[j]
+ if av, bv := a.Version, b.Version; av != bv {
+ return versionLess(av, bv)
+ }
+ if a.OS != b.OS {
+ return a.OS < b.OS
+ }
+ if a.Arch != b.Arch {
+ return a.Arch < b.Arch
+ }
+ if a.Kind != b.Kind {
+ return a.Kind < b.Kind
+ }
+ return a.Filename < b.Filename
+}
+
+func versionLess(a, b string) bool {
+ // Put stable releases first.
+ if isStable(a) != isStable(b) {
+ return isStable(a)
+ }
+ maja, mina, ta := parseVersion(a)
+ majb, minb, tb := parseVersion(b)
+ if maja == majb {
+ if mina == minb {
+ return ta >= tb
+ }
+ return mina >= minb
+ }
+ return maja >= majb
+}
+
+func parseVersion(v string) (maj, min int, tail string) {
+ if i := strings.Index(v, "beta"); i > 0 {
+ tail = v[i:]
+ v = v[:i]
+ }
+ if i := strings.Index(v, "rc"); i > 0 {
+ tail = v[i:]
+ v = v[:i]
+ }
+ p := strings.Split(strings.TrimPrefix(v, "go1."), ".")
+ maj, _ = strconv.Atoi(p[0])
+ if len(p) < 2 {
+ return
+ }
+ min, _ = strconv.Atoi(p[1])
+ return
+}
+
+func validUser(user string) bool {
+ switch user {
+ case "adg", "bradfitz", "cbro", "andybons", "valsorda", "dmitshur", "katiehockman", "julieqiu":
+ return true
+ }
+ return false
+}
+
+var (
+ fileRe = regexp.MustCompile(`^go[0-9a-z.]+\.[0-9a-z.-]+\.(tar\.gz|pkg|msi|zip)$`)
+ goGetRe = regexp.MustCompile(`^go[0-9a-z.]+\.[0-9a-z.-]+$`)
+)
+
+// pretty returns a human-readable version of the given OS, Arch, or Kind.
+func pretty(s string) string {
+ t, ok := prettyStrings[s]
+ if !ok {
+ return s
+ }
+ return t
+}
+
+var prettyStrings = map[string]string{
+ "darwin": "macOS",
+ "freebsd": "FreeBSD",
+ "linux": "Linux",
+ "windows": "Windows",
+
+ "386": "x86",
+ "amd64": "x86-64",
+ "armv6l": "ARMv6",
+ "arm64": "ARMv8",
+
+ "archive": "Archive",
+ "installer": "Installer",
+ "source": "Source",
+}
diff --git a/internal/dl/dl_test.go b/internal/dl/dl_test.go
new file mode 100644
index 00000000..9ea5f620
--- /dev/null
+++ b/internal/dl/dl_test.go
@@ -0,0 +1,159 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package dl
+
+import (
+ "sort"
+ "strings"
+ "testing"
+)
+
+func TestParseVersion(t *testing.T) {
+ for _, c := range []struct {
+ in string
+ maj, min int
+ tail string
+ }{
+ {"go1.5", 5, 0, ""},
+ {"go1.5beta1", 5, 0, "beta1"},
+ {"go1.5.1", 5, 1, ""},
+ {"go1.5.1rc1", 5, 1, "rc1"},
+ } {
+ maj, min, tail := parseVersion(c.in)
+ if maj != c.maj || min != c.min || tail != c.tail {
+ t.Errorf("parseVersion(%q) = %v, %v, %q; want %v, %v, %q",
+ c.in, maj, min, tail, c.maj, c.min, c.tail)
+ }
+ }
+}
+
+func TestFileOrder(t *testing.T) {
+ fs := []File{
+ {Filename: "go1.3.src.tar.gz", Version: "go1.3", OS: "", Arch: "", Kind: "source"},
+ {Filename: "go1.3.1.src.tar.gz", Version: "go1.3.1", OS: "", Arch: "", Kind: "source"},
+ {Filename: "go1.3.linux-amd64.tar.gz", Version: "go1.3", OS: "linux", Arch: "amd64", Kind: "archive"},
+ {Filename: "go1.3.1.linux-amd64.tar.gz", Version: "go1.3.1", OS: "linux", Arch: "amd64", Kind: "archive"},
+ {Filename: "go1.3.darwin-amd64.tar.gz", Version: "go1.3", OS: "darwin", Arch: "amd64", Kind: "archive"},
+ {Filename: "go1.3.darwin-amd64.pkg", Version: "go1.3", OS: "darwin", Arch: "amd64", Kind: "installer"},
+ {Filename: "go1.3.darwin-386.tar.gz", Version: "go1.3", OS: "darwin", Arch: "386", Kind: "archive"},
+ {Filename: "go1.3beta1.linux-amd64.tar.gz", Version: "go1.3beta1", OS: "linux", Arch: "amd64", Kind: "archive"},
+ {Filename: "go1.3beta2.linux-amd64.tar.gz", Version: "go1.3beta2", OS: "linux", Arch: "amd64", Kind: "archive"},
+ {Filename: "go1.3rc1.linux-amd64.tar.gz", Version: "go1.3rc1", OS: "linux", Arch: "amd64", Kind: "archive"},
+ {Filename: "go1.2.linux-amd64.tar.gz", Version: "go1.2", OS: "linux", Arch: "amd64", Kind: "archive"},
+ {Filename: "go1.2.2.linux-amd64.tar.gz", Version: "go1.2.2", OS: "linux", Arch: "amd64", Kind: "archive"},
+ }
+ sort.Sort(fileOrder(fs))
+ var s []string
+ for _, f := range fs {
+ s = append(s, f.Filename)
+ }
+ got := strings.Join(s, "\n")
+ want := strings.Join([]string{
+ "go1.3.1.src.tar.gz",
+ "go1.3.1.linux-amd64.tar.gz",
+ "go1.3.src.tar.gz",
+ "go1.3.darwin-386.tar.gz",
+ "go1.3.darwin-amd64.tar.gz",
+ "go1.3.darwin-amd64.pkg",
+ "go1.3.linux-amd64.tar.gz",
+ "go1.2.2.linux-amd64.tar.gz",
+ "go1.2.linux-amd64.tar.gz",
+ "go1.3rc1.linux-amd64.tar.gz",
+ "go1.3beta2.linux-amd64.tar.gz",
+ "go1.3beta1.linux-amd64.tar.gz",
+ }, "\n")
+ if got != want {
+ t.Errorf("sort order is\n%s\nwant:\n%s", got, want)
+ }
+}
+
+func TestFilesToReleases(t *testing.T) {
+ fs := []File{
+ {Version: "go1.7.4", OS: "darwin"},
+ {Version: "go1.7.4", OS: "windows"},
+ {Version: "go1.7", OS: "darwin"},
+ {Version: "go1.7", OS: "windows"},
+ {Version: "go1.6.2", OS: "darwin"},
+ {Version: "go1.6.2", OS: "windows"},
+ {Version: "go1.6", OS: "darwin"},
+ {Version: "go1.6", OS: "windows"},
+ {Version: "go1.5.2", OS: "darwin"},
+ {Version: "go1.5.2", OS: "windows"},
+ {Version: "go1.5", OS: "darwin"},
+ {Version: "go1.5", OS: "windows"},
+ {Version: "go1.5beta1", OS: "windows"},
+ }
+ stable, unstable, archive := filesToReleases(fs)
+ if got, want := list(stable), "go1.7.4, go1.6.2"; got != want {
+ t.Errorf("stable = %q; want %q", got, want)
+ }
+ if got, want := list(unstable), ""; got != want {
+ t.Errorf("unstable = %q; want %q", got, want)
+ }
+ if got, want := list(archive), "go1.7, go1.6, go1.5.2, go1.5"; got != want {
+ t.Errorf("archive = %q; want %q", got, want)
+ }
+}
+
+func TestOldUnstableNotShown(t *testing.T) {
+ fs := []File{
+ {Version: "go1.7.4"},
+ {Version: "go1.7"},
+ {Version: "go1.7beta1"},
+ }
+ _, unstable, _ := filesToReleases(fs)
+ if len(unstable) != 0 {
+ t.Errorf("got unstable, want none")
+ }
+}
+
+// A new beta should show up under unstable, but not show up under archive. See golang.org/issue/29669.
+func TestNewUnstableShownOnce(t *testing.T) {
+ fs := []File{
+ {Version: "go1.12beta2"},
+ {Version: "go1.11.4"},
+ {Version: "go1.11"},
+ {Version: "go1.10.7"},
+ {Version: "go1.10"},
+ {Version: "go1.9"},
+ }
+ stable, unstable, archive := filesToReleases(fs)
+ if got, want := list(stable), "go1.11.4, go1.10.7"; got != want {
+ t.Errorf("stable = %q; want %q", got, want)
+ }
+ if got, want := list(unstable), "go1.12beta2"; got != want {
+ t.Errorf("unstable = %q; want %q", got, want)
+ }
+ if got, want := list(archive), "go1.11, go1.10, go1.9"; got != want {
+ t.Errorf("archive = %q; want %q", got, want)
+ }
+}
+
+func TestUnstableShown(t *testing.T) {
+ fs := []File{
+ {Version: "go1.8beta2"},
+ {Version: "go1.8rc1"},
+ {Version: "go1.7.4"},
+ {Version: "go1.7"},
+ {Version: "go1.7beta1"},
+ }
+ _, unstable, _ := filesToReleases(fs)
+ // Show RCs ahead of betas.
+ if got, want := list(unstable), "go1.8rc1"; got != want {
+ t.Errorf("unstable = %q; want %q", got, want)
+ }
+}
+
+// list returns a version list string for the given releases.
+func list(rs []Release) string {
+ var s string
+ for i, r := range rs {
+ if i > 0 {
+ s += ", "
+ }
+ s += r.Version
+ }
+ return s
+}
diff --git a/internal/dl/server.go b/internal/dl/server.go
new file mode 100644
index 00000000..43fa4533
--- /dev/null
+++ b/internal/dl/server.go
@@ -0,0 +1,266 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+// +build golangorg
+
+package dl
+
+import (
+ "context"
+ "crypto/hmac"
+ "crypto/md5"
+ "encoding/json"
+ "fmt"
+ "html"
+ "io"
+ "log"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+
+ "cloud.google.com/go/datastore"
+ "golang.org/x/tools/godoc/env"
+ "golang.org/x/tools/internal/memcache"
+)
+
+type server struct {
+ datastore *datastore.Client
+ memcache *memcache.CodecClient
+}
+
+func RegisterHandlers(mux *http.ServeMux, dc *datastore.Client, mc *memcache.Client) {
+ s := server{dc, mc.WithCodec(memcache.Gob)}
+ mux.HandleFunc("/dl", s.getHandler)
+ mux.HandleFunc("/dl/", s.getHandler) // also serves listHandler
+ mux.HandleFunc("/dl/upload", s.uploadHandler)
+
+ // NOTE(cbro): this only needs to be run once per project,
+ // and should be behind an admin login.
+ // TODO(cbro): move into a locally-run program? or remove?
+ // mux.HandleFunc("/dl/init", initHandler)
+}
+
+// rootKey is the ancestor of all File entities.
+var rootKey = datastore.NameKey("FileRoot", "root", nil)
+
+func (h server) listHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "GET" {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ ctx := r.Context()
+ var d listTemplateData
+
+ if err := h.memcache.Get(ctx, cacheKey, &d); err != nil {
+ if err != memcache.ErrCacheMiss {
+ log.Printf("ERROR cache get error: %v", err)
+ // NOTE(cbro): continue to hit datastore if the memcache is down.
+ }
+
+ var fs []File
+ q := datastore.NewQuery("File").Ancestor(rootKey)
+ if _, err := h.datastore.GetAll(ctx, q, &fs); err != nil {
+ log.Printf("ERROR error listing: %v", err)
+ http.Error(w, "Could not get download page. Try again in a few minutes.", 500)
+ return
+ }
+ d.Stable, d.Unstable, d.Archive = filesToReleases(fs)
+ if len(d.Stable) > 0 {
+ d.Featured = filesToFeatured(d.Stable[0].Files)
+ }
+
+ item := &memcache.Item{Key: cacheKey, Object: &d, Expiration: cacheDuration}
+ if err := h.memcache.Set(ctx, item); err != nil {
+ log.Printf("ERROR cache set error: %v", err)
+ }
+ }
+
+ if r.URL.Query().Get("mode") == "json" {
+ w.Header().Set("Content-Type", "application/json")
+ enc := json.NewEncoder(w)
+ enc.SetIndent("", " ")
+ if err := enc.Encode(d.Stable); err != nil {
+ log.Printf("ERROR rendering JSON for releases: %v", err)
+ }
+ return
+ }
+
+ if err := listTemplate.ExecuteTemplate(w, "root", d); err != nil {
+ log.Printf("ERROR executing template: %v", err)
+ }
+}
+
+func (h server) uploadHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ ctx := r.Context()
+
+ // Authenticate using a user token (same as gomote).
+ user := r.FormValue("user")
+ if !validUser(user) {
+ http.Error(w, "bad user", http.StatusForbidden)
+ return
+ }
+ if r.FormValue("key") != h.userKey(ctx, user) {
+ http.Error(w, "bad key", http.StatusForbidden)
+ return
+ }
+
+ var f File
+ defer r.Body.Close()
+ if err := json.NewDecoder(r.Body).Decode(&f); err != nil {
+ log.Printf("ERROR decoding upload JSON: %v", err)
+ http.Error(w, "Something broke", http.StatusInternalServerError)
+ return
+ }
+ if f.Filename == "" {
+ http.Error(w, "Must provide Filename", http.StatusBadRequest)
+ return
+ }
+ if f.Uploaded.IsZero() {
+ f.Uploaded = time.Now()
+ }
+ k := datastore.NameKey("File", f.Filename, rootKey)
+ if _, err := h.datastore.Put(ctx, k, &f); err != nil {
+ log.Printf("ERROR File entity: %v", err)
+ http.Error(w, "could not put File entity", http.StatusInternalServerError)
+ return
+ }
+ if err := h.memcache.Delete(ctx, cacheKey); err != nil {
+ log.Printf("ERROR delete error: %v", err)
+ }
+ io.WriteString(w, "OK")
+}
+
+func (h server) getHandler(w http.ResponseWriter, r *http.Request) {
+ isGoGet := (r.Method == "GET" || r.Method == "HEAD") && r.FormValue("go-get") == "1"
+ // For go get, we need to serve the same meta tags at /dl for cmd/go to
+ // validate against the import path.
+ if r.URL.Path == "/dl" && isGoGet {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ fmt.Fprintf(w, `
+
+`)
+ return
+ }
+ if r.URL.Path == "/dl" {
+ http.Redirect(w, r, "/dl/", http.StatusFound)
+ return
+ }
+
+ name := strings.TrimPrefix(r.URL.Path, "/dl/")
+ var redirectURL string
+ switch {
+ case name == "":
+ h.listHandler(w, r)
+ return
+ case fileRe.MatchString(name):
+ http.Redirect(w, r, downloadBaseURL+name, http.StatusFound)
+ return
+ case name == "gotip":
+ redirectURL = "https://godoc.org/golang.org/dl/gotip"
+ case goGetRe.MatchString(name):
+ redirectURL = "https://golang.org/dl/#" + name
+ default:
+ http.NotFound(w, r)
+ return
+ }
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ if !isGoGet {
+ w.Header().Set("Location", redirectURL)
+ }
+ fmt.Fprintf(w, `
+
+
+
+
+
+
+Nothing to see here; move along.
+
+
+`, html.EscapeString(redirectURL), html.EscapeString(redirectURL))
+}
+
+func (h server) initHandler(w http.ResponseWriter, r *http.Request) {
+ var fileRoot struct {
+ Root string
+ }
+ ctx := r.Context()
+ k := rootKey
+ _, err := h.datastore.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
+ err := tx.Get(k, &fileRoot)
+ if err != nil && err != datastore.ErrNoSuchEntity {
+ return err
+ }
+ _, err = tx.Put(k, &fileRoot)
+ return err
+ }, nil)
+ if err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+ io.WriteString(w, "OK")
+}
+
+func (h server) userKey(c context.Context, user string) string {
+ hash := hmac.New(md5.New, []byte(h.secret(c)))
+ hash.Write([]byte("user-" + user))
+ return fmt.Sprintf("%x", hash.Sum(nil))
+}
+
+// Code below copied from x/build/app/key
+
+var theKey struct {
+ sync.RWMutex
+ builderKey
+}
+
+type builderKey struct {
+ Secret string
+}
+
+func (k *builderKey) Key() *datastore.Key {
+ return datastore.NameKey("BuilderKey", "root", nil)
+}
+
+func (h server) secret(ctx context.Context) string {
+ // check with rlock
+ theKey.RLock()
+ k := theKey.Secret
+ theKey.RUnlock()
+ if k != "" {
+ return k
+ }
+
+ // prepare to fill; check with lock and keep lock
+ theKey.Lock()
+ defer theKey.Unlock()
+ if theKey.Secret != "" {
+ return theKey.Secret
+ }
+
+ // fill
+ if err := h.datastore.Get(ctx, theKey.Key(), &theKey.builderKey); err != nil {
+ if err == datastore.ErrNoSuchEntity {
+ // If the key is not stored in datastore, write it.
+ // This only happens at the beginning of a new deployment.
+ // The code is left here for SDK use and in case a fresh
+ // deployment is ever needed. "gophers rule" is not the
+ // real key.
+ if env.IsProd() {
+ panic("lost key from datastore")
+ }
+ theKey.Secret = "gophers rule"
+ h.datastore.Put(ctx, theKey.Key(), &theKey.builderKey)
+ return theKey.Secret
+ }
+ panic("cannot load builder key: " + err.Error())
+ }
+
+ return theKey.Secret
+}
diff --git a/internal/dl/tmpl.go b/internal/dl/tmpl.go
new file mode 100644
index 00000000..d086b696
--- /dev/null
+++ b/internal/dl/tmpl.go
@@ -0,0 +1,277 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package dl
+
+// TODO(adg): refactor this to use the tools/godoc/static template.
+
+const templateHTML = `
+{{define "root"}}
+
+
+
+
+ Downloads - The Go Programming Language
+
+
+
+
+
+
+
+
+
+
+
+
Downloads
+
+
+After downloading a binary release suitable for your system,
+please follow the installation instructions.
+
+
+
+If you are building from source,
+follow the source installation instructions.
+
+
+
+See the release history for more
+information about Go releases.
+
+
+{{with .Featured}}
+
Featured downloads
+{{range .}}
+{{template "download" .}}
+{{end}}
+{{end}}
+
+
+
+{{with .Stable}}
+
Stable versions
+{{template "releases" .}}
+{{end}}
+
+{{with .Unstable}}
+
Unstable version
+{{template "releases" .}}
+{{end}}
+
+{{with .Archive}}
+
+
+
+
+
+
+ {{template "releases" .}}
+
+
+{{end}}
+
+
+
+
+
+
+
+
+
+
+
+{{end}}
+
+{{define "releases"}}
+{{range .}}
+
+
+
+
+
+
+ {{if .Stable}}{{else}}
+
This is an unstable version of Go. Use with caution.
+
If you already have Go installed, you can install this version by running:
+
+go get golang.org/dl/{{.Version}}
+
+
Then, use the {{.Version}}
command instead of the go
command to use {{.Version}}.
+ {{end}}
+ {{template "files" .}}
+
+
+{{end}}
+{{end}}
+
+{{define "files"}}
+
+
+
+ File name |
+ Kind |
+ OS |
+ Arch |
+ Size |
+ {{/* Use the checksum type of the first file for the column heading. */}}
+ {{(index .Files 0).ChecksumType}} Checksum |
+
+
+{{if .SplitPortTable}}
+ {{range .Files}}{{if .PrimaryPort}}{{template "file" .}}{{end}}{{end}}
+
+ {{/* TODO(cbro): add a link to an explanatory doc page */}}
+ Other Ports |
+ {{range .Files}}{{if not .PrimaryPort}}{{template "file" .}}{{end}}{{end}}
+{{else}}
+ {{range .Files}}{{template "file" .}}{{end}}
+{{end}}
+
+{{end}}
+
+{{define "file"}}
+
+ {{.Filename}} |
+ {{pretty .Kind}} |
+ {{.PrettyOS}} |
+ {{pretty .Arch}} |
+ {{.PrettySize}} |
+ {{.PrettyChecksum}} |
+
+{{end}}
+
+{{define "download"}}
+
+{{.Platform}}
+{{with .Requirements}}{{.}}
{{end}}
+
+ {{.Filename}}
+ {{if .Size}}({{.PrettySize}}){{end}}
+
+
+{{end}}
+`
diff --git a/internal/env/env.go b/internal/env/env.go
new file mode 100644
index 00000000..e1f55cd3
--- /dev/null
+++ b/internal/env/env.go
@@ -0,0 +1,41 @@
+// Copyright 2018 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 env provides environment information for the godoc server running on
+// golang.org.
+package env
+
+import (
+ "log"
+ "os"
+ "strconv"
+)
+
+var (
+ isProd = boolEnv("GODOC_PROD")
+ enforceHosts = boolEnv("GODOC_ENFORCE_HOSTS")
+)
+
+// IsProd reports whether the server is running in its production configuration
+// on golang.org.
+func IsProd() bool {
+ return isProd
+}
+
+// EnforceHosts reports whether host filtering should be enforced.
+func EnforceHosts() bool {
+ return enforceHosts
+}
+
+func boolEnv(key string) bool {
+ v := os.Getenv(key)
+ if v == "" {
+ return false
+ }
+ b, err := strconv.ParseBool(v)
+ if err != nil {
+ log.Fatalf("environment variable %s (%q) must be a boolean", key, v)
+ }
+ return b
+}
diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go
new file mode 100644
index 00000000..bb0e81c3
--- /dev/null
+++ b/internal/proxy/proxy.go
@@ -0,0 +1,170 @@
+// Copyright 2015 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 proxy proxies requests to the playground's compile and share handlers.
+// It is designed to run only on the instance of godoc that serves golang.org.
+package proxy
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "strings"
+ "time"
+
+ "golang.org/x/tools/godoc/env"
+)
+
+const playgroundURL = "https://play.golang.org"
+
+type Request struct {
+ Body string
+}
+
+type Response struct {
+ Errors string
+ Events []Event
+}
+
+type Event struct {
+ Message string
+ Kind string // "stdout" or "stderr"
+ Delay time.Duration // time to wait before printing Message
+}
+
+const expires = 7 * 24 * time.Hour // 1 week
+var cacheControlHeader = fmt.Sprintf("public, max-age=%d", int(expires.Seconds()))
+
+func RegisterHandlers(mux *http.ServeMux) {
+ mux.HandleFunc("/compile", compile)
+ mux.HandleFunc("/share", share)
+}
+
+func compile(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ http.Error(w, "I only answer to POST requests.", http.StatusMethodNotAllowed)
+ return
+ }
+
+ ctx := r.Context()
+
+ body := r.FormValue("body")
+ res := &Response{}
+ req := &Request{Body: body}
+ if err := makeCompileRequest(ctx, req, res); err != nil {
+ log.Printf("ERROR compile error: %v", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ var out interface{}
+ switch r.FormValue("version") {
+ case "2":
+ out = res
+ default: // "1"
+ out = struct {
+ CompileErrors string `json:"compile_errors"`
+ Output string `json:"output"`
+ }{res.Errors, flatten(res.Events)}
+ }
+ b, err := json.Marshal(out)
+ if err != nil {
+ log.Printf("ERROR encoding response: %v", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ expiresTime := time.Now().Add(expires).UTC()
+ w.Header().Set("Expires", expiresTime.Format(time.RFC1123))
+ w.Header().Set("Cache-Control", cacheControlHeader)
+ w.Write(b)
+}
+
+// makePlaygroundRequest sends the given Request to the playground compile
+// endpoint and stores the response in the given Response.
+func makeCompileRequest(ctx context.Context, req *Request, res *Response) error {
+ reqJ, err := json.Marshal(req)
+ if err != nil {
+ return fmt.Errorf("marshalling request: %v", err)
+ }
+ hReq, _ := http.NewRequest("POST", playgroundURL+"/compile", bytes.NewReader(reqJ))
+ hReq.Header.Set("Content-Type", "application/json")
+ hReq = hReq.WithContext(ctx)
+
+ r, err := http.DefaultClient.Do(hReq)
+ if err != nil {
+ return fmt.Errorf("making request: %v", err)
+ }
+ defer r.Body.Close()
+
+ if r.StatusCode != http.StatusOK {
+ b, _ := ioutil.ReadAll(r.Body)
+ return fmt.Errorf("bad status: %v body:\n%s", r.Status, b)
+ }
+
+ if err := json.NewDecoder(r.Body).Decode(res); err != nil {
+ return fmt.Errorf("unmarshalling response: %v", err)
+ }
+ return nil
+}
+
+// flatten takes a sequence of Events and returns their contents, concatenated.
+func flatten(seq []Event) string {
+ var buf bytes.Buffer
+ for _, e := range seq {
+ buf.WriteString(e.Message)
+ }
+ return buf.String()
+}
+
+func share(w http.ResponseWriter, r *http.Request) {
+ if googleCN(r) {
+ http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+ return
+ }
+
+ // HACK(cbro): use a simple proxy rather than httputil.ReverseProxy because of Issue #28168.
+ // TODO: investigate using ReverseProxy with a Director, unsetting whatever's necessary to make that work.
+ req, _ := http.NewRequest("POST", playgroundURL+"/share", r.Body)
+ req.Header.Set("Content-Type", r.Header.Get("Content-Type"))
+ req = req.WithContext(r.Context())
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ log.Printf("ERROR share error: %v", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ copyHeader := func(k string) {
+ if v := resp.Header.Get(k); v != "" {
+ w.Header().Set(k, v)
+ }
+ }
+ copyHeader("Content-Type")
+ copyHeader("Content-Length")
+ defer resp.Body.Close()
+ w.WriteHeader(resp.StatusCode)
+ io.Copy(w, resp.Body)
+}
+
+func googleCN(r *http.Request) bool {
+ if r.FormValue("googlecn") != "" {
+ return true
+ }
+ if !env.IsProd() {
+ return false
+ }
+ if strings.HasSuffix(r.Host, ".cn") {
+ return true
+ }
+ switch r.Header.Get("X-AppEngine-Country") {
+ case "", "ZZ", "CN":
+ return true
+ }
+ return false
+}
diff --git a/internal/redirect/hash.go b/internal/redirect/hash.go
new file mode 100644
index 00000000..d5a1e3eb
--- /dev/null
+++ b/internal/redirect/hash.go
@@ -0,0 +1,138 @@
+// Copyright 2014 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.
+
+// This file provides a compact encoding of
+// a map of Mercurial hashes to Git hashes.
+
+package redirect
+
+import (
+ "encoding/binary"
+ "fmt"
+ "io"
+ "os"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+// hashMap is a map of Mercurial hashes to Git hashes.
+type hashMap struct {
+ file *os.File
+ entries int
+}
+
+// newHashMap takes a file handle that contains a map of Mercurial to Git
+// hashes. The file should be a sequence of pairs of little-endian encoded
+// uint32s, representing a hgHash and a gitHash respectively.
+// The sequence must be sorted by hgHash.
+// The file must remain open for as long as the returned hashMap is used.
+func newHashMap(f *os.File) (*hashMap, error) {
+ fi, err := f.Stat()
+ if err != nil {
+ return nil, err
+ }
+ return &hashMap{file: f, entries: int(fi.Size() / 8)}, nil
+}
+
+// Lookup finds an hgHash in the map that matches the given prefix, and returns
+// its corresponding gitHash. The prefix must be at least 8 characters long.
+func (m *hashMap) Lookup(s string) gitHash {
+ if m == nil {
+ return 0
+ }
+ hg, err := hgHashFromString(s)
+ if err != nil {
+ return 0
+ }
+ var git gitHash
+ b := make([]byte, 8)
+ sort.Search(m.entries, func(i int) bool {
+ n, err := m.file.ReadAt(b, int64(i*8))
+ if err != nil {
+ panic(err)
+ }
+ if n != 8 {
+ panic(io.ErrUnexpectedEOF)
+ }
+ v := hgHash(binary.LittleEndian.Uint32(b[:4]))
+ if v == hg {
+ git = gitHash(binary.LittleEndian.Uint32(b[4:]))
+ }
+ return v >= hg
+ })
+ return git
+}
+
+// hgHash represents the lower (leftmost) 32 bits of a Mercurial hash.
+type hgHash uint32
+
+func (h hgHash) String() string {
+ return intToHash(int64(h))
+}
+
+func hgHashFromString(s string) (hgHash, error) {
+ if len(s) < 8 {
+ return 0, fmt.Errorf("string too small: len(s) = %d", len(s))
+ }
+ hash := s[:8]
+ i, err := strconv.ParseInt(hash, 16, 64)
+ if err != nil {
+ return 0, err
+ }
+ return hgHash(i), nil
+}
+
+// gitHash represents the leftmost 28 bits of a Git hash in its upper 28 bits,
+// and it encodes hash's repository in the lower 4 bits.
+type gitHash uint32
+
+func (h gitHash) Hash() string {
+ return intToHash(int64(h))[:7]
+}
+
+func (h gitHash) Repo() string {
+ return repo(h & 0xF).String()
+}
+
+func intToHash(i int64) string {
+ s := strconv.FormatInt(i, 16)
+ if len(s) < 8 {
+ s = strings.Repeat("0", 8-len(s)) + s
+ }
+ return s
+}
+
+// repo represents a Go Git repository.
+type repo byte
+
+const (
+ repoGo repo = iota
+ repoBlog
+ repoCrypto
+ repoExp
+ repoImage
+ repoMobile
+ repoNet
+ repoSys
+ repoTalks
+ repoText
+ repoTools
+)
+
+func (r repo) String() string {
+ return map[repo]string{
+ repoGo: "go",
+ repoBlog: "blog",
+ repoCrypto: "crypto",
+ repoExp: "exp",
+ repoImage: "image",
+ repoMobile: "mobile",
+ repoNet: "net",
+ repoSys: "sys",
+ repoTalks: "talks",
+ repoText: "text",
+ repoTools: "tools",
+ }[r]
+}
diff --git a/internal/redirect/redirect.go b/internal/redirect/redirect.go
new file mode 100644
index 00000000..b4599f6a
--- /dev/null
+++ b/internal/redirect/redirect.go
@@ -0,0 +1,324 @@
+// Copyright 2013 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 redirect provides hooks to register HTTP handlers that redirect old
+// godoc paths to their new equivalents and assist in accessing the issue
+// tracker, wiki, code review system, etc.
+package redirect // import "golang.org/x/tools/godoc/redirect"
+
+import (
+ "context"
+ "fmt"
+ "html/template"
+ "net/http"
+ "os"
+ "regexp"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "golang.org/x/net/context/ctxhttp"
+)
+
+// Register registers HTTP handlers that redirect old godoc paths to their new
+// equivalents and assist in accessing the issue tracker, wiki, code review
+// system, etc. If mux is nil it uses http.DefaultServeMux.
+func Register(mux *http.ServeMux) {
+ if mux == nil {
+ mux = http.DefaultServeMux
+ }
+ handlePathRedirects(mux, pkgRedirects, "/pkg/")
+ handlePathRedirects(mux, cmdRedirects, "/cmd/")
+ for prefix, redirect := range prefixHelpers {
+ p := "/" + prefix + "/"
+ mux.Handle(p, PrefixHandler(p, redirect))
+ }
+ for path, redirect := range redirects {
+ mux.Handle(path, Handler(redirect))
+ }
+ // NB: /src/pkg (sans trailing slash) is the index of packages.
+ mux.HandleFunc("/src/pkg/", srcPkgHandler)
+ mux.HandleFunc("/cl/", clHandler)
+ mux.HandleFunc("/change/", changeHandler)
+ mux.HandleFunc("/design/", designHandler)
+}
+
+func handlePathRedirects(mux *http.ServeMux, redirects map[string]string, prefix string) {
+ for source, target := range redirects {
+ h := Handler(prefix + target + "/")
+ p := prefix + source
+ mux.Handle(p, h)
+ mux.Handle(p+"/", h)
+ }
+}
+
+// Packages that were renamed between r60 and go1.
+var pkgRedirects = map[string]string{
+ "asn1": "encoding/asn1",
+ "big": "math/big",
+ "cmath": "math/cmplx",
+ "csv": "encoding/csv",
+ "exec": "os/exec",
+ "exp/template/html": "html/template",
+ "gob": "encoding/gob",
+ "http": "net/http",
+ "http/cgi": "net/http/cgi",
+ "http/fcgi": "net/http/fcgi",
+ "http/httptest": "net/http/httptest",
+ "http/pprof": "net/http/pprof",
+ "json": "encoding/json",
+ "mail": "net/mail",
+ "rand": "math/rand",
+ "rpc": "net/rpc",
+ "rpc/jsonrpc": "net/rpc/jsonrpc",
+ "scanner": "text/scanner",
+ "smtp": "net/smtp",
+ "tabwriter": "text/tabwriter",
+ "template": "text/template",
+ "template/parse": "text/template/parse",
+ "url": "net/url",
+ "utf16": "unicode/utf16",
+ "utf8": "unicode/utf8",
+ "xml": "encoding/xml",
+}
+
+// Commands that were renamed between r60 and go1.
+var cmdRedirects = map[string]string{
+ "gofix": "fix",
+ "goinstall": "go",
+ "gopack": "pack",
+ "gotest": "go",
+ "govet": "vet",
+ "goyacc": "yacc",
+}
+
+var redirects = map[string]string{
+ "/blog": "/blog/",
+ "/build": "http://build.golang.org",
+ "/change": "https://go.googlesource.com/go",
+ "/cl": "https://go-review.googlesource.com",
+ "/cmd/godoc/": "http://godoc.org/golang.org/x/tools/cmd/godoc/",
+ "/issue": "https://github.com/golang/go/issues",
+ "/issue/new": "https://github.com/golang/go/issues/new",
+ "/issues": "https://github.com/golang/go/issues",
+ "/issues/new": "https://github.com/golang/go/issues/new",
+ "/play": "http://play.golang.org",
+ "/design": "https://go.googlesource.com/proposal/+/master/design",
+
+ // In Go 1.2 the references page is part of /doc/.
+ "/ref": "/doc/#references",
+ // This next rule clobbers /ref/spec and /ref/mem.
+ // TODO(adg): figure out what to do here, if anything.
+ // "/ref/": "/doc/#references",
+
+ // Be nice to people who are looking in the wrong place.
+ "/doc/mem": "/ref/mem",
+ "/doc/spec": "/ref/spec",
+
+ "/talks": "http://talks.golang.org",
+ "/tour": "http://tour.golang.org",
+ "/wiki": "https://github.com/golang/go/wiki",
+
+ "/doc/articles/c_go_cgo.html": "/blog/c-go-cgo",
+ "/doc/articles/concurrency_patterns.html": "/blog/go-concurrency-patterns-timing-out-and",
+ "/doc/articles/defer_panic_recover.html": "/blog/defer-panic-and-recover",
+ "/doc/articles/error_handling.html": "/blog/error-handling-and-go",
+ "/doc/articles/gobs_of_data.html": "/blog/gobs-of-data",
+ "/doc/articles/godoc_documenting_go_code.html": "/blog/godoc-documenting-go-code",
+ "/doc/articles/gos_declaration_syntax.html": "/blog/gos-declaration-syntax",
+ "/doc/articles/image_draw.html": "/blog/go-imagedraw-package",
+ "/doc/articles/image_package.html": "/blog/go-image-package",
+ "/doc/articles/json_and_go.html": "/blog/json-and-go",
+ "/doc/articles/json_rpc_tale_of_interfaces.html": "/blog/json-rpc-tale-of-interfaces",
+ "/doc/articles/laws_of_reflection.html": "/blog/laws-of-reflection",
+ "/doc/articles/slices_usage_and_internals.html": "/blog/go-slices-usage-and-internals",
+ "/doc/go_for_cpp_programmers.html": "/wiki/GoForCPPProgrammers",
+ "/doc/go_tutorial.html": "http://tour.golang.org/",
+}
+
+var prefixHelpers = map[string]string{
+ "issue": "https://github.com/golang/go/issues/",
+ "issues": "https://github.com/golang/go/issues/",
+ "play": "http://play.golang.org/",
+ "talks": "http://talks.golang.org/",
+ "wiki": "https://github.com/golang/go/wiki/",
+}
+
+func Handler(target string) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ url := target
+ if qs := r.URL.RawQuery; qs != "" {
+ url += "?" + qs
+ }
+ http.Redirect(w, r, url, http.StatusMovedPermanently)
+ })
+}
+
+var validId = regexp.MustCompile(`^[A-Za-z0-9-]*/?$`)
+
+func PrefixHandler(prefix, baseURL string) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if p := r.URL.Path; p == prefix {
+ // redirect /prefix/ to /prefix
+ http.Redirect(w, r, p[:len(p)-1], http.StatusFound)
+ return
+ }
+ id := r.URL.Path[len(prefix):]
+ if !validId.MatchString(id) {
+ http.Error(w, "Not found", http.StatusNotFound)
+ return
+ }
+ target := baseURL + id
+ http.Redirect(w, r, target, http.StatusFound)
+ })
+}
+
+// Redirect requests from the old "/src/pkg/foo" to the new "/src/foo".
+// See http://golang.org/s/go14nopkg
+func srcPkgHandler(w http.ResponseWriter, r *http.Request) {
+ r.URL.Path = "/src/" + r.URL.Path[len("/src/pkg/"):]
+ http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently)
+}
+
+func clHandler(w http.ResponseWriter, r *http.Request) {
+ const prefix = "/cl/"
+ if p := r.URL.Path; p == prefix {
+ // redirect /prefix/ to /prefix
+ http.Redirect(w, r, p[:len(p)-1], http.StatusFound)
+ return
+ }
+ id := r.URL.Path[len(prefix):]
+ // support /cl/152700045/, which is used in commit 0edafefc36.
+ id = strings.TrimSuffix(id, "/")
+ if !validId.MatchString(id) {
+ http.Error(w, "Not found", http.StatusNotFound)
+ return
+ }
+ target := ""
+
+ if n, err := strconv.Atoi(id); err == nil && isRietveldCL(n) {
+ // Issue 28836: if this Rietveld CL happens to
+ // also be a Gerrit CL, render a disambiguation HTML
+ // page with two links instead. We need to make a
+ // Gerrit API call to figure that out, but we cache
+ // known Gerrit CLs so it's done at most once per CL.
+ if ok, err := isGerritCL(r.Context(), n); err == nil && ok {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ clDisambiguationHTML.Execute(w, n)
+ return
+ }
+
+ target = "https://codereview.appspot.com/" + id
+ } else {
+ target = "https://go-review.googlesource.com/" + id
+ }
+ http.Redirect(w, r, target, http.StatusFound)
+}
+
+var clDisambiguationHTML = template.Must(template.New("").Parse(`
+
+
+ Go CL {{.}} Disambiguation
+
+
+
+ CL number {{.}} exists in both Gerrit (the current code review system)
+ and Rietveld (the previous code review system). Please make a choice:
+
+
+
+`))
+
+// isGerritCL reports whether a Gerrit CL with the specified numeric change ID (e.g., "4247")
+// is known to exist by querying the Gerrit API at https://go-review.googlesource.com.
+// isGerritCL uses gerritCLCache as a cache of Gerrit CL IDs that exist.
+func isGerritCL(ctx context.Context, id int) (bool, error) {
+ // Check cache first.
+ gerritCLCache.Lock()
+ ok := gerritCLCache.exist[id]
+ gerritCLCache.Unlock()
+ if ok {
+ return true, nil
+ }
+
+ // Query the Gerrit API Get Change endpoint, as documented at
+ // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-change.
+ ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+ resp, err := ctxhttp.Get(ctx, nil, fmt.Sprintf("https://go-review.googlesource.com/changes/%d", id))
+ if err != nil {
+ return false, err
+ }
+ resp.Body.Close()
+ switch resp.StatusCode {
+ case http.StatusOK:
+ // A Gerrit CL with this ID exists. Add it to cache.
+ gerritCLCache.Lock()
+ gerritCLCache.exist[id] = true
+ gerritCLCache.Unlock()
+ return true, nil
+ case http.StatusNotFound:
+ // A Gerrit CL with this ID doesn't exist. It may get created in the future.
+ return false, nil
+ default:
+ return false, fmt.Errorf("unexpected status code: %v", resp.Status)
+ }
+}
+
+var gerritCLCache = struct {
+ sync.Mutex
+ exist map[int]bool // exist is a set of Gerrit CL IDs that are known to exist.
+}{exist: make(map[int]bool)}
+
+var changeMap *hashMap
+
+// LoadChangeMap loads the specified map of Mercurial to Git revisions,
+// which is used by the /change/ handler to intelligently map old hg
+// revisions to their new git equivalents.
+// It should be called before calling Register.
+// The file should remain open as long as the process is running.
+// See the implementation of this package for details.
+func LoadChangeMap(filename string) error {
+ f, err := os.Open(filename)
+ if err != nil {
+ return err
+ }
+ m, err := newHashMap(f)
+ if err != nil {
+ return err
+ }
+ changeMap = m
+ return nil
+}
+
+func changeHandler(w http.ResponseWriter, r *http.Request) {
+ const prefix = "/change/"
+ if p := r.URL.Path; p == prefix {
+ // redirect /prefix/ to /prefix
+ http.Redirect(w, r, p[:len(p)-1], http.StatusFound)
+ return
+ }
+ hash := r.URL.Path[len(prefix):]
+ target := "https://go.googlesource.com/go/+/" + hash
+ if git := changeMap.Lookup(hash); git > 0 {
+ target = fmt.Sprintf("https://go.googlesource.com/%v/+/%v", git.Repo(), git.Hash())
+ }
+ http.Redirect(w, r, target, http.StatusFound)
+}
+
+func designHandler(w http.ResponseWriter, r *http.Request) {
+ const prefix = "/design/"
+ if p := r.URL.Path; p == prefix {
+ // redirect /prefix/ to /prefix
+ http.Redirect(w, r, p[:len(p)-1], http.StatusFound)
+ return
+ }
+ name := r.URL.Path[len(prefix):]
+ target := "https://go.googlesource.com/proposal/+/master/design/" + name + ".md"
+ http.Redirect(w, r, target, http.StatusFound)
+}
diff --git a/internal/redirect/redirect_test.go b/internal/redirect/redirect_test.go
new file mode 100644
index 00000000..804bfb00
--- /dev/null
+++ b/internal/redirect/redirect_test.go
@@ -0,0 +1,113 @@
+// Copyright 2015 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 redirect
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+type redirectResult struct {
+ status int
+ path string
+}
+
+func errorResult(status int) redirectResult {
+ return redirectResult{status, ""}
+}
+
+func TestRedirects(t *testing.T) {
+ var tests = map[string]redirectResult{
+ "/build": {301, "http://build.golang.org"},
+ "/ref": {301, "/doc/#references"},
+ "/doc/mem": {301, "/ref/mem"},
+ "/doc/spec": {301, "/ref/spec"},
+ "/tour": {301, "http://tour.golang.org"},
+ "/foo": errorResult(404),
+
+ "/pkg/asn1": {301, "/pkg/encoding/asn1/"},
+ "/pkg/template/parse": {301, "/pkg/text/template/parse/"},
+
+ "/src/pkg/foo": {301, "/src/foo"},
+
+ "/cmd/gofix": {301, "/cmd/fix/"},
+
+ // git commits (/change)
+ // TODO: mercurial tags and LoadChangeMap.
+ "/change": {301, "https://go.googlesource.com/go"},
+ "/change/a": {302, "https://go.googlesource.com/go/+/a"},
+
+ "/issue": {301, "https://github.com/golang/go/issues"},
+ "/issue?": {301, "https://github.com/golang/go/issues"},
+ "/issue/1": {302, "https://github.com/golang/go/issues/1"},
+ "/issue/new": {301, "https://github.com/golang/go/issues/new"},
+ "/issue/new?a=b&c=d%20&e=f": {301, "https://github.com/golang/go/issues/new?a=b&c=d%20&e=f"},
+ "/issues": {301, "https://github.com/golang/go/issues"},
+ "/issues/1": {302, "https://github.com/golang/go/issues/1"},
+ "/issues/new": {301, "https://github.com/golang/go/issues/new"},
+ "/issues/1/2/3": errorResult(404),
+
+ "/wiki/foo": {302, "https://github.com/golang/go/wiki/foo"},
+ "/wiki/foo/": {302, "https://github.com/golang/go/wiki/foo/"},
+
+ "/design": {301, "https://go.googlesource.com/proposal/+/master/design"},
+ "/design/": {302, "/design"},
+ "/design/123-foo": {302, "https://go.googlesource.com/proposal/+/master/design/123-foo.md"},
+ "/design/text/123-foo": {302, "https://go.googlesource.com/proposal/+/master/design/text/123-foo.md"},
+
+ "/cl/1": {302, "https://go-review.googlesource.com/1"},
+ "/cl/1/": {302, "https://go-review.googlesource.com/1"},
+ "/cl/267120043": {302, "https://codereview.appspot.com/267120043"},
+ "/cl/267120043/": {302, "https://codereview.appspot.com/267120043"},
+
+ // Verify that we're using the Rietveld CL table:
+ "/cl/152046": {302, "https://codereview.appspot.com/152046"},
+ "/cl/152047": {302, "https://go-review.googlesource.com/152047"},
+ "/cl/152048": {302, "https://codereview.appspot.com/152048"},
+
+ // And verify we're using the the "bigEnoughAssumeRietveld" value:
+ "/cl/299999": {302, "https://go-review.googlesource.com/299999"},
+ "/cl/300000": {302, "https://codereview.appspot.com/300000"},
+ }
+
+ mux := http.NewServeMux()
+ Register(mux)
+ ts := httptest.NewServer(mux)
+ defer ts.Close()
+
+ for path, want := range tests {
+ if want.path != "" && want.path[0] == '/' {
+ // All redirects are absolute.
+ want.path = ts.URL + want.path
+ }
+
+ req, err := http.NewRequest("GET", ts.URL+path, nil)
+ if err != nil {
+ t.Errorf("(path: %q) unexpected error: %v", path, err)
+ continue
+ }
+
+ resp, err := http.DefaultTransport.RoundTrip(req)
+ if err != nil {
+ t.Errorf("(path: %q) unexpected error: %v", path, err)
+ continue
+ }
+
+ if resp.StatusCode != want.status {
+ t.Errorf("(path: %q) got status %d, want %d", path, resp.StatusCode, want.status)
+ }
+
+ if want.status != 301 && want.status != 302 {
+ // Not a redirect. Just check status.
+ continue
+ }
+
+ out, _ := resp.Location()
+ if got := out.String(); got != want.path {
+ t.Errorf("(path: %q) got %s, want %s", path, got, want.path)
+ }
+ }
+}
diff --git a/internal/redirect/rietveld.go b/internal/redirect/rietveld.go
new file mode 100644
index 00000000..81b1094d
--- /dev/null
+++ b/internal/redirect/rietveld.go
@@ -0,0 +1,1093 @@
+// Copyright 2018 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 redirect
+
+// bigEnoughAssumeRietveld is the value where CLs equal or great are
+// assumed to be on Rietveld. By including this threshold we shrink
+// the size of the table below. When Go amasses 150,000 more CLs, we'll
+// need to bump this number and regenerate the list below.
+const bigEnoughAssumeRietveld = 300000
+
+// isRietveldCL reports whether cl was a Rietveld CL number.
+func isRietveldCL(cl int) bool {
+ return cl >= bigEnoughAssumeRietveld || lowRietveldCL[cl]
+}
+
+// lowRietveldCLs are the old CL numbers assigned by Rietveld code
+// review system as used by Go prior to Gerrit which are less than
+// bigEnoughAssumeRietveld.
+//
+// This list of numbers is registered with the /cl/NNNN redirect
+// handler to disambiguate which code review system a particular
+// number corresponds to. In some rare cases there may be duplicates,
+// in which case we might render an HTML choice for the user.
+//
+// To re-generate this list, run:
+//
+// $ cd $GOROOT
+// $ git log 7d7c6a9..94151eb | grep "^ https://golang.org/cl/" | perl -ne 's,^\s+https://golang.org/cl/(\d+).*$,$1,; chomp; print "$_: true,\n" if $_ < 300000' | sort -n | uniq
+//
+// Note that we ignore the x/* repos because we didn't start using
+// "subrepos" until the Rietveld CLs numbers were already 4,000,000+,
+// well above bigEnoughAssumeRietveld.
+var lowRietveldCL = map[int]bool{
+ 152046: true,
+ 152048: true,
+ 152049: true,
+ 152050: true,
+ 152051: true,
+ 152052: true,
+ 152055: true,
+ 152056: true,
+ 152057: true,
+ 152072: true,
+ 152073: true,
+ 152075: true,
+ 152076: true,
+ 152077: true,
+ 152078: true,
+ 152079: true,
+ 152080: true,
+ 152082: true,
+ 152084: true,
+ 152085: true,
+ 152086: true,
+ 152088: true,
+ 152089: true,
+ 152091: true,
+ 152098: true,
+ 152101: true,
+ 152102: true,
+ 152105: true,
+ 152106: true,
+ 152107: true,
+ 152108: true,
+ 152109: true,
+ 152110: true,
+ 152114: true,
+ 152117: true,
+ 152118: true,
+ 152120: true,
+ 152123: true,
+ 152124: true,
+ 152128: true,
+ 152130: true,
+ 152131: true,
+ 152138: true,
+ 152141: true,
+ 152142: true,
+ 153048: true,
+ 153049: true,
+ 153050: true,
+ 153051: true,
+ 153055: true,
+ 153056: true,
+ 153057: true,
+ 154043: true,
+ 154044: true,
+ 154045: true,
+ 154049: true,
+ 154055: true,
+ 154057: true,
+ 154058: true,
+ 154059: true,
+ 154061: true,
+ 154064: true,
+ 154065: true,
+ 154067: true,
+ 154068: true,
+ 154069: true,
+ 154071: true,
+ 154072: true,
+ 154073: true,
+ 154076: true,
+ 154079: true,
+ 154096: true,
+ 154097: true,
+ 154099: true,
+ 154100: true,
+ 154101: true,
+ 154102: true,
+ 154108: true,
+ 154118: true,
+ 154121: true,
+ 154122: true,
+ 154123: true,
+ 154125: true,
+ 154126: true,
+ 154128: true,
+ 154136: true,
+ 154138: true,
+ 154139: true,
+ 154140: true,
+ 154141: true,
+ 154142: true,
+ 154143: true,
+ 154144: true,
+ 154145: true,
+ 154146: true,
+ 154152: true,
+ 154153: true,
+ 154156: true,
+ 154159: true,
+ 154161: true,
+ 154166: true,
+ 154167: true,
+ 154169: true,
+ 154171: true,
+ 154172: true,
+ 154173: true,
+ 154174: true,
+ 154175: true,
+ 154176: true,
+ 154177: true,
+ 154178: true,
+ 154179: true,
+ 154180: true,
+ 155041: true,
+ 155042: true,
+ 155045: true,
+ 155047: true,
+ 155048: true,
+ 155049: true,
+ 155050: true,
+ 155054: true,
+ 155055: true,
+ 155056: true,
+ 155057: true,
+ 155058: true,
+ 155059: true,
+ 155061: true,
+ 155062: true,
+ 155063: true,
+ 155065: true,
+ 155067: true,
+ 155069: true,
+ 155072: true,
+ 155074: true,
+ 155075: true,
+ 155077: true,
+ 155078: true,
+ 155079: true,
+ 156041: true,
+ 156044: true,
+ 156045: true,
+ 156046: true,
+ 156047: true,
+ 156051: true,
+ 156052: true,
+ 156054: true,
+ 156055: true,
+ 156056: true,
+ 156058: true,
+ 156059: true,
+ 156060: true,
+ 156061: true,
+ 156062: true,
+ 156063: true,
+ 156066: true,
+ 156067: true,
+ 156070: true,
+ 156071: true,
+ 156073: true,
+ 156075: true,
+ 156077: true,
+ 156079: true,
+ 156080: true,
+ 156081: true,
+ 156083: true,
+ 156084: true,
+ 156085: true,
+ 156086: true,
+ 156089: true,
+ 156091: true,
+ 156092: true,
+ 156093: true,
+ 156094: true,
+ 156097: true,
+ 156099: true,
+ 156100: true,
+ 156102: true,
+ 156103: true,
+ 156104: true,
+ 156106: true,
+ 156107: true,
+ 156108: true,
+ 156109: true,
+ 156110: true,
+ 156113: true,
+ 156115: true,
+ 156116: true,
+ 157041: true,
+ 157042: true,
+ 157043: true,
+ 157044: true,
+ 157046: true,
+ 157053: true,
+ 157055: true,
+ 157056: true,
+ 157058: true,
+ 157060: true,
+ 157061: true,
+ 157062: true,
+ 157065: true,
+ 157066: true,
+ 157067: true,
+ 157068: true,
+ 157069: true,
+ 157071: true,
+ 157072: true,
+ 157073: true,
+ 157074: true,
+ 157075: true,
+ 157076: true,
+ 157077: true,
+ 157082: true,
+ 157084: true,
+ 157085: true,
+ 157087: true,
+ 157088: true,
+ 157091: true,
+ 157095: true,
+ 157096: true,
+ 157099: true,
+ 157100: true,
+ 157101: true,
+ 157102: true,
+ 157103: true,
+ 157104: true,
+ 157106: true,
+ 157110: true,
+ 157111: true,
+ 157112: true,
+ 157114: true,
+ 157116: true,
+ 157119: true,
+ 157140: true,
+ 157142: true,
+ 157143: true,
+ 157144: true,
+ 157146: true,
+ 157147: true,
+ 157149: true,
+ 157151: true,
+ 157152: true,
+ 157153: true,
+ 157154: true,
+ 157156: true,
+ 157157: true,
+ 157158: true,
+ 157159: true,
+ 157160: true,
+ 157162: true,
+ 157166: true,
+ 157167: true,
+ 157168: true,
+ 157170: true,
+ 158041: true,
+ 159044: true,
+ 159049: true,
+ 159050: true,
+ 159051: true,
+ 160043: true,
+ 160044: true,
+ 160045: true,
+ 160046: true,
+ 160047: true,
+ 160054: true,
+ 160056: true,
+ 160057: true,
+ 160059: true,
+ 160060: true,
+ 160061: true,
+ 160064: true,
+ 160065: true,
+ 160069: true,
+ 160070: true,
+ 161049: true,
+ 161050: true,
+ 161056: true,
+ 161058: true,
+ 161060: true,
+ 161061: true,
+ 161069: true,
+ 161070: true,
+ 161073: true,
+ 161075: true,
+ 162041: true,
+ 162044: true,
+ 162046: true,
+ 162053: true,
+ 162054: true,
+ 162055: true,
+ 162056: true,
+ 162057: true,
+ 162058: true,
+ 162059: true,
+ 162061: true,
+ 162062: true,
+ 163042: true,
+ 163044: true,
+ 163049: true,
+ 163050: true,
+ 163051: true,
+ 163052: true,
+ 163053: true,
+ 163055: true,
+ 163058: true,
+ 163061: true,
+ 163062: true,
+ 163064: true,
+ 163067: true,
+ 163068: true,
+ 163069: true,
+ 163070: true,
+ 163071: true,
+ 163072: true,
+ 163082: true,
+ 163083: true,
+ 163085: true,
+ 163088: true,
+ 163091: true,
+ 163092: true,
+ 163097: true,
+ 163098: true,
+ 164043: true,
+ 164047: true,
+ 164049: true,
+ 164052: true,
+ 164053: true,
+ 164056: true,
+ 164059: true,
+ 164060: true,
+ 164062: true,
+ 164068: true,
+ 164069: true,
+ 164071: true,
+ 164073: true,
+ 164074: true,
+ 164075: true,
+ 164078: true,
+ 164079: true,
+ 164081: true,
+ 164082: true,
+ 164083: true,
+ 164085: true,
+ 164086: true,
+ 164088: true,
+ 164090: true,
+ 164091: true,
+ 164092: true,
+ 164093: true,
+ 164094: true,
+ 164095: true,
+ 165042: true,
+ 165044: true,
+ 165045: true,
+ 165048: true,
+ 165049: true,
+ 165050: true,
+ 165051: true,
+ 165055: true,
+ 165057: true,
+ 165058: true,
+ 165059: true,
+ 165061: true,
+ 165062: true,
+ 165063: true,
+ 165064: true,
+ 165065: true,
+ 165068: true,
+ 165070: true,
+ 165076: true,
+ 165078: true,
+ 165080: true,
+ 165083: true,
+ 165086: true,
+ 165097: true,
+ 165100: true,
+ 165101: true,
+ 166041: true,
+ 166043: true,
+ 166044: true,
+ 166047: true,
+ 166049: true,
+ 166052: true,
+ 166053: true,
+ 166055: true,
+ 166058: true,
+ 166059: true,
+ 166060: true,
+ 166064: true,
+ 166066: true,
+ 166067: true,
+ 166068: true,
+ 166070: true,
+ 166071: true,
+ 166072: true,
+ 166073: true,
+ 166074: true,
+ 166076: true,
+ 166077: true,
+ 166078: true,
+ 166080: true,
+ 167043: true,
+ 167044: true,
+ 167047: true,
+ 167050: true,
+ 167055: true,
+ 167057: true,
+ 167058: true,
+ 168041: true,
+ 168045: true,
+ 170042: true,
+ 170043: true,
+ 170044: true,
+ 170046: true,
+ 170047: true,
+ 170048: true,
+ 170049: true,
+ 171044: true,
+ 171046: true,
+ 171047: true,
+ 171048: true,
+ 171051: true,
+ 172041: true,
+ 172042: true,
+ 172043: true,
+ 172045: true,
+ 172049: true,
+ 173041: true,
+ 173044: true,
+ 173045: true,
+ 174042: true,
+ 174047: true,
+ 174048: true,
+ 174050: true,
+ 174051: true,
+ 174052: true,
+ 174053: true,
+ 174063: true,
+ 174064: true,
+ 174072: true,
+ 174076: true,
+ 174077: true,
+ 174078: true,
+ 174082: true,
+ 174083: true,
+ 174087: true,
+ 175045: true,
+ 175046: true,
+ 175047: true,
+ 175048: true,
+ 176056: true,
+ 176057: true,
+ 176058: true,
+ 176061: true,
+ 176062: true,
+ 176063: true,
+ 176064: true,
+ 176066: true,
+ 176067: true,
+ 176070: true,
+ 176071: true,
+ 176076: true,
+ 178043: true,
+ 178044: true,
+ 178046: true,
+ 178048: true,
+ 179047: true,
+ 179055: true,
+ 179061: true,
+ 179062: true,
+ 179063: true,
+ 179067: true,
+ 179069: true,
+ 179070: true,
+ 179072: true,
+ 179079: true,
+ 179088: true,
+ 179095: true,
+ 179096: true,
+ 179097: true,
+ 179099: true,
+ 179105: true,
+ 179106: true,
+ 179108: true,
+ 179118: true,
+ 179120: true,
+ 179125: true,
+ 179126: true,
+ 179128: true,
+ 179129: true,
+ 179130: true,
+ 180044: true,
+ 180045: true,
+ 180046: true,
+ 180047: true,
+ 180048: true,
+ 180049: true,
+ 180050: true,
+ 180052: true,
+ 180053: true,
+ 180054: true,
+ 180055: true,
+ 180056: true,
+ 180057: true,
+ 180059: true,
+ 180061: true,
+ 180064: true,
+ 180065: true,
+ 180068: true,
+ 180069: true,
+ 180070: true,
+ 180074: true,
+ 180075: true,
+ 180081: true,
+ 180082: true,
+ 180085: true,
+ 180092: true,
+ 180099: true,
+ 180105: true,
+ 180108: true,
+ 180112: true,
+ 180118: true,
+ 181041: true,
+ 181043: true,
+ 181044: true,
+ 181045: true,
+ 181049: true,
+ 181050: true,
+ 181055: true,
+ 181057: true,
+ 181058: true,
+ 181059: true,
+ 181063: true,
+ 181071: true,
+ 181073: true,
+ 181075: true,
+ 181077: true,
+ 181080: true,
+ 181083: true,
+ 181084: true,
+ 181085: true,
+ 181086: true,
+ 181087: true,
+ 181089: true,
+ 181097: true,
+ 181099: true,
+ 181102: true,
+ 181111: true,
+ 181130: true,
+ 181135: true,
+ 181137: true,
+ 181138: true,
+ 181139: true,
+ 181151: true,
+ 181152: true,
+ 181153: true,
+ 181155: true,
+ 181156: true,
+ 181157: true,
+ 181158: true,
+ 181160: true,
+ 181161: true,
+ 181163: true,
+ 181164: true,
+ 181171: true,
+ 181179: true,
+ 181183: true,
+ 181184: true,
+ 181186: true,
+ 182041: true,
+ 182043: true,
+ 182044: true,
+ 183042: true,
+ 183043: true,
+ 183044: true,
+ 183047: true,
+ 183049: true,
+ 183065: true,
+ 183066: true,
+ 183073: true,
+ 183074: true,
+ 183075: true,
+ 183083: true,
+ 183084: true,
+ 183087: true,
+ 183088: true,
+ 183090: true,
+ 183095: true,
+ 183104: true,
+ 183107: true,
+ 183109: true,
+ 183111: true,
+ 183112: true,
+ 183113: true,
+ 183116: true,
+ 183123: true,
+ 183124: true,
+ 183125: true,
+ 183126: true,
+ 183132: true,
+ 183133: true,
+ 183135: true,
+ 183136: true,
+ 183137: true,
+ 183138: true,
+ 183139: true,
+ 183140: true,
+ 183141: true,
+ 183142: true,
+ 183153: true,
+ 183155: true,
+ 183156: true,
+ 183157: true,
+ 183160: true,
+ 184043: true,
+ 184055: true,
+ 184058: true,
+ 184059: true,
+ 184068: true,
+ 184069: true,
+ 184079: true,
+ 184080: true,
+ 184081: true,
+ 185043: true,
+ 185045: true,
+ 186042: true,
+ 186043: true,
+ 186073: true,
+ 186076: true,
+ 186077: true,
+ 186078: true,
+ 186079: true,
+ 186081: true,
+ 186095: true,
+ 186108: true,
+ 186113: true,
+ 186115: true,
+ 186116: true,
+ 186118: true,
+ 186119: true,
+ 186132: true,
+ 186137: true,
+ 186138: true,
+ 186139: true,
+ 186143: true,
+ 186144: true,
+ 186145: true,
+ 186146: true,
+ 186147: true,
+ 186148: true,
+ 186159: true,
+ 186160: true,
+ 186161: true,
+ 186165: true,
+ 186169: true,
+ 186173: true,
+ 186180: true,
+ 186210: true,
+ 186211: true,
+ 186212: true,
+ 186213: true,
+ 186214: true,
+ 186215: true,
+ 186216: true,
+ 186228: true,
+ 186229: true,
+ 186230: true,
+ 186232: true,
+ 186234: true,
+ 186255: true,
+ 186263: true,
+ 186276: true,
+ 186279: true,
+ 186282: true,
+ 186283: true,
+ 188043: true,
+ 189042: true,
+ 189057: true,
+ 189059: true,
+ 189062: true,
+ 189078: true,
+ 189080: true,
+ 189083: true,
+ 189088: true,
+ 189093: true,
+ 189095: true,
+ 189096: true,
+ 189098: true,
+ 189100: true,
+ 190041: true,
+ 190042: true,
+ 190043: true,
+ 190044: true,
+ 190059: true,
+ 190062: true,
+ 190068: true,
+ 190074: true,
+ 190076: true,
+ 190077: true,
+ 190079: true,
+ 190085: true,
+ 190088: true,
+ 190103: true,
+ 190104: true,
+ 193055: true,
+ 193066: true,
+ 193067: true,
+ 193070: true,
+ 193075: true,
+ 193079: true,
+ 193080: true,
+ 193081: true,
+ 193091: true,
+ 193092: true,
+ 193095: true,
+ 193101: true,
+ 193104: true,
+ 194043: true,
+ 194045: true,
+ 194046: true,
+ 194050: true,
+ 194051: true,
+ 194052: true,
+ 194053: true,
+ 194064: true,
+ 194066: true,
+ 194069: true,
+ 194071: true,
+ 194072: true,
+ 194073: true,
+ 194074: true,
+ 194076: true,
+ 194077: true,
+ 194078: true,
+ 194082: true,
+ 194084: true,
+ 194085: true,
+ 194090: true,
+ 194091: true,
+ 194092: true,
+ 194094: true,
+ 194097: true,
+ 194098: true,
+ 194099: true,
+ 194100: true,
+ 194114: true,
+ 194116: true,
+ 194118: true,
+ 194119: true,
+ 194120: true,
+ 194121: true,
+ 194122: true,
+ 194126: true,
+ 194129: true,
+ 194131: true,
+ 194132: true,
+ 194133: true,
+ 194134: true,
+ 194146: true,
+ 194151: true,
+ 194156: true,
+ 194157: true,
+ 194159: true,
+ 194161: true,
+ 194165: true,
+ 195041: true,
+ 195044: true,
+ 195050: true,
+ 195051: true,
+ 195052: true,
+ 195068: true,
+ 195075: true,
+ 195076: true,
+ 195079: true,
+ 195080: true,
+ 195081: true,
+ 196042: true,
+ 196044: true,
+ 196050: true,
+ 196051: true,
+ 196055: true,
+ 196056: true,
+ 196061: true,
+ 196063: true,
+ 196065: true,
+ 196070: true,
+ 196071: true,
+ 196075: true,
+ 196077: true,
+ 196079: true,
+ 196087: true,
+ 196088: true,
+ 196090: true,
+ 196091: true,
+ 197041: true,
+ 197042: true,
+ 197043: true,
+ 197044: true,
+ 198044: true,
+ 198045: true,
+ 198046: true,
+ 198048: true,
+ 198049: true,
+ 198050: true,
+ 198053: true,
+ 198057: true,
+ 198058: true,
+ 198066: true,
+ 198071: true,
+ 198074: true,
+ 198081: true,
+ 198084: true,
+ 198085: true,
+ 198102: true,
+ 199042: true,
+ 199044: true,
+ 199045: true,
+ 199046: true,
+ 199047: true,
+ 199052: true,
+ 199054: true,
+ 199057: true,
+ 199066: true,
+ 199070: true,
+ 199082: true,
+ 199091: true,
+ 199094: true,
+ 199096: true,
+ 201041: true,
+ 201042: true,
+ 201043: true,
+ 201047: true,
+ 201048: true,
+ 201049: true,
+ 201058: true,
+ 201061: true,
+ 201064: true,
+ 201065: true,
+ 201068: true,
+ 202042: true,
+ 202043: true,
+ 202044: true,
+ 202051: true,
+ 202054: true,
+ 202055: true,
+ 203043: true,
+ 203050: true,
+ 203051: true,
+ 203053: true,
+ 203060: true,
+ 203062: true,
+ 204042: true,
+ 204044: true,
+ 204048: true,
+ 204052: true,
+ 204053: true,
+ 204061: true,
+ 204062: true,
+ 204064: true,
+ 204065: true,
+ 204067: true,
+ 204068: true,
+ 204069: true,
+ 205042: true,
+ 205044: true,
+ 206043: true,
+ 206044: true,
+ 206047: true,
+ 206050: true,
+ 206051: true,
+ 206052: true,
+ 206053: true,
+ 206054: true,
+ 206055: true,
+ 206058: true,
+ 206059: true,
+ 206060: true,
+ 206067: true,
+ 206069: true,
+ 206077: true,
+ 206078: true,
+ 206079: true,
+ 206084: true,
+ 206089: true,
+ 206101: true,
+ 206107: true,
+ 206109: true,
+ 207043: true,
+ 207044: true,
+ 207049: true,
+ 207050: true,
+ 207051: true,
+ 207052: true,
+ 207053: true,
+ 207054: true,
+ 207055: true,
+ 207061: true,
+ 207062: true,
+ 207069: true,
+ 207071: true,
+ 207085: true,
+ 207086: true,
+ 207087: true,
+ 207088: true,
+ 207095: true,
+ 207096: true,
+ 207102: true,
+ 207103: true,
+ 207106: true,
+ 207108: true,
+ 207110: true,
+ 207111: true,
+ 207112: true,
+ 209041: true,
+ 209042: true,
+ 209043: true,
+ 209044: true,
+ 210042: true,
+ 210043: true,
+ 210044: true,
+ 210047: true,
+ 211041: true,
+ 212041: true,
+ 212045: true,
+ 212046: true,
+ 212047: true,
+ 213041: true,
+ 213042: true,
+ 214042: true,
+ 214046: true,
+ 214049: true,
+ 214050: true,
+ 215042: true,
+ 215048: true,
+ 215050: true,
+ 216043: true,
+ 216046: true,
+ 216047: true,
+ 216052: true,
+ 216053: true,
+ 216054: true,
+ 216059: true,
+ 216068: true,
+ 217041: true,
+ 217044: true,
+ 217047: true,
+ 217048: true,
+ 217049: true,
+ 217056: true,
+ 217058: true,
+ 217059: true,
+ 217060: true,
+ 217061: true,
+ 217064: true,
+ 217066: true,
+ 217069: true,
+ 217071: true,
+ 217085: true,
+ 217086: true,
+ 217088: true,
+ 217093: true,
+ 217094: true,
+ 217108: true,
+ 217109: true,
+ 217111: true,
+ 217115: true,
+ 217116: true,
+ 218042: true,
+ 218044: true,
+ 218046: true,
+ 218050: true,
+ 218060: true,
+ 218061: true,
+ 218063: true,
+ 218064: true,
+ 218065: true,
+ 218070: true,
+ 218071: true,
+ 218072: true,
+ 218074: true,
+ 218076: true,
+ 222041: true,
+ 223041: true,
+ 223043: true,
+ 223044: true,
+ 223050: true,
+ 223052: true,
+ 223054: true,
+ 223058: true,
+ 223059: true,
+ 223061: true,
+ 223068: true,
+ 223069: true,
+ 223070: true,
+ 223071: true,
+ 223073: true,
+ 223075: true,
+ 223076: true,
+ 223083: true,
+ 223087: true,
+ 223094: true,
+ 223096: true,
+ 223101: true,
+ 223106: true,
+ 223108: true,
+ 224041: true,
+ 224042: true,
+ 224043: true,
+ 224045: true,
+ 224051: true,
+ 224053: true,
+ 224057: true,
+ 224060: true,
+ 224061: true,
+ 224062: true,
+ 224063: true,
+ 224068: true,
+ 224069: true,
+ 224081: true,
+ 224084: true,
+ 224087: true,
+ 224090: true,
+ 224096: true,
+ 224105: true,
+ 225042: true,
+ 227041: true,
+ 229045: true,
+ 229046: true,
+ 229048: true,
+ 229049: true,
+ 229050: true,
+ 231042: true,
+ 236041: true,
+ 237041: true,
+ 238041: true,
+ 238042: true,
+ 240041: true,
+ 240042: true,
+ 240043: true,
+ 241041: true,
+ 243041: true,
+ 244041: true,
+ 245041: true,
+ 247041: true,
+ 250041: true,
+ 252041: true,
+ 253041: true,
+ 253045: true,
+ 254043: true,
+ 255042: true,
+ 255043: true,
+ 257041: true,
+ 257042: true,
+ 258041: true,
+ 261041: true,
+ 264041: true,
+ 294042: true,
+ 296042: true,
+}
diff --git a/internal/short/short.go b/internal/short/short.go
new file mode 100644
index 00000000..3a399ab9
--- /dev/null
+++ b/internal/short/short.go
@@ -0,0 +1,186 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+// +build golangorg
+
+// Package short implements a simple URL shortener, serving an administrative
+// interface at /s and shortened urls from /s/key.
+// It is designed to run only on the instance of godoc that serves golang.org.
+package short
+
+// TODO(adg): collect statistics on URL visits
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "html/template"
+ "io"
+ "log"
+ "net/http"
+ "net/url"
+ "regexp"
+
+ "cloud.google.com/go/datastore"
+ "golang.org/x/tools/internal/memcache"
+ "google.golang.org/appengine/user"
+)
+
+const (
+ prefix = "/s"
+ kind = "Link"
+ baseURL = "https://golang.org" + prefix
+)
+
+// Link represents a short link.
+type Link struct {
+ Key, Target string
+}
+
+var validKey = regexp.MustCompile(`^[a-zA-Z0-9-_.]+$`)
+
+type server struct {
+ datastore *datastore.Client
+ memcache *memcache.CodecClient
+}
+
+func RegisterHandlers(mux *http.ServeMux, dc *datastore.Client, mc *memcache.Client) {
+ s := server{dc, mc.WithCodec(memcache.JSON)}
+ mux.HandleFunc(prefix+"/", s.linkHandler)
+
+ // TODO(cbro): move storage of the links to a text file in Gerrit.
+ // Disable the admin handler until that happens, since GAE Flex doesn't support
+ // the "google.golang.org/appengine/user" package.
+ // See golang.org/issue/27205#issuecomment-418673218
+ // mux.HandleFunc(prefix, adminHandler)
+ mux.HandleFunc(prefix, func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusForbidden)
+ io.WriteString(w, "Link creation temporarily unavailable. See golang.org/issue/27205.")
+ })
+}
+
+// linkHandler services requests to short URLs.
+// http://golang.org/s/key
+// It consults memcache and datastore for the Link for key.
+// It then sends a redirects or an error message.
+func (h server) linkHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ key := r.URL.Path[len(prefix)+1:]
+ if !validKey.MatchString(key) {
+ http.Error(w, "not found", http.StatusNotFound)
+ return
+ }
+
+ var link Link
+ if err := h.memcache.Get(ctx, cacheKey(key), &link); err != nil {
+ k := datastore.NameKey(kind, key, nil)
+ err = h.datastore.Get(ctx, k, &link)
+ switch err {
+ case datastore.ErrNoSuchEntity:
+ http.Error(w, "not found", http.StatusNotFound)
+ return
+ default: // != nil
+ log.Printf("ERROR %q: %v", key, err)
+ http.Error(w, "internal server error", http.StatusInternalServerError)
+ return
+ case nil:
+ item := &memcache.Item{
+ Key: cacheKey(key),
+ Object: &link,
+ }
+ if err := h.memcache.Set(ctx, item); err != nil {
+ log.Printf("WARNING %q: %v", key, err)
+ }
+ }
+ }
+
+ http.Redirect(w, r, link.Target, http.StatusFound)
+}
+
+var adminTemplate = template.Must(template.New("admin").Parse(templateHTML))
+
+// adminHandler serves an administrative interface.
+func (h server) adminHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ if !user.IsAdmin(ctx) {
+ http.Error(w, "forbidden", http.StatusForbidden)
+ return
+ }
+
+ var newLink *Link
+ var doErr error
+ if r.Method == "POST" {
+ key := r.FormValue("key")
+ switch r.FormValue("do") {
+ case "Add":
+ newLink = &Link{key, r.FormValue("target")}
+ doErr = h.putLink(ctx, newLink)
+ case "Delete":
+ k := datastore.NameKey(kind, key, nil)
+ doErr = h.datastore.Delete(ctx, k)
+ default:
+ http.Error(w, "unknown action", http.StatusBadRequest)
+ }
+ err := h.memcache.Delete(ctx, cacheKey(key))
+ if err != nil && err != memcache.ErrCacheMiss {
+ log.Printf("WARNING %q: %v", key, err)
+ }
+ }
+
+ var links []*Link
+ q := datastore.NewQuery(kind).Order("Key")
+ if _, err := h.datastore.GetAll(ctx, q, &links); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ log.Printf("ERROR %v", err)
+ return
+ }
+
+ // Put the new link in the list if it's not there already.
+ // (Eventual consistency means that it might not show up
+ // immediately, which might be confusing for the user.)
+ if newLink != nil && doErr == nil {
+ found := false
+ for i := range links {
+ if links[i].Key == newLink.Key {
+ found = true
+ break
+ }
+ }
+ if !found {
+ links = append([]*Link{newLink}, links...)
+ }
+ newLink = nil
+ }
+
+ var data = struct {
+ BaseURL string
+ Prefix string
+ Links []*Link
+ New *Link
+ Error error
+ }{baseURL, prefix, links, newLink, doErr}
+ if err := adminTemplate.Execute(w, &data); err != nil {
+ log.Printf("ERROR adminTemplate: %v", err)
+ }
+}
+
+// putLink validates the provided link and puts it into the datastore.
+func (h server) putLink(ctx context.Context, link *Link) error {
+ if !validKey.MatchString(link.Key) {
+ return errors.New("invalid key; must match " + validKey.String())
+ }
+ if _, err := url.Parse(link.Target); err != nil {
+ return fmt.Errorf("bad target: %v", err)
+ }
+ k := datastore.NameKey(kind, link.Key, nil)
+ _, err := h.datastore.Put(ctx, k, link)
+ return err
+}
+
+// cacheKey returns a short URL key as a memcache key.
+func cacheKey(key string) string {
+ return "link-" + key
+}
diff --git a/internal/short/tmpl.go b/internal/short/tmpl.go
new file mode 100644
index 00000000..66f5401e
--- /dev/null
+++ b/internal/short/tmpl.go
@@ -0,0 +1,119 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package short
+
+const templateHTML = `
+
+
+
+golang.org URL shortener
+
+
+
+
+
+
+
+
+
+
+
+`