export const CONNECTIVITY_IP_SOURCES = Object.freeze([ { id: 'cloudflare', label: 'Cloudflare', family: 4, url: 'https://www.cloudflare.com/cdn-cgi/trace' }, { id: 'ipify', label: 'ipify', family: 4, url: 'https://api.ipify.org' }, { id: 'aws', label: 'AWS', family: 4, url: 'https://checkip.amazonaws.com' }, { id: 'icanhazip', label: 'icanhazip', family: 4, url: 'https://icanhazip.com' }, { id: 'ifconfig-me', label: 'ifconfig.me', family: 4, url: 'https://ifconfig.me/ip' }, { id: 'yandex-internet', label: 'Яндекс Интернетометр', family: 4, url: 'https://yandex.ru/internet/' }, { id: 'ipify-v6', label: 'ipify IPv6', family: 6, url: 'https://api6.ipify.org' }, ]); export const CONNECTIVITY_NETWORK_SOURCE = Object.freeze({ id: 'network', label: 'Сеть', url: 'https://ipwho.is/', }); export const CONNECTIVITY_SITES = Object.freeze([ { id: 'google', label: 'Google', url: 'https://www.google.com/generate_204' }, { id: 'youtube', label: 'YouTube', url: 'https://www.youtube.com/generate_204' }, { id: 'instagram', label: 'Instagram', url: 'https://www.instagram.com/' }, { id: 'speedtest', label: 'Speedtest', url: 'https://www.speedtest.net/' }, { id: 'yandex', label: 'Яндекс Интернетометр', url: 'https://yandex.ru/internet/' }, { id: '2ip', label: '2IP', url: 'https://2ip.ru/' }, ]); export const MAX_CUSTOM_DIAGNOSTIC_SERVICES = 5; export interface DiagnosticService { id: string; label: string; url: string; } export interface DiagnosticSettings { configured: boolean; customServices: DiagnosticService[]; hiddenServiceIds: string[]; } function record(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; } function diagnosticService(value: unknown): DiagnosticService | null { const candidate = record(value); const id = typeof candidate.id === 'string' ? candidate.id.trim() : ''; const label = typeof candidate.label === 'string' ? candidate.label.trim() : ''; if (!/^custom-[a-z0-9-]{1,80}$/i.test(id) || !label || label.length > 40) return null; try { const url = new URL(typeof candidate.url === 'string' ? candidate.url.trim() : ''); if ( url.protocol !== 'https:' || url.username || url.password || (url.port && url.port !== '443') ) return null; return { id, label, url: url.href }; } catch { return null; } } export function normalizeDiagnosticSettings( value: unknown, { strict = false }: { strict?: boolean } = {}, ): DiagnosticSettings { const candidate = record(value); const requestedServices = Array.isArray(candidate.customServices) ? candidate.customServices : []; const customServices = requestedServices .map(diagnosticService) .filter((service): service is DiagnosticService => Boolean(service)) .filter((service, index, services) => services.findIndex(({ id }) => id === service.id) === index) .slice(0, MAX_CUSTOM_DIAGNOSTIC_SERVICES); const builtInIds = new Set(CONNECTIVITY_SITES.map(({ id }) => id)); const requestedHiddenIds = Array.isArray(candidate.hiddenServiceIds) ? candidate.hiddenServiceIds : []; const hiddenServiceIds = requestedHiddenIds .filter((id): id is string => typeof id === 'string' && builtInIds.has(id)) .filter((id, index, ids) => ids.indexOf(id) === index); if (strict && ( typeof candidate.configured !== 'boolean' || !Array.isArray(candidate.customServices) || !Array.isArray(candidate.hiddenServiceIds) || customServices.length !== requestedServices.length || hiddenServiceIds.length !== requestedHiddenIds.length )) throw new TypeError('Invalid diagnostic settings'); return { configured: candidate.configured === true, customServices, hiddenServiceIds, }; } export interface ConnectivitySiteResult { id: string; label: string; status: string; httpStatus?: number | null; [key: string]: unknown; } export interface ConnectivityPathResult { available?: boolean; internetAvailable: boolean; ipv4: { addresses: string[]; [key: string]: unknown }; ipv6: string | null; network?: unknown; sites: ConnectivitySiteResult[]; [key: string]: unknown; } export function assessConnectivity(direct: ConnectivityPathResult, vpn: ConnectivityPathResult) { const comparisons = direct.sites.map(({ id, label }) => { const directSite = direct.sites.find((site) => site.id === id); const vpnSite = vpn.sites?.find((site) => site.id === id); let assessment = 'inconclusive'; if (!vpn.available) assessment = 'not-tested'; else if (directSite?.status === 'available' && vpnSite?.status === 'available') { assessment = 'available'; } else if ( directSite?.status === 'responded' && [403, 451].includes(Number(directSite.httpStatus)) && vpnSite?.status === 'available' ) assessment = 'likely-direct-restriction'; else if ( directSite?.status === 'unavailable' && vpnSite?.status === 'available' && direct.internetAvailable ) { assessment = 'likely-direct-restriction'; } else if (directSite?.status === 'available' && vpnSite?.status !== 'available') { assessment = 'vpn-problem'; } else if (directSite?.status === 'unavailable' && vpnSite?.status === 'unavailable') { assessment = 'unavailable'; } return { id, label, assessment, direct: directSite, vpn: vpnSite || null }; }); const directAddresses = [...direct.ipv4.addresses, direct.ipv6].filter(Boolean); const vpnAddresses = [...(vpn.ipv4?.addresses || []), vpn.ipv6].filter(Boolean); const sameEgress = directAddresses.some((address) => vpnAddresses.includes(address)); let summary = 'inconclusive'; if (!vpn.available) summary = 'vpn-off'; else if (!direct.internetAvailable && !vpn.internetAvailable) summary = 'offline'; else if (!direct.internetAvailable && vpn.internetAvailable) summary = 'direct-offline'; else if (direct.internetAvailable && !vpn.internetAvailable) summary = 'vpn-problem'; else if (comparisons.some((item) => item.assessment === 'likely-direct-restriction')) { summary = 'likely-direct-restriction'; } else if (sameEgress) summary = 'same-ip'; else if (comparisons.every((item) => item.assessment === 'available')) summary = 'available'; return { summary, sameEgress, comparisons }; }