Web-Proxy / server.js
NathMen12's picture
Update server.js
e8a8f2e verified
Raw
History Blame Contribute Delete
14.4 kB
import http from 'http';
import { Readable } from 'stream';
const PORT = process.env.PORT || 7860;
process.on('uncaughtException', (err) => console.error('CRASH ÉVITÉ:', err));
process.on('unhandledRejection', (err) => console.error('PROMESSE REFUSÉE:', err));
const trafficStats = {};
function getClientStats(req) {
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || '127.0.0.1';
if (!trafficStats[ip]) {
trafficStats[ip] = { up: 0, down: 0 };
}
return trafficStats[ip];
}
const BROWSER_HOOK_SCRIPT = `
<script>
(function() {
const proxyOrigin = window.location.origin;
const pathParts = window.location.pathname.split('/');
let currentTargetOrigin = '';
if (pathParts[1] === 'p' && pathParts[2] && pathParts[3]) {
currentTargetOrigin = pathParts[2] + '://' + pathParts[3];
}
function rewriteUrl(url) {
if (!url || typeof url !== 'string') return url;
if (url === '/proxy-stats' || url === '/') return url;
if (url.startsWith('http://') || url.startsWith('https://')) {
const u = new URL(url);
const proto = u.protocol.replace(':', '');
return proxyOrigin + '/p/' + proto + '/' + u.host + u.pathname + u.search + u.hash;
}
if (url.startsWith('/') && !url.startsWith('/p/') && currentTargetOrigin) {
const u = new URL(url, currentTargetOrigin);
const proto = u.protocol.replace(':', '');
return proxyOrigin + '/p/' + proto + '/' + u.host + u.pathname + u.search + u.hash;
}
return url;
}
const _fetch = window.fetch;
window.fetch = function(resource, options) {
if (typeof resource === 'string') { resource = rewriteUrl(resource); }
else if (resource instanceof Request) {
const newUrl = rewriteUrl(resource.url);
resource = new Request(newUrl, resource);
}
return _fetch(resource, options);
};
const _open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, ...args) {
return _open.call(this, method, rewriteUrl(url), ...args);
};
window.open = (function(_openWin) {
return function(url, ...args) { return _openWin.call(window, rewriteUrl(url), ...args); };
})(window.open);
document.addEventListener('click', function(e) {
const target = e.target.closest('a');
if (target && target.href) { target.href = rewriteUrl(target.href); }
}, true);
function initProxyUI() {
if (document.getElementById('proxy-stats-badge')) return;
const homeBtn = document.createElement('div');
homeBtn.id = 'proxy-home-btn';
homeBtn.innerHTML = '🏠';
homeBtn.style = 'position:fixed; bottom:20px; right:20px; z-index:999999; background:#38bdf8; color:#0f172a; width:48px; height:48px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:22px; cursor:pointer; box-shadow:0 4px 15px rgba(0,0,0,0.5); user-select:none; transition:transform 0.2s;';
homeBtn.onclick = () => window.location.href = '/';
homeBtn.onmouseover = () => homeBtn.style.transform = 'scale(1.1)';
homeBtn.onmouseout = () => homeBtn.style.transform = 'scale(1)';
document.body.appendChild(homeBtn);
const statsBadge = document.createElement('div');
statsBadge.id = 'proxy-stats-badge';
statsBadge.innerHTML = '⬇️ 0.00 Mo | ⬆️ 0.00 Mo';
statsBadge.style = 'position:fixed; top:20px; right:20px; z-index:999999; background:rgba(15, 23, 42, 0.9); color:#38bdf8; padding:8px 14px; border-radius:8px; font-family:monospace; font-size:13px; font-weight:bold; border:1px solid #334155; backdrop-filter:blur(4px); box-shadow:0 4px 15px rgba(0,0,0,0.4); pointer-events:none; user-select:none;';
document.body.appendChild(statsBadge);
setInterval(async () => {
try {
const res = await _fetch('/proxy-stats');
if (res.ok) {
const data = await res.json();
statsBadge.innerHTML = '⬇️ ' + data.down + ' Mo | ⬆️ ' + data.up + ' Mo';
}
} catch(e) {}
}, 1500);
}
if (document.body) { initProxyUI(); }
else { window.addEventListener('DOMContentLoaded', initProxyUI); }
})();
</script>
`;
const LANDING_PAGE = `
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CyberProxy Pro</title>
<style>
body { font-family: 'Segoe UI', system-ui, sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; background: #0f172a; color: #f8fafc; margin: 0; }
.container { text-align: center; background: #1e293b; padding: 45px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); width: 90%; max-width: 550px; border: 1px solid #334155; }
h1 { color: #38bdf8; margin: 0 0 10px 0; font-size: 34px; font-weight: 800; }
p { color: #94a3b8; margin-bottom: 35px; font-size: 15px; }
.form-group { display: flex; flex-direction: column; gap: 16px; }
input { padding: 16px; border: 2px solid #334155; border-radius: 10px; background: #0f172a; color: white; font-size: 16px; outline: none; }
input:focus { border-color: #38bdf8; }
button { padding: 16px; border: none; border-radius: 10px; background: #38bdf8; color: #0f172a; font-size: 16px; cursor: pointer; font-weight: bold; }
button:hover { background: #7dd3fc; }
</style>
</head>
<body>
<div class="container">
<h1>🚀 CyberProxy Pro</h1>
<p>Accède à Modrinth et contourne les restrictions d'assets en temps réel</p>
<div class="form-group">
<input type="text" id="urlInput" placeholder="https://modrinth.com" value="https://modrinth.com">
<button onclick="go()">Lancer la session</button>
</div>
</div>
<script>
function go() {
let url = document.getElementById('urlInput').value.trim();
if(!url) return;
if(!url.startsWith('http')) url = 'https://' + url;
const u = new URL(url);
window.location.href = '/p/' + u.protocol.replace(':', '') + '/' + u.host + u.pathname + u.search;
}
</script>
</body>
</html>
`;
function rewriteCss(css, targetUrl) {
const proto = targetUrl.protocol.replace(':', '');
let rewritten = css.replace(/url\(['"]?(https?:\/\/[^'")]+)['"]?\)/g, (match, url) => {
try {
const u = new URL(url);
return `url(/p/${u.protocol.replace(':', '')}/${u.host}${u.pathname}${u.search})`;
} catch(e) { return match; }
});
rewritten = rewritten.replace(/url\(['"]?\/((?!p\/)[^'")]+)['"]?\)/g, (match, path) => {
return `url(/p/${proto}/${targetUrl.host}/${path})`;
});
return rewritten;
}
const server = http.createServer(async (req, res) => {
const clientStats = getClientStats(req);
// --- PRIORITÉ ABSOLUE AUX ROUTES INTERNES ---
if (req.url === '/' || req.url === '') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(LANDING_PAGE);
}
if (req.url === '/proxy-stats') {
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, max-age=0'
});
return res.end(JSON.stringify({
up: (clientStats.up / (1024 * 1024)).toFixed(2),
down: (clientStats.down / (1024 * 1024)).toFixed(2)
}));
}
// --- LOGIQUE DU PROXY ---
let targetUrlStr = null;
if (req.url.startsWith('/p/')) {
const match = req.url.match(/^\/p\/([^/]+)\/([^/]+)(.*)$/);
if (match) {
const [_, proto, domain, path] = match;
targetUrlStr = `${proto}://${domain}${path}`;
}
} else if (req.headers.referer) {
try {
const refUrl = new URL(req.headers.referer);
if (refUrl.pathname.startsWith('/p/')) {
const match = refUrl.pathname.match(/^\/p\/([^/]+)\/([^/]+)/);
if (match) {
targetUrlStr = `${match[1]}://${match[2]}${req.url}`;
}
}
} catch (e) {}
}
if (!targetUrlStr && req.headers.cookie) {
const match = req.headers.cookie.match(/proxy_fallback_origin=([^;]+)/);
if (match) targetUrlStr = decodeURIComponent(match[1]) + req.url;
}
if (!targetUrlStr) {
res.writeHead(404);
return res.end();
}
try {
const targetUrl = new URL(targetUrlStr);
const headers = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
if (!['host', 'cookie', 'accept-encoding', 'connection', 'origin', 'referer'].includes(key.toLowerCase())) {
headers.set(key, value);
}
}
headers.set('Host', targetUrl.host);
headers.set('User-Agent', req.headers['user-agent'] || 'Mozilla/5.0');
if (req.headers['origin']) headers.set('Origin', targetUrl.origin);
if (req.headers['referer']) headers.set('Referer', targetUrl.origin);
if (req.headers.cookie) {
const cleanCookies = req.headers.cookie.split(';').map(c => c.trim()).filter(c => !c.startsWith('proxy_fallback_origin=')).join('; ');
if (cleanCookies) headers.set('Cookie', cleanCookies);
}
let body = null;
if (req.method !== 'GET' && req.method !== 'HEAD') {
const buffers = [];
for await (const chunk of req) {
buffers.push(chunk);
clientStats.up += chunk.length;
}
body = Buffer.concat(buffers);
}
const response = await fetch(targetUrl.href, {
method: req.method,
headers: headers,
body: body,
redirect: 'manual'
});
const stripHeaders = ['connection', 'keep-alive', 'transfer-encoding', 'content-encoding', 'location', 'content-security-policy', 'strict-transport-security', 'content-length'];
const resHeaders = {};
response.headers.forEach((value, key) => {
if (!stripHeaders.includes(key.toLowerCase())) resHeaders[key] = value;
});
const setCookies = [`proxy_fallback_origin=${encodeURIComponent(targetUrl.origin)}; Path=/; HttpOnly; SameSite=Lax`];
const originalSetCookies = response.headers.getSetCookie ? response.headers.getSetCookie() : [];
originalSetCookies.forEach(c => setCookies.push(c));
resHeaders['Set-Cookie'] = setCookies;
if (response.status >= 300 && response.status < 400) {
let location = response.headers.get('location');
if (location) {
const absoluteLocation = new URL(location, targetUrl.href);
resHeaders['Location'] = `/p/${absoluteLocation.protocol.replace(':', '')}/${absoluteLocation.host}${absoluteLocation.pathname}${absoluteLocation.search}`;
}
res.writeHead(response.status, resHeaders);
return res.end();
}
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('text/html')) {
let text = await response.text();
text = text.replace('<head>', '<head>' + BROWSER_HOOK_SCRIPT);
text = text.replace(/(href|src)=["'](https?:\/\/)([^"']+)["']/g, (m, attr, proto, rest) => `${attr}="/p/${proto.replace('://', '')}/${rest}"`);
text = text.replace(/(href|src)=["']\/((?!p\/)[^"']+)["']/g, (m, attr, path) => `${attr}="/p/${targetUrl.protocol.replace(':', '')}/${targetUrl.host}/${path}"`);
const bodyBuffer = Buffer.from(text, 'utf-8');
clientStats.down += bodyBuffer.length;
resHeaders['content-length'] = bodyBuffer.length;
resHeaders['content-type'] = 'text/html; charset=utf-8';
res.writeHead(response.status, resHeaders);
return res.end(bodyBuffer);
}
if (contentType.includes('text/css')) {
let text = await response.text();
text = rewriteCss(text, targetUrl);
const bodyBuffer = Buffer.from(text, 'utf-8');
clientStats.down += bodyBuffer.length;
resHeaders['content-length'] = bodyBuffer.length;
res.writeHead(response.status, resHeaders);
return res.end(bodyBuffer);
}
res.writeHead(response.status, resHeaders);
if (response.body) {
const stream = Readable.fromWeb(response.body);
stream.on('data', (chunk) => { clientStats.down += chunk.length; });
stream.on('error', () => res.end());
stream.pipe(res);
} else { res.end(); }
} catch (err) {
if (!res.headersSent) {
res.writeHead(502);
res.end(`Erreur de communication.`);
}
}
});
server.on('upgrade', (req, socket, head) => {
let targetUrlStr = null;
if (req.url.startsWith('/p/')) {
const match = req.url.match(/^\/p\/([^/]+)\/([^/]+)(.*)$/);
if (match) targetUrlStr = `${match[1] === 'https' ? 'wss' : 'ws'}://${match[2]}${match[3]}`;
}
if (!targetUrlStr) return socket.end();
const target = new URL(targetUrlStr);
const options = {
hostname: target.hostname,
port: target.port || (target.protocol === 'wss:' ? 443 : 80),
method: req.method,
path: req.url,
headers: { ...req.headers, host: target.host }
};
const proxyReq = http.request(options);
proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => {
socket.write('HTTP/1.1 101 Switching Protocols\r\n');
for (const [key, value] of Object.entries(proxyRes.headers)) socket.write(`${key}: ${value}\r\n`);
socket.write('\r\n');
proxySocket.write(proxyHead);
proxySocket.pipe(socket);
socket.pipe(proxySocket);
});
proxyReq.on('error', () => socket.end());
proxyReq.end();
});
server.listen(PORT, '0.0.0.0', () => console.log(`CyberProxy Pro actif sur le port ${PORT}`));