#!/usr/bin/env bash set -Eeuo pipefail umask 077 domain_value="${TPROXY_HOSTNAME:-}" edge_network="telegram-web-proxy-edge" usage() { cat <<'USAGE' Использование: sudo ./quick-install.sh [--domain ДОМЕН] Быстрая установка Telegram WEB proxy и Caddy на чистый VPS. Без параметров скрипт спросит только домен. DNS-запись A домена должна уже указывать прямо на публичный IPv4 этого сервера. Поддерживаются 64-битные Ubuntu и Debian на архитектуре x86_64. USAGE } fail() { printf 'Ошибка: %s\n' "$1" >&2 exit 1 } while (($#)); do case "$1" in --domain) (($# >= 2)) || fail "после --domain нужно указать домен" domain_value="$2" shift 2 ;; -h | --help) usage exit 0 ;; *) printf 'Неизвестный параметр: %s\n\n' "$1" >&2 usage >&2 exit 2 ;; esac done if ((EUID != 0)); then if command -v sudo >/dev/null 2>&1; then if [[ -n "$domain_value" ]]; then exec sudo bash "$0" --domain "$domain_value" fi exec sudo bash "$0" fi fail "запустите скрипт от root: sudo ./quick-install.sh" fi if [[ "$(uname -s)" != Linux || "$(uname -m)" != x86_64 ]]; then fail "требуется 64-битный Linux x86_64" fi repository="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" for required_file in compose.yml compose.quick.yml Caddyfile Dockerfile; do [[ -f "$repository/$required_file" ]] || fail "рядом со скриптом отсутствует $required_file; скачайте весь репозиторий" done if [[ ! -r /etc/os-release ]]; then fail "не удалось определить операционную систему" fi # shellcheck disable=SC1091 source /etc/os-release case "${ID:-}" in ubuntu | debian) ;; *) fail "поддерживаются только Ubuntu и Debian, получено: ${ID:-неизвестно}" ;; esac [[ -n "${VERSION_CODENAME:-}" ]] || fail "не удалось определить версию системы" if [[ -z "$domain_value" ]]; then [[ -t 0 ]] || fail "в автоматическом режиме укажите --domain proxy.example.com" printf 'Введите домен прокси (например, proxy.example.com): ' read -r domain_value fi domain_value="$(printf '%s' "$domain_value" | tr '[:upper:]' '[:lower:]' | sed -E 's#^[[:space:]]*https?://##; s#/.*$##; s/:.*$//; s/[[:space:]]//g')" valid_hostname() { local value=$1 label local -a labels [[ ${#value} -le 253 && "$value" == *.* ]] || return 1 [[ "$value" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ ]] || return 1 local IFS=. read -r -a labels <<<"$value" for label in "${labels[@]}"; do [[ -n "$label" && ${#label} -le 63 ]] || return 1 [[ "$label" != -* && "$label" != *- ]] || return 1 done } valid_ipv4() { local ip=$1 octet local -a octets local IFS=. read -r -a octets <<<"$ip" [[ ${#octets[@]} -eq 4 ]] || return 1 for octet in "${octets[@]}"; do [[ "$octet" =~ ^[0-9]{1,3}$ ]] || return 1 ((10#$octet <= 255)) || return 1 done } public_ipv4() { local ip=$1 local -a octets local IFS=. valid_ipv4 "$ip" || return 1 read -r -a octets <<<"$ip" ((10#${octets[0]} != 0)) || return 1 ((10#${octets[0]} != 10)) || return 1 ((10#${octets[0]} != 127)) || return 1 ! ((10#${octets[0]} == 100 && 10#${octets[1]} >= 64 && 10#${octets[1]} <= 127)) || return 1 ! ((10#${octets[0]} == 169 && 10#${octets[1]} == 254)) || return 1 ! ((10#${octets[0]} == 172 && 10#${octets[1]} >= 16 && 10#${octets[1]} <= 31)) || return 1 ! ((10#${octets[0]} == 192 && 10#${octets[1]} == 0)) || return 1 ! ((10#${octets[0]} == 192 && 10#${octets[1]} == 88 && 10#${octets[2]} == 99)) || return 1 ! ((10#${octets[0]} == 192 && 10#${octets[1]} == 168)) || return 1 ! ((10#${octets[0]} == 198 && (10#${octets[1]} == 18 || 10#${octets[1]} == 19))) || return 1 ! ((10#${octets[0]} == 198 && 10#${octets[1]} == 51 && 10#${octets[2]} == 100)) || return 1 ! ((10#${octets[0]} == 203 && 10#${octets[1]} == 0 && 10#${octets[2]} == 113)) || return 1 ((10#${octets[0]} < 224)) || return 1 } valid_hostname "$domain_value" || fail "некорректный домен: $domain_value" export DEBIAN_FRONTEND=noninteractive printf '\n[1/6] Устанавливаю системные компоненты...\n' apt-get update apt-get install -y --no-install-recommends ca-certificates curl iproute2 mapfile -t resolved_ips < <( getent ahostsv4 "$domain_value" 2>/dev/null | awk '{print $1}' | sort -u ) if ((${#resolved_ips[@]} == 0)); then fail "домен $domain_value пока не имеет DNS-записи A" fi if ((${#resolved_ips[@]} != 1)); then fail "для простой установки оставьте у домена одну DNS-запись A; найдено: ${resolved_ips[*]}" fi public_ip="${resolved_ips[0]}" public_ipv4 "$public_ip" || fail "DNS домена указывает не на публичный IPv4: $public_ip" printf 'DNS найден: %s -> %s\n' "$domain_value" "$public_ip" install_docker() { local -a conflicts=() local package for package in docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc; do if dpkg-query -W -f='${db:Status-Abbrev}' "$package" 2>/dev/null | grep -q '^ii'; then conflicts+=("$package") fi done if ((${#conflicts[@]} > 0)); then fail "обнаружены конфликтующие пакеты (${conflicts[*]}). Используйте чистый VPS или удалите их вручную" fi printf '\n[2/6] Устанавливаю Docker из официального репозитория...\n' install -m 0755 -d /etc/apt/keyrings curl --fail --silent --show-error --location \ --proto '=https' --proto-redir '=https' --tlsv1.2 \ "https://download.docker.com/linux/${ID}/gpg" \ -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc cat >/etc/apt/sources.list.d/docker.sources </dev/null 2>&1 && docker compose version >/dev/null 2>&1; then printf '\n[2/6] Docker и Docker Compose уже установлены.\n' else install_docker fi systemctl enable --now docker docker info >/dev/null 2>&1 || fail "Docker установлен, но его служба не запустилась" docker compose version >/dev/null 2>&1 || fail "плагин Docker Compose недоступен" own_caddy_running="$(docker inspect --format '{{.State.Running}}' tg-web-proxy-caddy 2>/dev/null || true)" busy_ports="$(ss -H -ltn 2>/dev/null | awk '$4 ~ /:(80|443)$/ {print $4}' | sort -u)" if [[ -n "$busy_ports" && "$own_caddy_running" != true ]]; then fail "порты 80 или 443 уже заняты: ${busy_ports//$'\n'/, }. Для быстрой установки нужен чистый VPS" fi printf '\n[3/6] Готовлю закрытую Docker-сеть и настройки...\n' if ! docker network inspect "$edge_network" >/dev/null 2>&1; then docker network create "$edge_network" >/dev/null fi domain_check_token="$(od -An -N16 -tx1 /dev/urandom | tr -d '[:space:]')" [[ "$domain_check_token" =~ ^[0-9a-f]{32}$ ]] || fail "не удалось создать маркер проверки домена" environment_file="$repository/.env" temporary_environment="$(mktemp "$repository/.env.new.XXXXXX")" trap 'rm -f "$temporary_environment"' EXIT cat >"$temporary_environment" <&2 || true fail "контейнер прокси не запустился (состояние: ${proxy_health:-неизвестно})" fi client_secret="$(docker exec tg-web-proxy sh -c 'tr -d "\\r\\n" < /data/client-secret')" [[ "$client_secret" =~ ^[0-9a-f]{32}$ ]] || fail "не удалось прочитать клиентский секрет" printf '\n[6/6] Проверяю домен и жду выпуска HTTPS-сертификата' https_ready=false domain_check_url="https://$domain_value/.well-known/telegram-web-proxy-check" for _ in $(seq 1 60); do domain_check_response="$(curl --fail --silent --show-error \ --connect-timeout 5 --max-time 15 \ --proto '=https' --tlsv1.2 \ "$domain_check_url" 2>/dev/null || true)" if [[ "$domain_check_response" == "$domain_check_token" ]] && curl --fail --silent --show-error \ --connect-timeout 5 --max-time 15 \ --proto '=https' --tlsv1.2 \ "https://$domain_value/" >/dev/null 2>&1; then https_ready=true break fi printf '.' sleep 5 done printf '\n' if [[ "$https_ready" != true ]]; then docker logs --tail 100 tg-web-proxy-caddy >&2 || true cat >&2 <