diff --git a/package.json b/package.json index 462d1e0..ac1d297 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,15 @@ "dedent-js": "^1.0.1", "itty-router": "^4.0.23", "js-sha256": "^0.10.1", - "nanoid": "^5.0.2" + "nanoid": "^5.0.2", + "xml2js": "^0.6.2" }, "devDependencies": { "@cloudflare/workers-types": "^4.20231025.0", "@types/bootstrap": "^5.2.8", "@types/jquery": "^3.5.25", + "@types/crypto-js": "4.2.2", + "@types/xml2js": "^0.4.14", "eslint": "^8.52.0", "eslint-config-prettier": "^9.0.0", "eslint-formatter-friendly": "^7.0.0", diff --git a/src/index.ts b/src/index.ts index f245121..203efc9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,9 +27,22 @@ import { get_presign_url, router as large_upload } from './v2/large_upload'; const router = Router(); +// Shared common properties to all route +router.all('*', (request) => { + const { headers } = request; + // Detect if request from browsers + const agent = headers.get('user-agent') ?? ''; + request.is_browser = ['Chrome', 'Mozilla', 'AppleWebKit', 'Safari', 'Gecko', 'Chromium'].some((v) => + agent.includes(v) + ); + // Append the origin/referer + request.origin = headers.get('origin') ?? undefined; +}); + // Handle preflighted CORS request router.options('*', (request) => { - const url = new URL(request.url); + if (!request.origin) return new Response(null); + const url = new URL(request.origin); // Allow all subdomain of nekoid.cc if (url.hostname.endsWith('nekoid.cc')) { return new Response(null, { @@ -37,21 +50,12 @@ router.options('*', (request) => { headers: { 'Access-Control-Allow-Origin': url.origin, 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS', - 'Vary': 'Origin', - } - }) + Vary: 'Origin', + }, + }); } }); -// Shared common properties to all route -router.all('*', (request) => { - // Detect if request from browsers - const agent = request.headers.get('user-agent') ?? ''; - request.is_browser = ['Chrome', 'Mozilla', 'AppleWebKit', 'Safari', 'Gecko', 'Chromium'].some((v) => - agent.includes(v) - ); -}); - /* Static file path */ // Web homepage router.get('/', (request) => { @@ -546,13 +550,14 @@ router.all('*', () => { }); export default { - fetch: (req: Request, env: Env, ctx: ExecutionContext) => + fetch: (req: ERequest, env: Env, ctx: ExecutionContext) => router .handle(req, env, ctx) .catch(error) // Apply CORS headers .then((res: Response) => { - const url = new URL(req.url); + if (!req.origin) return res; + const url = new URL(req.origin); // Allow all subdomain of nekoid.cc if (url.hostname.endsWith('nekoid.cc')) { res.headers.set('Access-Control-Allow-Origin', url.origin); diff --git a/src/types.d.ts b/src/types.d.ts index ed419b8..c9fb09f 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -2,6 +2,7 @@ import { IRequest } from 'itty-router'; export type ERequest = { is_browser: boolean; + origin?: string; // match_etag?: string; } & IRequest; @@ -20,6 +21,8 @@ export interface PasteIndexEntry { // Only apply when large_paste upload_completed?: boolean; sha256_hash?: string; + cached_presigned_url?: string; + cached_presigned_url_expiration?: string; } export interface Env { diff --git a/src/utils.ts b/src/utils.ts index 366ed5e..52c869d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -23,14 +23,7 @@ import { PasteIndexEntry, Env } from './types'; export const gen_id = customAlphabet('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', UUID_LENGTH); -export async function get_paste_info( - uuid: string, - descriptor: PasteIndexEntry, - env: Env, - use_html: boolean = true, - need_qr: boolean = false, - reply_json = false -): Promise { +export function get_paste_info_obj(uuid: string, descriptor: PasteIndexEntry, env: Env) { const created = new Date(descriptor.last_modified); const expired = new Date(descriptor.expiration ?? descriptor.last_modified + 2419200000); const link = `https://${SERVICE_URL}/${uuid}`; @@ -49,6 +42,18 @@ export async function get_paste_info( expired: expired.toISOString(), update_completed: descriptor.upload_completed ?? undefined, // only for large_paste }; + return paste_info; +} + +export async function get_paste_info( + uuid: string, + descriptor: PasteIndexEntry, + env: Env, + use_html: boolean = true, + need_qr: boolean = false, + reply_json = false +): Promise { + const paste_info = get_paste_info_obj(uuid, descriptor, env); // Reply with JSON if (reply_json) { @@ -63,7 +68,7 @@ export async function get_paste_info( // Plain text reply let content = dedent` uuid: ${uuid} - link: ${link} + link: ${paste_info.link} type: ${paste_info.type ?? 'paste'} title: ${paste_info.title || '-'} mime-type: ${paste_info.mime_type ?? '-'} @@ -95,7 +100,7 @@ export async function get_paste_info( ${ need_qr ? `${link}` + alt="${paste_info.link}" style="max-width: 280px">` : '' } @@ -116,7 +121,7 @@ export async function get_paste_info( const res = await env.QRCODE.fetch( 'https://qrcode.nekoid.cc?' + new URLSearchParams({ - q: link, + q: paste_info.link, type: 'utf8', }) ); diff --git a/src/v2/large_upload.ts b/src/v2/large_upload.ts index dbfdc2a..9916357 100644 --- a/src/v2/large_upload.ts +++ b/src/v2/large_upload.ts @@ -1,8 +1,9 @@ import { Router } from 'itty-router'; import { sha256 } from 'js-sha256'; import { AwsClient } from 'aws4fetch'; +import { parseStringPromise } from 'xml2js'; import { ERequest, Env, PasteIndexEntry } from '../types'; -import { gen_id } from '../utils'; +import { gen_id, get_paste_info_obj } from '../utils'; import { UUID_LENGTH } from '../constant'; export const router = Router({ base: '/v2/large_upload' }); @@ -195,24 +196,31 @@ router.post('/complete/:uuid', async (request, env, ctx) => { }); try { - const objectmeta = await s3.fetch(`${env.LARGE_ENDPOINT}/${uuid}`, { - method: 'HEAD', + // Get object attributes + const objectmeta = await s3.fetch(`${env.LARGE_DOWNLOAD_ENDPOINT}/${uuid}?attributes`, { + method: 'GET', + headers: { + 'X-AMZ-Object-Attributes': 'ObjectSize', + }, }); if (objectmeta.ok) { - const { headers } = objectmeta; - const file_size = headers.get('Content-Length') || '0'; + const xml = await objectmeta.text(); + const parsed = await parseStringPromise(xml, { + tagNameProcessors: [(name) => name.toLowerCase()], + }); + const file_size = parsed.getobjectattributesresponse.objectsize[0]; if (parseInt(file_size) !== descriptor.size) { return new Response('This paste is not finishing the upload.\n', { status: 400, }); } } else { - return new Response('This paste is not finishing the upload.\n', { + return new Response('This paste is not finishing upload.\n', { status: 400, }); } } catch (err) { - return new Response('Unable to connect to remote.\n', { + return new Response('Internal server error.\n', { status: 500, }); } @@ -225,14 +233,12 @@ router.post('/complete/:uuid', async (request, env, ctx) => { ctx.waitUntil(env.PASTE_INDEX.put(uuid, JSON.stringify(descriptor), { expirationTtl: 2419200 })); const paste_info = { - uuid, upload_completed: true, expired: new Date(expriation).toISOString(), + paste_info: get_paste_info_obj(uuid, descriptor, env), }; - return new Response(JSON.stringify(paste_info), { - status: 400, - }); + return new Response(JSON.stringify(paste_info)); }); router.get('/:uuid', async (request, env, ctx) => { diff --git a/wrangler.toml b/wrangler.toml index f1f2cf9..5eb9e3e 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -8,6 +8,7 @@ kv_namespaces = [ services = [ { binding = "QRCODE", service = "qrcode-gen", environment = "production" } ] +node_compat = true # [secret] # AWS_ACCESS_KEY_ID diff --git a/yarn.lock b/yarn.lock index b3d0578..ee6da17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -302,6 +302,11 @@ dependencies: "@popperjs/core" "^2.9.2" +"@types/crypto-js@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea" + integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ== + "@types/jquery@^3.5.25": version "3.5.29" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.29.tgz#3c06a1f519cd5fc3a7a108971436c00685b5dcea" @@ -333,6 +338,13 @@ resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.8.tgz#518609aefb797da19bf222feb199e8f653ff7627" integrity sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg== +"@types/xml2js@^0.4.14": + version "0.4.14" + resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.14.tgz#5d462a2a7330345e2309c6b549a183a376de8f9a" + integrity sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ== + dependencies: + "@types/node" "*" + "@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" @@ -1729,6 +1741,11 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.2.2" is-regex "^1.1.4" +sax@>=0.6.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" + integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== + selfsigned@^2.0.1: version "2.4.1" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" @@ -2060,6 +2077,19 @@ ws@^8.11.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== +xml2js@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499" + integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + xxhash-wasm@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/xxhash-wasm/-/xxhash-wasm-1.0.2.tgz#ecc0f813219b727af4d5f3958ca6becee2f2f1ff"