Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion INSTALLER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ UDP-порты, firewall, Hysteria, Xray/3x-ui, nginx и обнаруженну

### 3. Публикация

- **Свой домен и сайт** — локальная витрина, HTTPS и кнопка подключения.
- **Прокси + сайт + домен** — рекомендуемый вариант по умолчанию: локальная
витрина, HTTPS и кнопка подключения.
Проверяются DNS, сертификат, порт `80` и внутренний порт сайта.
- **Только прокси** — без публичной витрины. Подходит дополнительному узлу,
которому достаточно клиентской кнопки в общем Telegram-боте.
Expand All @@ -55,6 +56,9 @@ UDP-порты, firewall, Hysteria, Xray/3x-ui, nginx и обнаруженну
регистрируются и отправляют heartbeat. Каждый сервер с включённым сайтом
показывает собственную локальную витрину; сайты между узлами не копируются.

При ошибке формата домена или DNS-проверки мастер остаётся на этом экране и
предлагает исправить адрес. `0` возвращает в меню без изменения служб.

Если для домена уже есть пригодный сертификат в `/etc/letsencrypt/live`,
мастер проверит имя и срок действия, покажет найденную линию и подключит её без
повторного выпуска. Если TCP/80 занят другой службой, IGProxy не меняет её
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,14 @@ sudo ./IGProxy
### Способ публикации

- **Свой домен и сайт** — MTProxy и HTTPS-витрина работают через один публичный
адрес. Нужна DNS-запись домена на IP сервера.
адрес. Это рекомендуемый вариант и выбор по умолчанию. Нужна DNS-запись
домена на IP сервера.
- **Только прокси** — без nginx, сертификата и сайта. Подходит, если домена нет
или существующие службы уже используют web-порты.

Если домен введён с ошибкой или его DNS ещё не готов, мастер объяснит причину и
останется на том же шаге. Установку не придётся начинать заново.

Порт `443` используется только если он свободен либо уже принадлежит IGProxy.
Hysteria, 3x-ui/Xray, nginx и другие найденные службы не перемещаются
автоматически. Мастер предложит другой свободный TCP-порт.
Expand Down
41 changes: 19 additions & 22 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:-<empty>}")"
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:-<empty>}")"
return
fi
fi
if [ "${INSTALLER_DOMAIN_CERT_REUSED:-0}" = "1" ]; then
installer_firewall_check_ports "$public_port" || return
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

# ── Пути ──────────────────────────────────────────────────────────────────────
Expand Down
42 changes: 35 additions & 7 deletions lib/installer_wizard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -731,23 +731,51 @@ 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-адрес для безопасного подключения дополнительных узлов."
ig_ui_note "Прокси может слушать не 443, если этот порт занят другой службой."
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
Expand All @@ -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 ;;
Expand Down
38 changes: 38 additions & 0 deletions tests/test_installer_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading