TomatitoToho commited on
Commit
8ddd60f
Β·
verified Β·
1 Parent(s): 146b7f9

Initial proxy setup

Browse files
Files changed (5) hide show
  1. .gitignore +2 -0
  2. Dockerfile +20 -0
  3. README.md +5 -6
  4. package.json +9 -0
  5. server.mjs +435 -0
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ node_modules/
2
+ .git/
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-slim
2
+
3
+ RUN apt-get update && apt-get install -y --no-install-recommends \
4
+ curl wget ca-certificates \
5
+ && rm -rf /var/lib/apt/lists/*
6
+
7
+ WORKDIR /app
8
+
9
+ COPY package.json ./
10
+ RUN npm install --production
11
+
12
+ COPY . .
13
+
14
+ ENV PORT=7860
15
+ EXPOSE 7860
16
+
17
+ HEALTHCHECK --interval=60s --timeout=10s --retries=3 \
18
+ CMD curl -f http://localhost:7860/health || exit 1
19
+
20
+ CMD ["node", "server.mjs"]
README.md CHANGED
@@ -1,10 +1,9 @@
1
  ---
2
- title: Mx Proxy
3
- emoji: 🐒
4
- colorFrom: red
5
- colorTo: gray
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: MX Proxy
3
+ emoji: 🌐
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  ---
 
 
package.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "mx-proxy",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "server.mjs",
6
+ "dependencies": {
7
+ "socks5": "^1.0.1"
8
+ }
9
+ }
server.mjs ADDED
@@ -0,0 +1,435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * MX Proxy β€” HTTP/CONNECT proxy on HuggingFace Spaces
3
+ *
4
+ * Endpoints:
5
+ * GET / β†’ Web UI (proxy browser)
6
+ * GET /health β†’ Health check (keeps Space alive)
7
+ * GET /stats β†’ Proxy stats
8
+ * ANY /proxy β†’ HTTP relay: POST {url, method, headers, body}
9
+ * GET /fetch?url= β†’ Simple GET proxy
10
+ * CONNECT support β†’ Via HTTP upgrade (if HF reverse proxy allows)
11
+ *
12
+ * Auth: Simple token via ?token= or Authorization header
13
+ */
14
+
15
+ import http from 'node:http';
16
+ import https from 'node:https';
17
+ import { URL } from 'node:url';
18
+ import { execSync } from 'node:child_process';
19
+
20
+ // ── Config ──────────────────────────────────────────────────────────────────
21
+ const PORT = parseInt(process.env.PORT || '7860', 10);
22
+ const PROXY_TOKEN = process.env.PROXY_TOKEN || 'mx-proxy-2026';
23
+ const MAX_BODY = 5 * 1024 * 1024; // 5MB max proxied request
24
+
25
+ // ── Stats ───────────────────────────────────────────────────────────────────
26
+ let requestCount = 0;
27
+ let proxiedCount = 0;
28
+ let startTime = Date.now();
29
+
30
+ // ── Auth check ──────────────────────────────────────────────────────────────
31
+ function checkAuth(req) {
32
+ const url = new URL(req.url, `http://${req.headers.host}`);
33
+ const tokenParam = url.searchParams.get('token');
34
+ const authHeader = req.headers['authorization']?.replace('Bearer ', '');
35
+ return tokenParam === PROXY_TOKEN || authHeader === PROXY_TOKEN;
36
+ }
37
+
38
+ // ── Fetch a URL and return the response ─────────────────────────────────────
39
+ function fetchUrl(targetUrl, options = {}) {
40
+ return new Promise((resolve, reject) => {
41
+ const parsedUrl = new URL(targetUrl);
42
+ const isHttps = parsedUrl.protocol === 'https:';
43
+ const httpModule = isHttps ? https : http;
44
+
45
+ const reqOptions = {
46
+ hostname: parsedUrl.hostname,
47
+ port: parsedUrl.port || (isHttps ? 443 : 80),
48
+ path: parsedUrl.pathname + parsedUrl.search,
49
+ method: options.method || 'GET',
50
+ headers: {
51
+ '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',
52
+ 'Accept': options.accept || '*/*',
53
+ 'Accept-Language': 'es-MX,es;q=0.9,en;q=0.8',
54
+ ...options.headers,
55
+ },
56
+ timeout: 30000,
57
+ // Don't verify SSL for maximum compatibility
58
+ rejectUnauthorized: false,
59
+ };
60
+
61
+ const proxyReq = httpModule.request(reqOptions, (proxyRes) => {
62
+ const chunks = [];
63
+ proxyRes.on('data', chunk => chunks.push(chunk));
64
+ proxyRes.on('end', () => {
65
+ resolve({
66
+ status: proxyRes.statusCode,
67
+ headers: proxyRes.headers,
68
+ body: Buffer.concat(chunks),
69
+ });
70
+ });
71
+ });
72
+
73
+ proxyReq.on('error', reject);
74
+ proxyReq.on('timeout', () => {
75
+ proxyReq.destroy();
76
+ reject(new Error('Target server timeout (30s)'));
77
+ });
78
+
79
+ if (options.body) {
80
+ proxyReq.write(options.body);
81
+ }
82
+ proxyReq.end();
83
+ });
84
+ }
85
+
86
+ // ── HTTP CONNECT tunnel ─────────────────────────────────────────────────────
87
+ function handleConnect(req, socket, head) {
88
+ if (!checkAuth(req)) {
89
+ socket.write('HTTP/1.1 407 Proxy Auth Required\r\nProxy-Authenticate: Basic realm="proxy"\r\n\r\n');
90
+ socket.destroy();
91
+ return;
92
+ }
93
+
94
+ const [host, port] = req.url.split(':');
95
+ const targetPort = parseInt(port) || 443;
96
+
97
+ const targetSocket = require('net').connect(targetPort, host, () => {
98
+ proxiedCount++;
99
+ socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
100
+ targetSocket.write(head);
101
+ targetSocket.pipe(socket);
102
+ socket.pipe(targetSocket);
103
+ });
104
+
105
+ targetSocket.on('error', (err) => {
106
+ console.error(`[CONNECT] Error: ${err.message}`);
107
+ socket.destroy();
108
+ });
109
+
110
+ socket.on('error', () => targetSocket.destroy());
111
+ }
112
+
113
+ // ── Web UI HTML ─────────────────────────────────────────────────────────────
114
+ function getWebUI(req) {
115
+ const host = req.headers.host;
116
+ const token = PROXY_TOKEN;
117
+ const baseUrl = `https://${host}`;
118
+
119
+ return `<!DOCTYPE html>
120
+ <html lang="es">
121
+ <head>
122
+ <meta charset="UTF-8">
123
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
124
+ <title>MX Proxy</title>
125
+ <style>
126
+ * { margin: 0; padding: 0; box-sizing: border-box; }
127
+ body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; background: #0d1117; color: #c9d1d9; min-height: 100vh; }
128
+ .container { max-width: 800px; margin: 0 auto; padding: 2rem; }
129
+ h1 { color: #58a6ff; margin-bottom: 0.5rem; font-size: 1.8rem; }
130
+ .subtitle { color: #8b949e; margin-bottom: 2rem; }
131
+ .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; }
132
+ .card h2 { color: #58a6ff; font-size: 1.1rem; margin-bottom: 0.8rem; }
133
+ label { display: block; color: #8b949e; font-size: 0.85rem; margin-bottom: 0.3rem; margin-top: 0.8rem; }
134
+ input, select, textarea { width: 100%; padding: 0.6rem; background: #0d1117; border: 1px solid #30363d; border-radius: 4px; color: #c9d1d9; font-size: 0.95rem; }
135
+ input:focus, textarea:focus { border-color: #58a6ff; outline: none; }
136
+ button { background: #238636; color: white; border: none; padding: 0.7rem 1.5rem; border-radius: 4px; cursor: pointer; font-size: 1rem; margin-top: 1rem; }
137
+ button:hover { background: #2ea043; }
138
+ button:disabled { background: #21262d; color: #484f58; cursor: not-allowed; }
139
+ .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; }
140
+ code { background: #1c2128; padding: 0.2rem 0.4rem; border-radius: 3px; font-size: 0.85rem; color: #79c0ff; }
141
+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
142
+ @media (max-width: 600px) { .grid { grid-template-columns: 1fr; } }
143
+ .tag { display: inline-block; background: #1f6feb33; color: #58a6ff; padding: 0.2rem 0.5rem; border-radius: 3px; font-size: 0.75rem; margin-right: 0.3rem; }
144
+ </style>
145
+ </head>
146
+ <body>
147
+ <div class="container">
148
+ <h1>πŸ‡²πŸ‡½ MX Proxy</h1>
149
+ <p class="subtitle">HTTP Proxy via HuggingFace Spaces β€” IP del servidor (no MX, pero funcional)</p>
150
+
151
+ <div class="card">
152
+ <h2>πŸ“‘ Proxy RΓ‘pido (GET)</h2>
153
+ <label>URL a obtener</label>
154
+ <input type="text" id="getUrl" placeholder="https://api.ipify.org?format=json" value="https://api.ipify.org?format=json">
155
+ <button onclick="proxyGet()">Obtener via Proxy</button>
156
+ <div id="getOutput" class="output" style="display:none"></div>
157
+ </div>
158
+
159
+ <div class="card">
160
+ <h2>πŸ”§ Proxy Avanzado (POST)</h2>
161
+ <label>URL destino</label>
162
+ <input type="text" id="postUrl" placeholder="https://httpbin.org/post">
163
+ <label>MΓ©todo</label>
164
+ <select id="postMethod">
165
+ <option>GET</option><option>POST</option><option>PUT</option><option>DELETE</option><option>PATCH</option>
166
+ </select>
167
+ <label>Headers (JSON)</label>
168
+ <textarea id="postHeaders" rows="3" placeholder='{"Content-Type": "application/json"}'></textarea>
169
+ <label>Body</label>
170
+ <textarea id="postBody" rows="3" placeholder='{"key": "value"}'></textarea>
171
+ <button onclick="proxyPost()">Enviar via Proxy</button>
172
+ <div id="postOutput" class="output" style="display:none"></div>
173
+ </div>
174
+
175
+ <div class="card">
176
+ <h2>βš™οΈ ConfiguraciΓ³n para usar como proxy del navegador</h2>
177
+ <p style="color:#8b949e;margin-bottom:0.8rem">Usa estos endpoints para enrutar trΓ‘fico desde tu navegador o scripts:</p>
178
+ <div class="grid">
179
+ <div>
180
+ <label>Endpoint GET</label>
181
+ <code>${baseUrl}/fetch?url=TARGET&token=${token}</code>
182
+ </div>
183
+ <div>
184
+ <label>Endpoint POST</label>
185
+ <code>${baseUrl}/proxy</code>
186
+ </div>
187
+ </div>
188
+ <p style="margin-top:1rem;color:#8b949e;font-size:0.85rem">
189
+ Ejemplo curl:<br>
190
+ <code>curl "${baseUrl}/fetch?url=https://api.ipify.org&token=${token}"</code>
191
+ </p>
192
+ <p style="margin-top:0.5rem;color:#8b949e;font-size:0.85rem">
193
+ POST ejemplo:<br>
194
+ <code>curl -X POST "${baseUrl}/proxy" -H "Content-Type: application/json" -d '{"url":"https://httpbin.org/get","method":"GET","token":"${token}"}'</code>
195
+ </p>
196
+ </div>
197
+
198
+ <div class="card">
199
+ <h2>πŸ“Š Estado</h2>
200
+ <div id="stats">Cargando...</div>
201
+ </div>
202
+ </div>
203
+
204
+ <script>
205
+ const TOKEN = '${token}';
206
+ const BASE = '${baseUrl}';
207
+
208
+ async function proxyGet() {
209
+ const url = document.getElementById('getUrl').value;
210
+ const out = document.getElementById('getOutput');
211
+ out.style.display = 'block';
212
+ out.textContent = 'Cargando...';
213
+ try {
214
+ const res = await fetch(BASE + '/fetch?url=' + encodeURIComponent(url) + '&token=' + TOKEN);
215
+ const ct = res.headers.get('content-type') || '';
216
+ if (ct.includes('json') || ct.includes('text')) {
217
+ const text = await res.text();
218
+ try { out.textContent = JSON.stringify(JSON.parse(text), null, 2); } catch { out.textContent = text; }
219
+ } else {
220
+ out.textContent = 'Respuesta binaria (' + res.headers.get('content-length') + ' bytes, ' + ct + ')';
221
+ }
222
+ } catch (e) { out.textContent = 'Error: ' + e.message; }
223
+ }
224
+
225
+ async function proxyPost() {
226
+ const url = document.getElementById('postUrl').value;
227
+ const method = document.getElementById('postMethod').value;
228
+ const headersStr = document.getElementById('postHeaders').value;
229
+ const bodyStr = document.getElementById('postBody').value;
230
+ const out = document.getElementById('postOutput');
231
+ out.style.display = 'block';
232
+ out.textContent = 'Enviando...';
233
+ try {
234
+ let headers = {};
235
+ if (headersStr.trim()) headers = JSON.parse(headersStr);
236
+ const res = await fetch(BASE + '/proxy', {
237
+ method: 'POST',
238
+ headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + TOKEN },
239
+ body: JSON.stringify({ url, method, headers, body: bodyStr || undefined, token: TOKEN }),
240
+ });
241
+ const text = await res.text();
242
+ try { out.textContent = JSON.stringify(JSON.parse(text), null, 2); } catch { out.textContent = text; }
243
+ } catch (e) { out.textContent = 'Error: ' + e.message; }
244
+ }
245
+
246
+ async function loadStats() {
247
+ try {
248
+ const res = await fetch(BASE + '/stats?token=' + TOKEN);
249
+ const data = await res.json();
250
+ document.getElementById('stats').innerHTML =
251
+ '<p>IP del proxy: <strong>' + (data.proxyIp || 'calculando...') + '</strong></p>' +
252
+ '<p>Peticiones proxied: <strong>' + data.proxiedCount + '</strong></p>' +
253
+ '<p>Uptime: <strong>' + data.uptime + '</strong></p>';
254
+ } catch {}
255
+ }
256
+ loadStats();
257
+ setInterval(loadStats, 30000);
258
+ </script>
259
+ </body>
260
+ </html>`;
261
+ }
262
+
263
+ // ── Main HTTP Server ────────────────────────────────────────────────────────
264
+ const server = http.createServer(async (req, res) => {
265
+ requestCount++;
266
+ const url = new URL(req.url, `http://${req.headers.host}`);
267
+
268
+ try {
269
+ // Health check β€” no auth needed
270
+ if (url.pathname === '/health') {
271
+ res.writeHead(200, { 'Content-Type': 'application/json' });
272
+ res.end(JSON.stringify({ status: 'alive', uptime: Math.floor((Date.now() - startTime) / 1000) }));
273
+ return;
274
+ }
275
+
276
+ // Stats
277
+ if (url.pathname === '/stats') {
278
+ if (!checkAuth(req)) { res.writeHead(401); res.end('Unauthorized'); return; }
279
+ let proxyIp = 'unknown';
280
+ try { proxyIp = await (await fetch('https://api.ipify.org?format=json')).json(); proxyIp = proxyIp.ip; } catch {}
281
+ res.writeHead(200, { 'Content-Type': 'application/json' });
282
+ res.end(JSON.stringify({
283
+ proxyIp,
284
+ proxiedCount,
285
+ requestCount,
286
+ uptime: `${Math.floor((Date.now() - startTime) / 60000)}m`,
287
+ }));
288
+ return;
289
+ }
290
+
291
+ // Web UI
292
+ if (url.pathname === '/' || url.pathname === '') {
293
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
294
+ res.end(getWebUI(req));
295
+ return;
296
+ }
297
+
298
+ // Simple GET proxy: /fetch?url=TARGET&token=TOKEN
299
+ if (url.pathname === '/fetch') {
300
+ if (!checkAuth(req)) { res.writeHead(401); res.end('Unauthorized β€” add ?token=YOUR_TOKEN'); return; }
301
+ const targetUrl = url.searchParams.get('url');
302
+ if (!targetUrl) { res.writeHead(400); res.end('Missing ?url= parameter'); return; }
303
+
304
+ console.log(`[PROXY] GET β†’ ${targetUrl}`);
305
+ try {
306
+ const result = await fetchUrl(targetUrl, { method: 'GET' });
307
+ proxiedCount++;
308
+ const contentType = result.headers['content-type'] || 'application/octet-stream';
309
+ res.writeHead(result.status, {
310
+ 'Content-Type': contentType,
311
+ 'X-Proxied-By': 'mx-proxy',
312
+ 'Access-Control-Allow-Origin': '*',
313
+ });
314
+ res.end(result.body);
315
+ } catch (err) {
316
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
317
+ res.end(`Proxy error: ${err.message}`);
318
+ }
319
+ return;
320
+ }
321
+
322
+ // Advanced POST proxy: /proxy β€” body = {url, method, headers, body, token}
323
+ if (url.pathname === '/proxy' && req.method === 'POST') {
324
+ if (!checkAuth(req)) { res.writeHead(401); res.end('Unauthorized'); return; }
325
+
326
+ const body = await new Promise((resolve, reject) => {
327
+ const chunks = [];
328
+ req.on('data', c => chunks.push(c));
329
+ req.on('end', () => resolve(Buffer.concat(chunks)));
330
+ req.on('error', reject);
331
+ });
332
+
333
+ let parsed;
334
+ try { parsed = JSON.parse(body.toString()); } catch {
335
+ res.writeHead(400); res.end('Invalid JSON body'); return;
336
+ }
337
+
338
+ const { url: targetUrl, method, headers, body: reqBody } = parsed;
339
+ if (!targetUrl) { res.writeHead(400); res.end('Missing "url" in body'); return; }
340
+
341
+ console.log(`[PROXY] ${method || 'GET'} β†’ ${targetUrl}`);
342
+ try {
343
+ const result = await fetchUrl(targetUrl, {
344
+ method: method || 'GET',
345
+ headers: headers || {},
346
+ body: reqBody || undefined,
347
+ });
348
+ proxiedCount++;
349
+
350
+ // Return structured response
351
+ const contentType = result.headers['content-type'] || '';
352
+ const isBinary = /image|video|audio|octet-stream|zip|pdf/.test(contentType);
353
+
354
+ if (isBinary) {
355
+ res.writeHead(200, {
356
+ 'Content-Type': 'application/json',
357
+ 'Access-Control-Allow-Origin': '*',
358
+ });
359
+ res.end(JSON.stringify({
360
+ status: result.status,
361
+ headers: result.headers,
362
+ bodyBase64: result.body.toString('base64'),
363
+ bodySize: result.body.length,
364
+ binary: true,
365
+ }));
366
+ } else {
367
+ const text = result.body.toString('utf-8');
368
+ res.writeHead(200, {
369
+ 'Content-Type': 'application/json',
370
+ 'Access-Control-Allow-Origin': '*',
371
+ });
372
+ let parsedBody = text;
373
+ try { parsedBody = JSON.parse(text); } catch {}
374
+ res.end(JSON.stringify({
375
+ status: result.status,
376
+ headers: result.headers,
377
+ body: parsedBody,
378
+ bodySize: result.body.length,
379
+ }));
380
+ }
381
+ } catch (err) {
382
+ res.writeHead(502, { 'Content-Type': 'application/json' });
383
+ res.end(JSON.stringify({ error: err.message }));
384
+ }
385
+ return;
386
+ }
387
+
388
+ // CORS preflight
389
+ if (req.method === 'OPTIONS') {
390
+ res.writeHead(204, {
391
+ 'Access-Control-Allow-Origin': '*',
392
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
393
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
394
+ 'Access-Control-Max-Age': '86400',
395
+ });
396
+ res.end();
397
+ return;
398
+ }
399
+
400
+ // 404
401
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
402
+ res.end('Not found. Endpoints: / /health /stats /fetch?url= /proxy');
403
+
404
+ } catch (err) {
405
+ console.error('[Server] Error:', err.message);
406
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
407
+ res.end('Server error: ' + err.message);
408
+ }
409
+ });
410
+
411
+ // ── CONNECT support for HTTP tunneling ──────────────────────────────────────
412
+ server.on('connect', handleConnect);
413
+
414
+ // ── Start ───────────────────────────────────────────────────────────────────
415
+ server.listen(PORT, '0.0.0.0', () => {
416
+ console.log(`[MX-Proxy] Listening on 0.0.0.0:${PORT}`);
417
+ console.log(`[MX-Proxy] Token: ${PROXY_TOKEN}`);
418
+ console.log(`[MX-Proxy] Endpoints: / /health /stats /fetch?url= /proxy`);
419
+ // Log the public IP
420
+ try {
421
+ const ip = execSync('curl -s --max-time 5 https://api.ipify.org').toString().trim();
422
+ console.log(`[MX-Proxy] Public IP: ${ip}`);
423
+ } catch { console.log('[MX-Proxy] Could not determine public IP'); }
424
+ });
425
+
426
+ // Keepalive
427
+ setInterval(() => {
428
+ http.get(`http://127.0.0.1:${PORT}/health`, () => {}).on('error', () => {});
429
+ }, 5 * 60 * 1000);
430
+
431
+ // Memory log
432
+ setInterval(() => {
433
+ const mem = process.memoryUsage();
434
+ 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}`);
435
+ }, 60000);