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

@ -27,9 +27,22 @@ import { get_presign_url, router as large_upload } from './v2/large_upload';
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
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);

3
src/types.d.ts vendored
View file

@ -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 {

View file

@ -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<Response> {
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<Response> {
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
? `<img src="${paste_info.link_qr}"
alt="${link}" style="max-width: 280px">`
alt="${paste_info.link}" style="max-width: 280px">`
: ''
}
</body>
@ -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',
})
);

View file

@ -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<ERequest, [Env, ExecutionContext]>({ 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) => {