mirror of
https://github.com/rikkaneko/paste.git
synced 2025-08-08 23:15:34 +01:00
Refactor the whole project
Use itty-router over manual routing Extract resuable code into modules Update copyright notice Remove path to paste v1 homepage Signed-off-by: Joe Ma <rikkaneko23@gmail.com>
This commit is contained in:
parent
9c336e4961
commit
3de4fa14ed
7 changed files with 757 additions and 696 deletions
3
src/constant.ts
Normal file
3
src/constant.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const SERVICE_URL = 'pb.nekoid.cc';
|
||||
export const PASTE_WEB_URL = 'https://raw.githubusercontent.com/rikkaneko/paste/main/static/v2';
|
||||
export const UUID_LENGTH = 4;
|
1045
src/index.ts
1045
src/index.ts
File diff suppressed because it is too large
Load diff
18
src/proxy.ts
18
src/proxy.ts
|
@ -1,3 +1,21 @@
|
|||
/*
|
||||
* This file is part of paste.
|
||||
* Copyright (c) 2022-2024 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Proxy URI (Accept *.js, *.css, *.html, *.ico only)
|
||||
// Use ETag and If-None-Match to cache file
|
||||
export async function serve_static(path: string, req_headers?: Headers): Promise<Response> {
|
||||
|
|
7
src/types.d.ts
vendored
7
src/types.d.ts
vendored
|
@ -1,3 +1,10 @@
|
|||
import { IRequest } from 'itty-router';
|
||||
|
||||
export type ERequest = {
|
||||
is_browser: boolean;
|
||||
// match_etag?: string;
|
||||
} & IRequest;
|
||||
|
||||
export interface PasteIndexEntry {
|
||||
title?: string;
|
||||
mime_type?: string;
|
||||
|
|
168
src/utils.ts
Normal file
168
src/utils.ts
Normal file
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* This file is part of paste.
|
||||
* Copyright (c) 2022-2024 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import dedent from 'dedent-js';
|
||||
import { SERVICE_URL } from './constant';
|
||||
import { PasteIndexEntry, Env } from './types';
|
||||
|
||||
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 created = new Date(descriptor.last_modified);
|
||||
const expired = new Date(descriptor.last_modified + 2419200000);
|
||||
const link = `https://${SERVICE_URL}/${uuid}`;
|
||||
const paste_info = {
|
||||
uuid,
|
||||
link,
|
||||
link_qr: 'https://qrcode.nekoid.cc/?' + new URLSearchParams({ q: link, type: 'svg' }),
|
||||
type: descriptor.type ?? 'paste',
|
||||
title: descriptor.title?.trim(),
|
||||
mime_type: descriptor.mime_type,
|
||||
human_readable_size: `${to_human_readable_size(descriptor.size)}`,
|
||||
size: descriptor.size,
|
||||
password: !!descriptor.password,
|
||||
read_count_remain: descriptor.read_count_remain,
|
||||
created: created.toISOString(),
|
||||
expired: expired.toISOString(),
|
||||
};
|
||||
|
||||
// Reply with JSON
|
||||
if (reply_json) {
|
||||
return new Response(JSON.stringify(paste_info), {
|
||||
headers: {
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'cache-control': 'no-store',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Plain text reply
|
||||
let content = dedent`
|
||||
uuid: ${uuid}
|
||||
link: ${link}
|
||||
type: ${paste_info.type ?? 'paste'}
|
||||
title: ${paste_info.title || '-'}
|
||||
mime-type: ${paste_info.mime_type ?? '-'}
|
||||
size: ${paste_info.size} bytes (${paste_info.human_readable_size})
|
||||
password: ${paste_info.password}
|
||||
remaining read count: ${
|
||||
paste_info.read_count_remain !== undefined
|
||||
? paste_info.read_count_remain
|
||||
? paste_info.read_count_remain
|
||||
: `0 (expired)`
|
||||
: '-'
|
||||
}
|
||||
created at ${paste_info.created}
|
||||
expired at ${paste_info.expired}
|
||||
`;
|
||||
|
||||
// Browser response
|
||||
if (use_html) {
|
||||
const html = dedent`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Paste</title>
|
||||
</head>
|
||||
<body>
|
||||
<pre style="word-wrap: break-word; white-space: pre-wrap;
|
||||
font-family: 'Fira Mono', monospace; font-size: 16px;">${content}</pre>
|
||||
${
|
||||
need_qr
|
||||
? `<img src="${paste_info.link_qr}"
|
||||
alt="${link}" style="max-width: 280px">`
|
||||
: ''
|
||||
}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
return new Response(html, {
|
||||
headers: {
|
||||
'content-type': 'text/html; charset=UTF-8;',
|
||||
'cache-control': 'no-store',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Console response
|
||||
if (need_qr) {
|
||||
// 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({
|
||||
q: link,
|
||||
type: 'utf8',
|
||||
})
|
||||
);
|
||||
|
||||
if (res.ok) {
|
||||
const qrcode = await res.text();
|
||||
content += '\n';
|
||||
content += qrcode;
|
||||
}
|
||||
}
|
||||
|
||||
content += '\n';
|
||||
return new Response(content, {
|
||||
headers: {
|
||||
'cache-control': 'no-store',
|
||||
},
|
||||
});
|
||||
}
|
||||
export function check_password_rules(password: string): boolean {
|
||||
return password.match('^[A-z0-9]{4,}$') !== null;
|
||||
}
|
||||
// Extract username and password from Basic Authorization header
|
||||
export function get_basic_auth(headers: Headers): [string, string] | null {
|
||||
if (headers.has('Authorization')) {
|
||||
const auth = headers.get('Authorization');
|
||||
const [scheme, encoded] = auth!.split(' ');
|
||||
// Validate authorization header format
|
||||
if (!encoded || scheme !== 'Basic') {
|
||||
return null;
|
||||
}
|
||||
// Decode base64 to string (UTF-8)
|
||||
const buffer = Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0));
|
||||
const decoded = new TextDecoder().decode(buffer).normalize();
|
||||
const index = decoded.indexOf(':');
|
||||
|
||||
// Check if user & password are split by the first colon and MUST NOT contain control characters.
|
||||
if (index === -1 || decoded.match('[\\0-\x1F\x7F]')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [decoded.slice(0, index), decoded.slice(index + 1)];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function to_human_readable_size(bytes: number): string {
|
||||
let size = bytes + ' bytes';
|
||||
const units = ['KiB', 'MiB', 'GiB', 'TiB'];
|
||||
for (let i = 0, approx = bytes / 1024; approx > 1; approx /= 1024, i++) {
|
||||
size = approx.toFixed(3) + ' ' + units[i];
|
||||
}
|
||||
return size;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue