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}} + +{{range .}} +{{template "download" .}} +{{end}} +{{end}} + +
+ +{{with .Stable}} +

Stable versions

+{{template "releases" .}} +{{end}} + +{{with .Unstable}} +

Unstable version

+{{template "releases" .}} +{{end}} + +{{with .Archive}} +
+ +
+

Archived versions▾

+ {{template "releases" .}} +
+
+{{end}} + + + +
+
+ + + + + + +{{end}} + +{{define "releases"}} +{{range .}} +
+ +
+

{{.Version}} ▾

+ {{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"}} + + + + + + + + + {{/* Use the checksum type of the first file for the column heading. */}} + + + +{{if .SplitPortTable}} + {{range .Files}}{{if .PrimaryPort}}{{template "file" .}}{{end}}{{end}} + + {{/* TODO(cbro): add a link to an explanatory doc page */}} + + {{range .Files}}{{if not .PrimaryPort}}{{template "file" .}}{{end}}{{end}} +{{else}} + {{range .Files}}{{template "file" .}}{{end}} +{{end}} +
File nameKindOSArchSize{{(index .Files 0).ChecksumType}} Checksum
Other Ports
+{{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 + + + + + + +{{with .Error}} + + + + + + +{{end}} + + + + + + + + + + + + + + +{{with .Links}} + + + + + +{{range .}} + + + + + +{{end}} +{{end}} + +
Error
{{.}}
KeyTarget
+
Short Link  
+
+ + +
+
+ + + + + + +`