254 lines
5.9 KiB
Go
254 lines
5.9 KiB
Go
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 {
|
|
panic(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 {
|
|
panic(errors.Wrapf(err, "invalid URL ('%v') in database for key '%v'", rawurl, p.Key))
|
|
}
|
|
http.Redirect(w, r, urlParse.String(), http.StatusSeeOther)
|
|
}
|
|
w.Write(p.Content)
|
|
case stateDeleted:
|
|
renderError(w, r, http.StatusGone, "paste has been deleted")
|
|
default:
|
|
panic(errors.Errorf("invalid paste.State (%v) for key '%v'", p.State, p.Key))
|
|
}
|
|
}
|
|
|
|
func newPasteHandler(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseMultipartForm(50 * 1000 * 1000); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// 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 {
|
|
panic(err)
|
|
}
|
|
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")
|
|
}
|