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

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!")
}
}