This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
'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);
|
||||
});
|
||||
Reference in New Issue
Block a user