From 8ec1ccfeccd754c2442f3da8190ccb8f9c8460ba Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sat, 22 Aug 2026 11:06:06 +0700 Subject: [PATCH] =?UTF-8?q?fix(censorcheck):=20=D0=B2=D0=BE=D1=81=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=B8=D1=82=D1=8C=20vendor-?= =?UTF-8?q?=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=20=D0=B8=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81?= =?UTF-8?q?=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20=D0=BF=D0=BE=20=D0=9E?= =?UTF-8?q?=D0=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сломанный here-doc съел is_installed; launcher определяет дистрибутив и ставит jq/dig/column без prompt, скрипты отдаются с LF. Co-authored-by: Cursor --- AGENTS.md | 2 +- apps/api/scripts/censorcheck/VENDOR.md | 1 + apps/api/scripts/censorcheck/censorcheck.sh | 1692 ++++++++++--------- apps/api/scripts/censorcheck/launcher.sh | 136 +- apps/api/src/routes/launcher.test.ts | 8 + apps/api/src/routes/launcher.ts | 6 +- 6 files changed, 1016 insertions(+), 829 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2dec31f..d3d9b67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,7 +95,7 @@ vps-tracker/ Ручная проверка с VPS: `curl -fsSL https://vt.shnt.top/cc | bash` (тот же контейнер, Traefik dual Host). - **Vendor:** `apps/api/scripts/censorcheck/censorcheck.sh` (pin SHA `12c5839`, MIT) -- **Launcher:** `GET /cc` минтит HMAC ingest-токен (TTL 20 мин); `GET /cc/vendor` — скрипт +- **Launcher:** `GET /cc` минтит HMAC ingest-токен (TTL 20 мин); по `/etc/os-release` ставит `jq`/`dig`/`column` без prompt; `GET /cc/vendor` — скрипт (LF) - **Ingest:** `POST /api/integrations/censorcheck/runs` (без portal JWT) - **UI:** `/blocking` — текущие прогоны и история, группировка VPS / сервис - Env: `CENSORCHECK_INGEST_SECRET`, `CENSORCHECK_PUBLIC_URL`, `VPS_LAUNCHER_DOMAIN` diff --git a/apps/api/scripts/censorcheck/VENDOR.md b/apps/api/scripts/censorcheck/VENDOR.md index e39a2f9..4898d2c 100644 --- a/apps/api/scripts/censorcheck/VENDOR.md +++ b/apps/api/scripts/censorcheck/VENDOR.md @@ -3,5 +3,6 @@ Vendor pin of https://github.com/vernette/censorcheck - File: `censorcheck.sh` - Commit: `12c5839` (2026-08-11) - License: MIT (see upstream repository) +- Keep Unix LF and the `display_help` here-doc intact (`is_installed` lives after it). Do not fetch this script from GitHub at runtime — some probe networks block github.com. diff --git a/apps/api/scripts/censorcheck/censorcheck.sh b/apps/api/scripts/censorcheck/censorcheck.sh index ba172b1..f8c2c2b 100644 --- a/apps/api/scripts/censorcheck/censorcheck.sh +++ b/apps/api/scripts/censorcheck/censorcheck.sh @@ -13,14 +13,14 @@ readonly CURL_SEPARATOR="--UNIQUE-SEPARATOR--" readonly DNS_SERVERS=("1.1.1.1" "1.0.0.1" "8.8.8.8" "8.8.4.4" "9.9.9.9" "9.9.9.10" "77.88.8.8" "77.88.8.1") readonly ENCRYPTED_DNS_SERVERS=( - "Cloudflare|1.1.1.1" - "Cloudflare|1.0.0.1" - "Google|8.8.8.8" - "Google|8.8.4.4" - "Quad9|9.9.9.9" - "Quad9|9.9.9.10" - "Yandex|77.88.8.8" - "Yandex|77.88.8.1" + "Cloudflare|1.1.1.1" + "Cloudflare|1.0.0.1" + "Google|8.8.8.8" + "Google|8.8.4.4" + "Quad9|9.9.9.9" + "Quad9|9.9.9.10" + "Yandex|77.88.8.8" + "Yandex|77.88.8.1" ) AVAILABLE_DOH=() @@ -41,36 +41,36 @@ DNS_CHECK=true SHOW_HEADER=true readonly DPI_BLOCKED_SITES=( - "youtube.com" - "redirector.googlevideo.com/report_mapping?di=no" - "discord.com" - "instagram.com" - "facebook.com" - "x.com" - "linkedin.com" - "rutracker.org" - "digitalocean.com" - # "ntc.party" - "amnezia.org" - "getoutline.org" - "mailfence.com" - "flibusta.is" - "rezka.ag" - "api.telegram.org" - "play.google.com" + "youtube.com" + "redirector.googlevideo.com/report_mapping?di=no" + "discord.com" + "instagram.com" + "facebook.com" + "x.com" + "linkedin.com" + "rutracker.org" + "digitalocean.com" + # "ntc.party" + "amnezia.org" + "getoutline.org" + "mailfence.com" + "flibusta.is" + "rezka.ag" + "api.telegram.org" + "play.google.com" ) readonly GEO_BLOCKED_SITES=( - "spotify.com" - "netflix.com" - "patreon.com" - "swagger.io" - "snyk.io" - "mongodb.com" - "autodesk.com" - "graylog.org" - "redis.io" - "copilot.microsoft.com" + "spotify.com" + "netflix.com" + "patreon.com" + "swagger.io" + "snyk.io" + "mongodb.com" + "autodesk.com" + "graylog.org" + "redis.io" + "copilot.microsoft.com" ) readonly MSG_AVAILABLE="Available" @@ -85,1030 +85,1072 @@ readonly MSG_OTHER="Responded with status code" declare -a TEXT_RESULTS=() error_exit() { - local message="$1" - local exit_code="${2:-1}" - printf "[%b%s%b] %b%s%b\n" "$COLOR_RED" "ERROR" "$COLOR_RESET" "$COLOR_WHITE" "$message" "$COLOR_RESET" >&2 - exit "$exit_code" + local message="$1" + local exit_code="${2:-1}" + printf "[%b%s%b] %b%s%b\n" "$COLOR_RED" "ERROR" "$COLOR_RESET" "$COLOR_WHITE" "$message" "$COLOR_RESET" >&2 + exit "$exit_code" } show_progress() { - local current=$1 - local total=$2 - local domain=$3 + local current=$1 + local total=$2 + local domain=$3 - if ! $JSON_OUTPUT; then - printf "\r\033[K%b[%d/%d] Checking:%b %b%s%b" \ - "$COLOR_BLUE" \ - "$current" \ - "$total" \ - "$COLOR_RESET" \ - "$COLOR_WHITE" \ - "$domain" \ - "$COLOR_RESET" >&2 - fi + if ! $JSON_OUTPUT; then + printf "\r\033[K%b[%d/%d] Checking:%b %b%s%b" \ + "$COLOR_BLUE" \ + "$current" \ + "$total" \ + "$COLOR_RESET" \ + "$COLOR_WHITE" \ + "$domain" \ + "$COLOR_RESET" >&2 + fi } clear_progress() { - if ! $JSON_OUTPUT; then - printf "\r%80s\r" " " >&2 - fi + if ! $JSON_OUTPUT; then + printf "\r%80s\r" " " >&2 + fi } cleanup() { - clear_progress - exit 130 + clear_progress + exit 130 } display_help() { - cat < /dev/null 2>&1 + cat </dev/null 2>&1 } check_missing_dependencies() { - local missing_pkgs=() + local missing_pkgs=() - for pkg in "${DEPENDENCIES[@]}"; do - if ! is_installed "$pkg"; then - missing_pkgs+=("$pkg") - fi - done + for pkg in "${DEPENDENCIES[@]}"; do + if ! is_installed "$pkg"; then + missing_pkgs+=("$pkg") + fi + done - echo "${missing_pkgs[@]}" + echo "${missing_pkgs[@]}" } prompt_for_installation() { - local missing_pkgs=("$@") + local missing_pkgs=("$@") - echo "Missing dependencies: ${missing_pkgs[*]}" - read -r -p "Do you want to install them? [y/N]: " answer - answer=${answer,,} + echo "Missing dependencies: ${missing_pkgs[*]}" + read -r -p "Do you want to install them? [y/N]: " answer + answer=${answer,,} - case "${answer,,}" in - y | yes) - return 0 - ;; - *) - exit 0 - ;; - esac + case "${answer,,}" in + y | yes) + return 0 + ;; + *) + exit 0 + ;; + esac } get_package_manager() { - # Check if the script is running in Termux - if [[ -d /data/data/com.termux ]]; then - echo "termux" - return - fi + # Check if the script is running in Termux + if [[ -d /data/data/com.termux ]]; then + echo "termux" + return + fi - if [[ -f /etc/os-release ]]; then - . /etc/os-release - case "$ID" in - debian | ubuntu) - echo "apt" - ;; - arch) - echo "pacman" - ;; - fedora) - echo "dnf" - ;; - centos | rhel | rocky | almalinux) - if command -v dnf >/dev/null 2>&1; then echo "dnf"; else echo "yum"; fi - ;; - *) - error_exit "Unknown distribution: $ID. Please install dependencies manually." - ;; - esac - else - error_exit "File /etc/os-release not found, unable to determine distribution. Please install dependencies manually." - fi + if [[ -f /etc/os-release ]]; then + . /etc/os-release + case "$ID" in + debian | ubuntu) + echo "apt" + ;; + arch) + echo "pacman" + ;; + fedora) + echo "dnf" + ;; + centos | rhel | rocky | almalinux) + if command -v dnf >/dev/null 2>&1; then echo "dnf"; else echo "yum"; fi + ;; + *) + error_exit "Unknown distribution: $ID. Please install dependencies manually." + ;; + esac + else + error_exit "File /etc/os-release not found, unable to determine distribution. Please install dependencies manually." + fi } install_with_package_manager() { - local pkg_manager="$1" - local use_sudo="" - local packages=() - shift + local pkg_manager="$1" + local use_sudo="" + local packages=() + shift - for dep in "$@"; do - case "$pkg_manager" in - apt | termux) - case "$dep" in - dig) packages+=("dnsutils") ;; - column) packages+=("bsdextrautils") ;; - *) packages+=("$dep") ;; - esac - ;; - pacman) - case "$dep" in - dig) packages+=("bind") ;; - column) packages+=("util-linux") ;; - *) packages+=("$dep") ;; - esac - ;; - dnf | yum) - case "$dep" in - dig) packages+=("bind-utils") ;; - column) packages+=("util-linux") ;; - *) packages+=("$dep") ;; - esac - ;; - *) - packages+=("$dep") - ;; - esac - done + for dep in "$@"; do + case "$pkg_manager" in + apt | termux) + case "$dep" in + dig) packages+=("dnsutils") ;; + column) packages+=("bsdextrautils") ;; + *) packages+=("$dep") ;; + esac + ;; + pacman) + case "$dep" in + dig) packages+=("bind") ;; + column) packages+=("util-linux") ;; + *) packages+=("$dep") ;; + esac + ;; + dnf | yum) + case "$dep" in + dig) packages+=("bind-utils") ;; + column) packages+=("util-linux") ;; + *) packages+=("$dep") ;; + esac + ;; + *) + packages+=("$dep") + ;; + esac + done - if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then - use_sudo="sudo" - fi + if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then + use_sudo="sudo" + fi - case "$pkg_manager" in - apt) - $use_sudo apt update - $use_sudo env NEEDRESTART_MODE=a apt install -y "${packages[@]}" - ;; - pacman) - $use_sudo pacman -Syy --noconfirm "${packages[@]}" - ;; - dnf) - $use_sudo dnf install -y "${packages[@]}" - ;; - yum) - $use_sudo yum install -y "${packages[@]}" - ;; - termux) - apt update - apt install -y "${packages[@]}" - ;; - *) - error_exit "Unknown package manager: $pkg_manager" - ;; - esac + case "$pkg_manager" in + apt) + $use_sudo apt update + $use_sudo env NEEDRESTART_MODE=a apt install -y "${packages[@]}" + ;; + pacman) + $use_sudo pacman -Syy --noconfirm "${packages[@]}" + ;; + dnf) + $use_sudo dnf install -y "${packages[@]}" + ;; + yum) + $use_sudo yum install -y "${packages[@]}" + ;; + termux) + apt update + apt install -y "${packages[@]}" + ;; + *) + error_exit "Unknown package manager: $pkg_manager" + ;; + esac } install_dependencies() { - local missing_packages - local pkg_manager + local missing_packages + local pkg_manager - read -r -a missing_packages <<<"$(check_missing_dependencies)" + read -r -a missing_packages <<<"$(check_missing_dependencies)" - if [[ ${#missing_packages[@]} -eq 0 ]]; then - return 0 - fi + if [[ ${#missing_packages[@]} -eq 0 ]]; then + return 0 + fi - if $JSON_OUTPUT; then - echo "Missing dependencies: ${missing_packages[*]}" >&2 - exit 1 - fi + if $JSON_OUTPUT; then + echo "Missing dependencies: ${missing_packages[*]}" >&2 + exit 1 + fi - prompt_for_installation "${missing_packages[@]}" + prompt_for_installation "${missing_packages[@]}" - pkg_manager=$(get_package_manager) - install_with_package_manager "$pkg_manager" "${missing_packages[@]}" + pkg_manager=$(get_package_manager) + install_with_package_manager "$pkg_manager" "${missing_packages[@]}" } check_ipv6_support() { - if [[ -n $(ip -6 addr show scope global 2>/dev/null) ]]; then - return 0 - fi + if [[ -n $(ip -6 addr show scope global 2>/dev/null) ]]; then + return 0 + fi - return 1 + return 1 } parse_arguments() { - while [[ $# -gt 0 ]]; do - case $1 in - -h | --help) - display_help - exit 0 - ;; - -m | --mode) - if [[ $2 == "dpi" || $2 == "geoblock" || $2 == "dns" || $2 == "both" ]]; then - MODE=$2 - else - error_exit "Invalid mode: $2. Valid modes are: dpi, geoblock, dns, both" - fi - shift 2 - ;; - -t | --timeout) - if [[ "$2" =~ ^[0-9]+$ ]]; then - TIMEOUT=$2 - else - error_exit "Invalid timeout value: $2. Timeout must be a positive integer" - fi - shift 2 - ;; - -r | --retries) - if [[ "$2" =~ ^[0-9]+$ ]]; then - RETRIES=$2 - else - error_exit "Invalid retries value: $2. Retry count must be a positive integer" - fi - shift 2 - ;; - -u | --user-agent) - if [[ -n "$2" ]]; then - USER_AGENT=$2 - else - error_exit "User-Agent cannot be empty" - fi - shift 2 - ;; - -f | --file) - if [[ -n "${2:-}" ]]; then - if [[ -f "$2" ]]; then - DOMAINS_FILE="$2" - else - error_exit "File '$2' does not exist" - fi - else - error_exit "File path cannot be empty" - fi - shift 2 - ;; - -6 | --ipv6) - if ! check_ipv6_support; then - error_exit "IPv6 is not supported on this system" - fi + while [[ $# -gt 0 ]]; do + case $1 in + -h | --help) + display_help + exit 0 + ;; + -m | --mode) + if [[ $2 == "dpi" || $2 == "geoblock" || $2 == "dns" || $2 == "both" ]]; then + MODE=$2 + else + error_exit "Invalid mode: $2. Valid modes are: dpi, geoblock, dns, both" + fi + shift 2 + ;; + -t | --timeout) + if [[ "$2" =~ ^[0-9]+$ ]]; then + TIMEOUT=$2 + else + error_exit "Invalid timeout value: $2. Timeout must be a positive integer" + fi + shift 2 + ;; + -r | --retries) + if [[ "$2" =~ ^[0-9]+$ ]]; then + RETRIES=$2 + else + error_exit "Invalid retries value: $2. Retry count must be a positive integer" + fi + shift 2 + ;; + -u | --user-agent) + if [[ -n "$2" ]]; then + USER_AGENT=$2 + else + error_exit "User-Agent cannot be empty" + fi + shift 2 + ;; + -f | --file) + if [[ -n "${2:-}" ]]; then + if [[ -f "$2" ]]; then + DOMAINS_FILE="$2" + else + error_exit "File '$2' does not exist" + fi + else + error_exit "File path cannot be empty" + fi + shift 2 + ;; + -6 | --ipv6) + if ! check_ipv6_support; then + error_exit "IPv6 is not supported on this system" + fi - IP_VERSION="6" - shift - ;; - -p | --proxy) - if [[ -n "${2:-}" ]]; then - PROXY="$2" - else - error_exit "Proxy address cannot be empty" - fi - shift 2 - ;; - -d | --domain) - if [[ -n "$2" ]]; then - SINGLE_DOMAIN="$2" - else - error_exit "Domain cannot be empty" - fi - shift 2 - ;; - --http-only) - PROTOCOL="http" - shift - ;; - --https-only) - PROTOCOL="https" - shift - ;; - --no-dns) - DNS_CHECK=false - shift - ;; - --no-header) - SHOW_HEADER=false - shift - ;; - -j | --json) - JSON_OUTPUT=true - shift - ;; - *) - error_exit "Unknown option: $1" - ;; - esac - done + IP_VERSION="6" + shift + ;; + -p | --proxy) + if [[ -n "${2:-}" ]]; then + PROXY="$2" + else + error_exit "Proxy address cannot be empty" + fi + shift 2 + ;; + -d | --domain) + if [[ -n "$2" ]]; then + SINGLE_DOMAIN="$2" + else + error_exit "Domain cannot be empty" + fi + shift 2 + ;; + --http-only) + PROTOCOL="http" + shift + ;; + --https-only) + PROTOCOL="https" + shift + ;; + --no-dns) + DNS_CHECK=false + shift + ;; + --no-header) + SHOW_HEADER=false + shift + ;; + -j | --json) + JSON_OUTPUT=true + shift + ;; + *) + error_exit "Unknown option: $1" + ;; + esac + done - if [[ "$MODE" == "dns" ]] && ! $DNS_CHECK; then - error_exit "--no-dns cannot be used with --mode dns" - fi + if [[ "$MODE" == "dns" ]] && ! $DNS_CHECK; then + error_exit "--no-dns cannot be used with --mode dns" + fi } print_header() { - local mode + local mode - printf "\nTimeout set to: %b%ss%b\n" "$COLOR_WHITE" "$TIMEOUT" "$COLOR_RESET" + printf "\nTimeout set to: %b%ss%b\n" "$COLOR_WHITE" "$TIMEOUT" "$COLOR_RESET" - case $MODE in - dpi) - mode="DPI" - ;; - geoblock) - mode="Geoblock" - ;; - dns) - mode="DNS" - ;; - both) - mode="DPI and Geoblock" - ;; - esac + case $MODE in + dpi) + mode="DPI" + ;; + geoblock) + mode="Geoblock" + ;; + dns) + mode="DNS" + ;; + both) + mode="DPI and Geoblock" + ;; + esac - if [[ -z "$DOMAINS_FILE" ]] && [[ -z "$SINGLE_DOMAIN" ]]; then - printf "Mode set to: %b%s%b\n" "$COLOR_WHITE" "$mode" "$COLOR_RESET" - fi + if [[ -z "$DOMAINS_FILE" ]] && [[ -z "$SINGLE_DOMAIN" ]]; then + printf "Mode set to: %b%s%b\n" "$COLOR_WHITE" "$mode" "$COLOR_RESET" + fi - if [[ "$MODE" == "dns" ]]; then - return - fi + if [[ "$MODE" == "dns" ]]; then + return + fi - printf "Retries set to: %b%s%b\n" "$COLOR_WHITE" "$RETRIES" "$COLOR_RESET" - printf "User-Agent set to: %b%s%b\n" "$COLOR_WHITE" "$USER_AGENT" "$COLOR_RESET" + printf "Retries set to: %b%s%b\n" "$COLOR_WHITE" "$RETRIES" "$COLOR_RESET" + printf "User-Agent set to: %b%s%b\n" "$COLOR_WHITE" "$USER_AGENT" "$COLOR_RESET" - if [[ -n "$DOMAINS_FILE" ]]; then - printf "Domain mode set to: %buser domains from %s%b\n" "$COLOR_WHITE" "$DOMAINS_FILE" "$COLOR_RESET" - elif [[ -n "$SINGLE_DOMAIN" ]]; then - printf "Checking single domain: %b%s%b\n" "$COLOR_WHITE" "$SINGLE_DOMAIN" "$COLOR_RESET" - else - printf "Domain mode set to: %bpredefined domains%b\n" "$COLOR_WHITE" "$COLOR_RESET" - fi + if [[ -n "$DOMAINS_FILE" ]]; then + printf "Domain mode set to: %buser domains from %s%b\n" "$COLOR_WHITE" "$DOMAINS_FILE" "$COLOR_RESET" + elif [[ -n "$SINGLE_DOMAIN" ]]; then + printf "Checking single domain: %b%s%b\n" "$COLOR_WHITE" "$SINGLE_DOMAIN" "$COLOR_RESET" + else + printf "Domain mode set to: %bpredefined domains%b\n" "$COLOR_WHITE" "$COLOR_RESET" + fi - printf "IP version set to: %bIPv%s%b\n" "$COLOR_WHITE" "$IP_VERSION" "$COLOR_RESET" + printf "IP version set to: %bIPv%s%b\n" "$COLOR_WHITE" "$IP_VERSION" "$COLOR_RESET" - if [ -n "$PROXY" ]; then - printf "SOCKS5 proxy set to: %b%s%b\n" "$COLOR_WHITE" "$PROXY" "$COLOR_RESET" - fi + if [ -n "$PROXY" ]; then + printf "SOCKS5 proxy set to: %b%s%b\n" "$COLOR_WHITE" "$PROXY" "$COLOR_RESET" + fi - case $PROTOCOL in - http) - printf "Protocol set to: %bHTTP only%b\n" "$COLOR_WHITE" "$COLOR_RESET" - ;; - https) - printf "Protocol set to: %bHTTPS only%b\n" "$COLOR_WHITE" "$COLOR_RESET" - ;; - both) - printf "Protocol set to: %bHTTP and HTTPS%b\n" "$COLOR_WHITE" "$COLOR_RESET" - ;; - esac + case $PROTOCOL in + http) + printf "Protocol set to: %bHTTP only%b\n" "$COLOR_WHITE" "$COLOR_RESET" + ;; + https) + printf "Protocol set to: %bHTTPS only%b\n" "$COLOR_WHITE" "$COLOR_RESET" + ;; + both) + printf "Protocol set to: %bHTTP and HTTPS%b\n" "$COLOR_WHITE" "$COLOR_RESET" + ;; + esac } run_dns_checks() { - check_encrypted_dns_servers - check_dns_hijacking + check_encrypted_dns_servers + check_dns_hijacking } read_domains_from_file() { - local file=$1 - local domains=() + local file=$1 + local domains=() - while IFS= read -r line || [[ -n "$line" ]]; do - if [[ -n "$line" && ! "$line" =~ ^[[:space:]]*# ]]; then - line=$(echo "$line" | xargs) - if [[ -n "$line" ]]; then - domains+=("$line") - fi - fi - done <"$file" + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ -n "$line" && ! "$line" =~ ^[[:space:]]*# ]]; then + line=$(echo "$line" | xargs) + if [[ -n "$line" ]]; then + domains+=("$line") + fi + fi + done <"$file" - echo "${domains[@]}" + echo "${domains[@]}" } execute_curl() { - local url=$1 - local protocol=$2 - local follow_redirects=$3 - local ip_version_to_use=${4:-$IP_VERSION} - local curl_output - local curl_opts=( - -s - --compressed - -o /dev/null - -w "%{http_code}${CURL_SEPARATOR}%{redirect_url}" - --retry-connrefused - --retry-all-errors - --retry "$RETRIES" - --connect-timeout "$TIMEOUT" - --max-time "$((TIMEOUT * (RETRIES + 1)))" - -"$ip_version_to_use" - -A "$USER_AGENT" - -H "Sec-Fetch-Site: none" - -H "Accept-Language: en-US,en;q=0.5" - ) + local url=$1 + local protocol=$2 + local follow_redirects=$3 + local ip_version_to_use=${4:-$IP_VERSION} + local curl_output + local curl_opts=( + -s + --compressed + -o /dev/null + -w "%{http_code}${CURL_SEPARATOR}%{redirect_url}" + --retry-connrefused + --retry-all-errors + --retry "$RETRIES" + --connect-timeout "$TIMEOUT" + --max-time "$((TIMEOUT * (RETRIES + 1)))" + -"$ip_version_to_use" + -A "$USER_AGENT" + -H "Sec-Fetch-Site: none" + -H "Accept-Language: en-US,en;q=0.5" + ) - if [ -n "$PROXY" ]; then - curl_opts+=(--proxy "socks5h://$PROXY") - fi + if [ -n "$PROXY" ]; then + curl_opts+=(--proxy "socks5h://$PROXY") + fi - if [ "$follow_redirects" = true ]; then - curl_opts+=(-L) - fi + if [ "$follow_redirects" = true ]; then + curl_opts+=(-L) + fi - if curl_output=$(curl "${curl_opts[@]}" "${protocol}://${url}"); then - echo "$curl_output" - else - echo "000${CURL_SEPARATOR}" - fi + if curl_output=$(curl "${curl_opts[@]}" "${protocol}://${url}"); then + echo "$curl_output" + else + echo "000${CURL_SEPARATOR}" + fi } get_domains_to_check() { - if [[ -n "$SINGLE_DOMAIN" ]]; then - echo "$SINGLE_DOMAIN" - elif [[ -z "$DOMAINS_FILE" ]]; then - case $MODE in - # TODO: Replace echo with function - dpi) echo "${DPI_BLOCKED_SITES[@]}" ;; - geoblock) echo "${GEO_BLOCKED_SITES[@]}" ;; - both) echo "${DPI_BLOCKED_SITES[@]}" "${GEO_BLOCKED_SITES[@]}" ;; - esac - else - read_domains_from_file "$DOMAINS_FILE" - fi + if [[ -n "$SINGLE_DOMAIN" ]]; then + echo "$SINGLE_DOMAIN" + elif [[ -z "$DOMAINS_FILE" ]]; then + case $MODE in + # TODO: Replace echo with function + dpi) echo "${DPI_BLOCKED_SITES[@]}" ;; + geoblock) echo "${GEO_BLOCKED_SITES[@]}" ;; + both) echo "${DPI_BLOCKED_SITES[@]}" "${GEO_BLOCKED_SITES[@]}" ;; + esac + else + read_domains_from_file "$DOMAINS_FILE" + fi } get_single_check_result() { - local domain=$1 - local protocol=$2 - local follow_redirects=$3 - local ip_version=$4 - local response status_code redirect_url + local domain=$1 + local protocol=$2 + local follow_redirects=$3 + local ip_version=$4 + local response status_code redirect_url - local port - [[ "${protocol,,}" == "https" ]] && port=443 || port=80 + local port + [[ "${protocol,,}" == "https" ]] && port=443 || port=80 - if [[ -z "$PROXY" ]]; then - local ip - ip=$(get_domain_ip "$domain" "$ip_version") + if [[ -z "$PROXY" ]]; then + local ip + ip=$(get_domain_ip "$domain" "$ip_version") - if [[ -n "$ip" ]] && ! is_port_open "$ip" "$port"; then - jq -n '{"status": -1, "redirect_url": null}' - return - fi - fi + if [[ -n "$ip" ]] && ! is_port_open "$ip" "$port"; then + jq -n '{"status": -1, "redirect_url": null}' + return + fi + fi - response=$(execute_curl "$domain" "$protocol" "$follow_redirects" "$ip_version") - status_code="${response%%$CURL_SEPARATOR*}" - redirect_url="${response#*$CURL_SEPARATOR}" + response=$(execute_curl "$domain" "$protocol" "$follow_redirects" "$ip_version") + status_code="${response%%$CURL_SEPARATOR*}" + redirect_url="${response#*$CURL_SEPARATOR}" - jq -n \ - --argjson status "${status_code:-0}" \ - --arg redirect_url "${redirect_url:-}" \ - ' - { - "status": ($status|tonumber), - "redirect_url": (if $redirect_url == "" then null else $redirect_url end) - } - ' + jq -n \ + --argjson status "${status_code:-0}" \ + --arg redirect_url "${redirect_url:-}" \ + ' + { + "status": ($status|tonumber), + "redirect_url": (if $redirect_url == "" then null else $redirect_url end) + } + ' } gather_single_domain_result() { - local domain=$1 - local ipv6_supported - local http_ipv4=null http_ipv6=null https_ipv4=null https_ipv6=null + local domain=$1 + local ipv6_supported + local http_ipv4=null http_ipv6=null https_ipv4=null https_ipv6=null - check_ipv6_support && ipv6_supported=true || ipv6_supported=false + check_ipv6_support && ipv6_supported=true || ipv6_supported=false - if [[ "$PROTOCOL" == "both" || "$PROTOCOL" == "http" ]]; then - http_ipv4=$(get_single_check_result "$domain" "HTTP" false 4) - if $ipv6_supported; then - http_ipv6=$(get_single_check_result "$domain" "HTTP" false 6) - fi - fi - if [[ "$PROTOCOL" == "both" || "$PROTOCOL" == "https" ]]; then - https_ipv4=$(get_single_check_result "$domain" "HTTPS" true 4) - if $ipv6_supported; then - https_ipv6=$(get_single_check_result "$domain" "HTTPS" true 6) - fi - fi + if [[ "$PROTOCOL" == "both" || "$PROTOCOL" == "http" ]]; then + http_ipv4=$(get_single_check_result "$domain" "HTTP" false 4) + if $ipv6_supported; then + http_ipv6=$(get_single_check_result "$domain" "HTTP" false 6) + fi + fi + if [[ "$PROTOCOL" == "both" || "$PROTOCOL" == "https" ]]; then + https_ipv4=$(get_single_check_result "$domain" "HTTPS" true 4) + if $ipv6_supported; then + https_ipv6=$(get_single_check_result "$domain" "HTTPS" true 6) + fi + fi - jq -n \ - --arg service "$(get_domain_host "$domain")" \ - --argjson http_ipv4 "$http_ipv4" \ - --argjson http_ipv6 "$http_ipv6" \ - --argjson https_ipv4 "$https_ipv4" \ - --argjson https_ipv6 "$https_ipv6" \ - ' - { - "service": $service, - "http": { - "ipv4": $http_ipv4, - "ipv6": $http_ipv6 - }, - "https": { - "ipv4": $https_ipv4, - "ipv6": $https_ipv6 - } - } - ' + jq -n \ + --arg service "$(get_domain_host "$domain")" \ + --argjson http_ipv4 "$http_ipv4" \ + --argjson http_ipv6 "$http_ipv6" \ + --argjson https_ipv4 "$https_ipv4" \ + --argjson https_ipv6 "$https_ipv6" \ + ' + { + "service": $service, + "http": { + "ipv4": $http_ipv4, + "ipv6": $http_ipv6 + }, + "https": { + "ipv4": $https_ipv4, + "ipv6": $https_ipv6 + } + } + ' } get_domain_host() { - printf "%s" "${1%%/*}" + printf "%s" "${1%%/*}" } get_record_type() { - local ip_version=${1:-$IP_VERSION} - [[ "$ip_version" == "6" ]] && echo "AAAA" || echo "A" + local ip_version=${1:-$IP_VERSION} + [[ "$ip_version" == "6" ]] && echo "AAAA" || echo "A" } get_domain_ip() { - local domain - domain=$(get_domain_host "$1") - local ip_version=${2:-$IP_VERSION} + local domain + domain=$(get_domain_host "$1") + local ip_version=${2:-$IP_VERSION} - dig +short "$domain" "$(get_record_type "$ip_version")" 2>/dev/null | - awk '/^[0-9a-fA-F.:]+$/ {print; exit}' || true + dig +short "$domain" "$(get_record_type "$ip_version")" 2>/dev/null | + awk '/^[0-9a-fA-F.:]+$/ {print; exit}' || true } domain_exists() { - local domain - domain=$(get_domain_host "$1") - local rcode + local domain + domain=$(get_domain_host "$1") + local rcode - rcode=$(dig +noall +comments "$domain" A 2>/dev/null | - awk -F'status: ' '/status:/ {split($2, parts, ","); print parts[1]; exit}') + rcode=$(dig +noall +comments "$domain" A 2>/dev/null | + awk -F'status: ' '/status:/ {split($2, parts, ","); print parts[1]; exit}') - [[ "$rcode" != "NXDOMAIN" ]] + [[ "$rcode" != "NXDOMAIN" ]] } get_domain_ips_via_dns() { - local domain=$1 - local server=$2 + local domain=$1 + local server=$2 - dig +short @"$server" "$domain" "$(get_record_type)" \ - +timeout="$TIMEOUT" +tries=1 2>/dev/null | - awk '/^[0-9a-fA-F.:]+$/' || true + dig +short @"$server" "$domain" "$(get_record_type)" \ + +timeout="$TIMEOUT" +tries=1 2>/dev/null | + awk '/^[0-9a-fA-F.:]+$/' || true } resolve_via_dig() { - local domain=$1 - local server_ip=$2 - local transport=$3 + local domain=$1 + local server_ip=$2 + local transport=$3 - dig +short +"$transport" @"$server_ip" "$domain" "$(get_record_type)" \ - +timeout="$TIMEOUT" +tries=1 2>/dev/null | - awk '/^[0-9a-fA-F.:]+$/' + dig +short +"$transport" @"$server_ip" "$domain" "$(get_record_type)" \ + +timeout="$TIMEOUT" +tries=1 2>/dev/null | + awk '/^[0-9a-fA-F.:]+$/' } have_ip_intersection() { - local -n first_ips=$1 - local -n second_ips=$2 + local -n first_ips=$1 + local -n second_ips=$2 - declare -A ip_set=() - local ip + declare -A ip_set=() + local ip - for ip in "${first_ips[@]}"; do - ip_set["$ip"]=1 - done + for ip in "${first_ips[@]}"; do + ip_set["$ip"]=1 + done - for ip in "${second_ips[@]}"; do - [[ -n ${ip_set["$ip"]+1} ]] && return 0 - done + for ip in "${second_ips[@]}"; do + [[ -n ${ip_set["$ip"]+1} ]] && return 0 + done - return 1 + return 1 } probe_resolver_transport() { - if [[ -n "$(resolve_via_dig "$1" "$2" "$3")" ]]; then - echo "available" - else - echo "blocked" - fi + if [[ -n "$(resolve_via_dig "$1" "$2" "$3")" ]]; then + echo "available" + else + echo "blocked" + fi } check_encrypted_dns_servers() { - local test_domain="rutracker.org" - local entry name ip doh_status dot_status - local table_rows=() - local i tmp_dir + local test_domain="rutracker.org" + local entry name ip doh_status dot_status + local table_rows=() + local i tmp_dir - AVAILABLE_DOH=() - AVAILABLE_DOT=() + AVAILABLE_DOH=() + AVAILABLE_DOT=() - tmp_dir=$(mktemp -d) + tmp_dir=$(mktemp -d) - for i in "${!ENCRYPTED_DNS_SERVERS[@]}"; do - ip="${ENCRYPTED_DNS_SERVERS[$i]##*|}" - probe_resolver_transport "$test_domain" "$ip" "https" >"$tmp_dir/${i}_https" & - probe_resolver_transport "$test_domain" "$ip" "tls" >"$tmp_dir/${i}_tls" & - done - wait + for i in "${!ENCRYPTED_DNS_SERVERS[@]}"; do + ip="${ENCRYPTED_DNS_SERVERS[$i]##*|}" + probe_resolver_transport "$test_domain" "$ip" "https" >"$tmp_dir/${i}_https" & + probe_resolver_transport "$test_domain" "$ip" "tls" >"$tmp_dir/${i}_tls" & + done + wait - for i in "${!ENCRYPTED_DNS_SERVERS[@]}"; do - entry="${ENCRYPTED_DNS_SERVERS[$i]}" - name="${entry%%|*}" - ip="${entry##*|}" + for i in "${!ENCRYPTED_DNS_SERVERS[@]}"; do + entry="${ENCRYPTED_DNS_SERVERS[$i]}" + name="${entry%%|*}" + ip="${entry##*|}" - if [[ "$(<"$tmp_dir/${i}_https")" == "available" ]]; then - AVAILABLE_DOH+=("$entry") - doh_status="$MSG_AVAILABLE" - else - doh_status="$MSG_BLOCKED" - fi + if [[ "$(<"$tmp_dir/${i}_https")" == "available" ]]; then + AVAILABLE_DOH+=("$entry") + doh_status="$MSG_AVAILABLE" + else + doh_status="$MSG_BLOCKED" + fi - if [[ "$(<"$tmp_dir/${i}_tls")" == "available" ]]; then - AVAILABLE_DOT+=("$entry") - dot_status="$MSG_AVAILABLE" - else - dot_status="$MSG_BLOCKED" - fi + if [[ "$(<"$tmp_dir/${i}_tls")" == "available" ]]; then + AVAILABLE_DOT+=("$entry") + dot_status="$MSG_AVAILABLE" + else + dot_status="$MSG_BLOCKED" + fi - table_rows+=("$(printf "%s%b\t%s\t%s\t%s" \ - "$name" "$COLOR_RESET" "$ip" \ - "$(colorize_summary "$doh_status")" \ - "$(colorize_summary "$dot_status")")") - done + table_rows+=("$(printf "%s%b\t%s\t%s\t%s" \ + "$name" "$COLOR_RESET" "$ip" \ + "$(colorize_summary "$doh_status")" \ + "$(colorize_summary "$dot_status")")") + done - rm -rf "$tmp_dir" + rm -rf "$tmp_dir" - printf "\n%bEncrypted DNS (DoH/DoT) availability:%b\n\n" "$COLOR_WHITE" "$COLOR_RESET" + printf "\n%bEncrypted DNS (DoH/DoT) availability:%b\n\n" "$COLOR_WHITE" "$COLOR_RESET" - { - printf "\033[1m%b%s\t%s\t%s\t%s%b\033[0m\n" \ - "$COLOR_WHITE" "Resolver" "IP" "DoH" "DoT" "$COLOR_RESET" - printf "%s\n" "${table_rows[@]}" - } | column -t -s $'\t' + { + printf "\033[1m%b%s\t%s\t%s\t%s%b\033[0m\n" \ + "$COLOR_WHITE" "Resolver" "IP" "DoH" "DoT" "$COLOR_RESET" + printf "%s\n" "${table_rows[@]}" + } | column -t -s $'\t' } get_reference_resolver() { - if [[ ${#AVAILABLE_DOH[@]} -gt 0 ]]; then - printf "%s\thttps" "${AVAILABLE_DOH[0]}" - elif [[ ${#AVAILABLE_DOT[@]} -gt 0 ]]; then - printf "%s\ttls" "${AVAILABLE_DOT[0]}" - fi + if [[ ${#AVAILABLE_DOH[@]} -gt 0 ]]; then + printf "%s\thttps" "${AVAILABLE_DOH[0]}" + elif [[ ${#AVAILABLE_DOT[@]} -gt 0 ]]; then + printf "%s\ttls" "${AVAILABLE_DOT[0]}" + fi } check_dns_hijacking() { - local test_domains=("rutracker.org" "linkedin.com" "flibusta.is") - local regular_dns_ips=() - local encrypted_ips=() - local hijacked_domain="" - local hijacked_ip="" + local test_domains=("rutracker.org" "linkedin.com" "flibusta.is") + local regular_dns_ips=() + local encrypted_ips=() + local hijacked_domain="" + local hijacked_ip="" - local reference reference_entry reference_transport reference_name reference_ip - reference=$(get_reference_resolver) + local reference reference_entry reference_transport reference_name reference_ip + reference=$(get_reference_resolver) - if [[ -z "$reference" ]]; then - printf "\n%b%s%b\n" \ - "$COLOR_ORANGE" \ - "Cannot verify DNS spoofing: no DoH/DoT server reachable, skipping check" \ - "$COLOR_RESET" - return 0 - fi + if [[ -z "$reference" ]]; then + printf "\n%b%s%b\n" \ + "$COLOR_ORANGE" \ + "Cannot verify DNS spoofing: no DoH/DoT server reachable, skipping check" \ + "$COLOR_RESET" + return 0 + fi - reference_entry="${reference%%$'\t'*}" - reference_transport="${reference##*$'\t'}" - reference_name="${reference_entry%%|*}" - reference_ip="${reference_entry##*|}" + reference_entry="${reference%%$'\t'*}" + reference_transport="${reference##*$'\t'}" + reference_name="${reference_entry%%|*}" + reference_ip="${reference_entry##*|}" - for test_domain in "${test_domains[@]}"; do - regular_dns_ips=() - encrypted_ips=() + for test_domain in "${test_domains[@]}"; do + regular_dns_ips=() + encrypted_ips=() - for dns_server in "${DNS_SERVERS[@]}"; do - mapfile -t regular_dns_ips < <(get_domain_ips_via_dns "$test_domain" "$dns_server") - [[ ${#regular_dns_ips[@]} -gt 0 ]] && break - done + for dns_server in "${DNS_SERVERS[@]}"; do + mapfile -t regular_dns_ips < <(get_domain_ips_via_dns "$test_domain" "$dns_server") + [[ ${#regular_dns_ips[@]} -gt 0 ]] && break + done - mapfile -t encrypted_ips < <(resolve_via_dig "$test_domain" "$reference_ip" "$reference_transport") + mapfile -t encrypted_ips < <(resolve_via_dig "$test_domain" "$reference_ip" "$reference_transport") - [[ ${#regular_dns_ips[@]} -eq 0 ]] || [[ ${#encrypted_ips[@]} -eq 0 ]] && continue + [[ ${#regular_dns_ips[@]} -eq 0 ]] || [[ ${#encrypted_ips[@]} -eq 0 ]] && continue - if ! have_ip_intersection regular_dns_ips encrypted_ips; then - hijacked_domain="$test_domain" - hijacked_ip="${regular_dns_ips[0]}" - break - fi - done + if ! have_ip_intersection regular_dns_ips encrypted_ips; then + hijacked_domain="$test_domain" + hijacked_ip="${regular_dns_ips[0]}" + break + fi + done - if [[ -n "$hijacked_domain" ]]; then - printf "\n%b%s%b %s %b%s%b %s %b%s%b\n" \ - "$COLOR_RED" "DNS hijacking detected!" "$COLOR_RESET" \ - "ISP redirects" "$COLOR_WHITE" "$hijacked_domain" "$COLOR_RESET" "to" \ - "$COLOR_RED" "$hijacked_ip" "$COLOR_RESET" + if [[ -n "$hijacked_domain" ]]; then + printf "\n%b%s%b %s %b%s%b %s %b%s%b\n" \ + "$COLOR_RED" "DNS hijacking detected!" "$COLOR_RESET" \ + "ISP redirects" "$COLOR_WHITE" "$hijacked_domain" "$COLOR_RESET" "to" \ + "$COLOR_RED" "$hijacked_ip" "$COLOR_RESET" - printf "%bReference resolver: %s (%s) over %s%b\n\n" \ - "$COLOR_WHITE" "$reference_name" "$reference_ip" "${reference_transport^^}" "$COLOR_RESET" + printf "%bReference resolver: %s (%s) over %s%b\n\n" \ + "$COLOR_WHITE" "$reference_name" "$reference_ip" "${reference_transport^^}" "$COLOR_RESET" - printf "%b%s%b\n%b%s%b\n" \ - "$COLOR_ORANGE" "DNS hijacking may affect the accuracy of this check" "$COLOR_RESET" \ - "$COLOR_ORANGE" "Configure encrypted DNS (DoH/DoT) on your system" "$COLOR_RESET" - else - printf "\n%b%s%b %b(reference: %s over %s)%b\n" \ - "$COLOR_GREEN" "Good news, no DNS hijacking detected!" "$COLOR_RESET" \ - "$COLOR_WHITE" "$reference_name" "${reference_transport^^}" "$COLOR_RESET" - fi + printf "%b%s%b\n%b%s%b\n" \ + "$COLOR_ORANGE" "DNS hijacking may affect the accuracy of this check" "$COLOR_RESET" \ + "$COLOR_ORANGE" "Configure encrypted DNS (DoH/DoT) on your system" "$COLOR_RESET" + else + printf "\n%b%s%b %b(reference: %s over %s)%b\n" \ + "$COLOR_GREEN" "Good news, no DNS hijacking detected!" "$COLOR_RESET" \ + "$COLOR_WHITE" "$reference_name" "${reference_transport^^}" "$COLOR_RESET" + fi } is_port_open() { - local ip="$1" - local port="$2" - timeout "$TIMEOUT" bash -c "(echo >/dev/tcp/$ip/$port)" 2>/dev/null + local ip="$1" + local port="$2" + timeout "$TIMEOUT" bash -c "(echo >/dev/tcp/$ip/$port)" 2>/dev/null } is_ip_reachable() { - local ip="$1" - is_port_open "$ip" 80 || is_port_open "$ip" 443 + local ip="$1" + is_port_open "$ip" 80 || is_port_open "$ip" 443 } make_json_error() { - local domain="$1" - local error_code="$2" - local error_message="$3" + local domain="$1" + local error_code="$2" + local error_message="$3" - jq -n \ - --arg service "$domain" \ - --arg code "$error_code" \ - --arg error "$error_message" \ - ' - { - "service": $service, - "error": $error, - "error_code": $code, - "http": null, - "https": null - } - ' + jq -n \ + --arg service "$domain" \ + --arg code "$error_code" \ + --arg error "$error_message" \ + ' + { + "service": $service, + "error": $error, + "error_code": $code, + "http": null, + "https": null + } + ' } summarize_status_description() { - local status_code=$1 - local redirect_url=$2 - local msg + local status_code=$1 + local redirect_url=$2 + local msg - if [[ "$status_code" -eq -1 ]]; then - msg="$MSG_BLOCKED_BY_PORT" - elif [[ -z "$status_code" || "$status_code" = "000" || "$status_code" -eq 0 ]]; then - msg="$MSG_TIMEOUT" - elif [[ "$status_code" -ge 300 && "$status_code" -lt 400 ]]; then - [[ -z "$redirect_url" ]] && redirect_url=" " - msg=$(printf "%s (%s) -> %s" "$MSG_REDIRECT" "$status_code" "$redirect_url") - elif [[ "$status_code" -eq 200 ]]; then - msg="$MSG_AVAILABLE ($status_code)" - elif [[ "$status_code" -eq 403 ]]; then - msg="$MSG_ACCESS_DENIED ($status_code)" - else - msg="$MSG_OTHER $status_code" - fi + if [[ "$status_code" -eq -1 ]]; then + msg="$MSG_BLOCKED_BY_PORT" + elif [[ -z "$status_code" || "$status_code" = "000" || "$status_code" -eq 0 ]]; then + msg="$MSG_TIMEOUT" + elif [[ "$status_code" -ge 300 && "$status_code" -lt 400 ]]; then + [[ -z "$redirect_url" ]] && redirect_url="" + msg=$(printf "%s (%s) -> %s" "$MSG_REDIRECT" "$status_code" "$redirect_url") + elif [[ "$status_code" -eq 200 ]]; then + msg="$MSG_AVAILABLE ($status_code)" + elif [[ "$status_code" -eq 403 ]]; then + msg="$MSG_ACCESS_DENIED ($status_code)" + else + msg="$MSG_OTHER $status_code" + fi - echo "$msg" + echo "$msg" } colorize_summary() { - local message="$1" - local first_word rest first_word_color + local message="$1" + local first_word rest first_word_color - first_word="${message%% *}" - if [[ "$first_word" == "$message" ]]; then - rest="" - else - rest="${message#* }" - fi + first_word="${message%% *}" + if [[ "$first_word" == "$message" ]]; then + rest="" + else + rest="${message#* }" + fi - case "$first_word" in - Blocked) - first_word_color=$COLOR_RED - ;; - Available) - first_word_color=$COLOR_GREEN - ;; - Redirected) - first_word_color=$COLOR_BLUE - ;; - Denied) - first_word_color=$COLOR_RED - ;; - *) - first_word_color=$COLOR_ORANGE - ;; - esac + case "$first_word" in + Blocked) + first_word_color=$COLOR_RED + ;; + Available) + first_word_color=$COLOR_GREEN + ;; + Redirected) + first_word_color=$COLOR_BLUE + ;; + Denied) + first_word_color=$COLOR_RED + ;; + *) + first_word_color=$COLOR_ORANGE + ;; + esac - if [[ -z "$rest" ]]; then - printf "%b%s%b" "$first_word_color" "$first_word" "$COLOR_RESET" - else - printf "%b%s%b %s" "$first_word_color" "$first_word" "$COLOR_RESET" "$rest" - fi + if [[ -z "$rest" ]]; then + printf "%b%s%b" "$first_word_color" "$first_word" "$COLOR_RESET" + else + printf "%b%s%b %s" "$first_word_color" "$first_word" "$COLOR_RESET" "$rest" + fi } summarize_protocol_result() { - local result_json=$1 - local protocol=$2 + local result_json=$1 + local protocol=$2 - if [[ "$PROTOCOL" != "both" && "$PROTOCOL" != "$protocol" ]]; then - echo "Skipped" - return - fi + if [[ "$PROTOCOL" != "both" && "$PROTOCOL" != "$protocol" ]]; then + echo "Skipped" + return + fi - local data - data=$(jq -c --arg protocol "$protocol" --arg ip_version "$IP_VERSION" ' - if $ip_version == "6" then .[$protocol].ipv6 - else .[$protocol].ipv4 - end - ' <<<"$result_json") + local data + data=$(jq -c --arg protocol "$protocol" --arg ip_version "$IP_VERSION" ' + if $ip_version == "6" then .[$protocol].ipv6 + else .[$protocol].ipv4 + end + ' <<<"$result_json") - if [[ "$data" == "null" || -z "$data" ]]; then - echo "N/A" - return - fi + if [[ "$data" == "null" || -z "$data" ]]; then + echo "N/A" + return + fi - local status redirect - status=$(jq -r '.status' <<<"$data") - redirect=$(jq -r '.redirect_url // ""' <<<"$data") - summarize_status_description "$status" "$redirect" + local status redirect + status=$(jq -r '.status' <<<"$data") + redirect=$(jq -r '.redirect_url // ""' <<<"$data") + summarize_status_description "$status" "$redirect" } add_text_result_row() { - local service=$1 - local ip=$2 - local http_cell=$3 - local https_cell=$4 + local service=$1 + local ip=$2 + local http_cell=$3 + local https_cell=$4 - TEXT_RESULTS+=("$( - jq -n \ - --arg service "$service" \ - --arg ip "$ip" \ - --arg http "$http_cell" \ - --arg https "$https_cell" \ - '{service: $service, ip: $ip, http: $http, https: $https}' - )") + TEXT_RESULTS+=("$( + jq -n \ + --arg service "$service" \ + --arg ip "$ip" \ + --arg http "$http_cell" \ + --arg https "$https_cell" \ + '{service: $service, ip: $ip, http: $http, https: $https}' + )") } add_text_result_from_json() { - local result_json=$1 - local ip=$2 - local service - service=$(echo "$result_json" | jq -r '.service') - local http_cell https_cell + local result_json=$1 + local ip=$2 + local service + service=$(echo "$result_json" | jq -r '.service') + local http_cell https_cell - http_cell=$(summarize_protocol_result "$result_json" "http") - https_cell=$(summarize_protocol_result "$result_json" "https") + http_cell=$(summarize_protocol_result "$result_json" "http") + https_cell=$(summarize_protocol_result "$result_json" "https") - add_text_result_row "$service" "$ip" "$http_cell" "$https_cell" + add_text_result_row "$service" "$ip" "$http_cell" "$https_cell" } print_table_results() { - printf "\n" - { - printf "\033[1m%b%s\t%s\t%s\t%s%b\033[0m\n" \ - "$COLOR_WHITE" \ - "Service" \ - "IP" \ - "HTTP" \ - "HTTPS" \ - "$COLOR_RESET" + printf "\n" + { + printf "\033[1m%b%s\t%s\t%s\t%s%b\033[0m\n" \ + "$COLOR_WHITE" \ + "Service" \ + "IP" \ + "HTTP" \ + "HTTPS" \ + "$COLOR_RESET" - for row_json in "${TEXT_RESULTS[@]}"; do - local service http https - service=$(jq -r '.service' <<<"$row_json") - ip=$(jq -r '.ip' <<<"$row_json") - http=$(jq -r '.http' <<<"$row_json") - https=$(jq -r '.https' <<<"$row_json") + for row_json in "${TEXT_RESULTS[@]}"; do + local service http https + service=$(jq -r '.service' <<<"$row_json") + ip=$(jq -r '.ip' <<<"$row_json") + http=$(jq -r '.http' <<<"$row_json") + https=$(jq -r '.https' <<<"$row_json") - printf "%s%b\t%s\t%s\t%s\n" \ - "$service" \ - "$COLOR_RESET" \ - "$ip" \ - "$(colorize_summary "$http")" \ - "$(colorize_summary "$https")" - done - } | column -t -s $'\t' + printf "%s%b\t%s\t%s\t%s\n" \ + "$service" \ + "$COLOR_RESET" \ + "$ip" \ + "$(colorize_summary "$http")" \ + "$(colorize_summary "$https")" + done + } | column -t -s $'\t' } run_checks_and_print() { - local domains - local all_results_json="[]" + local domains + local all_results_json="[]" - if [[ "$MODE" == "dns" ]]; then - if $JSON_OUTPUT; then - error_exit "JSON output is not supported for dns mode" - fi + if [[ "$MODE" == "dns" ]]; then + if $JSON_OUTPUT; then + error_exit "JSON output is not supported for dns mode" + fi - if $SHOW_HEADER; then - print_header - fi + if $SHOW_HEADER; then + print_header + fi - run_dns_checks - return - fi + run_dns_checks + return + fi - read -r -a domains <<<"$(get_domains_to_check)" + read -r -a domains <<<"$(get_domains_to_check)" - local total_domains=${#domains[@]} + local total_domains=${#domains[@]} - local current_index=0 + local current_index=0 - TEXT_RESULTS=() + TEXT_RESULTS=() - if ! $JSON_OUTPUT; then - if $SHOW_HEADER; then - print_header - fi - if $DNS_CHECK; then - run_dns_checks - fi - if $SHOW_HEADER || $DNS_CHECK; then - printf "\n" - fi - fi + if ! $JSON_OUTPUT; then + if $SHOW_HEADER; then + print_header + fi + if $DNS_CHECK; then + run_dns_checks + fi + if $SHOW_HEADER || $DNS_CHECK; then + printf "\n" + fi + fi - for domain in "${domains[@]}"; do - ((++current_index)) - show_progress "$current_index" "$total_domains" "$domain" + for domain in "${domains[@]}"; do + ((++current_index)) + show_progress "$current_index" "$total_domains" "$domain" - local host ip_address + local host ip_address - host=$(get_domain_host "$domain") - ip_address=$(get_domain_ip "$domain") + host=$(get_domain_host "$domain") + ip_address=$(get_domain_ip "$domain") - if [[ -z "$ip_address" ]]; then - local error_code error_message + if [[ -z "$ip_address" ]]; then + local error_code error_message - if domain_exists "$domain"; then - error_code="no_dns_record" - error_message="No $(get_record_type) record" - else - error_code="nxdomain" - error_message="Domain does not exist" - fi + if domain_exists "$domain"; then + error_code="no_dns_record" + error_message="No $(get_record_type) record" + else + error_code="nxdomain" + error_message="Domain does not exist" + fi - if $JSON_OUTPUT; then - all_results_json=$(echo "$all_results_json" | jq --argjson item "$(make_json_error "$host" "$error_code" "$error_message")" '. + [$item]') - else - add_text_result_row "$host" "N/A" "$error_message" "$error_message" - fi - continue - fi + if $JSON_OUTPUT; then + all_results_json=$(echo "$all_results_json" | jq --argjson item "$(make_json_error "$host" "$error_code" "$error_message")" '. + [$item]') + else + add_text_result_row "$host" "N/A" "$error_message" "$error_message" + fi + continue + fi - if [[ -z "$PROXY" ]] && ! is_ip_reachable "$ip_address"; then - if $JSON_OUTPUT; then - all_results_json=$(echo "$all_results_json" | jq --argjson item "$(make_json_error "$host" blocked_by_ip "$MSG_BLOCKED_BY_IP")" '. + [$item]') - else - add_text_result_row "$host" "$ip_address" "$MSG_BLOCKED_BY_IP" "$MSG_BLOCKED_BY_IP" - fi - continue - fi + if [[ -z "$PROXY" ]] && ! is_ip_reachable "$ip_address"; then + if $JSON_OUTPUT; then + all_results_json=$(echo "$all_results_json" | jq --argjson item "$(make_json_error "$host" blocked_by_ip "$MSG_BLOCKED_BY_IP")" '. + [$item]') + else + add_text_result_row "$host" "$ip_address" "$MSG_BLOCKED_BY_IP" "$MSG_BLOCKED_BY_IP" + fi + continue + fi - local domain_result_json - domain_result_json=$(gather_single_domain_result "$domain") + local domain_result_json + domain_result_json=$(gather_single_domain_result "$domain") - if $JSON_OUTPUT; then - all_results_json=$(echo "$all_results_json" | jq --argjson item "$domain_result_json" '. + [$item]') - else - add_text_result_from_json "$domain_result_json" "$ip_address" - fi - done + if $JSON_OUTPUT; then + all_results_json=$(echo "$all_results_json" | jq --argjson item "$domain_result_json" '. + [$item]') + else + add_text_result_from_json "$domain_result_json" "$ip_address" + fi + done - clear_progress + clear_progress - if $JSON_OUTPUT; then - local ipv6_supported - check_ipv6_support && ipv6_supported=true || ipv6_supported=false + if $JSON_OUTPUT; then + local ipv6_supported + check_ipv6_support && ipv6_supported=true || ipv6_supported=false - local ip_version_param_val - if $ipv6_supported; then - ip_version_param_val="IPv4 & IPv6" - else - ip_version_param_val="IPv4" - fi + local ip_version_param_val + if $ipv6_supported; then + ip_version_param_val="IPv4 & IPv6" + else + ip_version_param_val="IPv4" + fi - local params_json + local params_json - params_json=$( - jq -n \ - --arg timeout "${TIMEOUT}s" \ - --arg retries "$RETRIES" \ - --arg mode "${MODE^^}" \ - --arg user_agent "$USER_AGENT" \ - --arg domain_mode "$(if [[ -n "$DOMAINS_FILE" ]]; then echo "user domains from $DOMAINS_FILE"; elif [[ -n "$SINGLE_DOMAIN" ]]; then echo "single domain"; else echo "predefined domains"; fi)" \ - --arg ip_version "$ip_version_param_val" \ - --arg protocol "$(if [[ "$PROTOCOL" == "both" ]]; then echo "HTTP and HTTPS"; elif [[ "$PROTOCOL" == "http" ]]; then echo "HTTP only"; else echo "HTTPS only"; fi)" \ - '[ - {"key":"timeout", "value":$timeout}, + params_json=$( + jq -n \ + --arg timeout "${TIMEOUT}s" \ + --arg retries "$RETRIES" \ + --arg mode "${MODE^^}" \ + --arg user_agent "$USER_AGENT" \ + --arg domain_mode "$(if [[ -n "$DOMAINS_FILE" ]]; then echo "user domains from $DOMAINS_FILE"; elif [[ -n "$SINGLE_DOMAIN" ]]; then echo "single domain"; else echo "predefined domains"; fi)" \ + --arg ip_version "$ip_version_param_val" \ + --arg protocol "$(if [[ "$PROTOCOL" == "both" ]]; then echo "HTTP and HTTPS"; elif [[ "$PROTOCOL" == "http" ]]; then echo "HTTP only"; else echo "HTTPS only"; fi)" \ + '[ + {"key":"timeout", "value":$timeout}, - {"key":"retries", "value":$retries}, - {"key":"mode", "value":$mode}, - {"key":"user_agent", "value":$user_agent}, - {"key":"domain_mode", "value":$domain_mode}, - {"key":"ip_version", "value":$ip_version}, - {"key":"protocol", "value":$protocol} - ]' - ) + {"key":"retries", "value":$retries}, + {"key":"mode", "value":$mode}, + {"key":"user_agent", "value":$user_agent}, + {"key":"domain_mode", "value":$domain_mode}, + {"key":"ip_version", "value":$ip_version}, + {"key":"protocol", "value":$protocol} + ]' + ) - jq -n \ - --argjson params "$params_json" \ - --argjson results "$all_results_json" \ - '{ - "version": 1, - "params": $params, - "results": $results - }' + jq -n \ + --argjson params "$params_json" \ + --argjson results "$all_results_json" \ + '{ + "version": 1, + "params": $params, + "results": $results + }' - return - fi + return + fi - print_table_results + print_table_results } main() { - set -euo pipefail + set -euo pipefail - trap cleanup INT TERM + trap cleanup INT TERM - parse_arguments "$@" - install_dependencies - run_checks_and_print + parse_arguments "$@" + install_dependencies + run_checks_and_print - if ! $JSON_OUTPUT; then - printf "\n" - fi + if ! $JSON_OUTPUT; then + printf "\n" + fi - trap - INT TERM + trap - INT TERM } main "$@" diff --git a/apps/api/scripts/censorcheck/launcher.sh b/apps/api/scripts/censorcheck/launcher.sh index a108a5d..4b853a6 100644 --- a/apps/api/scripts/censorcheck/launcher.sh +++ b/apps/api/scripts/censorcheck/launcher.sh @@ -5,9 +5,13 @@ set -euo pipefail VT_API_URL="${VT_API_URL:-__VT_API_URL__}" VT_INGEST_TOKEN="${VT_INGEST_TOKEN:-__VT_INGEST_TOKEN__}" -LAUNCHER_VERSION="1" +LAUNCHER_VERSION="3" VENDOR_SHA="12c5839" +OS_ID="unknown" +OS_LIKE="" +OS_NAME="unknown" + trap 'exit 130' INT log() { printf '%s\n' "$*"; } @@ -17,9 +21,137 @@ require_cmd() { command -v "$1" >/dev/null 2>&1 || die "Нужна команда '$1' (установите пакет и повторите)." } +detect_os() { + OS_ID="unknown" + OS_LIKE="" + OS_NAME="unknown" + if [ -r /etc/os-release ]; then + # shellcheck disable=SC1091 + . /etc/os-release + OS_ID="${ID:-unknown}" + OS_LIKE="${ID_LIKE:-}" + OS_NAME="${PRETTY_NAME:-$OS_ID}" + fi +} + +detect_pkg_manager() { + case "$OS_ID" in + debian|ubuntu|linuxmint|pop|raspbian|kali|astra|devuan) printf 'apt'; return 0 ;; + alpine) printf 'apk'; return 0 ;; + arch|manjaro|endeavouros) printf 'pacman'; return 0 ;; + fedora) printf 'dnf'; return 0 ;; + rhel|centos|rocky|almalinux|ol) + if command -v dnf >/dev/null 2>&1; then printf 'dnf'; else printf 'yum'; fi + return 0 + ;; + esac + case " $OS_LIKE " in + *" debian "*|*" ubuntu "*) printf 'apt'; return 0 ;; + *" rhel "*|*" fedora "*|*" centos "*) + if command -v dnf >/dev/null 2>&1; then printf 'dnf'; else printf 'yum'; fi + return 0 + ;; + *" arch "*) printf 'pacman'; return 0 ;; + *" alpine "*) printf 'apk'; return 0 ;; + esac + if command -v apt-get >/dev/null 2>&1; then + printf 'apt' + elif command -v dnf >/dev/null 2>&1; then + printf 'dnf' + elif command -v yum >/dev/null 2>&1; then + printf 'yum' + elif command -v pacman >/dev/null 2>&1; then + printf 'pacman' + elif command -v apk >/dev/null 2>&1; then + printf 'apk' + else + return 1 + fi +} + +pkg_alts() { + local cmd="$1" pm="$2" + case "$pm:$cmd" in + *:jq) printf '%s\n' jq ;; + apt:dig) printf '%s\n' dnsutils bind9-dnsutils ;; + dnf:dig|yum:dig) printf '%s\n' bind-utils ;; + pacman:dig) printf '%s\n' bind ;; + apk:dig) printf '%s\n' bind-tools ;; + apt:column) printf '%s\n' bsdextrautils bsdmainutils ;; + dnf:column|yum:column|pacman:column) printf '%s\n' util-linux ;; + apk:column) printf '%s\n' util-linux-misc util-linux ;; + *) return 1 ;; + esac +} + +root_prefix() { + if [ "${EUID:-$(id -u)}" -eq 0 ]; then + return 0 + fi + if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then + printf 'sudo -n' + return 0 + fi + return 1 +} + +install_one_package() { + local pm="$1" pkg="$2" + local prefix="" + prefix="$(root_prefix)" || die "Нужны права root, чтобы установить: ${pkg}" + # shellcheck disable=SC2086 + case "$pm" in + apt) + $prefix env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y -qq "$pkg" + ;; + dnf) $prefix dnf install -y "$pkg" ;; + yum) $prefix yum install -y "$pkg" ;; + pacman) $prefix pacman -S --noconfirm --needed "$pkg" ;; + apk) $prefix apk add --no-cache "$pkg" ;; + *) return 1 ;; + esac +} + +install_cmd() { + local pm="$1" cmd="$2" alt + while IFS= read -r alt; do + [ -n "$alt" ] || continue + log " пакет ${alt} → команда ${cmd}" + if install_one_package "$pm" "$alt"; then + command -v "$cmd" >/dev/null 2>&1 && return 0 + fi + done < <(pkg_alts "$cmd" "$pm") + return 1 +} + +ensure_cmds() { + local missing=() cmd pm prefix="" + detect_os + pm="$(detect_pkg_manager)" || pm="" + log "ОС: ${OS_NAME} (id=${OS_ID}${OS_LIKE:+ like=${OS_LIKE}}, pkg=${pm:-unknown})" + + for cmd in "$@"; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + [ "${#missing[@]}" -eq 0 ] && return 0 + [ -n "$pm" ] || die "Не удалось определить пакетный менеджер (${OS_NAME}). Установите вручную: ${missing[*]}" + log "Отсутствуют команды: ${missing[*]}. Устанавливаю..." + + if [ "$pm" = apt ]; then + prefix="$(root_prefix)" || die "Нужны права root, чтобы установить: ${missing[*]}" + # shellcheck disable=SC2086 + $prefix env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update -qq + fi + + for cmd in "${missing[@]}"; do + install_cmd "$pm" "$cmd" || die "Не удалось установить зависимость для '$cmd' (${OS_NAME})" + command -v "$cmd" >/dev/null 2>&1 || die "Команда '$cmd' так и не появилась после установки" + done +} + require_cmd curl -require_cmd jq require_cmd bash +ensure_cmds jq dig column detect_public_ip() { local ip="" diff --git a/apps/api/src/routes/launcher.test.ts b/apps/api/src/routes/launcher.test.ts index 1070afa..817f882 100644 --- a/apps/api/src/routes/launcher.test.ts +++ b/apps/api/src/routes/launcher.test.ts @@ -26,6 +26,11 @@ describe('GET /cc launcher', () => { expect(res.headers['cache-control']).toMatch(/no-store/) expect(res.body).toContain('https://vt.shnt.top') expect(res.body).toContain('VT_INGEST_TOKEN') + expect(res.body).toContain('ensure_cmds jq dig column') + expect(res.body).toContain('detect_os') + expect(res.body).toContain('/etc/os-release') + expect(res.body).toContain('apt-get install -y -qq') + expect(res.body).not.toContain('\r') expect(res.body).not.toContain('__VT_API_URL__') expect(res.body).not.toContain('__VT_INGEST_TOKEN__') }) @@ -35,5 +40,8 @@ describe('GET /cc launcher', () => { expect(res.statusCode).toBe(200) expect(res.body).toContain('#!/usr/bin/env bash') expect(res.body).toContain('SCRIPT_NAME') + expect(res.body).toContain('is_installed()') + expect(res.body).toContain('cat < {