feat(xtream-proxy): rewrite proxy logic and update configuration format
Build and Push Docker Images / build-and-push (push) Successful in 39s
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:
1 parent
baaae2b850
commit
4bd2088edd
18 files changed
+1173
-1223
No files matched your search
@@ -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 };
|
||||
Reference in new issue
Block a user