feat: /m mirror — cache HTML + assets locally, rare upstream refresh (30min)
This commit is contained in:
124
server.js
124
server.js
@@ -224,6 +224,130 @@ const CHAT_OVERLAY = `
|
|||||||
</script>
|
</script>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// MIRROR — one-time asset mirror + in-memory HTML cache
|
||||||
|
// Goal: cache everything locally, refresh HTML rarely, never
|
||||||
|
// hammer sechenov. Steady-state: ~1 upstream request / 30 min.
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const MIRROR_DIR = path.join(PUBLIC_DIR, 'assets', 'mirror');
|
||||||
|
const MIRROR_TTL_MS = 30 * 60 * 1000;
|
||||||
|
const MIRROR_MIN_GAP = 60 * 1000;
|
||||||
|
const UPSTREAM_DELAY = 500;
|
||||||
|
const BROWSER_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
||||||
|
|
||||||
|
let cachedMirrorHtml = null;
|
||||||
|
let mirrorInProgress = false;
|
||||||
|
let lastMirrorAt = 0;
|
||||||
|
let mirrorSessionCookies = '';
|
||||||
|
|
||||||
|
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||||
|
|
||||||
|
async function safeFetch(url, extraHeaders = {}) {
|
||||||
|
const h = {
|
||||||
|
'User-Agent': BROWSER_UA,
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'ru,en;q=0.9',
|
||||||
|
'Referer': UPSTREAM + '/',
|
||||||
|
...extraHeaders,
|
||||||
|
};
|
||||||
|
if (mirrorSessionCookies) h['Cookie'] = mirrorSessionCookies;
|
||||||
|
return fetch(url, { dispatcher: directAgent, headers: h });
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAssetUrls(html) {
|
||||||
|
const urls = new Set();
|
||||||
|
const patterns = [
|
||||||
|
/(?:href|src)="(\/[^"\s]+)"/g,
|
||||||
|
/url\(["']?(\/[^"'\s\)]+)/g,
|
||||||
|
];
|
||||||
|
for (const p of patterns) {
|
||||||
|
let m;
|
||||||
|
while ((m = p.exec(html)) !== null) {
|
||||||
|
const u = m[1];
|
||||||
|
if (u.startsWith('//')) continue;
|
||||||
|
if (u.startsWith('/api/') || u.startsWith('/assets/') || u.startsWith('/proxy/')) continue;
|
||||||
|
urls.add(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...urls];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mirrorAsset(urlPath) {
|
||||||
|
const pure = urlPath.split(/[?#]/)[0];
|
||||||
|
const local = path.join(MIRROR_DIR, pure);
|
||||||
|
try { await fs.promises.stat(local); return false; } catch {}
|
||||||
|
const r = await safeFetch(UPSTREAM + urlPath);
|
||||||
|
if (!r.ok) { console.warn(`[mirror] ${r.status} ${urlPath}`); return false; }
|
||||||
|
const ct = r.headers.get('content-type') || '';
|
||||||
|
let buf;
|
||||||
|
if (ct.includes('text/css')) {
|
||||||
|
let css = await r.text();
|
||||||
|
css = css.replace(/url\(\s*(["']?)\/(?!\/)/g, 'url($1/assets/mirror/');
|
||||||
|
buf = Buffer.from(css, 'utf8');
|
||||||
|
} else {
|
||||||
|
buf = Buffer.from(await r.arrayBuffer());
|
||||||
|
}
|
||||||
|
await fs.promises.mkdir(path.dirname(local), { recursive: true });
|
||||||
|
await fs.promises.writeFile(local, buf);
|
||||||
|
console.log(`[mirror] + ${pure} (${buf.length}b)`);
|
||||||
|
await sleep(UPSTREAM_DELAY);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshMirror() {
|
||||||
|
if (mirrorInProgress) return;
|
||||||
|
if (Date.now() - lastMirrorAt < MIRROR_MIN_GAP && cachedMirrorHtml) return;
|
||||||
|
mirrorInProgress = true;
|
||||||
|
lastMirrorAt = Date.now();
|
||||||
|
try {
|
||||||
|
console.log('[mirror] fetching HTML…');
|
||||||
|
const r = await safeFetch(UPSTREAM + '/auth.php');
|
||||||
|
if (!r.ok) throw new Error('upstream ' + r.status);
|
||||||
|
|
||||||
|
const setCookies = typeof r.headers.getSetCookie === 'function' ? r.headers.getSetCookie() : [];
|
||||||
|
if (setCookies.length) {
|
||||||
|
mirrorSessionCookies = setCookies.map(c => c.split(';')[0]).join('; ');
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = await r.text();
|
||||||
|
const assetUrls = extractAssetUrls(html);
|
||||||
|
console.log(`[mirror] HTML ${html.length}b, ${assetUrls.length} assets referenced`);
|
||||||
|
|
||||||
|
let downloaded = 0;
|
||||||
|
for (const u of assetUrls) {
|
||||||
|
try { if (await mirrorAsset(u)) downloaded++; } catch (e) { console.warn('[mirror]', u, e.message); }
|
||||||
|
}
|
||||||
|
console.log(`[mirror] downloaded ${downloaded} new assets (cached: ${assetUrls.length - downloaded})`);
|
||||||
|
|
||||||
|
html = html
|
||||||
|
.replace(/(href|src)="\/(?!\/)/g, '$1="/assets/mirror/')
|
||||||
|
.replace(/url\(["']?\/(?!\/)/g, 'url(/assets/mirror/')
|
||||||
|
.replace(/https?:\/\/student\.sechenov\.ru/gi, '')
|
||||||
|
.replace(/<meta[^>]+http-equiv=["']?(Content-Security-Policy|X-Frame-Options)["']?[^>]*>/gi, '')
|
||||||
|
.replace(/\btarget=["'](_top|_parent|_blank)["']/gi, '');
|
||||||
|
html = html.replace(/<\/body>/i, CHAT_OVERLAY + '</body>');
|
||||||
|
|
||||||
|
cachedMirrorHtml = html;
|
||||||
|
console.log('[mirror] ready');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[mirror fail]', e.message);
|
||||||
|
} finally {
|
||||||
|
mirrorInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => refreshMirror(), 3000);
|
||||||
|
setInterval(() => refreshMirror(), MIRROR_TTL_MS);
|
||||||
|
|
||||||
|
app.get(['/m', '/m.html', '/mirror'], (req, res) => {
|
||||||
|
if (!cachedMirrorHtml) return res.status(503).send('mirror initializing — try again in 60s');
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
res.send(cachedMirrorHtml);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use('/assets/mirror', express.static(MIRROR_DIR));
|
||||||
|
|
||||||
// Reverse-proxy disabled: Sechenov banned us by IP. Left here for reference.
|
// Reverse-proxy disabled: Sechenov banned us by IP. Left here for reference.
|
||||||
app.use('/_disabled_reverse_proxy', async (req, res) => {
|
app.use('/_disabled_reverse_proxy', async (req, res) => {
|
||||||
const upstreamUrl = UPSTREAM + req.url;
|
const upstreamUrl = UPSTREAM + req.url;
|
||||||
|
|||||||
Reference in New Issue
Block a user