xtream proxy
Build and Push Docker Images / build-and-push (push) Failing after 20s

This commit is contained in:
2026-04-09 21:06:18 +02:00
parent 57e67d2366
commit 630ae66db1
7 changed files with 1120 additions and 0 deletions
+198
View File
@@ -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);
});