'use strict'; const fs = require('fs'); const http = require('http'); const https = require('https'); const express = require('express'); const httpProxy = require('http-proxy'); const LISTEN_PORT = Number(process.env.LISTEN_PORT || 8080); const HOSTS_FILE = process.env.HOSTS_FILE || '/config/hosts.txt'; const CHECK_INTERVAL_SEC = Math.max(5, Number(process.env.CHECK_INTERVAL_SEC || 30)); const PING_TIMEOUT_MS = Math.max(1000, Number(process.env.PING_TIMEOUT_MS || 8000)); const HEALTH_PATH = process.env.HEALTH_PATH || '/'; let currentTarget = null; let lastStats = { checkedAt: null, results: [], selected: null }; const proxy = httpProxy.createProxyServer({ ws: true, xfwd: true, proxyTimeout: 0, }); proxy.on('error', (err, req, res) => { if (res && !res.headersSent) { res.writeHead(502, { 'Content-Type': 'text/plain' }); res.end(`Upstream error: ${err.message}`); } }); function readHostsFile() { if (!fs.existsSync(HOSTS_FILE)) { console.error(`Hosts file not found: ${HOSTS_FILE}`); return []; } const raw = fs.readFileSync(HOSTS_FILE, 'utf8'); const lines = raw.split(/\r?\n/); const urls = []; for (const line of lines) { const t = line.trim(); if (!t || t.startsWith('#')) continue; try { const u = new URL(t); if (u.protocol !== 'http:' && u.protocol !== 'https:') continue; const base = `${u.protocol}//${u.host}`; urls.push(base); } catch { console.warn(`Skipping invalid URL line: ${t}`); } } return [...new Set(urls)]; } function pingBase(base, path) { return new Promise((resolve) => { let url; try { const p = path.startsWith('/') ? path : `/${path}`; url = new URL(p, base.endsWith('/') ? base : `${base}/`); } catch { resolve({ base, ok: false, ms: null, error: 'bad_url' }); return; } const lib = url.protocol === 'https:' ? https : http; const start = Date.now(); const req = lib.request( { protocol: url.protocol, hostname: url.hostname, port: url.port || (url.protocol === 'https:' ? 443 : 80), path: url.pathname + url.search, method: 'GET', timeout: PING_TIMEOUT_MS, headers: { 'User-Agent': 'xtream-proxy-health/1.0', Connection: 'close', }, }, (res) => { res.resume(); res.on('end', () => { const ms = Date.now() - start; const ok = res.statusCode >= 200 && res.statusCode < 500; resolve({ base, ok, ms, status: res.statusCode }); }); res.on('error', () => { resolve({ base, ok: false, ms: null, error: 'response_error' }); }); } ); req.on('error', (e) => { resolve({ base, ok: false, ms: null, error: e.code || e.message }); }); req.on('timeout', () => { req.destroy(); resolve({ base, ok: false, ms: null, error: 'timeout' }); }); req.end(); }); } async function pickFastestHost() { const hosts = readHostsFile(); if (hosts.length === 0) { console.warn('No valid hosts in file; keeping previous target if any.'); lastStats = { checkedAt: new Date().toISOString(), results: [], selected: currentTarget, }; return; } const pings = await Promise.all(hosts.map((h) => pingBase(h, HEALTH_PATH))); const ok = pings.filter((p) => p.ok && typeof p.ms === 'number'); ok.sort((a, b) => a.ms - b.ms); lastStats = { checkedAt: new Date().toISOString(), results: pings, selected: ok[0]?.base ?? currentTarget, }; if (ok[0]) { if (currentTarget !== ok[0].base) { console.log( `Selected upstream ${ok[0].base} (${ok[0].ms} ms). Others: ${ok .slice(1) .map((x) => `${x.base}=${x.ms}ms`) .join(', ') || 'none'}` ); } currentTarget = ok[0].base; } else { console.warn( 'No host responded successfully; keeping previous target:', currentTarget || '(none)' ); } } function scheduleChecks() { setInterval(() => { pickFastestHost().catch((e) => console.error('Health check error:', e)); }, CHECK_INTERVAL_SEC * 1000); } const app = express(); app.get('/health', (_req, res) => { res.json({ ok: Boolean(currentTarget), upstream: currentTarget, intervalSec: CHECK_INTERVAL_SEC, lastCheck: lastStats, }); }); app.use((req, res) => { if (!currentTarget) { res.status(503).type('text/plain').send('No upstream available yet (hosts file empty or all pings failed).'); return; } proxy.web(req, res, { target: currentTarget, changeOrigin: true, secure: process.env.PROXY_INSECURE_TLS !== '1', }); }); const server = http.createServer(app); server.on('upgrade', (req, socket, head) => { if (!currentTarget) { socket.destroy(); return; } proxy.ws(req, socket, head, { target: currentTarget, changeOrigin: true, secure: process.env.PROXY_INSECURE_TLS !== '1', }); }); async function main() { await pickFastestHost(); server.listen(LISTEN_PORT, () => { console.log(`Xtream proxy listening on :${LISTEN_PORT}`); console.log(`Hosts file: ${HOSTS_FILE}, interval: ${CHECK_INTERVAL_SEC}s, health path: ${HEALTH_PATH}`); scheduleChecks(); }); } main().catch((e) => { console.error(e); process.exit(1); });