- 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
91 lines
3.4 KiB
JavaScript
91 lines
3.4 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();
|
|
|
|
const UA_STRINGS = {
|
|
desktop: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
mobile: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
|
|
googlebot: 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
|
|
};
|
|
|
|
const redirectLimiter = rateLimit({ windowMs: 60_000, max: 20 });
|
|
|
|
router.post('/api/redirect-analyze', redirectLimiter, express.json(), async (req, res) => {
|
|
const { url: rawUrl, userAgent = 'desktop', method = 'GET' } = req.body || {};
|
|
|
|
const check = await validateUrl(rawUrl);
|
|
if (!check.safe) return res.status(400).json({ error: check.error });
|
|
|
|
const uaString = UA_STRINGS[userAgent] || UA_STRINGS.desktop;
|
|
const MAX_STEPS = 15;
|
|
const STEP_TIMEOUT = 10_000;
|
|
|
|
const chain = [];
|
|
const visitedUrls = new Set();
|
|
let currentUrl = check.url;
|
|
let loopDetected = false;
|
|
const totalStart = Date.now();
|
|
|
|
for (let step = 0; step < MAX_STEPS; step++) {
|
|
if (visitedUrls.has(currentUrl)) { loopDetected = true; break; }
|
|
visitedUrls.add(currentUrl);
|
|
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), STEP_TIMEOUT);
|
|
const stepStart = Date.now();
|
|
|
|
try {
|
|
const response = await fetch(currentUrl, {
|
|
method: method.toUpperCase(),
|
|
redirect: 'manual',
|
|
signal: controller.signal,
|
|
headers: {
|
|
'User-Agent': uaString,
|
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
},
|
|
});
|
|
clearTimeout(timer);
|
|
|
|
const stepTime = Date.now() - stepStart;
|
|
const responseHeaders = {};
|
|
response.headers.forEach((value, key) => { responseHeaders[key] = value; });
|
|
const location = response.headers.get('location') || null;
|
|
|
|
chain.push({ url: currentUrl, status: response.status, statusText: response.statusText, location, time: stepTime, headers: responseHeaders });
|
|
|
|
if (response.status < 300 || response.status >= 400) break;
|
|
if (!location) break;
|
|
|
|
try { currentUrl = new URL(location, currentUrl).href; } catch { break; }
|
|
} catch (err) {
|
|
clearTimeout(timer);
|
|
chain.push({ url: currentUrl, status: 0, statusText: err.name === 'AbortError' ? 'Timeout' : err.message, location: null, time: Date.now() - stepStart, headers: {} });
|
|
break;
|
|
}
|
|
}
|
|
|
|
const totalTime = Date.now() - totalStart;
|
|
const finalUrl = chain.length > 0 ? chain[chain.length - 1].url : check.url;
|
|
|
|
const issues = [];
|
|
const redirectSteps = chain.filter(s => s.status >= 300 && s.status < 400);
|
|
if (redirectSteps.length > 3) issues.push('long_chain');
|
|
for (let i = 0; i < chain.length - 1; i++) {
|
|
if (chain[i].url.startsWith('https://') && chain[i + 1].url.startsWith('http://')) { issues.push('mixed_protocol'); break; }
|
|
}
|
|
if (chain.some(s => s.status === 302)) issues.push('302_not_301');
|
|
|
|
res.json({ chain, final_url: finalUrl, total_time: totalTime, loop_detected: loopDetected, issues });
|
|
});
|
|
|
|
// Page
|
|
router.get('/redirects', (req, res) => {
|
|
res.sendFile(path.join(__dirname, '..', 'public', 'redirects.html'));
|
|
});
|
|
|
|
module.exports = router;
|