feat: iframe proxy test — fetch Sechenov page, strip X-Frame, rewrite URLs

This commit is contained in:
aedes
2026-04-21 16:55:10 +03:00
parent 8878cba3d1
commit 29ac92aa3e
2 changed files with 175 additions and 0 deletions

135
public/iframe.html Normal file
View File

@@ -0,0 +1,135 @@
<!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>

View File

@@ -137,4 +137,44 @@ app.post('/api/chat', async (req, res) => {
} }
}); });
// ── iframe proxy: fetch student.sechenov.ru, strip X-Frame-Options, rewrite URLs ──
app.get('/proxy/*', async (req, res) => {
const upstreamPath = req.url.replace(/^\/proxy/, '') || '/';
const upstreamUrl = 'https://student.sechenov.ru' + upstreamPath;
try {
const r = await fetch(upstreamUrl, {
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);
// strip framing restrictions
res.removeHeader('X-Frame-Options');
res.removeHeader('Content-Security-Policy');
if (ct.includes('text/html')) {
let html = await r.text();
// rewrite relative URLs to absolute so browser loads assets directly
html = html
.replace(/(href|src|action)="\/(?!\/)/g, '$1="https://student.sechenov.ru/')
.replace(/url\(["']?\/(?!\/)/g, 'url(https://student.sechenov.ru/')
// neutralize meta CSP if present
.replace(/<meta[^>]+http-equiv=["']?Content-Security-Policy["']?[^>]*>/gi, '');
res.send(html);
} else {
// stream binary/text as-is
const buf = Buffer.from(await r.arrayBuffer());
res.send(buf);
}
} catch (e) {
res.status(502).send('proxy error: ' + e.message);
}
});
app.listen(8108, () => console.log('sechenov :8108')); app.listen(8108, () => console.log('sechenov :8108'));