mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 15:36:03 +00:00
457 lines
19 KiB
Bash
457 lines
19 KiB
Bash
#!/bin/bash
|
||
# GoTelegram v2.2 — Общие утилиты
|
||
# Цвета, логирование, спиннер, системные функции, совместимость с v1
|
||
|
||
# ── Версия ────────────────────────────────────────────────────────────────────
|
||
GOTELEGRAM_VERSION="2.2.0"
|
||
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} $*"; }
|
||
log_success() { echo -e " ${GREEN}✓${NC} $*"; }
|
||
log_warning() { echo -e " ${YELLOW}⚠${NC} $*"; }
|
||
log_error() { echo -e " ${RED}✗${NC} $*"; }
|
||
log_step() { echo -e "\n${BOLD}${WHITE} $*${NC}"; }
|
||
log_dim() { echo -e " ${DIM}$*${NC}"; }
|
||
|
||
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<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
|
||
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:-quick}",
|
||
"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]: "
|
||
read -r ans
|
||
[[ ! "$ans" =~ ^[Nn] ]]
|
||
}
|
||
|
||
# ── Выбор из списка ──────────────────────────────────────────────────────────
|
||
select_option() {
|
||
local title="$1"
|
||
shift
|
||
local options=("$@")
|
||
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}${title}${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
|
||
local i=1
|
||
for opt in "${options[@]}"; do
|
||
echo -e " ${CYAN}${i})${NC} ${opt}"
|
||
((i++))
|
||
done
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
|
||
echo -ne " ${WHITE}Выбор:${NC} "
|
||
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
|
||
}
|