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
155 lines
3.9 KiB
JavaScript
155 lines
3.9 KiB
JavaScript
'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 };
|