web.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. package web
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "log"
  8. "net/http"
  9. "net/url"
  10. "strings"
  11. "time"
  12. "github.com/kellegous/go/context"
  13. "github.com/syndtr/goleveldb/leveldb"
  14. )
  15. const (
  16. alpha = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  17. prefix = ":"
  18. )
  19. var (
  20. errInvalidURL = errors.New("Invalid URL")
  21. errRedirectLoop = errors.New(" I'm sorry, Dave. I'm afraid I can't do that")
  22. )
  23. // A very simple encoding of numeric ids. This is simply a base62 encoding
  24. // prefixed with ":"
  25. func encodeID(id uint64) string {
  26. n := uint64(len(alpha))
  27. b := make([]byte, 0, 8)
  28. if id == 0 {
  29. return "0"
  30. }
  31. b = append(b, ':')
  32. for id > 0 {
  33. b = append(b, alpha[id%n])
  34. id /= n
  35. }
  36. return string(b)
  37. }
  38. // Clean a shortcut name. Currently this just means stripping any leading
  39. // ":" to avoid collisions with auto generated names.
  40. func cleanName(name string) string {
  41. for strings.HasPrefix(name, prefix) {
  42. name = name[1:]
  43. }
  44. return name
  45. }
  46. // Parse the shortcut name from the give URL path, given the base URL that is
  47. // handling the request.
  48. func parseName(base, path string) string {
  49. t := path[len(base):]
  50. ix := strings.Index(t, "/")
  51. if ix == -1 {
  52. return t
  53. }
  54. return t[:ix]
  55. }
  56. // Used as an API response, this is a route with its associated shortcut name.
  57. type routeWithName struct {
  58. Name string `json:"name"`
  59. *context.Route
  60. }
  61. // The response type for all API responses.
  62. type msg struct {
  63. Ok bool `json:"ok"`
  64. Error string `json:"error,omitempty"`
  65. Route *routeWithName `json:"route,omitempty"`
  66. }
  67. // Encode the given data to JSON and send it to the client.
  68. func writeJSON(w http.ResponseWriter, data interface{}, status int) {
  69. w.Header().Set("Content-Type", "application/json;charset=utf-8")
  70. w.WriteHeader(status)
  71. if err := json.NewEncoder(w).Encode(data); err != nil {
  72. log.Panic(err)
  73. }
  74. }
  75. // Encode the given named route as a msg and send it to the client.
  76. func writeJSONRoute(w http.ResponseWriter, name string, rt *context.Route) {
  77. writeJSON(w, &msg{
  78. Ok: true,
  79. Route: &routeWithName{
  80. Name: name,
  81. Route: rt,
  82. },
  83. }, http.StatusOK)
  84. }
  85. // Encode a simple success msg and send it to the client.
  86. func writeJSONOk(w http.ResponseWriter) {
  87. writeJSON(w, &msg{
  88. Ok: true,
  89. }, http.StatusOK)
  90. }
  91. // Encode an error response and send it to the client.
  92. func writeJSONError(w http.ResponseWriter, err string) {
  93. writeJSON(w, &msg{
  94. Ok: false,
  95. Error: err,
  96. }, http.StatusOK)
  97. }
  98. // Encode a generic backend error and send it to the client.
  99. func writeJSONBackendError(w http.ResponseWriter, err error) {
  100. log.Printf("[error] %s", err)
  101. writeJSONError(w, "backend error")
  102. }
  103. // Serve a bundled asset over HTTP.
  104. func serveAsset(w http.ResponseWriter, r *http.Request, name string) {
  105. n, err := AssetInfo(name)
  106. if err != nil {
  107. http.NotFound(w, r)
  108. return
  109. }
  110. a, err := Asset(name)
  111. if err != nil {
  112. http.NotFound(w, r)
  113. return
  114. }
  115. http.ServeContent(w, r, n.Name(), n.ModTime(), bytes.NewReader(a))
  116. }
  117. // The handler that processes all API requests.
  118. type apiHandler struct {
  119. ctx *context.Context
  120. }
  121. // Check that the given URL is suitable as a shortcut link.
  122. func validateURL(r *http.Request, s string) error {
  123. u, err := url.Parse(s)
  124. if err != nil {
  125. return errInvalidURL
  126. }
  127. switch u.Scheme {
  128. case "http", "https", "mailto", "ftp":
  129. break
  130. default:
  131. return errInvalidURL
  132. }
  133. if r.Host == u.Host {
  134. return errRedirectLoop
  135. }
  136. return nil
  137. }
  138. // Handle a POST request to the API.
  139. func apiPost(ctx *context.Context, w http.ResponseWriter, r *http.Request) {
  140. p := parseName("/api/url/", r.URL.Path)
  141. var req struct {
  142. URL string `json:"url"`
  143. }
  144. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  145. writeJSONError(w, "invalid json")
  146. return
  147. }
  148. // Handle delete requests
  149. if req.URL == "" {
  150. if p == "" {
  151. writeJSONError(w, "url required")
  152. return
  153. }
  154. if err := ctx.Del(p); err != nil {
  155. writeJSONBackendError(w, err)
  156. return
  157. }
  158. writeJSONOk(w)
  159. return
  160. }
  161. if err := validateURL(r, req.URL); err != nil {
  162. writeJSONError(w, err.Error())
  163. return
  164. }
  165. // If no name is specified, an ID must be generate.
  166. if p == "" {
  167. id, err := ctx.NextID()
  168. if err != nil {
  169. writeJSONBackendError(w, err)
  170. return
  171. }
  172. p = encodeID(id)
  173. }
  174. rt := context.Route{
  175. URL: req.URL,
  176. Time: time.Now(),
  177. }
  178. if err := ctx.Put(p, &rt); err != nil {
  179. writeJSONBackendError(w, err)
  180. return
  181. }
  182. writeJSONRoute(w, p, &rt)
  183. }
  184. // Handle a GET request to the API.
  185. func apiGet(ctx *context.Context, w http.ResponseWriter, r *http.Request) {
  186. p := parseName("/api/url/", r.URL.Path)
  187. if p == "" {
  188. writeJSONOk(w)
  189. return
  190. }
  191. rt, err := ctx.Get(p)
  192. if err == leveldb.ErrNotFound {
  193. writeJSONOk(w)
  194. return
  195. } else if err != nil {
  196. writeJSONBackendError(w, err)
  197. return
  198. }
  199. writeJSONRoute(w, p, rt)
  200. }
  201. func (h *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  202. switch r.Method {
  203. case "POST":
  204. apiPost(h.ctx, w, r)
  205. case "GET":
  206. apiGet(h.ctx, w, r)
  207. default:
  208. writeJSONError(w, http.StatusText(http.StatusMethodNotAllowed))
  209. }
  210. }
  211. // The default handler responds to most requests. It is responsible for the
  212. // shortcut redirects and for sending unmapped shortcuts to the edit page.
  213. type defaultHandler struct {
  214. ctx *context.Context
  215. }
  216. func (h *defaultHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  217. p := parseName("/", r.URL.Path)
  218. if p == "" {
  219. http.Redirect(w, r, "/edit/", http.StatusTemporaryRedirect)
  220. return
  221. }
  222. rt, err := h.ctx.Get(p)
  223. if err == leveldb.ErrNotFound {
  224. http.Redirect(w, r,
  225. fmt.Sprintf("/edit/%s", cleanName(p)),
  226. http.StatusTemporaryRedirect)
  227. return
  228. } else if err != nil {
  229. log.Panic(err)
  230. }
  231. http.Redirect(w, r,
  232. rt.URL,
  233. http.StatusTemporaryRedirect)
  234. }
  235. // Setup a Mux with all web routes.
  236. func allRoutes(ctx *context.Context) *http.ServeMux {
  237. mux := http.NewServeMux()
  238. mux.Handle("/", &defaultHandler{ctx})
  239. mux.Handle("/api/url/", &apiHandler{ctx})
  240. mux.HandleFunc("/edit/", func(w http.ResponseWriter, r *http.Request) {
  241. serveAsset(w, r, "index.html")
  242. })
  243. mux.HandleFunc("/s/", func(w http.ResponseWriter, r *http.Request) {
  244. serveAsset(w, r, r.URL.Path[len("/s/"):])
  245. })
  246. return mux
  247. }
  248. // ListenAndServe sets up all web routes, binds the port and handles incoming
  249. // web requests.
  250. func ListenAndServe(addr string, ctx *context.Context) error {
  251. return http.ListenAndServe(addr, allRoutes(ctx))
  252. }