cleanup: remove iframe/mirror/proxy experiments, keep clone + chat only

This commit is contained in:
aedes
2026-04-21 19:26:21 +03:00
parent d50e2ab547
commit cf1ee42c3b
2 changed files with 3 additions and 437 deletions

View File

@@ -1,135 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Авторизация и регистрация</title>
<link rel="icon" type="image/png" href="/assets/images/favicon.png">
<style>
html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; }
#portal {
display: block;
border: 0;
width: 100%;
height: calc(100vh - 50px);
}
/* chat stays fixed at the bottom 50px of viewport */
.chat-overlay {
position: fixed;
left: 0; right: 0; bottom: 0;
background: #fff;
border-top: 4px solid #003571;
z-index: 9999;
font-family: Arial, Helvetica, sans-serif;
}
#chat-output-tiny {
position: fixed;
left: 0; right: 0;
bottom: 50px;
max-height: 200px;
overflow-y: auto;
padding: 4px 15px;
font-size: 10px;
color: #c4c4c4;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
background: linear-gradient(to bottom, transparent 0%, rgba(255,255,255,0.92) 30%, #fff 100%);
pointer-events: none;
}
#chat-output-tiny:empty { display: none; }
.chat-overlay .inner {
max-width: 970px; margin: 0 auto;
padding: 8px 15px;
}
#chat-input {
width: 100%;
border: none;
background: transparent;
color: #f2f2f2;
font-size: 14px;
outline: none;
resize: none;
height: 28px;
max-height: 28px;
min-height: 28px;
line-height: 1.5;
padding: 0;
overflow: hidden;
white-space: nowrap;
caret-color: #c4c4c4;
font-family: inherit;
}
#chat-input::placeholder { color: transparent; }
</style>
</head>
<body>
<iframe id="portal" src="/proxy/auth.php"></iframe>
<div id="chat-output-tiny"></div>
<div class="chat-overlay">
<div class="inner">
<textarea id="chat-input" autocomplete="off" spellcheck="false" rows="1"></textarea>
</div>
</div>
<script>
const inputEl = document.getElementById('chat-input');
const outputEl = document.getElementById('chat-output-tiny');
function renderMarkdown(text) {
let html = text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
html = html.replace(/```[\w]*\n?([\s\S]*?)```/g, (_,c) => `<pre>${c.trim()}</pre>`);
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
return html;
}
let inFlight = false;
async function send(text) {
if (inFlight || !text.trim()) return;
inFlight = true;
outputEl.innerHTML = '';
let fullText = '';
try {
const resp = await fetch('/api/chat', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({text, model: 'sonnet'}),
});
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const {done, value} = await reader.read();
if (done) break;
buf += decoder.decode(value, {stream:true});
const parts = buf.split('\n\n');
buf = parts.pop();
for (const part of parts) {
if (!part.startsWith('data: ')) continue;
const raw = part.slice(6).trim();
if (raw === '[DONE]') continue;
try {
const obj = JSON.parse(raw);
if (obj.text) {
fullText += obj.text;
outputEl.innerHTML = renderMarkdown(fullText);
}
} catch(e) {}
}
}
} catch(e) {} finally { inFlight = false; }
}
inputEl.addEventListener('paste', () => {
setTimeout(() => {
const t = inputEl.value.trim();
if (t) send(t);
}, 50);
});
</script>
</body>
</html>

305
server.js
View File

@@ -7,17 +7,13 @@ const { ProxyAgent, Agent, setGlobalDispatcher } = require('undici');
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
const directAgent = new Agent(); const directAgent = new Agent();
const anthropicAgent = proxyUrl ? new ProxyAgent(proxyUrl) : directAgent; const anthropicAgent = proxyUrl ? new ProxyAgent(proxyUrl) : directAgent;
// Force global default = DIRECT. Anthropic calls override with their own dispatcher.
setGlobalDispatcher(directAgent); setGlobalDispatcher(directAgent);
if (proxyUrl) console.log(`✓ Anthropic proxy: ${proxyUrl} (sechenov: direct)`); if (proxyUrl) console.log(`✓ Anthropic proxy: ${proxyUrl}`);
const app = express(); const app = express();
const PUBLIC_DIR = path.join(__dirname, 'public'); const PUBLIC_DIR = path.join(__dirname, 'public');
const UPSTREAM = 'https://student.sechenov.ru';
// ══════════════════════════════════════════════════════════════ app.use(express.static(PUBLIC_DIR));
// CLAUDE CHAT (/api/chat)
// ══════════════════════════════════════════════════════════════
const MODELS = { const MODELS = {
sonnet: 'claude-sonnet-4-6', sonnet: 'claude-sonnet-4-6',
@@ -137,299 +133,4 @@ app.post('/api/chat', express.json({ limit: '10mb' }), async (req, res) => {
} }
}); });
// ══════════════════════════════════════════════════════════════ app.listen(8108, () => console.log('sechenov :8108'));
// LEGACY / LOCAL ROUTES
// ══════════════════════════════════════════════════════════════
// Main page: local clone (reverse-proxy disabled — Sechenov banned our IPs)
app.use(express.static(PUBLIC_DIR));
app.get(['/old', '/old.html'], (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'index.html')));
app.get(['/iframe', '/iframe.html'], (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'iframe.html')));
// Legacy HTML-only proxy used by /iframe.html
app.get('/proxy/*', async (req, res) => {
const upstreamPath = req.url.replace(/^\/proxy/, '') || '/';
try {
const r = await fetch(UPSTREAM + upstreamPath, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': req.headers.accept || '*/*',
'Accept-Language': req.headers['accept-language'] || 'ru,en;q=0.9',
},
});
const ct = r.headers.get('content-type') || 'application/octet-stream';
res.status(r.status);
res.setHeader('Content-Type', ct);
res.removeHeader('X-Frame-Options');
res.removeHeader('Content-Security-Policy');
if (ct.includes('text/html')) {
let html = await r.text();
html = html
.replace(/\b(href|action)="\/(?!\/)/g, '$1="/proxy/')
.replace(/\bsrc="\/(?!\/)/g, 'src="' + UPSTREAM + '/')
.replace(/url\(["']?\/(?!\/)/g, 'url(' + UPSTREAM + '/')
.replace(/\btarget=["'](_top|_parent|_blank)["']/gi, '')
.replace(/<meta[^>]+http-equiv=["']?(Content-Security-Policy|X-Frame-Options)["']?[^>]*>/gi, '')
.replace(/\b(document|window|top|self)\.location(\.href)?\s*=\s*(['"])\/(?!\/)/g, "document.location.href=$3/proxy/")
.replace(/\blocation\.replace\(\s*(['"])\/(?!\/)/g, "location.replace($1/proxy/");
html = html.replace(/<head[^>]*>/i, m => m + '\n<base href="/proxy/">');
res.send(html);
} else if (ct.includes('text/css')) {
let css = await r.text();
css = css.replace(/url\(\s*(["']?)\/(?!\/)/g, 'url($1' + UPSTREAM + '/');
res.send(css);
} else {
res.send(Buffer.from(await r.arrayBuffer()));
}
} catch (e) {
res.status(502).send('proxy error: ' + e.message);
}
});
// ══════════════════════════════════════════════════════════════
// FULL REVERSE PROXY (everything else → student.sechenov.ru)
// ══════════════════════════════════════════════════════════════
function readRawBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', c => chunks.push(c));
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
}
const CHAT_OVERLAY = `
<style>
#_chatOut { position:fixed; left:0; right:0; bottom:46px; max-height:200px; overflow-y:auto; padding:4px 15px; font-size:10px; color:#c4c4c4; line-height:1.5; white-space:pre-wrap; word-break:break-word; background:linear-gradient(to bottom, transparent 0%, rgba(255,255,255,0.92) 30%, #fff 100%); pointer-events:none; z-index:99998; font-family:Arial, sans-serif; }
#_chatOut:empty { display:none; }
#_chatBar { position:fixed; left:0; right:0; bottom:0; background:#fff; border-top:4px solid #003571; z-index:99999; font-family:Arial, sans-serif; }
#_chatBar .in { max-width:970px; margin:0 auto; padding:8px 15px; }
#_chatIn { width:100%; border:none; background:transparent; color:#f2f2f2; font-size:14px; outline:none; resize:none; height:28px; line-height:1.5; white-space:nowrap; overflow:hidden; caret-color:#c4c4c4; font-family:inherit; }
body { padding-bottom:55px !important; }
</style>
<div id="_chatOut"></div>
<div id="_chatBar"><div class="in"><textarea id="_chatIn" autocomplete="off" spellcheck="false" rows="1"></textarea></div></div>
<script>
(function() {
var i = document.getElementById('_chatIn'), o = document.getElementById('_chatOut'), busy = false;
function md(t){ var h=t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); h=h.replace(/\\\`\\\`\\\`[\\w]*\\n?([\\s\\S]*?)\\\`\\\`\\\`/g,function(_,c){return '<pre>'+c.trim()+'</pre>';}); h=h.replace(/\\\`([^\\\`]+)\\\`/g,'<code>$1</code>'); h=h.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>'); return h; }
async function send(t){ if(busy||!t.trim())return; busy=true; o.innerHTML=''; var full='';
try { var r=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:t,model:'sonnet'})}); var rd=r.body.getReader(), dec=new TextDecoder(), buf='';
while(true){ var x=await rd.read(); if(x.done)break; buf+=dec.decode(x.value,{stream:true}); var p=buf.split('\\n\\n'); buf=p.pop();
for(var k=0;k<p.length;k++){ var ln=p[k]; if(!ln.startsWith('data: '))continue; var raw=ln.slice(6).trim(); if(raw==='[DONE]')continue;
try{var obj=JSON.parse(raw); if(obj.text){full+=obj.text; o.innerHTML=md(full);}}catch(e){} } } } catch(e){} finally{busy=false;} }
i.addEventListener('paste',function(){ setTimeout(function(){ var t=i.value.trim(); if(t)send(t); },50); });
})();
</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);
const serveMirror = (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);
};
// Entry points that render the cached mirror HTML (any HTTP method)
app.all(['/m', '/m.html', '/mirror'], serveMirror);
app.all(['/auth.php', '/index.php'], serveMirror);
app.all(['/assets/mirror', '/assets/mirror/', '/assets/mirror/auth.php', '/assets/mirror/index.php'], serveMirror);
app.use('/assets/mirror', express.static(MIRROR_DIR));
// Reverse-proxy disabled: Sechenov banned us by IP. Left here for reference.
app.use('/_disabled_reverse_proxy', async (req, res) => {
const upstreamUrl = UPSTREAM + req.url;
// Build forward headers
const headers = {};
for (const [k, v] of Object.entries(req.headers)) {
const lk = k.toLowerCase();
if (['host', 'content-length', 'x-forwarded-for', 'x-forwarded-proto',
'x-forwarded-host', 'x-real-ip', 'connection'].includes(lk)) continue;
headers[k] = v;
}
if (!headers['user-agent']) {
headers['user-agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
}
let body;
if (!['GET', 'HEAD'].includes(req.method)) {
try { body = await readRawBody(req); } catch {}
}
try {
const upstream = await fetch(upstreamUrl, {
method: req.method, headers, body, redirect: 'manual',
});
res.status(upstream.status);
// Set-Cookie: strip Domain= so they bind to our origin
const setCookies = typeof upstream.headers.getSetCookie === 'function'
? upstream.headers.getSetCookie()
: (upstream.headers.raw && upstream.headers.raw()['set-cookie']) || [];
if (setCookies.length) {
const rewritten = setCookies.map(c =>
c.replace(/;\s*Domain=[^;]+/gi, '')
.replace(/;\s*domain=[^;]+/gi, '')
);
res.setHeader('set-cookie', rewritten);
}
for (const [k, v] of upstream.headers) {
const lk = k.toLowerCase();
if (['set-cookie', 'content-encoding', 'content-length',
'transfer-encoding', 'x-frame-options',
'content-security-policy', 'strict-transport-security',
'connection'].includes(lk)) continue;
if (lk === 'location') {
res.setHeader('location', v.replace(new RegExp('^https?://student\\.sechenov\\.ru', 'i'), ''));
continue;
}
res.setHeader(k, v);
}
const ct = upstream.headers.get('content-type') || '';
if (ct.includes('text/html')) {
let html = await upstream.text();
html = html
.replace(/https?:\/\/student\.sechenov\.ru/gi, '')
.replace(/<meta[^>]+http-equiv=["']?(Content-Security-Policy|X-Frame-Options)["']?[^>]*>/gi, '');
// inject chat overlay
if (html.match(/<\/body>/i)) {
html = html.replace(/<\/body>/i, CHAT_OVERLAY + '</body>');
} else {
html = html + CHAT_OVERLAY;
}
res.send(html);
} else if (ct.includes('text/css') || ct.includes('javascript') || ct.includes('application/json') || (ct.includes('text/') && !ct.includes('text/event-stream'))) {
let text = await upstream.text();
text = text.replace(/https?:\/\/student\.sechenov\.ru/gi, '');
res.send(text);
} else {
res.send(Buffer.from(await upstream.arrayBuffer()));
}
} catch (e) {
res.status(502).send('upstream error: ' + e.message);
}
});
app.listen(8108, () => console.log('sechenov :8108 (reverse-proxy mode)'));