Forward client HTPP headers to improve caching

Signed-off-by: Joe Ma <rikkaneko23@gmail.com>
This commit is contained in:
Joe Ma 2023-11-10 19:50:13 +08:00
parent 601bd0b7dc
commit e53deac322
No known key found for this signature in database
GPG key ID: 7A0ECF5F5EDC587F
3 changed files with 80 additions and 17 deletions

View file

@ -20,7 +20,8 @@ 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'; import { Env, PasteIndexEntry } from './types';
import { serve_static } from './proxy';
// Constants // Constants
const SERVICE_URL = 'pb.nekoid.cc'; const SERVICE_URL = 'pb.nekoid.cc';
@ -28,14 +29,6 @@ const PASTE_WEB_URL_v1 = 'https://raw.githubusercontent.com/rikkaneko/paste/main
const PASTE_WEB_URL = 'https://raw.githubusercontent.com/rikkaneko/paste/main/static/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 {
PASTE_INDEX: KVNamespace;
QRCODE: ServiceWorkerGlobalScope;
AWS_ACCESS_KEY_ID: string;
AWS_SECRET_ACCESS_KEY: string;
ENDPOINT: string;
}
const gen_id = customAlphabet('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', UUID_LENGTH); const gen_id = customAlphabet('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', UUID_LENGTH);
export default { export default {
@ -43,6 +36,7 @@ export default {
const { url, method, headers } = request; const { url, method, headers } = request;
const { pathname, searchParams } = new URL(url); const { pathname, searchParams } = new URL(url);
const path = pathname.replace(/\/+$/, '') || '/'; const path = pathname.replace(/\/+$/, '') || '/';
const match_etag = headers.get('If-None-Match') || undefined;
let cache = caches.default; let cache = caches.default;
const agent = headers.get('user-agent') ?? ''; const agent = headers.get('user-agent') ?? '';
@ -67,18 +61,18 @@ export default {
} }
if (path === '/v1' && method == 'GET') { if (path === '/v1' && method == 'GET') {
return await proxy_uri(PASTE_WEB_URL_v1 + '/paste.html'); return await serve_static(PASTE_WEB_URL_v1 + '/paste.html', headers);
} }
if (/\/(js|css)\/.*$/.test(path) && method == 'GET') { if (/\/(js|css)\/.*$/.test(path) && method == 'GET') {
return await proxy_uri(PASTE_WEB_URL + path); return await serve_static(PASTE_WEB_URL + path, headers);
} }
if (path === '/') { if (path === '/') {
switch (method) { switch (method) {
case 'GET': { case 'GET': {
// Fetch the HTML for uploading text/file // Fetch the HTML for uploading text/file
return await proxy_uri(PASTE_WEB_URL + '/paste.html'); return await serve_static(PASTE_WEB_URL + '/paste.html', headers);
} }
// Create new paste // Create new paste
@ -343,14 +337,32 @@ export default {
// Enable CF cache for authorized request // Enable CF cache for authorized request
// Match in existing cache // Match in existing cache
let res = await cache.match(request.url); let res = await cache.match(
new Request(`https://${SERVICE_URL}/${uuid}`, {
method: 'GET',
headers: match_etag
? {
// ETag to cache file
'if-none-match': match_etag,
}
: undefined,
})
);
if (res === undefined) { if (res === undefined) {
// Fetch form origin if not hit cache // Fetch form origin if not hit cache
let origin = await s3.fetch(`${env.ENDPOINT}/${uuid}`, { let origin = await s3.fetch(`${env.ENDPOINT}/${uuid}`, {
method: 'GET', method: 'GET',
headers: match_etag
? {
'if-none-match': match_etag,
}
: undefined,
}); });
res = new Response(origin.body); // Reserve ETag header
res = new Response(origin.body, { status: origin.status });
const etag = origin.headers.get('etag');
if (etag) res.headers.append('etag', etag);
if (res.status == 404) { if (res.status == 404) {
// UUID exists in index but not found in remote object storage service, probably expired // UUID exists in index but not found in remote object storage service, probably expired
@ -361,7 +373,7 @@ export default {
return new Response('Paste expired.\n', { return new Response('Paste expired.\n', {
status: 410, status: 410,
}); });
} else if (!res.ok) { } else if (!res.ok && res.status !== 304) {
// Other error // Other error
return new Response('Internal server error.\n', { return new Response('Internal server error.\n', {
status: 500, status: 500,
@ -410,13 +422,15 @@ export default {
// res.body cannot be read twice // res.body cannot be read twice
// Do not block when writing to cache // Do not block when writing to cache
ctx.waitUntil(cache.put(url, res.clone())); if (res.ok) ctx.waitUntil(cache.put(url, res.clone()));
return res; return res;
} }
// Cache hit // Cache hit
// Matched Etag, no body
if (res.status == 304) return res;
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);
} }

41
src/proxy.ts Normal file
View file

@ -0,0 +1,41 @@
// 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> {
// Filter static file extension
let mime = 'text/plain; charset=UTF-8;';
if (path.endsWith('.js')) mime = 'application/javascript; charset=UTF-8;';
else if (path.endsWith('.css')) mime = 'text/css; charset=UTF-8;';
else if (path.endsWith('.html')) mime = 'text/html; charset=UTF-8;';
else if (path.endsWith('.ico')) mime = 'image/x-icon';
else
return new Response(null, {
headers: {
'cache-control': 'public, max-age=14400',
},
status: 404,
});
try {
const res = await fetch(path, {
headers: req_headers,
cf: {
cacheEverything: true,
},
});
// Append ETag and Cache
const etag = res.headers.get('etag');
const nres = new Response(res.body, {
headers: {
'content-type': mime,
'cache-control': 'public, max-age=14400',
},
status: res.status,
});
if (etag) nres.headers.append('etag', etag);
return nres;
} catch (err) {
return new Response('Internal server error.\n', {
status: 500,
});
}
}

8
src/types.d.ts vendored
View file

@ -8,3 +8,11 @@ export interface PasteIndexEntry {
read_count_remain?: number; read_count_remain?: number;
type?: string; type?: string;
} }
export interface Env {
PASTE_INDEX: KVNamespace;
QRCODE: ServiceWorkerGlobalScope;
AWS_ACCESS_KEY_ID: string;
AWS_SECRET_ACCESS_KEY: string;
ENDPOINT: string;
}