Format Code

Signed-off-by: Joe Ma <rikkaneko23@gmail.com>
This commit is contained in:
Joe Ma 2023-11-02 17:22:10 +08:00
parent 2ea3b0dd8d
commit c4b1f06177
No known key found for this signature in database
GPG key ID: 7A0ECF5F5EDC587F
7 changed files with 80 additions and 63 deletions

View file

@ -16,15 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {AwsClient} from 'aws4fetch'; import { AwsClient } from 'aws4fetch';
import {customAlphabet} from 'nanoid'; import { customAlphabet } from 'nanoid';
import {sha256} from 'js-sha256'; import { sha256 } from 'js-sha256';
import dedent from 'dedent-js'; import dedent from 'dedent-js';
import { PasteIndexEntry } from './types';
// Constants // Constants
const SERVICE_URL = 'pb.nekoid.cc'; const SERVICE_URL = 'pb.nekoid.cc';
const PASTE_WEB_URL_v1 = 'https://raw.githubusercontent.com/rikkaneko/paste/main/web/v1'; const PASTE_WEB_URL_v1 = 'https://raw.githubusercontent.com/rikkaneko/paste/main/static/v1';
const PASTE_WEB_URL = 'https://raw.githubusercontent.com/rikkaneko/paste/main/web/v2'; const PASTE_WEB_URL = 'https://raw.githubusercontent.com/rikkaneko/paste/main/static/v2';
const UUID_LENGTH = 4; const UUID_LENGTH = 4;
export interface Env { export interface Env {
@ -35,23 +36,20 @@ export interface Env {
ENDPOINT: string; ENDPOINT: string;
} }
const gen_id = customAlphabet( const gen_id = customAlphabet('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', UUID_LENGTH);
'1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', UUID_LENGTH);
export default { export default {
async fetch( async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
request: Request, const { url, method, headers } = request;
env: Env, const { pathname, searchParams } = new URL(url);
ctx: ExecutionContext,
): Promise<Response> {
const {url, method, headers} = request;
const {pathname, searchParams} = new URL(url);
const path = pathname.replace(/\/+$/, '') || '/'; const path = pathname.replace(/\/+$/, '') || '/';
let cache = caches.default; let cache = caches.default;
const agent = headers.get('user-agent') ?? ''; const agent = headers.get('user-agent') ?? '';
// Detect if request from browsers // Detect if request from browsers
const is_browser = ['Chrome', 'Mozilla', 'AppleWebKit', 'Safari', 'Gecko', 'Chromium'].some(v => agent.includes(v)); const is_browser = ['Chrome', 'Mozilla', 'AppleWebKit', 'Safari', 'Gecko', 'Chromium'].some((v) =>
agent.includes(v)
);
const s3 = new AwsClient({ const s3 = new AwsClient({
accessKeyId: env.AWS_ACCESS_KEY_ID, accessKeyId: env.AWS_ACCESS_KEY_ID,
@ -200,10 +198,12 @@ export default {
// Check password rules // Check password rules
if (password && !check_password_rules(password)) { if (password && !check_password_rules(password)) {
return new Response('Invalid password. ' + return new Response(
'Password must contain alphabets and digits only, and has a length of 4 or more.', { 'Invalid password. ' + 'Password must contain alphabets and digits only, and has a length of 4 or more.',
status: 422, {
}); status: 422,
}
);
} }
// Check request.body size <= 25MB // Check request.body size <= 25MB
@ -239,16 +239,14 @@ export default {
}; };
// Key will be expired after 28 day if unmodified // Key will be expired after 28 day if unmodified
ctx.waitUntil(env.PASTE_INDEX.put(uuid, JSON.stringify(descriptor), {expirationTtl: 2419200})); ctx.waitUntil(env.PASTE_INDEX.put(uuid, JSON.stringify(descriptor), { expirationTtl: 2419200 }));
return await get_paste_info(uuid, descriptor, env, is_browser, need_qrcode, reply_json); return await get_paste_info(uuid, descriptor, env, is_browser, need_qrcode, reply_json);
} else { } else {
return new Response('Unable to upload the paste.\n', { return new Response('Unable to upload the paste.\n', {
status: 500, status: 500,
}); });
} }
} }
} else if (path.length >= UUID_LENGTH + 1) { } else if (path.length >= UUID_LENGTH + 1) {
// RegExpr to match /<uuid>/<option> // RegExpr to match /<uuid>/<option>
const found = path.match('/(?<uuid>[A-z0-9]+)(?:/(?<option>[A-z]+))?$'); const found = path.match('/(?<uuid>[A-z0-9]+)(?:/(?<option>[A-z]+))?$');
@ -258,7 +256,7 @@ export default {
}); });
} }
// @ts-ignore // @ts-ignore
const {uuid, option} = found.groups; const { uuid, option } = found.groups;
// UUID format: [A-z0-9]{UUID_LENGTH} // UUID format: [A-z0-9]{UUID_LENGTH}
if (uuid.length !== UUID_LENGTH) { if (uuid.length !== UUID_LENGTH) {
return new Response('Invalid UUID.\n', { return new Response('Invalid UUID.\n', {
@ -289,11 +287,10 @@ export default {
}); });
} }
} }
} }
switch (method) { switch (method) {
// Fetch the paste by uuid // Fetch the paste by uuid
case 'GET': { case 'GET': {
// Check password if needed // Check password if needed
if (descriptor.password !== undefined) { if (descriptor.password !== undefined) {
@ -337,9 +334,11 @@ export default {
}); });
} }
descriptor.read_count_remain--; descriptor.read_count_remain--;
ctx.waitUntil(env.PASTE_INDEX.put(uuid, JSON.stringify(descriptor), { ctx.waitUntil(
expiration: descriptor.last_modified / 1000 + 2419200, env.PASTE_INDEX.put(uuid, JSON.stringify(descriptor), {
})); expiration: descriptor.last_modified / 1000 + 2419200,
})
);
} }
// Enable CF cache for authorized request // Enable CF cache for authorized request
@ -370,20 +369,22 @@ export default {
} }
res.headers.set('cache-control', 'public, max-age=18000'); res.headers.set('cache-control', 'public, max-age=18000');
res.headers.set('content-disposition', res.headers.set(
`inline; filename="${encodeURIComponent(descriptor.title ?? uuid)}"`); 'content-disposition',
`inline; filename="${encodeURIComponent(descriptor.title ?? uuid)}"`
);
if (descriptor.mime_type) if (descriptor.mime_type) res.headers.set('content-type', descriptor.mime_type);
res.headers.set('content-type', descriptor.mime_type);
// Let the browser guess the content // Let the browser guess the content
else res.headers.delete('content-type'); else res.headers.delete('content-type');
// Handle option // Handle option
if (option === 'raw') res.headers.delete('content-type'); if (option === 'raw') res.headers.delete('content-type');
else if (option === 'download') else if (option === 'download')
res.headers.set('content-disposition', res.headers.set(
`attachment; filename="${encodeURIComponent(descriptor.title ?? uuid)}"`); 'content-disposition',
`attachment; filename="${encodeURIComponent(descriptor.title ?? uuid)}"`
);
// Link redirection // Link redirection
else if (descriptor.type === 'link' || option === 'link') { else if (descriptor.type === 'link' || option === 'link') {
const content = await res.clone().arrayBuffer(); const content = await res.clone().arrayBuffer();
@ -414,12 +415,12 @@ export default {
} }
// Cache hit // Cache hit
let {readable, writable} = new TransformStream(); let { readable, writable } = new TransformStream();
res.body!.pipeTo(writable); res.body!.pipeTo(writable);
return new Response(readable, res); return new Response(readable, res);
} }
// Delete paste by uuid // Delete paste by uuid
case 'DELETE': { case 'DELETE': {
if (descriptor.editable !== undefined && !descriptor.editable) { if (descriptor.editable !== undefined && !descriptor.editable) {
return new Response('This paste is immutable.\n', { return new Response('This paste is immutable.\n', {
@ -468,15 +469,21 @@ export default {
}, },
}; };
async function get_paste_info(uuid: string, descriptor: PasteIndexEntry, env: Env, async function get_paste_info(
use_html: boolean = true, need_qr: boolean = false, reply_json = false): Promise<Response> { 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.last_modified + 2419200000); const expired = new Date(descriptor.last_modified + 2419200000);
const link = `https://${SERVICE_URL}/${uuid}`; const link = `https://${SERVICE_URL}/${uuid}`;
const paste_info = { const paste_info = {
uuid, uuid,
link, link,
link_qr: 'https://qrcode.nekoid.cc/?' + new URLSearchParams({q: link, type: 'svg'}), link_qr: 'https://qrcode.nekoid.cc/?' + new URLSearchParams({ q: link, type: 'svg' }),
type: descriptor.type ?? 'paste', type: descriptor.type ?? 'paste',
title: descriptor.title?.trim(), title: descriptor.title?.trim(),
mime_type: descriptor.mime_type, mime_type: descriptor.mime_type,
@ -507,8 +514,13 @@ async function get_paste_info(uuid: string, descriptor: PasteIndexEntry, env: En
mime-type: ${paste_info.mime_type ?? '-'} mime-type: ${paste_info.mime_type ?? '-'}
size: ${paste_info.size} bytes (${paste_info.human_readable_size}) size: ${paste_info.size} bytes (${paste_info.human_readable_size})
password: ${paste_info.password} password: ${paste_info.password}
remaining read count: ${paste_info.read_count_remain !== undefined ? remaining read count: ${
paste_info.read_count_remain ? paste_info.read_count_remain : `0 (expired)` : '-'} paste_info.read_count_remain !== undefined
? paste_info.read_count_remain
? paste_info.read_count_remain
: `0 (expired)`
: '-'
}
created at ${paste_info.created} created at ${paste_info.created}
expired at ${paste_info.expired} expired at ${paste_info.expired}
`; `;
@ -525,8 +537,12 @@ async function get_paste_info(uuid: string, descriptor: PasteIndexEntry, env: En
<body> <body>
<pre style="word-wrap: break-word; white-space: pre-wrap; <pre style="word-wrap: break-word; white-space: pre-wrap;
font-family: 'Fira Mono', monospace; font-size: 16px;">${content}</pre> font-family: 'Fira Mono', monospace; font-size: 16px;">${content}</pre>
${(need_qr) ? `<img src="${paste_info.link_qr}" ${
alt="${link}" style="max-width: 280px">` : ''} need_qr
? `<img src="${paste_info.link_qr}"
alt="${link}" style="max-width: 280px">`
: ''
}
</body> </body>
</html> </html>
`; `;
@ -542,10 +558,13 @@ async function get_paste_info(uuid: string, descriptor: PasteIndexEntry, env: En
// Console response // Console response
if (need_qr) { if (need_qr) {
// Cloudflare currently does not support doing a subrequest to the same zone, use service binding instead // Cloudflare currently does not support doing a subrequest to the same zone, use service binding instead
const res = await env.QRCODE.fetch('https://qrcode.nekoid.cc?' + new URLSearchParams({ const res = await env.QRCODE.fetch(
q: link, 'https://qrcode.nekoid.cc?' +
type: 'utf8', new URLSearchParams({
})); q: link,
type: 'utf8',
})
);
if (res.ok) { if (res.ok) {
const qrcode = await res.text(); const qrcode = await res.text();
@ -576,7 +595,7 @@ function get_basic_auth(headers: Headers): [string, string] | null {
return null; return null;
} }
// Decode base64 to string (UTF-8) // Decode base64 to string (UTF-8)
const buffer = Uint8Array.from(atob(encoded), character => character.charCodeAt(0)); const buffer = Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0));
const decoded = new TextDecoder().decode(buffer).normalize(); const decoded = new TextDecoder().decode(buffer).normalize();
const index = decoded.indexOf(':'); const index = decoded.indexOf(':');
@ -586,7 +605,6 @@ function get_basic_auth(headers: Headers): [string, string] | null {
} }
return [decoded.slice(0, index), decoded.slice(index + 1)]; return [decoded.slice(0, index), decoded.slice(index + 1)];
} else { } else {
return null; return null;
} }
@ -602,7 +620,7 @@ function to_human_readable_size(bytes: number): string {
} }
// Proxy URI (limit to html/js/css) // Proxy URI (limit to html/js/css)
async function proxy_uri(path: string, cf: RequestInitCfProperties = {cacheEverything: true}) { async function proxy_uri(path: string, cf: RequestInitCfProperties = { cacheEverything: true }) {
// Fix content type // Fix content type
let file_type = 'text/plain'; let file_type = 'text/plain';
if (path.endsWith('.js')) file_type = 'application/javascript'; if (path.endsWith('.js')) file_type = 'application/javascript';
@ -611,7 +629,7 @@ async function proxy_uri(path: string, cf: RequestInitCfProperties = {cacheEvery
return await fetch(path, { return await fetch(path, {
cf, cf,
}).then(value => { }).then((value) => {
return new Response(value.body, { return new Response(value.body, {
// Add the correct content-type to response header // Add the correct content-type to response header
headers: { headers: {
@ -621,14 +639,3 @@ async function proxy_uri(path: string, cf: RequestInitCfProperties = {cacheEvery
}); });
}); });
} }
interface PasteIndexEntry {
title?: string,
mime_type?: string,
last_modified: number,
size: number,
password?: string,
editable?: boolean, // Default: False (unsupported)
read_count_remain?: number
type?: string;
}

10
src/types.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
export interface PasteIndexEntry {
title?: string;
mime_type?: string;
last_modified: number;
size: number;
password?: string;
editable?: boolean; // Default: False (unsupported)
read_count_remain?: number;
type?: string;
}