rushlink/paste.go
2019-11-22 18:41:54 +01:00

222 lines
4.8 KiB
Go

package rushlink
import (
"crypto/rand"
"encoding/hex"
"net/url"
"strings"
"time"
"github.com/google/uuid"
"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 (
pasteTypeUndef pasteType = 0
pasteTypePaste = 1
pasteTypeRedirect = 2
pasteTypeFileUpload = 3
)
const (
pasteStateUndef pasteState = 0
pasteStatePresent = 1
pasteStateDeleted = 2
)
func (t pasteType) String() string {
switch t {
case pasteTypeUndef:
return "unknown"
case pasteTypePaste:
return "paste"
case pasteTypeRedirect:
return "redirect"
case pasteTypeFileUpload:
return "file"
default:
return "invalid"
}
}
func (t pasteState) String() string {
switch t {
case pasteStateUndef:
return "unknown"
case pasteStatePresent:
return "present"
case pasteStateDeleted:
return "deleted"
default:
return "invalid"
}
}
// Retrieve a paste from the database
func getPaste(tx *bolt.Tx, key string) (*paste, error) {
pastesBucket := tx.Bucket([]byte(BUCKET_PASTES))
if pastesBucket == nil {
return nil, errors.Errorf("bucket %v does not exist", BUCKET_PASTES)
}
storedBytes := pastesBucket.Get([]byte(key))
if storedBytes == nil {
return nil, nil
}
p := &paste{}
err := Unmarshal(storedBytes, p)
return p, err
}
func (p *paste) save(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BUCKET_PASTES))
if bucket == nil {
return errors.Errorf("bucket %v does not exist", BUCKET_PASTES)
}
buf, err := 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 {
// Remove the (maybe) attached file
if p.Type == pasteTypeFileUpload {
fuID, err := uuid.FromBytes(p.Content)
if err != nil {
return errors.Wrap(err, "failed to parse uuid")
}
fu, err := getFileUpload(tx, fuID)
if err != nil {
return errors.Wrap(err, "failed to find file in database")
}
if err := fu.delete(tx); err != nil {
return errors.Wrap(err, "failed to remove file")
}
}
// Replace the old paste with a new empty paste
p.Type = pasteTypeUndef
p.State = pasteStateDeleted
p.Content = []byte{}
if err := p.save(tx); err != nil {
return errors.Wrap(err, "failed to delete paste in database")
}
return nil
}
// Get the URL from this paste.
//
// This function assumes that the paste is valid. If the paste struct is
// corrupted in some way, this function will panic.
func (p *paste) redirectURL() *url.URL {
if p.Type != pasteTypeRedirect {
panic("expected p.Type to be pasteTypeRedirect")
}
rawurl := string(p.Content)
urlParse, err := url.Parse(rawurl)
if err != nil {
panic(errors.Wrapf(err, "invalid URL ('%v') in database for key '%v'", rawurl, p.Key))
}
return urlParse
}
// 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(BUCKET_PASTES))
if pastesBucket == nil {
return "", errors.Errorf("bucket %v does not exist", 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
}