diff --git a/frontend/paste.html b/frontend/paste.html
index a785860..a3cb9f1 100644
--- a/frontend/paste.html
+++ b/frontend/paste.html
@@ -243,7 +243,7 @@
Access Times |
- - / 99999 |
+ - / |
Created |
diff --git a/frontend/static/paste.js b/frontend/static/paste.js
index 3090dd4..b3819e8 100644
--- a/frontend/static/paste.js
+++ b/frontend/static/paste.js
@@ -109,9 +109,17 @@ function build_paste_modal(paste_info, show_qrcode = true, saved = true, one_tim
Object.entries(paste_info).forEach(([key, val]) => {
if (key.includes('link')) return;
- $(`#paste_info_${key}`).text(val ?? '-');
+ $(`#paste_info_${key}`).text(val ?? '');
});
+ if (paste_info.max_access_n !== undefined) {
+ $('#paste_info_access_separator').show();
+ $('#paste_info_max_access_n').show();
+ } else {
+ $('#paste_info_access_separator').hide();
+ $('#paste_info_max_access_n').hide();
+ }
+
let modal = new bootstrap.Modal(paste_modal.modal);
if (!build_only) modal.show();
paste_modal.forget_btn.prop('disabled', one_time_only);
diff --git a/src/index.ts b/src/index.ts
index b05ef72..fcc79a4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -21,7 +21,7 @@ import { sha256 } from 'js-sha256';
import { Router, error, cors } from 'itty-router';
import { ERequest, Env } from './types';
import { serve_static } from './proxy';
-import { check_password_rules, get_paste_info, get_basic_auth, gen_id } from './utils';
+import { check_password_rules, get_paste_info, get_auth, gen_id } from './utils';
import constants, { fetch_constant } from './constant';
import { get_presign_url, router as large_upload } from './api/large_upload';
import v2api from './v2/api';
@@ -316,7 +316,7 @@ router.get('/:uuid/:option?', async (request, env, ctx) => {
// Check password if needed
if (descriptor.password !== undefined) {
if (headers.has('Authorization')) {
- let cert = get_basic_auth(headers);
+ let cert = get_auth(headers, 'Basic');
// Error occurred when parsing the header
if (cert === null) {
return new Response('Invalid Authorization header.', {
diff --git a/src/utils.ts b/src/utils.ts
index 11d5736..fe75272 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -150,28 +150,31 @@ export function check_password_rules(password: string): boolean {
return password.match('^[A-z0-9]{1,}$') !== null;
}
// Extract username and password from Basic Authorization header
-export function get_basic_auth(headers: Headers): [string, string] | null {
+export function get_auth(headers: Headers, required_scheme: string): string | [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') {
+ if (!encoded || scheme !== required_scheme) {
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(':');
+ if (scheme == 'Basic') {
+ // 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;
+ // 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 if (scheme == 'Bearer') {
+ return encoded;
}
-
- return [decoded.slice(0, index), decoded.slice(index + 1)];
- } else {
- return null;
}
+ return null;
}
function to_human_readable_size(bytes: number): string {
diff --git a/src/v2/api.ts b/src/v2/api.ts
index 6fdf8aa..046611a 100644
--- a/src/v2/api.ts
+++ b/src/v2/api.ts
@@ -9,8 +9,10 @@ import {
PasteIndexEntry,
PasteCreateUploadResponse,
PasteType,
+ PasteInfoUpdateParams,
+ PasteInfoUpdateParamsValidator,
} from './schema';
-import { gen_id } from '../utils';
+import { gen_id, get_auth } from '../utils';
import { AwsClient } from 'aws4fetch';
import { sha256 } from 'js-sha256';
import { xml2js } from 'xml-js';
@@ -37,6 +39,71 @@ router.get('/info/:uuid', async (req, env, ctx) => {
return PasteAPIRepsonse.info(descriptor);
});
+/* POST /info/:uuid
+ * Header: Authorization: Basic
+ *
+ * Response:
+ * |
+ */
+router.post('/info/:uuid', async (req, env, ctx) => {
+ const { uuid } = req.params;
+ if (uuid.length !== constants.UUID_LENGTH) {
+ new PasteAPIRepsonse();
+ return PasteAPIRepsonse.build(442, 'Invalid UUID.');
+ }
+ const val = await env.PASTE_INDEX.get(uuid);
+ if (val === null) {
+ return PasteAPIRepsonse.build(404, 'Paste not found.');
+ }
+ const descriptor: PasteIndexEntry = JSON.parse(val);
+
+ let params: PasteInfoUpdateParams | undefined;
+ try {
+ const _params: PasteInfoUpdateParams = await req.json();
+ if (!PasteInfoUpdateParamsValidator.test(_params)) {
+ return PasteAPIRepsonse.build(400, 'Invalid request fields.');
+ }
+ params = _params;
+ } catch (e) {
+ return PasteAPIRepsonse.build(400, 'Invalid request.');
+ }
+
+ // Check password if needed
+ if (descriptor.password !== undefined) {
+ const { headers } = req;
+ let cert = get_auth(headers, 'Bearer');
+ // Error occurred when parsing the header
+ if (cert === null) {
+ return PasteAPIRepsonse.build(
+ 403,
+ 'This paste is password-protected. You must provide the current access credentials to update its metadata.'
+ );
+ }
+ // Check password and username should be empty
+ if (cert.length != 0 || descriptor.password !== sha256(cert).slice(0, 16)) {
+ return PasteAPIRepsonse.build(403, 'Invalid access credentials.');
+ }
+ }
+
+ if (descriptor.upload_track?.pending_upload) {
+ return PasteAPIRepsonse.build(400, 'This paste is not yet finalized.');
+ }
+
+ // Change paste info logic
+ // Explict assign the fields
+ const updated_descriptor = {
+ ...descriptor,
+ password: params.password ? sha256(params.password).slice(0, 16) : undefined,
+ max_access_n: params.max_access_n,
+ title: params.title,
+ mime_type: params.mime_type,
+ expired_at: params.expired_at ? params.expired_at : descriptor.expired_at,
+ };
+
+ ctx.waitUntil(env.PASTE_INDEX.put(uuid, JSON.stringify(updated_descriptor), { expirationTtl: 2419200 }));
+ return PasteAPIRepsonse.info(updated_descriptor);
+});
+
/* POST /create
* Body:
*
diff --git a/src/v2/schema.ts b/src/v2/schema.ts
index 68dee38..a469778 100644
--- a/src/v2/schema.ts
+++ b/src/v2/schema.ts
@@ -70,8 +70,8 @@ export interface PasteCreateParams {
expired_at?: number;
}
-export const PasteCreateParamsValidator = new Validator({
- password: new Rule({ type: 'string', optional: true, notEmpty: true }),
+const param_rules = {
+ password: new Rule({ type: 'password', optional: true, notEmpty: true, maxLength: 40 }),
max_access_n: new Rule({ type: 'int', optional: true, min: 1 }),
title: new Rule({ type: 'string', optional: true, notEmpty: true }),
mime_type: new Rule({ type: 'string', optional: true, notEmpty: true }),
@@ -83,7 +83,9 @@ export const PasteCreateParamsValidator = new Validator({
min: Date.now(),
max: new Date(Date.now() + 2419200 * 1000).getTime(), // max. 28 days
}),
-});
+};
+
+export const PasteCreateParamsValidator = new Validator(param_rules);
export interface PasteCreateUploadResponse {
uuid: string;
@@ -95,6 +97,19 @@ export interface PasteCreateUploadResponse {
};
}
+export interface PasteInfoUpdateParams {
+ password?: string;
+ max_access_n?: number;
+ title?: string;
+ mime_type?: string;
+ expired_at?: number;
+}
+
+// Omit non-editable fields
+const { file_size, file_hash, ...editabe_fiels } = param_rules;
+
+export const PasteInfoUpdateParamsValidator = new Validator(editabe_fiels);
+
export class PasteAPIRepsonse {
static build(
status_code: number = 200,
@@ -129,6 +144,7 @@ export class PasteAPIRepsonse {
static info(descriptor: PasteIndexEntry) {
const paste_info: PasteInfo = {
uuid: descriptor.uuid,
+ title: descriptor.title,
paste_type: descriptor.paste_type,
file_size: descriptor.file_size,
mime_type: descriptor.mime_type,