Use GetObjectAttributes to get object size (large_paste)

Fix CORS handling

Signed-off-by: Joe Ma <rikkaneko23@gmail.com>
This commit is contained in:
Joe Ma 2024-02-04 19:23:09 +08:00
parent 6be1e97122
commit 2f67469ef5
No known key found for this signature in database
GPG key ID: 7A0ECF5F5EDC587F
7 changed files with 91 additions and 38 deletions

View file

@ -13,12 +13,15 @@
"dedent-js": "^1.0.1", "dedent-js": "^1.0.1",
"itty-router": "^4.0.23", "itty-router": "^4.0.23",
"js-sha256": "^0.10.1", "js-sha256": "^0.10.1",
"nanoid": "^5.0.2" "nanoid": "^5.0.2",
"xml2js": "^0.6.2"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20231025.0", "@cloudflare/workers-types": "^4.20231025.0",
"@types/bootstrap": "^5.2.8", "@types/bootstrap": "^5.2.8",
"@types/jquery": "^3.5.25", "@types/jquery": "^3.5.25",
"@types/crypto-js": "4.2.2",
"@types/xml2js": "^0.4.14",
"eslint": "^8.52.0", "eslint": "^8.52.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-formatter-friendly": "^7.0.0", "eslint-formatter-friendly": "^7.0.0",

View file

@ -27,9 +27,22 @@ import { get_presign_url, router as large_upload } from './v2/large_upload';
const router = Router<ERequest, [Env, ExecutionContext]>(); const router = Router<ERequest, [Env, ExecutionContext]>();
// 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 // Handle preflighted CORS request
router.options('*', (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 // Allow all subdomain of nekoid.cc
if (url.hostname.endsWith('nekoid.cc')) { if (url.hostname.endsWith('nekoid.cc')) {
return new Response(null, { return new Response(null, {
@ -37,21 +50,12 @@ router.options('*', (request) => {
headers: { headers: {
'Access-Control-Allow-Origin': url.origin, 'Access-Control-Allow-Origin': url.origin,
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS', '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 */ /* Static file path */
// Web homepage // Web homepage
router.get('/', (request) => { router.get('/', (request) => {
@ -546,13 +550,14 @@ router.all('*', () => {
}); });
export default { export default {
fetch: (req: Request, env: Env, ctx: ExecutionContext) => fetch: (req: ERequest, env: Env, ctx: ExecutionContext) =>
router router
.handle(req, env, ctx) .handle(req, env, ctx)
.catch(error) .catch(error)
// Apply CORS headers // Apply CORS headers
.then((res: Response) => { .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 // Allow all subdomain of nekoid.cc
if (url.hostname.endsWith('nekoid.cc')) { if (url.hostname.endsWith('nekoid.cc')) {
res.headers.set('Access-Control-Allow-Origin', url.origin); res.headers.set('Access-Control-Allow-Origin', url.origin);

3
src/types.d.ts vendored
View file

@ -2,6 +2,7 @@ import { IRequest } from 'itty-router';
export type ERequest = { export type ERequest = {
is_browser: boolean; is_browser: boolean;
origin?: string;
// match_etag?: string; // match_etag?: string;
} & IRequest; } & IRequest;
@ -20,6 +21,8 @@ export interface PasteIndexEntry {
// Only apply when large_paste // Only apply when large_paste
upload_completed?: boolean; upload_completed?: boolean;
sha256_hash?: string; sha256_hash?: string;
cached_presigned_url?: string;
cached_presigned_url_expiration?: string;
} }
export interface Env { export interface Env {

View file

@ -23,14 +23,7 @@ import { PasteIndexEntry, Env } from './types';
export const gen_id = customAlphabet('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', UUID_LENGTH); export const gen_id = customAlphabet('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', UUID_LENGTH);
export async function get_paste_info( export function get_paste_info_obj(uuid: string, descriptor: PasteIndexEntry, env: Env) {
uuid: string,
descriptor: PasteIndexEntry,
env: Env,
use_html: boolean = true,
need_qr: boolean = false,
reply_json = false
): Promise<Response> {
const created = new Date(descriptor.last_modified); const created = new Date(descriptor.last_modified);
const expired = new Date(descriptor.expiration ?? descriptor.last_modified + 2419200000); const expired = new Date(descriptor.expiration ?? descriptor.last_modified + 2419200000);
const link = `https://${SERVICE_URL}/${uuid}`; const link = `https://${SERVICE_URL}/${uuid}`;
@ -49,6 +42,18 @@ export async function get_paste_info(
expired: expired.toISOString(), expired: expired.toISOString(),
update_completed: descriptor.upload_completed ?? undefined, // only for large_paste 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<Response> {
const paste_info = get_paste_info_obj(uuid, descriptor, env);
// Reply with JSON // Reply with JSON
if (reply_json) { if (reply_json) {
@ -63,7 +68,7 @@ export async function get_paste_info(
// Plain text reply // Plain text reply
let content = dedent` let content = dedent`
uuid: ${uuid} uuid: ${uuid}
link: ${link} link: ${paste_info.link}
type: ${paste_info.type ?? 'paste'} type: ${paste_info.type ?? 'paste'}
title: ${paste_info.title || '-'} title: ${paste_info.title || '-'}
mime-type: ${paste_info.mime_type ?? '-'} mime-type: ${paste_info.mime_type ?? '-'}
@ -95,7 +100,7 @@ export async function get_paste_info(
${ ${
need_qr need_qr
? `<img src="${paste_info.link_qr}" ? `<img src="${paste_info.link_qr}"
alt="${link}" style="max-width: 280px">` alt="${paste_info.link}" style="max-width: 280px">`
: '' : ''
} }
</body> </body>
@ -116,7 +121,7 @@ export async function get_paste_info(
const res = await env.QRCODE.fetch( const res = await env.QRCODE.fetch(
'https://qrcode.nekoid.cc?' + 'https://qrcode.nekoid.cc?' +
new URLSearchParams({ new URLSearchParams({
q: link, q: paste_info.link,
type: 'utf8', type: 'utf8',
}) })
); );

View file

@ -1,8 +1,9 @@
import { Router } from 'itty-router'; import { Router } from 'itty-router';
import { sha256 } from 'js-sha256'; import { sha256 } from 'js-sha256';
import { AwsClient } from 'aws4fetch'; import { AwsClient } from 'aws4fetch';
import { parseStringPromise } from 'xml2js';
import { ERequest, Env, PasteIndexEntry } from '../types'; 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'; import { UUID_LENGTH } from '../constant';
export const router = Router<ERequest, [Env, ExecutionContext]>({ base: '/v2/large_upload' }); export const router = Router<ERequest, [Env, ExecutionContext]>({ base: '/v2/large_upload' });
@ -195,24 +196,31 @@ router.post('/complete/:uuid', async (request, env, ctx) => {
}); });
try { try {
const objectmeta = await s3.fetch(`${env.LARGE_ENDPOINT}/${uuid}`, { // Get object attributes
method: 'HEAD', const objectmeta = await s3.fetch(`${env.LARGE_DOWNLOAD_ENDPOINT}/${uuid}?attributes`, {
method: 'GET',
headers: {
'X-AMZ-Object-Attributes': 'ObjectSize',
},
}); });
if (objectmeta.ok) { if (objectmeta.ok) {
const { headers } = objectmeta; const xml = await objectmeta.text();
const file_size = headers.get('Content-Length') || '0'; const parsed = await parseStringPromise(xml, {
tagNameProcessors: [(name) => name.toLowerCase()],
});
const file_size = parsed.getobjectattributesresponse.objectsize[0];
if (parseInt(file_size) !== descriptor.size) { if (parseInt(file_size) !== descriptor.size) {
return new Response('This paste is not finishing the upload.\n', { return new Response('This paste is not finishing the upload.\n', {
status: 400, status: 400,
}); });
} }
} else { } else {
return new Response('This paste is not finishing the upload.\n', { return new Response('This paste is not finishing upload.\n', {
status: 400, status: 400,
}); });
} }
} catch (err) { } catch (err) {
return new Response('Unable to connect to remote.\n', { return new Response('Internal server error.\n', {
status: 500, 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 })); ctx.waitUntil(env.PASTE_INDEX.put(uuid, JSON.stringify(descriptor), { expirationTtl: 2419200 }));
const paste_info = { const paste_info = {
uuid,
upload_completed: true, upload_completed: true,
expired: new Date(expriation).toISOString(), expired: new Date(expriation).toISOString(),
paste_info: get_paste_info_obj(uuid, descriptor, env),
}; };
return new Response(JSON.stringify(paste_info), { return new Response(JSON.stringify(paste_info));
status: 400,
});
}); });
router.get('/:uuid', async (request, env, ctx) => { router.get('/:uuid', async (request, env, ctx) => {

View file

@ -8,6 +8,7 @@ kv_namespaces = [
services = [ services = [
{ binding = "QRCODE", service = "qrcode-gen", environment = "production" } { binding = "QRCODE", service = "qrcode-gen", environment = "production" }
] ]
node_compat = true
# [secret] # [secret]
# AWS_ACCESS_KEY_ID # AWS_ACCESS_KEY_ID

View file

@ -302,6 +302,11 @@
dependencies: dependencies:
"@popperjs/core" "^2.9.2" "@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": "@types/jquery@^3.5.25":
version "3.5.29" version "3.5.29"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.29.tgz#3c06a1f519cd5fc3a7a108971436c00685b5dcea" 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" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.8.tgz#518609aefb797da19bf222feb199e8f653ff7627"
integrity sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg== 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": "@ungap/structured-clone@^1.2.0":
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" 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" get-intrinsic "^1.2.2"
is-regex "^1.1.4" 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: selfsigned@^2.0.1:
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" 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" resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4"
integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== 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: xxhash-wasm@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/xxhash-wasm/-/xxhash-wasm-1.0.2.tgz#ecc0f813219b727af4d5f3958ca6becee2f2f1ff" resolved "https://registry.yarnpkg.com/xxhash-wasm/-/xxhash-wasm-1.0.2.tgz#ecc0f813219b727af4d5f3958ca6becee2f2f1ff"