const express = require('express'); const rateLimit = require('express-rate-limit'); const path = require('path'); const { validateUrl } = require('../lib/ssrf'); const router = express.Router(); // History (per session) router.get('/api/history', (req, res) => { res.json(req.session.httpHistory || []); }); router.post('/api/history/add', express.json(), (req, res) => { if (!req.session.httpHistory) req.session.httpHistory = []; req.session.httpHistory.unshift(req.body); if (req.session.httpHistory.length > 30) req.session.httpHistory.length = 30; res.json({ ok: true }); }); router.delete('/api/history', (req, res) => { req.session.httpHistory = []; res.json({ ok: true }); }); // Proxy const proxyLimiter = rateLimit({ keyGenerator: (req) => (req.session && req.session.user && req.session.user.id) ? "user_" + req.session.user.id : req.ip, windowMs: 60 * 1000, max: parseInt(process.env.PROXY_RATE_LIMIT_MAX) || 60, message: { error: 'Слишком много запросов. Попробуйте через минуту.' }, }); router.post('/api/proxy', proxyLimiter, express.json({ limit: '10mb' }), async (req, res) => { const { url, method = 'GET', headers = {}, body, timeout = 30000 } = req.body || {}; const check = await validateUrl(url); if (!check.safe) return res.status(400).json({ error: check.error }); const startTime = Date.now(); try { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), Math.min(Number(timeout) || 30000, 60000)); let curMethod = method.toUpperCase(); const fetchOptions = { method: curMethod, headers: Object.fromEntries( Object.entries(headers).filter(([k]) => !['host', 'connection', 'transfer-encoding'].includes(k.toLowerCase())) ), signal: controller.signal, redirect: 'manual', }; if (body != null && !['GET', 'HEAD'].includes(curMethod)) { fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body); } // Follow redirects manually, re-validating each hop against SSRF rules. const MAX_REDIRECTS = 5; let currentUrl = check.url; let response; for (let i = 0; ; i++) { response = await fetch(currentUrl, fetchOptions); if (![301, 302, 303, 307, 308].includes(response.status)) break; if (i >= MAX_REDIRECTS) { clearTimeout(timer); return res.status(400).json({ error: 'Слишком много редиректов' }); } const loc = response.headers.get('location'); if (!loc) break; let nextUrl; try { nextUrl = new URL(loc, currentUrl).href; } catch { clearTimeout(timer); return res.status(400).json({ error: 'Некорректный редирект' }); } const rcheck = await validateUrl(nextUrl); if (!rcheck.safe) { clearTimeout(timer); return res.status(400).json({ error: 'Редирект на запрещённый адрес: ' + rcheck.error }); } currentUrl = rcheck.url; // 303 (and 301/302 for non-GET/HEAD) → switch to GET without body, per HTTP semantics if (response.status === 303 || (response.status < 307 && !['GET', 'HEAD'].includes(curMethod))) { curMethod = 'GET'; fetchOptions.method = 'GET'; delete fetchOptions.body; } } clearTimeout(timer); const elapsed = Date.now() - startTime; const responseHeaders = {}; response.headers.forEach((value, key) => { responseHeaders[key] = value; }); const MAX_SIZE = 10 * 1024 * 1024; const reader = response.body?.getReader(); const chunks = []; let totalSize = 0; let truncated = false; if (reader) { while (true) { const { done, value } = await reader.read(); if (done) break; totalSize += value.length; if (totalSize > MAX_SIZE) { truncated = true; break; } chunks.push(value); } } const buffer = Buffer.concat(chunks.map(c => Buffer.from(c))); const responseBody = buffer.toString('utf-8'); res.json({ status: response.status, statusText: response.statusText, headers: responseHeaders, body: responseBody, time: elapsed, size: totalSize, truncated, url: response.url, }); } catch (err) { const elapsed = Date.now() - startTime; if (err.name === 'AbortError') return res.status(408).json({ error: 'Таймаут запроса', time: elapsed }); res.status(500).json({ error: err.message, time: elapsed }); } }); // Page router.get('/httpclient', (req, res) => { res.sendFile(path.join(__dirname, '..', 'public', 'httpclient.html')); }); module.exports = router;