mx-proxy / server.mjs
TomatitoToho's picture
Initial proxy setup
8ddd60f verified
/**
* 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);