Files
Radicade/Tools/recroom-mirror/public-cache.mjs
T
2026-10-16 23:33:13 -07:00

76 lines
3.7 KiB
JavaScript

import fs from 'node:fs/promises';
import path from 'node:path';
import { createHash } from 'node:crypto';
export function upstreamFor(url) {
const routes = {
'/_public/rooms/featuredrooms/current': 'https://rooms.recroom.network/featuredrooms/current',
'/_public/rooms/rooms/bulk': 'https://rooms.recroom.network/rooms/bulk',
'/_public/accounts/account/bulk': 'https://accounts.recroom.network/account/bulk',
'/_public/apim/apis/api/images/v3/feed/global': 'https://apim.recroom.network/apis/api/images/v3/feed/global',
};
if (routes[url.pathname]) {
const target = new URL(routes[url.pathname]);
for (const [key,value] of url.searchParams) {
if (!['id','ids','skip','take','include'].includes(key) || !/^[0-9,]{1,2000}$/.test(value)) return null;
target.searchParams.append(key,value);
}
return target;
}
const image = url.pathname.match(/^\/_public\/img\/([a-zA-Z0-9_.%-]{1,180})$/);
if (image) {
return new URL('https://img.recroom.network/' + image[1]);
}
return null;
}
export async function servePublicCache(url, req, res, root) {
if (!url.pathname.startsWith('/_public/')) return false;
const upstream = upstreamFor(url);
if (!upstream) { res.writeHead(404); res.end(); return true; }
const key = createHash('sha256').update(upstream.href).digest('hex');
const dir = path.join(root, 'public-cache');
try {
let bytes, type;
try {
bytes = await fs.readFile(path.join(dir,key));
type = await fs.readFile(path.join(dir,key+'.type'),'utf8');
} catch {
const response = await fetch(upstream, {redirect:'error', signal:AbortSignal.timeout(20000)});
if (response.status === 404 && upstream.hostname === 'img.recroom.network') {
res.writeHead(200, {'Content-Type':'image/svg+xml','X-Mirror-Placeholder':'source-image-missing'});
res.end('<svg xmlns="http://www.w3.org/2000/svg" width="480" height="300"><rect width="480" height="300" fill="#303030"/><text x="240" y="155" fill="#aaa" font-family="Arial" font-size="20" text-anchor="middle">Image unavailable in source archive</text></svg>');
return true;
}
if (!response.ok) throw new Error(`Public source returned ${response.status}`);
type = response.headers.get('content-type') || 'application/octet-stream';
bytes = Buffer.from(await response.arrayBuffer());
if (bytes.length > 15 * 1024 * 1024) throw new Error('Response too large');
await fs.mkdir(dir,{recursive:true});
await fs.writeFile(path.join(dir,key),bytes);
await fs.writeFile(path.join(dir,key+'.type'),type);
}
res.writeHead(200, {'Content-Type':type,'Cache-Control':'public, max-age=3600'});
res.end(req.method === 'HEAD' ? undefined : bytes);
} catch(error) {
console.error('Public cache:', upstream.href, error.message);
res.writeHead(502, {'Content-Type':'application/json'});
res.end(JSON.stringify({error:'public_snapshot_unavailable'}));
}
return true;
}
export const bridgeScript = `<script>
const mirrorFetch=window.fetch.bind(window);
window.fetch=function(input,init){
const u=new URL(input instanceof Request?input.url:input,location.href);
const routes={'rooms.recroom.network':'rooms','apim.recroom.network':'apim','accounts.recroom.network':'accounts'};
if(routes[u.hostname]){
const method=(init?.method||(input instanceof Request?input.method:'GET')).toUpperCase();
if(method!=='GET'&&method!=='HEAD')return Promise.resolve(new Response('{"error":"offline_preview"}',{status:501}));
return mirrorFetch('/_public/'+routes[u.hostname]+u.pathname+u.search,{method,signal:init?.signal,credentials:'omit',cache:'no-store'});
}
return mirrorFetch(input,init);
};
</script>`;