diff --git a/public/iframe.html b/public/iframe.html deleted file mode 100644 index 93638c6..0000000 --- a/public/iframe.html +++ /dev/null @@ -1,135 +0,0 @@ - - - - - -Авторизация и регистрация - - - - - - - -
- -
-
- -
-
- - - - diff --git a/server.js b/server.js index a258dd9..cf2f329 100644 --- a/server.js +++ b/server.js @@ -7,17 +7,13 @@ const { ProxyAgent, Agent, setGlobalDispatcher } = require('undici'); const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; const directAgent = new Agent(); const anthropicAgent = proxyUrl ? new ProxyAgent(proxyUrl) : directAgent; -// Force global default = DIRECT. Anthropic calls override with their own dispatcher. setGlobalDispatcher(directAgent); -if (proxyUrl) console.log(`✓ Anthropic proxy: ${proxyUrl} (sechenov: direct)`); +if (proxyUrl) console.log(`✓ Anthropic proxy: ${proxyUrl}`); const app = express(); const PUBLIC_DIR = path.join(__dirname, 'public'); -const UPSTREAM = 'https://student.sechenov.ru'; -// ══════════════════════════════════════════════════════════════ -// CLAUDE CHAT (/api/chat) -// ══════════════════════════════════════════════════════════════ +app.use(express.static(PUBLIC_DIR)); const MODELS = { sonnet: 'claude-sonnet-4-6', @@ -137,299 +133,4 @@ app.post('/api/chat', express.json({ limit: '10mb' }), async (req, res) => { } }); -// ══════════════════════════════════════════════════════════════ -// 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(/]+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(/]*>/i, m => m + '\n'); - 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 = ` - -
-
- -`; - -// ══════════════════════════════════════════════════════════════ -// 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(/]+http-equiv=["']?(Content-Security-Policy|X-Frame-Options)["']?[^>]*>/gi, '') - .replace(/\btarget=["'](_top|_parent|_blank)["']/gi, ''); - html = html.replace(/<\/body>/i, CHAT_OVERLAY + ''); - - 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(/]+http-equiv=["']?(Content-Security-Policy|X-Frame-Options)["']?[^>]*>/gi, ''); - // inject chat overlay - if (html.match(/<\/body>/i)) { - html = html.replace(/<\/body>/i, CHAT_OVERLAY + ''); - } 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)')); +app.listen(8108, () => console.log('sechenov :8108'));