- 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
81 lines
2.1 KiB
JavaScript
81 lines
2.1 KiB
JavaScript
const { URL } = require('url');
|
|
const dns = require('dns');
|
|
const net = require('net');
|
|
|
|
const BLOCKED_RANGES = [
|
|
// IPv4 private
|
|
/^10\./,
|
|
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
/^192\.168\./,
|
|
/^127\./,
|
|
/^0\./,
|
|
// Link-local
|
|
/^169\.254\./,
|
|
// Loopback IPv6
|
|
/^::1$/,
|
|
/^fc/i,
|
|
/^fd/i,
|
|
/^fe80/i,
|
|
];
|
|
|
|
function isPrivateIP(ip) {
|
|
return BLOCKED_RANGES.some(re => re.test(ip));
|
|
}
|
|
|
|
/**
|
|
* Validate URL is safe for server-side requests (no SSRF)
|
|
* Returns { safe: true, url } or { safe: false, error }
|
|
*/
|
|
async function validateUrl(rawUrl) {
|
|
if (!rawUrl || typeof rawUrl !== 'string') {
|
|
return { safe: false, error: 'URL обязателен' };
|
|
}
|
|
|
|
let parsed;
|
|
try {
|
|
parsed = new URL(rawUrl);
|
|
} catch {
|
|
return { safe: false, error: 'Некорректный URL' };
|
|
}
|
|
|
|
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
return { safe: false, error: 'Поддерживаются только http и https' };
|
|
}
|
|
|
|
const hostname = parsed.hostname;
|
|
|
|
// Block direct IP access to private ranges
|
|
if (net.isIP(hostname)) {
|
|
if (isPrivateIP(hostname)) {
|
|
return { safe: false, error: 'Доступ к внутренним адресам запрещён' };
|
|
}
|
|
return { safe: true, url: rawUrl };
|
|
}
|
|
|
|
// Block common internal hostnames
|
|
const lower = hostname.toLowerCase();
|
|
if (lower === 'localhost' || lower.endsWith('.local') || lower.endsWith('.internal')) {
|
|
return { safe: false, error: 'Доступ к внутренним адресам запрещён' };
|
|
}
|
|
|
|
// DNS resolve and check IP
|
|
return new Promise((resolve) => {
|
|
dns.resolve4(hostname, (err, addresses) => {
|
|
if (err || !addresses || addresses.length === 0) {
|
|
// Allow through — DNS might fail but fetch will handle it
|
|
resolve({ safe: true, url: rawUrl });
|
|
return;
|
|
}
|
|
for (const addr of addresses) {
|
|
if (isPrivateIP(addr)) {
|
|
resolve({ safe: false, error: 'Доступ к внутренним адресам запрещён' });
|
|
return;
|
|
}
|
|
}
|
|
resolve({ safe: true, url: rawUrl });
|
|
});
|
|
});
|
|
}
|
|
|
|
module.exports = { validateUrl, isPrivateIP };
|