import { Elysia, StatusMap, t } from "elysia" import { Database } from "bun:sqlite" import { randomUUIDv7 } from "bun" import staticPlugin from "@elysiajs/static" import { html } from '@elysiajs/html' import crypto from 'crypto' import { DecryptFile, filetypes, Index, NotFound, SetCookie, ShowFile, WrongPassword } from "./components" import cron from "@elysiajs/cron" //TODO: add deletion timer const db = new Database("./db.sqlite") db.exec("PRAGMA foreign_keys = ON") db.exec("PRAGMA journal_mode = WAL2") db.exec("CREATE TABLE IF NOT EXISTS files (uuid TEXT PRIMARY KEY, filename TEXT NOT NULL, content BLOB NOT NULL, filetype TEXT NOT NULL, encrypted INTEGER NOT NULL, delete_at INTEGER) STRICT") db.exec("PRAGMA optimize") function encrypt(content: Uint8Array, password: string): Buffer { let iv = crypto.randomBytes(16) const salt = crypto.randomBytes(16) const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512") let cipher = crypto.createCipheriv('aes-256-cbc', key, iv) let encrypted_content = Buffer.concat([salt, iv, cipher.update(content), cipher.final()]) return encrypted_content } function decrypt(encrypted_content: Uint8Array, password: string): Buffer { const salt = encrypted_content.slice(0, 16) const iv = encrypted_content.slice(16, 32) const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512") let decipher = crypto.createDecipheriv('aes-256-cbc', key, iv) let content = Buffer.concat([decipher.update(encrypted_content.slice(32)), decipher.final()]) return content } function stringArrayToEnum( arr: readonly T[] ): { [K in T]: K } { return arr.reduce((acc, key) => { acc[key] = key; return acc; }, Object.create(null)); } const app = new Elysia({ serve: { maxRequestBodySize: 1024 * 1024 * 1024 // 1GB } }) .use(staticPlugin({ assets: "./assets", prefix: "/" })) .use(html()) .use(cron({ name: "delete", pattern: "*/5 * * * * *", run() { db.exec("DELETE FROM files WHERE delete_at < strftime('%s', 'now')") } })) .get("/", ({ server }) => Index(server?.url.toString()!, false)) .get("/js", ({ server }) => Index(server?.url.toString()!, true)) .post("/upload", async ({ set, body, query }) => { const uuid = randomUUIDv7() let content = Buffer.from(await body.file.bytes()) let encrypted = false; let delete_at: number | null = null if (body.delete_in_minutes) { delete_at = Math.floor(Date.now() / 1000) + Number(body.delete_in_minutes) * 60 } if (body.encrypted === "on") { encrypted = true } else if (body.password) { content = encrypt(content, body.password) encrypted = true } db.exec("INSERT INTO files (uuid, filename, content, filetype, encrypted, delete_at) VALUES (?, ?, ?, ?, ?, ?)", [uuid, body.filename || body.file.name, content, body.filetype, encrypted, delete_at]) set.status = StatusMap["See Other"] if (query.withJs) { set.headers["location"] = `/show-js/${uuid}` } else { set.headers["location"] = `/show/${uuid}` } return `Created with id: ${uuid}` }, { body: t.Object({ file: t.File(), filename: t.Optional(t.String()), //filetype: t.String()/*todo:verifiy via t.Enum*/, filetype: t.Enum(stringArrayToEnum(filetypes)), password: t.Optional(t.String()), encrypted: t.Optional(t.String()), delete_in_minutes: t.String({ format: "regex", pattern: "(^$|^[0-9]+$)" }) }), query: t.Object({ withJs: t.Optional(t.Boolean()) }) }) .get("/show/:uuid", async ({ set, params, cookie }) => { const result = db.prepare("SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?").get(params.uuid) as { filename: string, content: Uint8Array, filetype: string, encrypted: boolean, delete_at: number | null } || null if (!result) { set.status = StatusMap["Not Found"] return NotFound() } if (result.encrypted) { const password = cookie.password.value if (!password) { return DecryptFile(result.filename, params.uuid) } else { try { result.content = decrypt(result.content, password) } catch (e) { set.status = StatusMap["Forbidden"] set.headers["set-cookie"] = [ `password=; Path=/show/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`, `password=; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT` ] return WrongPassword() } } } return ShowFile(result.filename, params.uuid, result.content, result.filetype, result.delete_at) }, { params: t.Object({ uuid: t.String() }) }) .get("/show-js/:uuid", async ({ set, params, cookie }) => { const result = db.prepare("SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?").get(params.uuid) as { filename: string, content: Uint8Array, filetype: string, encrypted: boolean, delete_at: number | null } || null if (!result) { set.status = StatusMap["Not Found"] return NotFound() } return ShowFile(result.filename, params.uuid, null, result.filetype, result.delete_at) }, { params: t.Object({ uuid: t.String() }) }) .post("/set-cookie/:uuid", ({ set, body, params }) => { set.headers["set-cookie"] = [ `password=${body.password}; Path=/show/${params.uuid}; SameSite=lax; HttpOnly`, `password=${body.password}; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly` ] set.headers["location"] = `/show/${params.uuid}` set.status = StatusMap["See Other"] return SetCookie(params.uuid) }, { body: t.Object({ password: t.String() }), params: t.Object({ uuid: t.String() }) }) .get("/raw/:uuid", ({ set, params, cookie, query }) => { const result = db.prepare("SELECT content, filename, encrypted, filetype FROM files WHERE uuid = ?").get(params.uuid) as { content: Uint8Array, filename: string, encrypted: boolean, filetype: string } || null if (!result) { set.status = StatusMap["Not Found"] return "File not found" } if (result.encrypted && query.ignore_password !== "true") { if (!cookie.password.value) { set.status = StatusMap["Unauthorized"] return "This file is encrypted, set the cookie \"password\" with the correct password to allow the server to decrypt it" } try { result.content = decrypt(result.content, cookie.password.value) } catch (e) { set.status = StatusMap["Forbidden"] return "Incorrect password" } } set.headers["encrypted"] = result.encrypted ? "true" : "false" set.headers["filetype"] = result.filetype set.headers["filename"] = result.filename set.headers["content-disposition"] = `inline; filename=${result.filename}` return result.content }, { params: t.Object({ uuid: t.String() }), cookie: t.Object({ password: t.Optional(t.String()) }), query: t.Object({ ignore_password: t.Optional(t.String()) }) }) .listen(3000) console.log(`⚡ Zigbin is running at ${app.server?.hostname}:${app.server?.port} ⚡`)