mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 06:06:18 -08:00
104 lines
3.8 KiB
TypeScript
104 lines
3.8 KiB
TypeScript
import { normalizeXtreamServerUrl } from './xtream-portal.utils';
|
|
|
|
export interface XtreamConnectionFailure {
|
|
kind: 'http' | 'tls' | 'connection' | 'unknown';
|
|
status?: number;
|
|
canTryHttp: boolean;
|
|
}
|
|
|
|
interface TransportFailureLike {
|
|
code?: unknown;
|
|
status?: unknown;
|
|
message?: unknown;
|
|
response?: { status?: unknown };
|
|
errors?: unknown;
|
|
cause?: unknown;
|
|
}
|
|
|
|
const unknownFailure = (): XtreamConnectionFailure => ({
|
|
kind: 'unknown',
|
|
canTryHttp: false,
|
|
});
|
|
const TLS_VERIFY_CODES = new Set([
|
|
'EPROTO',
|
|
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
|
|
'INVALID_CA',
|
|
'PATH_LENGTH_EXCEEDED',
|
|
'HOSTNAME_MISMATCH',
|
|
'INVALID_PURPOSE',
|
|
]);
|
|
|
|
/** Small, credential-free evidence produced by the transport, never by a panel. */
|
|
export function describeXtreamConnectionFailure(
|
|
error: unknown,
|
|
initialResponded: boolean
|
|
): XtreamConnectionFailure {
|
|
const active = new WeakSet<object>();
|
|
const memo = new WeakMap<object, XtreamConnectionFailure>();
|
|
let remaining = 64;
|
|
const visit = (value: unknown): XtreamConnectionFailure => {
|
|
if (!value || typeof value !== 'object') return unknownFailure();
|
|
const cached = memo.get(value);
|
|
if (cached) return cached;
|
|
if (active.has(value) || --remaining < 0) return unknownFailure();
|
|
active.add(value);
|
|
const candidate = value as TransportFailureLike;
|
|
const own = describeSingleFailure(candidate);
|
|
const evidence: XtreamConnectionFailure[] = own ? [own] : [];
|
|
// Node may summarize mixed IPv4/IPv6 failures with the first error's
|
|
// code. Require positive evidence from every address and nested cause.
|
|
if (candidate.errors !== undefined) {
|
|
if (Array.isArray(candidate.errors) && candidate.errors.length) {
|
|
if (candidate.errors.length > 64)
|
|
evidence.push(unknownFailure());
|
|
evidence.push(...candidate.errors.slice(0, 64).map(visit));
|
|
} else evidence.push(unknownFailure());
|
|
}
|
|
if (candidate.cause !== undefined)
|
|
evidence.push(visit(candidate.cause));
|
|
const detail =
|
|
evidence.find((e) => e.kind === 'http') ??
|
|
evidence.find((e) => e.kind === 'tls') ??
|
|
evidence.find((e) => e.kind === 'connection') ??
|
|
unknownFailure();
|
|
const result = {
|
|
...detail,
|
|
canTryHttp:
|
|
evidence.length > 0 && evidence.every((e) => e.canTryHttp),
|
|
};
|
|
active.delete(value);
|
|
memo.set(value, result);
|
|
return result;
|
|
};
|
|
const result = visit(error);
|
|
return { ...result, canTryHttp: !initialResponded && result.canTryHttp };
|
|
}
|
|
|
|
function describeSingleFailure(
|
|
candidate: TransportFailureLike
|
|
): XtreamConnectionFailure | null {
|
|
const status = candidate.response?.status ?? candidate.status;
|
|
if (typeof status === 'number' && status >= 100 && status <= 599)
|
|
return { kind: 'http', status, canTryHttp: false };
|
|
const code = typeof candidate.code === 'string' ? candidate.code : '';
|
|
if (!code) return null;
|
|
const wrongVersion =
|
|
code === 'ERR_SSL_WRONG_VERSION_NUMBER' ||
|
|
(code === 'EPROTO' &&
|
|
typeof candidate.message === 'string' &&
|
|
/wrong[ _]version[ _]number/i.test(candidate.message));
|
|
const tls = /CERT|TLS|SSL|CRL/.test(code) || TLS_VERIFY_CODES.has(code);
|
|
return {
|
|
kind: tls ? 'tls' : 'connection',
|
|
canTryHttp: code === 'ECONNREFUSED' || wrongVersion,
|
|
};
|
|
}
|
|
|
|
export function xtreamHttpAlternative(serverUrl: string): string | null {
|
|
const url = new URL(normalizeXtreamServerUrl(serverUrl));
|
|
if (url.protocol !== 'https:') return null;
|
|
// URL normalizes an explicit :443 away. Nonstandard ports stay explicit.
|
|
url.protocol = 'http:';
|
|
return normalizeXtreamServerUrl(url.href);
|
|
}
|