| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- package web
- import (
- "context"
- "encoding/base64"
- "encoding/json"
- "errors"
- "net/http"
- "net/url"
- "strconv"
- "strings"
- "time"
- "github.com/kellegous/go/backend"
- "github.com/kellegous/go/internal"
- )
- const (
- alpha = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
- )
- var (
- errInvalidURL = errors.New("Invalid URL")
- errRedirectLoop = errors.New(" I'm sorry, Dave. I'm afraid I can't do that")
- genURLPrefix byte = ':'
- postGenCursor = []byte{genURLPrefix + 1}
- )
- // 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, genURLPrefix)
- for id > 0 {
- b = append(b, alpha[id%n])
- id /= n
- }
- return string(b)
- }
- // Advance to the next id and encode it as an ID.
- func nextEncodedID(ctx context.Context, backend backend.Backend) (string, error) {
- id, err := backend.NextID(ctx)
- if err != nil {
- return "", err
- }
- return encodeID(id), nil
- }
- // 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
- }
- func apiURLPost(backend backend.Backend, host string, 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", http.StatusBadRequest)
- return
- }
- if req.URL == "" {
- writeJSONError(w, "url required", http.StatusBadRequest)
- return
- }
- if isBannedName(p) {
- writeJSONError(w, "name cannot be used", http.StatusBadRequest)
- return
- }
- if err := validateURL(r, req.URL); err != nil {
- writeJSONError(w, err.Error(), http.StatusBadRequest)
- return
- }
- ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
- defer cancel()
- // If no name is specified, an ID must be generated.
- if p == "" {
- var err error
- p, err = nextEncodedID(ctx, backend)
- if err != nil {
- writeJSONBackendError(w, err)
- return
- }
- }
- rt := internal.Route{
- URL: req.URL,
- Time: time.Now(),
- }
- if err := backend.Put(ctx, p, &rt); err != nil {
- writeJSONBackendError(w, err)
- return
- }
- writeJSONRoute(w, p, &rt, host)
- }
- func apiURLGet(backend backend.Backend, host string, w http.ResponseWriter, r *http.Request) {
- p := parseName("/api/url/", r.URL.Path)
- if p == "" {
- writeJSONError(w, "no name given", http.StatusBadRequest)
- return
- }
- ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
- defer cancel()
- rt, err := backend.Get(ctx, p)
- if errors.Is(err, internal.ErrRouteNotFound) {
- writeJSONError(w, "Not Found", http.StatusNotFound)
- return
- } else if err != nil {
- writeJSONBackendError(w, err)
- return
- }
- writeJSONRoute(w, p, rt, host)
- }
- func apiURLDelete(backend backend.Backend, w http.ResponseWriter, r *http.Request) {
- p := parseName("/api/url/", r.URL.Path)
- if p == "" {
- writeJSONError(w, "name required", http.StatusBadRequest)
- return
- }
- ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
- defer cancel()
- if err := backend.Del(ctx, p); err != nil {
- writeJSONBackendError(w, err)
- return
- }
- writeJSONOk(w)
- }
- func parseCursor(v string) ([]byte, error) {
- if v == "" {
- return nil, nil
- }
- return base64.URLEncoding.DecodeString(v)
- }
- func parseInt(v string, def int) (int, error) {
- if v == "" {
- return def, nil
- }
- i, err := strconv.ParseInt(v, 10, 64)
- if err != nil {
- return 0, err
- }
- return int(i), nil
- }
- func parseBool(v string, def bool) (bool, error) {
- if v == "" {
- return def, nil
- }
- v = strings.ToLower(v)
- if v == "true" || v == "t" || v == "1" {
- return true, nil
- }
- if v == "false" || v == "f" || v == "0" {
- return false, nil
- }
- return false, errors.New("invalid boolean value")
- }
- func apiURLsGet(backend backend.Backend, host string, w http.ResponseWriter, r *http.Request) {
- c, err := parseCursor(r.FormValue("cursor"))
- if err != nil {
- writeJSONError(w, "invalid cursor value", http.StatusBadRequest)
- return
- }
- lim, err := parseInt(r.FormValue("limit"), 100)
- if err != nil || lim <= 0 || lim > 10000 {
- writeJSONError(w, "invalid limit value", http.StatusBadRequest)
- return
- }
- ig, err := parseBool(r.FormValue("include-generated-names"), false)
- if err != nil {
- writeJSONError(w, "invalid include-generated-names value", http.StatusBadRequest)
- return
- }
- res := msgRoutes{
- Ok: true,
- }
- ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
- defer cancel()
- iter, err := backend.List(ctx, string(c))
- if err != nil {
- writeJSONBackendError(w, err)
- return
- }
- defer iter.Release()
- for iter.Next() {
- // if we should be ignoring generated links, skip over that range.
- if !ig && isGenerated(iter.Name()) {
- iter.Seek(string(postGenCursor))
- if !iter.Valid() {
- break
- }
- }
- r := routeWithName{
- Name: iter.Name(),
- Route: iter.Route(),
- }
- if host != "" {
- r.SourceHost = host
- }
- res.Routes = append(res.Routes, &r)
- if len(res.Routes) == lim {
- break
- }
- }
- if iter.Next() {
- res.Next = base64.URLEncoding.EncodeToString([]byte(iter.Name()))
- }
- if err := iter.Error(); err != nil {
- writeJSONBackendError(w, err)
- return
- }
- writeJSON(w, &res, http.StatusOK)
- }
- func apiURL(backend backend.Backend, host string, w http.ResponseWriter, r *http.Request) {
- switch r.Method {
- case "POST":
- apiURLPost(backend, host, w, r)
- case "GET":
- apiURLGet(backend, host, w, r)
- case "DELETE":
- apiURLDelete(backend, w, r)
- default:
- writeJSONError(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusOK) // fix
- }
- }
- func apiURLs(backend backend.Backend, host string, w http.ResponseWriter, r *http.Request) {
- switch r.Method {
- case "GET":
- apiURLsGet(backend, host, w, r)
- default:
- writeJSONError(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusOK) // fix
- }
- }
- // Setup ...
- func Setup(m *http.ServeMux, backend backend.Backend, host string) {
- m.HandleFunc("/api/url/", func(w http.ResponseWriter, r *http.Request) {
- apiURL(backend, host, w, r)
- })
- m.HandleFunc("/api/urls/", func(w http.ResponseWriter, r *http.Request) {
- apiURLs(backend, host, w, r)
- })
- }
|