You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
395 lines
10 KiB
395 lines
10 KiB
package main
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"math"
|
|
"math/rand"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"github.com/alecthomas/chroma/lexers"
|
|
"github.com/bytedance/sonic"
|
|
"github.com/go-enry/go-enry/v2"
|
|
"github.com/gofiber/fiber/v2"
|
|
recoverFiber "github.com/gofiber/fiber/v2/middleware/recover"
|
|
"github.com/jenggo/fiberlog"
|
|
"github.com/philippgille/gokv/badgerdb"
|
|
"github.com/philippgille/gokv/encoding"
|
|
"github.com/rs/zerolog"
|
|
"github.com/mattn/go-isatty"
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/xlab/closer"
|
|
"golang.org/x/crypto/chacha20poly1305"
|
|
)
|
|
|
|
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
|
|
type Config struct {
|
|
AdminKey string `toml:"admin-key"` // Bypasses size limit, intended for admins (?admin=[...])
|
|
MasterKey string `toml:"master-key"` // Use this to make your instance private (?master=[...])
|
|
MaxDocumentSize int `toml:"max-document-size"` // Maximum document size for regular users
|
|
}
|
|
|
|
type DatabaseDocument struct {
|
|
ID string
|
|
Body []byte
|
|
Nonce []byte
|
|
Name string
|
|
Lang string
|
|
DeletionToken string
|
|
BurnAfterRead bool
|
|
Encrypted bool
|
|
}
|
|
|
|
type Document struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Lang string `json:"lang"`
|
|
Body string `json:"body,omitempty"`
|
|
DeletionToken string `json:"deletionToken,omitempty"`
|
|
Encrypted bool `json:"encrypted"`
|
|
BurnAfterRead bool `json:"burnAfterRead"`
|
|
}
|
|
|
|
// func panicIfNotNil(err error) {
|
|
// if err != nil {
|
|
// log.Panic().Err(err).Msg("")
|
|
// }
|
|
// }
|
|
|
|
// func generateKey(length int) ([]byte, error) {
|
|
// key := make([]byte, length)
|
|
// _, err := cryptoRand.Read(key)
|
|
// if err != nil {
|
|
// return nil, err
|
|
// }
|
|
// return key, nil
|
|
// }
|
|
|
|
func generateUnsafeKey(length int) []byte {
|
|
key := make([]byte, length)
|
|
rand.Read(key)
|
|
return key
|
|
}
|
|
|
|
func randomString(length int, alphabet string) string {
|
|
str := make([]byte, 0, length)
|
|
for i := 0; i < length; i++ {
|
|
str = append(str, alphabet[int(math.Floor(rand.Float64()*float64(len(alphabet))))])
|
|
}
|
|
return string(str)
|
|
}
|
|
|
|
func padBytes(length int, orig []byte) []byte {
|
|
buffer := make([]byte, length)
|
|
copy(buffer, orig)
|
|
return buffer
|
|
}
|
|
|
|
func or[T any](values ...T) T {
|
|
for _, value := range values {
|
|
if any(value) != nil && any(value) != "" && any(value) != false && any(value) != 0 {
|
|
return value
|
|
}
|
|
}
|
|
return values[len(values)-1]
|
|
}
|
|
|
|
func main() {
|
|
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
|
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
|
}
|
|
var MAX_DOCUMENT_SIZE = 1024 * 1024 * 1024
|
|
var ADMIN_KEY = ""
|
|
var MASTER_KEY = ""
|
|
|
|
var conf Config
|
|
_, err := toml.DecodeFile("ctrl-v.toml", &conf)
|
|
if err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
log.Panic().Err(err).Msg("Failed to read config.")
|
|
} else {
|
|
log.Info().Msg("No configuration file found. Using default settings.")
|
|
}
|
|
}
|
|
MASTER_KEY = conf.MasterKey
|
|
ADMIN_KEY = conf.AdminKey
|
|
MAX_DOCUMENT_SIZE = conf.MaxDocumentSize
|
|
|
|
app := fiber.New(fiber.Config{
|
|
JSONEncoder: sonic.Marshal,
|
|
JSONDecoder: sonic.Unmarshal,
|
|
})
|
|
app.Use(recoverFiber.New())
|
|
app.Use(fiberlog.New(fiberlog.Config{
|
|
Logger: &log.Logger,
|
|
// Next: func(ctx *fiber.Ctx) bool {
|
|
// // return strings.HasPrefix(ctx.Path(), "/ctrl-c/")
|
|
// return false // ! DEV ONLY!!!
|
|
// },
|
|
}))
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Set("X-DNS-Prefetch-Control", "off") // Just in case...
|
|
if c.Query("master") != MASTER_KEY && MASTER_KEY != "" {
|
|
return c.Status(403).JSON(map[string]string{"error": "Master key doesn't match."})
|
|
}
|
|
return c.Next()
|
|
})
|
|
opts := badgerdb.DefaultOptions
|
|
opts.Dir = "database"
|
|
opts.Codec = encoding.Gob
|
|
store, err := badgerdb.NewStore(opts)
|
|
if err != nil {
|
|
log.Panic().Err(err)
|
|
}
|
|
closer.Bind(func() {
|
|
log.Info().Msg("Stopping Ctrl-V, please wait...")
|
|
err := store.Close()
|
|
if err != nil {
|
|
log.Err(err).Msg("Failed to close database!")
|
|
}
|
|
err = app.Shutdown()
|
|
if err != nil {
|
|
log.Err(err).Msg("Failed to shutdown Fiber!")
|
|
}
|
|
})
|
|
|
|
app.Post("/ctrl-v", func(c *fiber.Ctx) error {
|
|
name := c.Query("name")
|
|
key := []byte(c.Query("key"))
|
|
id := randomString(7, ID_ALPHABET)
|
|
deletionToken := randomString(15, ID_ALPHABET)
|
|
encrypted := false
|
|
nonce := []byte{}
|
|
if len(key) > 32 {
|
|
return c.Status(400).JSON(map[string]string{
|
|
"error": "Key must not exceed 32 bytes (256 bits)",
|
|
})
|
|
}
|
|
body := c.Body()
|
|
if len(body) > MAX_DOCUMENT_SIZE && MAX_DOCUMENT_SIZE > 0 && c.Query("admin") != ADMIN_KEY {
|
|
return c.Status(413).JSON(map[string]string{
|
|
"error": fmt.Sprintf("Document size must not exceed %d bytes", MAX_DOCUMENT_SIZE),
|
|
})
|
|
}
|
|
var result []byte
|
|
if len(key) > 0 {
|
|
encrypted = true
|
|
key := padBytes(32, key)
|
|
aead, err := chacha20poly1305.NewX(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nonce = generateUnsafeKey(24)
|
|
result = aead.Seal([]byte{}, nonce, body, []byte{})
|
|
} else {
|
|
result = body
|
|
}
|
|
detectedLang := enry.GetLanguage(name, body)
|
|
if detectedLang == "" {
|
|
a := lexers.Analyse(string(body))
|
|
if a != nil {
|
|
detectedLang = a.Config().Name
|
|
}
|
|
}
|
|
lang := or(strings.ToLower(detectedLang), "text")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = store.Set(id, DatabaseDocument{
|
|
ID: id,
|
|
Name: name,
|
|
Lang: lang,
|
|
Nonce: nonce,
|
|
Body: result,
|
|
Encrypted: encrypted,
|
|
DeletionToken: deletionToken,
|
|
BurnAfterRead: c.Query("burn", "false") != "false",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.JSON(Document{
|
|
ID: id,
|
|
Name: name,
|
|
Lang: lang,
|
|
Encrypted: encrypted,
|
|
DeletionToken: deletionToken,
|
|
BurnAfterRead: c.Query("burn", "false") != "false",
|
|
})
|
|
})
|
|
|
|
app.Get("/ctrl-c/:id", func(c *fiber.Ctx) error {
|
|
id := c.Params("id")
|
|
if id == "" {
|
|
c.Status(404)
|
|
return c.SendString("no")
|
|
}
|
|
key := []byte(c.Query("key"))
|
|
if len(key) > 32 {
|
|
return c.Status(400).JSON(map[string]string{
|
|
"error": "Key must not exceed 32 bytes (256 bits)",
|
|
})
|
|
}
|
|
key = padBytes(32, key)
|
|
var doc DatabaseDocument
|
|
found, err := store.Get(id, &doc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !found {
|
|
return c.Status(404).JSON(map[string]string{"error": "Document not found"})
|
|
}
|
|
// if sha512.Sum512(key) != doc.KeyHash {
|
|
// return c.Status(403).JSON(map[string]string{"error": "Invalid key provided!"})
|
|
// }
|
|
var body []byte
|
|
if doc.Encrypted {
|
|
if len(key) == 0 {
|
|
return c.Status(401).JSON(map[string]string{
|
|
"error": "No key provided (?key=[...] parameter missing or empty)",
|
|
})
|
|
}
|
|
aead, err := chacha20poly1305.NewX(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var dst []byte
|
|
body, err = aead.Open(dst, doc.Nonce, doc.Body, []byte{})
|
|
if err != nil {
|
|
if strings.HasSuffix(err.Error(), "message authentication failed") {
|
|
return c.Status(403).JSON(map[string]string{"error": "Invalid key provided!"})
|
|
}
|
|
return err
|
|
}
|
|
} else {
|
|
body = doc.Body
|
|
}
|
|
if doc.BurnAfterRead {
|
|
err = store.Delete(id)
|
|
if err != nil {
|
|
log.Err(err).Msg("Failed to burn document")
|
|
}
|
|
}
|
|
return c.JSON(Document{
|
|
ID: doc.ID,
|
|
Name: doc.Name,
|
|
Lang: doc.Lang,
|
|
Body: string(body),
|
|
Encrypted: doc.Encrypted,
|
|
BurnAfterRead: doc.BurnAfterRead,
|
|
})
|
|
})
|
|
|
|
app.Delete("/ctrl-c/:id", func(c *fiber.Ctx) error {
|
|
id := c.Params("id")
|
|
token := c.Query("token")
|
|
if token == "" {
|
|
return c.Status(401).JSON(map[string]string{
|
|
"error": "No deletion token provided!",
|
|
})
|
|
}
|
|
var doc DatabaseDocument
|
|
found, err := store.Get(id, &doc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !found {
|
|
return c.Status(404).JSON(map[string]string{"error": "Document not found"})
|
|
}
|
|
if token != doc.DeletionToken {
|
|
return c.Status(401).JSON(map[string]string{
|
|
"error": "Deletion token doesn't match!",
|
|
})
|
|
}
|
|
err = store.Delete(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.JSON(Document{
|
|
ID: doc.ID,
|
|
Name: doc.Name,
|
|
Lang: doc.Lang,
|
|
Encrypted: doc.Encrypted,
|
|
BurnAfterRead: doc.BurnAfterRead,
|
|
})
|
|
})
|
|
|
|
app.Post("/raw", func(c *fiber.Ctx) error {
|
|
name := c.Query("name")
|
|
nonce := c.Query("nonce")
|
|
lang := or(c.Query("lang"), "text")
|
|
id := randomString(7, ID_ALPHABET)
|
|
deletionToken := randomString(15, ID_ALPHABET)
|
|
encrypted := c.Query("encrypted", "false") != "false"
|
|
if nonce == "" && encrypted {
|
|
return c.Status(400).JSON(map[string]string{
|
|
"error": "No nonce provided and data is encrypted!",
|
|
})
|
|
}
|
|
body := c.Body()
|
|
if len(body) > MAX_DOCUMENT_SIZE && MAX_DOCUMENT_SIZE > 0 && c.Query("admin") != ADMIN_KEY {
|
|
return c.Status(413).JSON(map[string]string{
|
|
"error": fmt.Sprintf("Document size must not exceed %d bytes", MAX_DOCUMENT_SIZE),
|
|
})
|
|
}
|
|
var result []byte
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = store.Set(id, DatabaseDocument{
|
|
ID: id,
|
|
Name: name,
|
|
Lang: lang,
|
|
Body: result,
|
|
Encrypted: encrypted,
|
|
Nonce: []byte(nonce),
|
|
DeletionToken: deletionToken,
|
|
BurnAfterRead: c.Query("burn", "false") != "false",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.JSON(Document{
|
|
ID: id,
|
|
Name: name,
|
|
Lang: lang,
|
|
Encrypted: encrypted,
|
|
DeletionToken: deletionToken,
|
|
BurnAfterRead: c.Query("burn", "false") != "false",
|
|
})
|
|
})
|
|
|
|
app.Get("/raw/:id", func(c *fiber.Ctx) error {
|
|
id := c.Params("id")
|
|
if id == "" {
|
|
return c.Next()
|
|
}
|
|
var doc DatabaseDocument
|
|
found, err := store.Get(id, &doc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !found {
|
|
return c.Status(404).JSON(map[string]string{"error": "Document not found"})
|
|
}
|
|
return c.JSON(Document{
|
|
ID: id,
|
|
Name: doc.Name,
|
|
Lang: doc.Lang,
|
|
Body: base64.StdEncoding.EncodeToString(doc.Body),
|
|
Encrypted: doc.Encrypted,
|
|
BurnAfterRead: doc.BurnAfterRead,
|
|
})
|
|
})
|
|
|
|
err = app.Listen(":1323")
|
|
if err != nil {
|
|
log.Panic().Err(err).Msg("Failed to start the Fiber server!")
|
|
}
|
|
}
|