diff --git a/handlers.go b/handlers.go index e675ece..fce2788 100644 --- a/handlers.go +++ b/handlers.go @@ -98,7 +98,7 @@ func (rl *rushlink) viewPasteHandlerFlags(w http.ResponseWriter, r *http.Request key := vars["key"] var p *db.Paste var fu *db.FileUpload - if err := rl.db.Bolt.View(func(tx *bolt.Tx) error { + err := rl.db.Bolt.View(func(tx *bolt.Tx) error { var err error p, err = db.GetPaste(tx, key) if err != nil { @@ -113,12 +113,13 @@ func (rl *rushlink) viewPasteHandlerFlags(w http.ResponseWriter, r *http.Request } } return nil - }); err != nil { - panic(err) - } - - if p == nil { - rl.renderError(w, r, http.StatusNotFound, "url key not found in the database") + }) + if err != nil { + status := db.ErrHTTPStatusCode(err) + if status == http.StatusInternalServerError { + panic(err) + } + rl.renderError(w, r, status, err.Error()) return } diff --git a/internal/db/paste.go b/internal/db/paste.go index 4fbad07..8765320 100644 --- a/internal/db/paste.go +++ b/internal/db/paste.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "encoding/base64" "encoding/hex" + "net/http" "net/url" "strings" "time" @@ -53,6 +54,35 @@ const ( PasteStateDeleted ) +// minKeyLen specifies the mimimum length of a paste key. +const minKeyLen = 4 + +var ( + // ErrKeyInvalidChar occurs when a key contains an invalid character. + ErrKeyInvalidChar = errors.New("invalid character in key") + // ErrKeyInvalidLength occurs when a key embeds a length that is incorrect. + ErrKeyInvalidLength = errors.New("key length encoding is incorrect") + // ErrPasteDoesNotExist occurs when a key does not exist in the database. + ErrPasteDoesNotExist = errors.New("url key not found in the database") +) + +// ErrHTTPStatusCode returns the HTTP status code that should correspond to +// the provided error. +// server error, or false if it is not. +func ErrHTTPStatusCode(err error) int { + switch err { + case nil: + return 0 + case ErrKeyInvalidChar: + return http.StatusNotFound + case ErrKeyInvalidLength: + return http.StatusNotFound + case ErrPasteDoesNotExist: + return http.StatusNotFound + } + return http.StatusInternalServerError +} + // Base64 encoding and decoding var base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" var base64Encoder = base64.RawURLEncoding.WithPadding(base64.NoPadding) @@ -87,13 +117,45 @@ func (t PasteState) String() string { // GetPaste retrieves a paste from the database. func GetPaste(tx *bolt.Tx, key string) (*Paste, error) { + if err := ValidatePasteKey(key); err != nil { + return nil, err + } + return GetPasteNoValidate(tx, key) +} + +// ValidatePasteKey validates the format of the key that has +func ValidatePasteKey(key string) error { + internalLen := minKeyLen + countingOnes := true + for _, ch := range key { + limb := strings.IndexRune(base64Alphabet, ch) + if limb == -1 { + return ErrKeyInvalidChar + } + for i := 5; i >= 0 && countingOnes; i-- { + if (limb>>uint(i))&0x1 == 0 { + countingOnes = false + break + } + internalLen++ + } + } + if internalLen != len(key) { + return ErrKeyInvalidLength + } + return nil +} + +// GetPasteNoValidate retrieves a paste from the database without validating +// the key format first. +func GetPasteNoValidate(tx *bolt.Tx, key string) (*Paste, error) { pastesBucket := tx.Bucket([]byte(BucketPastes)) if pastesBucket == nil { return nil, errors.Errorf("bucket %v does not exist", BucketPastes) } storedBytes := pastesBucket.Get([]byte(key)) if storedBytes == nil { - return nil, nil + return nil, ErrPasteDoesNotExist } return decodePaste(storedBytes) } @@ -214,7 +276,6 @@ func GeneratePasteKey(tx *bolt.Tx, minimumEntropy int) (string, error) { // is used to ensure that a new key has at least some amount of guessing // entropy. func generatePasteKeyInner(epoch, entropy int) (string, error) { - minKeyLen := 4 entropyEpoch := entropy entropyEpoch -= minKeyLen * 6 // First 4 characters provide 24 bits. entropyEpoch++ // One bit less because of '0' bit.