| // 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/29988 and 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/29988.") |
| }) |
| } |
| |
| // 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 |
| } |