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
+}