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)); const fetchOptions = { method: method.toUpperCase(), headers: Object.fromEntries( Object.entries(headers).filter(([k]) => !['host', 'connection', 'transfer-encoding'].includes(k.toLowerCase())) ), signal: controller.signal, redirect: 'follow', }; if (body != null && !['GET', 'HEAD'].includes(method.toUpperCase())) { fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body); } const response = await fetch(check.url, fetchOptions); 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;