index.ts
Raw
1import { Elysia, StatusMap, t } from "elysia"
2import { Database } from "bun:sqlite"
3import { randomUUIDv7 } from "bun"
4import staticPlugin from "@elysiajs/static"
5import { html } from '@elysiajs/html'
6import crypto from 'crypto'
7import { DecryptFile, Index, NotFound, SetCookie, ShowFile, WrongPassword } from "./components"
8
9
10const db = new Database("./db.sqlite")
11db.exec("PRAGMA foreign_keys = ON")
12db.exec("PRAGMA journal_mode = WAL2")
13db.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) STRICT")
14db.exec("PRAGMA optimize")
15
16
17function encrypt(content: Uint8Array, password: string): Buffer<ArrayBuffer> {
18 let iv = crypto.randomBytes(16)
19 const salt = crypto.randomBytes(16)
20 const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512")
21 let cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
22 let encrypted_content = Buffer.concat([salt, iv, cipher.update(content), cipher.final()])
23 return encrypted_content
24}
25
26function decrypt(encrypted_content: Uint8Array, password: string): Buffer {
27 const salt = encrypted_content.slice(0, 16)
28 const iv = encrypted_content.slice(16, 32)
29 const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512")
30 let decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
31 let content = Buffer.concat([decipher.update(encrypted_content.slice(32)), decipher.final()])
32 return content
33}
34
35const app = new Elysia({
36 serve: {
37 maxRequestBodySize: 1024 * 1024 * 1024 // 1GB
38 }
39})
40 .use(staticPlugin({ assets: "./assets", prefix: "/" }))
41 .use(html())
42 .get("/", ({ server }) => Index(server?.url.toString()!, false))
43 .get("/js", ({ server }) => Index(server?.url.toString()!, true))
44 .post("/upload", async ({ set, body, query }) => {
45
46 const uuid = randomUUIDv7()
47 let content = Buffer.from(await body.file.bytes())
48 let encrypted = false;
49 if (body.encrypted === "on") {
50 encrypted = true
51 } else if (body.password) {
52 content = encrypt(content, body.password)
53 encrypted = true
54 }
55 db.exec("INSERT INTO files (uuid, filename, content, filetype, encrypted) VALUES (?, ?, ?, ?, ?)", [uuid, body.filename || body.file.name, content, body.filetype, encrypted])
56 set.status = StatusMap["See Other"]
57 if (query.withJs) {
58 set.headers["location"] = `/show-js/${uuid}`
59 } else {
60 set.headers["location"] = `/show/${uuid}`
61 }
62 return `Created with id: ${uuid}`
63
64 }, {
65 body: t.Object({ file: t.File(), filename: t.Optional(t.String()), filetype: t.String()/*todo:verifiy via t.Enum*/, password: t.Optional(t.String()), encrypted: t.Optional(t.String()) }),
66 query: t.Object({ withJs: t.Optional(t.Boolean()) })
67 })
68 .get("/show/:uuid", async ({ set, params, cookie }) => {
69
70 const result = db.prepare("SELECT filename, content, filetype, encrypted FROM files WHERE uuid = ?").get(params.uuid) as { filename: string, content: Uint8Array, filetype: string, encrypted: boolean } || null
71 if (!result) {
72 set.status = StatusMap["Not Found"]
73 return NotFound()
74 }
75 if (result.encrypted) {
76 const password = cookie.password.value
77 if (!password) {
78 return DecryptFile(result.filename, params.uuid)
79 } else {
80 try {
81 result.content = decrypt(result.content, password)
82 } catch (e) {
83 set.status = StatusMap["Forbidden"]
84 set.headers["set-cookie"] = [
85 `password=; Path=/show/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`,
86 `password=; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`
87 ]
88 return WrongPassword()
89 }
90 }
91 }
92 return ShowFile(result.filename, params.uuid, result.content, result.filetype)
93
94 }, { params: t.Object({ uuid: t.String() }) })
95 .get("/show-js/:uuid", async ({ set, params, cookie }) => {
96
97 const result = db.prepare("SELECT filename, content, filetype, encrypted FROM files WHERE uuid = ?").get(params.uuid) as { filename: string, content: Uint8Array, filetype: string, encrypted: boolean } || null
98 if (!result) {
99 set.status = StatusMap["Not Found"]
100 return NotFound()
101 }
102 return ShowFile(result.filename, params.uuid, null, result.filetype)
103
104 }, { params: t.Object({ uuid: t.String() }) })
105 .post("/set-cookie/:uuid", ({ set, body, params }) => {
106
107 set.headers["set-cookie"] = [
108 `password=${body.password}; Path=/show/${params.uuid}; SameSite=lax; HttpOnly`,
109 `password=${body.password}; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly`
110 ]
111 set.headers["location"] = `/show/${params.uuid}`
112 set.status = StatusMap["See Other"]
113 return SetCookie(params.uuid)
114
115 }, { body: t.Object({ password: t.String() }), params: t.Object({ uuid: t.String() }) })
116 .get("/raw/:uuid", ({ set, params, cookie, query }) => {
117 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
118 if (!result) {
119 set.status = StatusMap["Not Found"]
120 return "File not found"
121 }
122 if (result.encrypted && query.ignore_password !== "true") {
123 if (!cookie.password.value) {
124 set.status = StatusMap["Unauthorized"]
125 return "This file is encrypted, set the cookie \"password\" with the correct password to allow the server to decrypt it"
126 }
127 try {
128 result.content = decrypt(result.content, cookie.password.value)
129 } catch (e) {
130 set.status = StatusMap["Forbidden"]
131 return "Incorrect password"
132 }
133 }
134 set.headers["encrypted"] = result.encrypted ? "true" : "false"
135 set.headers["filetype"] = result.filetype
136 set.headers["filename"] = result.filename
137 set.headers["content-disposition"] = `inline; filename=${result.filename}`
138 return result.content
139
140 }, { params: t.Object({ uuid: t.String() }), cookie: t.Object({ password: t.Optional(t.String()) }), query: t.Object({ ignore_password: t.Optional(t.String()) }) })
141 .listen(3000)
142
143console.log(`⚡ Zigbin is running at ${app.server?.hostname}:${app.server?.port} ⚡`)