wa-dev-tools/routes/httpclient.js
treamz 5b528d243a Refactor: modular architecture, security, config
- Split monolithic server.js (1111 lines) into route modules
- Add .env config (port, session secret, quality, limits)
- Add SSRF protection for parser/proxy/redirect endpoints
- Add optional password auth middleware
- Add structured logger (replaces raw fs.appendFileSync)
- Add graceful shutdown with timeout
- Add extended /health endpoint (uptime, memory, pid)
- Add ecosystem.config.js for PM2 (memory limit, restart policy)
- Make compress quality configurable (was hardcoded 60)
- Expand SVG AI icons library (7 -> 20 icons)
- Add dotenv dependency
2026-03-21 22:18:54 +03:00

101 lines
3.2 KiB
JavaScript

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({
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;