feat(xtream-proxy): rewrite proxy logic and update configuration format
Build and Push Docker Images / build-and-push (push) Successful in 39s

Refactor the xtream-proxy from a single-file Express-based server to a
modular Node.js application using native http/https modules. This
change introduces a major version bump to 2.0.0.

- Replace `hosts.txt` with `upstreams.json` for configuration
- Remove heavy dependencies (`express`, `ws`) in favor of native modules
- Implement new environment variables for proxy credentials and timeouts
- Add `src/` directory for modular code structure
- Add `docker-compose.example.yml` for easier deployment
- Update Dockerfile to support the new directory structure and environment
- Improve error handling and probe logic for upstream selection
This commit is contained in:
Bram committed 2026-11-03 22:36:41 +01:00
1 parent baaae2b850
commit 4bd2088edd
18 files changed
+1173 -1223

No files matched your search

+154
View File
@@ -0,0 +1,154 @@
'use strict';
const http = require('http');
const https = require('https');
const { settings } = require('./settings');
const { baseState, nowIso, buildUpstreamUrl } = require('./upstream');
function toInt(value) {
if (value === null || value === undefined || value === '') return null;
const n = Number(value);
return Number.isFinite(n) ? n : null;
}
function interpretProbe(statusCode, body, elapsedMs) {
const state = { ...baseState(), lastChecked: nowIso() };
if (statusCode < 200 || statusCode >= 300) {
state.error = `http_${statusCode}`;
return state;
}
let json;
try {
json = JSON.parse(body);
} catch {
state.error = 'invalid_json';
return state;
}
const info = json && (json.user_info || json.userInfo);
if (!info || typeof info !== 'object') {
state.error = 'no_user_info';
return state;
}
state.auth = info.auth === undefined ? null : info.auth;
state.status = info.status === undefined ? null : info.status;
state.activeCons = toInt(info.active_cons);
state.maxCons = toInt(info.max_connections);
if (state.auth !== null && Number(state.auth) !== 1) {
state.error = 'not_authorized';
return state;
}
if (
state.status !== null &&
String(state.status).toLowerCase() !== 'active'
) {
state.error = `status_${state.status}`;
return state;
}
state.healthy = true;
state.latencyMs = Math.max(0, Math.round(elapsedMs));
state.busy =
state.maxCons !== null &&
state.maxCons > 0 &&
state.activeCons !== null &&
state.activeCons >= state.maxCons;
return state;
}
function probeUpstream(upstream) {
const search = `?${new URLSearchParams({
username: upstream.username,
password: upstream.password,
}).toString()}`;
const target = buildUpstreamUrl(upstream, '/player_api.php', search);
const lib = target.protocol === 'https:' ? https : http;
const started = Date.now();
return new Promise((resolve) => {
let settled = false;
let req = null;
const finish = (state) => {
if (settled) return;
settled = true;
clearTimeout(hardTimeout);
if (req) {
try {
req.destroy();
} catch {}
}
resolve(state);
};
const hardTimeout = setTimeout(
() => finish({ ...baseState(), lastChecked: nowIso(), error: 'timeout' }),
settings.probeTimeoutMs + 2000
);
try {
req = lib.request(
{
protocol: target.protocol,
hostname: target.hostname,
port: target.port || undefined,
path: `${target.pathname}${target.search}`,
method: 'GET',
headers: {
'User-Agent': settings.userAgent,
Accept: 'application/json',
'Accept-Encoding': 'identity',
Connection: 'close',
},
rejectUnauthorized: settings.rejectUnauthorized,
},
(res) => {
let body = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
if (body.length < 2_000_000) body += chunk;
});
res.on('end', () =>
finish(interpretProbe(res.statusCode, body, Date.now() - started))
);
res.on('error', () =>
finish({
...baseState(),
lastChecked: nowIso(),
error: 'response_error',
})
);
}
);
} catch (e) {
finish({
...baseState(),
lastChecked: nowIso(),
error: e.code || 'request_error',
});
return;
}
req.setTimeout(settings.probeTimeoutMs, () => {
try {
req.destroy();
} catch {}
finish({ ...baseState(), lastChecked: nowIso(), error: 'timeout' });
});
req.on('error', (e) =>
finish({
...baseState(),
lastChecked: nowIso(),
error: e.code || 'request_error',
})
);
req.end();
});
}
module.exports = { interpretProbe, probeUpstream };