'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 };