rushlink/handlers/paste.go
2019-09-22 14:03:54 +02:00

160 lines
3.4 KiB
Go

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
}