#!/usr/bin/env bash set -euo pipefail ################################# # КОНФИГУРАЦИЯ И ПЕРЕМЕННЫЕ ################################# PROJECT_DIR="/opt/3dp-manager" DOCKER_USER="denpiligrim" DOCKER_TAG="dp-new-release" IMAGE_SERVER="ghcr.io/${DOCKER_USER}/3dp-manager-server:${DOCKER_TAG}" IMAGE_CLIENT="ghcr.io/${DOCKER_USER}/3dp-manager-client:${DOCKER_TAG}" # Цвета для вывода RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' log() { echo -e "${GREEN}[INFO]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } die() { error "$1"; } resolve_compose_cmd() { if docker compose version >/dev/null 2>&1; then COMPOSE_CMD=("docker" "compose") return 0 fi if command -v docker-compose >/dev/null 2>&1; then COMPOSE_CMD=("docker-compose") return 0 fi warn "Не найден Docker Compose (ни v2 plugin, ни v1 binary). Пытаемся установить..." apt-get update apt-get install -y docker-compose-plugin || true if docker compose version >/dev/null 2>&1; then COMPOSE_CMD=("docker" "compose") return 0 fi apt-get install -y docker-compose || true if command -v docker-compose >/dev/null 2>&1; then COMPOSE_CMD=("docker-compose") return 0 fi die "Не удалось установить Docker Compose. Установите docker compose plugin (v2) или docker-compose (v1)." } check_containers_running() { log "Проверка статуса контейнеров..." local timeout=${1:-60} local elapsed=0 local failed=0 while [ $elapsed -lt $timeout ]; do failed=0 # Получаем статус всех контейнеров текущего compose проекта # Формат: NAME\tSTATUS (например: "3dp-postgres\tUp 2 days" или "3dp-postgres\tError") while IFS=$'\t' read -r container_name status; do if [ -n "$container_name" ] && [ -n "$status" ]; then # Проверяем, что статус содержит Up/running/healthy/restarting # Up, Up 2 days, Up Less than a second, (healthy), running, restarting if ! echo "$status" | grep -qiE "^up|running|healthy|restarting"; then failed=1 warn "Контейнер $container_name в статусе: $status" fi fi done < <("${COMPOSE_CMD[@]}" ps --format "table {{.Name}}\t{{.Status}}" --all 2>/dev/null | tail -n +2) if [ $failed -eq 0 ]; then log "Все контейнеры запущены успешно" return 0 fi sleep 2 elapsed=$((elapsed + 2)) done return 1 } get_random_port() { local MIN=${1:-3000} local MAX=${2:-6999} while :; do PORT=$(shuf -i "$MIN-$MAX" -n 1) if ! ss -ltun | awk '{print $4}' | grep -q ":$PORT\$"; then echo "$PORT" return fi done } ################################# # ASCII-баннер ################################# echo "===================================================" echo " ____ ____ _ ___ _ " echo " / __ \___ ____ / __ \(_) (_)___ ______(_)___ ___ " echo " / / / / _ \/ __ \/ /_/ / / / / __ \/ ___/ / __ \`__ \ " echo " / /_/ / __/ / / / ____/ / / / /_/ / / / / / / / / /" echo "/_____/\___/_/ /_/_/ /_/_/_/\__, /_/ /_/_/ /_/ /_/ " echo " /____/ " echo "" echo " 3DP-MANAGER FOR 3X-UI " echo "===================================================" echo "" ################################# # ПРОВЕРКИ И УСТАНОВКА ЗАВИСИМОСТЕЙ ################################# if [[ $EUID -ne 0 ]]; then error "Этот скрипт должен быть запущен от имени root" fi . /etc/os-release if [[ "$ID" != "ubuntu" && "$ID" != "debian" ]]; then die "Этот скрипт поддерживает только Ubuntu или Debian: $ID" fi if [ $(free -m | grep Mem: | awk '{print $2}') -lt 2000 ]; then if [ $(free -m | grep Swap: | awk '{print $2}') -eq 0 ]; then log "Мало RAM и нет Swap. Создаем swap-файл 2GB..." fallocate -l 2G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=2048 chmod 600 /swapfile mkswap /swapfile swapon /swapfile echo '/swapfile none swap sw 0 0' >> /etc/fstab log "Swap создан." fi fi log "Проверка зависимостей..." if ! command -v curl &> /dev/null; then apt-get update && apt-get install -y curl; fi if ! command -v jq &> /dev/null; then apt-get install -y jq; fi if ! command -v openssl &> /dev/null; then apt-get install -y openssl; fi if ! command -v tar &> /dev/null; then apt-get install -y tar; fi if ! command -v hostname &> /dev/null; then apt-get install -y net-tools || apt-get install -y hostname; fi # Установка Docker log "Проверка Docker" if command -v docker >/dev/null 2>&1; then log "Docker уже установлен" else log "Docker не найден, будет установлен из официального репозитория" # Add Docker's official GPG key: apt update apt install ca-certificates curl install -m 0755 -d /etc/apt/keyrings if [[ "$ID" == "ubuntu" ]]; then curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc || die "Ошибка добавления ключа Docker" else curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc || die "Ошибка добавления ключа Docker" fi chmod a+r /etc/apt/keyrings/docker.asc # Add the repository to Apt sources: CODENAME=${UBUNTU_CODENAME:-$VERSION_CODENAME} tee /etc/apt/sources.list.d/docker.sources </dev/null && \ openssl x509 -noout -pubkey -in "$LE_CERT" 2>/dev/null > /tmp/cert_pub && \ openssl pkey -pubout -in "$LE_KEY" 2>/dev/null > /tmp/key_pub && \ diff -q /tmp/cert_pub /tmp/key_pub >/dev/null 2>&1; then log "Найдены валидные сертификаты для $UI_HOST." USE_SSL=true CERT_PATH="$LE_CERT" KEY_PATH="$LE_KEY" NEED_NEW_CERT=false else warn "Найдены сертификаты для $UI_HOST, но они истекли или невалидны. Очистка..." # Удаляем невалидные файлы rm -rf "/etc/letsencrypt/live/$UI_HOST" rm -rf "/etc/letsencrypt/archive/$UI_HOST" rm -rf "/etc/letsencrypt/renewal/$UI_HOST.conf" fi fi if [ "$NEED_NEW_CERT" = true ]; then log "Получение Let's Encrypt сертификата для $UI_HOST..." read -e -p "Email для уведомлений Let's Encrypt: " LE_EMAIL LE_EMAIL=$(echo "$LE_EMAIL" | tr -cd 'a-zA-Z0-9.@_-') if command -v certbot &>/dev/null; then log "Certbot уже установлен." else log "Установка certbot..." apt update apt install -y certbot fi # КРИТИЧЕСКИЙ ШАГ: Принудительно удаляем старые сертификаты из базы Certbot certbot delete --cert-name "$UI_HOST" --non-interactive >/dev/null 2>&1 || true # Запрашиваем новый сертификат принудительно (--force-renewal и --cert-name) certbot certonly --standalone \ --agree-tos \ --non-interactive \ --force-renewal \ --cert-name "$UI_HOST" \ --email "$LE_EMAIL" \ -d "$UI_HOST" || warn "Не удалось получить сертификат Let's Encrypt" if [[ -f "$LE_CERT" && -f "$LE_KEY" ]]; then USE_SSL=true CERT_PATH="$LE_CERT" KEY_PATH="$LE_KEY" log "Let's Encrypt сертификат получен для $UI_HOST" else warn "Сертификат не получен. Переключение на HTTP." fi fi fi ;; 2) UI_HOST=$(hostname -I | awk '{print $1}') log "Генерация самоподписанного сертификата для $UI_HOST..." mkdir -p "/etc/letsencrypt/live/$UI_HOST" openssl req -x509 -nodes -days 365 \ -newkey rsa:2048 \ -keyout "/etc/letsencrypt/live/$UI_HOST/privkey.pem" \ -out "/etc/letsencrypt/live/$UI_HOST/fullchain.pem" \ -subj "/CN=$UI_HOST" \ -addext "subjectAltName=IP:$UI_HOST" 2>/dev/null if [[ -f "/etc/letsencrypt/live/$UI_HOST/fullchain.pem" && -f "/etc/letsencrypt/live/$UI_HOST/privkey.pem" ]]; then USE_SSL=true CERT_PATH="/etc/letsencrypt/live/$UI_HOST/fullchain.pem" KEY_PATH="/etc/letsencrypt/live/$UI_HOST/privkey.pem" log "Self-signed сертификат сгенерирован для $UI_HOST" warn "Браузер будет предупреждать — это нормально для тестов." else warn "Не удалось сгенерировать сертификат. Переключение на HTTP." fi ;; 3) read -rp "Путь к fullchain.pem: " user_cert read -rp "Путь к privkey.pem: " user_key if [[ -f "$user_cert" && -f "$user_key" ]]; then USE_SSL=true CERT_PATH="$user_cert" KEY_PATH="$user_key" UI_HOST=$(hostname -I | awk '{print $1}') else warn "Файлы не найдены. Переключение на HTTP." fi ;; *) UI_HOST=$(hostname -I | awk '{print $1}') log "Будет использоваться HTTP." ;; esac if [ -z "$UI_HOST" ]; then UI_HOST=$(hostname -I | awk '{print $1}') log "Используется IP сервера: $UI_HOST" fi ################################# # СБОР ДАННЫХ ################################ FINAL_PORT=$(get_random_port) # --- 4. Генерация паролей --- DB_PASS=$(openssl rand -base64 12) JWT_SECRET=$(openssl rand -base64 32) ADMIN_USER=$(openssl rand -base64 8) ADMIN_PASS=$(openssl rand -base64 12) # Определяем ALLOWED_ORIGINS из домена или IP if [[ -n "${UI_HOST:-}" ]]; then if [[ "$UI_HOST" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then ALLOWED_ORIGINS="http://${UI_HOST}" else ALLOWED_ORIGINS="https://${UI_HOST}" fi else ALLOWED_ORIGINS="" fi log "Сгенерированы секретные ключи для БД и JWT." ################################# # Hysteria 2 ################################# # Проверка установки Hysteria 2 через наличие systemd сервиса if ! systemctl cat hysteria-server.service &> /dev/null; then echo "Сервис Hysteria 2 не найден. Начинаем установку..." # Установка Hysteria 2 согласно документации bash <(curl -fsSL https://get.hy2.sh/) < /dev/null || true RANDOM_FREE_PORT=$(get_random_port 10000 20000) # Генерация надежных паролей GENERATED_PASSWORD=$(openssl rand -base64 16 | tr -dc 'A-Za-z0-9' | cut -c1-16) GENERATED_OBFS_PASSWORD=$(openssl rand -base64 16 | tr -dc 'A-Za-z0-9' | cut -c1-16) # Запрос данных у пользователя echo "=== Настройка Hysteria 2 ===" read -e -p "Введите email для уведомлений Let's Encrypt: " HYSTERIA_EMAIL HYSTERIA_EMAIL=$(echo "$HYSTERIA_EMAIL" | tr -cd 'a-zA-Z0-9.@_-') # Создание конфигурационного файла cat > /etc/hysteria/config.yaml < server/.env < client/nginx-client.conf < docker-compose.yml < client/nginx-client.conf < docker-compose.yml </dev/null | grep -q "Status: active"; then echo "Найден активный UFW. Настраиваю правила..." ufw allow 443/tcp ufw allow 443/udp ufw allow 8443/tcp ufw allow 8443/udp ufw allow 10000:60000/tcp ufw allow 10000:60000/udp fi echo "" echo "===================================================" if [[ "$USE_SSL" == "true" ]]; then echo -e "${GREEN}✔ Установка завершена! Доступно по адресу: https://${UI_HOST}:${FINAL_PORT}${NC}" else echo -e "${GREEN}✔ Установка завершена! Доступно по адресу: http://${UI_HOST}:${FINAL_PORT}${NC}" fi echo -e "${GREEN}Логин: ${ADMIN_USER}${NC}" echo -e "${GREEN}Пароль: ${ADMIN_PASS}${NC}" echo "" echo "Немедленно измените пароль в Настройках утилиты!" echo "==================================================="