diff --git a/INSTALLER_GUIDE.md b/INSTALLER_GUIDE.md index fe9c961..1c15ab1 100644 --- a/INSTALLER_GUIDE.md +++ b/INSTALLER_GUIDE.md @@ -46,7 +46,8 @@ UDP-порты, firewall, Hysteria, Xray/3x-ui, nginx и обнаруженну ### 3. Публикация -- **Свой домен и сайт** — локальная витрина, HTTPS и кнопка подключения. +- **Прокси + сайт + домен** — рекомендуемый вариант по умолчанию: локальная + витрина, HTTPS и кнопка подключения. Проверяются DNS, сертификат, порт `80` и внутренний порт сайта. - **Только прокси** — без публичной витрины. Подходит дополнительному узлу, которому достаточно клиентской кнопки в общем Telegram-боте. @@ -55,6 +56,9 @@ UDP-порты, firewall, Hysteria, Xray/3x-ui, nginx и обнаруженну регистрируются и отправляют heartbeat. Каждый сервер с включённым сайтом показывает собственную локальную витрину; сайты между узлами не копируются. +При ошибке формата домена или DNS-проверки мастер остаётся на этом экране и +предлагает исправить адрес. `0` возвращает в меню без изменения служб. + Если для домена уже есть пригодный сертификат в `/etc/letsencrypt/live`, мастер проверит имя и срок действия, покажет найденную линию и подключит её без повторного выпуска. Если TCP/80 занят другой службой, IGProxy не меняет её diff --git a/README.md b/README.md index 6f845af..40e8bdc 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,14 @@ sudo ./IGProxy ### Способ публикации - **Свой домен и сайт** — MTProxy и HTTPS-витрина работают через один публичный - адрес. Нужна DNS-запись домена на IP сервера. + адрес. Это рекомендуемый вариант и выбор по умолчанию. Нужна DNS-запись + домена на IP сервера. - **Только прокси** — без nginx, сертификата и сайта. Подходит, если домена нет или существующие службы уже используют web-порты. +Если домен введён с ошибкой или его DNS ещё не готов, мастер объяснит причину и +останется на том же шаге. Установку не придётся начинать заново. + Порт `443` используется только если он свободен либо уже принадлежит IGProxy. Hysteria, 3x-ui/Xray, nginx и другие найденные службы не перемещаются автоматически. Мастер предложит другой свободный TCP-порт. diff --git a/install.sh b/install.sh index 6e88271..406ead1 100644 --- a/install.sh +++ b/install.sh @@ -1016,17 +1016,20 @@ install_pro_mode() { else echo "" fi - echo -ne " ${WHITE}$(t install_enter_domain)${NC} " - read -r user_domain - user_domain="$(printf '%s' "$user_domain" | tr -d '[:space:]')" - if [ -z "$user_domain" ] || ! validate_domain "$user_domain"; then - log_error "$(tf install_bad_domain "${user_domain:-}")" - return - fi - - # Строгая DNS/ACME-проверка до установки пакетов и изменения nginx. - if type installer_domain_preflight >/dev/null 2>&1; then - installer_domain_preflight "$user_domain" || return + if type installer_prompt_domain >/dev/null 2>&1; then + if installer_prompt_domain; then + user_domain="$INSTALLER_SELECTED_DOMAIN" + else + return $? + fi + else + echo -ne " ${WHITE}$(t install_enter_domain)${NC} " + read -r user_domain + user_domain="$(printf '%s' "$user_domain" | tr -d '[:space:]')" + if [ -z "$user_domain" ] || ! validate_domain "$user_domain"; then + log_error "$(tf install_bad_domain "${user_domain:-}")" + return + fi fi if [ "${INSTALLER_DOMAIN_CERT_REUSED:-0}" = "1" ]; then installer_firewall_check_ports "$public_port" || return @@ -1097,12 +1100,6 @@ install_pro_mode() { installer_show_apply_plan "pro" "$public_port" "$user_domain" "$nginx_internal_port" \ "${INSTALLER_DOMAIN_HTTP_LISTENER:-1}" - if [ "$lazy" = "1" ]; then - log_info "$(t lazy_autoconfirm)" - else - if ! confirm "$(t install_confirm_proxy_site)"; then return; fi - fi - INSTALLER_TX_DOMAIN="$user_domain" INSTALLER_TX_CERT_NAME="${INSTALLER_DOMAIN_CERT_LINEAGE:-}" installer_transaction_begin "domain-site:$public_port" || return @@ -2273,11 +2270,11 @@ installer_dry_run() { fi if [ "$selected_mode" = "pro" ]; then - echo "" - echo -ne " Домен для сайта: " - read -r domain - domain=$(printf '%s' "$domain" | tr -d '[:space:]') - installer_domain_preflight "$domain" || return 1 + if installer_prompt_domain; then + domain="$INSTALLER_SELECTED_DOMAIN" + else + return $? + fi internal_port=$(installer_pick_internal_port "$public_port") || { log_error "Не найден свободный локальный порт для сайта." return 1 diff --git a/lib/common.sh b/lib/common.sh index 2f4f4f6..9a1cdec 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -3,7 +3,7 @@ # Colors, logging, spinner, system helpers, v1 compat, i18n-aware # ── Version ─────────────────────────────────────────────────────────────────── -GOTELEGRAM_VERSION="2.15.6" +GOTELEGRAM_VERSION="2.15.7" GOTELEGRAM_NAME="IGProxy" # ── Пути ────────────────────────────────────────────────────────────────────── diff --git a/lib/installer_wizard.sh b/lib/installer_wizard.sh index 438ff5d..2dcbe21 100644 --- a/lib/installer_wizard.sh +++ b/lib/installer_wizard.sh @@ -731,13 +731,41 @@ installer_domain_preflight() { installer_domain_http_preflight "$INSTALLER_DOMAIN_CERT_REUSED" } +INSTALLER_SELECTED_DOMAIN="${INSTALLER_SELECTED_DOMAIN:-}" + +installer_prompt_domain() { + local candidate="" + INSTALLER_SELECTED_DOMAIN="" + while true; do + echo -ne " ${WHITE:-}Домен для сайта (0 — в меню):${NC:-} " >&2 + if ! IFS= read -r candidate; then + return 10 + fi + candidate=$(printf '%s' "$candidate" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]') + [ "$candidate" = "0" ] && return 10 + if [ -z "$candidate" ] || ! validate_domain "$candidate"; then + log_error "Некорректный домен: ${candidate:-<пусто>}" + log_dim "Введите только имя, например proxy.example.com — без https://, пути и порта." + echo "" + continue + fi + if installer_domain_preflight "$candidate"; then + INSTALLER_SELECTED_DOMAIN="$candidate" + return 0 + fi + log_warning "Домен пока не прошёл проверку. Исправьте DNS или введите другой адрес." + log_dim "Мастер остаётся на этом шаге; уже работающие службы не изменялись." + echo "" + done +} + installer_choose_mode() { if type ig_ui_select >/dev/null 2>&1; then ig_ui_clear ig_ui_logo "Публикация" ig_ui_stepper 3 "Роль" "Проверка" "Публикация" "Установка" ig_ui_heading "ШАГ 3 · ПУБЛИКАЦИЯ" "Как публиковать прокси?" \ - "Роль уже выбрана: $(installer_role_title "${DEPLOYMENT_ROLE:-standalone}"). Теперь независимо выбирается способ публикации." + "По умолчанию IGProxy устанавливает прокси вместе с доменом и HTTPS-сайтом." if [ "${DEPLOYMENT_ROLE:-standalone}" = "hub" ]; then ig_ui_status active "Свой домен и сайт" \ "Центру нужен HTTPS-адрес для безопасного подключения дополнительных узлов." @@ -745,9 +773,9 @@ installer_choose_mode() { printf 'pro\n' return 0 fi - ig_ui_select "Выберите режим" 2 \ - "pro|Свой домен и сайт|HTTPS-витрина, сертификат Let's Encrypt и MTProxy.|Сайт + HTTPS" \ - "lite|Только прокси|Без nginx и сертификата; выбранная роль сервера не меняется.|Без сайта" + ig_ui_select "Выберите режим" 1 \ + "pro|Прокси + сайт + домен|MTProxy, HTTPS-витрина и сертификат Let's Encrypt.|Рекомендуется" \ + "lite|Только прокси|Запасной вариант без nginx, домена и сертификата.|Без сайта" return $? fi local choice @@ -758,14 +786,14 @@ installer_choose_mode() { fi echo "" >&2 echo -e " ${BOLD:-}Что установить?${NC:-}" >&2 - echo -e " ${CYAN:-}1)${NC:-} ${BOLD:-}Свой домен и сайт${NC:-}" >&2 + echo -e " ${CYAN:-}1)${NC:-} ${BOLD:-}Прокси + сайт + домен${NC:-} ${GREEN:-}(рекомендуется)${NC:-}" >&2 echo -e " ${DIM:-}Прокси маскируется под ваш HTTPS-сайт. Нужны готовые DNS-записи.${NC:-}" >&2 echo -e " ${CYAN:-}2)${NC:-} ${BOLD:-}Только прокси${NC:-}" >&2 echo -e " ${DIM:-}Без сайта и сертификата. Лучше для серверов с уже занятым 443.${NC:-}" >&2 echo -e " ${DIM:-}0) Отмена${NC:-}" >&2 - echo -ne " Выбор [2]: " >&2 + echo -ne " Выбор [1]: " >&2 read -r choice - case "${choice:-2}" in + case "${choice:-1}" in 1) printf 'pro\n' ;; 2) printf 'lite\n' ;; 0) return 10 ;; diff --git a/tests/test_installer_wizard.py b/tests/test_installer_wizard.py index 6c8423e..9c57f2e 100644 --- a/tests/test_installer_wizard.py +++ b/tests/test_installer_wizard.py @@ -235,6 +235,44 @@ def test_mode_picker_keeps_prompt_visible_and_stdout_machine_clean(self): self.assertEqual(result.stdout.strip(), "lite") self.assertIn("Что установить?", result.stderr) + def test_site_and_domain_mode_is_the_default(self): + result = run_bash("printf '\\n' | installer_choose_mode") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.strip(), "pro") + self.assertIn("Выбор [1]", result.stderr) + self.assertIn("рекомендуется", result.stderr) + self.assertNotIn("install_confirm_proxy_site", INSTALL.read_text(encoding="utf-8")) + + def test_domain_prompt_retries_format_and_dns_errors(self): + result = run_bash( + """ +validate_domain() { + [ "$1" = first.example ] || [ "$1" = ready.example ] +} +installer_domain_preflight() { [ "$1" = ready.example ]; } +log_error() { printf 'ERROR:%s\\n' "$*" >&2; } +log_warning() { printf 'WARN:%s\\n' "$*" >&2; } +log_dim() { printf 'INFO:%s\\n' "$*" >&2; } +installer_prompt_domain <<< $'https://wrong.example\\nFIRST.EXAMPLE\\nREADY.EXAMPLE' +status=$? +printf '%s:%s\\n' "$status" "$INSTALLER_SELECTED_DOMAIN" +""" + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.strip(), "0:ready.example") + self.assertIn("Некорректный домен", result.stderr) + self.assertIn("Домен пока не прошёл проверку", result.stderr) + + def test_domain_prompt_zero_returns_without_preflight(self): + result = run_bash( + """ +validate_domain() { return 0; } +installer_domain_preflight() { exit 99; } +installer_prompt_domain <<< '0' +""" + ) + self.assertEqual(result.returncode, 10, result.stderr) + def test_mode_zero_returns_to_previous_step(self): result = run_bash("printf '0\\n' | installer_choose_mode") self.assertEqual(result.returncode, 10, result.stderr)