package handlers import ( "crypto/subtle" "encoding/base64" "fmt" "log" "net/http" "net/url" "time" "github.com/gorilla/mux" "github.com/pkg/errors" bolt "go.etcd.io/bbolt" "gitea.hashru.nl/dsprenkels/rushlink/db" ) type viewPaste uint const ( _ viewPaste = 1 << iota viewNoRedirect viewShowMeta ) const CookieDeleteToken = "owner_token" // These keys are designated reserved, and will not be randomly chosen var ReservedPasteKeys = []string{"xd42", "example"} // Base64 encoding and decoding var base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" var base64Encoder = base64.RawURLEncoding.WithPadding(base64.NoPadding) func (t pasteType) String() string { switch t { case typeUndef: return "unknown" case typePaste: return "paste" case typeRedirect: return "redirect" default: return "invalid" } } func (t pasteState) String() string { switch t { case stateUndef: return "unknown" case statePresent: return "present" case stateDeleted: return "deleted" default: return "invalid" } } func indexGetHandler(w http.ResponseWriter, r *http.Request) { render(w, r, "index", map[string]interface{}{}) } func viewPasteHandler(w http.ResponseWriter, r *http.Request) { viewPasteHandlerInner(w, r, 0) } func viewPasteHandlerNoRedirect(w http.ResponseWriter, r *http.Request) { viewPasteHandlerInner(w, r, viewNoRedirect) } func viewPasteHandlerMeta(w http.ResponseWriter, r *http.Request) { viewPasteHandlerInner(w, r, viewShowMeta) } func viewPasteHandlerInner(w http.ResponseWriter, r *http.Request, flags viewPaste) { vars := mux.Vars(r) key := vars["key"] var p *paste if err := db.DB.View(func(tx *bolt.Tx) error { var err error p, err = getPaste(tx, key) return err }); err != nil { log.Printf("error: %v\n", err) renderInternalServerError(w, r, err) return } if p == nil { renderError(w, r, http.StatusNotFound, "url key not found in the database") return } if flags&viewShowMeta != 0 { canDelete := struct { Bool bool String string }{Bool: false} deleteToken := getDeleteTokenFromRequest(r) if deleteToken == "" { canDelete.String = "undefined" } else { if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(p.DeleteToken)) == 1 { canDelete.Bool = true canDelete.String = "correct" } else { canDelete.String = "invalid" } } data := map[string]interface{}{ "Paste": p, "CanDelete": canDelete, } render(w, r, "pasteMeta", data) return } switch p.State { case statePresent: if flags&viewNoRedirect == 0 { rawurl := string(p.Content) urlParse, err := url.Parse(rawurl) if err != nil { log.Printf("error: invalid URL ('%v') in database for key '%v': %v\n", rawurl, p.Key, err) renderInternalServerError(w, r, "invalid url in database") return } http.Redirect(w, r, urlParse.String(), http.StatusSeeOther) } w.Write(p.Content) case stateDeleted: renderError(w, r, http.StatusGone, "paste has been deleted") default: log.Printf("error: invalid paste.State (%v) for key '%v'\n", p.State, p.Key) msg := fmt.Sprintf("internal server error: invalid paste.State (%v\n)", p.State) renderInternalServerError(w, r, msg) } } func newPasteHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(50 * 1000 * 1000); err != nil { log.Printf("error: %v\n", err) renderInternalServerError(w, r, err) return } // Determine what kind of post this is, currently only `shorten=...` if len(r.PostForm) == 0 { renderError(w, r, http.StatusBadRequest, "empty body in POST request\n") return } shorten_values, prs := r.PostForm["shorten"] if !prs { renderError(w, r, http.StatusBadRequest, "no 'shorten' param given\n") return } if len(shorten_values) != 1 { renderError(w, r, http.StatusBadRequest, "only one 'shorten' param is allowed per request\n") return } newRedirectPasteHandler(w, r) } func newRedirectPasteHandler(w http.ResponseWriter, r *http.Request) { rawurl := r.PostForm.Get("shorten") userURL, err := url.ParseRequestURI(rawurl) if err != nil { msg := fmt.Sprintf("invalid url (%v): %v", err, rawurl) renderError(w, r, http.StatusBadRequest, msg) return } if userURL.Scheme == "" { renderError(w, r, http.StatusBadRequest, "invalid url (unspecified scheme)") return } if userURL.Host == "" { renderError(w, r, http.StatusBadRequest, "invalid url (unspecified host)") return } var paste *paste if err := db.DB.Update(func(tx *bolt.Tx) error { // Generate a new delete token for this paste var err error paste, err = shortenURL(tx, userURL) return err }); err != nil { log.Printf("error: %v\n", err) renderInternalServerError(w, r, err) return } data := map[string]interface{}{"Paste": paste} render(w, r, "newRedirectPasteSuccess", data) } // Delete a URL from the database func deletePasteHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) key := vars["key"] deleteToken := getDeleteTokenFromRequest(r) if deleteToken == "" { renderError(w, r, http.StatusBadRequest, "no delete token provided") return } var errorCode int if err := db.DB.Update(func(tx *bolt.Tx) error { p, err := getPaste(tx, key) if err != nil { errorCode = http.StatusNotFound return err } if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(p.DeleteToken)) == 1 { p.delete(tx) } errorCode = http.StatusForbidden return errors.New("invalid delete token") }); err != nil { log.Printf("error: %v\n", err) renderError(w, r, errorCode, fmt.Sprintf("error: %v", err)) return } } // Add a new URL to the database // // Returns the new ID if the url was successfully shortened func shortenURL(tx *bolt.Tx, userURL *url.URL) (*paste, error) { pasteKey, err := generatePasteKey(tx) if err != nil { return nil, errors.Wrap(err, "generating paste key") } // Also generate a deleteToken deleteToken, err := generateDeleteToken() if err != nil { return nil, errors.Wrap(err, "generating delete token") } // Store the new key p := paste{ Type: typeRedirect, State: statePresent, Content: []byte(userURL.String()), Key: pasteKey, DeleteToken: deleteToken, TimeCreated: time.Now().UTC(), } if err := p.save(tx); err != nil { return nil, err } return &p, nil } func getDeleteTokenFromRequest(r *http.Request) string { return r.URL.Query().Get("deleteToken") }