rProxy/rproxy

1665 lines
68 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
# rProxy — Менеджер обратного прокси для роутеров Keenetic / Netcraze
# Публикация локальных сервисов через SSH-туннели + nginx на VPS
# https://github.com/l-ptrol/rProxy
VERSION="1.8.8"
export PATH="/opt/bin:/opt/sbin:$PATH"
CONF_DIR="/opt/etc/rproxy"
CONF_FILE="$CONF_DIR/rproxy.conf"
SERVICES_DIR="$CONF_DIR/services"
VPS_DIR="$CONF_DIR/vps"
PID_DIR="/opt/var/run/rproxy"
SSH_KEY="$CONF_DIR/id_ed25519"
REMOTE_NGINX_DIR="/etc/nginx/sites-enabled"
REMOTE_LOCATIONS_DIR="/etc/nginx/rproxy_locations"
BASE_TUNNEL_PORT=10000
BASE_EXT_PORT=26000
CERTBOT_EMAIL="" # Будет заполнено из конфига
# ─── Цвета ───────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'
msg() { printf "${GREEN}${NC} %s\n" "$*"; }
warn() { printf "${YELLOW}${NC} %s\n" "$*"; }
err() { printf "${RED}${NC} %s\n" "$*" >&2; }
header() { printf "\n${CYAN}${BOLD}%s${NC}\n" "$*"; }
# ─── Утилиты ─────────────────────────────────────────────────────────
clear_screen() { printf "\033[2J\033[H"; }
draw_box() {
local title="$1"
header "$title"
}
draw_separator() {
printf "${DIM}──────────────────────────────────────────────────${NC}\n"
}
prompt() {
printf "\n${CYAN}${NC} $1"
read -r REPLY
}
pause() {
printf "\n${DIM}Нажмите Enter для продолжения...${NC}"
read -r _
}
check_conf() {
if [ ! -f "$CONF_FILE" ]; then
mkdir -p "$CONF_DIR"
touch "$CONF_FILE"
fi
. "$CONF_FILE"
# Строгая проверка: ищем хотя бы один файл в vps/, в котором заполнена переменная VPS_HOST
for f in "$VPS_DIR"/*.conf; do
[ -f "$f" ] || continue
if grep -qE "VPS_HOST=['\"]?[^'\" ]+" "$f"; then
return 0
fi
done
return 1
}
load_vps() {
local vps_id="$1"
local vps_file="$VPS_DIR/$vps_id.conf"
if [ ! -f "$vps_file" ]; then
return 1
fi
# Сброс старых переменных перед загрузкой
VPS_HOST="" VPS_PORT="" VPS_USER="" VPS_AUTH="" VPS_PASS=""
. "$vps_file"
CUR_VPS_ID="$vps_id"
return 0
}
migrate_config() {
mkdir -p "$VPS_DIR" "$SERVICES_DIR" "$PID_DIR"
[ ! -f "$CONF_FILE" ] && touch "$CONF_FILE"
# Проверка на "битый" (пустой) конфиг
local broken=0
[ -f "$VPS_DIR/default.conf" ] && ! grep -q "VPS_HOST=" "$VPS_DIR/default.conf" && broken=1
if [ -f "$CONF_FILE" ] && { [ ! -f "$VPS_DIR/default.conf" ] || [ "$broken" -eq 1 ]; }; then
# Если rproxy.conf ещё содержит данные серверов (не пустой)
if grep -q "VPS_HOST=" "$CONF_FILE"; then
msg "Восстановление конфигурации VPS..."
. "$CONF_FILE"
cat > "$VPS_DIR/default.conf" <<EOF
VPS_HOST="$VPS_HOST"
VPS_PORT="$VPS_PORT"
VPS_USER="$VPS_USER"
VPS_AUTH="$VPS_AUTH"
VPS_PASS="$VPS_PASS"
EOF
# Очищаем rproxy.conf от старых системных переменных, НО сохраняем новые
local tmp_conf="/tmp/rproxy.conf.tmp"
printf "CERTBOT_EMAIL='%s'\n" "$CERTBOT_EMAIL" > "$tmp_conf"
[ -n "$DEFAULT_AUTH_USER" ] && printf "DEFAULT_AUTH_USER='%s'\n" "$DEFAULT_AUTH_USER" >> "$tmp_conf"
[ -n "$DEFAULT_AUTH_PASS" ] && printf "DEFAULT_AUTH_PASS='%s'\n" "$DEFAULT_AUTH_PASS" >> "$tmp_conf"
mv "$tmp_conf" "$CONF_FILE"
fi
[ -f "$VPS_DIR/default.conf" ] && chmod 600 "$VPS_DIR/default.conf"
fi
# Если профиль default остался битым и восстановить нечего — удаляем его
if [ -f "$VPS_DIR/default.conf" ] && ! grep -q "VPS_HOST=" "$VPS_DIR/default.conf"; then
rm -f "$VPS_DIR/default.conf"
fi
}
ensure_local_deps() {
local missing=""
_has_cmd() {
command -v "$1" >/dev/null 2>&1 || [ -x "/opt/bin/$1" ] || [ -x "/opt/sbin/$1" ]
}
_has_cmd openssl || missing="$missing openssl-util"
_has_cmd curl || missing="$missing curl"
if [ -n "$missing" ]; then
warn "Обнаружены недостающие пакеты:$missing"
msg "Попытка автоматической установки через opkg..."
if ! opkg update; then
err "Не удалось обновить списки пакетов opkg. Проверьте интернет на роутере."
return 1
fi
for pkg in $missing; do
msg "Установка $pkg..."
if opkg install "$pkg"; then
msg "Пакет $pkg успешно установлен."
else
err "Не удалось установить $pkg. Возможно, его нет в репозитории вашей модели."
fi
done
hash -r 2>/dev/null
msg "Проверка зависимостей завершена."
fi
}
ssh_cmd() {
local ssh_exec="/opt/bin/ssh"
[ ! -f "$ssh_exec" ] && ssh_exec="ssh"
$ssh_exec -q -o StrictHostKeyChecking=no -o LogLevel=ERROR -i "$SSH_KEY" -p "$VPS_PORT" "$VPS_USER@$VPS_HOST" "$@"
}
scp_cmd() {
local scp_exec="/opt/bin/scp"
[ ! -f "$scp_exec" ] && scp_exec="scp"
$scp_exec -q -o StrictHostKeyChecking=no -o LogLevel=ERROR -i "$SSH_KEY" -P "$VPS_PORT" "$@"
}
next_free_port() {
local port=$BASE_TUNNEL_PORT
while true; do
local used=0
for f in "$SERVICES_DIR"/*.conf; do
[ -f "$f" ] || continue
grep -q "SVC_TUNNEL_PORT=\"$port\"" "$f" && used=1 && break
done
[ "$used" -eq 0 ] && echo "$port" && return
port=$((port + 1))
done
}
get_router_ip() {
# Метод 1: Через ndmq (Keenetic Bridge0)
local ip=$(ndmq -p "show interface Bridge0" -path "address" 2>/dev/null)
[ -n "$ip" ] && [ "$ip" != "0.0.0.0" ] && echo "$ip" && return
# Метод 2: Через маршруты (default gateway)
ip=$(ip route show | grep default | awk '{print $3}' | head -n 1)
[ -n "$ip" ] && echo "$ip" && return
# Метод 3: Через ip addr (Entware/Keenetic)
ip=$(ip addr show br0 2>/dev/null | grep 'inet ' | awk '{print $2}' | cut -d/ -f1 | head -n1)
[ -n "$ip" ] && echo "$ip" && return
# Запасной вариант
echo "192.168.1.1"
}
# Метод 2: Через ndmq (Локальный доступ в Entware)
# Этот метод не требует пароля и работает мгновенно
rci_call() {
local command="$1"
ndmq -p "$command" 2>/dev/null
}
enable_router_basic_auth() {
log_info "Настройка локального доступа через RCI/ndmq..."
local hostname=$(rci_call "show system" | grep "hostname" | awk '{print $NF}')
log_info "Хостнейм роутера: $hostname"
return 0
}
gen_htpasswd() {
local user="$1"
local pass="$2"
# Пытаемся использовать openssl если он есть в Entware
if command -v openssl >/dev/null 2>&1; then
local hash=$(openssl passwd -apr1 "$pass")
echo "$user:$hash"
else
warn "Утилита 'openssl' не найдена! Пароль будет сохранен в открытом виде (Nginx может его не принять)."
warn "Рекомендуется: opkg update && opkg install openssl-util"
echo "$user:$pass"
fi
}
next_free_ext_port() {
local port=$BASE_EXT_PORT
while true; do
local used=0
for f in "$SERVICES_DIR"/*.conf; do
[ -f "$f" ] || continue
grep -q "SVC_EXT_PORT=\"$port\"" "$f" && used=1 && break
done
[ "$used" -eq 0 ] && echo "$port" && return
port=$((port + 1))
done
}
get_pid_file() { echo "$PID_DIR/$1.pid"; }
is_running() {
local pf
pf=$(get_pid_file "$1")
[ -f "$pf" ] && kill -0 "$(cat "$pf")" 2>/dev/null
}
count_services() {
local count=0
for f in "$SERVICES_DIR"/*.conf; do
[ -f "$f" ] && count=$((count + 1))
done
echo $count
}
# ══════════════════════════════════════════════════════════════════════
# ГЛАВНОЕ МЕНЮ
# ══════════════════════════════════════════════════════════════════════
main_menu() {
while true; do
clear_screen
draw_box "rProxy v$VERSION"
printf "\n"
if ! check_conf; then
printf " ${YELLOW}⚠ VPS не настроен${NC}\n\n"
printf " ${BOLD}1)${NC} Настроить подключение к VPS\n"
printf " ${BOLD}0)${NC} Выход\n"
prompt "Выберите действие: "
case "$REPLY" in
1) do_setup ;;
0|q) printf "\n"; exit 0 ;;
*) ;;
esac
continue
fi
# Подсчёт сервисов и статусов по всем VPS
local total=0 online=0
for f in "$SERVICES_DIR"/*.conf; do
[ -f "$f" ] || continue
total=$((total + 1))
(
. "$f"
is_running "$SVC_NAME" && exit 0
exit 1
) && online=$((online + 1))
done
printf " ${DIM}Серверов VPS:${NC} ${BOLD}$(ls -A "$VPS_DIR" | wc -l)${NC} "
printf "${DIM}Сервисов:${NC} ${BOLD}$total${NC} "
printf "${DIM}Онлайн:${NC} ${GREEN}${BOLD}$online${NC}\n"
draw_separator
printf "\n"
printf " ${BOLD}1)${NC} 📋 Список сервисов и статус\n"
printf " ${BOLD}2)${NC} Добавить сервис\n"
printf " ${BOLD}3)${NC} 📝 Редактировать сервис\n"
printf " ${BOLD}4)${NC} ❌ Удалить сервис\n"
draw_separator
printf " ${BOLD}5)${NC} ▶️ Запустить туннель\n"
printf " ${BOLD}6)${NC} ⏹️ Остановить туннель\n"
printf " ${BOLD}7)${NC} 🔄 Перезапустить туннель\n"
draw_separator
draw_separator
printf " ${BOLD}8)${NC} 🔒 Получить/Обновить SSL (Certbot)\n"
printf " ${BOLD}9)${NC} ⚙️ Настройки VPS\n"
printf " ${BOLD}10)${NC} 🚀 Обновить rProxy\n"
printf " ${BOLD}11)${NC} 🏥 Проверка VPS (Health)\n"
printf " ${BOLD}0)${NC} 🚪 Выход\n"
prompt "Выберите действие: "
case "$REPLY" in
1) show_status ;;
2) do_add_interactive ;;
3) do_edit_interactive ;;
4) do_remove_interactive ;;
5) do_start_interactive ;;
6) do_stop_interactive ;;
7) do_restart_interactive ;;
8) do_ssl_interactive ;;
9) do_setup ;;
10) do_self_update ;;
11) do_health_check ;;
0|q) clear_screen; exit 0 ;;
*) ;;
esac
done
}
# ══════════════════════════════════════════════════════════════════════
# СТАТУС
# ══════════════════════════════════════════════════════════════════════
show_status() {
clear_screen
draw_box "Список сервисов"
printf "\n"
local has_services=0
local idx=0
printf " ${BOLD}%-4s %-14s %-22s %-7s %-9s %-18s${NC}\n" "№" "ИМЯ" "ЦЕЛЬ" "ПОРТ" "СТАТУС" "ДОМЕН"
draw_separator
for f in "$SERVICES_DIR"/*.conf; do
[ -f "$f" ] || continue
has_services=1
idx=$((idx + 1))
(
load_service "$(basename "$f" .conf)" >/dev/null 2>&1
local status_text
if is_running "$SVC_NAME"; then
status_text="${GREEN}● онлайн${NC}"
else
status_text="${RED}○ офлайн${NC}"
fi
local domain_text="${SVC_DOMAIN:-}"
if [ -n "$SVC_DOMAIN" ]; then
[ "$SVC_EXT_PORT" != "80" ] && [ "$SVC_EXT_PORT" != "443" ] && domain_text="$domain_text:$SVC_EXT_PORT"
[ "$SVC_PATH" != "/" ] && domain_text="$domain_text$SVC_PATH"
fi
local auto_text=""
[ "$SVC_ENABLED" = "yes" ] && auto_text=" ${DIM}[авто]${NC}"
printf " %-4s %-14s %-22s %-7s ${status_text} %-18s${auto_text}\n" \
"$idx" "$SVC_NAME" "$SVC_TARGET_HOST:$SVC_TARGET_PORT" "$SVC_EXT_PORT" "$domain_text"
)
done
if [ "$has_services" -eq 0 ]; then
printf "\n ${YELLOW}Нет добавленных сервисов.${NC}\n"
printf " ${DIM}Выберите «Добавить сервис» в главном меню.${NC}\n"
fi
printf "\n"
pause
}
# ══════════════════════════════════════════════════════════════════════
# ══════════════════════════════════════════════════════════════════════
# ПОМОЩНИКИ VPS
# ══════════════════════════════════════════════════════════════════════
find_vps_by_domain() {
local dom="$1"
FOUND_VPS_ID=""
# Пытаемся получить IP через ping (BusyBox стиль)
local ip=$(ping -c 1 "$dom" 2>/dev/null | grep -o "[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}" | head -n 1)
# Если не вышло, пробуем nslookup (парсим более надежно через grep -o)
[ -z "$ip" ] && ip=$(nslookup "$dom" 2>/dev/null | grep "Address" | tail -n 1 | grep -o "[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}" | head -n 1)
[ -z "$ip" ] && return 1
for f in "$VPS_DIR"/*.conf; do
[ -f "$f" ] || continue
# Используем grep для извлечения IP из конфига (быстрее и надежнее источника)
local v_host=$(grep "VPS_HOST=" "$f" | cut -d'"' -f2)
if [ "$v_host" = "$ip" ]; then
FOUND_VPS_ID=$(basename "$f" .conf)
return 0
fi
done
return 1
}
select_vps_interactive() {
SELECTED_VPS_ID=""
draw_box "Выберите VPS для сервиса"
printf "\n"
local vps_list=""
local idx=0
for f in "$VPS_DIR"/*.conf; do
[ -f "$f" ] || continue
idx=$((idx + 1))
(
. "$f"
printf " ${BOLD}%d)${NC} %s (${DIM}%s${NC})\n" "$idx" "$(basename "$f" .conf)" "$VPS_HOST"
)
[ -z "$vps_list" ] && vps_list="$(basename "$f" .conf)" || vps_list="$vps_list $(basename "$f" .conf)"
done
draw_separator
printf " ${BOLD}901)${NC} Добавить новый VPS\n"
printf " ${BOLD}0)${NC} Назад\n"
prompt "Выберите номер: "
[ "$REPLY" = "0" ] && { SELECTED_VPS_ID=""; return 0; }
if [ "$REPLY" = "901" ]; then
do_add_vps
# Возвращаем последний созданный (упрощение)
SELECTED_VPS_ID=$(ls -t "$VPS_DIR"/*.conf | head -n 1 | xargs basename | sed 's/\.conf//')
return 0
fi
# Валидация ввода для cut
if echo "$REPLY" | grep -qE '^[0-9]+$'; then
SELECTED_VPS_ID=$(echo "$vps_list" | cut -d' ' -f"$REPLY")
fi
return 0
}
# ══════════════════════════════════════════════════════════════════════
# ДОБАВИТЬ СЕРВИС (интерактивно)
# ══════════════════════════════════════════════════════════════════════
do_add_interactive() {
clear_screen
draw_box "Добавить новый сервис"
printf "\n"
prompt "Название сервиса (латиницей, без пробелов): "
local name=$(echo "$REPLY" | tr '[:upper:]' '[:lower:]' | tr ' ' '_' | tr -cd '[:alnum:]_-')
[ -z "$name" ] && { warn "Название не может быть пустым"; pause; return; }
if [ -f "$SERVICES_DIR/$name.conf" ]; then
err "Сервис '$name' уже существует"
pause; return
fi
prompt "Адрес локального сервиса (например, 127.0.0.1:8080): "
local target="$REPLY"
[ -z "$target" ] && { warn "Адрес не может быть пустым"; pause; return; }
printf "\n Привязать к домену?\n"
printf " ${BOLD}1)${NC} Да — указать доменное имя (порт 443)\n"
printf " ${BOLD}2)${NC} Нет — указать внешний порт\n"
prompt "Выберите [2]: "
local mode="${REPLY:-2}"
local domain="" ext_port="" use_ssl="no" vps_id="" path="/"
if [ "$mode" = "1" ]; then
prompt "Доменное имя (например, mysite.example.com): "
domain=$(echo "$REPLY" | tr '[:upper:]' '[:lower:]')
[ -z "$domain" ] && { warn "Домен не указан"; pause; return; }
# Проверка на дубликаты доменов
for f in "$SERVICES_DIR"/*.conf; do
[ -f "$f" ] || continue
if grep -q "SVC_DOMAIN=\"$domain\"" "$f" && grep -q "SVC_PATH=\"/\"" "$f"; then
# Конфликт только если и домен и путь совпадают
local conflict=$(basename "$f" .conf)
warn "Домен '$domain' с путем '/' уже используется сервисом '$conflict'"
prompt "Продолжить? (д/н) [н]: "
[ "${REPLY:-н}" != "д" ] && return
fi
done
prompt "Внешний порт [443]: "
ext_port="${REPLY:-443}"
[ "$ext_port" = "443" ] && use_ssl="yes" || use_ssl="no"
prompt "Путь на сервере (например, /app1) [/]: "
path="${REPLY:-/}"
[ "${path#/}" = "$path" ] && path="/$path"
if [ "$path" != "/" ]; then
printf " ${YELLOW} СОВЕТ: Сложные интерфейсы (как у роутеров) могут плохо работать в подпутях.${NC}\n"
printf " ${YELLOW} Для стабильного UI лучше использовать поддомен (напр. app.domain.com) с путем /${NC}\n"
fi
if [ "$use_ssl" = "yes" ] && [ -z "$CERTBOT_EMAIL" ]; then
prompt "Введите Email для Certbot: "
CERTBOT_EMAIL="$REPLY"
sed -i "/CERTBOT_EMAIL=/d" "$CONF_FILE"
echo "CERTBOT_EMAIL=\"$CERTBOT_EMAIL\"" >> "$CONF_FILE"
fi
# Автоопределение VPS по домену
msg "Проверяю куда направлен домен $domain..."
if find_vps_by_domain "$domain"; then
vps_id="$FOUND_VPS_ID"
msg "Домен $domain указывает на ваш VPS '$vps_id'. Выбираю его."
else
warn "Не удалось автоматически определить VPS для $domain"
fi
else
ext_port=$(next_free_ext_port)
prompt "Внешний порт [$ext_port]: "
ext_port="${REPLY:-$ext_port}"
prompt "Путь на сервере (например, /app1) [/]: "
path="${REPLY:-/}"
[ "${path#/}" = "$path" ] && path="/$path"
fi
if [ "$path" != "/" ]; then
printf " ${YELLOW} СОВЕТ: Сложные интерфейсы (как у роутеров) могут плохо работать в подпутях.${NC}\n"
printf " ${YELLOW} Для стабильного UI лучше использовать поддомен (напр. app.domain.com) с путем /${NC}\n"
fi
# Если VPS не определен автоматически — выбираем вручную
if [ -z "$vps_id" ]; then
select_vps_interactive
vps_id="$SELECTED_VPS_ID"
[ -z "$vps_id" ] && { warn "VPS не выбран"; pause; return; }
fi
# Загружаем выбранный VPS для деплоя
load_vps "$vps_id" || { err "Не удалось загрузить VPS '$vps_id'"; pause; return; }
# Разбор адреса
local t_host="${target%:*}"
local t_port="${target#*:}"
[ "$t_host" = "$t_port" ] && { t_port="$t_host"; t_host="127.0.0.1"; }
local tunnel_port=$(next_free_port)
printf "\n"
draw_separator
printf " ${BOLD}Сервис:${NC} $name\n"
printf " ${BOLD}VPS:${NC} $vps_id ($VPS_HOST)\n"
printf " ${BOLD}Цель:${NC} $t_host:$t_port\n"
printf " ${BOLD}Туннель:${NC} порт $tunnel_port\n"
[ -n "$domain" ] && printf " ${BOLD}Домен:${NC} $domain\n"
printf " ${BOLD}Путь:${NC} $path\n"
printf " ${BOLD}Внешний порт:${NC} $ext_port\n"
[ "$use_ssl" = "yes" ] && printf " ${BOLD}SSL:${NC} Да (авто)\n"
prompt "Защитить сервис паролем? (д/н) [н]: "
local use_ndm_auth="no"
local htpasswd_line=""
case "$REPLY" in
д|Д|y|Y|да|yes)
use_ndm_auth="yes"
local def_user="${DEFAULT_AUTH_USER:-admin}"
if [ -n "$DEFAULT_AUTH_PASS" ]; then
printf " ${DIM}Будут использованы сохраненные данные: $def_user / *****${NC}\n"
prompt "Использовать их? (д/н) [д]: "
if [ "${REPLY:-д}" = "д" ] || [ "${REPLY:-д}" = "y" ]; then
htpasswd_line=$(gen_htpasswd "$def_user" "$DEFAULT_AUTH_PASS")
fi
fi
if [ -z "$htpasswd_line" ]; then
prompt "Введите имя пользователя [$def_user]: "
local user="${REPLY:-$def_user}"
prompt "Введите пароль для '$user': "
local pass="$REPLY"
if [ -z "$pass" ]; then
warn "Пароль не может быть пустым. Авторизация будет отключена."
use_ndm_auth="no"
else
htpasswd_line=$(gen_htpasswd "$user" "$pass")
fi
fi
;;
esac
draw_separator
prompt "Всё верно? Добавить сервис? (д/н) [д]: "
[ "${REPLY:-д}" != "д" ] && [ "${REPLY:-д}" != "y" ] && { msg "Отменено"; pause; return; }
msg "Добавляю сервис '$name'..."
# Конфигурация авторизации
local auth_config=""
if [ "$use_ndm_auth" = "yes" ]; then
auth_config="
auth_basic \"Restricted Access\";
auth_basic_user_file /etc/nginx/rproxy_$name.htpasswd;
"
fi
msg "Деплой конфигурации на VPS..."
local tmp="/tmp/rproxy_$name.conf"
generate_nginx_conf "$name" "$t_host" "$t_port" "$tunnel_port" "$domain" "$ext_port" "$use_ndm_auth" "$path" "$tmp"
# Очистка старых конфигов (миграция на v1.8.0+)
ssh_cmd "rm -f $REMOTE_NGINX_DIR/rproxy_$name.conf"
# Деплой htpasswd если нужно
if [ "$use_ndm_auth" = "yes" ]; then
local ht_tmp="/tmp/rproxy_$name.htpasswd"
echo "$htpasswd_line" > "$ht_tmp"
scp_cmd "$ht_tmp" "$VPS_USER@$VPS_HOST:/etc/nginx/rproxy_$name.htpasswd"
rm -f "$ht_tmp"
fi
# Деплой сниппета локации
local remote_loc_dir
if [ -n "$domain" ]; then
remote_loc_dir="$REMOTE_LOCATIONS_DIR/$domain"
else
remote_loc_dir="$REMOTE_LOCATIONS_DIR/_ip_$name"
fi
ssh_cmd "mkdir -p $remote_loc_dir"
scp_cmd "$tmp" "$VPS_USER@$VPS_HOST:$remote_loc_dir/$name.conf" || { err "Ошибка деплоя сниппета"; rm -f "$tmp"; pause; return; }
rm -f "$tmp"
# Пересборка основного конфига
if [ -n "$domain" ]; then
rebuild_vhost_config "$domain" "$ext_port" "$vps_id"
else
rebuild_ip_config "$name" "$ext_port" "$tunnel_port" "$vps_id"
fi
# SSL (теперь Certbot вызывается только если сертификата НЕТ)
if [ "$use_ssl" = "yes" ]; then
if ssh_cmd "[ ! -f /etc/letsencrypt/live/$domain/fullchain.pem ]"; then
msg "SSL сертификат не найден. Запрашиваю через Certbot (certonly)..."
# Используем certonly чтобы Certbot не ломал наши конфиги своими правками
ssh_cmd "certbot certonly --nginx -d $domain --non-interactive --agree-tos -m $CERTBOT_EMAIL" || warn "Certbot вернул ошибку"
# После Certbot пересобираем конфиг, чтобы он прописал SSL пути
rebuild_vhost_config "$domain" "$ext_port" "$vps_id"
else
msg "SSL сертификат уже существует на сервере. Пропускаю Certbot."
fi
fi
# Сохранение конфига сервиса (используем printf для защиты от спецсимволов и одинарные кавычки)
{
printf 'SVC_NAME="%s"\n' "$name"
printf 'SVC_VPS="%s"\n' "$vps_id"
printf 'SVC_TARGET_HOST="%s"\n' "$t_host"
printf 'SVC_TARGET_PORT="%s"\n' "$t_port"
printf 'SVC_TUNNEL_PORT="%s"\n' "$tunnel_port"
printf 'SVC_EXT_PORT="%s"\n' "$ext_port"
printf 'SVC_DOMAIN="%s"\n' "$domain"
printf 'SVC_PATH="%s"\n' "$path"
printf 'SVC_SSL="%s"\n' "$use_ssl"
printf 'SVC_NDM_AUTH="%s"\n' "$use_ndm_auth"
printf "SVC_HTPASSWD='%s'\n" "$htpasswd_line"
printf 'SVC_ENABLED="yes"\n'
} > "$SERVICES_DIR/$name.conf"
msg "Сервис '$name' добавлен на VPS '$vps_id'!"
do_start_service "$name"
pause
}
# ══════════════════════════════════════════════════════════════════════
# РЕДАКТИРОВАТЬ СЕРВИС
# ══════════════════════════════════════════════════════════════════════
# ══════════════════════════════════════════════════════════════════════
# ПОЛУЧИТЬ SSL (Certbot)
# ══════════════════════════════════════════════════════════════════════
do_ssl_interactive() {
clear_screen
draw_box "Получить SSL сертификат (Certbot)"
printf "\n"
select_service "Выпуститиь SSL для" "all" || { pause; return; }
local name="$SELECTED_SERVICE"
[ "$name" = "__ALL__" ] && { warn "Выберите конкретный сервис"; pause; return; }
load_service "$name" || { pause; return; }
if [ -z "$SVC_DOMAIN" ]; then
err "Для получения SSL сервису должен быть назначен домен"
pause
return
fi
if [ -z "$CERTBOT_EMAIL" ]; then
prompt "Введите Email для регистрации в Certbot: "
CERTBOT_EMAIL="$REPLY"
sed -i "/CERTBOT_EMAIL=/d" "$CONF_FILE"
echo "CERTBOT_EMAIL=\"$CERTBOT_EMAIL\"" >> "$CONF_FILE"
fi
msg "Проверяю наличие Certbot на VPS..."
ssh_cmd "command -v certbot >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y -qq certbot python3-certbot-nginx || yum install -y certbot python3-certbot-nginx)" || {
warn "Не удалось автоматически установить Certbot на VPS"
}
msg "Запрашиваю сертификат для $SVC_DOMAIN (certonly)..."
if ssh_cmd "certbot certonly --nginx -d $SVC_DOMAIN --non-interactive --agree-tos -m $CERTBOT_EMAIL"; then
msg "SSL сертификат успешно получен!"
# Обновляем конфиг сервиса
sed -i "s/SVC_SSL=.*/SVC_SSL=\"yes\"/" "$SERVICES_DIR/$name.conf"
sed -i "s/SVC_EXT_PORT=.*/SVC_EXT_PORT=\"443\"/" "$SERVICES_DIR/$name.conf"
# Пересборка основного конфига (теперь он подхватит сертификат)
rebuild_vhost_config "$SVC_DOMAIN" "443" "$SVC_VPS"
# Проверка автопродления (добавляем таймер/крон если нет)
ssh_cmd "systemctl is-active certbot.timer >/dev/null 2>&1 || (crontab -l 2>/dev/null | grep -q certbot || (crontab -l 2>/dev/null; echo \"0 0,12 * * * certbot renew -q\") | crontab -)"
else
err "Certbot не смог выпустить сертификат"
warn "Убедитесь, что домен $SVC_DOMAIN указывает на этот VPS и порт 80 открыт"
fi
pause
}
# ══════════════════════════════════════════════════════════════════════
# ОБНОВЛЕНИЕ СКРИПТА
# ══════════════════════════════════════════════════════════════════════
do_self_update() {
clear_screen
draw_box "Обновление rProxy"
printf "\n"
msg "Проверяю наличие обновлений..."
local url="https://raw.githubusercontent.com/l-ptrol/rProxy/main/rproxy"
local tmp_file="/tmp/rproxy_update"
if curl -sSL "$url" -o "$tmp_file"; then
if [ -s "$tmp_file" ]; then
mv "$tmp_file" "/opt/bin/rproxy"
chmod +x "/opt/bin/rproxy"
msg "Обновление успешно завершено!"
pause
exec /opt/bin/rproxy # Перезапуск новой версии
else
err "Файл обновления не пуст или не найден"
fi
else
err "Не удалось загрузить обновление"
fi
pause
}
# ══════════════════════════════════════════════════════════════════════
# ПРОВЕРКА СОСТОЯНИЯ VPS
# ══════════════════════════════════════════════════════════════════════
do_health_check() {
clear_screen
draw_box "Состояние VPS и SSL"
printf "\n"
for f in "$VPS_DIR"/*.conf; do
[ -f "$f" ] || continue
local vps_id=$(basename "$f" .conf)
load_vps "$vps_id"
header "Сервер: $vps_id ($VPS_HOST)"
msg "Проверяю Nginx..."
if ssh_cmd "systemctl is-active nginx >/dev/null 2>&1"; then
printf " Nginx: ${GREEN}${BOLD}Запущен${NC}\n"
else
printf " Nginx: ${RED}${BOLD}Остановлен${NC}\n"
fi
msg "Проверяю автопродление SSL (Certbot)..."
local timer=$(ssh_cmd "systemctl is-active certbot.timer 2>/dev/null | tr -d '\r'")
if [ "$timer" = "active" ]; then
printf " SSL Timer: ${GREEN}${BOLD}Активен (Systemd)${NC}\n"
local next_run=$(ssh_cmd "systemctl list-timers certbot.timer --no-legend 2>/dev/null | awk '{print \$1, \$2}'" | head -n 1)
[ -n "$next_run" ] && printf " Следующее: $next_run\n"
else
if ssh_cmd "crontab -l 2>/dev/null | grep -q certbot"; then
printf " SSL Renewal: ${GREEN}${BOLD}Настроено (Cron)${NC}\n"
else
printf " SSL Renewal: ${RED}${BOLD}Не обнаружено${NC}\n"
fi
fi
msg "Действующие сертификаты:"
local certs=$(ssh_cmd "certbot certificates 2>/dev/null | grep -E 'Expiry Date:|Domains:'")
if [ -n "$certs" ]; then
echo "$certs" | while read -r line; do
printf " %s\n" "$line"
done
else
printf " ${DIM}Сертификаты не найдены${NC}\n"
fi
done
printf "\n"
msg "Утилиты на роутере:"
[ -x "/opt/bin/ssh" ] && printf " SSH: ${GREEN}Entware OK${NC}\n" || printf " SSH: ${RED}Missing${NC}\n"
[ -x "/opt/bin/autossh" ] && printf " AutoSSH: ${GREEN}OK${NC}\n" || printf " AutoSSH: ${RED}Missing${NC}\n"
pause
}
# ══════════════════════════════════════════════════════════════════════
# УДАЛИТЬ СЕРВИС (интерактивно)
# ══════════════════════════════════════════════════════════════════════
do_remove_interactive() {
clear_screen
draw_box "Удалить сервис"
printf "\n"
select_service "Выберите сервис для удаления" "all" || { pause; return; }
local name="$SELECTED_SERVICE"
[ "$name" = "__ALL__" ] && { warn "Для удаления выберите конкретный сервис"; pause; return; }
prompt "Удалить сервис '$name'? (д/н) [н]: "
case "${REPLY:-н}" in
д|Д|y|Y|да|yes) ;;
*) msg "Отменено"; pause; return ;;
esac
load_service "$name" || { pause; return; }
is_running "$name" && do_stop_service "$name"
msg "Удаляю конфигурации на VPS..."
local remote_loc_dir
local is_domain=0
if [ -n "$SVC_DOMAIN" ]; then
remote_loc_dir="$REMOTE_LOCATIONS_DIR/$SVC_DOMAIN"
is_domain=1
else
remote_loc_dir="$REMOTE_LOCATIONS_DIR/_ip_$name"
fi
# Удаляем сниппет и htpasswd
ssh_cmd "rm -f $remote_loc_dir/$name.conf /etc/nginx/rproxy_$name.htpasswd"
if [ "$is_domain" -eq 1 ]; then
# Проверяем, остались ли другие сервисы для этого домена
local remaining=$(ssh_cmd "ls $remote_loc_dir/*.conf 2>/dev/null | wc -l")
if [ "$remaining" -gt 0 ]; then
msg "Пересборка конфига домена (осталось $remaining сервисов)..."
rebuild_vhost_config "$SVC_DOMAIN" "$SVC_EXT_PORT" "$SVC_VPS"
else
msg "Последний сервис для домена. Удаляю основной конфиг..."
ssh_cmd "rm -f $REMOTE_NGINX_DIR/rproxy_dom_$SVC_DOMAIN.conf && rmdir $remote_loc_dir"
fi
else
ssh_cmd "rm -f $REMOTE_NGINX_DIR/rproxy_svc_$name.conf && rm -rf $remote_loc_dir"
fi
ssh_cmd "nginx -t && systemctl reload nginx" >/dev/null 2>&1
rm -f "$SERVICES_DIR/$name.conf"
rm -f "$(get_pid_file "$name")"
msg "Сервис '$name' удалён"
pause
}
# ══════════════════════════════════════════════════════════════════════
# РЕДАКТИРОВАТЬ СЕРВИС (интерактивно)
# ══════════════════════════════════════════════════════════════════════
do_edit_interactive() {
clear_screen
draw_box "Редактировать сервис"
printf "\n"
select_service "Выберите сервис для редактирования" "all" || { pause; return; }
local name="$SELECTED_SERVICE"
[ "$name" = "__ALL__" ] && { warn "Для редактирования выберите конкретный сервис"; pause; return; }
load_service "$name" || { pause; return; }
header "Редактирование: $name"
printf " ${DIM}Текущие параметры:${NC}\n"
printf " Цель: ${BOLD}$SVC_TARGET_HOST:$SVC_TARGET_PORT${NC}\n"
printf " Туннель: порт ${BOLD}$SVC_TUNNEL_PORT${NC}\n"
[ -n "$SVC_DOMAIN" ] && printf " Домен: ${BOLD}$SVC_DOMAIN${NC}\n"
printf " Внешний порт: ${BOLD}$SVC_EXT_PORT${NC}\n"
draw_separator
prompt "Новый адрес (IP:порт) [$SVC_TARGET_HOST:$SVC_TARGET_PORT]: "
local new_target="${REPLY:-$SVC_TARGET_HOST:$SVC_TARGET_PORT}"
local new_host="${new_target%:*}"
local new_port="${new_target#*:}"
if [ "$new_host" = "$new_port" ]; then
new_port="$new_host"
new_host="127.0.0.1"
fi
prompt "Внешний порт [$SVC_EXT_PORT]: "
local new_ext_port="${REPLY:-$SVC_EXT_PORT}"
prompt "Путь на сервере [$SVC_PATH]: "
local new_path="${REPLY:-$SVC_PATH}"
[ "${new_path#/}" = "$new_path" ] && new_path="/$new_path"
# Если ничего не изменилось
if [ "$new_host" = "$SVC_TARGET_HOST" ] && [ "$new_port" = "$SVC_TARGET_PORT" ] && \
[ "$new_ext_port" = "$SVC_EXT_PORT" ] && [ "$new_path" = "$SVC_PATH" ]; then
msg "Параметры не изменились."
pause
return
fi
printf "\n"
draw_separator
printf " ${DIM}Параметр Было Стало${NC}\n"
printf " Цель: %-22s %s\n" "$SVC_TARGET_HOST:$SVC_TARGET_PORT" "$new_host:$new_port"
printf " Порт: %-22s %s\n" "$SVC_EXT_PORT" "$new_ext_port"
printf " Путь: %-22s %s\n" "$SVC_PATH" "$new_path"
draw_separator
prompt "Применить изменения? (д/н) [д]: "
case "${REPLY:-д}" in
д|Д|y|Y|да|yes) ;;
*) msg "Отменено"; pause; return ;;
esac
printf "\n"
msg "Применяю изменения и обновляю VPS..."
# Остановить туннель если запущен
local was_running=0
if is_running "$name"; then
was_running=1
do_stop_service "$name"
fi
# Обновить локальный конфиг сервиса
sed -i "s|SVC_TARGET_HOST=\".*\"|SVC_TARGET_HOST=\"$new_host\"|" "$SERVICES_DIR/$name.conf"
sed -i "s|SVC_TARGET_PORT=\".*\"|SVC_TARGET_PORT=\"$new_port\"|" "$SERVICES_DIR/$name.conf"
sed -i "s|SVC_EXT_PORT=\".*\"|SVC_EXT_PORT=\"$new_ext_port\"|" "$SERVICES_DIR/$name.conf"
if grep -q "SVC_PATH=" "$SERVICES_DIR/$name.conf"; then
sed -i "s|SVC_PATH=\".*\"|SVC_PATH=\"$new_path\"|" "$SERVICES_DIR/$name.conf"
else
echo "SVC_PATH=\"$new_path\"" >> "$SERVICES_DIR/$name.conf"
fi
# Перезагружаем переменные для деплоя
load_service "$name"
# Перегенерация и деплой конфига Nginx на VPS
local tmp="/tmp/rproxy_edit_$name.conf"
generate_nginx_conf "$name" "$SVC_TARGET_HOST" "$SVC_TARGET_PORT" "$SVC_TUNNEL_PORT" "$SVC_DOMAIN" "$SVC_EXT_PORT" "$SVC_NDM_AUTH" "$SVC_PATH" "$tmp"
local remote_loc_dir
if [ -n "$SVC_DOMAIN" ]; then
remote_loc_dir="$REMOTE_LOCATIONS_DIR/$SVC_DOMAIN"
else
remote_loc_dir="$REMOTE_LOCATIONS_DIR/_ip_$name"
fi
msg "Обновление конфигурации на VPS..."
ssh_cmd "mkdir -p $remote_loc_dir"
# Удаляем старый файл если он был в корне sites-enabled (миграция)
ssh_cmd "rm -f $REMOTE_NGINX_DIR/rproxy_$name.conf"
scp_cmd "$tmp" "$VPS_USER@$VPS_HOST:$remote_loc_dir/$name.conf"
rm -f "$tmp"
# Теперь эта функция генерирует ТОЛЬКО блоки location
# Основной конфиг сервера будет собираться отдельно
cat > "$target_file" << NGINXEOF
# Редирект с пути без слеша на путь со слешем (чтобы браузер не терял контекст)
if (\$request_uri = $path) {
return 301 \$scheme://\$host$path/;
}
location $location_path {
$auth_config
proxy_pass http://127.0.0.1:$tunnel_port/;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host "$stealth_host";
proxy_set_header Origin "http://$stealth_host";
proxy_set_header Referer "http://$stealth_host";
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header X-Forwarded-Host \$host;
proxy_set_header X-Forwarded-Server \$host;
proxy_set_header X-Forwarded-Path "$path";
proxy_cookie_domain "$t_host" "\$host";
proxy_cookie_path / "$path";
# Редиректы: если приложение шлет Location: /login, Nginx заменит на Location: $path/login
proxy_redirect / "$path/";
# Экспериментальная замена ссылок в HTML/JS для работы в подпутях (v1.8.7)
if (\$path != "/") {
# Убираем сжатие от бэкенда, чтобы Nginx мог видеть текст
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter_types text/html text/css application/javascript application/json;
# Заменяем ссылки начинающиеся с / на /путь/
sub_filter '="/' '="$path/';
sub_filter '=\"/' '=\"$path/';
# Попытка поймать JS редиректы и AJAX
sub_filter 'location.href = \"/' 'location.href = \"$path/';
sub_filter 'url: \"/' 'url: \"$path/';
fi
}
NGINXEOF
<EFBFBD>══════
# ВЫБОР СЕРВИСА (общий)
# ══════════════════════════════════════════════════════════════════════
select_service() {
local title="$1"
local filter="$2" # all, running, stopped
local services=""
local idx=0
for f in "$SERVICES_DIR"/*.conf; do
[ -f "$f" ] || continue
. "$f"
local running=0
is_running "$SVC_NAME" && running=1
# Фильтр
case "$filter" in
running) [ "$running" -eq 0 ] && continue ;;
stopped) [ "$running" -eq 1 ] && continue ;;
esac
idx=$((idx + 1))
local st="${RED}${NC}"
[ "$running" -eq 1 ] && st="${GREEN}${NC}"
printf " ${BOLD}%d)${NC} %b %-14s %s:%s" "$idx" "$st" "$SVC_NAME" "$SVC_TARGET_HOST" "$SVC_TARGET_PORT"
[ -n "$SVC_DOMAIN" ] && printf " ${DIM}(%s)${NC}" "$SVC_DOMAIN"
printf "\n"
[ -z "$services" ] && services="$SVC_NAME" || services="$services $SVC_NAME"
done
if [ "$idx" -eq 0 ]; then
printf " ${YELLOW}Нет подходящих сервисов.${NC}\n"
SELECTED_SERVICE=""
return 1
fi
draw_separator
printf " ${BOLD}903)${NC} Все сервисы\n"
printf " ${BOLD}0)${NC} Назад\n"
prompt "Выберите сервис: "
[ "$REPLY" = "0" ] && { SELECTED_SERVICE=""; return 1; }
if [ "$REPLY" = "903" ]; then
SELECTED_SERVICE="__ALL__"
return 0
fi
# Извлечение имени сервиса по индексу (1-based) с валидацией
if echo "$REPLY" | grep -qE '^[0-9]+$'; then
SELECTED_SERVICE=$(echo "$services" | cut -d' ' -f"$REPLY")
else
SELECTED_SERVICE=""
fi
[ -z "$SELECTED_SERVICE" ] && { warn "Неверный выбор"; return 1; }
return 0
}
# ══════════════════════════════════════════════════════════════════════
# ЗАПУСК / ОСТАНОВ / ПЕРЕЗАПУСК (интерактивно)
# ══════════════════════════════════════════════════════════════════════
do_start_interactive() {
clear_screen
draw_box "Запустить туннель"
printf "\n"
select_service "Запустить" "stopped" || { pause; return; }
if [ "$SELECTED_SERVICE" = "__ALL__" ]; then
do_start_all
else
do_start_service "$SELECTED_SERVICE"
fi
pause
}
do_stop_interactive() {
clear_screen
draw_box "Остановить туннель"
printf "\n"
select_service "Остановить" "running" || { pause; return; }
if [ "$SELECTED_SERVICE" = "__ALL__" ]; then
do_stop_all
else
do_stop_service "$SELECTED_SERVICE"
fi
pause
}
do_restart_interactive() {
clear_screen
draw_box "Перезапустить туннель"
printf "\n"
select_service "Перезапустить" "all" || { pause; return; }
if [ "$SELECTED_SERVICE" = "__ALL__" ]; then
for f in "$SERVICES_DIR"/*.conf; do
[ -f "$f" ] || continue
. "$f"
do_stop_service "$SVC_NAME"
sleep 1
do_start_service "$SVC_NAME"
done
else
load_service "$SELECTED_SERVICE"
do_stop_service "$SELECTED_SERVICE"
sleep 1
do_start_service "$SELECTED_SERVICE"
fi
pause
}
# ══════════════════════════════════════════════════════════════════════
# ЗАПУСК / ОСТАНОВ (одного сервиса)
# ══════════════════════════════════════════════════════════════════════
do_start_service() {
local name="$1"
load_service "$name" || return
if is_running "$name"; then
warn "Сервис '$name' уже запущен"
return
fi
msg "Запускаю туннель '$name' ($SVC_TARGET_HOST:$SVC_TARGET_PORT → VPS:$SVC_TUNNEL_PORT)..."
local ssh_exec="/opt/bin/ssh"
[ ! -f "$ssh_exec" ] && ssh_exec="ssh"
export AUTOSSH_PATH="$ssh_exec"
mkdir -p "$PID_DIR"
local pid_file
pid_file=$(get_pid_file "$name")
msg "Синхронизация с VPS (очистка портов)..."
ssh_cmd "fuser -k $SVC_TUNNEL_PORT/tcp >/dev/null 2>&1 || true"
local ssh_opts="-o StrictHostKeyChecking=no -o ServerAliveInterval=10 -o ServerAliveCountMax=3 -o ConnectTimeout=10 -o ExitOnForwardFailure=yes"
local tunnel_args="-R 0.0.0.0:$SVC_TUNNEL_PORT:$SVC_TARGET_HOST:$SVC_TARGET_PORT"
if [ "$VPS_AUTH" = "password" ]; then
AUTOSSH_GATETIME=0 sshpass -p "$VPS_PASS" autossh -M 0 -f -N \
$ssh_opts \
-p "$VPS_PORT" \
$tunnel_args \
"$VPS_USER@$VPS_HOST" &
else
AUTOSSH_GATETIME=0 autossh -M 0 -f -N \
$ssh_opts \
-i "$SSH_KEY" \
-p "$VPS_PORT" \
$tunnel_args \
"$VPS_USER@$VPS_HOST" &
fi
local bg_pid=$!
sleep 2
local real_pid
real_pid=$(pgrep -f "autossh.*$SVC_TUNNEL_PORT:$SVC_TARGET_HOST:$SVC_TARGET_PORT" 2>/dev/null | head -1)
[ -z "$real_pid" ] && real_pid=$(pgrep -f "ssh.*$SVC_TUNNEL_PORT:$SVC_TARGET_HOST:$SVC_TARGET_PORT" 2>/dev/null | head -1)
[ -z "$real_pid" ] && real_pid=$bg_pid
echo "$real_pid" > "$pid_file"
msg "Туннель '$name' запущен (PID: $real_pid)"
}
do_stop_service() {
local name="$1"
load_service "$name" || return # Загружаем VPS для правильной очистки
local pid_file
pid_file=$(get_pid_file "$name")
if [ ! -f "$pid_file" ]; then
warn "Сервис '$name' не запущен"
return
fi
local pid
pid=$(cat "$pid_file")
kill "$pid" 2>/dev/null
pkill -f "ssh.*:$SVC_TUNNEL_PORT:.*$VPS_HOST" 2>/dev/null
rm -f "$pid_file"
msg "Туннель '$name' остановлен"
}
_clear_svc_vars() {
unset SVC_NAME SVC_VPS SVC_TARGET_HOST SVC_TARGET_PORT SVC_TUNNEL_PORT
unset SVC_EXT_PORT SVC_DOMAIN SVC_PATH SVC_SSL SVC_NDM_AUTH SVC_HTPASSWD SVC_ENABLED
}
load_service() {
_clear_svc_vars
local f="$SERVICES_DIR/$1.conf"
[ -f "$f" ] || return 1
. "$f"
load_vps "$SVC_VPS"
}
generate_nginx_conf() {
local name="$1"
local t_host="$2"
local t_port="$3"
local tunnel_port="$4"
local domain="$5"
local ext_port="$6"
local use_ndm_auth="$7"
local path="$8"
local target_file="$9"
# Гарантируем, что путь начинается с /
[ "${path#/}" = "$path" ] && path="/$path"
# Регулируем путь для корректного маппинга в proxy_pass
# Если локация /myapp, то proxy_pass http://.../ перенаправит /myapp/foo -> /foo
local location_path="$path"
# Для корня оставляем как есть, для подпутей добавляем опциональный слеш
[ "$path" != "/" ] && location_path="$path/"
local stealth_host="$t_host"
[ "$t_port" != "80" ] && stealth_host="$t_host:$t_port"
local auth_config=""
if [ "$use_ndm_auth" = "yes" ]; then
auth_config="
auth_basic \"Restricted Access\";
auth_basic_user_file /etc/nginx/rproxy_$name.htpasswd;
"
fi
# Теперь эта функция генерирует ТОЛЬКО блок location
# Основной конфиг сервера будет собираться отдельно
cat > "$target_file" << NGINXEOF
location $location_path {
$auth_config
proxy_pass http://127.0.0.1:$tunnel_port/;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host "$stealth_host";
proxy_set_header Origin "http://$stealth_host";
proxy_set_header Referer "http://$stealth_host";
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header X-Forwarded-Host \$host;
proxy_set_header X-Forwarded-Server \$host;
proxy_cookie_domain "$t_host" "\$host";
proxy_cookie_path / "$path";
# Редиректы: если приложение шлет Location: /login, Nginx заменит на Location: $path/login
proxy_redirect / "$path";
# Экспериментальная замена ссылок в HTML/JS для работы в подпутях (v1.8.6)
if [ "$path" != "/" ]; then
# Убираем сжатие от бэкенда, чтобы Nginx мог видеть текст
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter_types text/html text/css application/javascript application/json;
# Заменяем ссылки начинающиеся с / на /путь/
sub_filter '="/' '="$path/';
sub_filter '=\"/' '=\"$path/';
fi
}
NGINXEOF
}
# Функция для сборки основного конфига Nginx на VPS
rebuild_vhost_config() {
local domain="$1"
local ext_port="$2"
local vps_id="$3"
load_vps "$vps_id"
msg "Пересборка конфигурации для домена $domain..."
# Создаем директорию для локаций
ssh_cmd "mkdir -p $REMOTE_LOCATIONS_DIR/$domain"
# Проверяем наличие SSL сертификата на VPS
local cert_file="/etc/letsencrypt/live/$domain/fullchain.pem"
local key_file="/etc/letsencrypt/live/$domain/privkey.pem"
local has_ssl=0
ssh_cmd "[ -f $cert_file ] && [ -f $key_file ]" && has_ssl=1
local tmp_vhost="/tmp/rproxy_vhost_$domain.conf"
if [ -n "$domain" ]; then
# Конфиг для домена
cat > "$tmp_vhost" << EOF
server {
EOF
if [ "$has_ssl" -eq 1 ]; then
echo " listen $ext_port ssl;" >> "$tmp_vhost"
cat >> "$tmp_vhost" << EOF
ssl_certificate $cert_file;
ssl_certificate_key $key_file;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
EOF
else
echo " listen $ext_port;" >> "$tmp_vhost"
fi
cat >> "$tmp_vhost" << EOF
server_name "$domain";
proxy_buffering off;
proxy_request_buffering off;
include $REMOTE_LOCATIONS_DIR/$domain/*.conf;
}
EOF
# Редирект с 80 на HTTPS (только если есть SSL и это не порт 80)
if [ "$has_ssl" -eq 1 ] && [ "$ext_port" != "80" ]; then
local redirect_url="https://\$host"
[ "$ext_port" != "443" ] && redirect_url="https://\$host:$ext_port"
cat >> "$tmp_vhost" << EOF
server {
listen 80;
server_name "$domain";
return 301 $redirect_url\$request_uri;
}
EOF
fi
scp_cmd "$tmp_vhost" "$VPS_USER@$VPS_HOST:$REMOTE_NGINX_DIR/rproxy_dom_$domain.conf"
else
return 1
fi
rm -f "$tmp_vhost"
ssh_cmd "nginx -t && systemctl reload nginx" >/dev/null 2>&1
}
do_start_all() {
for f in "$SERVICES_DIR"/*.conf; do
[ -f "$f" ] || continue
. "$f"
[ "$SVC_ENABLED" = "yes" ] && do_start_service "$SVC_NAME"
done
}
do_stop_all() {
for f in "$SERVICES_DIR"/*.conf; do
[ -f "$f" ] || continue
. "$f"
do_stop_service "$SVC_NAME"
done
}
# ══════════════════════════════════════════════════════════════════════
# НАСТРОЙКА VPS (Менеджер профилей)
# ══════════════════════════════════════════════════════════════════════
do_setup() {
while true; do
clear_screen
draw_box "Управление VPS серверами"
printf "\n"
local vps_list=""
local idx=0
for f in "$VPS_DIR"/*.conf; do
[ -f "$f" ] || continue
idx=$((idx + 1))
VPS_HOST="" # Сброс перед загрузкой
. "$f"
local v_info="${DIM}${VPS_USER}@${VPS_HOST}:${VPS_PORT}${NC}"
[ -z "$VPS_HOST" ] && v_info="${RED}Не настроен${NC}"
printf " ${BOLD}%d)${NC} %-14s %b\n" "$idx" "$(basename "$f" .conf)" "$v_info"
vps_list="$vps_list $(basename "$f" .conf)"
done
if [ "$idx" -eq 0 ]; then
printf " ${YELLOW}Нет настроенных VPS серверов.${NC}\n"
fi
draw_separator
printf " ${BOLD}901)${NC} Добавить новый VPS\n"
[ "$idx" -gt 0 ] && printf " ${BOLD}902)${NC} ❌ Удалить VPS\n"
[ "$idx" -gt 0 ] && printf " ${BOLD}903)${NC} 🧹 Очистить VPS от мусора (фантомные конфиги)\n"
printf " ${BOLD}904)${NC} 🔑 Пароль публикации по умолчанию\n"
printf " ${BOLD}0)${NC} Назад\n"
prompt "Выберите действие: "
case "$REPLY" in
0) return ;;
901) do_add_vps ;;
902) [ "$idx" -gt 0 ] && do_delete_vps_interactive "$vps_list" ;;
903) [ "$idx" -gt 0 ] && do_vps_cleanup_menu "$vps_list" ;;
904) do_change_auth_defaults ;;
[1-9]*)
local sel_vps=$(echo "$vps_list" | cut -d' ' -f"$REPLY")
[ -n "$sel_vps" ] && do_add_vps "$sel_vps"
;;
esac
done
}
do_delete_vps_interactive() {
local list="$1"
prompt "Введите номер VPS для удаления: "
local sel_vps=$(echo "$list" | cut -d' ' -f"$REPLY")
[ -z "$sel_vps" ] && { warn "Неверный выбор"; pause; return; }
prompt "Удалить профиль VPS '$sel_vps'? (д/н) [н]: "
case "$REPLY" in
д|Д|y|Y) rm -f "$VPS_DIR/$sel_vps.conf"; msg "Удалено" ;;
*) msg "Отменено" ;;
esac
pause
}
do_vps_cleanup_menu() {
local list="$1"
prompt "Введите номер VPS для очистки: "
local sel_vps=$(echo "$list" | cut -d' ' -f"$REPLY")
[ -z "$sel_vps" ] && { warn "Неверный выбор"; pause; return; }
do_vps_cleanup_interactive "$sel_vps"
}
do_vps_cleanup_interactive() {
local vps_id="$1"
load_vps "$vps_id" || return
clear_screen
draw_box "Очистка VPS: $vps_id ($VPS_HOST)"
printf "\n"
msg "Сканирую VPS на наличие конфигураций rProxy..."
local remote_files
remote_files=$(ssh_cmd "ls $REMOTE_NGINX_DIR/rproxy_*.conf /etc/nginx/rproxy_*.htpasswd 2>/dev/null | xargs -n1 basename 2>/dev/null")
if [ -z "$remote_files" ]; then
msg "На сервере не обнаружено файлов rProxy."
pause
return
fi
local to_delete=""
local local_found=""
header "Обнаруженные файлы на VPS:"
echo "$remote_files" | while read -r line; do
# Извлекаем имя сервиса из rproxy_NAME.conf или rproxy_NAME.htpasswd
local s_name=$(echo "$line" | sed -E 's/^rproxy_(.+)\.(conf|htpasswd)$/\1/')
if [ -f "$SERVICES_DIR/$s_name.conf" ]; then
printf " ${GREEN}${NC} %-30s (Активен локально)\n" "$line"
else
printf " ${RED}${NC} %-30s (ФАНТОМНЫЙ - нет на роутере)\n" "$line"
local_found="1"
fi
done
draw_separator
printf " ${BOLD}1)${NC} 🧹 Умная очистка (удалить только фантомные файлы)\n"
printf " ${BOLD}2)${NC} 🔥 Полная очистка (удалить ВСЕ конфиги rProxy на этом VPS)\n"
printf " ${BOLD}0)${NC} Отмена\n"
prompt "Выберите действие: "
case "$REPLY" in
1)
msg "Выполняю умную очистку..."
echo "$remote_files" | while read -r line; do
local s_name=$(echo "$line" | sed -E 's/^rproxy_(.+)\.(conf|htpasswd)$/\1/')
if [ ! -f "$SERVICES_DIR/$s_name.conf" ]; then
msg "Удаляю $line..."
ssh_cmd "rm -f $REMOTE_NGINX_DIR/$line /etc/nginx/$line" >/dev/null 2>&1
fi
done
ssh_cmd "nginx -t && systemctl reload nginx" >/dev/null 2>&1
msg "Готово!"
;;
2)
prompt "Это удалит ВСЕ публикации rProxy на этом VPS. Уверены? (y/n): "
[ "$REPLY" != "y" ] && { msg "Отменено"; pause; return; }
msg "Полная очистка..."
ssh_cmd "rm -f $REMOTE_NGINX_DIR/rproxy_*.conf /etc/nginx/rproxy_*.htpasswd && nginx -t && systemctl reload nginx" >/dev/null 2>&1
msg "Все конфигурации rProxy на VPS '$vps_id' удалены."
;;
*) msg "Отменено" ;;
esac
pause
}
do_change_auth_defaults() {
draw_box "Настройки публикации (Basic Auth)"
printf "\n"
printf " Эти данные будут предлагаться при защите новых сервисов.\n"
prompt "Имя пользователя [${DEFAULT_AUTH_USER:-admin}]: "
local user="${REPLY:-${DEFAULT_AUTH_USER:-admin}}"
prompt "Пароль (оставьте пустым чтобы не менять): "
local pass="$REPLY"
if [ -z "$pass" ] && [ -n "$DEFAULT_AUTH_PASS" ]; then
pass="$DEFAULT_AUTH_PASS"
fi
[ -z "$pass" ] && { warn "Пароль не может быть пустым"; pause; return; }
sed -i "/DEFAULT_AUTH_USER=/d" "$CONF_FILE"
sed -i "/DEFAULT_AUTH_PASS=/d" "$CONF_FILE"
printf "DEFAULT_AUTH_USER='%s'\n" "$user" >> "$CONF_FILE"
printf "DEFAULT_AUTH_PASS='%s'\n" "$pass" >> "$CONF_FILE"
msg "Данные успешно сохранены"
DEFAULT_AUTH_USER="$user"
DEFAULT_AUTH_PASS="$pass"
pause
}
do_add_vps() {
local vps_id="${1:-}"
local is_new=0
[ -z "$vps_id" ] && is_new=1
if [ "$is_new" -eq 1 ]; then
prompt "Название сервера (например, amsterdam): "
vps_id=$(echo "$REPLY" | tr -dc 'a-zA-Z0-9_-')
[ -z "$vps_id" ] && { warn "Недопустимое название"; pause; return; }
fi
local vps_file="$VPS_DIR/$vps_id.conf"
local v_host="" v_port="22" v_user="root"
if [ -f "$vps_file" ]; then
. "$vps_file"
v_host="$VPS_HOST"; v_port="$VPS_PORT"; v_user="$VPS_USER"
fi
header "Настройка VPS: $vps_id"
prompt "IP-адрес VPS [$v_host]: "
v_host="${REPLY:-$v_host}"
[ -z "$v_host" ] && { warn "IP обязателен"; pause; return; }
prompt "SSH порт [$v_port]: "
v_port="${REPLY:-$v_port}"
prompt "SSH пользователь [$v_user]: "
v_user="${REPLY:-$v_user}"
# Настройка ключей
ensure_ssh_key
msg "Настраиваю доступ по SSH-ключу для $v_host..."
# Копирование ключа (пароль запросит сама система)
local ssh_exec="/opt/bin/ssh"
[ ! -f "$ssh_exec" ] && ssh_exec="ssh"
local copy_id="/opt/bin/ssh-copy-id"
[ ! -f "$copy_id" ] && copy_id="ssh-copy-id"
printf " ${YELLOW}Сейчас будет запрошен пароль VPS для копирования ключа${NC}\n"
$copy_id -o StrictHostKeyChecking=no -i "$SSH_KEY.pub" -p "$v_port" "$v_user@$v_host" 2>/dev/null || {
cat "$SSH_KEY.pub" | $ssh_exec -o StrictHostKeyChecking=no -p "$v_port" "$v_user@$v_host" \
"mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys"
}
if ! $ssh_exec -o StrictHostKeyChecking=no -o ConnectTimeout=10 -i "$SSH_KEY" -p "$v_port" "$v_user@$v_host" "echo ok" >/dev/null 2>&1; then
err "Ошибка подключения по ключу. Убедитесь, что пароль введен верно."
pause; return
fi
# Сохраняем (БЕЗ ПАРОЛЯ)
cat > "$vps_file" <<EOF
VPS_HOST="$v_host"
VPS_PORT="$v_port"
VPS_USER="$v_user"
VPS_AUTH="key"
EOF
chmod 600 "$vps_file"
msg "Профиль '$vps_id' сохранён (авторизация по ключу)."
# Настройка Nginx на новом VPS
msg "Настройка окружения на VPS..."
VPS_HOST="$v_host" VPS_PORT="$v_port" VPS_USER="$v_user" VPS_AUTH="key"
ssh_cmd "
if ! command -v nginx >/dev/null 2>&1; then
apt-get update -qq && apt-get install -y -qq nginx psmisc || (yum update -y && yum install -y nginx psmisc)
else
apt-get update -qq && apt-get install -y -qq psmisc || yum install -y psmisc
fi
mkdir -p /etc/nginx/sites-enabled
grep -q 'sites-enabled' /etc/nginx/nginx.conf || sed -i '/http {/a\ include /etc/nginx/sites-enabled/*.conf;' /etc/nginx/nginx.conf
command -v certbot >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y -qq certbot python3-certbot-nginx || yum install -y certbot python3-certbot-nginx)
systemctl enable nginx && systemctl start nginx
# Оптимизация SSH на стороне сервера для туннелей
grep -q 'ClientAliveInterval' /etc/ssh/sshd_config || echo 'ClientAliveInterval 30' >> /etc/ssh/sshd_config
grep -q 'ClientAliveCountMax' /etc/ssh/sshd_config || echo 'ClientAliveCountMax 2' >> /etc/ssh/sshd_config
systemctl restart ssh
"
pause
}
ensure_ssh_key() {
if [ ! -f "$SSH_KEY" ]; then
msg "Генерирую SSH-ключ (ed25519)..."
local keygen="/opt/bin/ssh-keygen"
[ ! -f "$keygen" ] && keygen="ssh-keygen"
$keygen -t ed25519 -f "$SSH_KEY" -N "" -q
[ ! -f "$SSH_KEY.pub" ] && $keygen -y -f "$SSH_KEY" > "$SSH_KEY.pub"
chmod 600 "$SSH_KEY"
fi
}
# ══════════════════════════════════════════════════════════════════════
# ТОЧКА ВХОДА
# ══════════════════════════════════════════════════════════════════════
migrate_config 2>/dev/null
ensure_local_deps
# Поддержка прямых команд для init-скрипта и автоматизации
case "${1:-}" in
start) check_conf && do_start_all ;;
stop) check_conf && do_stop_all ;;
restart) check_conf && do_stop_all; sleep 1; do_start_all ;;
status)
check_conf || { err "VPS не настроен"; exit 1; }
for f in "$SERVICES_DIR"/*.conf; do
[ -f "$f" ] || continue
local name=$(basename "$f" .conf)
(
_clear_svc_vars
. "$f"
local status="${RED}OFFLINE${NC}"
is_running "$SVC_NAME" && status="${GREEN}ONLINE${NC}"
local type="[PORT]"
[ -n "$SVC_DOMAIN" ] && type="[DOM ]"
local addr="$SVC_EXT_PORT"
[ -n "$SVC_DOMAIN" ] && addr="$SVC_DOMAIN"
printf " %-15s %-7s %-20s %s\n" "$SVC_NAME" "$type" "$addr" "$status"
)
done
;;
-v|--version) echo "rProxy v$VERSION" ;;
"") main_menu ;;
*) main_menu ;;
esac