mirror of
https://github.com/rikkaneko/paste.git
synced 2025-08-05 05:30:09 +01:00
Update itty-router to 5.x
Fix minor bug Update package dependencies Signed-off-by: Joe Ma <rikkaneko23@gmail.com>
This commit is contained in:
parent
1fb3076115
commit
f90633bce3
8 changed files with 127 additions and 88 deletions
|
@ -381,7 +381,7 @@ $(function () {
|
|||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://pb.nekoid.cc/${uuid}/settings?${new URLSearchParams({ json: '1' })}`);
|
||||
const res = await fetch(`${ENDPOINT}/${uuid}/settings?${new URLSearchParams({ json: '1' })}`);
|
||||
if (res.ok) {
|
||||
const paste_info = await res.json();
|
||||
build_paste_modal(paste_info, show_qrcode, false, true);
|
||||
|
|
30
package.json
30
package.json
|
@ -9,27 +9,27 @@
|
|||
"lint": "eslint . --color --cache -f friendly --max-warnings 10"
|
||||
},
|
||||
"dependencies": {
|
||||
"aws4fetch": "^1.0.17",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"buffer": "^6.0.3",
|
||||
"dedent-js": "^1.0.1",
|
||||
"itty-router": "^4.0.23",
|
||||
"js-sha256": "^0.10.1",
|
||||
"nanoid": "^5.0.2",
|
||||
"xml-js": "^1.6.11",
|
||||
"itty-router": "^5.0.18",
|
||||
"js-sha256": "^0.11.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"string_decoder": "^1.3.0",
|
||||
"buffer": "^6.0.3"
|
||||
"xml-js": "^1.6.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20231025.0",
|
||||
"@types/bootstrap": "^5.2.8",
|
||||
"@cloudflare/workers-types": "^4.20250725.0",
|
||||
"@types/bootstrap": "^5.2.10",
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/jquery": "^3.5.25",
|
||||
"@types/jquery": "^3.5.32",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"eslint": "^8.52.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-formatter-friendly": "^7.0.0",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"prettier": "^3.0.3",
|
||||
"typescript": "^5.2.2",
|
||||
"wrangler": "^3.15.0"
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.8.3",
|
||||
"wrangler": "^4.26.0"
|
||||
}
|
||||
}
|
|
@ -1,4 +1,15 @@
|
|||
export const SERVICE_URL = 'pb.nekoid.cc';
|
||||
export const PASTE_WEB_URL = 'https://raw.githubusercontent.com/rikkaneko/paste/main/frontend';
|
||||
export const UUID_LENGTH = 4;
|
||||
export const CORS_DOMAIN = 'nekoid.cc';
|
||||
import { Config, Env } from './types';
|
||||
|
||||
// @ts-ignore
|
||||
let CONSTANTS: Config = {
|
||||
UUID_LENGTH: 4,
|
||||
};
|
||||
export default CONSTANTS;
|
||||
|
||||
// Fetch variable from Env
|
||||
export const fetch_constant = (env: Env) => {
|
||||
CONSTANTS = {
|
||||
...env,
|
||||
...CONSTANTS,
|
||||
};
|
||||
};
|
||||
|
|
92
src/index.ts
92
src/index.ts
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* This file is part of paste.
|
||||
* Copyright (c) 2022-2024 Joe Ma <rikkaneko23@gmail.com>
|
||||
* Copyright (c) 2022-2025 Joe Ma <rikkaneko23@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
|
@ -18,14 +18,34 @@
|
|||
|
||||
import { AwsClient } from 'aws4fetch';
|
||||
import { sha256 } from 'js-sha256';
|
||||
import { Router, error } from 'itty-router';
|
||||
import { Router, error, cors } from 'itty-router';
|
||||
import { ERequest, Env, PasteIndexEntry } from './types';
|
||||
import { serve_static } from './proxy';
|
||||
import { check_password_rules, get_paste_info, get_basic_auth, gen_id } from './utils';
|
||||
import { UUID_LENGTH, PASTE_WEB_URL, SERVICE_URL, CORS_DOMAIN } from './constant';
|
||||
import constants, { fetch_constant } from './constant';
|
||||
import { get_presign_url, router as large_upload } from './v2/large_upload';
|
||||
|
||||
const router = Router<ERequest, [Env, ExecutionContext]>();
|
||||
// In favour of new cors() in itty-router v5
|
||||
const { preflight, corsify } = cors({
|
||||
origin: (o) => {
|
||||
if (constants?.CORS_DOMAIN) {
|
||||
return o?.endsWith(constants.CORS_DOMAIN) ? o : undefined;
|
||||
}
|
||||
},
|
||||
credentials: true,
|
||||
allowMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
|
||||
});
|
||||
|
||||
const router = Router<ERequest, [Env, ExecutionContext]>({
|
||||
before: [
|
||||
preflight,
|
||||
(_, env) => {
|
||||
fetch_constant(env);
|
||||
},
|
||||
],
|
||||
catch: error,
|
||||
finally: [corsify],
|
||||
});
|
||||
|
||||
// Shared common properties to all route
|
||||
router.all('*', (request) => {
|
||||
|
@ -39,27 +59,10 @@ router.all('*', (request) => {
|
|||
request.origin = headers.get('referer') ?? undefined;
|
||||
});
|
||||
|
||||
// Handle preflighted CORS request
|
||||
router.options('*', (request) => {
|
||||
if (!request.origin) return new Response(null);
|
||||
const url = new URL(request.origin);
|
||||
// Allow all subdomain of nekoid.cc
|
||||
if (url.hostname.endsWith(CORS_DOMAIN)) {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': url.origin,
|
||||
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
||||
Vary: 'Origin',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/* Static file path */
|
||||
// Web homepage
|
||||
router.get('/', (request) => {
|
||||
return serve_static(PASTE_WEB_URL + '/paste.html', request.headers);
|
||||
router.get('/', (request, env, ctx) => {
|
||||
return serve_static(env.PASTE_WEB_URL + '/paste.html', request.headers);
|
||||
});
|
||||
|
||||
// Favicon
|
||||
|
@ -73,11 +76,11 @@ router.get('/favicon.ico', () => {
|
|||
});
|
||||
|
||||
// Web script and style file
|
||||
router.get('/static/*', (request) => {
|
||||
router.get('/static/*', (request, env, ctx) => {
|
||||
const { url } = request;
|
||||
const { pathname } = new URL(url);
|
||||
const path = pathname.replace(/\/+$/, '') || '/';
|
||||
return serve_static(PASTE_WEB_URL + path, request.headers);
|
||||
return serve_static(env.PASTE_WEB_URL + path, request.headers);
|
||||
});
|
||||
|
||||
// Create new paste (10MB limit)
|
||||
|
@ -95,7 +98,7 @@ router.post('/', async (request, env, ctx) => {
|
|||
let need_qrcode: boolean = false;
|
||||
let paste_type: string | undefined;
|
||||
let reply_json: boolean = false;
|
||||
// Content-Type: multipart/form-data (deprecated)
|
||||
// Content-Type: multipart/form-data
|
||||
if (content_type.includes('multipart/form-data')) {
|
||||
const formdata = await request.formData();
|
||||
const data: File | string | any = formdata.get('u');
|
||||
|
@ -114,6 +117,7 @@ router.post('/', async (request, env, ctx) => {
|
|||
buffer = await data.arrayBuffer();
|
||||
// Text
|
||||
} else {
|
||||
// @ts-ignore
|
||||
buffer = new TextEncoder().encode(data);
|
||||
mime_type = 'text/plain; charset=UTF-8;';
|
||||
}
|
||||
|
@ -158,6 +162,7 @@ router.post('/', async (request, env, ctx) => {
|
|||
reply_json = true;
|
||||
}
|
||||
} else {
|
||||
// HTTP API
|
||||
title = headers.get('x-paste-title') || undefined;
|
||||
mime_type = headers.get('x-paste-content-type') || undefined;
|
||||
password = headers.get('x-paste-pass') || undefined;
|
||||
|
@ -222,6 +227,9 @@ router.post('/', async (request, env, ctx) => {
|
|||
body: buffer,
|
||||
});
|
||||
|
||||
// Default paste type
|
||||
paste_type = paste_type ? paste_type : 'paste';
|
||||
|
||||
if (paste_type === 'link') {
|
||||
mime_type = 'text/x-uri';
|
||||
}
|
||||
|
@ -256,14 +264,14 @@ router.post('/', async (request, env, ctx) => {
|
|||
});
|
||||
|
||||
// Handle large upload (> 25MB)
|
||||
router.all('/v2/large_upload/*', large_upload.handle);
|
||||
router.all('/v2/large_upload/*', large_upload.fetch);
|
||||
|
||||
// Fetch paste by uuid [4-digit UUID]
|
||||
router.get('/:uuid/:option?', async (request, env, ctx) => {
|
||||
const { headers } = request;
|
||||
const { uuid, option } = request.params;
|
||||
// UUID format: [A-z0-9]{UUID_LENGTH}
|
||||
if (uuid.length !== UUID_LENGTH) {
|
||||
if (uuid.length !== constants.UUID_LENGTH) {
|
||||
return new Response('Invalid UUID.\n', {
|
||||
status: 442,
|
||||
});
|
||||
|
@ -343,7 +351,12 @@ router.get('/:uuid/:option?', async (request, env, ctx) => {
|
|||
}
|
||||
|
||||
if (descriptor.size >= 209715200) {
|
||||
const signed_url = await get_presign_url(uuid, descriptor, env);
|
||||
const signed_url = await get_presign_url(uuid, descriptor);
|
||||
if (signed_url == null) {
|
||||
return new Response('No available download endpoint.\n', {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
ctx.waitUntil(
|
||||
env.PASTE_INDEX.put(uuid, JSON.stringify(descriptor), {
|
||||
|
@ -366,7 +379,7 @@ router.get('/:uuid/:option?', async (request, env, ctx) => {
|
|||
const cache = caches.default;
|
||||
const match_etag = headers.get('If-None-Match') || undefined;
|
||||
// Define the Request object as cache key
|
||||
const req_key = new Request(`https://${SERVICE_URL}/${uuid}`, {
|
||||
const req_key = new Request(`https://${env.SERVICE_URL}/${uuid}`, {
|
||||
method: 'GET',
|
||||
headers: match_etag
|
||||
? {
|
||||
|
@ -493,7 +506,7 @@ router.delete('/:uuid', async (request, env, ctx) => {
|
|||
const { headers } = request;
|
||||
const { uuid } = request.params;
|
||||
// UUID format: [A-z0-9]{UUID_LENGTH}
|
||||
if (uuid.length !== UUID_LENGTH) {
|
||||
if (uuid.length !== constants.UUID_LENGTH) {
|
||||
return new Response('Invalid UUID.\n', {
|
||||
status: 442,
|
||||
});
|
||||
|
@ -557,7 +570,7 @@ router.delete('/:uuid', async (request, env, ctx) => {
|
|||
if (res.ok) {
|
||||
ctx.waitUntil(env.PASTE_INDEX.delete(uuid));
|
||||
// Invalidate CF cache
|
||||
ctx.waitUntil(cache.delete(new Request(`https://${SERVICE_URL}/${uuid}`)));
|
||||
ctx.waitUntil(cache.delete(new Request(`https://${env.SERVICE_URL}/${uuid}`)));
|
||||
return new Response('OK\n');
|
||||
} else {
|
||||
return new Response('Unable to process such request.\n', {
|
||||
|
@ -576,17 +589,6 @@ router.all('*', () => {
|
|||
export default {
|
||||
fetch: (req: ERequest, env: Env, ctx: ExecutionContext) =>
|
||||
router
|
||||
.handle(req, env, ctx)
|
||||
.catch(error)
|
||||
// Apply CORS headers
|
||||
.then((res: Response) => {
|
||||
if (!req.origin) return res;
|
||||
const url = new URL(req.origin);
|
||||
// Allow all subdomain of nekoid.cc
|
||||
if (url.hostname.endsWith(CORS_DOMAIN)) {
|
||||
res.headers.set('Access-Control-Allow-Origin', url.origin);
|
||||
res.headers.set('Vary', 'Origin');
|
||||
}
|
||||
return res;
|
||||
}),
|
||||
// Update with itty-router 5.x
|
||||
.fetch(req, env, ctx),
|
||||
};
|
||||
|
|
10
src/types.d.ts
vendored
10
src/types.d.ts
vendored
|
@ -25,6 +25,12 @@ export interface PasteIndexEntry {
|
|||
}
|
||||
|
||||
export interface Env {
|
||||
// Variable
|
||||
SERVICE_URL: string;
|
||||
PASTE_WEB_URL?: string;
|
||||
UUID_LENGTH: string;
|
||||
CORS_DOMAIN?: string;
|
||||
// Secret
|
||||
PASTE_INDEX: KVNamespace;
|
||||
QRCODE: ServiceWorkerGlobalScope;
|
||||
AWS_ACCESS_KEY_ID: string;
|
||||
|
@ -35,3 +41,7 @@ export interface Env {
|
|||
LARGE_ENDPOINT?: string;
|
||||
LARGE_DOWNLOAD_ENDPOINT?: string;
|
||||
}
|
||||
|
||||
export interface Config extends Env {
|
||||
UUID_LENGTH: number;
|
||||
}
|
13
src/utils.ts
13
src/utils.ts
|
@ -18,15 +18,18 @@
|
|||
|
||||
import dedent from 'dedent-js';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { SERVICE_URL, UUID_LENGTH } from './constant';
|
||||
import constants from './constant';
|
||||
import { PasteIndexEntry, Env } from './types';
|
||||
|
||||
export const gen_id = customAlphabet('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', UUID_LENGTH);
|
||||
export const gen_id = customAlphabet(
|
||||
'1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
||||
constants.UUID_LENGTH
|
||||
);
|
||||
|
||||
export function get_paste_info_obj(uuid: string, descriptor: PasteIndexEntry) {
|
||||
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}`;
|
||||
const link = `https://${env.SERVICE_URL}/${uuid}`;
|
||||
const paste_info = {
|
||||
uuid,
|
||||
link,
|
||||
|
@ -53,7 +56,7 @@ export async function get_paste_info(
|
|||
need_qr: boolean = false,
|
||||
reply_json = false
|
||||
): Promise<Response> {
|
||||
const paste_info = get_paste_info_obj(uuid, descriptor);
|
||||
const paste_info = get_paste_info_obj(uuid, descriptor, env);
|
||||
|
||||
// Reply with JSON
|
||||
if (reply_json) {
|
||||
|
|
|
@ -4,11 +4,11 @@ import { AwsClient } from 'aws4fetch';
|
|||
import { xml2js } from 'xml-js';
|
||||
import { ERequest, Env, PasteIndexEntry } from '../types';
|
||||
import { gen_id, get_paste_info_obj } from '../utils';
|
||||
import { UUID_LENGTH } from '../constant';
|
||||
import constants from '../constant';
|
||||
|
||||
export const router = Router<ERequest, [Env, ExecutionContext]>({ base: '/v2/large_upload' });
|
||||
|
||||
export async function get_presign_url(uuid: string, descriptor: PasteIndexEntry, env: Env) {
|
||||
export async function get_presign_url(uuid: string, descriptor: PasteIndexEntry) {
|
||||
// Use cached presigned url if expiration is more than 10 mins
|
||||
if (descriptor.cached_presigned_url) {
|
||||
const expiration = new Date(descriptor.cached_presigned_url_expiration ?? 0);
|
||||
|
@ -18,22 +18,28 @@ export async function get_presign_url(uuid: string, descriptor: PasteIndexEntry,
|
|||
}
|
||||
}
|
||||
|
||||
const endpoint_url = new URL(`${env.LARGE_DOWNLOAD_ENDPOINT}/${uuid}`);
|
||||
endpoint_url.searchParams.set('X-Amz-Expires', '14400'); // Valid for 4 hours
|
||||
endpoint_url.searchParams.set(
|
||||
const download_url = constants.LARGE_DOWNLOAD_ENDPOINT ?? constants.LARGE_ENDPOINT;
|
||||
if (download_url == null) {
|
||||
// Not method to download
|
||||
return null;
|
||||
}
|
||||
|
||||
const download_path = new URL(`${download_url}/${uuid}`);
|
||||
download_path.searchParams.set('X-Amz-Expires', '14400'); // Valid for 4 hours
|
||||
download_path.searchParams.set(
|
||||
'response-content-disposition',
|
||||
`inline; filename*=UTF-8''${encodeURIComponent(descriptor.title ?? uuid)}`
|
||||
);
|
||||
endpoint_url.searchParams.set('response-content-type', descriptor.mime_type ?? 'text/plain; charset=UTF-8;');
|
||||
download_path.searchParams.set('response-content-type', descriptor.mime_type ?? 'text/plain; charset=UTF-8;');
|
||||
|
||||
// Generate Presigned Request
|
||||
const s3 = new AwsClient({
|
||||
accessKeyId: env.LARGE_AWS_ACCESS_KEY_ID!,
|
||||
secretAccessKey: env.LARGE_AWS_SECRET_ACCESS_KEY!,
|
||||
accessKeyId: constants.LARGE_AWS_ACCESS_KEY_ID!,
|
||||
secretAccessKey: constants.LARGE_AWS_SECRET_ACCESS_KEY!,
|
||||
service: 's3', // required
|
||||
});
|
||||
|
||||
const signed = await s3.sign(endpoint_url, {
|
||||
const signed = await s3.sign(download_path, {
|
||||
method: 'GET',
|
||||
headers: {},
|
||||
aws: {
|
||||
|
@ -65,6 +71,8 @@ router.post('/create', async (request, env, ctx) => {
|
|||
let read_limit: number | undefined;
|
||||
let file_size: number | undefined;
|
||||
let file_hash: string | undefined;
|
||||
|
||||
// Content-Type: multipart/form-data
|
||||
if (content_type?.includes('multipart/form-data')) {
|
||||
const formdata = await request.formData();
|
||||
const title = formdata.get('title');
|
||||
|
@ -128,15 +136,15 @@ router.post('/create', async (request, env, ctx) => {
|
|||
|
||||
const current = Date.now();
|
||||
const expiration = new Date(current + 14400 * 1000).getTime();
|
||||
const endpoint_url = new URL(`${env.LARGE_ENDPOINT}/${uuid}`);
|
||||
endpoint_url.searchParams.set('X-Amz-Expires', '900'); // Valid for 15 mins
|
||||
const upload_path = new URL(`${env.LARGE_ENDPOINT}/${uuid}`);
|
||||
upload_path.searchParams.set('X-Amz-Expires', '900'); // Valid for 15 mins
|
||||
const required_headers = {
|
||||
'Content-Length': file_size.toString(),
|
||||
'X-Amz-Content-Sha256': file_hash,
|
||||
};
|
||||
|
||||
// Generate Presigned Request
|
||||
const signed = await s3.sign(endpoint_url, {
|
||||
const signed = await s3.sign(upload_path, {
|
||||
method: 'PUT',
|
||||
headers: required_headers,
|
||||
aws: {
|
||||
|
@ -180,7 +188,7 @@ router.post('/complete/:uuid', async (request, env, ctx) => {
|
|||
const { headers } = request;
|
||||
const { uuid } = request.params;
|
||||
// UUID format: [A-z0-9]{UUID_LENGTH}
|
||||
if (uuid.length !== UUID_LENGTH) {
|
||||
if (uuid.length !== constants.UUID_LENGTH) {
|
||||
return new Response('Invalid UUID.\n', {
|
||||
status: 442,
|
||||
});
|
||||
|
@ -208,7 +216,7 @@ router.post('/complete/:uuid', async (request, env, ctx) => {
|
|||
|
||||
try {
|
||||
// Get object attributes
|
||||
const objectmeta = await s3.fetch(`${env.LARGE_DOWNLOAD_ENDPOINT}/${uuid}?attributes`, {
|
||||
const objectmeta = await s3.fetch(`${env.LARGE_ENDPOINT}/${uuid}?attributes`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-AMZ-Object-Attributes': 'ObjectSize',
|
||||
|
@ -224,7 +232,7 @@ router.post('/complete/:uuid', async (request, env, ctx) => {
|
|||
});
|
||||
const file_size: number = parsed.getobjectattributesresponse.objectsize._text;
|
||||
if (file_size !== descriptor.size) {
|
||||
return new Response('This paste is not finishing the upload.\n', {
|
||||
return new Response(`This paste is not finishing upload. (${file_size} != ${descriptor.size})\n`, {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
@ -249,7 +257,7 @@ router.post('/complete/:uuid', async (request, env, ctx) => {
|
|||
const paste_info = {
|
||||
upload_completed: true,
|
||||
expired: new Date(expriation).toISOString(),
|
||||
paste_info: get_paste_info_obj(uuid, descriptor),
|
||||
paste_info: get_paste_info_obj(uuid, descriptor, env),
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify(paste_info));
|
||||
|
@ -258,7 +266,7 @@ router.post('/complete/:uuid', async (request, env, ctx) => {
|
|||
router.get('/:uuid', async (request, env, ctx) => {
|
||||
const { uuid } = request.params;
|
||||
// UUID format: [A-z0-9]{UUID_LENGTH}
|
||||
if (uuid.length !== UUID_LENGTH) {
|
||||
if (uuid.length !== constants.UUID_LENGTH) {
|
||||
return new Response('Invalid UUID.\n', {
|
||||
status: 442,
|
||||
});
|
||||
|
@ -284,7 +292,7 @@ router.get('/:uuid', async (request, env, ctx) => {
|
|||
});
|
||||
}
|
||||
|
||||
const signed_url = await get_presign_url(uuid, descriptor, env);
|
||||
const signed_url = await get_presign_url(uuid, descriptor);
|
||||
const result = {
|
||||
uuid,
|
||||
expire: new Date(descriptor.expiration || 0).toISOString(),
|
||||
|
|
|
@ -9,6 +9,11 @@ services = [
|
|||
{ binding = "QRCODE", service = "qrcode-gen", environment = "production" }
|
||||
]
|
||||
|
||||
[vars]
|
||||
SERVICE_URL = "pb.nekoid.cc"
|
||||
PASTE_WEB_URL = "https://raw.githubusercontent.com/rikkaneko/paste/main/frontend"
|
||||
CORS_DOMAIN = "nekoid.cc"
|
||||
|
||||
# [secret]
|
||||
# AWS_ACCESS_KEY_ID
|
||||
# AWS_SECRET_ACCESS_KEY
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue