import { Database } from "bun:sqlite"; import cron from "@elysiajs/cron"; import { html } from "@elysiajs/html"; import staticPlugin from "@elysiajs/static"; import { randomUUIDv7 } from "bun"; import { Elysia, StatusMap, t } from "elysia"; import { fileTypeFromBuffer } from "file-type"; import { filetypes, Index, NotFound, SetCookie, ShowFile, WrongPassword, } from "./components"; import { config } from "./config"; import { decrypt, encrypt } from "./crypto"; const db = new Database("./db/db.sqlite"); db.run("PRAGMA foreign_keys = ON"); db.run("PRAGMA journal_mode = WAL"); db.run( "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.run("PRAGMA optimize"); // uuid route params are constrained to this shape so they can't be used to // inject CRLF/extra directives into the Set-Cookie Path or content-disposition. const UUID_PATTERN = "^[0-9a-fA-F-]{36}$"; // Per-IP timestamp of the last accepted upload, used for the upload cooldown. // Pruned by the cron below so it can't grow without bound. const lastUpload = new Map(); type MinimalServer = { requestIP(req: Request): { address: string } | null; } | null; function clientIp( server: MinimalServer, request: Request, headers: Record, ): string { if (config.behindProxy) { const xff = headers["x-forwarded-for"]?.split(",")[0]?.trim(); if (xff) return xff; const real = headers["x-real-ip"]; if (real) return real; } return server?.requestIP(request)?.address ?? "unknown"; } 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: config.maxUploadBytes, }, }) .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')"); if (config.uploadCooldownSeconds > 0) { const cutoff = Date.now() - config.uploadCooldownSeconds * 1000; for (const [ip, ts] of lastUpload) { if (ts < cutoff) lastUpload.delete(ip); } } }, }), ) .get("/", ({ server }) => Index(server?.url.toString() ?? "")) .post( "/upload", async ({ set, body, server, request, headers }) => { const ip = clientIp(server, request, headers); const now = Date.now(); if (config.uploadCooldownSeconds > 0) { const last = lastUpload.get(ip) ?? 0; if (now - last < config.uploadCooldownSeconds * 1000) { set.status = 429; // Too Many Requests return "Upload cooldown active, please wait before uploading again"; } } if (body.file.size > config.maxUploadBytes) { set.status = 413; // Payload Too Large return `File exceeds the maximum upload size of ${config.maxUploadBytes} bytes`; } const uuid = randomUUIDv7(); let content: Uint8Array = Buffer.from(await body.file.bytes()); let encrypted = false; // Retention: take the requested minutes (if any) and clamp it to the // configured maximum age, so storage is time-bounded when MAX_AGE_MINUTES is set. let minutes: number | null = body.delete_in_minutes && Number(body.delete_in_minutes) > 0 ? Number(body.delete_in_minutes) : null; if (config.maxAgeMinutes !== null) { minutes = Math.min(minutes ?? config.maxAgeMinutes, config.maxAgeMinutes); } const delete_at = minutes !== null ? Math.floor(now / 1000) + minutes * 60 : null; if (body.encrypted === "on") { encrypted = true; } else if (body.password) { content = await 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, ], ); if (config.uploadCooldownSeconds > 0) lastUpload.set(ip, now); set.status = StatusMap["See Other"]; set.headers.location = `/show/${uuid}`; return `Created with id: ${uuid}`; }, { body: t.Object({ file: t.File(), filename: t.Optional(t.String()), filetype: t.Enum(stringArrayToEnum(filetypes)), password: t.Optional(t.String()), encrypted: t.Optional(t.String()), delete_in_minutes: t.Optional( t.String({ format: "regex", pattern: "(^$|^[0-9]+$)", }), ), }), }, ) .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: number; 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 ShowFile( result.filename, params.uuid, null, result.filetype, result.delete_at, ); } else { try { result.content = await decrypt(result.content, password); } catch (_e) { const secure = config.behindProxy ? "; Secure" : ""; set.status = StatusMap.Forbidden; set.headers["set-cookie"] = [ `password=; Path=/show/${params.uuid}; SameSite=lax; HttpOnly${secure}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`, `password=; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly${secure}; 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({ format: "regex", pattern: UUID_PATTERN }), }), cookie: t.Object({ password: t.Optional(t.String()) }), }, ) .post( "/set-cookie/:uuid", ({ set, body, params }) => { // encodeURIComponent keeps ';', CR/LF and other separators out of the // cookie value; Elysia URL-decodes the value again when it reads it back. const value = encodeURIComponent(body.password); const secure = config.behindProxy ? "; Secure" : ""; set.headers["set-cookie"] = [ `password=${value}; Path=/show/${params.uuid}; SameSite=lax; HttpOnly${secure}`, `password=${value}; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly${secure}`, ]; 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({ format: "regex", pattern: UUID_PATTERN }), }), }, ) .get( "/raw/:uuid", async ({ 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: number; filetype: string; }) || null; if (!result) { set.status = StatusMap["Not Found"]; return "File not found"; } const servingEncrypted = result.encrypted && query.ignore_password === "true"; if (result.encrypted && !servingEncrypted) { 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 = await decrypt(result.content, cookie.password.value); } catch (_e) { set.status = StatusMap.Forbidden; return "Incorrect password"; } } // Never let the browser sniff stored content into an executable type // (e.g. HTML/SVG running as same-origin script). Only whitelisted, // non-scriptable media is served inline with its real type; everything // else (incl. still-encrypted bytes) is an octet-stream attachment. let mime = "application/octet-stream"; let disposition = "attachment"; if (!servingEncrypted) { const detected = await fileTypeFromBuffer(result.content); if ( detected && detected.mime !== "image/svg+xml" && (detected.mime.startsWith("image/") || detected.mime.startsWith("audio/") || detected.mime.startsWith("video/")) ) { mime = detected.mime; disposition = "inline"; } } const safeName = encodeURIComponent(result.filename); set.headers["x-content-type-options"] = "nosniff"; set.headers["content-type"] = mime; set.headers.encrypted = result.encrypted ? "true" : "false"; set.headers.filetype = result.filetype; set.headers.filename = safeName; set.headers["content-disposition"] = `${disposition}; filename*=UTF-8''${safeName}`; return result.content; }, { params: t.Object({ uuid: t.String({ format: "regex", pattern: UUID_PATTERN }), }), cookie: t.Object({ password: t.Optional(t.String()) }), query: t.Object({ ignore_password: t.Optional(t.String()) }), }, ) .listen(3000); console.log( `⚡ ZBin is running at ${app.server?.hostname}:${app.server?.port} ⚡`, );