feat: iframe proxy test — fetch Sechenov page, strip X-Frame, rewrite URLs
This commit is contained in:
135
public/iframe.html
Normal file
135
public/iframe.html
Normal 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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
|
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>
|
||||||
40
server.js
40
server.js
@@ -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'));
|
||||||
|
|||||||
Reference in New Issue
Block a user