#!/usr/bin/env bash # Shows how GeoIP databases, popular platforms, CDNs and AI services see this host over IPv4 and IPv6. set -euo pipefail shopt -s extglob readonly MIN_BASH_MAJOR=4 readonly MIN_BASH_MINOR=4 # Runs before the other constants: bash 3.2 (macOS) fails on readonly -A below. require_bash_version() { local major="$1" local minor="$2" if ((major > MIN_BASH_MAJOR || (major == MIN_BASH_MAJOR && minor >= MIN_BASH_MINOR))); then return fi printf 'Error: bash %s.%s+ is required, this is bash %s.%s\n' "$MIN_BASH_MAJOR" "$MIN_BASH_MINOR" "$major" "$minor" >&2 if [[ "$OSTYPE" == darwin* ]]; then printf 'Install a newer one with: brew install bash, then run the script again in a new terminal\n' >&2 fi exit 1 } require_bash_version "${BASH_VERSINFO[0]}" "${BASH_VERSINFO[1]}" readonly SCRIPT_NAME="ipregion.sh" readonly SCRIPT_URL="https://github.com/vernette/ipregion" readonly REQUIRED_COMMANDS=(curl jq) readonly PACKAGE_MANAGERS=(apt-get dnf yum pacman zypper apk xbps-install) readonly SERVICE_GROUPS=(platforms cdn availability ai geoip) readonly ALL_GROUPS="all" readonly DEFAULT_TIMEOUT=5 readonly TIMEOUT_PATTERN='^[1-9][0-9]*$' readonly DEFAULT_YOUTUBE_LANG="ru" readonly YOUTUBE_LANG_PATTERN='^[a-z]{2,3}(-[A-Za-z0-9]{2,4})?$' readonly PROXY_PATTERN='^([^:/[:space:]]+|\[[0-9A-Fa-f:]+\]):[0-9]{1,5}$' readonly COUNTRY_CODE_PATTERN='^[A-Z]{2}$' readonly REGIONAL_INDICATOR_A=0x1F1E6 readonly ASCII_A=65 readonly UTF8_LOCALE="C.UTF-8" # A bracket range like [๐ŸŒ€-๐Ÿซฟ] follows the locale collation and misses emoji under en_US.UTF-8 on glibc 2.36 readonly WIDE_EMOJI_PATTERN='[๐Ÿšจ๐Ÿ“ฑ๐ŸŒ๐Ÿ”“๐Ÿค–๐Ÿ“กโœ…]' readonly FLAG_WIDTH=3 readonly -A GROUP_EMOJI=( ["Attention"]=๐Ÿšจ ["Platforms"]=๐Ÿ“ฑ ["GeoIP"]=๐ŸŒ ["Availability"]=๐Ÿ”“ ["AI"]=๐Ÿค– ["CDN"]=๐Ÿ“ก ) readonly ANSI_SEQUENCE=$'\033\[+([0-9;])m' readonly CARD_GAP=1 readonly WIDE_LEFT_CARD_WIDTH=40 readonly WIDE_RIGHT_CARD_WIDTH=39 readonly CARD_CONTENT_INDENT=1 readonly LEGEND_INDENT=3 readonly CARD_SIDES_WIDTH=2 readonly CARD_TRAILING_GAP=1 readonly CARD_TITLE_OFFSET=1 readonly LEFT_GRID_NAME=19 readonly LEFT_GRID_CELL=8 readonly RIGHT_GRID_NAME=18 readonly RIGHT_GRID_CELL=8 readonly CARD_TOP_LEFT="โ•ญ" readonly CARD_TOP_RIGHT="โ•ฎ" readonly CARD_BOTTOM_LEFT="โ•ฐ" readonly CARD_BOTTOM_RIGHT="โ•ฏ" readonly CARD_SIDE="โ”‚" readonly CARD_HORIZONTAL="โ”€" readonly NAME_GAP=2 readonly GEOIP_COUNTRY_WIDTH=4 readonly HEADER_LABEL_WIDTH=6 readonly CHECKS_BAR_MARGIN=26 readonly SUMMARY_BAR_CHAR="โ”" readonly SUMMARY_BAR_REST_CHAR="โ”€" readonly ATTENTION_LIST_LINES=2 readonly ATTENTION_EMPTY_CELL="ยท" readonly GROUP_ROW_FIELDS=3 readonly CDN_ROW_FIELDS=9 readonly ATTENTION_ENTRY_FIELDS=5 readonly GEOIP_DEVIATION_FIELDS=3 readonly HEADER_MARK="โ”ƒ" readonly AUTHOR="vernette" readonly VERDICT_NOTE="seen by most services" readonly TIE_NOTE="services split evenly" readonly CHECKS_PASSED_NOTE="checks passed" readonly CHECKS_GOOD_PERCENT=85 readonly CHECKS_FAIR_PERCENT=65 readonly CHECKS_GOOD="good" readonly CHECKS_FAIR="fair" readonly CHECKS_POOR="poor" readonly REGISTERED_NOTE="registered in" readonly MAXMIND_LIMIT_HEADER_NOTE="MaxMind daily limit used up ยท ASN from RIPE" readonly MAXMIND_FAILURE_HEADER_NOTE="MaxMind unavailable ยท ASN from RIPE" readonly MAXMIND_LIMIT_SUMMARY_NOTE="MaxMind limit used up" readonly GEOIP_AGREEMENT_NOTE="every service agrees" readonly GEOIP_AGREEMENT_EMOJI="โœ…" readonly MAXMIND_LIMIT_CELL="limit" readonly EXITS_NOTE="exits over" readonly DUAL_STACK_EXITS="IPv4 and IPv6" readonly PROXY_CHECKED_ONCE_NOTE="Each service is checked once, the proxy picks the protocol" readonly PROXY_IGNORES_IP_VERSION_WARNING="--ipv4 and --ipv6 are ignored with --proxy, the proxy picks the protocol" readonly PROXY_IP_VERSION="proxy" readonly BOTH_IP_VERSION="both" readonly STATUS_YES="yes" readonly STATUS_NO="no" readonly STATUS_IPBLOCK="ipblock" readonly STATUS_NONE="none" readonly SYMBOL_YES="โœ“" readonly SYMBOL_NO="โœ—" readonly SYMBOL_IPBLOCK="โŠ˜" readonly SYMBOL_UNKNOWN="?" readonly SYMBOL_NONE="-" readonly STATUS_UNKNOWN="unknown" readonly CURL_BASE_ARGS=(--disable --silent --compressed --retry 1 --retry-connrefused) readonly CURL_WRITE_OUT='%{http_code}\n%{redirect_url}\n' readonly CACHE_POLL_INTERVAL=0.1 readonly MAX_PARALLEL_JOBS=16 readonly JOB_POLL_INTERVAL=0.1 readonly RESULT_FILE_PREFIX="result-" readonly CLEAR_LINE=$'\r\033[K' readonly PROGRESS_LABEL="Checking services" readonly PREPARATION_LABEL="Detecting IP addresses" readonly PROGRESS_SPINNER_FRAMES=(โ ‹ โ ™ โ น โ ธ โ ผ โ ด โ ฆ โ ง โ ‡ โ ) readonly PROGRESS_BAR_WIDTH=24 readonly PROGRESS_BAR_DONE="โ”" readonly PROGRESS_BAR_LEFT="โ”€" readonly VERBOSE_TIME_FORMAT='%H:%M:%S' # shellcheck disable=SC2016 # expanded by bash on every trace line, not here readonly DEBUG_TRACE_PS4='+ [${BASHPID}] ${FUNCNAME[0]:-main}:${LINENO} ' readonly DEBUG_LOG_TEMPLATE="ipregion-debug.XXXXXX" readonly DEBUG_RECORD_LIMIT=4096 readonly DEBUG_UPLOAD_TIMEOUT=60 readonly UPLOAD_SPEED_LIMIT=1024 readonly UPLOAD_SPEED_TIME=10 readonly X0_AT_NAME="x0.at" readonly X0_AT_UPLOAD_URL="https://x0.at" readonly LITTERBOX_NAME="litterbox" readonly LITTERBOX_UPLOAD_URL="https://litterbox.catbox.moe/resources/internals/api.php" readonly LITTERBOX_RETENTION="72h" readonly UPLOADED_URL_PATTERN='^https://[^[:space:]]+$' readonly UPLOAD_RESPONSE_NAME="upload-response" readonly UPLOAD_WRITE_OUT='%{http_code}' readonly MASKED_VALUE="masked" readonly MASKED_ASN="AS64496" readonly MASKED_ORG="Example Hosting" readonly MASKED_HOME="~" # shellcheck disable=SC2016 # awk program, $0 belongs to awk readonly DEBUG_LOG_FILTER=' function replace_literal(text, from_text, to_text, result, position) { result = "" while ((position = index(text, from_text)) > 0) { result = result substr(text, 1, position - 1) to_text text = substr(text, position + length(from_text)) } return result text } function mask_keys(text, result, matched, colon) { result = "" while (match(text, key_pattern)) { matched = substr(text, RSTART, RLENGTH) colon = index(matched, ":") result = result substr(text, 1, RSTART - 1) substr(matched, 1, colon) "\"" masked "\"" text = substr(text, RSTART + RLENGTH) } return result text } function print_truncated() { if (truncated > 0) { printf "[... %d bytes truncated]\n", truncated } truncated = 0 } BEGIN { for (i = 1; i < ARGC; i += 2) { from[++pairs] = ARGV[i] to[pairs] = ARGV[i + 1] delete ARGV[i] delete ARGV[i + 1] } key_pattern = "\"(ip|query|ip_address|network|hostname|host|domain|org|isp|isp_name|ispName|as|asn|" \ "asname|organization|company|name|sponsor|autonomous_system_number|autonomous_system_organization|" \ "city|city_name|cityName|region|region_name|regionName|" \ "region_code|state|district|postal|zip|postal_code|latitude|longitude|lat|lon|loc|names|" \ "metro_code|accuracy_radius|timezone|time_zone)\" *: *(\"[^\"]*\"|-?[0-9.]+|[{][^}]*[}])" } /^\++ \[[0-9]+\] / { print_truncated() record_size = 0 } { line = $0 for (i = 1; i <= pairs; i++) { line = replace_literal(line, from[i], to[i]) } line = mask_keys(line) if (record_size >= limit) { truncated += length(line) + 1 next } if (record_size + length(line) > limit) { truncated += record_size + length(line) - limit line = substr(line, 1, limit - record_size) } record_size += length(line) + 1 print line } END { print_truncated() }' readonly IPV4_IDENTITY_URLS=( https://ipv4.icanhazip.com https://api.ipify.org https://ipv4.seeip.org https://ipv4.wtfismyip.com/text ) readonly IPV6_IDENTITY_URLS=( https://ipv6.icanhazip.com https://api6.ipify.org https://ipv6.seeip.org https://ipv6.wtfismyip.com/text ) readonly PROC_NET_DIR="${IPREGION_PROC_NET_DIR:-/proc/net}" readonly IPV4_ROUTE_TABLE="$PROC_NET_DIR/route" readonly IPV6_ROUTE_TABLE="$PROC_NET_DIR/ipv6_route" readonly IPV4_DEFAULT_ROUTE="00000000" readonly IPV6_DEFAULT_ROUTE="00000000000000000000000000000000" readonly IPV6_DEFAULT_PREFIX_LENGTH="00" readonly ROUTE_FLAG_UP=1 readonly ROUTE_FLAG_GATEWAY=2 readonly IPV4_PATTERN='^([0-9]{1,3}\.){3}[0-9]{1,3}$' readonly IPV6_PATTERN='^[0-9A-Fa-f]{0,4}(:[0-9A-Fa-f]{0,4}){2,7}$' readonly IPV6_GROUP_COUNT=8 readonly MAXMIND_DEMO_PAGE_URL="https://www.maxmind.com/en/geoip-web-services-demo" readonly MAXMIND_TOKEN_URL="https://www.maxmind.com/en/geoip2/demo/token" readonly MAXMIND_CITY_URL="https://geoip.maxmind.com/geoip/v2.1/city" readonly MAXMIND_CSRF_PATTERN='value="([0-9a-f]+)" name="csrf_token"' readonly RIPE_RDAP_IP_URL="https://rdap.db.ripe.net/ip" readonly RIPESTAT_PREFIX_OVERVIEW_URL="https://stat.ripe.net/data/prefix-overview/data.json" readonly RIPESTAT_RIR_STATS_COUNTRY_URL="https://stat.ripe.net/data/rir-stats-country/data.json" readonly IPV4_TRANSPORT=4 readonly ANY_TRANSPORT="" readonly IPINFO_IPV4_URL="https://ipinfo.io/widget/demo" readonly IPINFO_IPV6_URL="https://v6.ipinfo.io/widget/demo" readonly CLOUDFLARE_TRACE_URL="https://www.cloudflare.com/cdn-cgi/trace" readonly CLOUDFLARE_COUNTRY_PATTERN=$'(^|\n)loc=([A-Z]{2})(\n|$)' readonly CLOUDFLARE_IP_PATTERN=$'(^|\n)ip=([^\n]+)(\n|$)' readonly IPREGISTRY_URL="https://api.ipregistry.co" readonly IPREGISTRY_KEY="sb69ksjcajfs4c" readonly IPREGISTRY_ORIGIN="https://ipregistry.co" readonly IPAPI_CO_URL="https://ipapi.co" readonly IPAPI_CO_REFERER="https://ipapi.co/" readonly IFCONFIG_CO_URL="https://ifconfig.co/country-iso" readonly IP2LOCATION_URL="https://api.ip2location.io/" readonly COUNTRY_IS_URL="https://api.country.is" readonly GEOAPIFY_URL="https://api.geoapify.com/v1/ipinfo" readonly GEOAPIFY_KEY="b8568cb9afc64fad861a69edbddb2658" readonly GEOJS_URL="https://get.geojs.io/v1/ip/country.json" readonly IPBASE_URL="https://api.ipbase.com/v2/info" readonly IPQUERY_URL="https://api.ipquery.io" readonly IPWHO_URL="https://ipwho.is" readonly IP_API_URL="https://demo.ip-api.com/json" readonly IP_API_ORIGIN="https://ip-api.com" readonly USER_AGENT="Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0" readonly GOOGLE_URL="https://accounts.google.com/v3/signin/identifier?flowName=GlifSetupAndroid" readonly GOOGLE_COUNTRY_PATTERN='name="region" value="([^"]*)"' readonly YOUTUBE_URL="https://www.youtube.com/sw.js_data" readonly YOUTUBE_JSON_PREFIX=")]}'" readonly TWITCH_URL="https://gql.twitch.tv/gql" readonly TWITCH_CLIENT_ID="kimne78kx3ncx6brgo4mv6wki5h1ko" readonly TWITCH_QUERY='[{"operationName":"VerifyEmail_CurrentUser","variables":{},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"f9e7dcdf7e99c314c82d8f7f725fab5f99d1df3d7359b53c9ae122deec590198"}}}]' readonly CHATGPT_URL="https://ab.chatgpt.com/v1/initialize" readonly CHATGPT_STATSIG_API_KEY="client-zUdXdSTygXJdzoE0sWTkP8GKTVsUMF2IRM7ShVO2JAG" readonly CHATGPT_COUNTRY_PATTERN='"derived_fields":\{[^}]*"country":"([^"]*)"' readonly CLAUDE_URL="https://claude.ai/api/bootstrap" readonly CLAUDE_USER_AGENT="Claude com.anthropic.claude/1.261023.20 (Android 30) Claude/1.261023.20" readonly CLAUDE_PLATFORM_HEADER="Anthropic-Client-Platform: android" readonly NETFLIX_URL="https://api.fast.com/netflix/speedtest/v2?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=1" readonly SPOTIFY_URL="https://spclient.wg.spotify.com/signup/public/v1/account/?validate=1&key=142b583129b2df829de3656f9eb484e6" readonly SPOTIFY_CLIENT_ID="9a8d2f0ce77a4e248bb71fefcb557637" readonly REDDIT_NOT_FOUND_URL="https://www.reddit.com/api/v1/not-found" readonly REDDIT_COUNTRY_PATTERN='country code: ([A-Z]{2})' readonly DISNEY_PLUS_URL="https://disney.api.edge.bamgrid.com/graph/v1/device/graphql" readonly DISNEY_PLUS_API_KEY="ZGlzbmV5JmFuZHJvaWQmMS4wLjA.bkeb0m230uUhv8qrAXuNu39tbE_mD5EEhM_NAcohjyA" # shellcheck disable=SC2016 # $registerDevice is a GraphQL variable, not a shell expansion readonly DISNEY_PLUS_QUERY='{"query":"mutation registerDevice($registerDevice: RegisterDeviceInput!) { registerDevice(registerDevice: $registerDevice) { __typename } }","variables":{"registerDevice":{"applicationRuntime":"android","attributes":{"operatingSystem":"Android","operatingSystemVersion":"13"},"deviceFamily":"android","deviceLanguage":"en","deviceProfile":"phone","devicePlatformId":"android"}},"operationName":"registerDevice"}' readonly TIKTOK_URL="https://www.tiktok.com/api/v1/web-cookie-privacy/config?appId=1988" readonly APPLE_URL="https://gspe1-ssl.ls.apple.com/pep/gcc" readonly STEAM_URL="https://store.steampowered.com" readonly STEAM_COUNTRY_PATTERN='steamCountry=([^%;]*)' readonly MICROSOFT_URL="https://login.live.com" readonly MICROSOFT_COUNTRY_PATTERN='"sRequestCountry":"([^"]*)"' readonly OOKLA_URL="https://www.speedtest.net/api/js/config-sdk" readonly JETBRAINS_URL="https://data.services.jetbrains.com/geo" readonly PLAYSTATION_URL="https://www.playstation.com" readonly PLAYSTATION_COUNTRY_PATTERN='[Cc]ookie: country=([^;]*)' readonly CLOUDFLARE_SPEED_META_URL="https://speed.cloudflare.com/meta" readonly CLOUDFLARE_SPEED_REFERER="https://speed.cloudflare.com" readonly YOUTUBE_CDN_MAPPING_URL="https://redirector.googlevideo.com/report_mapping?di=no" readonly YOUTUBE_CDN_NODE_PATTERN='=> ([^ ]+)' readonly GOOGLE_NODE_PATTERN='^([a-z]{3})[0-9]' readonly ISP_NODE_PATTERN='^[a-z]+-([a-z]{3})[0-9]' readonly GGC_NODE_PATTERN='^[a-z]{2}([a-z]{3})[a-z]-[a-z0-9]+$' readonly NETFLIX_CDN_URL_PATTERN='^https://ipv[46]-([a-z0-9]+-([a-z]{3})[0-9]+[a-z0-9-]*)\.' readonly AIRPORT_CODES_URL="https://www.air-port-codes.com/api/v1/single" readonly AIRPORT_CODES_KEY="96dc04b3fb" readonly AIRPORT_CODES_REFERER="https://www.air-port-codes.com/" readonly GOOGLE_SOCS_COOKIE="Cookie: SOCS=CAI" readonly ACCEPT_LANGUAGE_ENGLISH="Accept-Language: en-US,en;q=0.9" readonly YOUTUBE_INNERTUBE_BROWSE_URL="https://www.youtube.com/youtubei/v1/browse" readonly YOUTUBE_PREMIUM_QUERY='{"context":{"client":{"clientName":"MWEB","clientVersion":"2.20261020.00.00","hl":"en","gl":"US"}},"browseId":"SPunlimited"}' readonly YOUTUBE_PREMIUM_PURCHASE_MARKER="premiumPurchaseButtonRenderer" readonly YOUTUBE_WATCH_URL="https://www.youtube.com/watch?v=dQw4w9WgXcQ" readonly YOUTUBE_VIEWER_COOKIE_PREFIX="Cookie: SOCS=CAI; PREF=hl=" readonly ACCEPT_LANGUAGE_PREFIX="Accept-Language: " readonly YOUTUBE_NO_ADS_SERVICE_PREFIX="YouTube No Ads" readonly YOUTUBE_PLAYER_RESPONSE_MARKER="ytInitialPlayerResponse" readonly YOUTUBE_VIDEO_AD_MARKER="SLOT_TYPE_PLAYER_BYTES" readonly YOUTUBE_COOKIE_JAR_PREFIX="youtube-cookies-" readonly SPOTIFY_SIGNUP_BLOCKED_STATUS_PATTERN='^(120|320)$' readonly DISNEY_PLUS_FORBIDDEN_MARKER="Forbidden location" readonly REDDIT_GUEST_URL="https://www.reddit.com" readonly GOOGLE_SEARCH_URL="https://www.google.com/search?q=cats" readonly GOOGLE_SEARCH_CAPTCHA_PATH="/sorry/" readonly GOOGLE_SEARCH_NO_JS_MARKER="enablejs" readonly GEMINI_WEB_REGIONS_URL="https://support.google.com/gemini/answer/13575153?hl=en" readonly GEMINI_WEB_REGIONS_FILTER='split("Supported countries & territories")[1] // "" | [scan("
  • ([^<]+)
  • ")[0] | gsub("&[a-z]+;"; " ")]' readonly GEMINI_API_REGIONS_URL="https://ai.google.dev/gemini-api/docs/available-regions.md.txt" readonly GEMINI_API_REGIONS_FILTER='split("\n") | map(select(startswith("- ")) | .[2:])' readonly CLDR_TERRITORIES_URL="https://cdn.jsdelivr.net/npm/cldr-localenames-full/main/en/territories.json" # shellcheck disable=SC2016 # $code, $regions, $yes and $no are jq variables, not shell expansions readonly GEMINI_MATCH_FILTER=' def stopwords: ["the", "of", "and", "republic", "island", "islands", "saint", "st"]; def words: ascii_downcase | [splits("[^\\p{L}]+")] | map(select(. != "" and (IN(stopwords[]) | not))) | unique; def names_missing_in_cldr: {CD: ["Democratic Republic of the Congo"], MO: ["Macau"], UM: ["United States Minor Outlying Islands"]}[$code] // []; (.main.en.localeDisplayNames.territories | to_entries | map(select(.key | startswith($code)) | .value)) + names_missing_in_cldr | map(words) | if any(IN($regions[] | words)) then $yes else $no end' readonly OPENAI_API_URL="https://api.openai.com/v1/models" readonly OPENAI_API_REGION_BLOCKED_MARKER="unsupported_country_region_territory" readonly ANTHROPIC_API_URL="https://api.anthropic.com/v1/models" readonly ANTHROPIC_API_REGION_BLOCKED_MARKER="Request not allowed" readonly GROK_API_URL="https://api.x.ai/v1/models" readonly GROK_API_REGION_BLOCKED_MARKER="not available in your region" readonly QWEN_API_URL="https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models" readonly QWEN_CN_API_URL="https://dashscope.aliyuncs.com/compatible-mode/v1/models" readonly KIMI_API_URL="https://api.moonshot.ai/v1/models" readonly KIMI_CN_API_URL="https://api.moonshot.cn/v1/models" readonly GLM_API_URL="https://api.z.ai/api/paas/v4/models" readonly GLM_CN_API_URL="https://open.bigmodel.cn/api/paas/v4/models" readonly MINIMAX_API_URL="https://api.minimax.io/v1/models" readonly MINIMAX_CN_API_URL="https://api.minimaxi.com/v1/models" readonly DOUBAO_API_URL="https://ark.cn-beijing.volces.com/api/v3/models" readonly SILICONFLOW_API_URL="https://api.siliconflow.cn/v1/models" readonly DEEPSEEK_API_URL="https://api.deepseek.com/models" readonly CURL_EXIT_OK=0 readonly CURL_EXIT_DNS=6 readonly CURL_EXIT_CONNECT=7 readonly CURL_EXIT_TIMEOUT=28 readonly CURL_EXIT_TLS_HANDSHAKE=35 readonly CURL_EXIT_TLS_CERTIFICATE=60 readonly CURL_EXIT_PROXY=97 readonly HTTP_OK=200 readonly HTTP_CREATED=201 readonly HTTP_UNAUTHORIZED=401 readonly HTTP_PAYMENT_REQUIRED=402 readonly HTTP_FORBIDDEN=403 readonly HTTP_TOO_MANY_REQUESTS=429 readonly REASON_DNS="DNS error" readonly REASON_CONNECT="connection failed" readonly REASON_TIMEOUT="timeout" readonly REASON_TLS="TLS error" readonly REASON_PROXY="proxy error" readonly REASON_CAPTCHA="captcha" readonly REASON_PROXY_PICKED="proxy picked IPv" readonly REASON_NETWORK="network error" readonly REASON_FORBIDDEN="access denied" readonly REASON_RATE_LIMITED="rate limited" readonly REASON_SERVER_ERROR="server error" readonly REASON_UNEXPECTED_RESPONSE="unexpected response" readonly REASON_DAILY_LIMIT="daily limit used up" readonly REASON_NO_DEMO_TOKEN="no demo token" readonly REASON_NO_IPV6="no IPv6" readonly REASON_VIDEO_ADS="video ads" readonly GROUP_PLATFORMS="platforms" readonly PLATFORMS_CHECKS=( check_platforms_google check_platforms_youtube check_platforms_twitch check_platforms_chatgpt check_platforms_claude check_platforms_netflix check_platforms_spotify check_platforms_reddit check_platforms_disney_plus check_platforms_tiktok check_platforms_apple check_platforms_steam check_platforms_microsoft check_platforms_ookla check_platforms_jetbrains check_platforms_playstation ) readonly GROUP_CDN="cdn" readonly CDN_CHECKS=( check_cdn_cloudflare check_cdn_youtube check_cdn_netflix ) readonly GROUP_AVAILABILITY="availability" readonly AVAILABILITY_CHECKS=( check_availability_youtube_premium check_availability_youtube_no_ads check_availability_google_search check_availability_spotify_signup check_availability_disney_plus check_availability_reddit_guest ) readonly GROUP_AI="ai" readonly AI_CHECKS=( check_ai_gemini_web check_ai_gemini_api check_ai_openai_api check_ai_anthropic_api check_ai_grok_api check_ai_qwen_api check_ai_qwen_cn_api check_ai_kimi_api check_ai_kimi_cn_api check_ai_glm_api check_ai_glm_cn_api check_ai_minimax_api check_ai_minimax_cn_api check_ai_doubao_api check_ai_siliconflow_api check_ai_deepseek_api ) readonly GROUP_GEOIP="geoip" readonly GEOIP_CHECKS=( check_geoip_maxmind check_geoip_ripe check_geoip_ipinfo_io check_geoip_cloudflare check_geoip_ipregistry_co check_geoip_ipapi_co check_geoip_ifconfig_co check_geoip_ip2location_io check_geoip_country_is check_geoip_geoapify_com check_geoip_geojs_io check_geoip_ipbase_com check_geoip_ipquery_io check_geoip_ipwho_is check_geoip_ip_api_com ) readonly ALL_CHECKS=( "${PLATFORMS_CHECKS[@]}" "${CDN_CHECKS[@]}" "${AVAILABILITY_CHECKS[@]}" "${AI_CHECKS[@]}" "${GEOIP_CHECKS[@]}" ) readonly REASON_DIFFERENT_COUNTRY="different country" readonly ATTENTION_COLLAPSE_MIN=3 # shellcheck disable=SC2016 # $yes, $platforms and $geoip are jq variables, not shell expansions readonly VERDICT_FILTER=' def country_counts: group_by(.country) | map({country: .[0].country, count: length}); map(select(.status == $yes and (.group == $platforms or .group == $geoip))) | group_by(.group) | (map(length) | reduce .[] as $total (1; . * $total)) as $common_total | map(($common_total / length) as $scale | country_counts | map(.weight = .count * $scale)) | add // [] | group_by(.country) | map({country: .[0].country, weight: (map(.weight) | add)}) | (map(.weight) | max) as $top_weight | map(select(.weight == $top_weight) | .country) | .[]' # shellcheck disable=SC2016 # $daily_limit, $limit, $unknown and the other names are jq variables, not shell expansions readonly GROUP_ROWS_FILTER=' def cell: if .reason == $daily_limit then $limit elif .status == $yes and .country != "" then .country elif .status == "" then $unknown else .status end; def result_of($ip_version): (map(select(.ip_version == $ip_version)) | first) // first; map(select(.group == $group)) as $rows | reduce ($rows[].service) as $service ([]; if any(.[]; . == $service) then . else . + [$service] end) | .[] as $service | ($rows | map(select(.service == $service))) as $results | $service, ($results | result_of("4") | cell), ($results | result_of("6") | cell)' # shellcheck disable=SC2016 # $cdn, $unknown and $service are jq variables, not shell expansions readonly CDN_ROWS_FILTER=' def result_of($ip_version): (map(select(.ip_version == $ip_version)) | first) // first; def edge: (if .status == "" then $unknown else .status end), .country, .iata, .node; map(select(.group == $cdn)) as $rows | reduce ($rows[].service) as $service ([]; if any(.[]; . == $service) then . else . + [$service] end) | .[] as $service | ($rows | map(select(.service == $service))) as $results | $service, ($results | result_of("4") | edge), ($results | result_of("6") | edge)' # shellcheck disable=SC2016 # $geoip and $yes are jq variables, not shell expansions readonly GEOIP_MAJORITY_FILTER=' map(select(.group == $geoip and .status == $yes and .country != "")) | group_by(.country) | max_by(length) | .[0].country // empty' # shellcheck disable=SC2016 # $geoip, $reference and the other names are jq variables, not shell expansions readonly GEOIP_AGREEMENT_FILTER=' map(select(.group == $geoip)) | group_by(.service) | map(select(all(.[]; .reason == $daily_limit) | not)) as $counted | ($counted | map(map(select(.status == $yes and .country != "") | .country) | select(length > 0 and all(. == $reference))) | length), ($counted | length), (length > ($counted | length))' # shellcheck disable=SC2016 # $geoip, $reference and $yes are jq variables, not shell expansions readonly GEOIP_DEVIATIONS_FILTER=' map(select(.group == $geoip and .status == $yes and .country != "" and .country != $reference)) | reduce .[] as $result ([]; (map(.country == $result.country and .ip_version == $result.ip_version) | index(true)) as $index | if $index == null then . + [{country: $result.country, ip_version: $result.ip_version, services: [$result.service]}] else .[$index].services += [$result.service] end) | .[] | .country, .ip_version, (.services | length), .services[]' # shellcheck disable=SC2016 # $verdict, $geoip and the other names are jq variables, not shell expansions readonly NEEDS_ATTENTION_DEFINITION=' def is_failed: if .group == $cdn or .status == $none or .reason == $daily_limit or .reason == $video_ads then false elif .status == "" then true elif .group == $geoip then false elif .country != "" then $verdict != "" and .country != $verdict else .status != $yes end; def needs_attention: is_failed and (.status == "" or .country != "");' # shellcheck disable=SC2016 # $verdict and $geoip are jq variables, not shell expansions readonly ABNORMAL_CHECKS_FILTER="$NEEDS_ATTENTION_DEFINITION"' def differs_from_verdict: .group == $geoip and .country != "" and $verdict != "" and .country != $verdict; [.[] | select(is_failed or differs_from_verdict) | [.group, .service]] | unique | length' # shellcheck disable=SC2016 # $collapse_min, $different_country and the other names are jq variables, not shell expansions readonly ATTENTION_FILTER="$NEEDS_ATTENTION_DEFINITION"' def severity: if .country != "" then 0 else 1 end; to_entries | map(.value + {order: .key} | select(needs_attention)) | map(if .status == $yes then .reason = $different_country else . end) | group_by([.group, .service, .country, .status, .reason]) | map(.[0] + {ip_version: (if length == 2 then "both" else .[0].ip_version end)}) | group_by([.country, .status, .reason, .ip_version]) | map(if length >= $collapse_min then sort_by(.order) | [.[0] + {services: map(.service)}] else map(. + {services: [.service]}) end) | add // [] | sort_by([(.services | length) >= $collapse_min, severity, .order]) | map({services, ip_version, country, status, reason})' readonly JSON_VERSION=2 readonly JSON_SCHEMA_URL="https://raw.githubusercontent.com/vernette/ipregion/master/schema/ipregion-v2.schema.json" # shellcheck disable=SC2016 # $ip, $asn and the other names are jq variables, not shell expansions readonly JSON_ADDRESS_FILTER=' def nullable: if . == "" then null else . end; { ip: $ip, asn: ($asn | if . == "" then null else tonumber end), org: ($org | nullable), registered: ($registered | nullable), maxmind_failure: ($maxmind_failure | nullable) }' # shellcheck disable=SC2016 # $schema, $version, $availability and the other names are jq variables, not shell expansions readonly JSON_REPORT_FILTER=' def nullable: if . == "" then null else . end; def access: if (.group == $availability or .group == $ai) and .status != $none then .status | nullable else null end; def protocol_result: {country: (.country | nullable), access: access, reason: (.reason | nullable)} + if has("iata") then {iata: (.iata | nullable), node: (.node | nullable)} else {} end; def protocol_entry($rows; $ip_version): first($rows[] | select(.ip_version == $ip_version) | protocol_result) // null; def service_entries($results; $group): [$results[] | select(.group == $group)] as $rows | reduce $rows[].id as $id ([]; if any(.[]; . == $id) then . else . + [$id] end) | map(. as $id | [$rows[] | select(.id == $id)] as $service_rows | { id: $id, service: $service_rows[0].service, ipv4: protocol_entry($service_rows; "4"), ipv6: protocol_entry($service_rows; "6"), proxy: protocol_entry($service_rows; $proxy) }); . as $results | { "$schema": $schema, version: $version, ipv4: $ipv4, ipv6: $ipv6, verdict: {country: ($verdict | nullable), tied: $tied}, attention: ($attention | map({ services, ip_version, country: (.country | nullable), access: (if .status == $yes or .status == $none then null else .status | nullable end), reason: (.reason | nullable) })), results: (reduce $groups[] as $group ({}; .[$group] = service_entries($results; $group))) }' if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then readonly C_TITLE=$'\033[1;36m' readonly C_BADGE=$'\033[1;30;46m' readonly C_BADGE_FAIR=$'\033[1;30;43m' readonly C_BADGE_POOR=$'\033[1;30;41m' readonly C_BAR=$'\033[0;36m' readonly C_BAR_FAIR=$'\033[0;33m' readonly C_BAR_POOR=$'\033[0;31m' readonly C_VALUE=$'\033[1m' readonly C_DIM=$'\033[0;90m' readonly C_YES=$'\033[1;32m' readonly C_NO=$'\033[1;31m' readonly C_WARN=$'\033[1;33m' readonly C_RESET=$'\033[0m' else readonly C_TITLE="" readonly C_BADGE="" readonly C_BADGE_FAIR="" readonly C_BADGE_POOR="" readonly C_BAR="" readonly C_BAR_FAIR="" readonly C_BAR_POOR="" readonly C_VALUE="" readonly C_DIM="" readonly C_YES="" readonly C_NO="" readonly C_WARN="" readonly C_RESET="" fi if [[ -t 2 && -z "${NO_COLOR:-}" ]]; then readonly M_ERROR=$'\033[1;31m' readonly M_WARN=$'\033[1;33m' readonly M_ASK=$'\033[1;36m' readonly M_ACCENT=$'\033[0;36m' readonly M_VALUE=$'\033[1m' readonly M_DIM=$'\033[0;90m' readonly M_RESET=$'\033[0m' else readonly M_ERROR="" readonly M_WARN="" readonly M_ASK="" readonly M_ACCENT="" readonly M_VALUE="" readonly M_DIM="" readonly M_RESET="" fi VERBOSE=false DEBUG=false ASSUME_YES=false MISSING_COMMANDS=() PACKAGE_MANAGER="" ROOT_COMMAND=() JSON_OUTPUT=false REQUESTED_GROUPS=() SELECTED_GROUPS=("${SERVICE_GROUPS[@]}") REQUESTED_SERVICES=() SELECTED_CHECKS=() CURL_TIMEOUT="$DEFAULT_TIMEOUT" YOUTUBE_LANG="$DEFAULT_YOUTUBE_LANG" IPV4_ONLY=false IPV6_ONLY=false PROXY_ADDR="" INTERFACE_NAME="" MAIN_INTERFACE="" SHOW_EMOJI=false COUNTRY_EXTRA=0 GEOIP_BREAKDOWN=false SHOW_ATTENTION=true VERDICT="" VERDICT_TIED_COUNTRIES=() ATTENTION="[]" CHECKS_TOTAL=0 CHECKS_NORMAL=0 WORK_DIR="" EXTERNAL_IPV4="" EXTERNAL_IPV6="" CURL_ARGS=() MAXMIND_TOKEN="" MAXMIND_TOKEN_FAILURE="" JOB_COUNT=0 JOB_SLOTS_FD="" PROGRESS_FRAME=0 RESULTS="" DEBUG_TRACE_FILE="" DEBUG_TRACE_DIR="" DEBUG_LOG_FILE="" DEBUG_MASKS=() HEADER_IP_VERSIONS=() HEADER_IP_VALUES=() HEADER_NETWORKS=() HEADER_EXIT_LABEL="" HEADER_VALUE_WIDTH=0 HEADER_NETWORK_WIDTH=0 LEFT_CARD_WIDTH=0 RIGHT_CARD_WIDTH=0 FULL_CARD_WIDTH=0 RIGHT_COLUMN_START=0 RIGHT_COLUMN_WIDTH=0 GEOIP_LINE_WIDTH=0 CHECKS_BAR_WIDTH=0 declare -gA REGISTRATION_ASN=() declare -gA REGISTRATION_ORG=() declare -gA REGISTRATION_COUNTRY=() declare -gA REGISTRATION_MAXMIND_FAILURE=() use_utf8_locale() { local probe="$SYMBOL_YES" if ((${#probe} == 1)); then return fi export LC_ALL="$UTF8_LOCALE" 2>/dev/null || true } print_error() { local message="$1" printf '%sError:%s %s\n' "$M_ERROR" "$M_RESET" "$message" >&2 } print_warning() { local message="$1" printf '%sWarning:%s %s\n' "$M_WARN" "$M_RESET" "$message" >&2 } print_note() { local message="$1" printf '%s%s%s\n' "$M_DIM" "$message" "$M_RESET" >&2 } ask_yes_no() { local question="$1" local answer printf '%s%s%s %s[y/N]%s ' "$M_ASK" "$question" "$M_RESET" "$M_DIM" "$M_RESET" >/dev/tty read -r answer &2 print_note "Run $SCRIPT_NAME --help for usage" exit 1 } log_verbose() { local message="$1" if [[ "$VERBOSE" != true ]]; then return fi if [[ -n "$EXTERNAL_IPV4" ]]; then message="${message//"$EXTERNAL_IPV4"/"$(mask_ipv4 "$EXTERNAL_IPV4")"}" fi if [[ -n "$EXTERNAL_IPV6" ]]; then message="${message//"$EXTERNAL_IPV6"/"$(mask_ipv6 "$EXTERNAL_IPV6")"}" fi printf "[%($VERBOSE_TIME_FORMAT)T] %s\n" -1 "$message" >&2 } usage() { cat </dev/null) if [[ -z "$iface" ]]; then return fi printf '%s %x\n' "$iface" "$flags" } print_default_routes() { local ip_version="$1" if [[ ! -d "$PROC_NET_DIR" ]]; then print_bsd_default_route "$ip_version" return fi case "$ip_version" in 4) print_ipv4_default_routes ;; 6) print_ipv6_default_routes ;; esac } has_route_flag() { local flags="$1" local flag="$2" ((16#$flags & flag)) } print_gateway_interface() { local ip_version="$1" local iface flags while read -r iface flags; do if has_route_flag "$flags" "$ROUTE_FLAG_UP" && has_route_flag "$flags" "$ROUTE_FLAG_GATEWAY"; then printf '%s' "$iface" return fi done < <(print_default_routes "$ip_version") } has_default_route_through() { local ip_version="$1" local interface="$2" local iface flags while read -r iface flags; do if [[ "$iface" == "$interface" ]] && has_route_flag "$flags" "$ROUTE_FLAG_UP"; then return 0 fi done < <(print_default_routes "$ip_version") return 1 } has_default_route() { local ip_version="$1" local iface flags while read -r iface flags; do if has_route_flag "$flags" "$ROUTE_FLAG_UP"; then return 0 fi done < <(print_default_routes "$ip_version") return 1 } detect_main_interface() { if [[ -n "$INTERFACE_NAME" ]]; then return fi MAIN_INTERFACE=$(print_gateway_interface 4) if [[ -z "$MAIN_INTERFACE" ]]; then MAIN_INTERFACE=$(print_gateway_interface 6) fi } log_interface() { if [[ -n "$INTERFACE_NAME" ]]; then log_verbose "Interface: $INTERFACE_NAME (--interface)" return fi if [[ -n "$MAIN_INTERFACE" ]]; then log_verbose "Interface: $MAIN_INTERFACE (main, carries the default route)" return fi log_verbose "Interface: not detected, the system routing picks it" } log_proxy() { if [[ -z "$PROXY_ADDR" ]]; then return fi log_verbose "Proxy: socks5h://$PROXY_ADDR" } limit_ip_versions_to_main_interface() { if [[ -z "$MAIN_INTERFACE" || -n "$PROXY_ADDR" || "$IPV4_ONLY" == true || "$IPV6_ONLY" == true ]]; then return fi if ! has_default_route_through 6 "$MAIN_INTERFACE"; then IPV4_ONLY=true if has_default_route 6; then print_note "IPv6 goes out through another interface than the main one $MAIN_INTERFACE, checking IPv4 only (-6 or -i IFACE to check IPv6)" fi return fi if ! has_default_route_through 4 "$MAIN_INTERFACE"; then IPV6_ONLY=true if has_default_route 4; then print_note "IPv4 goes out through another interface than the main one $MAIN_INTERFACE, checking IPv6 only (-4 or -i IFACE to check IPv4)" fi fi } detect_missing_commands() { local command_name for command_name in "${REQUIRED_COMMANDS[@]}"; do if ! command -v "$command_name" >/dev/null 2>&1; then MISSING_COMMANDS+=("$command_name") fi done } missing_commands_list() { local list printf -v list '%s, ' "${MISSING_COMMANDS[@]}" printf '%s' "${list%, }" } detect_package_manager() { local manager for manager in "${PACKAGE_MANAGERS[@]}"; do if command -v "$manager" >/dev/null 2>&1; then PACKAGE_MANAGER="$manager" return fi done print_error "missing dependencies ($(missing_commands_list)) and no supported package manager found, install them manually" exit 1 } resolve_root_command() { if ((EUID == 0)) || [[ -n "${TERMUX_VERSION:-}" ]]; then return fi if command -v sudo >/dev/null 2>&1; then ROOT_COMMAND=(sudo) return fi print_error "missing dependencies ($(missing_commands_list)), installing them with $PACKAGE_MANAGER needs root or sudo" exit 1 } has_terminal() { : 2>/dev/null /dev/tty if ask_yes_no "Install them with $PACKAGE_MANAGER?"; then return fi print_error "missing dependencies ($(missing_commands_list)), installation cancelled" exit 1 } install_packages() { local -a manager=("${ROOT_COMMAND[@]}" "$PACKAGE_MANAGER") case "$PACKAGE_MANAGER" in apt-get) "${manager[@]}" update "${manager[@]}" install -y "${MISSING_COMMANDS[@]}" ;; dnf | yum) "${manager[@]}" install -y "${MISSING_COMMANDS[@]}" ;; pacman) "${manager[@]}" -Sy --needed --noconfirm "${MISSING_COMMANDS[@]}" ;; zypper) "${manager[@]}" --non-interactive install "${MISSING_COMMANDS[@]}" ;; apk) "${manager[@]}" add --no-cache "${MISSING_COMMANDS[@]}" ;; xbps-install) "${manager[@]}" -Sy "${MISSING_COMMANDS[@]}" ;; esac >&2 } require_installed_commands() { local command_name for command_name in "${MISSING_COMMANDS[@]}"; do if ! command -v "$command_name" >/dev/null 2>&1; then print_error "$command_name is still missing after installing it with $PACKAGE_MANAGER" exit 1 fi done } install_missing_dependencies() { detect_missing_commands if ((${#MISSING_COMMANDS[@]} == 0)); then return fi detect_package_manager resolve_root_command confirm_install install_packages require_installed_commands } resolve_selected_groups() { local group if is_in_list "$ALL_GROUPS" "${REQUESTED_GROUPS[@]}"; then return fi if ((${#REQUESTED_GROUPS[@]} + ${#REQUESTED_SERVICES[@]} == 0)); then return fi SELECTED_GROUPS=() for group in "${SERVICE_GROUPS[@]}"; do if is_in_list "$group" "${REQUESTED_GROUPS[@]}"; then SELECTED_GROUPS+=("$group") fi done } normalize_service_name() { local name="${1,,}" name="${name//+/_plus}" printf '%s' "${name//[. -]/_}" } matching_checks() { local request="$1" local group="" local name="$request" local -a exact=() shortened=() local check id if [[ "$request" == */* ]]; then group="${request%%/*}" group="${group,,}" name="${request#*/}" fi name=$(normalize_service_name "$name") for check in "${ALL_CHECKS[@]}"; do if [[ -n "$group" && "$(check_group "$check")" != "$group" ]]; then continue fi id=$(check_id "$check") if [[ "$id" == "$name" ]]; then exact+=("$check") elif [[ "${id%_*}" == "$name" ]]; then shortened+=("$check") fi done if ((${#exact[@]} > 0)); then printf '%s\n' "${exact[@]}" return fi printf '%s\n' "${shortened[@]}" } resolve_selected_checks() { local -a matches candidates local request check for request in "${REQUESTED_SERVICES[@]}"; do mapfile -t matches < <(matching_checks "$request") if [[ -z "${matches[0]:-}" ]]; then die "unknown service $request, see --list-services" fi if ((${#matches[@]} > 1)); then candidates=() for check in "${matches[@]}"; do candidates+=("$(service_name "$check")") done die "ambiguous service $request, expected one of: ${candidates[*]}" fi SELECTED_CHECKS+=("${matches[0]}") done } visible_length() { local text="$1" local plain="${text//$ANSI_SEQUENCE/}" local narrow="${plain//$WIDE_EMOJI_PATTERN/}" printf '%d' $((2 * ${#plain} - ${#narrow})) } repeat_char() { local char="$1" local count="$2" local line if ((count <= 0)); then return fi printf -v line '%*s' "$count" '' printf '%s' "${line// /$char}" } pad() { local text="$1" local width="$2" local fill fill=$((width - $(visible_length "$text"))) if ((fill < 0)); then fill=0 fi printf '%s%*s' "$text" "$fill" '' } pad_cell() { local color="$1" local text="$2" local width="$3" local fill fill=$((width - $(visible_length "$text"))) if ((fill < 1)); then fill=1 fi printf '%s%s%s%*s' "$color" "$text" "$C_RESET" "$fill" '' } max_length() { local max=0 local text length for text in "$@"; do length=$(visible_length "$text") if ((length > max)); then max="$length" fi done printf '%d' "$max" } paint() { local color="$1" local text="$2" printf '%s%s%s' "$color" "$text" "$C_RESET" } format_flag() { local country="$1" local first=$((REGIONAL_INDICATOR_A + $(printf '%d' "'${country:0:1}") - ASCII_A)) local second=$((REGIONAL_INDICATOR_A + $(printf '%d' "'${country:1:1}") - ASCII_A)) printf '%b' "\\U$(printf '%08X' "$first")\\U$(printf '%08X' "$second")" } label_country() { local country="$1" if [[ "$SHOW_EMOJI" != true || ! "$country" =~ $COUNTRY_CODE_PATTERN ]]; then printf '%s' "$country" return fi printf '%s %s' "$(format_flag "$country")" "$country" } label_group() { local title="$1" if [[ "$SHOW_EMOJI" != true || -z "$title" ]]; then printf '%s' "$title" return fi printf '%s %s' "${GROUP_EMOJI[$title]}" "$title" } is_verdict_country() { local country="$1" [[ "$country" == "$VERDICT" ]] || is_in_list "$country" "${VERDICT_TIED_COUNTRIES[@]}" } format_country() { local country="$1" local width="$2" if [[ -z "$country" ]]; then paint "$C_DIM" "$(pad "$SYMBOL_UNKNOWN" "$width")" return fi if is_verdict_country "$country"; then paint "$C_VALUE" "$(pad "$(label_country "$country")" "$width")" return fi paint "$C_WARN" "$(pad "$(label_country "$country")" "$width")" } format_status() { local status="$1" local width="$2" case "$status" in "$STATUS_YES") paint "$C_YES" "$(pad "$SYMBOL_YES" "$width")" ;; "$STATUS_NO") paint "$C_NO" "$(pad "$SYMBOL_NO" "$width")" ;; "$STATUS_IPBLOCK") paint "$C_NO" "$(pad "$SYMBOL_IPBLOCK" "$width")" ;; "$STATUS_NONE") paint "$C_DIM" "$(pad "$SYMBOL_NONE" "$width")" ;; "$MAXMIND_LIMIT_CELL") paint "$C_DIM" "$(pad "$MAXMIND_LIMIT_CELL" "$width")" ;; *) paint "$C_DIM" "$(pad "$SYMBOL_UNKNOWN" "$width")" ;; esac } format_cell() { local value="$1" local width="$2" if [[ "$value" =~ $COUNTRY_CODE_PATTERN ]]; then format_country "$value" "$width" return fi format_status "$value" "$width" } print_card_top() { local title_color="$1" local title="$2" local width="$3" local summary="${4:-}" local label label="$(paint "$title_color" " $(label_group "$title") ")" if [[ -n "$summary" ]]; then label="$(paint "$title_color" " $(label_group "$title")") $(paint "$C_DIM" "ยท") $summary " fi printf '%s%s%s%s%s%s%s%s\n' "$C_DIM" "$CARD_TOP_LEFT" "$(repeat_char "$CARD_HORIZONTAL" "$CARD_TITLE_OFFSET")" \ "$label" "$C_DIM" \ "$(repeat_char "$CARD_HORIZONTAL" $((width - CARD_SIDES_WIDTH - CARD_TITLE_OFFSET - $(visible_length "$label"))))" \ "$CARD_TOP_RIGHT" "$C_RESET" } print_card_line() { local line="$1" local width="$2" local fill line="${line%%+( )}" fill=$((width - CARD_SIDES_WIDTH - CARD_CONTENT_INDENT - CARD_TRAILING_GAP - $(visible_length "$line"))) if ((fill < 0)); then fill=0 fi printf '%s%s%s%*s%s%*s%s%s%s\n' "$C_DIM" "$CARD_SIDE" "$C_RESET" "$CARD_CONTENT_INDENT" '' "$line" \ $((fill + CARD_TRAILING_GAP)) '' "$C_DIM" "$CARD_SIDE" "$C_RESET" } print_card_bottom() { local width="$1" printf '%s%s%s%s%s\n' "$C_DIM" "$CARD_BOTTOM_LEFT" "$(repeat_char "$CARD_HORIZONTAL" $((width - CARD_SIDES_WIDTH)))" "$CARD_BOTTOM_RIGHT" "$C_RESET" } print_titled_card() { local title_color="$1" local title="$2" local width="$3" local height="$4" local summary="${5:-}" local -a content_lines=() local index mapfile -t content_lines while ((${#content_lines[@]} > 0)) && [[ -z "${content_lines[-1]}" ]]; do unset 'content_lines[-1]' done if ((${#content_lines[@]} > height)); then height="${#content_lines[@]}" fi print_card_top "$title_color" "$title" "$width" "$summary" for ((index = 0; index < height; index++)); do print_card_line "${content_lines[index]:-}" "$width" done print_card_bottom "$width" } print_titled_card_pair() { local left_title="$1" local right_title="$2" local left_text="$3" local right_text="$4" local -a left_card=() right_card=() local height index mapfile -t left_card <<<"$left_text" mapfile -t right_card <<<"$right_text" height="${#left_card[@]}" if ((${#right_card[@]} > height)); then height="${#right_card[@]}" fi mapfile -t left_card < <(printf '%s\n' "${left_card[@]}" | print_titled_card "$C_TITLE" "$left_title" "$LEFT_CARD_WIDTH" "$height") mapfile -t right_card < <(printf '%s\n' "${right_card[@]}" | print_titled_card "$C_TITLE" "$right_title" "$RIGHT_CARD_WIDTH" "$height") for ((index = 0; index < ${#left_card[@]}; index++)); do printf '%s%*s%s\n' "${left_card[index]}" "$CARD_GAP" '' "${right_card[index]:-}" done } print_bar_block() { local -a block_lines=() local line mapfile -t block_lines for line in "${block_lines[@]}"; do line="${line%%+( )}" if [[ -z "$line" ]]; then printf '%s%s%s\n' "$C_BAR" "$HEADER_MARK" "$C_RESET" continue fi printf '%s%s%s%*s%s\n' "$C_BAR" "$HEADER_MARK" "$C_RESET" "$CARD_CONTENT_INDENT" '' "$line" done printf '\n' } stop_jobs() { local pid for pid in $(jobs -pr); do kill "$pid" 2>/dev/null || true done } print_debug_environment() { local curl_version curl_version=$(curl --version) printf 'Arguments: %s\n' "$*" printf 'Bash: %s\n' "$BASH_VERSION" printf 'System: %s\n' "$(uname -srm)" printf 'Locale: %s\n' "${LC_ALL:-${LANG:-}}" printf 'Curl: %s\n' "${curl_version%%$'\n'*}" printf 'Jq: %s\n\n' "$(jq --version)" } add_debug_mask() { local value="$1" local replacement="$2" if [[ -n "$value" ]]; then DEBUG_MASKS+=("$value" "$replacement") fi } collect_debug_masks() { local ip_version DEBUG_MASKS=() if [[ -n "$EXTERNAL_IPV4" ]]; then add_debug_mask "$EXTERNAL_IPV4" "$(mask_ipv4 "$EXTERNAL_IPV4")" add_debug_mask "${EXTERNAL_IPV4%.*}." "${EXTERNAL_IPV4%.*.*}.*." fi if [[ -n "$EXTERNAL_IPV6" ]]; then add_debug_mask "$EXTERNAL_IPV6" "$(mask_ipv6 "$EXTERNAL_IPV6")" fi for ip_version in 4 6; do add_debug_mask "${REGISTRATION_ORG[$ip_version]:-}" "$MASKED_ORG" if [[ -n "${REGISTRATION_ASN[$ip_version]:-}" ]]; then add_debug_mask "AS${REGISTRATION_ASN[$ip_version]}" "$MASKED_ASN" fi done add_debug_mask "$PROXY_ADDR" "$MASKED_VALUE" add_debug_mask "$HOME" "$MASKED_HOME" } print_debug_traces() { local trace cat -- "$DEBUG_TRACE_FILE" for trace in "$DEBUG_TRACE_DIR"/*; do if [[ ! -e "$trace" ]]; then continue fi printf '\n=== job %s\n' "${trace##*/}" cat -- "$trace" done } save_debug_log() { collect_debug_masks DEBUG_LOG_FILE=$(mktemp "${TMPDIR:-/tmp}/$DEBUG_LOG_TEMPLATE") print_debug_traces | LC_ALL=C awk -v limit="$DEBUG_RECORD_LIMIT" -v masked="$MASKED_VALUE" \ "$DEBUG_LOG_FILTER" "${DEBUG_MASKS[@]}" >"$DEBUG_LOG_FILE" } confirm_debug_log_upload() { if [[ "$ASSUME_YES" == true ]]; then return 0 fi if ! has_terminal; then print_note "Run with --yes to upload it" return 1 fi printf '%sIP addresses, ASN and location are masked, but the log is public for anyone with the link.%s\n' "$M_DIM" "$M_RESET" >/dev/tty ask_yes_no "Upload it and get a link for the issue?" } upload_request() { local url="$1" shift local response_file="$WORK_DIR/$UPLOAD_RESPONSE_NAME" rm -f -- "$response_file" HTTP_CURL_EXIT="$CURL_EXIT_OK" HTTP_CODE=$(curl "${CURL_ARGS[@]}" --output "$response_file" --write-out "$UPLOAD_WRITE_OUT" "$@" "$url") || HTTP_CURL_EXIT=$? HTTP_BODY="" if [[ -f "$response_file" ]]; then HTTP_BODY=$(<"$response_file") fi } upload_to_x0_at() { upload_request "$X0_AT_UPLOAD_URL" --form "file=@$DEBUG_LOG_FILE" } upload_to_litterbox() { upload_request "$LITTERBOX_UPLOAD_URL" --form reqtype=fileupload --form "time=$LITTERBOX_RETENTION" \ --form "fileToUpload=@$DEBUG_LOG_FILE" } print_debug_log_url() { if [[ "$HTTP_CURL_EXIT" != "$CURL_EXIT_OK" || ! "$HTTP_BODY" =~ $UPLOADED_URL_PATTERN ]]; then return 1 fi printf 'Debug log: %s%s%s\n' "$M_VALUE" "$HTTP_BODY" "$M_RESET" >&2 } upload_debug_log() { build_curl_args "" CURL_ARGS+=(--max-time "$DEBUG_UPLOAD_TIMEOUT" --speed-limit "$UPLOAD_SPEED_LIMIT" --speed-time "$UPLOAD_SPEED_TIME") print_note "Uploading the debug log..." upload_to_x0_at if print_debug_log_url; then return 0 fi print_warning "$X0_AT_NAME failed: $(response_failure_reason), trying $LITTERBOX_NAME" upload_to_litterbox if print_debug_log_url; then return 0 fi print_warning "$LITTERBOX_NAME failed: $(response_failure_reason)" print_error "failed to upload the debug log, attach $DEBUG_LOG_FILE to the issue instead" } start_debug_trace() { local trace_fd if [[ "$DEBUG" != true ]]; then return fi DEBUG_TRACE_FILE="$WORK_DIR/debug-trace" DEBUG_TRACE_DIR="$WORK_DIR/debug-traces" mkdir -- "$DEBUG_TRACE_DIR" print_debug_environment "$@" >"$DEBUG_TRACE_FILE" exec {trace_fd}>>"$DEBUG_TRACE_FILE" BASH_XTRACEFD="$trace_fd" PS4="$DEBUG_TRACE_PS4" set -x } finish_debug_trace() { if [[ -z "$DEBUG_TRACE_FILE" ]]; then return 0 fi set +x save_debug_log printf '\nDebug log saved to %s%s%s\n' "$M_VALUE" "$DEBUG_LOG_FILE" "$M_RESET" >&2 if confirm_debug_log_upload; then upload_debug_log fi } cleanup() { stop_jobs finish_debug_trace if [[ -n "$WORK_DIR" ]]; then rm -rf -- "$WORK_DIR" fi } create_work_dir() { WORK_DIR=$(mktemp -d) trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM } request_cache_entry() { local key key=$(printf '%s\0' "$@" | cksum) printf '%s/http-%s' "$WORK_DIR" "${key// /-}" } build_curl_args() { local ip_version="$1" CURL_ARGS=("${CURL_BASE_ARGS[@]}" --max-time "$CURL_TIMEOUT" --retry-max-time "$CURL_TIMEOUT") if [[ -n "$PROXY_ADDR" ]]; then CURL_ARGS+=(--proxy "socks5h://$PROXY_ADDR") elif [[ -n "$ip_version" ]]; then CURL_ARGS+=("-$ip_version") fi if [[ -n "$INTERFACE_NAME" ]]; then CURL_ARGS+=(--interface "$INTERFACE_NAME") fi } fetch_to_cache() { local entry="$1" local ip_version="$2" local url="$3" shift 3 local curl_exit=0 local http_code build_curl_args "$ip_version" curl "${CURL_ARGS[@]}" --output "$entry.body" --write-out "$CURL_WRITE_OUT" "$@" "$url" >"$entry.meta" || curl_exit=$? printf '%s\n' "$curl_exit" >>"$entry.meta" read -r http_code <"$entry.meta" log_verbose "IPv${ip_version:-4/6} $url: HTTP $http_code, curl exit $curl_exit" touch -- "$entry.body" : >"$entry.done" } wait_for_cache_entry() { local entry="$1" while [[ ! -e "$entry.done" ]]; do sleep "$CACHE_POLL_INTERVAL" done } read_cache_entry() { local entry="$1" { read -r HTTP_CODE read -r HTTP_REDIRECT_URL read -r HTTP_CURL_EXIT } <"$entry.meta" HTTP_BODY=$(<"$entry.body") } http_request() { local ip_version="$1" local url="$2" shift 2 local entry HTTP_IP_VERSION="$ip_version" entry=$(request_cache_entry "$ip_version" "$url" "$@") if (set -o noclobber && : >"$entry.lock") 2>/dev/null; then fetch_to_cache "$entry" "$ip_version" "$url" "$@" fi wait_for_cache_entry "$entry" read_cache_entry "$entry" } curl_failure_reason() { case "$HTTP_CURL_EXIT" in "$CURL_EXIT_DNS") printf '%s' "$REASON_DNS" ;; "$CURL_EXIT_CONNECT") printf '%s' "$REASON_CONNECT" ;; "$CURL_EXIT_TIMEOUT") printf '%s' "$REASON_TIMEOUT" ;; "$CURL_EXIT_TLS_HANDSHAKE" | "$CURL_EXIT_TLS_CERTIFICATE") printf '%s' "$REASON_TLS" ;; "$CURL_EXIT_PROXY") printf '%s' "$REASON_PROXY" ;; *) printf '%s' "$REASON_NETWORK" ;; esac } http_failure_reason() { if [[ "$HTTP_CURL_EXIT" != "$CURL_EXIT_OK" ]]; then curl_failure_reason return 0 fi case "$HTTP_CODE" in "$HTTP_FORBIDDEN") printf '%s (%s)' "$REASON_FORBIDDEN" "$HTTP_CODE" ;; "$HTTP_TOO_MANY_REQUESTS") printf '%s (%s)' "$REASON_RATE_LIMITED" "$HTTP_CODE" ;; 5[0-9][0-9]) printf '%s (%s)' "$REASON_SERVER_ERROR" "$HTTP_CODE" ;; esac } response_failure_reason() { local reason reason=$(http_failure_reason) if [[ -n "$reason" ]]; then printf '%s' "$reason" return 0 fi printf '%s (%s)' "$REASON_UNEXPECTED_RESPONSE" "$HTTP_CODE" } is_ip_address() { local ip_version="$1" local ip="$2" case "$ip_version" in 4) [[ "$ip" =~ $IPV4_PATTERN ]] ;; 6) [[ "$ip" =~ $IPV6_PATTERN ]] ;; esac } print_identity_answer() { local ip_version="$1" local answers_dir="$2" local answer_file local answer for answer_file in "$answers_dir"/*; do if [[ ! -f "$answer_file" ]]; then continue fi answer=$(<"$answer_file") if is_ip_address "$ip_version" "$answer"; then printf '%s' "$answer" return 0 fi done return 1 } fetch_external_ip() { local ip_version="$1" local answers_dir="$WORK_DIR/identity-ipv$ip_version" local -a urls local -a pids=() local index case "$ip_version" in 4) urls=("${IPV4_IDENTITY_URLS[@]}") ;; 6) urls=("${IPV6_IDENTITY_URLS[@]}") ;; esac mkdir -p -- "$answers_dir" build_curl_args "$ip_version" for index in "${!urls[@]}"; do # curl runs directly, not via http_request: killing a wrapper subshell would leave its curl running until the timeout curl "${CURL_ARGS[@]}" --output "$answers_dir/$index" "${urls[$index]}" & pids+=("$!") done for index in "${!urls[@]}"; do wait -n || true if print_identity_answer "$ip_version" "$answers_dir"; then break fi done kill "${pids[@]}" 2>/dev/null || true wait 2>/dev/null } mask_ipv4() { local ip="$1" printf '%s.*.*' "${ip%.*.*}" } mask_ipv6() { local ip="$1" local head="${ip%%::*}" local tail="" local -a left right groups local missing if [[ "$ip" == *::* ]]; then tail="${ip#*::}" fi IFS=: read -ra left <<<"$head" IFS=: read -ra right <<<"$tail" missing=$((IPV6_GROUP_COUNT - ${#left[@]} - ${#right[@]})) groups=("${left[@]}") while ((missing-- > 0)); do groups+=(0) done groups+=("${right[@]}") printf '%s:%s:%s::' "${groups[@]:0:3}" } json_value() { local filter="$1" jq -r "$filter" <<<"$HTTP_BODY" 2>/dev/null || true } request_maxmind_token() { local cookie_jar="$WORK_DIR/maxmind-cookies" local csrf_token http_request "$ANY_TRANSPORT" "$MAXMIND_DEMO_PAGE_URL" --cookie-jar "$cookie_jar" if [[ ! "$HTTP_BODY" =~ $MAXMIND_CSRF_PATTERN ]]; then MAXMIND_TOKEN_FAILURE=$(response_failure_reason) return fi csrf_token="${BASH_REMATCH[1]}" http_request "$ANY_TRANSPORT" "$MAXMIND_TOKEN_URL" --request POST --cookie "$cookie_jar" --header "X-CSRF-Token: $csrf_token" if [[ "$HTTP_CODE" != "$HTTP_CREATED" ]]; then MAXMIND_TOKEN_FAILURE=$(response_failure_reason) return fi MAXMIND_TOKEN=$(json_value '.token // empty') } no_demo_token_reason() { if [[ -z "$MAXMIND_TOKEN_FAILURE" ]]; then printf '%s' "$REASON_NO_DEMO_TOKEN" return fi printf '%s: %s' "$REASON_NO_DEMO_TOKEN" "$MAXMIND_TOKEN_FAILURE" } fetch_maxmind_city() { local ip_version="$1" local ip="$2" local other_ip_version=4 if [[ "$ip_version" == 4 ]]; then other_ip_version=6 fi http_request "$ip_version" "$MAXMIND_CITY_URL/$ip?demo=1" --header "Authorization: Bearer $MAXMIND_TOKEN" if [[ "$HTTP_CODE" != "$HTTP_PAYMENT_REQUIRED" || -n "$PROXY_ADDR" || -z "$(external_ip "$other_ip_version")" ]]; then return fi http_request "$other_ip_version" "$MAXMIND_CITY_URL/$ip?demo=1" --header "Authorization: Bearer $MAXMIND_TOKEN" } maxmind_failure_reason() { local reason reason=$(http_failure_reason) if [[ -n "$reason" ]]; then printf '%s' "$reason" return fi case "$HTTP_CODE" in "$HTTP_OK") ;; "$HTTP_PAYMENT_REQUIRED") printf '%s' "$REASON_DAILY_LIMIT" ;; *) printf '%s (%s)' "$REASON_UNEXPECTED_RESPONSE" "$HTTP_CODE" ;; esac } lookup_maxmind_registration() { local ip_version="$1" local ip="$2" local failure if [[ -z "$MAXMIND_TOKEN" ]]; then REGISTRATION_MAXMIND_FAILURE[$ip_version]=$(no_demo_token_reason) return 1 fi fetch_maxmind_city "$ip_version" "$ip" failure=$(maxmind_failure_reason) if [[ -n "$failure" ]]; then REGISTRATION_MAXMIND_FAILURE[$ip_version]="$failure" return 1 fi REGISTRATION_ASN[$ip_version]=$(json_value '.traits.autonomous_system_number // empty') REGISTRATION_ORG[$ip_version]=$(json_value '.traits.autonomous_system_organization // empty') } lookup_ripe_registration() { local ip_version="$1" local ip="$2" http_request "$ip_version" "$RIPESTAT_PREFIX_OVERVIEW_URL?resource=$ip" REGISTRATION_ASN[$ip_version]=$(json_value '.data.asns[0].asn // empty') REGISTRATION_ORG[$ip_version]=$(json_value '.data.asns[0].holder // empty | sub("^\\S+ (- )?"; "")') } lookup_registration_country() { local ip_version="$1" local ip="$2" http_request "$ip_version" "$RIPESTAT_RIR_STATS_COUNTRY_URL?resource=$ip" REGISTRATION_COUNTRY[$ip_version]=$(json_value '.data.located_resources[0].location // empty') } lookup_ip_registration() { local ip_version="$1" local ip="$2" lookup_registration_country "$ip_version" "$ip" if lookup_maxmind_registration "$ip_version" "$ip"; then log_verbose "IPv$ip_version ASN from MaxMind" return fi log_verbose "IPv$ip_version ASN from RIPE, MaxMind: ${REGISTRATION_MAXMIND_FAILURE[$ip_version]}" lookup_ripe_registration "$ip_version" "$ip" } count_running_jobs() { jobs -pr | wc -l } is_progress_shown() { [[ -t 2 && "$VERBOSE" != true ]] } draw_spinner_line() { local text="$1" local spinner="${PROGRESS_SPINNER_FRAMES[PROGRESS_FRAME % ${#PROGRESS_SPINNER_FRAMES[@]}]}" PROGRESS_FRAME=$((PROGRESS_FRAME + 1)) printf '%s%s%s%s %s' "$CLEAR_LINE" "$M_ACCENT" "$spinner" "$M_RESET" "$text" >&2 } clear_progress() { if ! is_progress_shown; then return fi printf '%s' "$CLEAR_LINE" >&2 } prefetch_ip_registration() { local ip_version="$1" local ip="$2" lookup_registration_country "$ip_version" "$ip" request_maxmind_token if [[ -n "$MAXMIND_TOKEN" ]]; then fetch_maxmind_city "$ip_version" "$ip" if [[ -z "$(maxmind_failure_reason)" ]]; then return fi fi lookup_ripe_registration "$ip_version" "$ip" } fetch_external_ip_and_registration() { local ip_version="$1" local ip_file="$2" local ip fetch_external_ip "$ip_version" >"$ip_file" ip=$(<"$ip_file") if [[ -z "$ip" ]]; then return fi case "$ip_version" in 4) EXTERNAL_IPV4="$ip" ;; 6) EXTERNAL_IPV6="$ip" ;; esac prefetch_ip_registration "$ip_version" "$ip" } wait_for_preparation() { while (($(count_running_jobs) > 0)); do if is_progress_shown; then draw_spinner_line "$PREPARATION_LABEL" fi sleep "$JOB_POLL_INTERVAL" done clear_progress } discover_external_ips() { local ipv4_file="$WORK_DIR/external-ipv4" local ipv6_file="$WORK_DIR/external-ipv6" : >"$ipv4_file" : >"$ipv6_file" request_maxmind_token & if [[ "$IPV6_ONLY" != true ]]; then fetch_external_ip_and_registration 4 "$ipv4_file" & fi if [[ "$IPV4_ONLY" != true ]]; then fetch_external_ip_and_registration 6 "$ipv6_file" & fi wait_for_preparation EXTERNAL_IPV4=$(<"$ipv4_file") EXTERNAL_IPV6=$(<"$ipv6_file") log_verbose "External IPv4: ${EXTERNAL_IPV4:-not found}, IPv6: ${EXTERNAL_IPV6:-not found}" if [[ -n "$EXTERNAL_IPV4" || -n "$EXTERNAL_IPV6" ]]; then return fi if [[ -n "$INTERFACE_NAME" ]]; then print_error "failed to detect an external IP address through interface $INTERFACE_NAME, check that it exists and routes traffic out" exit 1 fi print_error "failed to detect an external IP address, check the network connection" exit 1 } lookup_ip_registrations() { request_maxmind_token if [[ -n "$EXTERNAL_IPV4" ]]; then lookup_ip_registration 4 "$EXTERNAL_IPV4" fi if [[ -n "$EXTERNAL_IPV6" ]]; then lookup_ip_registration 6 "$EXTERNAL_IPV6" fi } escape_json_string() { local -n json_value_ref=$1 json_value_ref=${json_value_ref//\\/\\\\} json_value_ref=${json_value_ref//\"/\\\"} json_value_ref=${json_value_ref//$'\n'/\\n} json_value_ref=${json_value_ref//$'\r'/\\r} json_value_ref=${json_value_ref//$'\t'/\\t} json_value_ref=${json_value_ref//[$'\001'-$'\037']/} } print_result() { local group="$1" local service="$2" local ip_version="$3" local country="$4" local status="$5" local reason="$6" local field log_verbose "$group / $service IPv$ip_version: country ${country:--}, status ${status:--}, reason ${reason:--}" for field in group service ip_version country status reason; do escape_json_string "$field" done printf '{"group":"%s","service":"%s","ip_version":"%s","country":"%s","status":"%s","reason":"%s"}\n' \ "$group" "$service" "$ip_version" "$country" "$status" "$reason" } print_progress_bar() { local finished="$1" local done_width=$((PROGRESS_BAR_WIDTH * finished / JOB_COUNT)) local done_part left_part printf -v done_part '%*s' "$done_width" '' printf -v left_part '%*s' "$((PROGRESS_BAR_WIDTH - done_width))" '' printf '%s%s%s%s%s' "$M_ACCENT" "${done_part// /$PROGRESS_BAR_DONE}" "$M_DIM" "${left_part// /$PROGRESS_BAR_LEFT}" "$M_RESET" } draw_progress() { local finished if ! is_progress_shown; then return fi finished=$((JOB_COUNT - $(count_running_jobs))) draw_spinner_line "$PROGRESS_LABEL $(print_progress_bar "$finished") $M_DIM$finished/$JOB_COUNT$M_RESET" } create_job_slots() { local slots_fifo="$WORK_DIR/job-slots" local slots mkfifo -- "$slots_fifo" exec {JOB_SLOTS_FD}<>"$slots_fifo" printf -v slots '%*s' "$MAX_PARALLEL_JOBS" '' printf '%s' "$slots" >&"$JOB_SLOTS_FD" } take_job_slot() { read -r -N 1 -u "$JOB_SLOTS_FD" _ } return_job_slot() { printf ' ' >&"$JOB_SLOTS_FD" } run_job() { local result_file="$1" shift local trace_fd if [[ -n "$DEBUG_TRACE_DIR" ]]; then exec {trace_fd}>"$DEBUG_TRACE_DIR/${result_file##*/}" BASH_XTRACEFD="$trace_fd" fi take_job_slot trap return_job_slot EXIT "$@" >"$result_file" # bash 4.4 skips the EXIT trap of a background function that returns without exit exit } run_check() { local check="$1" local ip_version="$2" local id result id=$(check_id "$check") result=$("$check" "$ip_version") printf '{"id":"%s",%s\n' "$id" "${result#\{}" } start_job() { local result_file JOB_COUNT=$((JOB_COUNT + 1)) printf -v result_file '%s/%s%04d' "$WORK_DIR" "$RESULT_FILE_PREFIX" "$JOB_COUNT" run_job "$result_file" "$@" & } start_check() { local check="$1" if is_checked_once "$(check_group "$check")"; then start_job run_check "$check" "$PROXY_IP_VERSION" return fi if [[ -n "$EXTERNAL_IPV4" ]]; then start_job run_check "$check" 4 fi if [[ -n "$EXTERNAL_IPV6" ]]; then start_job run_check "$check" 6 fi } is_dual_stack() { [[ -n "$EXTERNAL_IPV4" && -n "$EXTERNAL_IPV6" ]] } is_checked_once() { local group="$1" [[ -n "$PROXY_ADDR" && "$group" != "$GROUP_GEOIP" ]] && is_dual_stack } is_check_selected() { local check="$1" is_group_selected "$(check_group "$check")" || is_in_list "$check" "${SELECTED_CHECKS[@]}" } external_ip() { local ip_version="$1" case "$ip_version" in 4) printf '%s' "$EXTERNAL_IPV4" ;; 6) printf '%s' "$EXTERNAL_IPV6" ;; esac } is_service_without_ipv6() { if [[ "$HTTP_IP_VERSION" != 6 || -n "$PROXY_ADDR" ]]; then return 1 fi [[ "$HTTP_CURL_EXIT" == "$CURL_EXIT_DNS" || "$HTTP_CURL_EXIT" == "$CURL_EXIT_CONNECT" ]] } print_failure() { local group="$1" local service="$2" local ip_version="$3" local reason="$4" print_result "$group" "$service" "$ip_version" "" "" "$reason" } print_country() { local group="$1" local service="$2" local ip_version="$3" local country="$4" local reason if is_service_without_ipv6; then print_result "$group" "$service" "$ip_version" "" "$STATUS_NONE" "$REASON_NO_IPV6" return fi reason=$(http_failure_reason) if [[ -n "$reason" ]]; then print_failure "$group" "$service" "$ip_version" "$reason" return fi if [[ ! "$country" =~ $COUNTRY_CODE_PATTERN ]]; then print_failure "$group" "$service" "$ip_version" "$REASON_UNEXPECTED_RESPONSE" return fi print_result "$group" "$service" "$ip_version" "$country" "$STATUS_YES" "" } print_status() { local group="$1" local service="$2" local ip_version="$3" local status="$4" local reason if is_service_without_ipv6; then print_result "$group" "$service" "$ip_version" "" "$STATUS_NONE" "$REASON_NO_IPV6" return fi reason=$(http_failure_reason) if [[ -n "$status" ]]; then print_result "$group" "$service" "$ip_version" "" "$status" "$reason" return fi print_failure "$group" "$service" "$ip_version" "${reason:-$REASON_UNEXPECTED_RESPONSE}" } check_geoip_json() { local service="$1" local ip_version="$2" local transport="$3" local url="$4" local filter="$5" shift 5 http_request "$transport" "$url" "$@" print_country "$GROUP_GEOIP" "$service" "$ip_version" "$(json_value "$filter")" } check_geoip_maxmind() { local ip_version="$1" local failure if [[ -z "$MAXMIND_TOKEN" ]]; then print_failure "$GROUP_GEOIP" MaxMind "$ip_version" "$(no_demo_token_reason)" return fi fetch_maxmind_city "$ip_version" "$(external_ip "$ip_version")" failure=$(maxmind_failure_reason) if [[ -n "$failure" ]]; then print_failure "$GROUP_GEOIP" MaxMind "$ip_version" "$failure" return fi print_country "$GROUP_GEOIP" MaxMind "$ip_version" "$(json_value '.country.iso_code // empty')" } check_geoip_ripe() { local ip_version="$1" check_geoip_json RIPE "$ip_version" "$ip_version" "$RIPE_RDAP_IP_URL/$(external_ip "$ip_version")" '.country // empty' --location } check_geoip_ipinfo_io() { local ip_version="$1" local url case "$ip_version" in 4) url="$IPINFO_IPV4_URL" ;; 6) url="$IPINFO_IPV6_URL" ;; esac check_geoip_json ipinfo.io "$ip_version" "$ip_version" "$url/$(external_ip "$ip_version")" '.data.country // empty' } cloudflare_trace_ip_version() { if [[ ! "$HTTP_BODY" =~ $CLOUDFLARE_IP_PATTERN ]]; then return fi case "${BASH_REMATCH[2]}" in "$EXTERNAL_IPV4") printf '4' ;; "$EXTERNAL_IPV6") printf '6' ;; esac } check_geoip_cloudflare() { local ip_version="$1" local country="" local used_ip_version http_request "$ip_version" "$CLOUDFLARE_TRACE_URL" used_ip_version=$(cloudflare_trace_ip_version) if [[ -n "$PROXY_ADDR" && -n "$used_ip_version" && "$used_ip_version" != "$ip_version" ]]; then print_result "$GROUP_GEOIP" Cloudflare "$ip_version" "" "$STATUS_NONE" "$REASON_PROXY_PICKED$used_ip_version" return fi if [[ "$HTTP_BODY" =~ $CLOUDFLARE_COUNTRY_PATTERN ]]; then country="${BASH_REMATCH[2]}" fi print_country "$GROUP_GEOIP" Cloudflare "$ip_version" "$country" } check_geoip_ipregistry_co() { local ip_version="$1" check_geoip_json ipregistry.co "$ip_version" "$ip_version" \ "$IPREGISTRY_URL/$(external_ip "$ip_version")?hostname=true&key=$IPREGISTRY_KEY" '.location.country.code // empty' \ --header "Origin: $IPREGISTRY_ORIGIN" } check_geoip_ipapi_co() { local ip_version="$1" check_geoip_json ipapi.co "$ip_version" "$ip_version" "$IPAPI_CO_URL/$(external_ip "$ip_version")/json" '.country // empty' \ --header "Referer: $IPAPI_CO_REFERER" } check_geoip_ifconfig_co() { local ip_version="$1" http_request "$ip_version" "$IFCONFIG_CO_URL?ip=$(external_ip "$ip_version")" print_country "$GROUP_GEOIP" ifconfig.co "$ip_version" "$HTTP_BODY" } check_geoip_ip2location_io() { local ip_version="$1" check_geoip_json ip2location.io "$ip_version" "$ip_version" "$IP2LOCATION_URL?ip=$(external_ip "$ip_version")" '.country_code // empty' } check_geoip_country_is() { local ip_version="$1" check_geoip_json country.is "$ip_version" "$ip_version" "$COUNTRY_IS_URL/$(external_ip "$ip_version")" '.country // empty' } check_geoip_geoapify_com() { local ip_version="$1" check_geoip_json geoapify.com "$ip_version" "$ip_version" \ "$GEOAPIFY_URL?ip=$(external_ip "$ip_version")&apiKey=$GEOAPIFY_KEY" '.country.iso_code // empty' } check_geoip_geojs_io() { local ip_version="$1" check_geoip_json geojs.io "$ip_version" "$ip_version" "$GEOJS_URL?ip=$(external_ip "$ip_version")" '.[0].country // empty' } check_geoip_ipbase_com() { local ip_version="$1" check_geoip_json ipbase.com "$ip_version" "$ip_version" "$IPBASE_URL?ip=$(external_ip "$ip_version")" '.data.location.country.alpha2 // empty' } check_geoip_ipquery_io() { local ip_version="$1" check_geoip_json ipquery.io "$ip_version" "$ip_version" "$IPQUERY_URL/$(external_ip "$ip_version")" '.location.country_code // empty' } check_geoip_ipwho_is() { local ip_version="$1" check_geoip_json ipwho.is "$ip_version" "$ip_version" "$IPWHO_URL/$(external_ip "$ip_version")" '.country_code // empty' } check_geoip_ip_api_com() { local ip_version="$1" check_geoip_json ip-api.com "$ip_version" "$IPV4_TRANSPORT" "$IP_API_URL/$(external_ip "$ip_version")?fields=countryCode" '.countryCode // empty' \ --header "Origin: $IP_API_ORIGIN" } check_platforms_json() { local service="$1" local ip_version="$2" local url="$3" local filter="$4" shift 4 http_request "$ip_version" "$url" "$@" print_country "$GROUP_PLATFORMS" "$service" "$ip_version" "$(json_value "$filter")" } check_platforms_pattern() { local service="$1" local ip_version="$2" local url="$3" local pattern="$4" shift 4 local country="" http_request "$ip_version" "$url" "$@" if [[ "$HTTP_BODY" =~ $pattern ]]; then country="${BASH_REMATCH[1]}" fi print_country "$GROUP_PLATFORMS" "$service" "$ip_version" "$country" } check_platforms_google() { local ip_version="$1" check_platforms_pattern Google "$ip_version" "$GOOGLE_URL" "$GOOGLE_COUNTRY_PATTERN" --user-agent "$USER_AGENT" } check_platforms_youtube() { local ip_version="$1" local country http_request "$ip_version" "$YOUTUBE_URL" country=$(jq -r '.[0][2][0][0][1] // empty' <<<"${HTTP_BODY#"$YOUTUBE_JSON_PREFIX"}" 2>/dev/null || true) print_country "$GROUP_PLATFORMS" YouTube "$ip_version" "$country" } check_platforms_twitch() { local ip_version="$1" check_platforms_json Twitch "$ip_version" "$TWITCH_URL" '.[0].data.requestInfo.countryCode // empty' \ --header "Client-Id: $TWITCH_CLIENT_ID" --json "$TWITCH_QUERY" } check_platforms_chatgpt() { local ip_version="$1" check_platforms_pattern ChatGPT "$ip_version" "$CHATGPT_URL" "$CHATGPT_COUNTRY_PATTERN" \ --request POST --header "Statsig-Api-Key: $CHATGPT_STATSIG_API_KEY" } check_platforms_claude() { local ip_version="$1" check_platforms_json Claude "$ip_version" "$CLAUDE_URL" '.statsig.user.country // empty' \ --user-agent "$CLAUDE_USER_AGENT" --header "$CLAUDE_PLATFORM_HEADER" } check_platforms_netflix() { local ip_version="$1" check_platforms_json Netflix "$ip_version" "$NETFLIX_URL" '.client.location.country // empty' } check_platforms_spotify() { local ip_version="$1" check_platforms_json Spotify "$ip_version" "$SPOTIFY_URL" '.country // empty' --header "X-Client-Id: $SPOTIFY_CLIENT_ID" } check_platforms_reddit() { local ip_version="$1" http_request "$ip_version" "$REDDIT_NOT_FOUND_URL" --user-agent "$USER_AGENT" if [[ "$HTTP_CODE" == "$HTTP_FORBIDDEN" ]]; then print_result "$GROUP_PLATFORMS" Reddit "$ip_version" "" "$STATUS_IPBLOCK" "$(http_failure_reason)" return fi check_platforms_pattern Reddit "$ip_version" "$REDDIT_NOT_FOUND_URL" "$REDDIT_COUNTRY_PATTERN" --user-agent "$USER_AGENT" } check_platforms_disney_plus() { local ip_version="$1" check_platforms_json Disney+ "$ip_version" "$DISNEY_PLUS_URL" '.extensions.sdk.session.location.countryCode // empty' \ --header "Authorization: Bearer $DISNEY_PLUS_API_KEY" --json "$DISNEY_PLUS_QUERY" } check_platforms_tiktok() { local ip_version="$1" check_platforms_json TikTok "$ip_version" "$TIKTOK_URL" '.body.appProps.region // empty' } check_platforms_apple() { local ip_version="$1" http_request "$ip_version" "$APPLE_URL" print_country "$GROUP_PLATFORMS" Apple "$ip_version" "$HTTP_BODY" } check_platforms_steam() { local ip_version="$1" check_platforms_pattern Steam "$ip_version" "$STEAM_URL" "$STEAM_COUNTRY_PATTERN" --head } check_platforms_microsoft() { local ip_version="$1" check_platforms_pattern Microsoft "$ip_version" "$MICROSOFT_URL" "$MICROSOFT_COUNTRY_PATTERN" } check_platforms_ookla() { local ip_version="$1" check_platforms_json Ookla "$ip_version" "$OOKLA_URL" '.location.countryCode // empty' } check_platforms_jetbrains() { local ip_version="$1" check_platforms_json JetBrains "$ip_version" "$JETBRAINS_URL" '.code // empty' } check_platforms_playstation() { local ip_version="$1" check_platforms_pattern PlayStation "$ip_version" "$PLAYSTATION_URL" "$PLAYSTATION_COUNTRY_PATTERN" --head } print_edge() { local service="$1" local ip_version="$2" local country="$3" local iata="$4" local node="$5" print_country "$GROUP_CDN" "$service" "$ip_version" "$country" | jq -c --arg iata "$iata" --arg node "$node" '. + {iata: $iata, node: $node}' } iata_country() { local iata="$1" if [[ -z "$iata" ]]; then return fi http_request "$IPV4_TRANSPORT" "$AIRPORT_CODES_URL" \ --header "APC-Auth: $AIRPORT_CODES_KEY" --header "Referer: $AIRPORT_CODES_REFERER" --data "iata=$iata" json_value '.airport.country.iso // empty' } youtube_node_iata() { local node="$1" if [[ "$node" =~ $GOOGLE_NODE_PATTERN || "$node" =~ $ISP_NODE_PATTERN || "$node" =~ $GGC_NODE_PATTERN ]]; then printf '%s' "${BASH_REMATCH[1]^^}" fi } check_cdn_cloudflare() { local ip_version="$1" http_request "$ip_version" "$CLOUDFLARE_SPEED_META_URL" --header "Referer: $CLOUDFLARE_SPEED_REFERER" print_edge Cloudflare "$ip_version" "$(json_value '.colo.cca2 // empty')" "$(json_value '.colo.iata // empty')" "" } check_cdn_youtube() { local ip_version="$1" local node="" local iata http_request "$ip_version" "$YOUTUBE_CDN_MAPPING_URL" if [[ "$HTTP_BODY" =~ $YOUTUBE_CDN_NODE_PATTERN ]]; then node="${BASH_REMATCH[1]}" fi iata=$(youtube_node_iata "$node") print_edge YouTube "$ip_version" "$(iata_country "$iata")" "$iata" "$node" } check_cdn_netflix() { local ip_version="$1" local node="" local iata="" http_request "$ip_version" "$NETFLIX_URL" if [[ "$(json_value '.targets[0].url // empty')" =~ $NETFLIX_CDN_URL_PATTERN ]]; then node="${BASH_REMATCH[1]}" iata="${BASH_REMATCH[2]^^}" fi print_edge Netflix "$ip_version" "$(json_value '.targets[0].location.country // empty')" "$iata" "$node" } youtube_premium_status() { if [[ "$HTTP_CODE" != "$HTTP_OK" ]]; then return fi if [[ "$HTTP_BODY" == *"$YOUTUBE_PREMIUM_PURCHASE_MARKER"* ]]; then printf '%s' "$STATUS_YES" return fi printf '%s' "$STATUS_NO" } check_availability_youtube_premium() { local ip_version="$1" http_request "$ip_version" "$YOUTUBE_INNERTUBE_BROWSE_URL" --json "$YOUTUBE_PREMIUM_QUERY" print_status "$GROUP_AVAILABILITY" "YouTube Premium" "$ip_version" "$(youtube_premium_status)" } youtube_no_ads_status() { if [[ "$HTTP_CODE" != "$HTTP_OK" || "$HTTP_BODY" != *"$YOUTUBE_PLAYER_RESPONSE_MARKER"* ]]; then return fi if [[ "$HTTP_BODY" == *"$YOUTUBE_VIDEO_AD_MARKER"* ]]; then printf '%s' "$STATUS_NO" return fi printf '%s' "$STATUS_YES" } # YouTube serves ads only to a visitor with cookies, so the first load just collects them. check_availability_youtube_no_ads() { local ip_version="$1" local cookie_jar="$WORK_DIR/$YOUTUBE_COOKIE_JAR_PREFIX$ip_version" local service="$YOUTUBE_NO_ADS_SERVICE_PREFIX ${YOUTUBE_LANG^^}" local viewer_cookie="$YOUTUBE_VIEWER_COOKIE_PREFIX$YOUTUBE_LANG" local accept_language="$ACCEPT_LANGUAGE_PREFIX$YOUTUBE_LANG" local status http_request "$ip_version" "$YOUTUBE_WATCH_URL" --user-agent "$USER_AGENT" \ --header "$viewer_cookie" --header "$accept_language" --cookie-jar "$cookie_jar" http_request "$ip_version" "$YOUTUBE_WATCH_URL" --user-agent "$USER_AGENT" \ --header "$viewer_cookie" --header "$accept_language" --cookie "$cookie_jar" status=$(youtube_no_ads_status) if [[ "$status" == "$STATUS_NO" ]]; then print_result "$GROUP_AVAILABILITY" "$service" "$ip_version" "" "$STATUS_NO" "$REASON_VIDEO_ADS" return fi print_status "$GROUP_AVAILABILITY" "$service" "$ip_version" "$status" } spotify_signup_status() { if [[ "$(json_value '.status')" =~ $SPOTIFY_SIGNUP_BLOCKED_STATUS_PATTERN ]]; then printf '%s' "$STATUS_NO" return fi case "$(json_value '.is_country_launched')" in true) printf '%s' "$STATUS_YES" ;; false) printf '%s' "$STATUS_NO" ;; esac } check_availability_spotify_signup() { local ip_version="$1" http_request "$ip_version" "$SPOTIFY_URL" --header "X-Client-Id: $SPOTIFY_CLIENT_ID" print_status "$GROUP_AVAILABILITY" "Spotify Signup" "$ip_version" "$(spotify_signup_status)" } disney_plus_status() { case "$(json_value '.extensions.sdk.session.inSupportedLocation')" in true) printf '%s' "$STATUS_YES" return ;; false) printf '%s' "$STATUS_NO" return ;; esac if [[ "$(json_value '.errors[0].message // empty')" == *"$DISNEY_PLUS_FORBIDDEN_MARKER"* ]]; then printf '%s' "$STATUS_NO" fi } check_availability_disney_plus() { local ip_version="$1" http_request "$ip_version" "$DISNEY_PLUS_URL" \ --header "Authorization: Bearer $DISNEY_PLUS_API_KEY" --json "$DISNEY_PLUS_QUERY" print_status "$GROUP_AVAILABILITY" Disney+ "$ip_version" "$(disney_plus_status)" } reddit_guest_status() { case "$HTTP_CODE" in "$HTTP_OK") printf '%s' "$STATUS_YES" ;; "$HTTP_FORBIDDEN") printf '%s' "$STATUS_IPBLOCK" ;; esac } check_availability_reddit_guest() { local ip_version="$1" http_request "$ip_version" "$REDDIT_GUEST_URL" --user-agent "$USER_AGENT" print_status "$GROUP_AVAILABILITY" "Reddit Guest" "$ip_version" "$(reddit_guest_status)" } is_google_search_captcha() { [[ "$HTTP_REDIRECT_URL" == *"$GOOGLE_SEARCH_CAPTCHA_PATH"* || "$HTTP_CODE" == "$HTTP_TOO_MANY_REQUESTS" ]] } google_search_status() { if [[ "$HTTP_CODE" == "$HTTP_OK" && "$HTTP_BODY" == *"$GOOGLE_SEARCH_NO_JS_MARKER"* ]]; then printf '%s' "$STATUS_YES" fi } check_availability_google_search() { local ip_version="$1" http_request "$ip_version" "$GOOGLE_SEARCH_URL" \ --user-agent "$USER_AGENT" --header "$GOOGLE_SOCS_COOKIE" --header "$ACCEPT_LANGUAGE_ENGLISH" if is_google_search_captcha; then print_result "$GROUP_AVAILABILITY" "Google Search" "$ip_version" "" "$STATUS_IPBLOCK" "$REASON_CAPTCHA" return fi print_status "$GROUP_AVAILABILITY" "Google Search" "$ip_version" "$(google_search_status)" } gemini_status() { local country="$1" local regions="$2" jq -r --arg code "$country" --argjson regions "$regions" --arg yes "$STATUS_YES" --arg no "$STATUS_NO" \ "$GEMINI_MATCH_FILTER" <<<"$HTTP_BODY" 2>/dev/null || true } check_ai_gemini() { local service="$1" local ip_version="$2" local regions_url="$3" local regions_filter="$4" local regions local country="" http_request "$ip_version" "$regions_url" regions=$(jq -Rsc "$regions_filter | select(length > 0)" <<<"$HTTP_BODY" 2>/dev/null || true) if [[ -z "$regions" ]]; then print_status "$GROUP_AI" "$service" "$ip_version" "" return fi http_request "$ip_version" "$GOOGLE_URL" --user-agent "$USER_AGENT" if [[ "$HTTP_BODY" =~ $GOOGLE_COUNTRY_PATTERN ]]; then country="${BASH_REMATCH[1]}" fi if [[ ! "$country" =~ $COUNTRY_CODE_PATTERN ]]; then print_status "$GROUP_AI" "$service" "$ip_version" "" return fi http_request "$ip_version" "$CLDR_TERRITORIES_URL" print_status "$GROUP_AI" "$service" "$ip_version" "$(gemini_status "$country" "$regions")" } check_ai_gemini_web() { local ip_version="$1" check_ai_gemini "Gemini Web" "$ip_version" "$GEMINI_WEB_REGIONS_URL" "$GEMINI_WEB_REGIONS_FILTER" } check_ai_gemini_api() { local ip_version="$1" check_ai_gemini "Gemini API" "$ip_version" "$GEMINI_API_REGIONS_URL" "$GEMINI_API_REGIONS_FILTER" } ai_api_status() { local region_blocked_marker="$1" if [[ "$HTTP_CODE" == "$HTTP_UNAUTHORIZED" ]]; then printf '%s' "$STATUS_YES" return fi if [[ "$HTTP_CODE" != "$HTTP_FORBIDDEN" ]]; then return fi if [[ "$HTTP_BODY" == *"$region_blocked_marker"* ]]; then printf '%s' "$STATUS_NO" return fi printf '%s' "$STATUS_IPBLOCK" } check_ai_api() { local service="$1" local ip_version="$2" local url="$3" local region_blocked_marker="$4" http_request "$ip_version" "$url" print_status "$GROUP_AI" "$service" "$ip_version" "$(ai_api_status "$region_blocked_marker")" } check_ai_openai_api() { local ip_version="$1" check_ai_api "OpenAI API" "$ip_version" "$OPENAI_API_URL" "$OPENAI_API_REGION_BLOCKED_MARKER" } check_ai_anthropic_api() { local ip_version="$1" check_ai_api "Anthropic API" "$ip_version" "$ANTHROPIC_API_URL" "$ANTHROPIC_API_REGION_BLOCKED_MARKER" } check_ai_grok_api() { local ip_version="$1" check_ai_api "Grok API" "$ip_version" "$GROK_API_URL" "$GROK_API_REGION_BLOCKED_MARKER" } reachability_status() { if [[ "$HTTP_CODE" == "$HTTP_UNAUTHORIZED" ]]; then printf '%s' "$STATUS_YES" fi } check_ai_reachability() { local service="$1" local ip_version="$2" local url="$3" http_request "$ip_version" "$url" print_status "$GROUP_AI" "$service" "$ip_version" "$(reachability_status)" } check_ai_qwen_api() { local ip_version="$1" check_ai_reachability "Qwen API" "$ip_version" "$QWEN_API_URL" } check_ai_qwen_cn_api() { local ip_version="$1" check_ai_reachability "Qwen CN API" "$ip_version" "$QWEN_CN_API_URL" } check_ai_kimi_api() { local ip_version="$1" check_ai_reachability "Kimi API" "$ip_version" "$KIMI_API_URL" } check_ai_kimi_cn_api() { local ip_version="$1" check_ai_reachability "Kimi CN API" "$ip_version" "$KIMI_CN_API_URL" } check_ai_glm_api() { local ip_version="$1" check_ai_reachability "GLM API" "$ip_version" "$GLM_API_URL" } check_ai_glm_cn_api() { local ip_version="$1" check_ai_reachability "GLM CN API" "$ip_version" "$GLM_CN_API_URL" } check_ai_minimax_api() { local ip_version="$1" check_ai_reachability "MiniMax API" "$ip_version" "$MINIMAX_API_URL" } check_ai_minimax_cn_api() { local ip_version="$1" check_ai_reachability "MiniMax CN API" "$ip_version" "$MINIMAX_CN_API_URL" } check_ai_doubao_api() { local ip_version="$1" check_ai_reachability "Doubao API" "$ip_version" "$DOUBAO_API_URL" } check_ai_siliconflow_api() { local ip_version="$1" check_ai_reachability "SiliconFlow API" "$ip_version" "$SILICONFLOW_API_URL" } check_ai_deepseek_api() { local ip_version="$1" check_ai_reachability "DeepSeek API" "$ip_version" "$DEEPSEEK_API_URL" } start_checks() { local check for check in "$@"; do if is_check_selected "$check"; then start_check "$check" fi done } wait_for_jobs() { while (($(count_running_jobs) > 0)); do draw_progress sleep "$JOB_POLL_INTERVAL" done clear_progress } collect_results() { if ((JOB_COUNT == 0)); then RESULTS="[]" return fi RESULTS=$(jq -cs . "$WORK_DIR/$RESULT_FILE_PREFIX"*) } compute_verdict() { local -a leaders mapfile -t leaders < <( jq -r \ --arg yes "$STATUS_YES" \ --arg platforms "$GROUP_PLATFORMS" \ --arg geoip "$GROUP_GEOIP" \ "$VERDICT_FILTER" <<<"$RESULTS" ) VERDICT="" VERDICT_TIED_COUNTRIES=() if ((${#leaders[@]} > 1)); then VERDICT_TIED_COUNTRIES=("${leaders[@]}") return fi VERDICT="${leaders[0]:-}" } collect_attention() { ATTENTION=$( jq -c \ --arg verdict "$VERDICT" \ --arg yes "$STATUS_YES" \ --arg none "$STATUS_NONE" \ --arg cdn "$GROUP_CDN" \ --arg geoip "$GROUP_GEOIP" \ --arg different_country "$REASON_DIFFERENT_COUNTRY" \ --arg video_ads "$REASON_VIDEO_ADS" \ --arg daily_limit "$REASON_DAILY_LIMIT" \ --argjson collapse_min "$ATTENTION_COLLAPSE_MIN" \ "$ATTENTION_FILTER" <<<"$RESULTS" ) } first_ip_version() { if [[ -n "$EXTERNAL_IPV4" ]]; then printf '4' return fi printf '6' } count_abnormal_checks() { jq \ --arg verdict "$VERDICT" \ --arg yes "$STATUS_YES" \ --arg none "$STATUS_NONE" \ --arg cdn "$GROUP_CDN" \ --arg geoip "$GROUP_GEOIP" \ --arg video_ads "$REASON_VIDEO_ADS" \ --arg daily_limit "$REASON_DAILY_LIMIT" \ "$ABNORMAL_CHECKS_FILTER" <<<"$RESULTS" } count_checks() { CHECKS_TOTAL=$(jq --arg cdn "$GROUP_CDN" '[.[] | select(.group != $cdn) | [.group, .service]] | unique | length' <<<"$RESULTS") CHECKS_NORMAL=$((CHECKS_TOTAL - $(count_abnormal_checks))) } checks_health() { local percent if ((CHECKS_TOTAL == 0)); then printf '%s' "$CHECKS_GOOD" return fi percent=$((CHECKS_NORMAL * 100 / CHECKS_TOTAL)) if ((percent >= CHECKS_GOOD_PERCENT)); then printf '%s' "$CHECKS_GOOD" elif ((percent >= CHECKS_FAIR_PERCENT)); then printf '%s' "$CHECKS_FAIR" else printf '%s' "$CHECKS_POOR" fi } checks_bar_color() { case "$(checks_health)" in "$CHECKS_GOOD") printf '%s' "$C_BAR" ;; "$CHECKS_FAIR") printf '%s' "$C_BAR_FAIR" ;; "$CHECKS_POOR") printf '%s' "$C_BAR_POOR" ;; esac } checks_badge_color() { case "$(checks_health)" in "$CHECKS_GOOD") printf '%s' "$C_BADGE" ;; "$CHECKS_FAIR") printf '%s' "$C_BADGE_FAIR" ;; "$CHECKS_POOR") printf '%s' "$C_BADGE_POOR" ;; esac } print_bar() { local color="$1" local filled_parts="$2" local total_parts="$3" local width="$4" local filled_width if ((total_parts == 0)); then return fi filled_width=$((width * filled_parts / total_parts)) printf '%s%s%s%s%s' "$color" "$(repeat_char "$SUMMARY_BAR_CHAR" "$filled_width")" "$C_DIM" \ "$(repeat_char "$SUMMARY_BAR_REST_CHAR" "$((width - filled_width))")" "$C_RESET" } has_protocol_columns() { local group="$1" is_dual_stack && ! is_checked_once "$group" } country_name() { local country="$1" http_request "$(first_ip_version)" "$CLDR_TERRITORIES_URL" json_value ".main.en.localeDisplayNames.territories.$country // empty" } group_rows() { local group="$1" jq -r \ --arg group "$group" \ --arg yes "$STATUS_YES" \ --arg unknown "$STATUS_UNKNOWN" \ --arg daily_limit "$REASON_DAILY_LIMIT" \ --arg limit "$MAXMIND_LIMIT_CELL" \ "$GROUP_ROWS_FILTER" <<<"$RESULTS" } cdn_edge_rows() { jq -r --arg cdn "$GROUP_CDN" --arg unknown "$STATUS_UNKNOWN" "$CDN_ROWS_FILTER" <<<"$RESULTS" } geoip_majority() { jq -r --arg geoip "$GROUP_GEOIP" --arg yes "$STATUS_YES" "$GEOIP_MAJORITY_FILTER" <<<"$RESULTS" } format_network() { local ip_version="$1" local asn="${REGISTRATION_ASN[$ip_version]:-}" local organization="${REGISTRATION_ORG[$ip_version]:-}" if [[ -z "$asn" ]]; then printf '%s' "$organization" return fi printf 'AS%s%s' "$asn" "${organization:+ $organization}" } build_header_columns() { HEADER_IP_VERSIONS=() HEADER_IP_VALUES=() HEADER_NETWORKS=() HEADER_EXIT_LABEL="" if [[ -n "$EXTERNAL_IPV4" ]]; then HEADER_IP_VERSIONS+=(4) HEADER_IP_VALUES+=("$(mask_ipv4 "$EXTERNAL_IPV4")") HEADER_NETWORKS+=("$(format_network 4)") fi if [[ -n "$EXTERNAL_IPV6" ]]; then HEADER_IP_VERSIONS+=(6) HEADER_IP_VALUES+=("$(mask_ipv6 "$EXTERNAL_IPV6")") HEADER_NETWORKS+=("$(format_network 6)") fi if [[ -n "$PROXY_ADDR" ]]; then HEADER_EXIT_LABEL="proxy" elif [[ -n "$INTERFACE_NAME" ]]; then HEADER_EXIT_LABEL="$INTERFACE_NAME interface" fi HEADER_VALUE_WIDTH=$(($(max_length "${HEADER_IP_VALUES[@]}" "$HEADER_EXIT_LABEL") + NAME_GAP)) HEADER_NETWORK_WIDTH=$(($(max_length "${HEADER_NETWORKS[@]}") + NAME_GAP)) } print_ip_line() { local ip_version="$1" local masked_ip="$2" local network="$3" local country="${REGISTRATION_COUNTRY[$ip_version]:-$SYMBOL_UNKNOWN}" printf ' ' pad_cell "" "IPv$ip_version" "$HEADER_LABEL_WIDTH" pad_cell "$C_VALUE" "$masked_ip" "$HEADER_VALUE_WIDTH" pad_cell "" "$network" "$HEADER_NETWORK_WIDTH" printf '%s %s\n' "$(paint "$C_DIM" "$REGISTERED_NOTE")" "$(paint "$C_VALUE" "$(label_country "$country")")" } is_maxmind_limit_used_up() { local failure for failure in "${REGISTRATION_MAXMIND_FAILURE[@]}"; do if [[ "$failure" == "$REASON_DAILY_LIMIT" ]]; then return 0 fi done return 1 } has_maxmind_failure() { local failure for failure in "${REGISTRATION_MAXMIND_FAILURE[@]}"; do if [[ -n "$failure" ]]; then return 0 fi done return 1 } maxmind_header_note() { if is_maxmind_limit_used_up; then printf '%s' "$MAXMIND_LIMIT_HEADER_NOTE" return fi if has_maxmind_failure; then printf '%s' "$MAXMIND_FAILURE_HEADER_NOTE" fi } print_ip_lines() { local i note for i in "${!HEADER_IP_VERSIONS[@]}"; do print_ip_line "${HEADER_IP_VERSIONS[i]}" "${HEADER_IP_VALUES[i]}" "${HEADER_NETWORKS[i]}" done note=$(maxmind_header_note) if [[ -n "$note" ]]; then printf ' %s%s\n' "$(pad "" "$HEADER_LABEL_WIDTH")" "$(paint "$C_DIM" "$note")" fi } print_verdict_line() { local country="$1" local note="$2" local name name=$(country_name "$country") printf ' ' pad_cell "" "$(paint "$(checks_badge_color)" " $country ")" "$HEADER_LABEL_WIDTH" pad_cell "$C_VALUE" "${name:-$country}" "$HEADER_VALUE_WIDTH" printf '%s\n' "${note:+$(paint "$C_DIM" "$note")}" } print_verdict_lines() { local -a countries=("${VERDICT_TIED_COUNTRIES[@]}") local note="$TIE_NOTE" local country if [[ -n "$VERDICT" ]]; then countries=("$VERDICT") note="$VERDICT_NOTE" fi if ((${#countries[@]} == 0)); then return fi printf '\n' for country in "${countries[@]}"; do print_verdict_line "$country" "$note" note="" done } print_checks_bar() { if ((CHECKS_TOTAL == 0)); then return fi printf ' %s %s\n' "$(print_bar "$(checks_bar_color)" "$CHECKS_NORMAL" "$CHECKS_TOTAL" "$CHECKS_BAR_WIDTH")" \ "$(paint "$C_DIM" "$CHECKS_NORMAL/$CHECKS_TOTAL $CHECKS_PASSED_NOTE")" } format_exits() { if is_dual_stack; then printf '%s' "$DUAL_STACK_EXITS" return fi printf 'IPv%s only' "$(first_ip_version)" } print_exits_line() { if [[ -z "$HEADER_EXIT_LABEL" ]]; then return fi printf ' ' pad_cell "" "" "$HEADER_LABEL_WIDTH" pad_cell "$C_VALUE" "$HEADER_EXIT_LABEL" "$HEADER_VALUE_WIDTH" printf '%s %s\n' "$(paint "$C_DIM" "$EXITS_NOTE")" "$(paint "$C_VALUE" "$(format_exits)")" } print_host_lines() { printf ' %s\n' "$(paint "$C_DIM" "Made with $(paint "$C_NO" "<3")${C_DIM} by $AUTHOR ยท") ${SCRIPT_URL#https://}" printf '\n' print_ip_lines print_exits_line } print_header() { build_header_columns { print_host_lines print_verdict_lines print_checks_bar } | print_bar_block } set_card_widths() { LEFT_CARD_WIDTH="$1" RIGHT_CARD_WIDTH="$2" FULL_CARD_WIDTH=$((LEFT_CARD_WIDTH + CARD_GAP + RIGHT_CARD_WIDTH)) # The Issue / Node column of a full-width card starts where the paired right card's content starts RIGHT_COLUMN_START=$((LEFT_CARD_WIDTH + CARD_GAP + 1)) RIGHT_COLUMN_WIDTH=$((FULL_CARD_WIDTH - CARD_SIDES_WIDTH - CARD_CONTENT_INDENT - CARD_TRAILING_GAP - RIGHT_COLUMN_START)) GEOIP_LINE_WIDTH=$((FULL_CARD_WIDTH - CARD_SIDES_WIDTH - 2 * CARD_CONTENT_INDENT - 2 * CARD_TRAILING_GAP)) CHECKS_BAR_WIDTH=$((FULL_CARD_WIDTH - CHECKS_BAR_MARGIN)) } is_service_report() { ((${#SELECTED_GROUPS[@]} == 0)) } has_group_results() { local group="$1" jq -e --arg group "$group" 'any(.group == $group)' <<<"$RESULTS" >/dev/null } is_geoip_list_shown() { [[ "$GEOIP_BREAKDOWN" == true || "${SELECTED_GROUPS[*]}" == "$GROUP_GEOIP" ]] || is_service_report } is_geoip_list_split_by_protocol() { has_group_results "$GROUP_GEOIP" && has_protocol_columns "$GROUP_GEOIP" && is_geoip_list_shown } set_report_widths() { if has_protocol_columns "$GROUP_PLATFORMS" || is_geoip_list_split_by_protocol; then set_card_widths "$WIDE_LEFT_CARD_WIDTH" "$WIDE_RIGHT_CARD_WIDTH" return fi set_card_widths $((WIDE_LEFT_CARD_WIDTH - LEFT_GRID_CELL)) $((WIDE_RIGHT_CARD_WIDTH - RIGHT_GRID_CELL)) } report_columns() { local group="$1" if is_checked_once "$group"; then printf '%s\n' "$PROXY_IP_VERSION" return fi if is_dual_stack; then printf '4\n6\n' return fi printf '%s\n' "$(first_ip_version)" } column_title() { case "$1" in 4) printf 'IPv4' ;; 6) printf 'IPv6' ;; "$PROXY_IP_VERSION") printf 'Proxy' ;; esac } attention_value() { local country="$1" local status="$2" if [[ -n "$country" ]]; then printf '%s' "$country" return fi printf '%s' "${status:-$STATUS_UNKNOWN}" } attention_cell() { local value="$1" if [[ -z "$value" ]]; then paint "$C_DIM" "$(pad "$ATTENTION_EMPTY_CELL" "$LEFT_GRID_CELL")" return fi format_cell "$value" "$LEFT_GRID_CELL" } wrap_words() { local -n wrapped_words=$1 local width="$2" local text="$3" local -a parts=() local part line="" read -ra parts <<<"$text" wrapped_words=() for part in "${parts[@]}"; do if [[ -n "$line" ]] && ((${#line} + 1 + ${#part} > width)); then wrapped_words+=("$line") line="" fi line+="${line:+ }$part" done wrapped_words+=("$line") } wrap_services() { local -n wrapped_services=$1 local width="$2" shift 2 local total=$# local reserved=$((3 + ${#total})) local line="" local shown=0 local service budget wrapped_services=() for service in "$@"; do budget="$width" if ((${#wrapped_services[@]} == ATTENTION_LIST_LINES - 1)); then budget=$((width - reserved)) fi if [[ -n "$line" ]] && ((${#line} + 3 + ${#service} > budget)); then wrapped_services+=("$line,") line="" if ((${#wrapped_services[@]} == ATTENTION_LIST_LINES)); then break fi fi line+="${line:+, }$service" shown=$((shown + 1)) done if [[ -n "$line" ]] && ((${#wrapped_services[@]} < ATTENTION_LIST_LINES)); then wrapped_services+=("$line") fi if ((shown < total)); then wrapped_services[-1]="${wrapped_services[-1]%,}, +$((total - shown))" fi } append_card_titles() { local -n card_titles=$1 local group="$2" local name_width="$3" local cell_width="$4" local -a columns=() local line column mapfile -t columns < <(report_columns "$group") line=" $(pad_cell "$C_DIM" Service "$name_width")" for column in "${columns[@]}"; do line+="$(pad_cell "$C_DIM" "$(column_title "$column")" "$cell_width")" done card_titles+=("$line") } append_attention_titles() { local -n titles=$1 append_card_titles titles "$GROUP_PLATFORMS" "$LEFT_GRID_NAME" "$LEFT_GRID_CELL" titles[-1]="$(pad "${titles[-1]}" "$RIGHT_COLUMN_START")$(paint "$C_DIM" Issue)" } append_attention_row() { local -n attention_rows=$1 local name="$2" ip_version="$3" value="$4" reason="$5" local -a columns=() issue_lines=() local line column issue mapfile -t columns < <(report_columns "$GROUP_PLATFORMS") line=" $(pad_cell "$C_VALUE" "$name" "$LEFT_GRID_NAME")" for column in "${columns[@]}"; do if ((${#columns[@]} == 1)) || [[ "$ip_version" == "$column" || "$ip_version" == "$BOTH_IP_VERSION" ]]; then line+="$(attention_cell "$value")" else line+="$(attention_cell "")" fi done wrap_words issue_lines "$RIGHT_COLUMN_WIDTH" "$reason" attention_rows+=("$(pad "$line" "$RIGHT_COLUMN_START")${issue_lines[0]}") for issue in "${issue_lines[@]:1}"; do attention_rows+=("$(printf '%*s' "$RIGHT_COLUMN_START" '')$issue") done } append_attention_services() { local -n attention_lines=$1 shift local -a service_lines=() local line wrap_services service_lines "$RIGHT_COLUMN_WIDTH" "$@" for line in "${service_lines[@]}"; do attention_lines+=("$(printf '%*s' "$RIGHT_COLUMN_START" '')$(paint "$C_DIM" "$line")") done } print_attention() { local -a fields rows=() services=() local i count value if [[ "$SHOW_ATTENTION" == false ]] || is_service_report; then return fi mapfile -t fields < <(jq -r '.[] | .ip_version, .country, .status, .reason, (.services | length), .services[]' <<<"$ATTENTION") if ((${#fields[@]} == 0)); then return fi append_attention_titles rows for ((i = 0; i < ${#fields[@]}; i += ATTENTION_ENTRY_FIELDS + count)); do count="${fields[i + 4]}" services=("${fields[@]:i+ATTENTION_ENTRY_FIELDS:count}") value=$(attention_value "${fields[i + 1]}" "${fields[i + 2]}") if ((count == 1)); then append_attention_row rows "${services[0]}" "${fields[i]}" "$value" "${fields[i + 3]}" continue fi append_attention_row rows "$count services" "${fields[i]}" "$value" "${fields[i + 3]}:" append_attention_services rows "${services[@]}" done printf '%s\n' "${rows[@]}" | print_titled_card "$C_WARN" Attention "$FULL_CARD_WIDTH" 0 } print_card_rows() { local group="$1" local name_width="$2" local cell_width="$3" shift 3 local -a rows=("$@") card_lines=() local i line append_card_titles card_lines "$group" "$name_width" "$cell_width" for ((i = 0; i < ${#rows[@]}; i += GROUP_ROW_FIELDS)); do line=" $(pad_cell "" "${rows[i]}" "$name_width")$(format_cell "${rows[i + 1]}" "$cell_width")" if has_protocol_columns "$group"; then line+="$(format_cell "${rows[i + 2]}" "$cell_width")" fi card_lines+=("$line") done printf '%s\n' "${card_lines[@]}" } print_group_card_lines() { local group="$1" local name_width="$2" local cell_width="$3" local -a fields mapfile -t fields < <(group_rows "$group") if ((${#fields[@]} == 0)); then return fi print_card_rows "$group" "$name_width" "$cell_width" "${fields[@]}" } print_halves() { local group="$1" local -a fields left_half=() right_half=() local half i mapfile -t fields < <(group_rows "$group") if ((${#fields[@]} == 0)); then return fi half=$((${#fields[@]} / GROUP_ROW_FIELDS)) half=$(((half + 1) / 2)) half=$((half * GROUP_ROW_FIELDS)) mapfile -t left_half < <(print_card_rows "$group" "$LEFT_GRID_NAME" "$LEFT_GRID_CELL" "${fields[@]:0:half}") if ((${#fields[@]} > half)); then mapfile -t right_half < <(print_card_rows "$group" "$RIGHT_GRID_NAME" "$RIGHT_GRID_CELL" "${fields[@]:half}") fi for i in "${!left_half[@]}"; do if [[ -z "${right_half[i]:-}" ]]; then printf '%s\n' "${left_half[i]}" continue fi printf '%s%s\n' "$(pad "${left_half[i]}" "$RIGHT_COLUMN_START")" "${right_half[i]# }" done } print_halves_card() { local group="$1" local title="$2" local halves halves=$(print_halves "$group") if [[ -z "$halves" ]]; then return fi printf '%s\n' "$halves" | print_titled_card "$C_TITLE" "$title" "$FULL_CARD_WIDTH" 0 } print_availability() { print_halves_card "$GROUP_AVAILABILITY" Availability } print_platforms_and_ai() { local platforms ai platforms=$(print_group_card_lines "$GROUP_PLATFORMS" "$LEFT_GRID_NAME" "$LEFT_GRID_CELL") ai=$(print_group_card_lines "$GROUP_AI" "$RIGHT_GRID_NAME" "$RIGHT_GRID_CELL") if [[ -n "$platforms" && -n "$ai" ]]; then print_titled_card_pair Platforms AI "$platforms" "$ai" return fi print_halves_card "$GROUP_PLATFORMS" Platforms print_halves_card "$GROUP_AI" AI } append_geoip_deviation() { local -n deviation_lines=$1 local country="$2" local ip_version="$3" shift 3 local country_width=$((GEOIP_COUNTRY_WIDTH + COUNTRY_EXTRA)) local protocol="" local service_list="" local -a service_lines=() local service line if has_protocol_columns "$GROUP_GEOIP"; then protocol="v$ip_version" fi for service in "$@"; do service_list+="${service_list:+, }$service" done wrap_words service_lines $((GEOIP_LINE_WIDTH - country_width - ${#protocol} - 1)) "$service_list" line="$(format_country "$country" "$country_width")$(pad "${service_lines[0]}" $((GEOIP_LINE_WIDTH - country_width - ${#protocol})))" deviation_lines+=(" $line$(paint "$C_DIM" "$protocol")") for line in "${service_lines[@]:1}"; do deviation_lines+=(" $(printf '%*s' "$country_width" '')$line") done } print_geoip_deviations() { local reference="$1" local -a fields lines=() local i count mapfile -t fields < <(jq -r --arg geoip "$GROUP_GEOIP" --arg yes "$STATUS_YES" --arg reference "$reference" "$GEOIP_DEVIATIONS_FILTER" <<<"$RESULTS") for ((i = 0; i < ${#fields[@]}; i += GEOIP_DEVIATION_FIELDS + count)); do count="${fields[i + 2]}" append_geoip_deviation lines "${fields[i]}" "${fields[i + 1]}" "${fields[@]:i+GEOIP_DEVIATION_FIELDS:count}" done if ((${#lines[@]} > 0)); then printf '%s\n' "${lines[@]}" fi } print_geoip_agreement() { local emoji="" if [[ "$SHOW_EMOJI" == true ]]; then emoji="$GEOIP_AGREEMENT_EMOJI " fi printf ' %s%s\n' "$emoji" "$(paint "$C_YES" "$GEOIP_AGREEMENT_NOTE")" } print_geoip_lines() { local reference="$1" local agree="$2" local total="$3" local limited="$4" printf ' %s\n' "$(print_bar "$C_BAR" "$agree" "$total" "$GEOIP_LINE_WIDTH")" if is_geoip_list_shown; then print_halves "$GROUP_GEOIP" return fi print_geoip_deviations "$reference" if ((agree == total)); then print_geoip_agreement fi if [[ "$limited" == true ]]; then printf ' %s\n' "$(paint "$C_DIM" "$MAXMIND_LIMIT_SUMMARY_NOTE")" fi } print_geoip() { local reference summary local -a agreement if is_service_report; then print_halves_card "$GROUP_GEOIP" GeoIP return fi reference="${VERDICT:-$(geoip_majority)}" mapfile -t agreement < <(jq -r --arg geoip "$GROUP_GEOIP" --arg yes "$STATUS_YES" --arg reference "$reference" \ --arg daily_limit "$REASON_DAILY_LIMIT" "$GEOIP_AGREEMENT_FILTER" <<<"$RESULTS") if ((agreement[1] == 0)) && [[ "${agreement[2]}" != true ]]; then return fi summary="$(paint "$C_VALUE" "$(label_country "${reference:-$SYMBOL_UNKNOWN}")") $(paint "$C_DIM" "${agreement[0]}/${agreement[1]}")" print_geoip_lines "$reference" "${agreement[@]}" | print_titled_card "$C_TITLE" GeoIP "$FULL_CARD_WIDTH" 0 "$summary" } print_cards() { local cards cards=$( print_attention print_platforms_and_ai print_availability print_geoip ) if [[ -z "$cards" ]]; then return fi printf '%s\n\n' "$cards" } cdn_cell() { local status="$1" local country="$2" if [[ "$status" != "$STATUS_YES" ]]; then format_status "$status" "$LEFT_GRID_CELL" return fi paint "$C_VALUE" "$(pad "$(label_country "$country")" "$LEFT_GRID_CELL")" } cdn_node() { local status="$1" local iata="$3" local node="$4" if [[ "$status" != "$STATUS_YES" ]]; then paint "$C_DIM" "$SYMBOL_UNKNOWN" return fi paint "$C_VALUE" "${node:-$iata}" } append_cdn_row() { local -n cdn_rows=$1 local name="$2" local -a v4_edge=("${@:3:4}") v6_edge=("${@:7:4}") local cells nodes cells="$(cdn_cell "${v4_edge[@]:0:2}")" nodes="$(cdn_node "${v4_edge[@]}")" if has_protocol_columns "$GROUP_CDN"; then cells+="$(cdn_cell "${v6_edge[@]:0:2}")" nodes+=" $(paint "$C_DIM" "ยท") $(cdn_node "${v6_edge[@]}")" fi cdn_rows+=("$(pad " $(pad_cell "" "$name" "$LEFT_GRID_NAME")$cells" "$RIGHT_COLUMN_START")$nodes") } print_cdn() { local -a fields rows=() local i mapfile -t fields < <(cdn_edge_rows) if ((${#fields[@]} == 0)); then return fi rows+=(" $(paint "$C_TITLE" "$(label_group CDN)")") append_card_titles rows "$GROUP_CDN" "$LEFT_GRID_NAME" "$LEFT_GRID_CELL" rows[-1]="$(pad "${rows[-1]}" "$RIGHT_COLUMN_START")$(paint "$C_DIM" Node)" for ((i = 0; i < ${#fields[@]}; i += CDN_ROW_FIELDS)); do append_cdn_row rows "${fields[@]:i:CDN_ROW_FIELDS}" done printf '%s\n' "${rows[@]}" | print_bar_block } print_legend() { local separator legend if jq -e --arg cdn "$GROUP_CDN" 'all(.group == $cdn)' <<<"$RESULTS" >/dev/null; then return fi separator=" $(paint "$C_DIM" "ยท") " legend="$(paint "$C_YES" "$SYMBOL_YES") $(paint "$C_DIM" available)$separator" legend+="$(paint "$C_NO" "$SYMBOL_NO") $(paint "$C_DIM" "region blocked")$separator" legend+="$(paint "$C_NO" "$SYMBOL_IPBLOCK") $(paint "$C_DIM" "IP blocked")$separator" legend+="$(paint "$C_DIM" "$SYMBOL_UNKNOWN unknown")" if has_protocol_columns "$GROUP_PLATFORMS"; then legend+="$separator$(paint "$C_DIM" "$SYMBOL_NONE no IPv6")" fi printf '%*s%s\n' "$LEGEND_INDENT" '' "$legend" if is_checked_once "$GROUP_PLATFORMS"; then printf '%*s%s\n' "$LEGEND_INDENT" '' "$(paint "$C_DIM" "$PROXY_CHECKED_ONCE_NOTE")" fi printf '\n' } fetch_verdict_country_names() { if [[ -z "$VERDICT" ]] && ((${#VERDICT_TIED_COUNTRIES[@]} == 0)); then return fi http_request "$(first_ip_version)" "$CLDR_TERRITORIES_URL" } print_report() { set_report_widths fetch_verdict_country_names print_header print_cards print_cdn print_legend } print_service_report() { set_report_widths build_header_columns print_host_lines | print_bar_block print_cards print_cdn print_legend } print_json_array() { jq -cn '$ARGS.positional' --args "$@" } print_address_json() { local ip_version="$1" local ip ip=$(external_ip "$ip_version") if [[ -z "$ip" ]]; then printf 'null' return fi jq -cn \ --arg ip "$ip" \ --arg asn "${REGISTRATION_ASN[$ip_version]:-}" \ --arg org "${REGISTRATION_ORG[$ip_version]:-}" \ --arg registered "${REGISTRATION_COUNTRY[$ip_version]:-}" \ --arg maxmind_failure "${REGISTRATION_MAXMIND_FAILURE[$ip_version]:-}" \ "$JSON_ADDRESS_FILTER" } print_json() { jq \ --arg schema "$JSON_SCHEMA_URL" \ --argjson version "$JSON_VERSION" \ --argjson ipv4 "$(print_address_json 4)" \ --argjson ipv6 "$(print_address_json 6)" \ --arg verdict "$VERDICT" \ --argjson tied "$(print_json_array "${VERDICT_TIED_COUNTRIES[@]}")" \ --argjson attention "$ATTENTION" \ --argjson groups "$(print_json_array "${SERVICE_GROUPS[@]}")" \ --arg availability "$GROUP_AVAILABILITY" \ --arg ai "$GROUP_AI" \ --arg yes "$STATUS_YES" \ --arg none "$STATUS_NONE" \ --arg proxy "$PROXY_IP_VERSION" \ "$JSON_REPORT_FILTER" <<<"$RESULTS" } print_output() { if [[ "$JSON_OUTPUT" == true ]]; then print_json return fi if is_service_report; then print_service_report return fi print_report } main() { use_utf8_locale parse_args "$@" validate_args ignore_ip_version_with_proxy detect_main_interface log_interface log_proxy limit_ip_versions_to_main_interface install_missing_dependencies resolve_selected_groups resolve_selected_checks create_work_dir start_debug_trace "$@" create_job_slots discover_external_ips lookup_ip_registrations start_checks "${PLATFORMS_CHECKS[@]}" start_checks "${CDN_CHECKS[@]}" start_checks "${AVAILABILITY_CHECKS[@]}" start_checks "${AI_CHECKS[@]}" start_checks "${GEOIP_CHECKS[@]}" wait_for_jobs collect_results compute_verdict collect_attention count_checks print_output } if [[ "${BASH_SOURCE[0]:-$0}" == "$0" ]]; then main "$@" fi