#!/bin/bash # GoTelegram v2.3 — Общие утилиты # Цвета, логирование, спиннер, системные функции, совместимость с v1 # ── Версия ──────────────────────────────────────────────────────────────────── GOTELEGRAM_VERSION="2.3.1" GOTELEGRAM_NAME="GoTelegram" # ── Пути ────────────────────────────────────────────────────────────────────── 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" # ── 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 } # ── Спиннер ────────────────────────────────────────────────────────────────── _spin_pid="" spinner_start() { local msg="${1:-Подождите...}" ( 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&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 log_error "$label ${RED}(ошибка, код: $rc)${NC}" if [ -s "$err_file" ]; then log_dim " $(head -3 "$err_file")" fi fi rm -f "$err_file" return $rc } # ── Баннер ─────────────────────────────────────────────────────────────────── show_banner() { echo "" echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║${NC} ${BOLD}${WHITE}🚀 GoTelegram v${GOTELEGRAM_VERSION}${NC} ${CYAN}║${NC}" echo -e "${CYAN}║${NC} ${DIM}MTProxy на ядре telemt (Rust + Tokio)${NC} ${CYAN}║${NC}" echo -e "${CYAN}║${NC} ${DIM}Anti-DPI • Fake TLS • TCP Splice • JA3/JA4${NC} ${CYAN}║${NC}" echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}" echo "" } # ── Благодарности ──────────────────────────────────────────────────────────── show_credits() { echo "" echo -e "${MAGENTA}╔══════════════════════════════════════════════════════════╗${NC}" echo -e "${MAGENTA}║${NC} ${BOLD}Благодарности / Credits${NC} ${MAGENTA}║${NC}" echo -e "${MAGENTA}╟──────────────────────────────────────────────────────────╢${NC}" echo -e "${MAGENTA}║${NC} ${WHITE}telemt${NC} — MTProxy engine (Rust) ${MAGENTA}║${NC}" echo -e "${MAGENTA}║${NC} ${DIM}github.com/telemt/telemt${NC} ${MAGENTA}║${NC}" echo -e "${MAGENTA}║${NC} ${MAGENTA}║${NC}" echo -e "${MAGENTA}║${NC} ${WHITE}HTML5 UP${NC} — адаптивные HTML/CSS шаблоны ${MAGENTA}║${NC}" echo -e "${MAGENTA}║${NC} ${DIM}html5up.net • CC BY 3.0 • @ajlkn${NC} ${MAGENTA}║${NC}" echo -e "${MAGENTA}║${NC} ${MAGENTA}║${NC}" echo -e "${MAGENTA}║${NC} ${WHITE}learning-zone${NC} — 150+ HTML5 шаблонов ${MAGENTA}║${NC}" echo -e "${MAGENTA}║${NC} ${DIM}github.com/learning-zone/website-templates${NC} ${MAGENTA}║${NC}" echo -e "${MAGENTA}║${NC} ${MAGENTA}║${NC}" echo -e "${MAGENTA}║${NC} ${WHITE}Start Bootstrap${NC} — MIT лицензия ${MAGENTA}║${NC}" echo -e "${MAGENTA}║${NC} ${DIM}startbootstrap.com${NC} ${MAGENTA}║${NC}" echo -e "${MAGENTA}╚══════════════════════════════════════════════════════════╝${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 } check_root() { if [ "$EUID" -ne 0 ]; then log_error "Запустите скрипт с sudo / от root" exit 1 fi } check_os() { if [ ! -f /etc/os-release ]; then log_error "Не удалось определить ОС. Требуется Linux." 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 содержит подозрительные строки, пропускаем" return 0 fi . /etc/os-release case "$ID" in ubuntu|debian|centos|rocky|almalinux|fedora|rhel) log_dim "ОС: $PRETTY_NAME" return 0 ;; *) log_warning "ОС $ID может быть несовместима. Поддерживаются: 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-get install -y -qq "$pkg" ;; dnf) dnf install -y -q "$pkg" ;; yum) yum install -y -q "$pkg" ;; *) log_error "Неизвестный пакетный менеджер"; return 1 ;; esac } ensure_deps() { local missing=() for cmd in curl jq openssl git; do if ! command -v "$cmd" &>/dev/null; then missing+=("$cmd") fi done if [ ${#missing[@]} -gt 0 ]; then log_step "Установка зависимостей: ${missing[*]}" case "$(get_pkg_manager)" in apt) apt-get update -qq && apt-get install -y -qq "${missing[@]}" ;; dnf) dnf install -y -q "${missing[@]}" ;; yum) yum install -y -q "${missing[@]}" ;; esac fi } 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 # свободен } 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 log_error "Мало места на диске: ${avail_mb}MB (нужно ${min_mb}MB+)" return 1 fi return 0 } # ── Конфигурация GoTelegram (JSON) ────────────────────────────────────────── save_gotelegram_config() { mkdir -p "$(dirname "$GOTELEGRAM_CONFIG")" 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:-}", "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 log_dim "Конфиг не найден: $GOTELEGRAM_CONFIG" >&2 return 2 # file missing fi local val val=$(jq -r ".$key // empty" "$GOTELEGRAM_CONFIG" 2>/dev/null) if [ $? -ne 0 ]; then log_dim "Ошибка чтения JSON: $GOTELEGRAM_CONFIG" >&2 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 "Миграция с v1 (mtg) на 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 "Не удалось извлечь secret из v1. Будет создан новый." return 1 fi echo "" echo -e " ${WHITE}Найдена установка v1 (mtg):${NC}" echo -e " Порт: ${CYAN}${old_port}${NC}" echo -e " Secret: ${CYAN}${old_secret:0:16}...${NC}" echo "" echo -e " ${YELLOW}Внимание:${NC} секрет mtg НЕ совместим с telemt напрямую." echo -e " Клиентам потребуется новая ссылка." echo "" echo -ne " Остановить v1 контейнер и перейти на v2? [Y/n]: " read -r ans if [[ "$ans" =~ ^[Nn] ]]; then log_info "Миграция отменена. v1 оставлен без изменений." return 1 fi # Останавливаем v1 log_info "Остановка v1 контейнера..." docker stop "$V1_CONTAINER_NAME" 2>/dev/null docker rm "$V1_CONTAINER_NAME" 2>/dev/null # Бекапим v1 конфиг if [ -f "$V1_CONFIG_FILE" ]; then mkdir -p "$GOTELEGRAM_DIR" cp "$V1_CONFIG_FILE" "$GOTELEGRAM_DIR/v1_backup_proxy.json" 2>/dev/null log_success "Конфиг v1 сохранён в $GOTELEGRAM_DIR/v1_backup_proxy.json" fi log_success "v1 остановлен. Порт $old_port освобождён." return 0 } # ── Подтверждение ──────────────────────────────────────────────────────────── confirm() { local msg="${1:-Продолжить?}" 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}Выбор:${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 }