mirror of
https://github.com/andrey271192/PCAtelegram_web.git
synced 2026-09-20 11:55:32 +00:00
feat: split out GoTelegram project
This commit is contained in:
740
lib/common.sh
Normal file
740
lib/common.sh
Normal file
@@ -0,0 +1,740 @@
|
||||
#!/bin/bash
|
||||
# goTelegram Pro v2.5.0 — common utilities
|
||||
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
|
||||
|
||||
# ── Version ───────────────────────────────────────────────────────────────────
|
||||
GOTELEGRAM_VERSION="2.5.0"
|
||||
GOTELEGRAM_NAME="goTelegram Pro"
|
||||
|
||||
# ── Пути ──────────────────────────────────────────────────────────────────────
|
||||
GOTELEGRAM_DIR="/opt/gotelegram"
|
||||
GOTELEGRAM_CONFIG="$GOTELEGRAM_DIR/config.json"
|
||||
TELEMT_CONFIG="/etc/telemt/config.toml"
|
||||
TELEMT_BIN="/usr/local/bin/telemt"
|
||||
TELEMT_SERVICE="telemt"
|
||||
NGINX_SITE_CONF="/etc/nginx/sites-available/gotelegram"
|
||||
NGINX_SITE_LINK="/etc/nginx/sites-enabled/gotelegram"
|
||||
WEBSITE_ROOT="/var/www/gotelegram-site"
|
||||
BACKUP_DIR="$GOTELEGRAM_DIR/backups"
|
||||
LOG_FILE="/var/log/gotelegram.log"
|
||||
BOT_DIR="/opt/gotelegram-bot"
|
||||
ADMIN_WEB_DIR="/opt/gotelegram-admin"
|
||||
ADMIN_WEB_SERVICE="gotelegram-admin"
|
||||
ADMIN_WEB_HOST="127.0.0.1"
|
||||
ADMIN_WEB_PORT="1984"
|
||||
|
||||
# ── V1 совместимость ─────────────────────────────────────────────────────────
|
||||
V1_CONTAINER_NAME="mtproto-proxy"
|
||||
V1_CONFIG_FILE="/opt/gotelegram-bot/proxy.json"
|
||||
V1_SERVICE_NAME="gotelegram-bot"
|
||||
|
||||
# ── Цвета ────────────────────────────────────────────────────────────────────
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
YELLOW='\033[1;33m'
|
||||
MAGENTA='\033[0;35m'
|
||||
BLUE='\033[0;34m'
|
||||
WHITE='\033[1;37m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
NC='\033[0m'
|
||||
|
||||
# ── Логирование ──────────────────────────────────────────────────────────────
|
||||
log_info() { echo -e " ${CYAN}ℹ${NC} $*" >&2; }
|
||||
log_success() { echo -e " ${GREEN}✓${NC} $*" >&2; }
|
||||
log_warning() { echo -e " ${YELLOW}⚠${NC} $*" >&2; }
|
||||
log_error() { echo -e " ${RED}✗${NC} $*" >&2; }
|
||||
log_step() { echo -e "\n${BOLD}${WHITE} $*${NC}" >&2; }
|
||||
log_dim() { echo -e " ${DIM}$*${NC}" >&2; }
|
||||
|
||||
log_to_file() {
|
||||
local ts; ts=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
echo "[$ts] $*" >> "$LOG_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
# ── Spinner ──────────────────────────────────────────────────────────────────
|
||||
_spin_pid=""
|
||||
spinner_start() {
|
||||
local default_msg
|
||||
default_msg=$(type t &>/dev/null && t wait || echo "Please wait...")
|
||||
local msg="${1:-$default_msg}"
|
||||
(
|
||||
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
|
||||
local i=0
|
||||
while true; do
|
||||
printf "\r ${CYAN}${frames[$i]}${NC} ${msg}" >&2
|
||||
i=$(( (i+1) % ${#frames[@]} ))
|
||||
sleep 0.1
|
||||
done
|
||||
) &
|
||||
_spin_pid=$!
|
||||
}
|
||||
|
||||
spinner_stop() {
|
||||
[ -n "$_spin_pid" ] && kill "$_spin_pid" 2>/dev/null && wait "$_spin_pid" 2>/dev/null
|
||||
_spin_pid=""
|
||||
printf "\r\033[K" >&2
|
||||
}
|
||||
|
||||
# ── Прогресс-бар ─────────────────────────────────────────────────────────────
|
||||
progress_bar() {
|
||||
local current="$1" total="$2" label="${3:-}"
|
||||
local pct=$(( current * 100 / total ))
|
||||
local filled=$(( pct / 2 ))
|
||||
local empty=$(( 50 - filled ))
|
||||
local bar=""
|
||||
for ((i=0; i<filled; i++)); do bar+="█"; done
|
||||
for ((i=0; i<empty; i++)); do bar+="░"; done
|
||||
printf "\r ${GREEN}[${bar}]${NC} ${pct}%% ${label}" >&2
|
||||
[ "$current" -eq "$total" ] && echo "" >&2
|
||||
}
|
||||
|
||||
# ── Выполнение с индикатором ─────────────────────────────────────────────────
|
||||
run_with_spinner() {
|
||||
local label="$1"; shift
|
||||
local err_file="/tmp/.gotelegram_spinner_err_$$"
|
||||
spinner_start "$label"
|
||||
"$@" >/dev/null 2>"$err_file"
|
||||
local rc=$?
|
||||
spinner_stop
|
||||
if [ $rc -eq 0 ]; then
|
||||
log_success "$label"
|
||||
else
|
||||
local err_label
|
||||
err_label=$(type t &>/dev/null && t error || echo "error")
|
||||
log_error "$label ${RED}(${err_label}, code: $rc)${NC}"
|
||||
if [ -s "$err_file" ]; then
|
||||
log_dim " $(head -3 "$err_file")"
|
||||
fi
|
||||
fi
|
||||
rm -f "$err_file"
|
||||
return $rc
|
||||
}
|
||||
|
||||
# ── Banner ───────────────────────────────────────────────────────────────────
|
||||
show_banner() {
|
||||
local line
|
||||
line=$(printf '━%.0s' $(seq 1 60))
|
||||
echo ""
|
||||
echo -e "${CYAN}${line}${NC}"
|
||||
if type tf &>/dev/null; then
|
||||
echo -e " ${BOLD}${WHITE}🚀 $(tf banner_title "$GOTELEGRAM_VERSION")${NC}"
|
||||
echo -e " ${DIM}$(t banner_subtitle)${NC}"
|
||||
echo -e " ${DIM}$(t banner_features)${NC}"
|
||||
else
|
||||
echo -e " ${BOLD}${WHITE}🚀 goTelegram Pro v${GOTELEGRAM_VERSION}${NC}"
|
||||
echo -e " ${DIM}MTProxy powered by telemt (Rust + Tokio)${NC}"
|
||||
echo -e " ${DIM}Anti-DPI • Fake TLS • TCP Splice • JA3/JA4${NC}"
|
||||
fi
|
||||
echo -e "${CYAN}${line}${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Credits ──────────────────────────────────────────────────────────────────
|
||||
show_credits() {
|
||||
local line
|
||||
line=$(printf '─%.0s' $(seq 1 60))
|
||||
echo ""
|
||||
echo -e "${MAGENTA}${line}${NC}"
|
||||
echo -e " ${BOLD}$(type t &>/dev/null && t credits_title || echo 'Credits')${NC}"
|
||||
echo -e "${MAGENTA}${line}${NC}"
|
||||
echo -e " ${WHITE}telemt${NC} — MTProxy engine (Rust)"
|
||||
echo -e " ${DIM}github.com/telemt/telemt${NC}"
|
||||
echo ""
|
||||
echo -e " ${WHITE}HTML5 UP${NC} — responsive HTML/CSS templates"
|
||||
echo -e " ${DIM}html5up.net • CC BY 3.0 • @ajlkn${NC}"
|
||||
echo ""
|
||||
echo -e " ${WHITE}learning-zone${NC} — 150+ HTML5 templates"
|
||||
echo -e " ${DIM}github.com/learning-zone/website-templates${NC}"
|
||||
echo ""
|
||||
echo -e " ${WHITE}Start Bootstrap${NC} — MIT license"
|
||||
echo -e " ${DIM}startbootstrap.com${NC}"
|
||||
echo -e "${MAGENTA}${line}${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Системные утилиты ────────────────────────────────────────────────────────
|
||||
_valid_ip() {
|
||||
# Validate that each octet is 0-255
|
||||
local ip="$1"
|
||||
local IFS='.'
|
||||
read -ra octets <<< "$ip"
|
||||
[ ${#octets[@]} -ne 4 ] && return 1
|
||||
for octet in "${octets[@]}"; do
|
||||
[[ "$octet" =~ ^[0-9]+$ ]] || return 1
|
||||
[ "$octet" -gt 255 ] && return 1
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
get_server_ip() {
|
||||
local ip raw
|
||||
for url in "https://api.ipify.org" "https://icanhazip.com" "https://ifconfig.me"; do
|
||||
raw=$(curl -s -4 --max-time 5 "$url" 2>/dev/null)
|
||||
ip=$(echo "$raw" | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1)
|
||||
if [ -n "$ip" ] && _valid_ip "$ip"; then
|
||||
echo "$ip"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
echo "0.0.0.0"
|
||||
return 1
|
||||
}
|
||||
|
||||
_t_or() {
|
||||
# Helper: translate if i18n available, otherwise return fallback
|
||||
local key="$1" fallback="$2"
|
||||
if type t &>/dev/null; then
|
||||
t "$key"
|
||||
else
|
||||
echo "$fallback"
|
||||
fi
|
||||
}
|
||||
|
||||
check_root() {
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
log_error "$(_t_or err_need_root 'Run the script with sudo / as root')"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_os() {
|
||||
if [ ! -f /etc/os-release ]; then
|
||||
log_error "$(_t_or err_os_unknown 'Failed to detect OS. Linux is required.')"
|
||||
return 1
|
||||
fi
|
||||
# Validate os-release before sourcing (reject command injection: ;, backticks, $())
|
||||
if grep -qE '(;|`|\$\(|\$\{)' /etc/os-release 2>/dev/null; then
|
||||
log_warning "/etc/os-release contains suspicious strings, skipping"
|
||||
return 0
|
||||
fi
|
||||
. /etc/os-release
|
||||
case "$ID" in
|
||||
ubuntu|debian|centos|rocky|almalinux|fedora|rhel)
|
||||
log_dim "OS: $PRETTY_NAME"
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
log_warning "OS $ID may be incompatible. Supported: Ubuntu, Debian, CentOS, Rocky."
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_arch() {
|
||||
local arch
|
||||
arch=$(uname -m)
|
||||
case "$arch" in
|
||||
x86_64|amd64) echo "amd64" ;;
|
||||
aarch64|arm64) echo "arm64" ;;
|
||||
armv7*|armhf) echo "armv7" ;;
|
||||
*) echo "$arch" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_pkg_manager() {
|
||||
if command -v apt-get &>/dev/null; then echo "apt"
|
||||
elif command -v dnf &>/dev/null; then echo "dnf"
|
||||
elif command -v yum &>/dev/null; then echo "yum"
|
||||
else echo "unknown"
|
||||
fi
|
||||
}
|
||||
|
||||
install_pkg() {
|
||||
local pkg="$1"
|
||||
case "$(get_pkg_manager)" in
|
||||
apt) apt_install "$pkg" ;;
|
||||
dnf) dnf install -y -q "$pkg" ;;
|
||||
yum) yum install -y -q "$pkg" ;;
|
||||
*) log_error "$(_t_or err_bad_pkg_mgr 'Unknown package manager')"; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── apt lock wait + install ─────────────────────────────────────────────────
|
||||
# На свежих Ubuntu/Debian unattended-upgrades часто держит dpkg lock на старте
|
||||
# → любой apt-get install падает с "Could not get lock /var/lib/dpkg/lock-frontend".
|
||||
# Эти функции ждут освобождения лока до 300с, потом запускают apt с нативным
|
||||
# таймаутом DPkg::Lock::Timeout. Использовать везде, где раньше был
|
||||
# "apt-get install ...".
|
||||
apt_lock_wait() {
|
||||
local max_wait="${1:-300}"
|
||||
local waited=0
|
||||
local warned=0
|
||||
while fuser /var/lib/dpkg/lock-frontend &>/dev/null \
|
||||
|| fuser /var/lib/dpkg/lock &>/dev/null \
|
||||
|| fuser /var/lib/apt/lists/lock &>/dev/null \
|
||||
|| pgrep -f '^/usr/bin/unattended-upgrade' &>/dev/null; do
|
||||
if [ "$warned" = "0" ]; then
|
||||
log_warning "apt/dpkg locked by unattended-upgrades, waiting up to ${max_wait}s..."
|
||||
warned=1
|
||||
fi
|
||||
sleep 3
|
||||
waited=$((waited + 3))
|
||||
if [ "$waited" -ge "$max_wait" ]; then
|
||||
log_error "apt lock not released after ${max_wait}s"
|
||||
log_dim "Manual fix: systemctl stop unattended-upgrades && killall -9 unattended-upgr 2>/dev/null; dpkg --configure -a"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
[ "$warned" = "1" ] && log_success "apt lock released (waited ${waited}s)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# apt_install <pkg> [pkg2 ...] — ждёт lock + ставит пакеты + показывает ошибку
|
||||
apt_install() {
|
||||
[ $# -eq 0 ] && return 0
|
||||
apt_lock_wait || return 1
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
local opts="-o DPkg::Lock::Timeout=120"
|
||||
local err_file; err_file=$(mktemp 2>/dev/null || echo /tmp/apt_err.$$)
|
||||
if ! apt-get $opts install -y -qq "$@" 2>"$err_file"; then
|
||||
log_error "apt-get install failed: $*"
|
||||
[ -s "$err_file" ] && tail -n 5 "$err_file" | sed 's/^/ /' >&2
|
||||
rm -f "$err_file"
|
||||
return 1
|
||||
fi
|
||||
rm -f "$err_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
# apt_update — тихий update с ожиданием лока
|
||||
apt_update() {
|
||||
apt_lock_wait || return 1
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get -o DPkg::Lock::Timeout=120 update -qq 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Зависимости GoTelegram ──────────────────────────────────────────────────
|
||||
# Полный список внешних команд, которые скрипт использует. Для каждой команды
|
||||
# указан пакет на apt и dnf/yum (имена различаются: например dig = dnsutils на
|
||||
# Debian, bind-utils на RHEL).
|
||||
#
|
||||
# КРИТИЧЕСКИЕ (без них скрипт просто не работает):
|
||||
# jq — парсинг config.json, templates_catalog.json
|
||||
# curl — скачивание telemt и проверки HTTPS
|
||||
# openssl — генерация секретов, шифрование бекапов, SSL проверка
|
||||
# git — клонирование шаблонов через download_template
|
||||
# xxd — hex-encode домена для fake-TLS секрета (ee-prefix)
|
||||
# tar — распаковка telemt архива и бекапы
|
||||
# dig — DNS-проверка домена в Pro-режиме
|
||||
#
|
||||
# ЖЕЛАТЕЛЬНЫЕ (есть fallback, но с ними лучше):
|
||||
# qrencode — QR-коды для прокси-ссылок
|
||||
# bc — красивое форматирование чисел в статистике
|
||||
#
|
||||
# Pro-режим доустанавливает nginx/certbot через install_nginx/install_certbot
|
||||
# (они большие и нужны только если пользователь выбрал Pro).
|
||||
|
||||
# Маппинг команды -> (apt_pkg, dnf_pkg). apt_pkg_for_cmd <cmd>
|
||||
apt_pkg_for_cmd() {
|
||||
case "$1" in
|
||||
dig) echo "dnsutils" ;;
|
||||
xxd) echo "xxd" ;; # Ubuntu 22+: отдельный пакет, fallback ниже
|
||||
nslookup) echo "dnsutils" ;;
|
||||
host) echo "dnsutils" ;;
|
||||
ss) echo "iproute2" ;;
|
||||
netstat) echo "net-tools" ;;
|
||||
flock) echo "util-linux" ;;
|
||||
iptables) echo "iptables" ;;
|
||||
*) echo "$1" ;; # команда == имя пакета
|
||||
esac
|
||||
}
|
||||
|
||||
dnf_pkg_for_cmd() {
|
||||
case "$1" in
|
||||
dig|nslookup|host) echo "bind-utils" ;;
|
||||
xxd) echo "vim-common" ;;
|
||||
ss) echo "iproute" ;;
|
||||
netstat) echo "net-tools" ;;
|
||||
flock) echo "util-linux" ;;
|
||||
iptables) echo "iptables" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ensure_deps() {
|
||||
# Критические зависимости — без них скрипт не работает.
|
||||
# flock используется bot_action_dispatch для сериализации параллельных
|
||||
# вызовов (иначе гонка на config.json при одновременных change-template /
|
||||
# change-lite-domain из бота).
|
||||
local critical=(curl jq openssl git xxd tar dig flock)
|
||||
# Желательные — есть fallback, устанавливать всё равно, но не падать если не смогли
|
||||
local optional=(qrencode bc iptables)
|
||||
|
||||
local missing_critical=() missing_optional=() cmd
|
||||
for cmd in "${critical[@]}"; do
|
||||
command -v "$cmd" &>/dev/null || missing_critical+=("$cmd")
|
||||
done
|
||||
for cmd in "${optional[@]}"; do
|
||||
command -v "$cmd" &>/dev/null || missing_optional+=("$cmd")
|
||||
done
|
||||
|
||||
local all_missing=("${missing_critical[@]}" "${missing_optional[@]}")
|
||||
[ ${#all_missing[@]} -eq 0 ] && return 0
|
||||
|
||||
# Собираем список пакетов для выбранного менеджера
|
||||
local pkg_mgr pkg pkgs=()
|
||||
pkg_mgr=$(get_pkg_manager)
|
||||
|
||||
for cmd in "${all_missing[@]}"; do
|
||||
case "$pkg_mgr" in
|
||||
apt) pkg=$(apt_pkg_for_cmd "$cmd") ;;
|
||||
dnf|yum) pkg=$(dnf_pkg_for_cmd "$cmd") ;;
|
||||
*) pkg="$cmd" ;;
|
||||
esac
|
||||
pkgs+=("$pkg")
|
||||
done
|
||||
|
||||
# Убираем дубликаты (например dig+nslookup оба = dnsutils)
|
||||
local uniq_pkgs=()
|
||||
for pkg in "${pkgs[@]}"; do
|
||||
local found=0 p
|
||||
for p in "${uniq_pkgs[@]}"; do
|
||||
[ "$p" = "$pkg" ] && { found=1; break; }
|
||||
done
|
||||
[ "$found" = "0" ] && uniq_pkgs+=("$pkg")
|
||||
done
|
||||
|
||||
if type tf &>/dev/null; then
|
||||
log_step "$(tf deps_installing "${all_missing[*]}")"
|
||||
else
|
||||
log_step "Installing dependencies: ${all_missing[*]} (packages: ${uniq_pkgs[*]})"
|
||||
fi
|
||||
|
||||
case "$pkg_mgr" in
|
||||
apt)
|
||||
apt_update
|
||||
apt_install "${uniq_pkgs[@]}" || true
|
||||
;;
|
||||
dnf) dnf install -y -q "${uniq_pkgs[@]}" 2>/dev/null ;;
|
||||
yum) yum install -y -q "${uniq_pkgs[@]}" 2>/dev/null ;;
|
||||
*)
|
||||
log_error "Unknown package manager — install manually: ${uniq_pkgs[*]}"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Фолбэки для xxd: на некоторых системах нужен vim-common вместо xxd
|
||||
if ! command -v xxd &>/dev/null && [ "$pkg_mgr" = "apt" ]; then
|
||||
apt_install vim-common || true
|
||||
fi
|
||||
|
||||
# Повторная проверка критических команд
|
||||
local still_missing=()
|
||||
for cmd in "${critical[@]}"; do
|
||||
command -v "$cmd" &>/dev/null || still_missing+=("$cmd")
|
||||
done
|
||||
|
||||
if [ ${#still_missing[@]} -gt 0 ]; then
|
||||
log_error "Critical dependencies still missing: ${still_missing[*]}"
|
||||
log_error "Install manually and re-run gotelegram"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Опциональные — только предупреждение
|
||||
local still_missing_opt=()
|
||||
for cmd in "${optional[@]}"; do
|
||||
command -v "$cmd" &>/dev/null || still_missing_opt+=("$cmd")
|
||||
done
|
||||
if [ ${#still_missing_opt[@]} -gt 0 ]; then
|
||||
log_warning "Optional deps missing (features degraded): ${still_missing_opt[*]}"
|
||||
fi
|
||||
|
||||
log_success "Dependencies ready"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Быстрая проверка — только смотрит что критические установлены, ничего не ставит.
|
||||
# Возвращает 0 если всё ок, 1 если что-то отсутствует. Используется на старте
|
||||
# main() чтобы не дёргать apt-get update при каждом запуске меню.
|
||||
check_deps_present() {
|
||||
local cmd
|
||||
for cmd in curl jq openssl git xxd tar dig flock; do
|
||||
command -v "$cmd" &>/dev/null || return 1
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
check_port() {
|
||||
local port="$1"
|
||||
local line
|
||||
line=$(ss -tlnp 2>/dev/null | grep -E ":${port}\b" | head -1)
|
||||
[ -z "$line" ] && line=$(netstat -tlnp 2>/dev/null | grep -E ":${port}\b" | head -1)
|
||||
if [ -n "$line" ]; then
|
||||
echo "$line"
|
||||
return 0 # порт занят
|
||||
fi
|
||||
return 1 # свободен
|
||||
}
|
||||
|
||||
detect_3xui() {
|
||||
if systemctl list-unit-files 2>/dev/null | grep -Eq '^(x-ui|3x-ui)\.service'; then
|
||||
return 0
|
||||
fi
|
||||
[ -d /etc/x-ui ] || [ -d /usr/local/x-ui ] || [ -f /etc/x-ui/x-ui.db ]
|
||||
}
|
||||
|
||||
detect_3xui_443_listener() {
|
||||
ss -ltnp 2>/dev/null | grep -E '(:|])443[[:space:]]' | grep -Eiq '(xray|x-ui|3x-ui)'
|
||||
}
|
||||
|
||||
warn_3xui_443_conflict() {
|
||||
detect_3xui_443_listener || return 1
|
||||
log_warning "Обнаружен 3x-ui/Xray, который уже слушает TCP/443."
|
||||
log_warning "goTelegram Pro не будет молча останавливать или переписывать 3x-ui."
|
||||
log_dim "Для настоящего shared-443 нужен один фронтовой TLS/SNI-диспетчер и разные SNI-домены для Xray и goTelegram Pro."
|
||||
mkdir -p "$GOTELEGRAM_DIR" 2>/dev/null
|
||||
cat > "$GOTELEGRAM_DIR/shared-443-3xui.md" <<'EOF' 2>/dev/null || true
|
||||
# goTelegram Pro + 3x-ui on one TCP/443
|
||||
|
||||
goTelegram Pro detected that 3x-ui/Xray already owns TCP/443. Two independent
|
||||
processes cannot bind the same IP:port at the same time. A safe shared setup
|
||||
needs one front TLS/SNI dispatcher on 443 and internal backends, for example:
|
||||
|
||||
- dispatcher: 0.0.0.0:443 (nginx stream ssl_preread)
|
||||
- goTelegram Pro telemt: 127.0.0.1:7443
|
||||
- 3x-ui/Xray inbound: 127.0.0.1:9443
|
||||
- goTelegram Pro nginx mask site: 127.0.0.1:8443
|
||||
|
||||
The dispatcher routes Xray SNI domains to Xray. Everything else goes to telemt;
|
||||
telemt then decides whether the session is MTProxy or regular HTTPS and forwards
|
||||
the website to nginx through dns_overrides.
|
||||
|
||||
goTelegram Pro can generate the dispatcher with:
|
||||
|
||||
source /opt/gotelegram/lib/shared443.sh
|
||||
shared443_enable <gotelegram-domain> <xray-sni-domain> 127.0.0.1:9443
|
||||
|
||||
Move the 3x-ui/Xray inbound from 0.0.0.0:443 to 127.0.0.1:9443 in the panel first,
|
||||
or nginx will not be able to own the public 443 socket. goTelegram Pro intentionally
|
||||
does not rewrite the 3x-ui SQLite database or generated Xray config without explicit
|
||||
operator confirmation, because 3x-ui can overwrite manual JSON edits on the next
|
||||
panel change.
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
check_disk_space() {
|
||||
local min_mb="${1:-500}"
|
||||
local avail_mb
|
||||
avail_mb=$(df -m / | awk 'NR==2 {print $4}')
|
||||
if [ "$avail_mb" -lt "$min_mb" ]; then
|
||||
if type tf &>/dev/null; then
|
||||
log_error "$(tf err_low_disk "$avail_mb" "$min_mb")"
|
||||
else
|
||||
log_error "Low disk space: ${avail_mb}MB (need ${min_mb}MB+)"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Конфигурация GoTelegram (JSON) ──────────────────────────────────────────
|
||||
save_gotelegram_config() {
|
||||
mkdir -p "$(dirname "$GOTELEGRAM_CONFIG")"
|
||||
local cur_lang
|
||||
cur_lang=$(type get_language &>/dev/null && get_language || echo en)
|
||||
cat > "$GOTELEGRAM_CONFIG" << EOJSON
|
||||
{
|
||||
"version": "$GOTELEGRAM_VERSION",
|
||||
"engine": "${1:-telemt}",
|
||||
"mode": "${2:-lite}",
|
||||
"port": ${3:-443},
|
||||
"secret": "${4:-}",
|
||||
"mask_host": "${5:-google.com}",
|
||||
"domain": "${6:-}",
|
||||
"template_id": "${7:-}",
|
||||
"language": "${cur_lang}",
|
||||
"installed_at": "$(date -Iseconds)",
|
||||
"updated_at": "$(date -Iseconds)"
|
||||
}
|
||||
EOJSON
|
||||
chmod 600 "$GOTELEGRAM_CONFIG"
|
||||
}
|
||||
|
||||
load_gotelegram_config() {
|
||||
if [ -f "$GOTELEGRAM_CONFIG" ]; then
|
||||
cat "$GOTELEGRAM_CONFIG"
|
||||
return 0
|
||||
fi
|
||||
echo "{}"
|
||||
return 1
|
||||
}
|
||||
|
||||
config_get() {
|
||||
local key="$1"
|
||||
if [ ! -f "$GOTELEGRAM_CONFIG" ]; then
|
||||
return 2 # file missing
|
||||
fi
|
||||
local val
|
||||
val=$(jq -r ".$key // empty" "$GOTELEGRAM_CONFIG" 2>/dev/null)
|
||||
if [ $? -ne 0 ]; then
|
||||
return 3 # invalid JSON
|
||||
fi
|
||||
if [ -z "$val" ]; then
|
||||
return 1 # key missing or empty
|
||||
fi
|
||||
echo "$val"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── V1 совместимость ─────────────────────────────────────────────────────────
|
||||
detect_v1_installation() {
|
||||
# Проверяем наличие mtg Docker контейнера (v1)
|
||||
if command -v docker &>/dev/null; then
|
||||
if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${V1_CONTAINER_NAME}$"; then
|
||||
return 0 # v1 обнаружена
|
||||
fi
|
||||
fi
|
||||
# Проверяем наличие конфига v1
|
||||
if [ -f "$V1_CONFIG_FILE" ]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_v1_config() {
|
||||
# Извлекаем данные из работающего v1 контейнера
|
||||
if ! command -v docker &>/dev/null; then
|
||||
echo "{}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local running
|
||||
running=$(docker ps --format '{{.Names}}' 2>/dev/null | grep "^${V1_CONTAINER_NAME}$")
|
||||
|
||||
if [ -z "$running" ]; then
|
||||
# Пробуем из сохранённого конфига
|
||||
if [ -f "$V1_CONFIG_FILE" ]; then
|
||||
cat "$V1_CONFIG_FILE"
|
||||
return 0
|
||||
fi
|
||||
echo "{}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Достаём из Docker
|
||||
local cmd_str port secret ip
|
||||
cmd_str=$(docker inspect "$V1_CONTAINER_NAME" --format='{{range .Config.Cmd}}{{.}} {{end}}' 2>/dev/null)
|
||||
secret=$(echo "$cmd_str" | awk '{print $NF}')
|
||||
port=$(docker inspect "$V1_CONTAINER_NAME" --format='{{range $p,$c := .HostConfig.PortBindings}}{{(index $c 0).HostPort}}{{end}}' 2>/dev/null)
|
||||
ip=$(get_server_ip)
|
||||
|
||||
jq -n \
|
||||
--arg secret "$secret" \
|
||||
--arg port "${port:-443}" \
|
||||
--arg ip "$ip" \
|
||||
'{secret: $secret, port: ($port | tonumber), ip: $ip, engine: "mtg"}'
|
||||
}
|
||||
|
||||
migrate_v1_to_v2() {
|
||||
log_step "$(_t_or v1_migration_step 'Migrating from v1 (mtg) to v2 (telemt)')"
|
||||
|
||||
local v1_config
|
||||
v1_config=$(get_v1_config)
|
||||
|
||||
local old_port old_secret
|
||||
old_port=$(echo "$v1_config" | jq -r '.port // 443')
|
||||
old_secret=$(echo "$v1_config" | jq -r '.secret // empty')
|
||||
|
||||
if [ -z "$old_secret" ]; then
|
||||
log_warning "Failed to extract secret from v1. A new one will be generated."
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${WHITE}$(_t_or v1_found_title 'Found v1 (mtg) installation:')${NC}"
|
||||
if type tf &>/dev/null; then
|
||||
echo -e " $(tf v1_port "$old_port")"
|
||||
echo -e " $(tf v1_secret "${old_secret:0:16}")"
|
||||
else
|
||||
echo -e " Port: ${CYAN}${old_port}${NC}"
|
||||
echo -e " Secret: ${CYAN}${old_secret:0:16}...${NC}"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " ${YELLOW}$(_t_or warning 'Warning'):${NC} $(_t_or v1_incompatible 'mtg secret is NOT directly compatible with telemt.')"
|
||||
echo -e " $(_t_or v1_new_link 'Clients will need a new link.')"
|
||||
echo ""
|
||||
echo -ne " $(_t_or v1_stop_migrate 'Stop v1 container and migrate to v2? [Y/n]:') "
|
||||
read -r ans
|
||||
if [[ "$ans" =~ ^[Nn] ]]; then
|
||||
log_info "$(_t_or v1_migration_cancelled 'Migration cancelled. v1 left intact.')"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Stop v1
|
||||
log_info "$(_t_or v1_stopping 'Stopping v1 container...')"
|
||||
docker stop "$V1_CONTAINER_NAME" 2>/dev/null
|
||||
docker rm "$V1_CONTAINER_NAME" 2>/dev/null
|
||||
|
||||
# Backup v1 config
|
||||
if [ -f "$V1_CONFIG_FILE" ]; then
|
||||
mkdir -p "$GOTELEGRAM_DIR"
|
||||
cp "$V1_CONFIG_FILE" "$GOTELEGRAM_DIR/v1_backup_proxy.json" 2>/dev/null
|
||||
if type tf &>/dev/null; then
|
||||
log_success "$(tf v1_config_saved "$GOTELEGRAM_DIR/v1_backup_proxy.json")"
|
||||
else
|
||||
log_success "v1 config saved to $GOTELEGRAM_DIR/v1_backup_proxy.json"
|
||||
fi
|
||||
fi
|
||||
|
||||
if type tf &>/dev/null; then
|
||||
log_success "$(tf v1_port_freed "$old_port")"
|
||||
else
|
||||
log_success "v1 stopped. Port $old_port freed."
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Confirm prompt ───────────────────────────────────────────────────────────
|
||||
confirm() {
|
||||
local default_msg
|
||||
default_msg=$(_t_or install_continue_anyway 'Continue?')
|
||||
local msg="${1:-$default_msg}"
|
||||
echo -ne " ${msg} [Y/n]: " >&2
|
||||
read -r ans
|
||||
[[ ! "$ans" =~ ^[Nn] ]]
|
||||
}
|
||||
|
||||
# ── Выбор из списка ──────────────────────────────────────────────────────────
|
||||
select_option() {
|
||||
local title="$1"
|
||||
shift
|
||||
local options=("$@")
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}${title}${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" >&2
|
||||
local i=1
|
||||
for opt in "${options[@]}"; do
|
||||
echo -e " ${CYAN}${i})${NC} ${opt}" >&2
|
||||
((i++))
|
||||
done
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" >&2
|
||||
echo -ne " ${WHITE}$(_t_or choose 'Choose'):${NC} " >&2
|
||||
read -r choice
|
||||
echo "$choice"
|
||||
}
|
||||
|
||||
# ── Генерация случайного hex ─────────────────────────────────────────────────
|
||||
generate_hex() {
|
||||
local len="${1:-32}"
|
||||
openssl rand -hex "$((len/2))" 2>/dev/null || head -c "$((len/2))" /dev/urandom | xxd -p | tr -d '\n'
|
||||
}
|
||||
|
||||
# ── Проверка домена ──────────────────────────────────────────────────────────
|
||||
validate_domain() {
|
||||
local domain="$1"
|
||||
if echo "$domain" | grep -qE '^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Init: создание директорий ────────────────────────────────────────────────
|
||||
init_dirs() {
|
||||
mkdir -p "$GOTELEGRAM_DIR" "$BACKUP_DIR" /etc/telemt 2>/dev/null
|
||||
touch "$LOG_FILE" 2>/dev/null
|
||||
}
|
||||
Reference in New Issue
Block a user