From b64940e6cc4413f1e60330c6a5629075be4aa6fa Mon Sep 17 00:00:00 2001 From: royal <137996547+ritascarlet@users.noreply.github.com> Date: Sun, 5 Oct 2025 03:48:13 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9,=20=D0=BC=D0=BE=D0=B4=D0=B8?= =?UTF-8?q?=D1=84=D0=B8=D1=86=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20ipregion.sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ipregion.sh | 2040 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2040 insertions(+) create mode 100644 ipregion.sh diff --git a/ipregion.sh b/ipregion.sh new file mode 100644 index 0000000..8734552 --- /dev/null +++ b/ipregion.sh @@ -0,0 +1,2040 @@ +#!/usr/bin/env bash + +SCRIPT_URL="https://github.com/vernette/ipregion" +USER_AGENT="Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0" +SPINNER_SERVICE_FILE=$(mktemp "${TMPDIR:-/tmp}/ipregion_spinner_XXXXXX") +DEBUG_LOG_FILE="ipregion_debug_$(date +%Y%m%d_%H%M%S)_$$.log" + +SPOTIFY_API_KEY="142b583129b2df829de3656f9eb484e6" +SPOTIFY_CLIENT_ID="9a8d2f0ce77a4e248bb71fefcb557637" +NETFLIX_API_KEY="YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm" +TWITCH_CLIENT_ID="kimne78kx3ncx6brgo4mv6wki5h1ko" +CHATGPT_STATSIG_API_KEY="client-zUdXdSTygXJdzoE0sWTkP8GKTVsUMF2IRM7ShVO2JAG" +REDDIT_BASIC_ACCESS_TOKEN="b2hYcG9xclpZdWIxa2c6" +YOUTUBE_SOCS_COOKIE="CAISNQgDEitib3FfaWRlbnRpdHlmcm9udGVuZHVpc2VydmVyXzIwMjUwNzMwLjA1X3AwGgJlbiACGgYIgPC_xAY" +DISNEY_PLUS_API_KEY="ZGlzbmV5JmFuZHJvaWQmMS4wLjA.bkeb0m230uUhv8qrAXuNu39tbE_mD5EEhM_NAcohjyA" +DISNEY_PLUS_JSON_BODY='{"query":"\n mutation registerDevice($registerDevice: RegisterDeviceInput!) {\n registerDevice(registerDevice: $registerDevice) {\n __typename\n }\n }\n ","variables":{"registerDevice":{"applicationRuntime":"android","attributes":{"operatingSystem":"Android","operatingSystemVersion":"13"},"deviceFamily":"android","deviceLanguage":"en","deviceProfile":"phone","devicePlatformId":"android"}},"operationName":"registerDevice"}' + +VERBOSE=false +JSON_OUTPUT=false +GROUPS_TO_SHOW="all" +CURL_TIMEOUT=10 +CURL_RETRIES=1 +IPV4_ONLY=false +IPV6_ONLY=false +PROXY_ADDR="" +INTERFACE_NAME="" +DEBUG=false + +RESULT_JSON="" +ARR_PRIMARY=() +ARR_CUSTOM=() +ARR_CDN=() + +COLOR_HEADER="1;36" +COLOR_SERVICE="1;32" +COLOR_HEART="1;31" +COLOR_URL="1;90" +COLOR_ASN="1;33" +COLOR_TABLE_HEADER="1;97" +COLOR_TABLE_VALUE="1" +COLOR_NULL="0;90" +COLOR_ERROR="1;31" +COLOR_WARN="1;33" +COLOR_INFO="1;36" +COLOR_RESET="0" + +LOG_INFO="INFO" +LOG_WARN="WARNING" +LOG_ERROR="ERROR" + +STATUS_NA="N/A" +STATUS_DENIED="Denied" +STATUS_RATE_LIMIT="Rate-limit" +STATUS_SERVER_ERROR="Server error" + +declare -A DEPENDENCIES=( + [jq]="jq" + [curl]="curl" + [column]="util-linux" + [nslookup]="bind-utils" +) + +declare -A PACKAGE_MAPPING=( + ["apt:nslookup"]="dnsutils" + ["apt:column"]="bsdmainutils" + ["pacman:nslookup"]="bind" + ["dnf:nslookup"]="bind-utils" + ["yum:nslookup"]="bind-utils" + ["termux:column"]="util-linux" +) + +declare -A PRIMARY_SERVICES=( + [MAXMIND]="maxmind.com|geoip.maxmind.com|/geoip/v2.1/city/me" + [RIPE]="rdap.db.ripe.net|rdap.db.ripe.net|/ip/{ip}" + [IPINFO_IO]="ipinfo.io|ipinfo.io|/widget/demo/{ip}" + [IPREGISTRY]="ipregistry.co|api.ipregistry.co|/{ip}?hostname=true&key=sb69ksjcajfs4c" + [IPAPI_CO]="ipapi.co|ipapi.co|/{ip}/json" + [CLOUDFLARE]="cloudflare.com|speed.cloudflare.com|/meta" + [IFCONFIG_CO]="ifconfig.co|ifconfig.co|/country-iso?ip={ip}|plain" + [IPLOCATION_COM]="iplocation.com|iplocation.com" + [COUNTRY_IS]="country.is|api.country.is|/{ip}" + [GEOAPIFY_COM]="geoapify.com|api.geoapify.com|/v1/ipinfo?&ip={ip}&apiKey=b8568cb9afc64fad861a69edbddb2658" + [GEOJS_IO]="geojs.io|get.geojs.io|/v1/ip/country.json?ip={ip}" + [IPAPI_IS]="ipapi.is|api.ipapi.is|/?q={ip}" + [IPBASE_COM]="ipbase.com|api.ipbase.com|/v2/info?ip={ip}" + [IPQUERY_IO]="ipquery.io|api.ipquery.io|/{ip}" + [IPWHO_IS]="ipwho.is|ipwho.is|/{ip}" + [IPAPI_COM]="ip-api.com|demo.ip-api.com|/json/{ip}?fields=countryCode" +) + +PRIMARY_SERVICES_ORDER=( + "MAXMIND" + "RIPE" + "IPINFO_IO" + "CLOUDFLARE" + "IPREGISTRY" + "IPAPI_CO" + "IFCONFIG_CO" + "IPLOCATION_COM" + "COUNTRY_IS" + "GEOAPIFY_COM" + "GEOJS_IO" + "IPAPI_IS" + "IPBASE_COM" + "IPQUERY_IO" + "IPWHO_IS" + "IPAPI_COM" +) + +declare -A PRIMARY_SERVICES_CUSTOM_HANDLERS=( + [IPLOCATION_COM]="lookup_iplocation_com" +) + +declare -A SERVICE_HEADERS=( + [IPREGISTRY]="Origin: https://ipregistry.co" + [MAXMIND]="Referer: https://www.maxmind.com" + [IPAPI_COM]="Origin: https://ip-api.com" +) + +declare -A CUSTOM_SERVICES=( + [GOOGLE]="Google" + [YOUTUBE]="YouTube" + [TWITCH]="Twitch" + [CHATGPT]="ChatGPT" + [NETFLIX]="Netflix" + [SPOTIFY]="Spotify" + [REDDIT]="Reddit" + [DISNEY_PLUS]="Disney+" + [REDDIT_GUEST_ACCESS]="Reddit (Guest Access)" + [YOUTUBE_PREMIUM]="YouTube Premium" + [GOOGLE_SEARCH_CAPTCHA]="Google Search Captcha" + [SPOTIFY_SIGNUP]="Spotify Signup" + [DISNEY_PLUS_ACCESS]="Disney+ Access" + [APPLE]="Apple" + [STEAM]="Steam" + [TIKTOK]="Tiktok" + [OOKLA_SPEEDTEST]="Ookla Speedtest" + [JETBRAINS]="JetBrains" + [PLAYSTATION]="PlayStation" + [MICROSOFT]="Microsoft" +) + +CUSTOM_SERVICES_ORDER=( + "GOOGLE" + "YOUTUBE" + "TWITCH" + "CHATGPT" + "NETFLIX" + "SPOTIFY" + "REDDIT" + "DISNEY_PLUS" + "REDDIT_GUEST_ACCESS" + "YOUTUBE_PREMIUM" + "GOOGLE_SEARCH_CAPTCHA" + "SPOTIFY_SIGNUP" + "DISNEY_PLUS_ACCESS" + "APPLE" + "STEAM" + "TIKTOK" + "OOKLA_SPEEDTEST" + "JETBRAINS" + "PLAYSTATION" + "MICROSOFT" +) + +declare -A CUSTOM_SERVICES_HANDLERS=( + [GOOGLE]="lookup_google" + [YOUTUBE]="lookup_youtube" + [TWITCH]="lookup_twitch" + [CHATGPT]="lookup_chatgpt" + [NETFLIX]="lookup_netflix" + [SPOTIFY]="lookup_spotify" + [REDDIT]="lookup_reddit" + [DISNEY_PLUS]="lookup_disney_plus" + [REDDIT_GUEST_ACCESS]="lookup_reddit_guest_access" + [YOUTUBE_PREMIUM]="lookup_youtube_premium" + [GOOGLE_SEARCH_CAPTCHA]="lookup_google_search_captcha" + [SPOTIFY_SIGNUP]="lookup_spotify_signup" + [DISNEY_PLUS_ACCESS]="lookup_disney_plus_access" + [APPLE]="lookup_apple" + [STEAM]="lookup_steam" + [TIKTOK]="lookup_tiktok" + [CLOUDFLARE_CDN]="lookup_cloudflare_cdn" + [YOUTUBE_CDN]="lookup_youtube_cdn" + [NETFLIX_CDN]="lookup_netflix_cdn" + [OOKLA_SPEEDTEST]="lookup_ookla_speedtest" + [JETBRAINS]="lookup_jetbrains" + [PLAYSTATION]="lookup_playstation" + [MICROSOFT]="lookup_microsoft" +) + +declare -A CDN_SERVICES=( + [CLOUDFLARE_CDN]="Cloudflare CDN" + [YOUTUBE_CDN]="YouTube CDN" + [NETFLIX_CDN]="Netflix CDN" +) + +CDN_SERVICES_ORDER=( + "CLOUDFLARE_CDN" + "YOUTUBE_CDN" + "NETFLIX_CDN" +) + +declare -A SERVICE_GROUPS=( + [primary]="${PRIMARY_SERVICES_ORDER[*]}" + [custom]="${CUSTOM_SERVICES_ORDER[*]}" + [cdn]="${CDN_SERVICES_ORDER[*]}" +) + +EXCLUDED_SERVICES=( + # "IPINFO_IO" + # "IPREGISTRY" + # "IPAPI_CO" + "GOOGLE_SEARCH_CAPTCHA" +) + +IDENTITY_SERVICES=( + "ident.me" + "ifconfig.me" + "api64.ipify.org" + "ifconfig.co" + "ifconfig.me" +) + +IPV6_OVER_IPV4_SERVICES=( + "IPINFO_IO" +) + +color() { + local color_name="$1" + local text="$2" + local code + + case "$color_name" in + HEADER) code="$COLOR_HEADER" ;; + SERVICE) code="$COLOR_SERVICE" ;; + HEART) code="$COLOR_HEART" ;; + URL) code="$COLOR_URL" ;; + ASN) code="$COLOR_ASN" ;; + TABLE_HEADER) code="$COLOR_TABLE_HEADER" ;; + TABLE_VALUE) code="$COLOR_TABLE_VALUE" ;; + NULL) code="$COLOR_NULL" ;; + ERROR) code="$COLOR_ERROR" ;; + WARN) code="$COLOR_WARN" ;; + INFO) code="$COLOR_INFO" ;; + RESET) code="$COLOR_RESET" ;; + *) code="$color_name" ;; + esac + + printf "\033[%sm%s\033[0m" "$code" "$text" +} + +bold() { + local text="$1" + printf "\033[1m%s\033[0m" "$text" +} + +get_timestamp() { + local format="$1" + date +"$format" +} + +log() { + local log_level="$1" + local message="${*:2}" + local timestamp + + if [[ "$VERBOSE" == true ]]; then + local color_code + + timestamp=$(get_timestamp "%d.%m.%Y %H:%M:%S") + + case "$log_level" in + "$LOG_ERROR") color_code=ERROR ;; + "$LOG_WARN") color_code=WARN ;; + "$LOG_INFO") color_code=INFO ;; + *) color_code=RESET ;; + esac + + printf "[%s] [%s]: %s\n" "$timestamp" "$(color $color_code "$log_level")" "$message" >&2 + fi +} + +error_exit() { + local message="$1" + local exit_code="${2:-1}" + printf "%s %s\n" "$(color ERROR '[ERROR]')" "$(color TABLE_HEADER "$message")" >&2 + display_help + exit "$exit_code" +} + +display_help() { + cat <&1 4>&2 + + exec 1> >(tee -a "$DEBUG_LOG_FILE" >&3) + exec 2> >(tee -a "$DEBUG_LOG_FILE" >&4) + + set -x + return 0 +} + +grep_wrapper() { + local grep_args=() + + if [[ "$1" == "--perl" ]]; then + grep_args+=("-oP") + shift + fi + + grep "${grep_args[@]}" "$@" +} + +extract_debug_url() { + local response="$1" + url=$(grep_wrapper bashupload.com <<<"$response") + url=${url##* } + echo "$url" +} + +upload_debug() { + local ip_version=4 + local response + response=$(make_request POST "https://bashupload.com" --file "$DEBUG_LOG_FILE" --ip-version "$ip_version") + extract_debug_url "$response" +} + +cleanup_debug() { + local debug_url + + if [[ ! -f "$DEBUG_LOG_FILE" ]]; then + return 1 + fi + + set +x + exec 1>&3 2>&4 3>&- 4>&- + + debug_url="$(upload_debug)" + + printf "\n%s\n %s\n %s\n\n%s\n" \ + "$(color WARN 'Debug information:')" \ + "Local file: $DEBUG_LOG_FILE" \ + "Remote URL: $debug_url" \ + "$(color INFO 'If you open a GitHub Issue, please download the log and attach it')" +} + +is_command_available() { + local cmd="$1" + command -v "$cmd" >/dev/null 2>&1 +} + +detect_distro() { + if [[ -f /etc/os-release ]]; then + source /etc/os-release + distro="$ID" + elif [[ -f /etc/redhat-release ]]; then + distro="rhel" + elif [[ -d /data/data/com.termux ]]; then + distro="termux" + fi +} + +detect_package_manager() { + local pkg_manager + + case "$distro" in + ubuntu | debian | termux) + pkg_manager="apt" + ;; + arch | manjaro) + pkg_manager="pacman" + ;; + fedora) + pkg_manager="dnf" + ;; + centos | rhel) + if is_command_available "dnf"; then + pkg_manager="dnf" + else + pkg_manager="yum" + fi + ;; + opensuse*) + pkg_manager="zypper" + ;; + alpine) + pkg_manager="apk" + ;; + *) + error_exit "Unknown distro: $distro" + ;; + esac + + echo "$pkg_manager" +} + +get_missing_commands() { + local missing=() + + for cmd in "${!DEPENDENCIES[@]}"; do + if ! is_command_available "$cmd"; then + missing+=("$cmd") + fi + done + + printf '%s\n' "${missing[@]}" +} + +get_package_name() { + local pkg_manager="$1" + local command="$2" + local mapping_key="${pkg_manager}:${command}" + + if [[ -n "${PACKAGE_MAPPING[$mapping_key]}" ]]; then + echo "${PACKAGE_MAPPING[$mapping_key]}" + return + fi + + echo "${DEPENDENCIES[$command]:-$command}" +} + +is_sudo_required() { + if [[ "${EUID:-$(id -u)}" -eq 0 || "$distro" == "termux" ]]; then + return 1 + fi + + return 0 +} + +get_install_args() { + local pkg_manager="$1" + local install_args + + case "$pkg_manager" in + apt) + install_args=("install" "-y") + ;; + pacman) + install_args=("-Sy" "--noconfirm") + ;; + dnf | yum | zypper) + install_args=("install" "-y") + ;; + apk) + install_args=("add" "--no-cache") + ;; + esac + + echo "${install_args[@]}" +} + +install_packages() { + local pkg_manager="$1" + shift + local packages=("$@") + local cmd_prefix=() + local install_cmd=() + + if is_sudo_required; then + cmd_prefix=("sudo") + log "$LOG_INFO" "Running as non-root user, using sudo" + fi + + cmd_prefix+=("$pkg_manager") + + if [[ "$pkg_manager" == "apt" ]]; then + log "$LOG_INFO" "Updating package lists" + if ! "${cmd_prefix[@]}" update; then + error_exit "Error occurred while updating package lists" + fi + fi + + read -ra install_args <<<"$(get_install_args "$pkg_manager")" + install_cmd+=("${cmd_prefix[@]}" "${install_args[@]}" "${packages[@]}") + + log "$LOG_INFO" "Running: ${install_cmd[*]}" + + if ! "${install_cmd[@]}"; then + error_exit "Error occurred while installing packages" + fi +} + +prompt_for_installation() { + local missing=("$@") + local response + local formatted_deps="" + + for dep in "${missing[@]}"; do + formatted_deps+=" $dep\n" + done + + printf "\n%s\n%b\n%s " \ + "$(color WARN 'Missing dependencies:')" \ + "$formatted_deps" \ + "$(color INFO 'Do you want to install them? [y/N]:')" + + read -r response + response=${response,,} + + case "$response" in + y | yes) + return 0 + ;; + *) + return 1 + ;; + esac +} + +install_dependencies() { + local missing_dependencies=() + local missing_commands pkg_manager package_name + + log "$LOG_INFO" "Checking dependencies" + + mapfile -t missing_commands < <(get_missing_commands) + + if [[ "${missing_commands[*]}" =~ ^[[:space:]]*$ ]]; then + log "$LOG_INFO" "All dependencies are installed" + return 0 + fi + + log "$LOG_INFO" "Missing commands: ${missing_commands[*]}" + + pkg_manager=$(detect_package_manager) + + log "$LOG_INFO" "Detected package manager: $pkg_manager" + + for cmd in "${missing_commands[@]}"; do + package_name=$(get_package_name "$pkg_manager" "$cmd") + missing_dependencies+=("$package_name") + done + + log "$LOG_INFO" "Missing dependencies: ${missing_dependencies[*]}" + + if ! prompt_for_installation "${missing_dependencies[@]}" /dev/null 2>&1 <<<"$json" +} + +process_json() { + local json="$1" + local jq_filter="$2" + + if is_status_string "$json"; then + echo "$json" + return + fi + + jq -r "$jq_filter" <<<"$json" +} + +format_value() { + local value="$1" + + case "$value" in + "$STATUS_NA") + color NULL "$value" + ;; + "$STATUS_DENIED" | "$STATUS_SERVER_ERROR") + color ERROR "$value" + ;; + "$STATUS_RATE_LIMIT") + color WARN "$value" + ;; + *) + bold "$value" + ;; + esac +} + +print_value_or_colored() { + local value="$1" + local color_name="$2" + + if [[ "$JSON_OUTPUT" == true ]]; then + echo "$value" + return + fi + + color "$color_name" "$value" +} + +mask_ipv4() { + local ip="$1" + echo "${ip%.*.*}.*.*" +} + +mask_ipv6() { + local ip="$1" + echo "$ip" | awk -F: '{ + for(i=1;i<=NF;i++) if($i=="") $i="0"; + while(NF<8) for(i=1;i<=8;i++) if($i=="0"){NF++; break;} + printf "%s:%s:%s::\n", $1, $2, $3 + }' +} + +parse_arguments() { + while [[ $# -gt 0 ]]; do + case $1 in + -h | --help) + display_help + exit 0 + ;; + -v | --verbose) + VERBOSE=true + shift + ;; + -d | --debug) + DEBUG=true + shift + ;; + -j | --json) + JSON_OUTPUT=true + shift + ;; + -g | --group) + GROUPS_TO_SHOW="$2" + shift 2 + ;; + -t | --timeout) + if [[ "$2" =~ ^[0-9]+$ ]]; then + CURL_TIMEOUT="$2" + else + error_exit "Invalid timeout value: $2. Timeout must be a positive integer" + fi + shift 2 + ;; + -4 | --ipv4) + IPV4_ONLY=true + shift + ;; + -6 | --ipv6) + if ! check_ip_support 6; then + error_exit "IPv6 is not supported on this system" + fi + + IPV6_ONLY=true + shift + ;; + -p | --proxy) + PROXY_ADDR="$2" + log "$LOG_INFO" "Using SOCKS5 proxy: $PROXY_ADDR" + shift 2 + ;; + -i | --interface) + INTERFACE_NAME="$2" + log "$LOG_INFO" "Using interface: $INTERFACE_NAME" + shift 2 + ;; + *) + error_exit "Unknown option: $1" + ;; + esac + done +} + +is_status_string() { + local value="$1" + + case "$value" in + "$STATUS_DENIED" | "$STATUS_SERVER_ERROR" | "$STATUS_RATE_LIMIT" | "$STATUS_NA") + return 0 + ;; + *) + return 1 + ;; + esac +} + +status_from_http_code() { + local code="$1" + + case "$code" in + 403) + echo "$STATUS_DENIED" + ;; + 429) + echo "$STATUS_RATE_LIMIT" + ;; + 5*) + echo "$STATUS_SERVER_ERROR" + ;; + 4*) + echo "$STATUS_NA" + ;; + *) + echo "" + ;; + esac +} + +check_ip_interfaces() { + local version="$1" + + log "$LOG_INFO" "Checking for IPv${version} interfaces" + + if [[ -n $(ip -"${version}" addr show scope global 2>/dev/null) ]]; then + log "$LOG_INFO" "IPv${version} global interfaces found" + return 0 + fi + + log "$LOG_ERROR" "No global IPv${version} addresses found on interfaces" + return 1 +} + +check_ip_connectivity() { + local version="$1" + local test_hosts_v4=("8.8.8.8" "8.8.4.4" "1.1.1.1" "1.0.0.1" "9.9.9.9") + local test_hosts_v6=("2001:4860:4860::8888" "2001:4860:4860::8844" "2606:4700:4700::1111" "2606:4700:4700::1001" "2620:fe::9") + local timeout=3 + local count=1 + local test_hosts + + log "$LOG_INFO" "Checking IPv${version} connectivity" + + if [[ "$version" == "4" ]]; then + test_hosts=("${test_hosts_v4[@]}") + else + test_hosts=("${test_hosts_v6[@]}") + fi + + for host in "${test_hosts[@]}"; do + if ping -"${version}" -c "$count" -W "$timeout" "$host" >/dev/null 2>&1; then + log "$LOG_INFO" "IPv${version} connectivity confirmed via $host" + return 0 + fi + done + + log "$LOG_ERROR" "IPv${version} connectivity test failed" + return 1 +} + +check_ip_dns() { + local version="$1" + local test_domain="google.com" + local record_type + + log "$LOG_INFO" "Checking IPv${version} DNS resolution" + + if [[ "$version" == "4" ]]; then + record_type="A" + else + record_type="AAAA" + fi + + if nslookup -type="$record_type" "$test_domain" >/dev/null 2>&1; then + log "$LOG_INFO" "IPv${version} DNS resolution works via nslookup" + return 0 + fi + + log "$LOG_ERROR" "IPv${version} DNS resolution failed" + return 1 +} + +check_ip_support() { + local version="$1" + local -a checks=("interfaces" "connectivity" "dns") + local -a failed=() + + spinner_update "IPv$version support" + log "$LOG_INFO" "Starting comprehensive IPv${version} support check" + + for check in "${checks[@]}"; do + if ! "check_ip_${check}" "$version"; then + failed+=("$check") + fi + done + + if [[ ${#failed[@]} -eq 0 ]]; then + log "$LOG_INFO" "IPv${version} is fully supported (${checks[*]})" + return 0 + else + log "$LOG_ERROR" "IPv${version} is not fully supported. Failed checks: ${failed[*]}" + return 1 + fi +} + +shuffle_identity_services() { + local i tmp size rand_idx + size=${#IDENTITY_SERVICES[@]} + + for ((i = size - 1; i > 0; i--)); do + rand_idx=$((RANDOM % (i + 1))) + + if ((rand_idx != i)); then + tmp=${IDENTITY_SERVICES[i]} + IDENTITY_SERVICES[i]=${IDENTITY_SERVICES[rand_idx]} + IDENTITY_SERVICES[rand_idx]=$tmp + fi + done +} + +fetch_ip_from_service() { + local service="$1" + local ip_version="$2" + local response + + response=$(make_request GET "https://$service" --ip-version "$ip_version") + + if [[ -n "$response" ]]; then + echo "$response" + fi +} + +fetch_external_ip() { + local ip_version="$1" + local service ip + + spinner_update "External IPv$ip_version address" + log "$LOG_INFO" "Getting external IPv${ip_version} address" + + shuffle_identity_services + + for service in "${IDENTITY_SERVICES[@]}"; do + ip=$(fetch_ip_from_service "$service" "$ip_version") + + if [[ -n "$ip" ]]; then + log "$LOG_INFO" "Successfully obtained IPv${ip_version} address from $service: $ip" + echo "$ip" + return + else + log "$LOG_WARN" "No response from $service for IPv${ip_version}" + fi + done + + log "$LOG_ERROR" "Failed to obtain IPv${ip_version} address from any service" +} + +discover_external_ips() { + if [[ "$IPV4_ONLY" == true ]] || [[ "$IPV6_ONLY" != true ]]; then + EXTERNAL_IPV4=$(fetch_external_ip 4) + fi + + if [[ "$IPV6_ONLY" == true ]] || { [[ "$IPV6_SUPPORTED" -eq 0 ]] && [[ "$IPV4_ONLY" != true ]]; }; then + EXTERNAL_IPV6=$(fetch_external_ip 6) + fi + + if [[ -z "$EXTERNAL_IPV4" ]] && [[ -z "$EXTERNAL_IPV6" ]]; then + error_exit "Failed to obtain external IPv4 or IPv6 address" + fi +} + +get_asn() { + local ipbase_api_key="sgiPfh4j3aXFR3l2CnjWqdKQzxpqGn9pX5b3CUsz" + local ip ip_version response connection_data + + if [[ -n "$EXTERNAL_IPV4" ]]; then + ip="$EXTERNAL_IPV4" + ip_version=4 + else + ip="$EXTERNAL_IPV6" + ip_version=6 + fi + + spinner_update "ASN info" + log "$LOG_INFO" "Getting ASN info for IP $ip" + + response=$(make_request GET "https://api.ipbase.com/v2/info?apikey=$ipbase_api_key&ip=$ip" --ip-version "$ip_version") + connection_data=$(process_json "$response" ".data.connection") + asn=$(process_json "$connection_data" ".asn") + asn_name=$(process_json "$connection_data" ".organization") + + log "$LOG_INFO" "ASN info: AS$asn $asn_name" +} + +get_registered_country() { + local ip_version="$1" + local response + + response=$(make_request GET "https://geoip.maxmind.com/geoip/v2.1/city/me" \ + --header "Referer: https://www.maxmind.com" \ + --ip-version "$ip_version") + + process_json "$response" ".registered_country.names.en" +} + +get_server_country() { + local ip_version="$1" + local response country_code country_name + + # Используем Cloudflare как основной источник (быстрый и надежный) + response=$(make_request GET "https://speed.cloudflare.com/meta" --ip-version "$ip_version") + + if [[ -n "$response" && "$response" != *"error"* ]]; then + country_code=$(process_json "$response" ".country") + if [[ -n "$country_code" && "$country_code" != "null" ]]; then + echo "$country_code" + return + fi + fi + + # Если Cloudflare не сработал, пробуем MaxMind + response=$(make_request GET "https://geoip.maxmind.com/geoip/v2.1/city/me" \ + --header "Referer: https://www.maxmind.com" \ + --ip-version "$ip_version") + + if [[ -n "$response" && "$response" != *"error"* ]]; then + country_code=$(process_json "$response" ".country.iso_code") + if [[ -n "$country_code" && "$country_code" != "null" ]]; then + echo "$country_code" + return + fi + fi + + # Если и MaxMind не сработал, пробуем IPAPI.co + response=$(make_request GET "https://ipapi.co/json" --ip-version "$ip_version") + + if [[ -n "$response" && "$response" != *"error"* ]]; then + country_code=$(process_json "$response" ".country_code") + if [[ -n "$country_code" && "$country_code" != "null" ]]; then + echo "$country_code" + return + fi + fi + + # Если ничего не сработало + echo "Unknown" +} + +get_iata_location() { + local iata_code="$1" + local url="https://www.air-port-codes.com/api/v1/single" + local payload="iata=$iata_code" + local apc_auth="96dc04b3fb" + local referer="https://www.air-port-codes.com/" + local response + + response=$(make_request POST "$url" \ + --header "APC-Auth: $apc_auth" \ + --header "Referer: $referer" \ + --data "$payload" \ + --ip-version 4) + + process_json "$response" ".airport.country.iso" +} + +is_ipv6_over_ipv4_service() { + local service="$1" + for s in "${IPV6_OVER_IPV4_SERVICES[@]}"; do + [[ "$s" == "$service" ]] && return 0 + done + return 1 +} + +spinner_start() { + local delay=0.1 + # shellcheck disable=SC1003 + local spinstr='|/-\\' + local current_service + + spinner_running=true + + ( + while $spinner_running; do + for ((i = 0; i < ${#spinstr}; i++)); do + current_service="" + + if [[ -f "$SPINNER_SERVICE_FILE" ]]; then + current_service="$(cat "$SPINNER_SERVICE_FILE")" + fi + + printf "\r\033[K%s %s %s" \ + "$(color HEADER "${spinstr:$i:1}")" \ + "$(color HEADER "Checking:")" \ + "$(color SERVICE "$current_service")" + + sleep $delay + done + done + ) & + + spinner_pid=$! +} + +spinner_stop() { + spinner_running=false + + if [[ -n "$spinner_pid" ]]; then + kill "$spinner_pid" 2>/dev/null + wait "$spinner_pid" 2>/dev/null + spinner_pid="" + printf "\\r%*s\\r" 40 " " + fi + + if [[ -f "$SPINNER_SERVICE_FILE" ]]; then + rm -f "$SPINNER_SERVICE_FILE" + unset SPINNER_SERVICE_FILE + fi +} + +spinner_update() { + local value="$1" + + if [[ -n "$SPINNER_SERVICE_FILE" ]]; then + echo "$value" >"$SPINNER_SERVICE_FILE" + fi +} + +spinner_cleanup() { + spinner_stop + exit 130 +} + +make_request() { + local method="$1" + local url="$2" + shift 2 + local ip_version user_agent json data file headers response_with_code response http_code + local curl_args=( + --silent + --compressed + --location + --retry-connrefused + --retry-all-errors + --retry "$CURL_RETRIES" + --max-time "$CURL_TIMEOUT" + -w '\n%{http_code}' + ) + + case "$method" in + HEAD) + curl_args+=(--head) + ;; + *) + curl_args+=(--request "$method") + ;; + esac + + while (($#)); do + case "$1" in + --ip-version) + ip_version="$2" + shift 2 + ;; + --user-agent) + user_agent="$2" + shift 2 + ;; + --header) + headers+=("$2") + shift 2 + ;; + --json) + json="$2" + shift 2 + ;; + --data) + data="$2" + shift 2 + ;; + --file) + file="$2" + shift 2 + ;; + esac + done + + if [[ "$ip_version" == "4" ]]; then + curl_args+=(-4) + else + curl_args+=(-6) + fi + + for h in "${headers[@]}"; do + curl_args+=(-H "$h") + done + + if [[ -n "$user_agent" ]]; then + curl_args+=(-A "$user_agent") + fi + + if [[ -n "$json" ]]; then + curl_args+=(--data "$json") + curl_args+=(-H 'Content-Type: application/json') + fi + + if [[ -n "$data" ]]; then + curl_args+=(--data "$data") + curl_args+=(-H 'Content-Type: application/x-www-form-urlencoded') + fi + + if [[ -n "$file" ]]; then + curl_args+=(--upload-file "$file") + fi + + if [[ -n "$PROXY_ADDR" ]]; then + curl_args+=(--proxy "socks5://$PROXY_ADDR") + fi + + if [[ -n "$INTERFACE_NAME" ]]; then + curl_args+=(--interface "$INTERFACE_NAME") + fi + + curl_args+=("$url") + + response_with_code=$(curl "${curl_args[@]}") + http_code=$(tail -n1 <<<"$response_with_code") + response=$(head -n -1 <<<"$response_with_code") + + if [[ "$http_code" == 4* || "$http_code" == 5* ]]; then + status_from_http_code "$http_code" + return 0 + fi + + echo "$response" +} + +service_build_request() { + local service="$1" ip="$2" ip_version="$3" + local cfg="${PRIMARY_SERVICES[$service]}" + local display_name domain url_template url headers_str response_format + + IFS='|' read -r display_name domain url_template response_format <<<"$cfg" + + if [[ -z "$display_name" ]]; then + display_name="$service" + fi + + url="https://$domain${url_template//\{ip\}/$ip}" + + if [[ -n "${SERVICE_HEADERS[$service]}" ]]; then + headers_str="${SERVICE_HEADERS[$service]}" + fi + + printf "%s\n%s\n%s\n%s" "$display_name" "$url" "${response_format:-json}" "$headers_str" +} + +probe_service() { + local service="$1" + local ip_version="$2" + local ip="$3" + local built display_name url response_format headers_line request_params response + + mapfile -t built < <(service_build_request "$service" "$ip" "$ip_version") + display_name="${built[0]}" + url="${built[1]}" + response_format="${built[2]}" + headers_line="${built[3]}" + + if [[ -n "$headers_line" ]]; then + IFS='||' read -ra hs <<<"$headers_line" + for h in "${hs[@]}"; do + if [[ -n "$h" ]]; then + request_params+=(--header "$h") + fi + done + fi + + if [[ "$ip_version" == "6" ]] && is_ipv6_over_ipv4_service "$service"; then + ip_version="4" + fi + + response=$(make_request GET "$url" "${request_params[@]}" --ip-version "$ip_version") + + process_response "$service" "$response" "$display_name" "$response_format" +} + +process_response() { + local service="$1" + local response="$2" + local display_name="$3" + local response_format="${4:-json}" + local jq_filter + + if is_status_string "$response"; then + echo "$response" + return + fi + + if [[ -z "$response" || "$response" == *" 0)); then + t_primary=$(printf '%s\n' "${ARR_PRIMARY[@]//|||/$'\t'}") + fi + + if ((${#ARR_CUSTOM[@]} > 0)); then + t_custom=$(printf '%s\n' "${ARR_CUSTOM[@]//|||/$'\t'}") + fi + + if ((${#ARR_CDN[@]} > 0)); then + t_cdn=$(printf '%s\n' "${ARR_CDN[@]//|||/$'\t'}") + fi + + # Добавляем страну сервера в JSON + RESULT_JSON=$( + jq -n \ + --rawfile p <(printf "%s" "$t_primary") \ + --rawfile c <(printf "%s" "$t_custom") \ + --rawfile d <(printf "%s" "$t_cdn") \ + --arg ipv4 "$EXTERNAL_IPV4" \ + --arg ipv6 "$EXTERNAL_IPV6" \ + --arg server_country "$SERVER_COUNTRY" \ + --arg version "1" ' + def lines_to_array($raw): + if ($raw | length) == 0 then [] else + ($raw | split("\n")) + | map(select(length > 0)) + | map( + (split("\t")) as $f + | { + service: $f[0], + ipv4: ( ($f[1] // "") | if length>0 then . else null end ), + ipv6: ( ($f[2] // "") | if length>0 then . else null end ) + } + ) + end; + + { + version: ($version|tonumber), + ipv4: ($ipv4 | select(length > 0) // null), + ipv6: ($ipv6 | select(length > 0) // null), + server_country: ($server_country | select(length > 0) // null), + results: { + primary: lines_to_array($p), + custom: lines_to_array($c), + cdn: lines_to_array($d) + } + } + ' + ) +} + +add_result() { + local group="$1" + local service="$2" + local ipv4="$3" + local ipv6="$4" + + ipv4=${ipv4//$'\n'/} + ipv4=${ipv4//$'\t'/ } + ipv6=${ipv6//$'\n'/} + ipv6=${ipv6//$'\t'/ } + + case "$group" in + primary) ARR_PRIMARY+=("$service|||$ipv4|||$ipv6") ;; + custom) ARR_CUSTOM+=("$service|||$ipv4|||$ipv6") ;; + cdn) ARR_CDN+=("$service|||$ipv4|||$ipv6") ;; + esac +} + +print_table_group() { + local group="$1" + local group_title="$2" + local na="N/A" + local show_ipv4=0 + local show_ipv6=0 + local separator=$'\t' + + if [[ "$IPV6_ONLY" != true && -n "$EXTERNAL_IPV4" ]]; then + show_ipv4=1 + fi + + if [[ "$IPV4_ONLY" != true && -n "$EXTERNAL_IPV6" ]]; then + show_ipv6=1 + fi + + printf "%s\n\n" "$(color HEADER "$group_title")" + + { + printf "%s" "$(color TABLE_HEADER 'Service')" + + if [[ $show_ipv4 -eq 1 ]]; then + printf "%s%s" "$separator" "$(color TABLE_HEADER 'IPv4')" + fi + + if [[ $show_ipv6 -eq 1 ]]; then + printf "%s%s" "$separator" "$(color TABLE_HEADER 'IPv6')" + fi + + printf "\n" + + jq -r --arg group "$group" ' + (.results // {}) as $r + | ($r[$group] // []) + | .[] + | [ .service, (.ipv4 // "N/A"), (.ipv6 // "N/A") ] + | @tsv + ' <<<"$RESULT_JSON" | while IFS=$'\t' read -r s v4 v6; do + + printf "%s" "$(color SERVICE "$s")" + + if [[ $show_ipv4 -eq 1 ]]; then + if [[ "$v4" == "null" || -z "$v4" ]]; then + v4="$na" + fi + printf "%s%s" "$separator" "$(format_value "$v4")" + fi + + if [[ $show_ipv6 -eq 1 ]]; then + if [[ "$v6" == "null" || -z "$v6" ]]; then + v6="$na" + fi + printf "%s%s" "$separator" "$(format_value "$v6")" + fi + + printf "\n" + done + } | column -t -s "$separator" +} + +print_header() { + local ipv4 ipv6 + + ipv4=$(process_json "$RESULT_JSON" ".ipv4") + ipv6=$(process_json "$RESULT_JSON" ".ipv6") + + printf "%s\n%s\n\n" "$(color URL "Made with ")$(color HEART "<3")$(color URL " by vernette")" "$(color URL "$SCRIPT_URL")" + + # TODO: Get registered country while initializing + if [[ "$ipv4" != "null" ]]; then + printf "%s: %s, %s %s\n" "$(color HEADER 'IPv4')" "$(bold "$(mask_ipv4 "$ipv4")")" "registered in" "$(bold "$(get_registered_country 4)")" + fi + + if [[ "$ipv6" != "null" ]]; then + printf "%s: %s, %s %s\n" "$(color HEADER 'IPv6')" "$(bold "$(mask_ipv6 "$ipv6")")" "registered in" "$(bold "$(get_registered_country 6)")" + fi + + printf "%s: %s\n\n" "$(color HEADER 'ASN')" "$(bold "AS$asn $asn_name")" +} + +print_results() { + finalize_json + + if [[ "$JSON_OUTPUT" == true ]]; then + echo "$RESULT_JSON" | jq + return + fi + + print_header + + case "$GROUPS_TO_SHOW" in + primary) + print_table_group "primary" "GeoIP services" + ;; + custom) + print_table_group "custom" "Popular services" + ;; + cdn) + print_table_group "cdn" "CDN services" + ;; + *) + print_table_group "custom" "Popular services" + printf "\n" + print_table_group "cdn" "CDN services" + printf "\n" + print_table_group "primary" "GeoIP services" + ;; + esac +} + +lookup_maxmind() { + process_service "MAXMIND" +} + +lookup_ripe() { + process_service "RIPE" +} + +lookup_ipinfo_io() { + process_service "IPINFO_IO" +} + +lookup_ipregistry() { + process_service "IPREGISTRY" +} + +lookup_ipapi_co() { + process_service "IPAPI_CO" +} + +lookup_cloudflare() { + process_service "CLOUDFLARE" +} + +lookup_ifconfig_co() { + process_service "IFCONFIG_CO" +} + +lookup_iplocation_com() { + local ip_version="$1" + local response ip + + if [[ -n "$EXTERNAL_IPV4" ]]; then + ip="$EXTERNAL_IPV4" + else + ip="$EXTERNAL_IPV6" + fi + + response=$(make_request POST "https://iplocation.com" --ip-version "$ip_version" --user-agent "$USER_AGENT" --data "ip=$ip") + process_json "$response" ".country_code" +} + +lookup_google() { + local ip_version="$1" + local response + + response=$(make_request GET "https://www.google.com" \ + --user-agent "$USER_AGENT" \ + --ip-version "$ip_version") + + grep_wrapper --perl '"MgUcDb":"\K[^"]*' <<<"$response" +} + +lookup_youtube() { + local ip_version="$1" + local response json_result + + response=$(make_request GET "https://www.youtube.com/sw.js_data" --ip-version "$ip_version") + + json_result=$(tail -n +3 <<<"$response") + process_json "$json_result" ".[0][2][0][0][1]" +} + +lookup_twitch() { + local ip_version="$1" + local response + + response=$(make_request POST "https://gql.twitch.tv/gql" \ + --header "Client-Id: $TWITCH_CLIENT_ID" \ + --json '[{"operationName":"VerifyEmail_CurrentUser","variables":{},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"f9e7dcdf7e99c314c82d8f7f725fab5f99d1df3d7359b53c9ae122deec590198"}}}]' \ + --ip-version "$ip_version") + process_json "$response" ".[0].data.requestInfo.countryCode" +} + +lookup_chatgpt() { + local ip_version="$1" + local response + + response=$(make_request POST "https://ab.chatgpt.com/v1/initialize" --ip-version "$ip_version" \ + --header "Statsig-Api-Key: $CHATGPT_STATSIG_API_KEY") + process_json "$response" ".derived_fields.country" +} + +lookup_netflix() { + local ip_version="$1" + local response + + response=$(make_request GET "https://api.fast.com/netflix/speedtest/v2?https=true&token=$NETFLIX_API_KEY&urlCount=1" --ip-version "$ip_version") + + if is_valid_json "$response"; then + process_json "$response" ".client.location.country" + return + fi + + echo "$response" +} + +lookup_spotify() { + local ip_version="$1" + local response + + response=$(make_request GET "https://spclient.wg.spotify.com/signup/public/v1/account/?validate=1&key=$SPOTIFY_API_KEY" \ + --header "X-Client-Id: $SPOTIFY_CLIENT_ID" \ + --ip-version "$ip_version") + + process_json "$response" ".country" +} + +lookup_reddit() { + local ip_version="$1" + local basic_access_token="Basic $REDDIT_BASIC_ACCESS_TOKEN" + local user_agent="Reddit/Version 2025.29.0/Build 2529021/Android 13" + local response access_token + + response=$(make_request POST "https://www.reddit.com/auth/v2/oauth/access-token/loid" \ + --ip-version "$ip_version" \ + --user-agent "$user_agent" \ + --header "Authorization: $basic_access_token" \ + --json '{"scopes":["email"]}') + + access_token=$(process_json "$response" ".access_token") + + response=$(make_request POST "https://gql-fed.reddit.com" \ + --ip-version "$ip_version" \ + --user-agent "$user_agent" \ + --header "Authorization: Bearer $access_token" \ + --json '{"operationName":"UserLocation","variables":{},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"f07de258c54537e24d7856080f662c1b1268210251e5789c8c08f20d76cc8ab2"}}}') + + process_json "$response" ".data.userLocation.countryCode" +} + +lookup_disney_plus() { + local ip_version="$1" + local response + + response=$(make_request POST "https://disney.api.edge.bamgrid.com/graph/v1/device/graphql" \ + --header "Authorization: Bearer $DISNEY_PLUS_API_KEY" \ + --json "$DISNEY_PLUS_JSON_BODY" \ + --ip-version "$ip_version") + + process_json "$response" ".extensions.sdk.session.location.countryCode" +} + +lookup_reddit_guest_access() { + local ip_version="$1" + local response is_available color_name + + response=$(make_request GET "https://www.reddit.com" --ip-version "$ip_version" --user-agent "$USER_AGENT") + + if [[ "$response" != "Denied" ]]; then + is_available="Yes" + color_name="SERVICE" + else + is_available="No" + color_name="HEART" + fi + + print_value_or_colored "$is_available" "$color_name" +} + +lookup_youtube_premium() { + local ip_version="$1" + local response is_available + + response=$(make_request GET "https://www.youtube.com/premium" \ + --ip-version "$ip_version" \ + --user-agent "$USER_AGENT" \ + --header "Cookie: SOCS=$YOUTUBE_SOCS_COOKIE" \ + --header "Accept-Language: en-US,en;q=0.9") + + if [[ -z "$response" ]]; then + echo "" + return + fi + + is_available=$(grep_wrapper -io "youtube premium is not available in your country" <<<"$response") + + if [[ -z "$is_available" ]]; then + is_available="Yes" + color_name="SERVICE" + else + is_available="No" + color_name="HEART" + fi + + print_value_or_colored "$is_available" "$color_name" +} + +lookup_google_search_captcha() { + local ip_version="$1" + local response is_captcha color_name + + response=$(make_request GET "https://www.google.com/search?q=cats" --ip-version "$ip_version" \ + --user-agent "$USER_AGENT" \ + --header "Accept-Language: en-US,en;q=0.9") + + if [[ -z "$response" ]]; then + echo "" + return + fi + + is_captcha=$(grep_wrapper -iE "unusual traffic from|is blocked|unaddressed abuse" <<<"$response") + + if [[ -z "$is_captcha" ]]; then + is_captcha="No" + color_name="SERVICE" + else + is_captcha="Yes" + color_name="HEART" + fi + + print_value_or_colored "$is_captcha" "$color_name" +} + +lookup_spotify_signup() { + local ip_version="$1" + local response status is_country_launched available color_name + + response=$(make_request GET "https://spclient.wg.spotify.com/signup/public/v1/account/?validate=1&key=$SPOTIFY_API_KEY" \ + --header "X-Client-Id: $SPOTIFY_CLIENT_ID" \ + --ip-version "$ip_version") + + status=$(process_json "$response" ".status") + is_country_launched=$(process_json "$response" ".is_country_launched") + + if [[ "$status" == "120" || "$status" == "320" || "$is_country_launched" == "false" ]]; then + available="No" + color_name="HEART" + else + available="Yes" + color_name="SERVICE" + fi + + print_value_or_colored "$available" "$color_name" +} + +lookup_disney_plus_access() { + local ip_version="$1" + local response errors_count in_supported_location is_available color_name + + response=$(make_request POST "https://disney.api.edge.bamgrid.com/graph/v1/device/graphql" \ + --header "Authorization: Bearer $DISNEY_PLUS_API_KEY" \ + --json "$DISNEY_PLUS_JSON_BODY" \ + --ip-version "$ip_version") + + errors_count=$(process_json "$response" ".errors | length") + in_supported_location=$(process_json "$response" ".extensions.sdk.session.inSupportedLocation") + + if [[ "$errors_count" == "0" && "$in_supported_location" == "true" ]]; then + is_available="Yes" + color_name="SERVICE" + else + is_available="No" + color_name="HEART" + fi + + print_value_or_colored "$is_available" "$color_name" +} + +lookup_apple() { + local ip_version="$1" + make_request GET "https://gspe1-ssl.ls.apple.com/pep/gcc" --ip-version "$ip_version" +} + +lookup_steam() { + local ip_version="$1" + local response + + response=$(make_request HEAD "https://store.steampowered.com" --ip-version "$ip_version") + grep_wrapper --perl 'steamCountry=\K[^%;]*' <<<"$response" +} + +lookup_tiktok() { + local ip_version="$1" + local response + + response=$(make_request GET "https://www.tiktok.com/api/v1/web-cookie-privacy/config?appId=1988" --ip-version "$ip_version") + process_json "$response" ".body.appProps.region" +} + +lookup_cloudflare_cdn() { + local ip_version="$1" + local response iata location + + response=$(make_request GET "https://speed.cloudflare.com/meta" --ip-version "$ip_version") + + iata=$(process_json "$response" ".colo") + location=$(get_iata_location "$iata") + echo "$location ($iata)" +} + +lookup_youtube_cdn() { + local ip_version="$1" + local response iata location + + response=$(make_request GET "https://redirector.googlevideo.com/report_mapping?di=no" --ip-version "$ip_version") + iata=$(echo "$response" | awk '{print $3}' | cut -f2 -d'-' | cut -c1-3) + iata=${iata^^} + + if [[ -z "$iata" ]]; then + echo "" + return + fi + + location=$(get_iata_location "$iata") + echo "$location ($iata)" +} + +lookup_netflix_cdn() { + local ip_version="$1" + local response + + response=$(make_request GET "https://api.fast.com/netflix/speedtest/v2?https=true&token=$NETFLIX_API_KEY&urlCount=1" --ip-version "$ip_version") + + if is_valid_json "$response"; then + process_json "$response" ".targets[0].location.country" + else + echo "" + fi +} + +lookup_ookla_speedtest() { + local ip_version="$1" + local response + + response=$(make_request GET "https://www.speedtest.net/api/js/config-sdk" --ip-version "$ip_version") + process_json "$response" ".location.countryCode" +} + +lookup_jetbrains() { + local ip_version="$1" + local response + + response=$(make_request GET "https://data.services.jetbrains.com/geo" --ip-version "$ip_version") + process_json "$response" ".code" +} + +lookup_playstation() { + local ip_version="$1" + local response + + response=$(make_request HEAD "https://www.playstation.com" --ip-version "$ip_version") + grep_wrapper --perl 'country=\K[^;]*' <<<"$response" | head -n1 +} + +lookup_microsoft() { + local ip_version="$1" + local response + + response=$(make_request GET "https://login.live.com" --ip-version "$ip_version") + grep_wrapper --perl '"sRequestCountry":"\K[^"]*' <<<"$response" +} + +main() { + parse_arguments "$@" + + setup_debug + + trap spinner_cleanup EXIT INT TERM + + detect_distro + install_dependencies + + if [[ "$JSON_OUTPUT" != "true" && "$VERBOSE" != "true" ]]; then + spinner_start + fi + + check_ip_support 4 + IPV4_SUPPORTED=$? + + check_ip_support 6 + IPV6_SUPPORTED=$? + + discover_external_ips + get_asn + + # Определяем страну сервера + if [[ -n "$EXTERNAL_IPV4" ]]; then + SERVER_COUNTRY=$(get_server_country 4) + elif [[ -n "$EXTERNAL_IPV6" ]]; then + SERVER_COUNTRY=$(get_server_country 6) + else + SERVER_COUNTRY="Unknown" + fi + + log "$LOG_INFO" "Server country detected: $SERVER_COUNTRY" + + case "$GROUPS_TO_SHOW" in + primary) + run_service_group "primary" + ;; + custom) + run_service_group "custom" + ;; + cdn) + run_service_group "cdn" + ;; + *) + run_service_group "primary" + run_service_group "custom" + run_service_group "cdn" + ;; + esac + + if [[ "$JSON_OUTPUT" != "true" && "$VERBOSE" != "true" ]]; then + spinner_stop + fi + + print_results + + cleanup_debug + + trap - EXIT INT TERM +} + +main "$@"