mirror of
https://github.com/rikkaneko/paste.git
synced 2025-06-06 16:45:41 +00:00
Format Code
Signed-off-by: Joe Ma <rikkaneko23@gmail.com>
This commit is contained in:
parent
2ea3b0dd8d
commit
c4b1f06177
7 changed files with 80 additions and 63 deletions
133
src/index.ts
133
src/index.ts
|
@ -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
10
src/types.d.ts
vendored
Normal 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;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue