diff --git a/handlers.go b/handlers.go index 020db28..e084c27 100644 --- a/handlers.go +++ b/handlers.go @@ -6,12 +6,16 @@ package main import ( "crypto/rand" + "crypto/subtle" + "encoding/base64" "fmt" "io" "log" "net/http" "net/url" + "strings" "time" + "unicode" "gitea.hashru.nl/dsprenkels/rushlink/gobmarsh" "github.com/gorilla/mux" @@ -27,6 +31,7 @@ type StoredPaste struct { State PasteState Content []byte Key []byte + OwnerToken [16]byte TimeCreated time.Time } @@ -40,9 +45,37 @@ const ( StateDeleted ) -var base64Alphabet = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") +const CookieOwnerToken = "owner_token" + +// Base64 encoding and decoding +var base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" +var base64Encoder = base64.NewEncoding(base64Alphabet).WithPadding(base64.NoPadding) + +// Page contents var indexContents = MustAsset("assets/index.txt") +func (t PasteType) String() (string, error) { + switch t { + case TypePaste: + return "paste", nil + case TypeRedirect: + return "redirect", nil + default: + return "", fmt.Errorf("invalid PasteType (%v)", t) + } +} + +func (t PasteState) String() (string, error) { + switch t { + case StatePresent: + return "present", nil + case StateDeleted: + return "deleted", nil + default: + return "", fmt.Errorf("invalid PasteState (%v)", t) + } +} + func indexGetHandler(w http.ResponseWriter, r *http.Request) { _, err := w.Write(indexContents) if err != nil { @@ -53,7 +86,7 @@ func indexGetHandler(w http.ResponseWriter, r *http.Request) { func indexPostHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(50 * 1000 * 1000); err != nil { w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "Internal server error: %v", err) + fmt.Fprintf(w, "Internal server error: %v\n", err) return } @@ -62,18 +95,18 @@ func indexPostHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) var buf []byte r.Body.Read(buf) - io.WriteString(w, "empty body in POST request") + io.WriteString(w, "empty body in POST request\n") return } shorten_values, prs := r.PostForm["shorten"] if !prs { w.WriteHeader(http.StatusBadRequest) - io.WriteString(w, "no 'shorten' param supplied") + io.WriteString(w, "no 'shorten' param supplied\n") return } if len(shorten_values) != 1 { w.WriteHeader(http.StatusBadRequest) - io.WriteString(w, "only one 'shorten' param is allowed per request") + io.WriteString(w, "only one 'shorten' param is allowed per request\n") return } @@ -81,14 +114,18 @@ func indexPostHandler(w http.ResponseWriter, r *http.Request) { } func pasteGetHandler(w http.ResponseWriter, r *http.Request) { - pasteGetHandlerInner(w, r, false) + pasteGetHandlerInner(w, r, false, false) } func pasteGetHandlerNoRedirect(w http.ResponseWriter, r *http.Request) { - pasteGetHandlerInner(w, r, true) + pasteGetHandlerInner(w, r, true, false) } -func pasteGetHandlerInner(w http.ResponseWriter, r *http.Request, noRedirect bool) { +func pasteGetHandlerMeta(w http.ResponseWriter, r *http.Request) { + pasteGetHandlerInner(w, r, false, true) +} + +func pasteGetHandlerInner(w http.ResponseWriter, r *http.Request, noRedirect, showMeta bool) { vars := mux.Vars(r) key := vars["key"] var storedPaste *StoredPaste @@ -99,14 +136,45 @@ func pasteGetHandlerInner(w http.ResponseWriter, r *http.Request, noRedirect boo }); err != nil { w.WriteHeader(http.StatusInternalServerError) log.Printf("error: %v\n", err) - fmt.Fprintf(w, "internal server error: %v", err) + fmt.Fprintf(w, "internal server error: %v\n", err) return } if storedPaste == nil { w.WriteHeader(http.StatusNotFound) - fmt.Fprintf(w, "url key not found in the database") + fmt.Fprintf(w, "url key not found in the database\n") return } + + if showMeta { + typeString, err := storedPaste.Type.String() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + log.Printf("error: %v\n", err) + fmt.Fprintf(w, "internal server error: %v\n", err) + return + } + stateString, err := storedPaste.State.String() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + log.Printf("error: %v\n", err) + fmt.Fprintf(w, "internal server error: %v\n", err) + return + } + isOwner := "no" + ownerToken, ok := getOwnerTokenFromRequest(r) + if ok && subtle.ConstantTimeCompare(ownerToken[:], storedPaste.OwnerToken[:]) == 1 { + isOwner = "yes" + } + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "key: %v\n", string(storedPaste.Key)) + fmt.Fprintf(w, "type: %v\n", typeString) + fmt.Fprintf(w, "state: %v\n", stateString) + fmt.Fprintf(w, "created: %v\n", storedPaste.TimeCreated.String()) + fmt.Fprintf(w, "are you the owner: %v\n", isOwner) + return + } + switch storedPaste.State { case StatePresent: if !noRedirect { @@ -115,7 +183,7 @@ func pasteGetHandlerInner(w http.ResponseWriter, r *http.Request, noRedirect boo if err != nil { w.WriteHeader(http.StatusInternalServerError) log.Printf("error: invalid URL ('%v') in database for key '%v': %v\n", rawurl, storedPaste.Key, err) - fmt.Fprintf(w, "internal server error: invalid url in database") + fmt.Fprintf(w, "internal server error: invalid url in database\n") return } http.Redirect(w, r, urlParse.String(), http.StatusSeeOther) @@ -123,11 +191,11 @@ func pasteGetHandlerInner(w http.ResponseWriter, r *http.Request, noRedirect boo w.Write(storedPaste.Content) case StateDeleted: w.WriteHeader(http.StatusGone) - fmt.Fprintf(w, "key has been deleted") + fmt.Fprintf(w, "key has been deleted\n") default: w.WriteHeader(http.StatusInternalServerError) log.Printf("error: invalid storedPaste.State (%v) for key '%v'\n", storedPaste.State, storedPaste.Key) - fmt.Fprintf(w, "internal server error: invalid storedPaste.State (%v)", storedPaste.State) + fmt.Fprintf(w, "internal server error: invalid storedPaste.State (%v\n)", storedPaste.State) } } @@ -136,34 +204,52 @@ func shortenPostHandler(w http.ResponseWriter, r *http.Request) { userURL, err := url.ParseRequestURI(rawurl) if err != nil { w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, "invalid url (%v): %v", err, rawurl) + fmt.Fprintf(w, "invalid url (%v): %v\n", err, rawurl) return } if userURL.Scheme == "" { w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, "invalid url (unspecified scheme)", rawurl) + fmt.Fprintf(w, "invalid url (unspecified scheme)\n", rawurl) return } if userURL.Host == "" { w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, "invalid url (unspecified host)", rawurl) + fmt.Fprintf(w, "invalid url (unspecified host)\n", rawurl) return } var storedPaste *StoredPaste if err := db.Update(func(tx *bolt.Tx) error { - u, err := shortenURL(tx, userURL) - storedPaste = u + ownerKey, ok := getOwnerTokenFromRequest(r) + if ok == false { + // Owner key not supplied or invalid, generate a new one + ownerKey, err = generateOwnerToken() + if err != nil { + return errors.Wrap(err, "generating OwnerToken") + } + } + + sp, err := shortenURL(tx, userURL, ownerKey) + storedPaste = sp return err }); err != nil { w.WriteHeader(http.StatusInternalServerError) log.Printf("error: %v\n", err) - fmt.Fprintf(w, "internal server error: %v", err) + fmt.Fprintf(w, "internal server error: %v\n", err) return } + saveURL, err := r.URL.Parse(string(storedPaste.Key)) + if err != nil { + log.Printf("error: %v\n", errors.Wrap(err, "parsing url")) + } + var base64OwnerToken = make([]byte, 24) + base64Encoder.Encode(base64OwnerToken, storedPaste.OwnerToken[:]) + w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, "URL saved at /%v", string(storedPaste.Key)) + fmt.Fprintf(w, "URL saved at %v\n", saveURL) + isNotPrint := func(r rune) bool { return !unicode.IsPrint(r) } + fmt.Fprintf(w, "Owner key is %s\n", strings.TrimRightFunc(string(base64OwnerToken), isNotPrint)) } // Retrieve a URL from the database @@ -184,7 +270,7 @@ func getURL(tx *bolt.Tx, key []byte) (*StoredPaste, error) { // 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) (*StoredPaste, error) { +func shortenURL(tx *bolt.Tx, userURL *url.URL, ownerKey [16]byte) (*StoredPaste, error) { shortenBucket := tx.Bucket([]byte(BUCKET_PASTES)) if shortenBucket == nil { return nil, fmt.Errorf("bucket %v does not exist", BUCKET_PASTES) @@ -213,6 +299,7 @@ func shortenURL(tx *bolt.Tx, userURL *url.URL) (*StoredPaste, error) { State: StatePresent, Content: []byte(userURL.String()), Key: urlKey, + OwnerToken: ownerKey, TimeCreated: time.Now().UTC(), } storedBytes, err := gobmarsh.Marshal(storedPaste) @@ -259,3 +346,27 @@ func generateURLKey(epoch int) ([]byte, error) { } return urlKey, nil } + +func generateOwnerToken() ([16]byte, error) { + var ownerKey [16]byte + _, err := rand.Read(ownerKey[:]) + if err != nil { + return ownerKey, err + } + return ownerKey, nil +} + +func getOwnerTokenFromRequest(r *http.Request) ([16]byte, bool) { + var ownerKey [16]byte + ownerKeyCookie, err := r.Cookie(CookieOwnerToken) + if err != nil && err != http.ErrNoCookie { + return ownerKey, false + } + if ownerKeyCookie != nil { + n, err := base64Encoder.Strict().Decode(ownerKey[:], []byte(ownerKeyCookie.Value)) + if err == nil || n == 16 { + return ownerKey, true + } + } + return ownerKey, false +} diff --git a/rushlink.go b/rushlink.go index ea444ba..70c95af 100644 --- a/rushlink.go +++ b/rushlink.go @@ -48,6 +48,7 @@ func main() { router.HandleFunc("/", indexPostHandler).Methods("POST") router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}", pasteGetHandler).Methods("GET") router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}/nr", pasteGetHandlerNoRedirect).Methods("GET") + router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}/meta", pasteGetHandlerMeta).Methods("GET") // Start the server srv := &http.Server{