/**
* 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 `
MX Proxy
🇲🇽 MX Proxy
HTTP Proxy via HuggingFace Spaces — IP del servidor (no MX, pero funcional)
⚙️ Configuración para usar como proxy del navegador
Usa estos endpoints para enrutar tráfico desde tu navegador o scripts:
${baseUrl}/fetch?url=TARGET&token=${token}
${baseUrl}/proxy
Ejemplo curl:
curl "${baseUrl}/fetch?url=https://api.ipify.org&token=${token}"
POST ejemplo:
curl -X POST "${baseUrl}/proxy" -H "Content-Type: application/json" -d '{"url":"https://httpbin.org/get","method":"GET","token":"${token}"}'
`;
}
// ── 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);