Files
gotelegram_pro/lib/common.sh

457 lines
18 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# GoTelegram v2.2 — Общие утилиты
# Цвета, логирование, спиннер, системные функции, совместимость с v1
# ── Версия ────────────────────────────────────────────────────────────────────
GOTELEGRAM_VERSION="2.2.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} $*"; }
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
}