wa-dev-tools/routes/redirects.js

92 lines
3.6 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({
keyGenerator: (req) => (req.session && req.session.user && req.session.user.id) ? 'user_' + req.session.user.id : req.ip, 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;