| 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); |
|
|
| |
| 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) |
| })); |
| } |
|
|
| |
| 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}`)); |