From 0ecdaafe2fb9d495485c1e74c002cd844c79557e Mon Sep 17 00:00:00 2001 From: Daan Sprenkels Date: Sun, 22 Sep 2019 14:03:27 +0200 Subject: [PATCH] refactor: Put paste model in separate file --- assets/templates/html/index.html.tmpl | 3 + assets/templates/txt/index.txt.tmpl | 3 + handlers/handlers.go | 188 ++++---------------------- handlers/paste.go | 159 ++++++++++++++++++++++ 4 files changed, 190 insertions(+), 163 deletions(-) create mode 100644 handlers/paste.go diff --git a/assets/templates/html/index.html.tmpl b/assets/templates/html/index.html.tmpl index eef52ba..b12774b 100644 --- a/assets/templates/html/index.html.tmpl +++ b/assets/templates/html/index.html.tmpl @@ -8,6 +8,9 @@ the command line. ## USAGE + # Upload a file + curl -F'file=@yourfile.png' {{Request.Host}} + # Shorten a URL curl -F'shorten=http://example.com/some/long/url' {{Request.Host}} diff --git a/assets/templates/txt/index.txt.tmpl b/assets/templates/txt/index.txt.tmpl index f589108..7023676 100644 --- a/assets/templates/txt/index.txt.tmpl +++ b/assets/templates/txt/index.txt.tmpl @@ -6,6 +6,9 @@ the command line. ## USAGE + # Upload a file + curl -F'file=@yourfile.png' {{Request.Host}} + # Shorten a URL curl -F'shorten=http://example.com/some/long/url' {{Request.Host}} diff --git a/handlers/handlers.go b/handlers/handlers.go index 491cfc7..8d982a3 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -1,15 +1,12 @@ package handlers import ( - "crypto/rand" "crypto/subtle" "encoding/base64" - "encoding/hex" "fmt" "log" "net/http" "net/url" - "strings" "time" "github.com/gorilla/mux" @@ -17,31 +14,6 @@ import ( bolt "go.etcd.io/bbolt" "gitea.hashru.nl/dsprenkels/rushlink/db" - "gitea.hashru.nl/dsprenkels/rushlink/gobmarsh" -) - -type pasteType int -type pasteState int - -type storedPaste struct { - Type pasteType - State pasteState - Content []byte - Key string - DeleteToken string - TimeCreated time.Time -} - -const ( - typeUndef pasteType = 0 - typePaste = 1 - typeRedirect = 2 -) - -const ( - stateUndef pasteState = 0 - statePresent = 1 - stateDeleted = 2 ) type viewPaste uint @@ -106,17 +78,17 @@ func viewPasteHandlerMeta(w http.ResponseWriter, r *http.Request) { func viewPasteHandlerInner(w http.ResponseWriter, r *http.Request, flags viewPaste) { vars := mux.Vars(r) key := vars["key"] - var storedPaste *storedPaste + var p *paste if err := db.DB.View(func(tx *bolt.Tx) error { var err error - storedPaste, err = getURL(tx, key) + p, err = getPaste(tx, key) return err }); err != nil { log.Printf("error: %v\n", err) renderInternalServerError(w, r, err) return } - if storedPaste == nil { + if p == nil { renderError(w, r, http.StatusNotFound, "url key not found in the database") return } @@ -130,7 +102,7 @@ func viewPasteHandlerInner(w http.ResponseWriter, r *http.Request, flags viewPas if deleteToken == "" { canDelete.String = "undefined" } else { - if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(storedPaste.DeleteToken)) == 1 { + if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(p.DeleteToken)) == 1 { canDelete.Bool = true canDelete.String = "correct" } else { @@ -139,31 +111,31 @@ func viewPasteHandlerInner(w http.ResponseWriter, r *http.Request, flags viewPas } data := map[string]interface{}{ - "Paste": storedPaste, + "Paste": p, "CanDelete": canDelete, } render(w, r, "pasteMeta", data) return } - switch storedPaste.State { + switch p.State { case statePresent: if flags&viewNoRedirect == 0 { - rawurl := string(storedPaste.Content) + 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, storedPaste.Key, err) + 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(storedPaste.Content) + w.Write(p.Content) case stateDeleted: renderError(w, r, http.StatusGone, "paste has been deleted") default: - log.Printf("error: invalid storedPaste.State (%v) for key '%v'\n", storedPaste.State, storedPaste.Key) - msg := fmt.Sprintf("internal server error: invalid storedPaste.State (%v\n)", storedPaste.State) + 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) } } @@ -210,18 +182,18 @@ func newRedirectPasteHandler(w http.ResponseWriter, r *http.Request) { return } - var storedPaste *storedPaste + var paste *paste if err := db.DB.Update(func(tx *bolt.Tx) error { // Generate a new delete token for this paste var err error - storedPaste, err = shortenURL(tx, userURL) + 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": storedPaste} + data := map[string]interface{}{"Paste": paste} render(w, r, "newRedirectPasteSuccess", data) } @@ -238,18 +210,13 @@ func deletePasteHandler(w http.ResponseWriter, r *http.Request) { var errorCode int if err := db.DB.Update(func(tx *bolt.Tx) error { - paste, err := getURL(tx, key) + p, err := getPaste(tx, key) if err != nil { errorCode = http.StatusNotFound return err } - if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(paste.DeleteToken)) == 1 { - // Replace the old paste with a new empty paste - return savePaste(tx, key, storedPaste{ - Key: paste.Key, - State: stateDeleted, - DeleteToken: paste.DeleteToken, - }) + if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(p.DeleteToken)) == 1 { + p.delete(tx) } errorCode = http.StatusForbidden return errors.New("invalid delete token") @@ -260,58 +227,13 @@ func deletePasteHandler(w http.ResponseWriter, r *http.Request) { } } -// Retrieve a URL from the database -func getURL(tx *bolt.Tx, key string) (*storedPaste, error) { - pastesBucket := tx.Bucket([]byte(db.BUCKET_PASTES)) - if pastesBucket == nil { - return nil, errors.Errorf("bucket %v does not exist", db.BUCKET_PASTES) - } - storedBytes := pastesBucket.Get([]byte(key)) - if storedBytes == nil { - return nil, nil - } - storedPaste := &storedPaste{} - err := gobmarsh.Unmarshal(storedBytes, storedPaste) - return storedPaste, err -} - // 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) { - pastesBucket := tx.Bucket([]byte(db.BUCKET_PASTES)) - if pastesBucket == nil { - return nil, errors.Errorf("bucket %v does not exist", db.BUCKET_PASTES) - } - - // Generate a key until it is not in the database, this occurs in O(log N), - // where N is the amount of keys stored in the url-shorten database. - epoch := 0 - var urlKey string - for { - var err error - urlKey, err = generateURLKey(epoch) - if err != nil { - return nil, errors.Wrap(err, "url-key generation failed") - } - - found := pastesBucket.Get([]byte(urlKey)) - if found == nil { - break - } - - isReserved := false - for _, reservedKey := range ReservedPasteKeys { - if strings.HasPrefix(urlKey, reservedKey) { - isReserved = true - break - } - } - if !isReserved { - break - } - - epoch++ +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 @@ -321,78 +243,18 @@ func shortenURL(tx *bolt.Tx, userURL *url.URL) (*storedPaste, error) { } // Store the new key - storedPaste := storedPaste{ + p := paste{ Type: typeRedirect, State: statePresent, Content: []byte(userURL.String()), - Key: urlKey, + Key: pasteKey, DeleteToken: deleteToken, TimeCreated: time.Now().UTC(), } - if err := savePaste(tx, urlKey, storedPaste); err != nil { + if err := p.save(tx); err != nil { return nil, err } - return &storedPaste, nil -} - -func savePaste(tx *bolt.Tx, key string, paste storedPaste) error { - bucket := tx.Bucket([]byte(db.BUCKET_PASTES)) - if bucket == nil { - return errors.Errorf("bucket %v does not exist", db.BUCKET_PASTES) - } - - buf, err := gobmarsh.Marshal(paste) - if err != nil { - return errors.Wrap(err, "encoding for database failed") - } - if err := bucket.Put([]byte(key), buf); err != nil { - return errors.Wrap(err, "database transaction failed") - } - return nil -} - -func generateURLKey(epoch int) (string, error) { - urlKey := make([]byte, 4+epoch) - _, err := rand.Read(urlKey) - if err != nil { - return "", err - } - // Put all the values in the range 0..64 for easier base64-encoding - for i := 0; i < len(urlKey); i++ { - urlKey[i] &= 0x3F - } - // Implement truncate-resistance by forcing the prefix to - // 0b111110xxxxxxxxxx - // ^----- {epoch} ones followed by a single 0 - // - // Example when epoch is 1: prefix is 0b10. - i := 0 - for i < epoch { - // Set this bit to 1 - limb := i / 6 - bit := i % 6 - urlKey[limb] |= 1 << uint(5-bit) - i++ - } - // Finally set the next bit to 0 - limb := i / 6 - bit := i % 6 - urlKey[limb] &= ^(1 << uint(5-bit)) - - // Convert this ID to a canonical base64 notation - for i := range urlKey { - urlKey[i] = base64Alphabet[urlKey[i]] - } - return string(urlKey), nil -} - -func generateDeleteToken() (string, error) { - var deleteToken [16]byte - _, err := rand.Read(deleteToken[:]) - if err != nil { - return "", err - } - return hex.EncodeToString(deleteToken[:]), nil + return &p, nil } func getDeleteTokenFromRequest(r *http.Request) string { diff --git a/handlers/paste.go b/handlers/paste.go new file mode 100644 index 0000000..e54ba59 --- /dev/null +++ b/handlers/paste.go @@ -0,0 +1,159 @@ +package handlers + +import ( + "crypto/rand" + "encoding/hex" + "strings" + "time" + + "gitea.hashru.nl/dsprenkels/rushlink/db" + "gitea.hashru.nl/dsprenkels/rushlink/gobmarsh" + "github.com/pkg/errors" + bolt "go.etcd.io/bbolt" +) + +type pasteType int +type pasteState int + +type paste struct { + Type pasteType + State pasteState + Content []byte + Key string + DeleteToken string + TimeCreated time.Time +} + +const ( + typeUndef pasteType = 0 + typePaste = 1 + typeRedirect = 2 +) + +const ( + stateUndef pasteState = 0 + statePresent = 1 + stateDeleted = 2 +) + +// Retrieve a paste from the database +func getPaste(tx *bolt.Tx, key string) (*paste, error) { + pastesBucket := tx.Bucket([]byte(db.BUCKET_PASTES)) + if pastesBucket == nil { + return nil, errors.Errorf("bucket %v does not exist", db.BUCKET_PASTES) + } + storedBytes := pastesBucket.Get([]byte(key)) + if storedBytes == nil { + return nil, nil + } + p := &paste{} + err := gobmarsh.Unmarshal(storedBytes, p) + return p, err +} + +func (p *paste) save(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(db.BUCKET_PASTES)) + if bucket == nil { + return errors.Errorf("bucket %v does not exist", db.BUCKET_PASTES) + } + + buf, err := gobmarsh.Marshal(p) + if err != nil { + return errors.Wrap(err, "encoding for database failed") + } + if err := bucket.Put([]byte(p.Key), buf); err != nil { + return errors.Wrap(err, "database transaction failed") + } + return nil +} + +func (p paste) delete(tx *bolt.Tx) error { + // Replace the old paste with a new empty paste + return (&paste{ + Key: p.Key, + State: stateDeleted, + DeleteToken: p.DeleteToken, + }).save(tx) +} + +// Generate a key until it is not in the database, this occurs in O(log N), +// where N is the amount of keys stored in the url-shorten database. +func generatePasteKey(tx *bolt.Tx) (string, error) { + pastesBucket := tx.Bucket([]byte(db.BUCKET_PASTES)) + if pastesBucket == nil { + return "", errors.Errorf("bucket %v does not exist", db.BUCKET_PASTES) + } + + epoch := 0 + var key string + for { + var err error + key, err = generatePasteKeyInner(epoch) + if err != nil { + return "", errors.Wrap(err, "url-key generation failed") + } + + found := pastesBucket.Get([]byte(key)) + if found == nil { + break + } + + isReserved := false + for _, reservedKey := range ReservedPasteKeys { + if strings.HasPrefix(key, reservedKey) { + isReserved = true + break + } + } + if !isReserved { + break + } + + epoch++ + } + return key, nil +} + +func generatePasteKeyInner(epoch int) (string, error) { + urlKey := make([]byte, 4+epoch) + _, err := rand.Read(urlKey) + if err != nil { + return "", err + } + // Put all the values in the range 0..64 for easier base64-encoding + for i := 0; i < len(urlKey); i++ { + urlKey[i] &= 0x3F + } + // Implement truncate-resistance by forcing the prefix to + // 0b111110xxxxxxxxxx + // ^----- {epoch} ones followed by a single 0 + // + // Example when epoch is 1: prefix is 0b10. + i := 0 + for i < epoch { + // Set this bit to 1 + limb := i / 6 + bit := i % 6 + urlKey[limb] |= 1 << uint(5-bit) + i++ + } + // Finally set the next bit to 0 + limb := i / 6 + bit := i % 6 + urlKey[limb] &= ^(1 << uint(5-bit)) + + // Convert this ID to a canonical base64 notation + for i := range urlKey { + urlKey[i] = base64Alphabet[urlKey[i]] + } + return string(urlKey), nil +} + +func generateDeleteToken() (string, error) { + var deleteToken [16]byte + _, err := rand.Read(deleteToken[:]) + if err != nil { + return "", err + } + return hex.EncodeToString(deleteToken[:]), nil +}