330 lines
9.7 KiB
Bash
330 lines
9.7 KiB
Bash
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
#################################
|
||
# TRAP ДЛЯ ОТЛАДКИ
|
||
#################################
|
||
trap 'echo -e "\033[1;31m[ERROR]\033[0m Ошибка в строке $LINENO"; exit 1' ERR
|
||
|
||
#################################
|
||
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
|
||
#################################
|
||
|
||
log() { echo -e "\033[1;32m[INFO]\033[0m $1"; }
|
||
die() { echo -e "\033[1;31m[ERROR]\033[0m $1"; exit 1; }
|
||
|
||
need_root() {
|
||
[[ $EUID -eq 0 ]] || die "Запускать только от root"
|
||
}
|
||
|
||
validate_url() {
|
||
[[ "$1" =~ ^https?://[^/]+:[0-9]+/.+ ]]
|
||
}
|
||
|
||
#################################
|
||
# ПРОВЕРКИ
|
||
#################################
|
||
|
||
need_root
|
||
|
||
# --- ASCII-баннер ---
|
||
echo "==================================================="
|
||
echo " ____ ____ _ ___ _ "
|
||
echo " / __ \___ ____ / __ \(_) (_)___ ______(_)___ ___ "
|
||
echo " / / / / _ \/ __ \/ /_/ / / / / __ \/ ___/ / __ \`__ \ "
|
||
echo " / /_/ / __/ / / / ____/ / / / /_/ / / / / / / / / /"
|
||
echo "/_____/\___/_/ /_/_/ /_/_/_/\__, /_/ /_/_/ /_/ /_/ "
|
||
echo " /____/ "
|
||
echo ""
|
||
echo " 3DP-MANAGER FOR 3X-UI "
|
||
echo "==================================================="
|
||
echo ""
|
||
# --------------------
|
||
|
||
#################################
|
||
# ВВОД ДАННЫХ
|
||
#################################
|
||
|
||
read -rp "URL панели 3x-ui (с портом и путем): " UI_URL
|
||
UI_URL="${UI_URL%/}"
|
||
validate_url "$UI_URL" || die "Некорректный URL 3x-ui"
|
||
|
||
read -rp "Логин 3x-ui: " UI_LOGIN
|
||
read -rsp "Пароль 3x-ui: " UI_PASSWORD
|
||
echo
|
||
[[ -z "$UI_LOGIN" || -z "$UI_PASSWORD" ]] && die "Логин/пароль обязательны"
|
||
|
||
# Проверка логина
|
||
LOGIN_RESPONSE=$(curl -s -X POST "$UI_URL/login" -H "Content-Type: application/json" -d "{\"username\":\"$UI_LOGIN\",\"password\":\"$UI_PASSWORD\"}")
|
||
|
||
# Проверка успешного логина по ключевому полю "success" или наличию куки
|
||
if ! echo "$LOGIN_RESPONSE" | grep -q '"success":true'; then
|
||
echo "Не удалось залогиниться в 3x-ui. Проверьте URL, логин и пароль."
|
||
exit 1
|
||
fi
|
||
|
||
echo "✔ Успешный логин в 3x-ui"
|
||
|
||
# Разбор UI_URL
|
||
UI_HOST=$(echo "$UI_URL" | awk -F[/:] '{print $4}') # домен или IP
|
||
UI_PROTO=$(echo "$UI_URL" | awk -F: '{print $1}') # http или https
|
||
|
||
if [[ "$UI_PROTO" == "https" ]]; then
|
||
log "HTTPS панель обнаружена, проверяем SSL сертификаты"
|
||
|
||
DEFAULT_CERT="/etc/letsencrypt/live/$UI_HOST/fullchain.pem"
|
||
DEFAULT_KEY="/etc/letsencrypt/live/$UI_HOST/privkey.pem"
|
||
|
||
if [[ -f "$DEFAULT_CERT" && -f "$DEFAULT_KEY" ]]; then
|
||
CERT_PATH="$DEFAULT_CERT"
|
||
KEY_PATH="$DEFAULT_KEY"
|
||
log "Найдены сертификаты Let's Encrypt для $UI_HOST"
|
||
else
|
||
log "⚠ Сертификаты Let's Encrypt для $UI_HOST не найдены"
|
||
read -rp "Введите полный путь к SSL сертификату (fullchain.pem): " CERT_PATH
|
||
read -rp "Введите полный путь к SSL ключу (privkey.pem): " KEY_PATH
|
||
|
||
[[ -f "$CERT_PATH" ]] || die "Файл сертификата не найден: $CERT_PATH"
|
||
[[ -f "$KEY_PATH" ]] || die "Файл ключа не найден: $KEY_PATH"
|
||
fi
|
||
fi
|
||
|
||
|
||
# Ввод интервала ротации
|
||
read -rp "Интервал генерации инбаундов в минутах (от 10, по умолчанию 30): " ROTATE_INTERVAL
|
||
ROTATE_INTERVAL="${ROTATE_INTERVAL:-30}" # по умолчанию 30
|
||
|
||
# Проверка, что это число и ≥10
|
||
if ! [[ "$ROTATE_INTERVAL" =~ ^[0-9]+$ ]] || [ "$ROTATE_INTERVAL" -lt 10 ]; then
|
||
echo "Неверное значение. Используется значение по умолчанию 30 минут."
|
||
ROTATE_INTERVAL=30
|
||
fi
|
||
|
||
echo "Интервал ротации установлен: $ROTATE_INTERVAL минут"
|
||
|
||
PROJECT_DIR="/opt/3dp-manager"
|
||
log "Используется директория проекта: $PROJECT_DIR"
|
||
|
||
mkdir -p "$PROJECT_DIR"
|
||
cd "$PROJECT_DIR"
|
||
|
||
#################################
|
||
# Белый лист
|
||
#################################
|
||
|
||
REPO_BASE="https://raw.githubusercontent.com/denpiligrim/3dp-manager/main"
|
||
|
||
curl -fsSL "$REPO_BASE/whitelist.txt" -o whitelist.txt
|
||
log "whitelist.txt скопирован"
|
||
|
||
#################################
|
||
# ГЕНЕРАЦИЯ ТОКЕНА
|
||
#################################
|
||
|
||
SUB_TOKEN="$(openssl rand -hex 24)"
|
||
log "Сгенерирован токен подписки"
|
||
|
||
#################################
|
||
# DOCKER
|
||
#################################
|
||
|
||
log "Проверка Docker"
|
||
|
||
if command -v docker >/dev/null 2>&1; then
|
||
log "Docker уже установлен"
|
||
else
|
||
log "Docker не найден, будет установлен"
|
||
apt update
|
||
apt install -y docker.io || die "Ошибка установки docker.io"
|
||
systemctl enable docker
|
||
systemctl restart docker
|
||
fi
|
||
|
||
# Проверка docker compose v2
|
||
if docker compose version >/dev/null 2>&1; then
|
||
log "docker compose v2 доступен"
|
||
else
|
||
log "Устанавливаем docker-compose-v2"
|
||
# решаем конфликт с docker-compose-plugin
|
||
apt install -y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||
docker-compose-v2 || warn "docker-compose-v2 не установлен, возможно уже есть плагин"
|
||
fi
|
||
|
||
#################################
|
||
# СТРУКТУРА
|
||
#################################
|
||
|
||
mkdir -p app app/builders subscriptions
|
||
|
||
# Генерация случайного свободного порта для подписки/Nginx
|
||
get_random_port() {
|
||
while :; do
|
||
PORT=$((RANDOM % 50000 + 10000)) # диапазон 10000-60000
|
||
if ! ss -ltn | awk '{print $4}' | grep -q ":$PORT\$"; then
|
||
echo "$PORT"
|
||
return
|
||
fi
|
||
done
|
||
}
|
||
NGINX_PORT=$(get_random_port)
|
||
|
||
# Генерация URL подписки
|
||
SUB_URL="$UI_PROTO://$UI_HOST:$NGINX_PORT/bus/$SUB_TOKEN"
|
||
|
||
#################################
|
||
# ENV
|
||
#################################
|
||
|
||
cat > .env <<EOF
|
||
SUB_TOKEN=$SUB_TOKEN
|
||
UI_URL=$UI_URL
|
||
UI_LOGIN=$UI_LOGIN
|
||
UI_PASSWORD=$UI_PASSWORD
|
||
NGINX_PORT=$NGINX_PORT
|
||
UI_HOST=$UI_HOST
|
||
UI_PROTO=$UI_PROTO
|
||
ROTATE_INTERVAL=$ROTATE_INTERVAL
|
||
SUB_URL=$SUB_URL
|
||
EOF
|
||
|
||
#################################
|
||
# Dockerfile
|
||
#################################
|
||
|
||
curl -fsSL "$REPO_BASE/app/Dockerfile" -o app/Dockerfile
|
||
|
||
#################################
|
||
# package.json
|
||
#################################
|
||
|
||
curl -fsSL "$REPO_BASE/app/package.json" -o app/package.json
|
||
|
||
#################################
|
||
# JS Files
|
||
#################################
|
||
|
||
curl -fsSL "$REPO_BASE/app/index.js" -o app/index.js
|
||
curl -fsSL "$REPO_BASE/app/builders/buildVlessRealityTcp.js" -o app/builders/buildVlessRealityTcp.js
|
||
curl -fsSL "$REPO_BASE/app/builders/buildVlessRealityXhttp.js" -o app/builders/buildVlessRealityXhttp.js
|
||
curl -fsSL "$REPO_BASE/app/builders/buildTrojanRealityTcp.js" -o app/builders/buildTrojanRealityTcp.js
|
||
curl -fsSL "$REPO_BASE/app/builders/buildShadowsocksTcp.js" -o app/builders/buildShadowsocksTcp.js
|
||
curl -fsSL "$REPO_BASE/app/builders/buildVmessTcp.js" -o app/builders/buildVmessTcp.js
|
||
curl -fsSL "$REPO_BASE/app/builders/buildVlessRealityGrpc.js" -o app/builders/buildVlessRealityGrpc.js
|
||
curl -fsSL "$REPO_BASE/app/builders/buildVlessWs.js" -o app/builders/buildVlessWs.js
|
||
curl -fsSL "$REPO_BASE/app/builders/buildInboundLink.js" -o app/builders/buildInboundLink.js
|
||
|
||
#################################
|
||
# NGINX & DOCKER COMPOSE
|
||
#################################
|
||
|
||
# Генерация nginx.conf
|
||
if [[ "$UI_PROTO" == "https" ]]; then
|
||
cat > docker-compose.yml <<EOF
|
||
services:
|
||
node:
|
||
build: ./app
|
||
env_file: .env
|
||
container_name: node
|
||
volumes:
|
||
- ./subscriptions:/subscriptions
|
||
- ./whitelist.txt:/app/whitelist.txt
|
||
restart: unless-stopped
|
||
|
||
nginx:
|
||
image: nginx:alpine
|
||
depends_on: [node]
|
||
ports:
|
||
- "$NGINX_PORT:$NGINX_PORT"
|
||
container_name: nginx
|
||
volumes:
|
||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||
- ./subscriptions:/subscriptions
|
||
- $CERT_PATH:$CERT_PATH:ro
|
||
- $KEY_PATH:$KEY_PATH:ro
|
||
EOF
|
||
|
||
cat > nginx.conf <<EOF
|
||
events {}
|
||
http {
|
||
server {
|
||
listen $NGINX_PORT ssl;
|
||
server_name $UI_HOST;
|
||
|
||
ssl_certificate $CERT_PATH;
|
||
ssl_certificate_key $KEY_PATH;
|
||
|
||
location = /bus/$SUB_TOKEN {
|
||
alias /subscriptions/list.txt;
|
||
default_type text/plain;
|
||
}
|
||
|
||
location / {
|
||
return 404;
|
||
}
|
||
}
|
||
}
|
||
EOF
|
||
else
|
||
cat > docker-compose.yml <<EOF
|
||
services:
|
||
node:
|
||
build: ./app
|
||
env_file: .env
|
||
container_name: node
|
||
volumes:
|
||
- ./subscriptions:/subscriptions
|
||
- ./whitelist.txt:/app/whitelist.txt
|
||
restart: unless-stopped
|
||
|
||
nginx:
|
||
image: nginx:alpine
|
||
depends_on: [node]
|
||
ports:
|
||
- "$NGINX_PORT:$NGINX_PORT"
|
||
container_name: nginx
|
||
volumes:
|
||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||
- ./subscriptions:/subscriptions
|
||
EOF
|
||
|
||
cat > nginx.conf <<EOF
|
||
events {}
|
||
http {
|
||
server {
|
||
listen $NGINX_PORT;
|
||
server_name $UI_HOST;
|
||
|
||
location = /bus/$SUB_TOKEN {
|
||
alias /subscriptions/list.txt;
|
||
default_type text/plain;
|
||
}
|
||
|
||
location / {
|
||
return 404;
|
||
}
|
||
}
|
||
}
|
||
EOF
|
||
fi
|
||
|
||
#################################
|
||
# ЗАПУСК
|
||
#################################
|
||
|
||
log "Сборка контейнеров"
|
||
docker compose build
|
||
|
||
log "Запуск контейнеров"
|
||
docker compose up -d
|
||
|
||
docker compose ps | grep node >/dev/null || die "Backend не запущен"
|
||
docker compose ps | grep nginx >/dev/null || die "Nginx не запущен"
|
||
|
||
#################################
|
||
# РЕЗУЛЬТАТ
|
||
#################################
|
||
|
||
log "✔ Установка завершена"
|
||
echo
|
||
echo "URL подписки:"
|
||
echo "$SUB_URL" |