diff --git a/package.json b/package.json index 96817ca..9f4d92a 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "dependencies": { "aws4fetch": "^1.0.13", "nanoid": "^3.3.4", - "mime-types": "^2.1.35", "js-sha256": "^0.9.0" }, "devDependencies": { diff --git a/paste.html b/paste.html index 9185f1f..90e70f8 100644 --- a/paste.html +++ b/paste.html @@ -29,7 +29,7 @@ pb.nekoul.com is a pastebin-like service hosted on Cloudflare Worker.
This service is primarily designed for own usage and interest only.
All data may be deleted or expired without any notification and guarantee. Please DO NOT abuse this service.
- The limit for file upload is 10 MB and the paste will be kept for 28 days only by default.
+ The limit for file upload is 10 MB and the paste will be kept for 28 days only by default.
The source code is available in my GitHub repository [here].
This webpage is designed for upload files only. For other operations like changing paste settings and deleting paste, please make use of the diff --git a/src/index.ts b/src/index.ts index 15dea72..fe53aa0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,6 @@ import {AwsClient} from "aws4fetch"; import {customAlphabet} from "nanoid"; -import {contentType} from "mime-types"; import {sha256} from "js-sha256"; // Constants @@ -27,14 +26,14 @@ const PASTE_INDEX_HTML_URL = "https://raw.githubusercontent.com/rikkaneko/paste/ const UUID_LENGTH = 4 export interface Env { - PASTE_INDEX: KVNamespace; - AWS_ACCESS_KEY_ID: string; - AWS_SECRET_ACCESS_KEY: string; - ENDPOINT: string + PASTE_INDEX: KVNamespace; + AWS_ACCESS_KEY_ID: string; + AWS_SECRET_ACCESS_KEY: string; + ENDPOINT: string } const API_SPEC_TEXT = - `Paste service https://${SERVICE_URL} +`Paste service https://${SERVICE_URL} [API Specification] GET / Fetch the Web frontpage for uploading text/file [x] @@ -82,407 +81,415 @@ const gen_id = customAlphabet( "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", UUID_LENGTH); export default { - async fetch( - request: Request, - env: Env, - ctx: ExecutionContext - ): Promise { - const {url, method, headers} = request; - const {pathname} = new URL(url); - const path = pathname.replace(/\/+$/, "") || "/"; - let cache = caches.default; + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + const {url, method, headers} = request; + const {pathname} = new URL(url); + const path = pathname.replace(/\/+$/, "") || "/"; + let cache = caches.default; - const s3 = new AwsClient({ - accessKeyId: env.AWS_ACCESS_KEY_ID, - secretAccessKey: env.AWS_SECRET_ACCESS_KEY - }); + const s3 = new AwsClient({ + accessKeyId: env.AWS_ACCESS_KEY_ID, + secretAccessKey: env.AWS_SECRET_ACCESS_KEY + }); - // Special path - if (path === "/favicon.ico" && method == "GET") { - return new Response(null, { - status: 404 + // Special path + if (path === "/favicon.ico" && method == "GET") { + return new Response(null, { + status: 404 + }) + } + + if (path === "/api" && method == "GET") { + return new Response(API_SPEC_TEXT); + } + + if (path === "/") { + switch (method) { + // Fetch the HTML for uploading text/file + case "GET": { + return await fetch(PASTE_INDEX_HTML_URL, { + cf: { + cacheEverything: true + } + }).then(value => { + let res = new Response(value.body, value); + // Add the correct content-type to response header + res.headers.set("content-type", "text/html; charset=UTF-8;"); + // Remove the default CSP header + res.headers.delete("content-security-policy"); + return res; + }) + } + + // Create new paste + case "POST": + const uuid = gen_id(); + let buffer: ArrayBuffer; + let title: string | undefined; + // Handle content-type + const content_type = headers.get("content-type") || ""; + let mime_type: string | undefined; + let password: string | undefined; + let read_limit: number | undefined; + // Content-Type: multipart/form-data + if (content_type.includes("form")) { + const formdata = await request.formData(); + const data = formdata.get("u"); + if (data === null) { + return new Response("Invalid request.\n", { + status: 422 + }) + } + // File + if (data instanceof File) { + if (data.name) { + title = data.name; + } + buffer = await data.arrayBuffer(); + // Text + } else { + buffer = new TextEncoder().encode(data) + mime_type = "text/plain; charset=UTF-8;" + } + + // Set password + const pass = formdata.get("pass"); + if (typeof pass === "string") { + password = pass || undefined; + } + + const count = formdata.get("read-limit"); + if (typeof count === "string" && !isNaN(+count)) { + read_limit = Number(count) || undefined; + } + + // Raw body + } else { + if (headers.has("x-title")) { + title = headers.get("x-title") || ""; + } + mime_type = headers.get("content-type") || mime_type; + password = headers.get("x-pass") || undefined; + const count = headers.get("x-read-limit") || undefined; + if (count !== undefined && !isNaN(+count)) { + read_limit = Number(count) || undefined; + } + buffer = await request.arrayBuffer(); + } + + // Check password rules + if (password && !check_password_rules(password)) { + return new Response("Invalid password. " + + "Password must contain alphabets and digits only, and has a length of 4 or more.", { + status: 422 }) - } + } - if (path === "/api" && method == "GET") { - return new Response(API_SPEC_TEXT); - } + // Check request.body size <= 10MB + const size = buffer.byteLength; + if (size > 10485760) { + return new Response("Paste size must be under 10MB.\n", { + status: 422 + }); + } - if (path === "/") { - switch (method) { - // Fetch the HTML for uploading text/file - case "GET": { - return await fetch(PASTE_INDEX_HTML_URL, { - cf: { - cacheEverything: true - } - }).then(value => { - let res = new Response(value.body, value); - // Add the correct content-type to response header - res.headers.set("content-type", "text/html; charset=UTF-8;"); - // Remove the default CSP header - res.headers.delete("content-security-policy"); - return res; - }) - } + // Check request.body size not empty + if (buffer.byteLength == 0) { + return new Response("Paste cannot be empty.\n", { + status: 422 + }); + } - // Create new paste - case "POST": - const uuid = gen_id(); - let buffer: ArrayBuffer; - let title: string | undefined; - // Handle content-type - const content_type = headers.get("content-type") || ""; - let mime_type: string | undefined; - let password: string | undefined; - let read_limit: number | undefined; - // Content-Type: multipart/form-data - if (content_type.includes("form")) { - const formdata = await request.formData(); - const data = formdata.get("u"); - if (data === null) { - return new Response("Invalid request.\n", { - status: 422 - }) - } - // File - if (data instanceof File) { - if (data.name) { - title = data.name; - mime_type = contentType(title) || undefined; - } - buffer = await data.arrayBuffer(); - // Text - } else { - buffer = new TextEncoder().encode(data) - mime_type = "text/plain; charset=UTF-8;" - } + const res = await s3.fetch(`${env.ENDPOINT}/${uuid}`, { + method: "PUT", + body: buffer + }); - // Set password - const pass = formdata.get("pass"); - if (typeof pass === "string") { - password = pass || undefined; - } + if (res.ok) { + // Upload success + const descriptor: PasteIndexEntry = { + title: title ?? undefined, + last_modified: Date.now(), + password: password? sha256(password).slice(0, 16): undefined, + size, + read_count_remain: read_limit, + mime_type + }; - const count = formdata.get("read-limit"); - if (typeof count === "string" && !isNaN(+count)) { - read_limit = Number(count) || undefined; - } + // Key will be expired after 28 day if unmodified + ctx.waitUntil(env.PASTE_INDEX.put(uuid, JSON.stringify(descriptor), {expirationTtl: 100800})); + return new Response(get_paste_info(uuid, descriptor)); + } else { + return new Response("Unable to upload the paste.\n", { + status: 500 + }); + } - // Raw body - } else { - if (headers.has("x-title")) { - title = headers.get("x-title") || ""; - mime_type = contentType(title) || undefined; - } - mime_type = headers.get("content-type") || mime_type; - password = headers.get("x-pass") || undefined; - const count = headers.get("x-read-limit") || undefined; - if (count !== undefined && !isNaN(+count)) { - read_limit = Number(count) || undefined; - } - buffer = await request.arrayBuffer(); - } + } - // Check password rules - if (password && !check_password_rules(password)) { - return new Response("Invalid password. " + - "Password must contain alphabets and digits only, and has a length of 4 or more.", { - status: 422 - }) - } - - // Check request.body size <= 10MB - const size = buffer.byteLength; - if (size > 10485760) { - return new Response("Paste size must be under 10MB.\n", { - status: 422 - }); - } - - // Check request.body size not empty - if (buffer.byteLength == 0) { - return new Response("Paste cannot be empty.\n", { - status: 422 - }); - } - - const res = await s3.fetch(`${env.ENDPOINT}/${uuid}`, { - method: "PUT", - body: buffer - }); - - if (res.ok) { - // Upload success - const descriptor: PasteIndexEntry = { - title: title ?? undefined, - last_modified: Date.now(), - password: password? sha256(password).slice(0, 16): undefined, - size, - read_count_remain: read_limit, - mime_type - }; - - ctx.waitUntil(env.PASTE_INDEX.put(uuid, JSON.stringify(descriptor))); - return new Response(get_paste_info(uuid, descriptor)); - } else { - return new Response("Unable to upload the paste.\n", { - status: 500 - }); - } - - } - - } else if (path.length >= UUID_LENGTH + 1) { - // RegExpr to match //