// 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 }