| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 |
- package web
- import (
- "bytes"
- "encoding/json"
- "errors"
- "fmt"
- "log"
- "net/http"
- "net/url"
- "strings"
- "time"
- "github.com/kellegous/go/context"
- "github.com/syndtr/goleveldb/leveldb"
- )
- const (
- alpha = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
- prefix = ":"
- )
- var (
- errInvalidURL = errors.New("Invalid URL")
- errRedirectLoop = errors.New(" I'm sorry, Dave. I'm afraid I can't do that")
- )
- // A very simple encoding of numeric ids. This is simply a base62 encoding
- // prefixed with ":"
- func encodeID(id uint64) string {
- n := uint64(len(alpha))
- b := make([]byte, 0, 8)
- if id == 0 {
- return "0"
- }
- b = append(b, ':')
- for id > 0 {
- b = append(b, alpha[id%n])
- id /= n
- }
- return string(b)
- }
- // Clean a shortcut name. Currently this just means stripping any leading
- // ":" to avoid collisions with auto generated names.
- func cleanName(name string) string {
- for strings.HasPrefix(name, prefix) {
- name = name[1:]
- }
- return name
- }
- // Parse the shortcut name from the give URL path, given the base URL that is
- // handling the request.
- func parseName(base, path string) string {
- t := path[len(base):]
- ix := strings.Index(t, "/")
- if ix == -1 {
- return t
- }
- return t[:ix]
- }
- // Used as an API response, this is a route with its associated shortcut name.
- type routeWithName struct {
- Name string `json:"name"`
- *context.Route
- }
- // The response type for all API responses.
- type msg struct {
- Ok bool `json:"ok"`
- Error string `json:"error,omitempty"`
- Route *routeWithName `json:"route,omitempty"`
- }
- // Encode the given data to JSON and send it to the client.
- func writeJSON(w http.ResponseWriter, data interface{}, status int) {
- w.Header().Set("Content-Type", "application/json;charset=utf-8")
- w.WriteHeader(status)
- if err := json.NewEncoder(w).Encode(data); err != nil {
- log.Panic(err)
- }
- }
- // Encode the given named route as a msg and send it to the client.
- func writeJSONRoute(w http.ResponseWriter, name string, rt *context.Route) {
- writeJSON(w, &msg{
- Ok: true,
- Route: &routeWithName{
- Name: name,
- Route: rt,
- },
- }, http.StatusOK)
- }
- // Encode a simple success msg and send it to the client.
- func writeJSONOk(w http.ResponseWriter) {
- writeJSON(w, &msg{
- Ok: true,
- }, http.StatusOK)
- }
- // Encode an error response and send it to the client.
- func writeJSONError(w http.ResponseWriter, err string) {
- writeJSON(w, &msg{
- Ok: false,
- Error: err,
- }, http.StatusOK)
- }
- // Encode a generic backend error and send it to the client.
- func writeJSONBackendError(w http.ResponseWriter, err error) {
- log.Printf("[error] %s", err)
- writeJSONError(w, "backend error")
- }
- // Serve a bundled asset over HTTP.
- func serveAsset(w http.ResponseWriter, r *http.Request, name string) {
- n, err := AssetInfo(name)
- if err != nil {
- http.NotFound(w, r)
- return
- }
- a, err := Asset(name)
- if err != nil {
- http.NotFound(w, r)
- return
- }
- http.ServeContent(w, r, n.Name(), n.ModTime(), bytes.NewReader(a))
- }
- // The handler that processes all API requests.
- type apiHandler struct {
- ctx *context.Context
- }
- // Check that the given URL is suitable as a shortcut link.
- func validateURL(r *http.Request, s string) error {
- u, err := url.Parse(s)
- if err != nil {
- return errInvalidURL
- }
- switch u.Scheme {
- case "http", "https", "mailto", "ftp":
- break
- default:
- return errInvalidURL
- }
- if r.Host == u.Host {
- return errRedirectLoop
- }
- return nil
- }
- // Handle a POST request to the API.
- func apiPost(ctx *context.Context, w http.ResponseWriter, r *http.Request) {
- p := parseName("/api/url/", r.URL.Path)
- var req struct {
- URL string `json:"url"`
- }
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- writeJSONError(w, "invalid json")
- return
- }
- // Handle delete requests
- if req.URL == "" {
- if p == "" {
- writeJSONError(w, "url required")
- return
- }
- if err := ctx.Del(p); err != nil {
- writeJSONBackendError(w, err)
- return
- }
- writeJSONOk(w)
- return
- }
- if err := validateURL(r, req.URL); err != nil {
- writeJSONError(w, err.Error())
- return
- }
- // If no name is specified, an ID must be generate.
- if p == "" {
- id, err := ctx.NextID()
- if err != nil {
- writeJSONBackendError(w, err)
- return
- }
- p = encodeID(id)
- }
- rt := context.Route{
- URL: req.URL,
- Time: time.Now(),
- }
- if err := ctx.Put(p, &rt); err != nil {
- writeJSONBackendError(w, err)
- return
- }
- writeJSONRoute(w, p, &rt)
- }
- // Handle a GET request to the API.
- func apiGet(ctx *context.Context, w http.ResponseWriter, r *http.Request) {
- p := parseName("/api/url/", r.URL.Path)
- if p == "" {
- writeJSONOk(w)
- return
- }
- rt, err := ctx.Get(p)
- if err == leveldb.ErrNotFound {
- writeJSONOk(w)
- return
- } else if err != nil {
- writeJSONBackendError(w, err)
- return
- }
- writeJSONRoute(w, p, rt)
- }
- func (h *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- switch r.Method {
- case "POST":
- apiPost(h.ctx, w, r)
- case "GET":
- apiGet(h.ctx, w, r)
- default:
- writeJSONError(w, http.StatusText(http.StatusMethodNotAllowed))
- }
- }
- // The default handler responds to most requests. It is responsible for the
- // shortcut redirects and for sending unmapped shortcuts to the edit page.
- type defaultHandler struct {
- ctx *context.Context
- }
- func (h *defaultHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- p := parseName("/", r.URL.Path)
- if p == "" {
- http.Redirect(w, r, "/edit/", http.StatusTemporaryRedirect)
- return
- }
- rt, err := h.ctx.Get(p)
- if err == leveldb.ErrNotFound {
- http.Redirect(w, r,
- fmt.Sprintf("/edit/%s", cleanName(p)),
- http.StatusTemporaryRedirect)
- return
- } else if err != nil {
- log.Panic(err)
- }
- http.Redirect(w, r,
- rt.URL,
- http.StatusTemporaryRedirect)
- }
- // Setup a Mux with all web routes.
- func allRoutes(ctx *context.Context, version string) *http.ServeMux {
- mux := http.NewServeMux()
- mux.Handle("/", &defaultHandler{ctx})
- mux.Handle("/api/url/", &apiHandler{ctx})
- mux.HandleFunc("/edit/", func(w http.ResponseWriter, r *http.Request) {
- serveAsset(w, r, "index.html")
- })
- mux.HandleFunc("/s/", func(w http.ResponseWriter, r *http.Request) {
- serveAsset(w, r, r.URL.Path[len("/s/"):])
- })
- mux.HandleFunc("/:version", func(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintln(w, version)
- })
- return mux
- }
- // ListenAndServe sets up all web routes, binds the port and handles incoming
- // web requests.
- func ListenAndServe(addr, version string, ctx *context.Context) error {
- return http.ListenAndServe(addr, allRoutes(ctx, version))
- }
|