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, filetypes, Index, NotFound, SetCookie, ShowFile, WrongPassword } from "./components"
8import cron from "@elysiajs/cron"
9
10const db = new Database("./db/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, delete_at INTEGER) 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
35function stringArrayToEnum<T extends string>(
36 arr: readonly T[]
37): { [K in T]: K } {
38 return arr.reduce((acc, key) => {
39 acc[key] = key;
40 return acc;
41 }, Object.create(null));
42}
43
44const app = new Elysia({
45 serve: {
46 maxRequestBodySize: 1024 * 1024 * 1024 // 1GB
47 }
48})
49 .use(staticPlugin({ assets: "./assets", prefix: "/" }))
50 .use(html())
51 .use(cron({
52 name: "delete",
53 pattern: "*/5 * * * * *",
54 run() {
55 db.exec("DELETE FROM files WHERE delete_at < strftime('%s', 'now')")
56 }
57 }))
58 .get("/", ({ server }) => Index(server?.url.toString()!, false))
59 .get("/js", ({ server }) => Index(server?.url.toString()!, true))
60 .post("/upload", async ({ set, body, query }) => {
61
62 const uuid = randomUUIDv7()
63 let content = Buffer.from(await body.file.bytes())
64 let encrypted = false;
65 let delete_at: number | null = null
66 if (body.delete_in_minutes) {
67 delete_at = Math.floor(Date.now() / 1000) + Number(body.delete_in_minutes) * 60
68 }
69 if (body.encrypted === "on") {
70 encrypted = true
71 } else if (body.password) {
72 content = encrypt(content, body.password)
73 encrypted = true
74 }
75 db.exec("INSERT INTO files (uuid, filename, content, filetype, encrypted, delete_at) VALUES (?, ?, ?, ?, ?, ?)",
76 [uuid, body.filename || body.file.name, content, body.filetype, encrypted, delete_at])
77 set.status = StatusMap["See Other"]
78 if (query.withJs) {
79 set.headers["location"] = `/show-js/${uuid}`
80 } else {
81 set.headers["location"] = `/show/${uuid}`
82 }
83 return `Created with id: ${uuid}`
84
85 }, {
86 body: t.Object({
87 file: t.File(),
88 filename: t.Optional(t.String()),
89 //filetype: t.String()/*todo:verifiy via t.Enum*/,
90 filetype: t.Enum(stringArrayToEnum(filetypes)),
91 password: t.Optional(t.String()),
92 encrypted: t.Optional(t.String()),
93 delete_in_minutes: t.String({ format: "regex", pattern: "(^$|^[0-9]+$)" })
94 }),
95 query: t.Object({ withJs: t.Optional(t.Boolean()) })
96 })
97 .get("/show/:uuid", async ({ set, params, cookie }) => {
98
99 const result = db.prepare("SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?").get(params.uuid) as
100 { filename: string, content: Uint8Array, filetype: string, encrypted: boolean, delete_at: number | null } || null
101 if (!result) {
102 set.status = StatusMap["Not Found"]
103 return NotFound()
104 }
105 if (result.encrypted) {
106 const password = cookie.password.value
107 if (!password) {
108 return DecryptFile(result.filename, params.uuid)
109 } else {
110 try {
111 result.content = decrypt(result.content, password)
112 } catch (e) {
113 set.status = StatusMap["Forbidden"]
114 set.headers["set-cookie"] = [
115 `password=; Path=/show/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`,
116 `password=; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`
117 ]
118 return WrongPassword()
119 }
120 }
121 }
122 return ShowFile(result.filename, params.uuid, result.content, result.filetype, result.delete_at)
123
124 }, { params: t.Object({ uuid: t.String() }) })
125 .get("/show-js/:uuid", async ({ set, params, cookie }) => {
126
127 const result = db.prepare("SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?").get(params.uuid) as
128 { filename: string, content: Uint8Array, filetype: string, encrypted: boolean, delete_at: number | null } || null
129 if (!result) {
130 set.status = StatusMap["Not Found"]
131 return NotFound()
132 }
133 return ShowFile(result.filename, params.uuid, null, result.filetype, result.delete_at)
134
135 }, { params: t.Object({ uuid: t.String() }) })
136 .post("/set-cookie/:uuid", ({ set, body, params }) => {
137
138 set.headers["set-cookie"] = [
139 `password=${body.password}; Path=/show/${params.uuid}; SameSite=lax; HttpOnly`,
140 `password=${body.password}; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly`
141 ]
142 set.headers["location"] = `/show/${params.uuid}`
143 set.status = StatusMap["See Other"]
144 return SetCookie(params.uuid)
145
146 }, { body: t.Object({ password: t.String() }), params: t.Object({ uuid: t.String() }) })
147 .get("/raw/:uuid", ({ set, params, cookie, query }) => {
148 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
149 if (!result) {
150 set.status = StatusMap["Not Found"]
151 return "File not found"
152 }
153 if (result.encrypted && query.ignore_password !== "true") {
154 if (!cookie.password.value) {
155 set.status = StatusMap["Unauthorized"]
156 return "This file is encrypted, set the cookie \"password\" with the correct password to allow the server to decrypt it"
157 }
158 try {
159 result.content = decrypt(result.content, cookie.password.value)
160 } catch (e) {
161 set.status = StatusMap["Forbidden"]
162 return "Incorrect password"
163 }
164 }
165 set.headers["encrypted"] = result.encrypted ? "true" : "false"
166 set.headers["filetype"] = result.filetype
167 set.headers["filename"] = result.filename
168 set.headers["content-disposition"] = `inline; filename=${result.filename}`
169 return result.content
170
171 }, { params: t.Object({ uuid: t.String() }), cookie: t.Object({ password: t.Optional(t.String()) }), query: t.Object({ ignore_password: t.Optional(t.String()) }) })
172 .listen(3000)
173
174console.log(`⚡ Zigbin is running at ${app.server?.hostname}:${app.server?.port} ⚡`)