Spaces:
Sleeping
Sleeping
| /** | |
| * MX Proxy β HTTP/CONNECT proxy on HuggingFace Spaces | |
| * | |
| * Endpoints: | |
| * GET / β Web UI (proxy browser) | |
| * GET /health β Health check (keeps Space alive) | |
| * GET /stats β Proxy stats | |
| * ANY /proxy β HTTP relay: POST {url, method, headers, body} | |
| * GET /fetch?url= β Simple GET proxy | |
| * CONNECT support β Via HTTP upgrade (if HF reverse proxy allows) | |
| * | |
| * Auth: Simple token via ?token= or Authorization header | |
| */ | |
| import http from 'node:http'; | |
| import https from 'node:https'; | |
| import { URL } from 'node:url'; | |
| import { execSync } from 'node:child_process'; | |
| // ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const PORT = parseInt(process.env.PORT || '7860', 10); | |
| const PROXY_TOKEN = process.env.PROXY_TOKEN || 'mx-proxy-2026'; | |
| const MAX_BODY = 5 * 1024 * 1024; // 5MB max proxied request | |
| // ββ Stats βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let requestCount = 0; | |
| let proxiedCount = 0; | |
| let startTime = Date.now(); | |
| // ββ Auth check ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function checkAuth(req) { | |
| const url = new URL(req.url, `http://${req.headers.host}`); | |
| const tokenParam = url.searchParams.get('token'); | |
| const authHeader = req.headers['authorization']?.replace('Bearer ', ''); | |
| return tokenParam === PROXY_TOKEN || authHeader === PROXY_TOKEN; | |
| } | |
| // ββ Fetch a URL and return the response βββββββββββββββββββββββββββββββββββββ | |
| function fetchUrl(targetUrl, options = {}) { | |
| return new Promise((resolve, reject) => { | |
| const parsedUrl = new URL(targetUrl); | |
| const isHttps = parsedUrl.protocol === 'https:'; | |
| const httpModule = isHttps ? https : http; | |
| const reqOptions = { | |
| hostname: parsedUrl.hostname, | |
| port: parsedUrl.port || (isHttps ? 443 : 80), | |
| path: parsedUrl.pathname + parsedUrl.search, | |
| method: options.method || 'GET', | |
| headers: { | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36', | |
| 'Accept': options.accept || '*/*', | |
| 'Accept-Language': 'es-MX,es;q=0.9,en;q=0.8', | |
| ...options.headers, | |
| }, | |
| timeout: 30000, | |
| // Don't verify SSL for maximum compatibility | |
| rejectUnauthorized: false, | |
| }; | |
| const proxyReq = httpModule.request(reqOptions, (proxyRes) => { | |
| const chunks = []; | |
| proxyRes.on('data', chunk => chunks.push(chunk)); | |
| proxyRes.on('end', () => { | |
| resolve({ | |
| status: proxyRes.statusCode, | |
| headers: proxyRes.headers, | |
| body: Buffer.concat(chunks), | |
| }); | |
| }); | |
| }); | |
| proxyReq.on('error', reject); | |
| proxyReq.on('timeout', () => { | |
| proxyReq.destroy(); | |
| reject(new Error('Target server timeout (30s)')); | |
| }); | |
| if (options.body) { | |
| proxyReq.write(options.body); | |
| } | |
| proxyReq.end(); | |
| }); | |
| } | |
| // ββ HTTP CONNECT tunnel βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function handleConnect(req, socket, head) { | |
| if (!checkAuth(req)) { | |
| socket.write('HTTP/1.1 407 Proxy Auth Required\r\nProxy-Authenticate: Basic realm="proxy"\r\n\r\n'); | |
| socket.destroy(); | |
| return; | |
| } | |
| const [host, port] = req.url.split(':'); | |
| const targetPort = parseInt(port) || 443; | |
| const targetSocket = require('net').connect(targetPort, host, () => { | |
| proxiedCount++; | |
| socket.write('HTTP/1.1 200 Connection Established\r\n\r\n'); | |
| targetSocket.write(head); | |
| targetSocket.pipe(socket); | |
| socket.pipe(targetSocket); | |
| }); | |
| targetSocket.on('error', (err) => { | |
| console.error(`[CONNECT] Error: ${err.message}`); | |
| socket.destroy(); | |
| }); | |
| socket.on('error', () => targetSocket.destroy()); | |
| } | |
| // ββ Web UI HTML βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function getWebUI(req) { | |
| const host = req.headers.host; | |
| const token = PROXY_TOKEN; | |
| const baseUrl = `https://${host}`; | |
| return `<!DOCTYPE html> | |
| <html lang="es"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>MX Proxy</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; background: #0d1117; color: #c9d1d9; min-height: 100vh; } | |
| .container { max-width: 800px; margin: 0 auto; padding: 2rem; } | |
| h1 { color: #58a6ff; margin-bottom: 0.5rem; font-size: 1.8rem; } | |
| .subtitle { color: #8b949e; margin-bottom: 2rem; } | |
| .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; } | |
| .card h2 { color: #58a6ff; font-size: 1.1rem; margin-bottom: 0.8rem; } | |
| label { display: block; color: #8b949e; font-size: 0.85rem; margin-bottom: 0.3rem; margin-top: 0.8rem; } | |
| input, select, textarea { width: 100%; padding: 0.6rem; background: #0d1117; border: 1px solid #30363d; border-radius: 4px; color: #c9d1d9; font-size: 0.95rem; } | |
| input:focus, textarea:focus { border-color: #58a6ff; outline: none; } | |
| button { background: #238636; color: white; border: none; padding: 0.7rem 1.5rem; border-radius: 4px; cursor: pointer; font-size: 1rem; margin-top: 1rem; } | |
| button:hover { background: #2ea043; } | |
| button:disabled { background: #21262d; color: #484f58; cursor: not-allowed; } | |
| .output { background: #0d1117; border: 1px solid #30363d; border-radius: 4px; padding: 1rem; margin-top: 1rem; max-height: 500px; overflow: auto; white-space: pre-wrap; word-break: break-all; font-family: 'Courier New', monospace; font-size: 0.85rem; } | |
| code { background: #1c2128; padding: 0.2rem 0.4rem; border-radius: 3px; font-size: 0.85rem; color: #79c0ff; } | |
| .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } | |
| @media (max-width: 600px) { .grid { grid-template-columns: 1fr; } } | |
| .tag { display: inline-block; background: #1f6feb33; color: #58a6ff; padding: 0.2rem 0.5rem; border-radius: 3px; font-size: 0.75rem; margin-right: 0.3rem; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>π²π½ MX Proxy</h1> | |
| <p class="subtitle">HTTP Proxy via HuggingFace Spaces β IP del servidor (no MX, pero funcional)</p> | |
| <div class="card"> | |
| <h2>π‘ Proxy RΓ‘pido (GET)</h2> | |
| <label>URL a obtener</label> | |
| <input type="text" id="getUrl" placeholder="https://api.ipify.org?format=json" value="https://api.ipify.org?format=json"> | |
| <button onclick="proxyGet()">Obtener via Proxy</button> | |
| <div id="getOutput" class="output" style="display:none"></div> | |
| </div> | |
| <div class="card"> | |
| <h2>π§ Proxy Avanzado (POST)</h2> | |
| <label>URL destino</label> | |
| <input type="text" id="postUrl" placeholder="https://httpbin.org/post"> | |
| <label>MΓ©todo</label> | |
| <select id="postMethod"> | |
| <option>GET</option><option>POST</option><option>PUT</option><option>DELETE</option><option>PATCH</option> | |
| </select> | |
| <label>Headers (JSON)</label> | |
| <textarea id="postHeaders" rows="3" placeholder='{"Content-Type": "application/json"}'></textarea> | |
| <label>Body</label> | |
| <textarea id="postBody" rows="3" placeholder='{"key": "value"}'></textarea> | |
| <button onclick="proxyPost()">Enviar via Proxy</button> | |
| <div id="postOutput" class="output" style="display:none"></div> | |
| </div> | |
| <div class="card"> | |
| <h2>βοΈ ConfiguraciΓ³n para usar como proxy del navegador</h2> | |
| <p style="color:#8b949e;margin-bottom:0.8rem">Usa estos endpoints para enrutar trΓ‘fico desde tu navegador o scripts:</p> | |
| <div class="grid"> | |
| <div> | |
| <label>Endpoint GET</label> | |
| <code>${baseUrl}/fetch?url=TARGET&token=${token}</code> | |
| </div> | |
| <div> | |
| <label>Endpoint POST</label> | |
| <code>${baseUrl}/proxy</code> | |
| </div> | |
| </div> | |
| <p style="margin-top:1rem;color:#8b949e;font-size:0.85rem"> | |
| Ejemplo curl:<br> | |
| <code>curl "${baseUrl}/fetch?url=https://api.ipify.org&token=${token}"</code> | |
| </p> | |
| <p style="margin-top:0.5rem;color:#8b949e;font-size:0.85rem"> | |
| POST ejemplo:<br> | |
| <code>curl -X POST "${baseUrl}/proxy" -H "Content-Type: application/json" -d '{"url":"https://httpbin.org/get","method":"GET","token":"${token}"}'</code> | |
| </p> | |
| </div> | |
| <div class="card"> | |
| <h2>π Estado</h2> | |
| <div id="stats">Cargando...</div> | |
| </div> | |
| </div> | |
| <script> | |
| const TOKEN = '${token}'; | |
| const BASE = '${baseUrl}'; | |
| async function proxyGet() { | |
| const url = document.getElementById('getUrl').value; | |
| const out = document.getElementById('getOutput'); | |
| out.style.display = 'block'; | |
| out.textContent = 'Cargando...'; | |
| try { | |
| const res = await fetch(BASE + '/fetch?url=' + encodeURIComponent(url) + '&token=' + TOKEN); | |
| const ct = res.headers.get('content-type') || ''; | |
| if (ct.includes('json') || ct.includes('text')) { | |
| const text = await res.text(); | |
| try { out.textContent = JSON.stringify(JSON.parse(text), null, 2); } catch { out.textContent = text; } | |
| } else { | |
| out.textContent = 'Respuesta binaria (' + res.headers.get('content-length') + ' bytes, ' + ct + ')'; | |
| } | |
| } catch (e) { out.textContent = 'Error: ' + e.message; } | |
| } | |
| async function proxyPost() { | |
| const url = document.getElementById('postUrl').value; | |
| const method = document.getElementById('postMethod').value; | |
| const headersStr = document.getElementById('postHeaders').value; | |
| const bodyStr = document.getElementById('postBody').value; | |
| const out = document.getElementById('postOutput'); | |
| out.style.display = 'block'; | |
| out.textContent = 'Enviando...'; | |
| try { | |
| let headers = {}; | |
| if (headersStr.trim()) headers = JSON.parse(headersStr); | |
| const res = await fetch(BASE + '/proxy', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + TOKEN }, | |
| body: JSON.stringify({ url, method, headers, body: bodyStr || undefined, token: TOKEN }), | |
| }); | |
| const text = await res.text(); | |
| try { out.textContent = JSON.stringify(JSON.parse(text), null, 2); } catch { out.textContent = text; } | |
| } catch (e) { out.textContent = 'Error: ' + e.message; } | |
| } | |
| async function loadStats() { | |
| try { | |
| const res = await fetch(BASE + '/stats?token=' + TOKEN); | |
| const data = await res.json(); | |
| document.getElementById('stats').innerHTML = | |
| '<p>IP del proxy: <strong>' + (data.proxyIp || 'calculando...') + '</strong></p>' + | |
| '<p>Peticiones proxied: <strong>' + data.proxiedCount + '</strong></p>' + | |
| '<p>Uptime: <strong>' + data.uptime + '</strong></p>'; | |
| } catch {} | |
| } | |
| loadStats(); | |
| setInterval(loadStats, 30000); | |
| </script> | |
| </body> | |
| </html>`; | |
| } | |
| // ββ Main HTTP Server ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const server = http.createServer(async (req, res) => { | |
| requestCount++; | |
| const url = new URL(req.url, `http://${req.headers.host}`); | |
| try { | |
| // Health check β no auth needed | |
| if (url.pathname === '/health') { | |
| res.writeHead(200, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ status: 'alive', uptime: Math.floor((Date.now() - startTime) / 1000) })); | |
| return; | |
| } | |
| // Stats | |
| if (url.pathname === '/stats') { | |
| if (!checkAuth(req)) { res.writeHead(401); res.end('Unauthorized'); return; } | |
| let proxyIp = 'unknown'; | |
| try { proxyIp = await (await fetch('https://api.ipify.org?format=json')).json(); proxyIp = proxyIp.ip; } catch {} | |
| res.writeHead(200, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| proxyIp, | |
| proxiedCount, | |
| requestCount, | |
| uptime: `${Math.floor((Date.now() - startTime) / 60000)}m`, | |
| })); | |
| return; | |
| } | |
| // Web UI | |
| if (url.pathname === '/' || url.pathname === '') { | |
| res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); | |
| res.end(getWebUI(req)); | |
| return; | |
| } | |
| // Simple GET proxy: /fetch?url=TARGET&token=TOKEN | |
| if (url.pathname === '/fetch') { | |
| if (!checkAuth(req)) { res.writeHead(401); res.end('Unauthorized β add ?token=YOUR_TOKEN'); return; } | |
| const targetUrl = url.searchParams.get('url'); | |
| if (!targetUrl) { res.writeHead(400); res.end('Missing ?url= parameter'); return; } | |
| console.log(`[PROXY] GET β ${targetUrl}`); | |
| try { | |
| const result = await fetchUrl(targetUrl, { method: 'GET' }); | |
| proxiedCount++; | |
| const contentType = result.headers['content-type'] || 'application/octet-stream'; | |
| res.writeHead(result.status, { | |
| 'Content-Type': contentType, | |
| 'X-Proxied-By': 'mx-proxy', | |
| 'Access-Control-Allow-Origin': '*', | |
| }); | |
| res.end(result.body); | |
| } catch (err) { | |
| res.writeHead(502, { 'Content-Type': 'text/plain' }); | |
| res.end(`Proxy error: ${err.message}`); | |
| } | |
| return; | |
| } | |
| // Advanced POST proxy: /proxy β body = {url, method, headers, body, token} | |
| if (url.pathname === '/proxy' && req.method === 'POST') { | |
| if (!checkAuth(req)) { res.writeHead(401); res.end('Unauthorized'); return; } | |
| const body = await new Promise((resolve, reject) => { | |
| const chunks = []; | |
| req.on('data', c => chunks.push(c)); | |
| req.on('end', () => resolve(Buffer.concat(chunks))); | |
| req.on('error', reject); | |
| }); | |
| let parsed; | |
| try { parsed = JSON.parse(body.toString()); } catch { | |
| res.writeHead(400); res.end('Invalid JSON body'); return; | |
| } | |
| const { url: targetUrl, method, headers, body: reqBody } = parsed; | |
| if (!targetUrl) { res.writeHead(400); res.end('Missing "url" in body'); return; } | |
| console.log(`[PROXY] ${method || 'GET'} β ${targetUrl}`); | |
| try { | |
| const result = await fetchUrl(targetUrl, { | |
| method: method || 'GET', | |
| headers: headers || {}, | |
| body: reqBody || undefined, | |
| }); | |
| proxiedCount++; | |
| // Return structured response | |
| const contentType = result.headers['content-type'] || ''; | |
| const isBinary = /image|video|audio|octet-stream|zip|pdf/.test(contentType); | |
| if (isBinary) { | |
| res.writeHead(200, { | |
| 'Content-Type': 'application/json', | |
| 'Access-Control-Allow-Origin': '*', | |
| }); | |
| res.end(JSON.stringify({ | |
| status: result.status, | |
| headers: result.headers, | |
| bodyBase64: result.body.toString('base64'), | |
| bodySize: result.body.length, | |
| binary: true, | |
| })); | |
| } else { | |
| const text = result.body.toString('utf-8'); | |
| res.writeHead(200, { | |
| 'Content-Type': 'application/json', | |
| 'Access-Control-Allow-Origin': '*', | |
| }); | |
| let parsedBody = text; | |
| try { parsedBody = JSON.parse(text); } catch {} | |
| res.end(JSON.stringify({ | |
| status: result.status, | |
| headers: result.headers, | |
| body: parsedBody, | |
| bodySize: result.body.length, | |
| })); | |
| } | |
| } catch (err) { | |
| res.writeHead(502, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ error: err.message })); | |
| } | |
| return; | |
| } | |
| // CORS preflight | |
| if (req.method === 'OPTIONS') { | |
| res.writeHead(204, { | |
| 'Access-Control-Allow-Origin': '*', | |
| 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', | |
| 'Access-Control-Allow-Headers': 'Content-Type, Authorization', | |
| 'Access-Control-Max-Age': '86400', | |
| }); | |
| res.end(); | |
| return; | |
| } | |
| // 404 | |
| res.writeHead(404, { 'Content-Type': 'text/plain' }); | |
| res.end('Not found. Endpoints: / /health /stats /fetch?url= /proxy'); | |
| } catch (err) { | |
| console.error('[Server] Error:', err.message); | |
| res.writeHead(500, { 'Content-Type': 'text/plain' }); | |
| res.end('Server error: ' + err.message); | |
| } | |
| }); | |
| // ββ CONNECT support for HTTP tunneling ββββββββββββββββββββββββββββββββββββββ | |
| server.on('connect', handleConnect); | |
| // ββ Start βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| server.listen(PORT, '0.0.0.0', () => { | |
| console.log(`[MX-Proxy] Listening on 0.0.0.0:${PORT}`); | |
| console.log(`[MX-Proxy] Token: ${PROXY_TOKEN}`); | |
| console.log(`[MX-Proxy] Endpoints: / /health /stats /fetch?url= /proxy`); | |
| // Log the public IP | |
| try { | |
| const ip = execSync('curl -s --max-time 5 https://api.ipify.org').toString().trim(); | |
| console.log(`[MX-Proxy] Public IP: ${ip}`); | |
| } catch { console.log('[MX-Proxy] Could not determine public IP'); } | |
| }); | |
| // Keepalive | |
| setInterval(() => { | |
| http.get(`http://127.0.0.1:${PORT}/health`, () => {}).on('error', () => {}); | |
| }, 5 * 60 * 1000); | |
| // Memory log | |
| setInterval(() => { | |
| const mem = process.memoryUsage(); | |
| console.log(`[KeepAlive] RSS: ${(mem.rss/1024/1024).toFixed(1)}MB | Heap: ${(mem.heapUsed/1024/1024).toFixed(1)}MB | Uptime: ${Math.floor(process.uptime())}s | Proxied: ${proxiedCount}`); | |
| }, 60000); | |