mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 21:56:05 +00:00
Compare commits
6 Commits
v2.5.0-alf
...
rollback-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fec096676 | ||
|
|
7075ff8696 | ||
|
|
0e38c2b5b6 | ||
|
|
9d9d12e150 | ||
|
|
8f249c35e5 | ||
|
|
bb2502e1fc |
@@ -100,7 +100,7 @@ logger = logging.getLogger(__name__)
|
||||
# CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
GOTELEGRAM_VERSION = "2.4.6"
|
||||
GOTELEGRAM_VERSION = "2.4.10"
|
||||
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
||||
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
||||
TELEMT_SERVICE = "telemt"
|
||||
@@ -389,7 +389,7 @@ async def check_service_status(service: str) -> bool:
|
||||
|
||||
async def get_telemt_version() -> str:
|
||||
"""Get telemt version."""
|
||||
code, stdout, _ = await sh("telemt", "-v")
|
||||
code, stdout, _ = await sh("telemt", "--version")
|
||||
if code == 0:
|
||||
return stdout.strip().split()[-1] if stdout else "unknown"
|
||||
return "unknown"
|
||||
@@ -687,7 +687,7 @@ async def get_status_text(user_id: Optional[int] = None) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def get_traffic_stats() -> str:
|
||||
async def get_traffic_stats(user_id: Optional[int] = None) -> str:
|
||||
"""Get formatted traffic statistics."""
|
||||
# Read current snapshot
|
||||
current_file = "/run/gotelegram/stats_current.json"
|
||||
@@ -697,20 +697,23 @@ async def get_traffic_stats() -> str:
|
||||
with open(current_file, "r") as f:
|
||||
current = json.load(f)
|
||||
except Exception:
|
||||
return "📊 <b>Статистика</b>\n\n<i>Данные недоступны. Убедитесь что модуль статистики включён.</i>"
|
||||
return f"📊 <b>{_t(user_id, 'stats_title', 'Statistics')}</b>\n\n<i>{_t(user_id, 'stats_unavailable', 'Data unavailable. Make sure stats module is enabled.')}</i>"
|
||||
|
||||
# Read history
|
||||
# Read history (skip header and non-numeric rows)
|
||||
history = []
|
||||
try:
|
||||
with open(history_file, "r") as f:
|
||||
reader = csv.reader(f)
|
||||
for row in reader:
|
||||
if len(row) >= 3:
|
||||
history.append({
|
||||
"ts": int(row[0]),
|
||||
"proxy": int(row[1]),
|
||||
"site": int(row[2]),
|
||||
})
|
||||
try:
|
||||
history.append({
|
||||
"ts": int(row[0]),
|
||||
"proxy": int(row[1]),
|
||||
"site": int(row[2]),
|
||||
})
|
||||
except (ValueError, TypeError):
|
||||
continue # skip header or malformed rows
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -754,21 +757,27 @@ async def get_traffic_stats() -> str:
|
||||
return format_bytes(diff), format_rate(rate)
|
||||
|
||||
periods = [
|
||||
("1 мин", 60),
|
||||
("5 мин", 300),
|
||||
("60 мин", 3600),
|
||||
("1 день", 86400),
|
||||
("7 дней", 604800),
|
||||
("30 дней", 2592000),
|
||||
("365 дней", 31536000),
|
||||
(_t(user_id, "stats_1min", "1 min"), 60),
|
||||
(_t(user_id, "stats_5min", "5 min"), 300),
|
||||
(_t(user_id, "stats_60min", "60 min"), 3600),
|
||||
(_t(user_id, "stats_1day", "1 day"), 86400),
|
||||
(_t(user_id, "stats_7days", "7 days"), 604800),
|
||||
(_t(user_id, "stats_30days", "30 days"), 2592000),
|
||||
(_t(user_id, "stats_365days", "365 days"), 31536000),
|
||||
]
|
||||
|
||||
lines = ["📊 <b>Статистика трафика</b>\n"]
|
||||
hdr_period = _t(user_id, "stats_hdr_period", "Period")
|
||||
hdr_traffic = _t(user_id, "stats_hdr_traffic", "Traffic")
|
||||
hdr_rate = _t(user_id, "stats_hdr_rate", "Rate")
|
||||
|
||||
for label, key in [("Proxy (telemt)", "proxy"), ("Сайт (nginx)", "site")]:
|
||||
lines = [f"📊 <b>{_t(user_id, 'stats_traffic_title', 'Traffic statistics')}</b>\n"]
|
||||
|
||||
lbl_proxy = _t(user_id, "stats_proxy_label", "Proxy (telemt)")
|
||||
lbl_site = _t(user_id, "stats_site_label", "Site (nginx)")
|
||||
for label, key in [(lbl_proxy, "proxy"), (lbl_site, "site")]:
|
||||
lines.append(f"\n<b>{label}:</b>")
|
||||
lines.append("<pre>")
|
||||
lines.append(f"{'Период':<10} │ {'Трафик':>10} │ {'Скорость':>10}")
|
||||
lines.append(f"{hdr_period:<10} │ {hdr_traffic:>10} │ {hdr_rate:>10}")
|
||||
lines.append("─" * 36)
|
||||
for name, secs in periods:
|
||||
total, rate = calc_for_period(secs, key)
|
||||
@@ -783,11 +792,12 @@ async def cb_menu_stats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
stats_text = await get_traffic_stats()
|
||||
uid = _uid(update)
|
||||
stats_text = await get_traffic_stats(uid)
|
||||
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("🔄 Обновить", callback_data="menu_stats")],
|
||||
[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")],
|
||||
[InlineKeyboardButton(_t(uid, "btn_refresh", "🔄 Refresh"), callback_data="menu_stats")],
|
||||
[InlineKeyboardButton(_t(uid, "btn_back"), callback_data="menu_main")],
|
||||
]
|
||||
|
||||
await safe_edit_message(
|
||||
|
||||
@@ -112,5 +112,21 @@
|
||||
"cg_timeout": "❌ Clone timeout (repository too large or slow)",
|
||||
"cg_too_big": "❌ Repository too large (>100MB)",
|
||||
"cg_no_index": "❌ No index.html found in repository",
|
||||
"cg_ok_fmt": "✅ Custom template downloaded: %s"
|
||||
"cg_ok_fmt": "✅ Custom template downloaded: %s",
|
||||
|
||||
"stats_title": "Statistics",
|
||||
"stats_unavailable": "Data unavailable. Make sure stats module is enabled.",
|
||||
"stats_traffic_title": "Traffic statistics",
|
||||
"stats_proxy_label": "Proxy (telemt)",
|
||||
"stats_site_label": "Site (nginx)",
|
||||
"stats_hdr_period": "Period",
|
||||
"stats_hdr_traffic": "Traffic",
|
||||
"stats_hdr_rate": "Rate",
|
||||
"stats_1min": "1 min",
|
||||
"stats_5min": "5 min",
|
||||
"stats_60min": "60 min",
|
||||
"stats_1day": "1 day",
|
||||
"stats_7days": "7 days",
|
||||
"stats_30days": "30 days",
|
||||
"stats_365days": "365 days"
|
||||
}
|
||||
|
||||
@@ -112,5 +112,21 @@
|
||||
"cg_timeout": "❌ Таймаут клонирования (репозиторий слишком большой или медленный)",
|
||||
"cg_too_big": "❌ Репозиторий слишком большой (>100МБ)",
|
||||
"cg_no_index": "❌ В репозитории не найден index.html",
|
||||
"cg_ok_fmt": "✅ Свой шаблон загружен: %s"
|
||||
"cg_ok_fmt": "✅ Свой шаблон загружен: %s",
|
||||
|
||||
"stats_title": "Статистика",
|
||||
"stats_unavailable": "Данные недоступны. Убедитесь что модуль статистики включён.",
|
||||
"stats_traffic_title": "Статистика трафика",
|
||||
"stats_proxy_label": "Proxy (telemt)",
|
||||
"stats_site_label": "Сайт (nginx)",
|
||||
"stats_hdr_period": "Период",
|
||||
"stats_hdr_traffic": "Трафик",
|
||||
"stats_hdr_rate": "Скорость",
|
||||
"stats_1min": "1 мин",
|
||||
"stats_5min": "5 мин",
|
||||
"stats_60min": "60 мин",
|
||||
"stats_1day": "1 день",
|
||||
"stats_7days": "7 дней",
|
||||
"stats_30days": "30 дней",
|
||||
"stats_365days": "365 дней"
|
||||
}
|
||||
|
||||
121
install.sh
Executable file → Normal file
121
install.sh
Executable file → Normal file
@@ -258,6 +258,49 @@ menu_install() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Always start from a clean slate — any leftover env vars from a previous
|
||||
# manual-key entry must not leak into a "new install" flow.
|
||||
unset GOTELEGRAM_EXISTING_SECRET GOTELEGRAM_EXISTING_DOMAIN GOTELEGRAM_EXISTING_PORT
|
||||
|
||||
# ── Step 1: install source picker ────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}$(_t_or install_source_title 'Источник установки')${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||
echo -e " ${CYAN}1)${NC} ${GREEN}$(_t_or install_menu_new 'Новая установка')${NC}"
|
||||
echo -e " ${DIM}$(_t_or install_menu_new_desc 'Сгенерировать новый ключ и настроить с нуля')${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}2)${NC} ${BLUE}$(_t_or install_menu_restore 'Восстановить из бекапа')${NC}"
|
||||
echo -e " ${DIM}$(_t_or install_menu_restore_desc 'Полное восстановление из файла .tar.gz[.enc]')${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}3)${NC} ${YELLOW}$(_t_or install_menu_existing_key 'Использовать существующий ключ')${NC}"
|
||||
echo -e " ${DIM}$(_t_or install_menu_existing_key_desc 'Ввести ссылку tg://proxy или ключ вручную')${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||
echo -ne " ${WHITE}$(_t_or install_source_choice 'Выберите источник')${NC} "
|
||||
read -r src_choice
|
||||
src_choice="${src_choice:-}"
|
||||
|
||||
case "$src_choice" in
|
||||
1) : ;; # fall through to mode picker
|
||||
2)
|
||||
if type interactive_restore &>/dev/null; then
|
||||
interactive_restore
|
||||
else
|
||||
log_error "backup.sh not loaded"
|
||||
fi
|
||||
return
|
||||
;;
|
||||
3)
|
||||
if type manual_secret_input &>/dev/null; then
|
||||
manual_secret_input || return
|
||||
else
|
||||
log_error "backup.sh not loaded"
|
||||
return
|
||||
fi
|
||||
;;
|
||||
*) log_error "$(tf install_bad_choice "${src_choice:-<empty>}")" ; return ;;
|
||||
esac
|
||||
|
||||
# ── Step 2: lite/pro mode picker ─────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}$(t install_select_mode)${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||
@@ -274,11 +317,19 @@ menu_install() {
|
||||
read -r mode_choice
|
||||
mode_choice="${mode_choice:-}"
|
||||
|
||||
# If user provided an ee-prefixed key with a domain, hint at pro mode
|
||||
if [ -n "${GOTELEGRAM_EXISTING_DOMAIN:-}" ] && [ "$mode_choice" = "1" ]; then
|
||||
log_warning "$(_t_or install_hint_pro_mode 'Ключ содержит домен — обычно это Pro режим')"
|
||||
fi
|
||||
|
||||
case "$mode_choice" in
|
||||
1) install_lite_mode ;;
|
||||
2) install_pro_mode ;;
|
||||
*) log_error "$(tf install_bad_choice "${mode_choice:-<empty>}")" ;;
|
||||
esac
|
||||
|
||||
# Clean up env vars after install, just in case
|
||||
unset GOTELEGRAM_EXISTING_SECRET GOTELEGRAM_EXISTING_DOMAIN GOTELEGRAM_EXISTING_PORT
|
||||
}
|
||||
|
||||
# ── Lite mode ───────────────────────────────────────────────────────────────
|
||||
@@ -290,14 +341,30 @@ install_lite_mode() {
|
||||
domain=$(select_quick_domain)
|
||||
[ $? -ne 0 ] && return
|
||||
|
||||
# Port selection
|
||||
# Port selection — if user provided a port via existing-key flow, reuse it
|
||||
local port
|
||||
port=$(select_port)
|
||||
[ $? -ne 0 ] && return
|
||||
if [ -n "${GOTELEGRAM_EXISTING_PORT:-}" ] && [[ "$GOTELEGRAM_EXISTING_PORT" =~ ^[0-9]+$ ]]; then
|
||||
port="$GOTELEGRAM_EXISTING_PORT"
|
||||
log_info "$(_t_or install_reuse_port 'Используется порт из ключа'): ${port}"
|
||||
else
|
||||
port=$(select_port)
|
||||
[ $? -ne 0 ] && return
|
||||
fi
|
||||
|
||||
# Generate secret
|
||||
# Preflight: port conflict check (checks the external port only for lite)
|
||||
if ! preflight_check "lite" "$port"; then
|
||||
show_promo_with_qr 15
|
||||
return
|
||||
fi
|
||||
|
||||
# Secret: reuse if provided via manual_secret_input, otherwise generate new
|
||||
local secret
|
||||
secret=$(generate_hex 32)
|
||||
if [ -n "${GOTELEGRAM_EXISTING_SECRET:-}" ]; then
|
||||
secret="$GOTELEGRAM_EXISTING_SECRET"
|
||||
log_info "$(_t_or install_reuse_secret 'Используется переданный ключ'): ${secret:0:8}...${secret: -4}"
|
||||
else
|
||||
secret=$(generate_hex 32)
|
||||
fi
|
||||
|
||||
# Confirm
|
||||
local ip
|
||||
@@ -330,6 +397,11 @@ install_lite_mode() {
|
||||
# Save GoTelegram config
|
||||
save_gotelegram_config "telemt" "lite" "$port" "$secret" "$domain" "" ""
|
||||
|
||||
# Auto-install stats collector so stats work from the start
|
||||
if type install_stats_collector &>/dev/null; then
|
||||
install_stats_collector 2>/dev/null
|
||||
fi
|
||||
|
||||
# Credits
|
||||
show_credits
|
||||
|
||||
@@ -342,10 +414,22 @@ install_lite_mode() {
|
||||
install_pro_mode() {
|
||||
log_step "$(t install_pro_step)"
|
||||
|
||||
# Enter domain
|
||||
echo ""
|
||||
echo -ne " ${WHITE}$(t install_enter_domain)${NC} "
|
||||
read -r user_domain
|
||||
# Preflight: pro mode needs 443, 80 and 8443 (internal nginx mask)
|
||||
if ! preflight_check "pro"; then
|
||||
show_promo_with_qr 15
|
||||
return
|
||||
fi
|
||||
|
||||
# Enter domain — if provided via existing-key flow, reuse it
|
||||
local user_domain=""
|
||||
if [ -n "${GOTELEGRAM_EXISTING_DOMAIN:-}" ]; then
|
||||
user_domain="$GOTELEGRAM_EXISTING_DOMAIN"
|
||||
log_info "$(_t_or install_reuse_domain 'Используется домен из ключа'): ${user_domain}"
|
||||
else
|
||||
echo ""
|
||||
echo -ne " ${WHITE}$(t install_enter_domain)${NC} "
|
||||
read -r user_domain
|
||||
fi
|
||||
|
||||
if [ -z "$user_domain" ] || ! validate_domain "$user_domain"; then
|
||||
log_error "$(tf install_bad_domain "${user_domain:-<empty>}")"
|
||||
@@ -387,8 +471,14 @@ install_pro_mode() {
|
||||
|
||||
# Generate fake-TLS secret (ee + secret + hex domain)
|
||||
# ee prefix tells Telegram client to masquerade traffic as TLS to domain
|
||||
# Reuse existing secret if manual_secret_input provided it
|
||||
local raw_secret
|
||||
raw_secret=$(generate_hex 32)
|
||||
if [ -n "${GOTELEGRAM_EXISTING_SECRET:-}" ]; then
|
||||
raw_secret="$GOTELEGRAM_EXISTING_SECRET"
|
||||
log_info "$(_t_or install_reuse_secret 'Используется переданный ключ'): ${raw_secret:0:8}...${raw_secret: -4}"
|
||||
else
|
||||
raw_secret=$(generate_hex 32)
|
||||
fi
|
||||
local domain_hex
|
||||
domain_hex=$(printf '%s' "$user_domain" | xxd -p | tr -d '\n')
|
||||
local faketls_secret="ee${raw_secret}${domain_hex}"
|
||||
@@ -427,6 +517,11 @@ install_pro_mode() {
|
||||
tpl_id=$(basename "$template_dir")
|
||||
save_gotelegram_config "telemt" "pro" "443" "$raw_secret" "$user_domain" "$user_domain" "$tpl_id"
|
||||
|
||||
# Auto-install stats collector so stats work from the start
|
||||
if type install_stats_collector &>/dev/null; then
|
||||
install_stats_collector 2>/dev/null
|
||||
fi
|
||||
|
||||
# Result — use domain and fake-TLS link
|
||||
show_proxy_info_pro "$user_domain" "$faketls_secret"
|
||||
echo -e " ${WHITE}$(t svc_site):${NC} ${GREEN}https://${user_domain}${NC}"
|
||||
@@ -1142,6 +1237,7 @@ mark_promo_shown() {
|
||||
# текстовые ссылки и промокоды (см. _promo_block) — QR-коды хостеров
|
||||
# визуально конкурировали с чаевыми и перегружали экран.
|
||||
show_promo_with_qr() {
|
||||
local countdown="${1:-5}"
|
||||
_promo_block
|
||||
|
||||
# QR только для чаевых
|
||||
@@ -1154,8 +1250,9 @@ show_promo_with_qr() {
|
||||
|
||||
mark_promo_shown
|
||||
|
||||
# 5-second countdown
|
||||
for i in 5 4 3 2 1; do
|
||||
# Countdown (default 5s, caller may pass longer for preflight abort)
|
||||
local i
|
||||
for ((i=countdown; i>0; i--)); do
|
||||
echo -ne "\r ${DIM}$(tf promo_menu_in "$i")${NC} "
|
||||
sleep 1
|
||||
done
|
||||
|
||||
497
lib/backup.sh
Executable file → Normal file
497
lib/backup.sh
Executable file → Normal file
@@ -1,71 +1,179 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4 — backup and restore (i18n-aware)
|
||||
# GoTelegram v2.4.9 — Unified Backup Format (UBF) v2.0
|
||||
#
|
||||
# UBF v2.0 layout (inside the tarball):
|
||||
# gotelegram_backup_YYYYMMDD_HHMMSS_<shortid>/
|
||||
# ├── metadata.json # backup_id, versions, fingerprint, ...
|
||||
# ├── secrets.json # raw_secret, faketls_secret, proxy_link, bot_token
|
||||
# ├── telemt/config.toml
|
||||
# ├── gotelegram/config.json
|
||||
# ├── gotelegram/.language
|
||||
# ├── nginx/site.conf
|
||||
# ├── letsencrypt/
|
||||
# │ ├── live/<domain>/{fullchain,privkey,chain,cert}.pem
|
||||
# │ └── renewal/<domain>.conf
|
||||
# ├── site/ (nginx document root)
|
||||
# └── bot/.env
|
||||
#
|
||||
# Backup ID format: GT-YYMMDD-<last6hex-of-raw-secret>
|
||||
# Archive name: gotelegram_backup_YYYYMMDD_HHMMSS_<shortid>.tar.gz[.enc]
|
||||
#
|
||||
# Encryption: AES-256-CBC + PBKDF2 (optional, password-based)
|
||||
# Integrity: SHA-256 sidecar file (<archive>.sha256)
|
||||
#
|
||||
# Restore path automatically detects v1.1 (legacy) vs v2.0 layouts by reading
|
||||
# metadata.json.backup_version. When restoring a v1.1 archive the script
|
||||
# immediately writes a fresh v2.0 backup alongside the old one, so subsequent
|
||||
# reinstalls can benefit from the new format.
|
||||
|
||||
# ── Создание бекапа ──────────────────────────────────────────────────────────
|
||||
# ── Utility: generate a backup ID from a raw secret ─────────────────────────
|
||||
# Format: GT-YYMMDD-<last6hex>. Deterministic per-day per-key; easy to read.
|
||||
generate_backup_id() {
|
||||
local raw_secret="$1"
|
||||
local date_part
|
||||
date_part=$(date +%y%m%d)
|
||||
local last6="000000"
|
||||
if [ -n "$raw_secret" ] && [ ${#raw_secret} -ge 6 ]; then
|
||||
last6="${raw_secret: -6}"
|
||||
last6=$(echo "$last6" | tr 'A-F' 'a-f')
|
||||
fi
|
||||
echo "GT-${date_part}-${last6}"
|
||||
}
|
||||
|
||||
# ── Utility: SHA-256 fingerprint of a raw secret ────────────────────────────
|
||||
secret_fingerprint() {
|
||||
local raw_secret="$1"
|
||||
[ -z "$raw_secret" ] && { echo ""; return; }
|
||||
printf '%s' "$raw_secret" | openssl dgst -sha256 2>/dev/null | awk '{print $NF}'
|
||||
}
|
||||
|
||||
# ── Utility: hex-encode an ASCII string (for fake-TLS secret) ───────────────
|
||||
_hex_encode() {
|
||||
printf '%s' "$1" | xxd -p | tr -d '\n'
|
||||
}
|
||||
|
||||
# ── Создание бекапа (UBF v2.0) ──────────────────────────────────────────────
|
||||
create_backup() {
|
||||
local password="$1"
|
||||
local password="${1:-}"
|
||||
local output_dir="${2:-$BACKUP_DIR}"
|
||||
|
||||
# Pull current config (so backup_id can include the real secret)
|
||||
local raw_secret domain mode engine port lang tpl_id mask_host
|
||||
raw_secret=$(config_get secret 2>/dev/null || echo "")
|
||||
domain=$(config_get domain 2>/dev/null || echo "")
|
||||
mode=$(config_get mode 2>/dev/null || echo "unknown")
|
||||
engine=$(config_get engine 2>/dev/null || echo "telemt")
|
||||
port=$(config_get port 2>/dev/null || echo "443")
|
||||
lang=$(type get_language &>/dev/null && get_language 2>/dev/null || echo "en")
|
||||
tpl_id=$(config_get template_id 2>/dev/null || echo "")
|
||||
mask_host=$(config_get mask_host 2>/dev/null || echo "")
|
||||
|
||||
# Sanitise port
|
||||
[[ "$port" =~ ^[0-9]+$ ]] || port=443
|
||||
|
||||
# Backup id / short id (last 6 of secret, or random if unknown)
|
||||
local backup_id short_id
|
||||
backup_id=$(generate_backup_id "$raw_secret")
|
||||
short_id="${backup_id##*-}"
|
||||
|
||||
local timestamp
|
||||
timestamp=$(date +%Y%m%d_%H%M%S)
|
||||
local backup_name="gotelegram_backup_${timestamp}"
|
||||
local backup_name="gotelegram_backup_${timestamp}_${short_id}"
|
||||
local tmp_dir="/tmp/${backup_name}"
|
||||
|
||||
mkdir -p "$tmp_dir" "$output_dir"
|
||||
|
||||
# Собираем файлы
|
||||
log_info "$(_t_or backup_collecting 'Собираю конфигурацию...')"
|
||||
|
||||
# telemt конфиг
|
||||
# ── telemt ──
|
||||
if [ -f "$TELEMT_CONFIG" ]; then
|
||||
cp "$TELEMT_CONFIG" "$tmp_dir/config.toml"
|
||||
mkdir -p "$tmp_dir/telemt"
|
||||
cp "$TELEMT_CONFIG" "$tmp_dir/telemt/config.toml"
|
||||
fi
|
||||
|
||||
# GoTelegram конфиг
|
||||
# ── gotelegram ──
|
||||
mkdir -p "$tmp_dir/gotelegram"
|
||||
if [ -f "$GOTELEGRAM_CONFIG" ]; then
|
||||
cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram.json"
|
||||
cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram/config.json"
|
||||
fi
|
||||
|
||||
# Language marker (i18n)
|
||||
if [ -f "$GOTELEGRAM_DIR/.language" ]; then
|
||||
cp "$GOTELEGRAM_DIR/.language" "$tmp_dir/.language"
|
||||
cp "$GOTELEGRAM_DIR/.language" "$tmp_dir/gotelegram/.language"
|
||||
fi
|
||||
|
||||
# nginx конфиг (stealth mode)
|
||||
# ── nginx ──
|
||||
if [ -f "$NGINX_SITE_CONF" ]; then
|
||||
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx.conf"
|
||||
mkdir -p "$tmp_dir/nginx"
|
||||
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx/site.conf"
|
||||
fi
|
||||
|
||||
# SSL сертификаты
|
||||
local domain
|
||||
domain=$(config_get domain 2>/dev/null)
|
||||
# ── Let's Encrypt (full tree: live/<d>/*.pem + renewal/<d>.conf) ──
|
||||
if [ -n "$domain" ] && [ -d "/etc/letsencrypt/live/$domain" ]; then
|
||||
mkdir -p "$tmp_dir/certs"
|
||||
cp "/etc/letsencrypt/live/$domain/fullchain.pem" "$tmp_dir/certs/" 2>/dev/null
|
||||
cp "/etc/letsencrypt/live/$domain/privkey.pem" "$tmp_dir/certs/" 2>/dev/null
|
||||
log_dim "SSL сертификаты включены"
|
||||
mkdir -p "$tmp_dir/letsencrypt/live/$domain"
|
||||
# Follow symlinks — letsencrypt's live/ tree is symlinks into archive/
|
||||
cp -L "/etc/letsencrypt/live/$domain/"*.pem "$tmp_dir/letsencrypt/live/$domain/" 2>/dev/null
|
||||
if [ -f "/etc/letsencrypt/renewal/${domain}.conf" ]; then
|
||||
mkdir -p "$tmp_dir/letsencrypt/renewal"
|
||||
cp "/etc/letsencrypt/renewal/${domain}.conf" "$tmp_dir/letsencrypt/renewal/"
|
||||
fi
|
||||
log_dim "$(_t_or backup_ssl_included 'SSL-сертификаты включены (+ chain + renewal)')"
|
||||
fi
|
||||
|
||||
# Шаблон сайта (если есть)
|
||||
# ── Website template ──
|
||||
if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ]; then
|
||||
mkdir -p "$tmp_dir/site"
|
||||
cp -r "$WEBSITE_ROOT"/* "$tmp_dir/site/"
|
||||
cp -r "$WEBSITE_ROOT"/* "$tmp_dir/site/" 2>/dev/null
|
||||
log_dim "$(_t_or backup_site_included 'Шаблон сайта включён')"
|
||||
fi
|
||||
|
||||
# Метаданные
|
||||
local ip mode engine lang port domain
|
||||
# ── Telegram bot ──
|
||||
if [ -f "$BOT_DIR/.env" ]; then
|
||||
mkdir -p "$tmp_dir/bot"
|
||||
cp "$BOT_DIR/.env" "$tmp_dir/bot/.env"
|
||||
chmod 600 "$tmp_dir/bot/.env" 2>/dev/null
|
||||
log_dim "$(_t_or backup_bot_included 'Конфиг Telegram-бота включён')"
|
||||
fi
|
||||
|
||||
# ── secrets.json ──
|
||||
local faketls_secret="" proxy_link="" bot_token=""
|
||||
if [ -n "$raw_secret" ] && [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||
faketls_secret="ee${raw_secret}$(_hex_encode "$domain")"
|
||||
fi
|
||||
if type generate_proxy_link &>/dev/null; then
|
||||
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||
proxy_link=$(generate_proxy_link "$domain" "$port" "$raw_secret" "$domain" 2>/dev/null || echo "")
|
||||
elif [ -n "$raw_secret" ]; then
|
||||
local ip
|
||||
ip=$(get_server_ip)
|
||||
proxy_link=$(generate_proxy_link "$ip" "$port" "$raw_secret" "$mask_host" 2>/dev/null || echo "")
|
||||
fi
|
||||
fi
|
||||
if [ -f "$BOT_DIR/.env" ]; then
|
||||
bot_token=$(grep -E '^BOT_TOKEN=' "$BOT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2-)
|
||||
bot_token="${bot_token%\"}"
|
||||
bot_token="${bot_token#\"}"
|
||||
fi
|
||||
|
||||
cat > "$tmp_dir/secrets.json" << EOSEC
|
||||
{
|
||||
"version": "1",
|
||||
"raw_secret": "${raw_secret}",
|
||||
"faketls_secret": "${faketls_secret}",
|
||||
"proxy_link": "${proxy_link}",
|
||||
"bot_token": "${bot_token}",
|
||||
"exported_at": "$(date -Iseconds)"
|
||||
}
|
||||
EOSEC
|
||||
chmod 600 "$tmp_dir/secrets.json"
|
||||
|
||||
# ── metadata.json v2.0 ──
|
||||
local ip fingerprint
|
||||
ip=$(get_server_ip)
|
||||
mode=$(config_get mode 2>/dev/null || echo "unknown")
|
||||
engine=$(config_get engine 2>/dev/null || echo "telemt")
|
||||
lang=$(type get_language &>/dev/null && get_language 2>/dev/null || echo "en")
|
||||
port=$(config_get port 2>/dev/null || echo "443")
|
||||
# Ensure port is numeric; fall back to 443 if garbage
|
||||
[[ "$port" =~ ^[0-9]+$ ]] || port=443
|
||||
domain=$(config_get domain 2>/dev/null || echo "")
|
||||
fingerprint=$(secret_fingerprint "$raw_secret")
|
||||
|
||||
cat > "$tmp_dir/metadata.json" << EOMETA
|
||||
{
|
||||
"backup_version": "1.1",
|
||||
"backup_version": "2.0",
|
||||
"backup_id": "${backup_id}",
|
||||
"gotelegram_version": "$GOTELEGRAM_VERSION",
|
||||
"created_at": "$(date -Iseconds)",
|
||||
"hostname": "$(hostname)",
|
||||
@@ -74,34 +182,37 @@ create_backup() {
|
||||
"mode": "$mode",
|
||||
"language": "$lang",
|
||||
"port": $port,
|
||||
"domain": "$domain"
|
||||
"domain": "$domain",
|
||||
"template_id": "$tpl_id",
|
||||
"mask_host": "$mask_host",
|
||||
"secret_fingerprint_sha256": "$fingerprint",
|
||||
"has_secrets": true,
|
||||
"has_letsencrypt": $([ -d "$tmp_dir/letsencrypt" ] && echo true || echo false),
|
||||
"has_site": $([ -d "$tmp_dir/site" ] && echo true || echo false),
|
||||
"has_bot": $([ -d "$tmp_dir/bot" ] && echo true || echo false)
|
||||
}
|
||||
EOMETA
|
||||
|
||||
# Архивируем
|
||||
# ── Archive ──
|
||||
local tar_file="/tmp/${backup_name}.tar.gz"
|
||||
if ! tar czf "$tar_file" -C /tmp "$backup_name" 2>/dev/null; then
|
||||
log_error "$(_t_or backup_archive_err 'Ошибка создания архива')"
|
||||
rm -rf "$tmp_dir"
|
||||
rm -f "$tar_file"
|
||||
rm -rf "$tmp_dir"; rm -f "$tar_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$tar_file" ]; then
|
||||
log_error "$(_t_or backup_archive_missing 'Архив не создан')"
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Шифруем если задан пароль
|
||||
# ── Encrypt (optional) ──
|
||||
local final_file=""
|
||||
if [ -n "$password" ]; then
|
||||
final_file="${output_dir}/${backup_name}.tar.gz.enc"
|
||||
openssl enc -aes-256-cbc -salt -pbkdf2 -in "$tar_file" -out "$final_file" -pass "pass:${password}" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
if ! openssl enc -aes-256-cbc -salt -pbkdf2 -in "$tar_file" -out "$final_file" -pass "pass:${password}" 2>/dev/null; then
|
||||
log_error "$(_t_or backup_encrypt_err 'Ошибка шифрования')"
|
||||
rm -f "$tar_file"
|
||||
rm -rf "$tmp_dir"
|
||||
rm -f "$tar_file"; rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
rm -f "$tar_file"
|
||||
@@ -111,27 +222,36 @@ EOMETA
|
||||
mv "$tar_file" "$final_file"
|
||||
fi
|
||||
|
||||
# SHA256 подпись
|
||||
# SHA-256 sidecar
|
||||
sha256sum "$final_file" > "${final_file}.sha256" 2>/dev/null
|
||||
|
||||
# Очистка
|
||||
# Cleanup
|
||||
rm -rf "$tmp_dir"
|
||||
|
||||
local size
|
||||
size=$(du -h "$final_file" | cut -f1)
|
||||
if type tf &>/dev/null; then
|
||||
log_success "$(tf backup_created_fmt "$final_file" "$size")"
|
||||
else
|
||||
log_success "Бекап создан: $final_file ($size)"
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${GREEN}✓ $(_t_or backup_created 'Бекап создан')${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_id_label 'Backup ID'):${NC} ${BOLD}${CYAN}${backup_id}${NC}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_file_label 'Файл'):${NC} ${final_file}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_size_label 'Размер'):${NC} ${size}" >&2
|
||||
if [ -n "$raw_secret" ]; then
|
||||
echo -e " ${WHITE}$(_t_or backup_key_label 'Ключ в бекапе (fingerprint)'):${NC} ${DIM}${fingerprint:0:32}...${NC}" >&2
|
||||
fi
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
# Only the final file path on stdout (callers capture it)
|
||||
echo "$final_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Восстановление из бекапа ────────────────────────────────────────────────
|
||||
# ── Восстановление из бекапа (auto-detect v1.1 vs v2.0) ─────────────────────
|
||||
restore_backup() {
|
||||
local backup_file="$1"
|
||||
local password="$2"
|
||||
local password="${2:-}"
|
||||
|
||||
if [ ! -f "$backup_file" ]; then
|
||||
if type tf &>/dev/null; then
|
||||
@@ -145,17 +265,16 @@ restore_backup() {
|
||||
local tmp_dir="/tmp/gotelegram_restore_$$"
|
||||
mkdir -p "$tmp_dir"
|
||||
|
||||
# Расшифровываем если нужно
|
||||
# ── Decrypt if needed ──
|
||||
local tar_file=""
|
||||
if echo "$backup_file" | grep -q '\.enc$'; then
|
||||
if [ -z "$password" ]; then
|
||||
echo -ne " $(_t_or backup_enter_pass 'Введите пароль от бекапа'): "
|
||||
echo -ne " $(_t_or backup_enter_pass 'Введите пароль от бекапа'): " >&2
|
||||
read -rs password
|
||||
echo ""
|
||||
echo "" >&2
|
||||
fi
|
||||
tar_file="/tmp/gotelegram_restore_$$.tar.gz"
|
||||
openssl enc -aes-256-cbc -d -pbkdf2 -in "$backup_file" -out "$tar_file" -pass "pass:${password}" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
if ! openssl enc -aes-256-cbc -d -pbkdf2 -in "$backup_file" -out "$tar_file" -pass "pass:${password}" 2>/dev/null; then
|
||||
log_error "$(_t_or backup_bad_pass 'Неверный пароль или повреждённый файл')"
|
||||
rm -rf "$tmp_dir" "$tar_file"
|
||||
return 1
|
||||
@@ -164,105 +283,155 @@ restore_backup() {
|
||||
tar_file="$backup_file"
|
||||
fi
|
||||
|
||||
# Распаковываем
|
||||
tar xzf "$tar_file" -C "$tmp_dir" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
# ── Extract ──
|
||||
if ! tar xzf "$tar_file" -C "$tmp_dir" 2>/dev/null; then
|
||||
log_error "$(_t_or backup_extract_err 'Ошибка распаковки архива')"
|
||||
rm -rf "$tmp_dir"
|
||||
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Находим папку бекапа
|
||||
# Find the single top-level dir inside the archive
|
||||
local backup_dir
|
||||
backup_dir=$(find "$tmp_dir" -maxdepth 1 -type d -name "gotelegram_backup_*" | head -1)
|
||||
backup_dir=$(find "$tmp_dir" -maxdepth 1 -mindepth 1 -type d -name "gotelegram_backup_*" | head -1)
|
||||
[ -z "$backup_dir" ] && backup_dir="$tmp_dir"
|
||||
|
||||
# Проверяем метаданные
|
||||
# ── Parse metadata.json ──
|
||||
local bk_version="1.1" bk_id="" bk_mode="" bk_domain="" bk_ip="" bk_lang="" bk_date=""
|
||||
if [ -f "$backup_dir/metadata.json" ]; then
|
||||
local bk_version bk_mode bk_ip bk_lang bk_date
|
||||
bk_version=$(jq -r '.gotelegram_version // "unknown"' "$backup_dir/metadata.json")
|
||||
bk_version=$(jq -r '.backup_version // "1.1"' "$backup_dir/metadata.json")
|
||||
bk_id=$(jq -r '.backup_id // empty' "$backup_dir/metadata.json")
|
||||
bk_mode=$(jq -r '.mode // "unknown"' "$backup_dir/metadata.json")
|
||||
bk_ip=$(jq -r '.ip // "unknown"' "$backup_dir/metadata.json")
|
||||
bk_domain=$(jq -r '.domain // empty' "$backup_dir/metadata.json")
|
||||
bk_ip=$(jq -r '.ip // "-"' "$backup_dir/metadata.json")
|
||||
bk_lang=$(jq -r '.language // "-"' "$backup_dir/metadata.json")
|
||||
bk_date=$(jq -r '.created_at // "-"' "$backup_dir/metadata.json")
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_label 'Бекап'):${NC}"
|
||||
echo -e " $(_t_or backup_version_label 'Версия'): $bk_version | $(_t_or backup_mode_label 'Режим'): $bk_mode | IP: $bk_ip | $(_t_or backup_lang_label 'Язык'): $bk_lang"
|
||||
echo -e " $(_t_or backup_date_label 'Дата'): $bk_date"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_label 'Бекап'):${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
if [ -n "$bk_id" ]; then
|
||||
echo -e " ${WHITE}$(_t_or backup_id_label 'Backup ID'):${NC} ${BOLD}${CYAN}${bk_id}${NC}" >&2
|
||||
fi
|
||||
echo -e " ${WHITE}$(_t_or backup_format_label 'Формат'):${NC} UBF ${bk_version}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_mode_label 'Режим'):${NC} ${bk_mode}${bk_domain:+ | $bk_domain}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_lang_label 'Язык'):${NC} ${bk_lang} | IP: ${bk_ip}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_date_label 'Дата'):${NC} ${bk_date}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
if ! confirm "$(_t_or backup_confirm_restore 'Восстановить конфигурацию? Текущие настройки будут перезаписаны.')"; then
|
||||
rm -rf "$tmp_dir"
|
||||
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Останавливаем сервисы
|
||||
stop_telemt 2>/dev/null
|
||||
systemctl stop nginx 2>/dev/null
|
||||
|
||||
# Восстанавливаем telemt конфиг
|
||||
if [ -f "$backup_dir/config.toml" ]; then
|
||||
# ── Detect layout ──
|
||||
# v2.0 paths: telemt/config.toml, gotelegram/config.json, nginx/site.conf, letsencrypt/live/<d>/
|
||||
# v1.1 paths: config.toml, gotelegram.json, nginx.conf, certs/
|
||||
local src_telemt src_gt src_lang src_nginx src_le_live src_le_renewal src_site src_bot
|
||||
if [ "$bk_version" = "2.0" ] || [ -d "$backup_dir/telemt" ]; then
|
||||
src_telemt="$backup_dir/telemt/config.toml"
|
||||
src_gt="$backup_dir/gotelegram/config.json"
|
||||
src_lang="$backup_dir/gotelegram/.language"
|
||||
src_nginx="$backup_dir/nginx/site.conf"
|
||||
src_le_live=$(find "$backup_dir/letsencrypt/live" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | head -1)
|
||||
src_le_renewal="$backup_dir/letsencrypt/renewal"
|
||||
src_site="$backup_dir/site"
|
||||
src_bot="$backup_dir/bot/.env"
|
||||
else
|
||||
src_telemt="$backup_dir/config.toml"
|
||||
src_gt="$backup_dir/gotelegram.json"
|
||||
src_lang="$backup_dir/.language"
|
||||
src_nginx="$backup_dir/nginx.conf"
|
||||
src_le_live="$backup_dir/certs" # v1.1 dumps certs flat
|
||||
src_le_renewal=""
|
||||
src_site="$backup_dir/site"
|
||||
src_bot="" # v1.1 never backed up bot
|
||||
fi
|
||||
|
||||
# ── telemt config ──
|
||||
if [ -f "$src_telemt" ]; then
|
||||
mkdir -p /etc/telemt
|
||||
cp "$backup_dir/config.toml" "$TELEMT_CONFIG"
|
||||
cp "$src_telemt" "$TELEMT_CONFIG"
|
||||
chmod 600 "$TELEMT_CONFIG"
|
||||
log_success "$(_t_or backup_restored_telemt 'telemt конфиг восстановлен')"
|
||||
fi
|
||||
|
||||
# Восстанавливаем GoTelegram конфиг
|
||||
if [ -f "$backup_dir/gotelegram.json" ]; then
|
||||
# ── GoTelegram config ──
|
||||
if [ -f "$src_gt" ]; then
|
||||
mkdir -p "$GOTELEGRAM_DIR"
|
||||
cp "$backup_dir/gotelegram.json" "$GOTELEGRAM_CONFIG"
|
||||
cp "$src_gt" "$GOTELEGRAM_CONFIG"
|
||||
log_success "$(_t_or backup_restored_gotelegram 'GoTelegram конфиг восстановлен')"
|
||||
fi
|
||||
|
||||
# Восстанавливаем language marker (i18n)
|
||||
if [ -f "$backup_dir/.language" ]; then
|
||||
# ── Language ──
|
||||
if [ -f "$src_lang" ]; then
|
||||
mkdir -p "$GOTELEGRAM_DIR"
|
||||
cp "$backup_dir/.language" "$GOTELEGRAM_DIR/.language"
|
||||
cp "$src_lang" "$GOTELEGRAM_DIR/.language"
|
||||
log_success "$(_t_or backup_restored_lang 'Язык интерфейса восстановлен')"
|
||||
fi
|
||||
|
||||
# Восстанавливаем nginx конфиг
|
||||
if [ -f "$backup_dir/nginx.conf" ]; then
|
||||
# ── nginx ──
|
||||
if [ -f "$src_nginx" ]; then
|
||||
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
|
||||
cp "$backup_dir/nginx.conf" "$NGINX_SITE_CONF"
|
||||
cp "$src_nginx" "$NGINX_SITE_CONF"
|
||||
ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
|
||||
log_success "$(_t_or backup_restored_nginx 'nginx конфиг восстановлен')"
|
||||
fi
|
||||
|
||||
# Восстанавливаем SSL
|
||||
if [ -d "$backup_dir/certs" ]; then
|
||||
local domain
|
||||
domain=$(config_get domain 2>/dev/null)
|
||||
if [ -n "$domain" ]; then
|
||||
local cert_dir="/etc/letsencrypt/live/$domain"
|
||||
mkdir -p "$cert_dir"
|
||||
cp "$backup_dir/certs/"* "$cert_dir/" 2>/dev/null
|
||||
log_success "$(_t_or backup_restored_ssl 'SSL сертификаты восстановлены')"
|
||||
# ── Let's Encrypt (v2.0: full tree; v1.1: just flat certs/) ──
|
||||
if [ -n "$bk_domain" ] && [ -d "$src_le_live" ]; then
|
||||
local live_dir="/etc/letsencrypt/live/$bk_domain"
|
||||
mkdir -p "$live_dir"
|
||||
cp "$src_le_live/"*.pem "$live_dir/" 2>/dev/null
|
||||
if [ -n "$src_le_renewal" ] && [ -f "$src_le_renewal/${bk_domain}.conf" ]; then
|
||||
mkdir -p /etc/letsencrypt/renewal
|
||||
cp "$src_le_renewal/${bk_domain}.conf" "/etc/letsencrypt/renewal/"
|
||||
fi
|
||||
log_success "$(_t_or backup_restored_ssl 'SSL сертификаты восстановлены')"
|
||||
fi
|
||||
|
||||
# Восстанавливаем шаблон сайта
|
||||
if [ -d "$backup_dir/site" ]; then
|
||||
# ── Site ──
|
||||
if [ -d "$src_site" ] && [ -f "$src_site/index.html" ]; then
|
||||
mkdir -p "$WEBSITE_ROOT"
|
||||
cp -r "$backup_dir/site"/* "$WEBSITE_ROOT/"
|
||||
cp -r "$src_site"/* "$WEBSITE_ROOT/"
|
||||
chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null
|
||||
log_success "$(_t_or backup_restored_site 'Шаблон сайта восстановлен')"
|
||||
fi
|
||||
|
||||
# Запускаем сервисы
|
||||
if is_telemt_installed; then
|
||||
# ── Bot .env (v2.0 only) ──
|
||||
if [ -n "$src_bot" ] && [ -f "$src_bot" ]; then
|
||||
mkdir -p "$BOT_DIR"
|
||||
cp "$src_bot" "$BOT_DIR/.env"
|
||||
chmod 600 "$BOT_DIR/.env"
|
||||
log_success "$(_t_or backup_restored_bot 'Конфиг Telegram-бота восстановлен')"
|
||||
fi
|
||||
|
||||
# ── Start services ──
|
||||
if type is_telemt_installed &>/dev/null && is_telemt_installed; then
|
||||
start_telemt
|
||||
fi
|
||||
systemctl start nginx 2>/dev/null
|
||||
|
||||
# Очистка
|
||||
# ── Cleanup ──
|
||||
rm -rf "$tmp_dir"
|
||||
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
|
||||
|
||||
log_success "$(_t_or backup_restore_done 'Восстановление завершено!')"
|
||||
show_proxy_info
|
||||
|
||||
# ── Auto-migrate v1.1 → v2.0 ──
|
||||
if [ "$bk_version" != "2.0" ]; then
|
||||
log_info "$(_t_or backup_automigrate 'Конвертирую старый бекап в UBF v2.0...')"
|
||||
create_backup "" >/dev/null 2>&1 && \
|
||||
log_success "$(_t_or backup_migrated 'Свежий UBF v2.0 бекап сохранён в $BACKUP_DIR')"
|
||||
fi
|
||||
|
||||
type show_proxy_info &>/dev/null && show_proxy_info
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -273,24 +442,26 @@ list_backups() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_list_title 'Доступные бекапы'):${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_list_title 'Доступные бекапы'):${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..70})${NC}" >&2
|
||||
|
||||
local i=1
|
||||
for f in "$BACKUP_DIR"/gotelegram_backup_*.tar.gz*; do
|
||||
[ -f "$f" ] || continue
|
||||
[[ "$f" == *.sha256 ]] && continue
|
||||
local size date_str name
|
||||
local size name date_str id_tail encrypted=""
|
||||
size=$(du -h "$f" | cut -f1)
|
||||
name=$(basename "$f")
|
||||
date_str=$(echo "$name" | grep -oE '[0-9]{8}_[0-9]{6}' | head -1)
|
||||
local encrypted=""
|
||||
id_tail=$(echo "$name" | grep -oE '_[0-9a-f]{6}\.tar' | head -1 | tr -d '_.tar')
|
||||
[[ "$f" == *.enc ]] && encrypted=" 🔒"
|
||||
echo -e " ${CYAN}${i})${NC} ${name} (${size})${encrypted}"
|
||||
local id_display=""
|
||||
[ -n "$id_tail" ] && id_display=" ${DIM}[...${id_tail}]${NC}"
|
||||
echo -e " ${CYAN}${i})${NC} ${name} (${size})${encrypted}${id_display}" >&2
|
||||
((i++))
|
||||
done
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..70})${NC}" >&2
|
||||
}
|
||||
|
||||
# ── Очистка старых бекапов ───────────────────────────────────────────────────
|
||||
@@ -337,7 +508,7 @@ interactive_backup() {
|
||||
fi
|
||||
fi
|
||||
|
||||
create_backup "$password"
|
||||
create_backup "$password" >/dev/null
|
||||
cleanup_old_backups
|
||||
}
|
||||
|
||||
@@ -371,3 +542,115 @@ interactive_restore() {
|
||||
|
||||
restore_backup "$backup_file"
|
||||
}
|
||||
|
||||
# ── Manual secret recovery (v2.4.9) ──────────────────────────────────────────
|
||||
# Parser accepts any of the 3 formats and emits key=value lines on stdout:
|
||||
# tg://proxy?server=X&port=Y&secret=Z → raw_secret, server, port, domain (if ee-prefix)
|
||||
# ee<32hex><hex_domain> → raw_secret, domain
|
||||
# <32hex> → raw_secret only
|
||||
# Returns 0 on success, 1 on parse failure.
|
||||
parse_manual_secret() {
|
||||
local input="$1"
|
||||
input=$(echo "$input" | tr -d ' \t\n\r')
|
||||
[ -z "$input" ] && return 1
|
||||
|
||||
local raw_secret="" domain="" server="" port=""
|
||||
|
||||
if echo "$input" | grep -q '^tg://proxy?'; then
|
||||
local qs="${input#tg://proxy?}"
|
||||
local kv k v
|
||||
local -a kvs
|
||||
IFS='&' read -ra kvs <<< "$qs"
|
||||
for kv in "${kvs[@]}"; do
|
||||
k="${kv%%=*}"
|
||||
v="${kv#*=}"
|
||||
case "$k" in
|
||||
server) server="$v" ;;
|
||||
port) port="$v" ;;
|
||||
secret) raw_secret="$v" ;;
|
||||
esac
|
||||
done
|
||||
[ -z "$raw_secret" ] && return 1
|
||||
# Strip hex-escapes that Telegram sometimes URL-encodes
|
||||
raw_secret=$(echo "$raw_secret" | tr -d '%')
|
||||
fi
|
||||
|
||||
# After pulling from URL (if any), raw_secret might still be ee-prefixed.
|
||||
# Otherwise, try the raw_secret as the whole input.
|
||||
local candidate="${raw_secret:-$input}"
|
||||
|
||||
if [[ "$candidate" =~ ^[eE][eE][0-9a-fA-F]{32}[0-9a-fA-F]*$ ]]; then
|
||||
raw_secret="${candidate:2:32}"
|
||||
local hex_domain="${candidate:34}"
|
||||
if [ -n "$hex_domain" ]; then
|
||||
local decoded
|
||||
decoded=$(echo "$hex_domain" | xxd -r -p 2>/dev/null)
|
||||
# Validate decoded looks like a domain
|
||||
if echo "$decoded" | grep -qE '^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; then
|
||||
domain="$decoded"
|
||||
fi
|
||||
fi
|
||||
elif [[ "$candidate" =~ ^[0-9a-fA-F]{32}$ ]]; then
|
||||
raw_secret="$candidate"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
|
||||
[[ "$raw_secret" =~ ^[0-9a-fA-F]{32}$ ]] || return 1
|
||||
raw_secret=$(echo "$raw_secret" | tr 'A-F' 'a-f')
|
||||
|
||||
echo "raw_secret=$raw_secret"
|
||||
[ -n "$domain" ] && echo "domain=$domain"
|
||||
[ -n "$server" ] && echo "server=$server"
|
||||
[ -n "$port" ] && echo "port=$port"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Interactive: user types their old key, we parse it ─────────────────────
|
||||
manual_secret_input() {
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}🔑 $(_t_or manual_secret_title 'Ввод существующего ключа')${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo -e " ${DIM}$(_t_or manual_secret_help1 'Поддерживаются форматы:')${NC}" >&2
|
||||
echo -e " ${DIM} • tg://proxy?server=...&port=...&secret=...${NC}" >&2
|
||||
echo -e " ${DIM} • ee<32hex><hexdomain> (fake-TLS)${NC}" >&2
|
||||
echo -e " ${DIM} • 32hex (только raw secret)${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo -ne " ${WHITE}$(_t_or manual_secret_prompt 'Вставьте ключ'):${NC} " >&2
|
||||
read -r user_input
|
||||
|
||||
if [ -z "$user_input" ]; then
|
||||
log_error "$(_t_or manual_secret_empty 'Ключ не введён')"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local parsed
|
||||
if ! parsed=$(parse_manual_secret "$user_input"); then
|
||||
log_error "$(_t_or manual_secret_bad 'Не удалось распознать формат ключа')"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local p_raw="" p_domain="" p_server="" p_port=""
|
||||
while IFS='=' read -r k v; do
|
||||
case "$k" in
|
||||
raw_secret) p_raw="$v" ;;
|
||||
domain) p_domain="$v" ;;
|
||||
server) p_server="$v" ;;
|
||||
port) p_port="$v" ;;
|
||||
esac
|
||||
done <<< "$parsed"
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${GREEN}✓ $(_t_or manual_secret_parsed 'Ключ распознан')${NC}" >&2
|
||||
echo -e " ${WHITE}raw_secret:${NC} ${DIM}${p_raw:0:8}...${p_raw: -4}${NC}" >&2
|
||||
[ -n "$p_domain" ] && echo -e " ${WHITE}domain:${NC} ${CYAN}${p_domain}${NC}" >&2
|
||||
[ -n "$p_server" ] && echo -e " ${WHITE}server:${NC} ${CYAN}${p_server}${NC}" >&2
|
||||
[ -n "$p_port" ] && echo -e " ${WHITE}port:${NC} ${CYAN}${p_port}${NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
# Export for the subsequent install flow to pick up
|
||||
export GOTELEGRAM_EXISTING_SECRET="$p_raw"
|
||||
export GOTELEGRAM_EXISTING_DOMAIN="$p_domain"
|
||||
export GOTELEGRAM_EXISTING_PORT="$p_port"
|
||||
return 0
|
||||
}
|
||||
|
||||
163
lib/common.sh
Executable file → Normal file
163
lib/common.sh
Executable file → Normal file
@@ -3,7 +3,7 @@
|
||||
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
|
||||
|
||||
# ── Version ───────────────────────────────────────────────────────────────────
|
||||
GOTELEGRAM_VERSION="2.4.6"
|
||||
GOTELEGRAM_VERSION="2.4.10"
|
||||
GOTELEGRAM_NAME="GoTelegram"
|
||||
|
||||
# ── Пути ──────────────────────────────────────────────────────────────────────
|
||||
@@ -463,6 +463,167 @@ check_port() {
|
||||
return 1 # свободен
|
||||
}
|
||||
|
||||
# ── Preflight: port conflict detection ───────────────────────────────────────
|
||||
# Проверяет, что нужные для установки порты свободны. Если порт занят —
|
||||
# определяет процесс и сопоставляет с известным списком proxy/VPN софта
|
||||
# (xray, sing-box, v2ray, trojan, hysteria, mtg, shadowsocks, x-ui/3x-ui,
|
||||
# marzban, amneziawg, caddy, apache, haproxy). Пользователь видит явное
|
||||
# предупреждение и может либо прервать установку, либо продолжить на свой
|
||||
# страх и риск (GOTELEGRAM_SKIP_PREFLIGHT=1 — полностью отключить проверку).
|
||||
#
|
||||
# Используемые порты GoTelegram:
|
||||
# 443 — telemt (внешний, MTProxy + fake-TLS) — lite и pro
|
||||
# 80 — nginx redirect + certbot ACME HTTP-01 — только pro
|
||||
# 8443 — nginx internal mask (127.0.0.1:8443) — только pro
|
||||
|
||||
# get_port_process <port> → "<pid>|<comm>" если занят, иначе пусто
|
||||
get_port_process() {
|
||||
local port="$1"
|
||||
local line="" pid="" proc=""
|
||||
line=$(ss -tlnp 2>/dev/null | grep -E ":${port}[[:space:]]" | head -1)
|
||||
if [ -z "$line" ]; then
|
||||
line=$(netstat -tlnp 2>/dev/null | grep -E ":${port}[[:space:]]" | head -1)
|
||||
fi
|
||||
if [ -n "$line" ]; then
|
||||
pid=$(echo "$line" | grep -oE 'pid=[0-9]+' | head -1 | cut -d= -f2)
|
||||
if [ -z "$pid" ]; then
|
||||
# netstat format: "12345/procname"
|
||||
pid=$(echo "$line" | grep -oE '[0-9]+/[^ ]+' | head -1 | cut -d/ -f1)
|
||||
fi
|
||||
fi
|
||||
if [ -z "$pid" ]; then
|
||||
pid=$(fuser -n tcp "$port" 2>/dev/null | tr -s ' ' | awk '{print $1}' | head -1)
|
||||
pid="${pid:-}"
|
||||
fi
|
||||
if [ -n "$pid" ] && [ "$pid" -gt 0 ] 2>/dev/null; then
|
||||
proc=$(ps -p "$pid" -o comm= 2>/dev/null | tr -d ' \n')
|
||||
[ -z "$proc" ] && proc="unknown"
|
||||
echo "${pid}|${proc}"
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$line" ]; then
|
||||
# Port is occupied but process cannot be identified (kernel socket / no root)
|
||||
echo "0|unknown"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# match_known_conflict <comm> → печатает человекочитаемое имя если это
|
||||
# известный proxy/VPN/web софт. Возвращает 0 если нашли, 1 иначе.
|
||||
match_known_conflict() {
|
||||
local proc="$1"
|
||||
case "$proc" in
|
||||
*xray*|*Xray*) echo "Xray"; return 0 ;;
|
||||
*sing-box*|*sing_box*|*singbox*) echo "sing-box"; return 0 ;;
|
||||
*v2ray*|*V2Ray*) echo "V2Ray"; return 0 ;;
|
||||
*trojan*) echo "Trojan"; return 0 ;;
|
||||
*hysteria*) echo "Hysteria"; return 0 ;;
|
||||
*mtg*) echo "mtg (old MTProxy)"; return 0 ;;
|
||||
*ss-server*|*ss-local*|*shadowsocks*|*ssserver*) echo "Shadowsocks"; return 0 ;;
|
||||
*x-ui*|*3x-ui*|*xui*) echo "x-ui / 3x-ui panel"; return 0 ;;
|
||||
*marzban*) echo "Marzban panel"; return 0 ;;
|
||||
*amneziawg*|*awg-go*|*awg*) echo "AmneziaWG"; return 0 ;;
|
||||
*caddy*) echo "Caddy web server"; return 0 ;;
|
||||
*apache2*|*httpd*) echo "Apache httpd"; return 0 ;;
|
||||
*haproxy*) echo "HAProxy"; return 0 ;;
|
||||
*nginx*) echo "nginx (already running)"; return 0 ;;
|
||||
*tgproxy*|*mtproxy*|*mtproto*) echo "MTProto Proxy (other impl)"; return 0 ;;
|
||||
*wireguard*|*wg-quick*) echo "WireGuard"; return 0 ;;
|
||||
*openvpn*) echo "OpenVPN"; return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
# preflight_check <mode> [port]
|
||||
# mode = "lite" | "pro"
|
||||
# port = selected port for lite mode (default 443)
|
||||
# Returns:
|
||||
# 0 — OK to proceed (no conflicts, or user confirmed to force)
|
||||
# 1 — user aborted (caller should show promo and return)
|
||||
preflight_check() {
|
||||
local mode="${1:-lite}"
|
||||
local lite_port="${2:-443}"
|
||||
|
||||
# Escape hatch
|
||||
if [ "${GOTELEGRAM_SKIP_PREFLIGHT:-0}" = "1" ]; then
|
||||
log_dim "preflight: skipped (GOTELEGRAM_SKIP_PREFLIGHT=1)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local required_ports=()
|
||||
if [ "$mode" = "pro" ]; then
|
||||
required_ports=(443 80 8443)
|
||||
else
|
||||
# lite: проверяем только выбранный внешний порт
|
||||
required_ports=("$lite_port")
|
||||
fi
|
||||
|
||||
local known_conflicts=() unknown_conflicts=() info port pid proc label
|
||||
for port in "${required_ports[@]}"; do
|
||||
info=$(get_port_process "$port")
|
||||
if [ -n "$info" ]; then
|
||||
pid="${info%%|*}"
|
||||
proc="${info##*|}"
|
||||
if label=$(match_known_conflict "$proc"); then
|
||||
known_conflicts+=("${port}|${label}|${pid}|${proc}")
|
||||
else
|
||||
unknown_conflicts+=("${port}|${pid}|${proc}")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#known_conflicts[@]} -eq 0 ] && [ ${#unknown_conflicts[@]} -eq 0 ]; then
|
||||
log_dim "preflight: ports ${required_ports[*]} свободны"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Показываем баннер конфликта
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${YELLOW}⚠ $(_t_or preflight_title 'Предустановочная проверка: обнаружены конфликты портов')${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
|
||||
local item p label2 pid2 proc2 rest
|
||||
if [ ${#known_conflicts[@]} -gt 0 ]; then
|
||||
echo -e " ${RED}$(_t_or preflight_known 'Известный proxy/VPN/веб-софт занимает нужные порты:')${NC}" >&2
|
||||
for item in "${known_conflicts[@]}"; do
|
||||
p="${item%%|*}"
|
||||
rest="${item#*|}"
|
||||
label2="${rest%%|*}"
|
||||
rest="${rest#*|}"
|
||||
pid2="${rest%%|*}"
|
||||
proc2="${rest##*|}"
|
||||
echo -e " ${RED}✗${NC} ${BOLD}:${p}${NC} → ${BOLD}${label2}${NC} ${DIM}(pid=${pid2}, cmd=${proc2})${NC}" >&2
|
||||
done
|
||||
fi
|
||||
if [ ${#unknown_conflicts[@]} -gt 0 ]; then
|
||||
echo -e " ${YELLOW}$(_t_or preflight_unknown 'Порты заняты неизвестными процессами:')${NC}" >&2
|
||||
for item in "${unknown_conflicts[@]}"; do
|
||||
p="${item%%|*}"
|
||||
rest="${item#*|}"
|
||||
pid2="${rest%%|*}"
|
||||
proc2="${rest##*|}"
|
||||
echo -e " ${YELLOW}⚠${NC} ${BOLD}:${p}${NC} ${DIM}(pid=${pid2}, cmd=${proc2})${NC}" >&2
|
||||
done
|
||||
fi
|
||||
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo -e " ${WHITE}$(_t_or preflight_needed 'GoTelegram нужны порты:')${NC} ${CYAN}${required_ports[*]}${NC}" >&2
|
||||
echo -e " ${WHITE}$(_t_or preflight_hint_header 'Рекомендации:')${NC}" >&2
|
||||
echo -e " ${DIM}• $(_t_or preflight_hint1 'Остановите и удалите конфликтующие сервисы (systemctl stop ...)')${NC}" >&2
|
||||
echo -e " ${DIM}• $(_t_or preflight_hint2 'Либо возьмите чистый VPS без других прокси')${NC}" >&2
|
||||
echo -e " ${DIM}• $(_t_or preflight_hint3 'Установка поверх, скорее всего, завершится некорректно')${NC}" >&2
|
||||
echo -e " ${DIM}$(_t_or preflight_skip_hint 'Override: GOTELEGRAM_SKIP_PREFLIGHT=1 gotelegram')${NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
if confirm "$(_t_or preflight_proceed 'Продолжить установку всё равно (скорее всего не заработает)?')"; then
|
||||
log_warning "$(_t_or preflight_forced 'Установка продолжена вопреки конфликтам — возможны ошибки')"
|
||||
return 0
|
||||
fi
|
||||
log_info "$(_t_or preflight_aborted 'Установка отменена из-за конфликтов портов')"
|
||||
return 1
|
||||
}
|
||||
|
||||
check_disk_space() {
|
||||
local min_mb="${1:-500}"
|
||||
local avail_mb
|
||||
|
||||
106
lib/lang/en.sh
Executable file → Normal file
106
lib/lang/en.sh
Executable file → Normal file
@@ -44,6 +44,33 @@ I18N[net_mode]="Mode:"
|
||||
I18N[net_domain]="Domain:"
|
||||
I18N[connection_link]="Telegram connection link:"
|
||||
I18N[proxy_not_configured]="Proxy is not configured. Select option 1."
|
||||
# ── show_proxy_info labels ─────────────────────────────────────────────
|
||||
I18N[info_status_running]="Running"
|
||||
I18N[info_status_stopped]="Stopped"
|
||||
I18N[info_status_not_installed]="Not installed"
|
||||
I18N[info_proxy_status]="Proxy status"
|
||||
I18N[info_engine]="Engine"
|
||||
I18N[info_ip]="IP"
|
||||
I18N[info_domain]="Domain"
|
||||
I18N[info_port]="Port"
|
||||
I18N[info_mode]="Mode"
|
||||
I18N[info_mask]="Mask host"
|
||||
I18N[info_secret]="Secret"
|
||||
I18N[info_link]="Link"
|
||||
# ── show_traffic_stats labels ──────────────────────────────────────────
|
||||
I18N[stats_sh_proxy]="Proxy (telemt, port 443)"
|
||||
I18N[stats_sh_site]="Site (nginx, port 8443)"
|
||||
I18N[stats_sh_hdr_period]="Period"
|
||||
I18N[stats_sh_hdr_inbound]="Inbound"
|
||||
I18N[stats_sh_hdr_rate]="Rate"
|
||||
I18N[stats_sh_packets]="Packets"
|
||||
I18N[stats_sh_1min]="1 min"
|
||||
I18N[stats_sh_5min]="5 min"
|
||||
I18N[stats_sh_60min]="60 min"
|
||||
I18N[stats_sh_1day]="1 day"
|
||||
I18N[stats_sh_7days]="7 days"
|
||||
I18N[stats_sh_30days]="30 days"
|
||||
I18N[stats_sh_365days]="365 days"
|
||||
I18N[menu_proxy]="Proxy ▸"
|
||||
I18N[menu_stats]="Statistics ▸"
|
||||
I18N[menu_manage]="Management ▸"
|
||||
@@ -347,6 +374,20 @@ I18N[backup_pass_short]="Password too short (minimum 6 characters)"
|
||||
I18N[backup_pick_prompt]="Backup number (or path to file)"
|
||||
I18N[backup_not_found]="Backup not found"
|
||||
|
||||
# ── Preflight (v2.4.8) ──────────────────────────────────────────────────
|
||||
I18N[preflight_title]="Preflight: port conflicts detected"
|
||||
I18N[preflight_known]="Known proxy/VPN/web software is using required ports:"
|
||||
I18N[preflight_unknown]="Required ports are held by unknown processes:"
|
||||
I18N[preflight_needed]="GoTelegram requires ports:"
|
||||
I18N[preflight_hint_header]="Recommended actions:"
|
||||
I18N[preflight_hint1]="Stop and remove the conflicting services (systemctl stop ...)"
|
||||
I18N[preflight_hint2]="Or use a clean VPS without other proxies"
|
||||
I18N[preflight_hint3]="Installing on top will most likely fail"
|
||||
I18N[preflight_skip_hint]="Bypass: GOTELEGRAM_SKIP_PREFLIGHT=1 gotelegram"
|
||||
I18N[preflight_proceed]="Continue installation anyway (likely to fail)?"
|
||||
I18N[preflight_forced]="Installation continued despite conflicts — errors likely"
|
||||
I18N[preflight_aborted]="Installation aborted due to port conflicts"
|
||||
|
||||
# ── Errors / misc ───────────────────────────────────────────────────────
|
||||
I18N[err_need_root]="Run the script with sudo / as root"
|
||||
I18N[err_os_unknown]="Failed to detect OS. Linux is required."
|
||||
@@ -373,3 +414,68 @@ I18N[v1_migration_cancelled]="Migration cancelled. v1 left intact."
|
||||
I18N[v1_stopping]="Stopping v1 container..."
|
||||
I18N[v1_config_saved]="v1 config saved to %s"
|
||||
I18N[v1_port_freed]="v1 stopped. Port %s freed."
|
||||
|
||||
# ── v2.4.9: UBF v2.0 backup + manual secret recovery ─────────────────────
|
||||
I18N[install_source_title]="Installation source"
|
||||
I18N[install_source_choice]="Choose source [1-3]:"
|
||||
I18N[install_menu_new]="Fresh installation"
|
||||
I18N[install_menu_new_desc]="Generate a new key and set up from scratch"
|
||||
I18N[install_menu_restore]="Restore from backup"
|
||||
I18N[install_menu_restore_desc]="Full restore from a .tar.gz[.enc] file"
|
||||
I18N[install_menu_existing_key]="Use existing key"
|
||||
I18N[install_menu_existing_key_desc]="Paste a tg://proxy link or a key manually"
|
||||
I18N[install_hint_pro_mode]="The key contains a domain — this is usually Pro mode"
|
||||
I18N[install_reuse_secret]="Using the provided key"
|
||||
I18N[install_reuse_domain]="Using domain from the key"
|
||||
I18N[install_reuse_port]="Using port from the key"
|
||||
|
||||
I18N[manual_secret_title]="Enter existing key"
|
||||
I18N[manual_secret_help1]="Supported formats:"
|
||||
I18N[manual_secret_prompt]="Paste the key"
|
||||
I18N[manual_secret_empty]="Key is empty"
|
||||
I18N[manual_secret_bad]="Could not parse the key format"
|
||||
I18N[manual_secret_parsed]="Key parsed"
|
||||
|
||||
I18N[backup_id_label]="Backup ID"
|
||||
I18N[backup_file_label]="File"
|
||||
I18N[backup_size_label]="Size"
|
||||
I18N[backup_key_label]="Key in backup (fingerprint)"
|
||||
I18N[backup_format_label]="Format"
|
||||
I18N[backup_mode_label]="Mode"
|
||||
I18N[backup_lang_label]="Language"
|
||||
I18N[backup_date_label]="Date"
|
||||
I18N[backup_label]="Backup"
|
||||
I18N[backup_ssl_included]="SSL certificates included (+ chain + renewal)"
|
||||
I18N[backup_site_included]="Website template included"
|
||||
I18N[backup_bot_included]="Telegram bot config included"
|
||||
I18N[backup_restored_bot]="Telegram bot config restored"
|
||||
I18N[backup_automigrate]="Converting legacy backup to UBF v2.0..."
|
||||
I18N[backup_migrated]="Fresh UBF v2.0 backup saved"
|
||||
I18N[backup_collecting]="Collecting configuration..."
|
||||
I18N[backup_archive_err]="Archive creation failed"
|
||||
I18N[backup_archive_missing]="Archive was not created"
|
||||
I18N[backup_encrypt_err]="Encryption failed"
|
||||
I18N[backup_encrypted]="Backup encrypted (AES-256-CBC)"
|
||||
I18N[backup_created]="Backup created"
|
||||
I18N[backup_enter_pass]="Enter password"
|
||||
I18N[backup_repeat_pass]="Repeat password"
|
||||
I18N[backup_pass_mismatch]="Passwords do not match"
|
||||
I18N[backup_pass_short]="Password too short (min 6 chars)"
|
||||
I18N[backup_bad_pass]="Wrong password or corrupted file"
|
||||
I18N[backup_extract_err]="Archive extraction failed"
|
||||
I18N[backup_confirm_restore]="Restore configuration? Current settings will be overwritten."
|
||||
I18N[backup_restored_telemt]="telemt config restored"
|
||||
I18N[backup_restored_gotelegram]="GoTelegram config restored"
|
||||
I18N[backup_restored_lang]="Interface language restored"
|
||||
I18N[backup_restored_nginx]="nginx config restored"
|
||||
I18N[backup_restored_ssl]="SSL certificates restored"
|
||||
I18N[backup_restored_site]="Website template restored"
|
||||
I18N[backup_restore_done]="Restore completed!"
|
||||
I18N[backup_create_title]="Create backup"
|
||||
I18N[backup_encrypt_prompt]="Encrypt the backup with a password?"
|
||||
I18N[backup_none]="No backups found"
|
||||
I18N[backup_list_title]="Available backups"
|
||||
I18N[backup_pick_prompt]="Backup number (or file path)"
|
||||
I18N[backup_not_found]="Backup not found"
|
||||
I18N[backup_file_not_found_fmt]="File not found: %s"
|
||||
I18N[backup_cleanup_fmt]="Deleted %s old backups (kept %s)"
|
||||
|
||||
106
lib/lang/ru.sh
Executable file → Normal file
106
lib/lang/ru.sh
Executable file → Normal file
@@ -44,6 +44,33 @@ I18N[net_mode]="Режим:"
|
||||
I18N[net_domain]="Домен:"
|
||||
I18N[connection_link]="Ссылка для Telegram:"
|
||||
I18N[proxy_not_configured]="Прокси не настроен. Выберите пункт 1."
|
||||
# ── show_proxy_info labels ─────────────────────────────────────────────
|
||||
I18N[info_status_running]="Работает"
|
||||
I18N[info_status_stopped]="Остановлен"
|
||||
I18N[info_status_not_installed]="Не установлен"
|
||||
I18N[info_proxy_status]="Статус прокси"
|
||||
I18N[info_engine]="Ядро"
|
||||
I18N[info_ip]="IP"
|
||||
I18N[info_domain]="Домен"
|
||||
I18N[info_port]="Порт"
|
||||
I18N[info_mode]="Режим"
|
||||
I18N[info_mask]="Маскировка"
|
||||
I18N[info_secret]="Secret"
|
||||
I18N[info_link]="Ссылка"
|
||||
# ── show_traffic_stats labels ──────────────────────────────────────────
|
||||
I18N[stats_sh_proxy]="Proxy (telemt, порт 443)"
|
||||
I18N[stats_sh_site]="Сайт (nginx, порт 8443)"
|
||||
I18N[stats_sh_hdr_period]="Период"
|
||||
I18N[stats_sh_hdr_inbound]="Входящий"
|
||||
I18N[stats_sh_hdr_rate]="Скорость"
|
||||
I18N[stats_sh_packets]="Пакетов"
|
||||
I18N[stats_sh_1min]="1 мин"
|
||||
I18N[stats_sh_5min]="5 мин"
|
||||
I18N[stats_sh_60min]="60 мин"
|
||||
I18N[stats_sh_1day]="1 день"
|
||||
I18N[stats_sh_7days]="7 дней"
|
||||
I18N[stats_sh_30days]="30 дней"
|
||||
I18N[stats_sh_365days]="365 дней"
|
||||
I18N[menu_proxy]="Прокси ▸"
|
||||
I18N[menu_stats]="Статистика ▸"
|
||||
I18N[menu_manage]="Управление ▸"
|
||||
@@ -347,6 +374,20 @@ I18N[backup_pass_short]="Пароль слишком короткий (мини
|
||||
I18N[backup_pick_prompt]="Номер бекапа (или путь к файлу)"
|
||||
I18N[backup_not_found]="Бекап не найден"
|
||||
|
||||
# ── Preflight (v2.4.8) ──────────────────────────────────────────────────
|
||||
I18N[preflight_title]="Предустановочная проверка: обнаружены конфликты портов"
|
||||
I18N[preflight_known]="Известный proxy/VPN/веб-софт занимает нужные порты:"
|
||||
I18N[preflight_unknown]="Порты заняты неизвестными процессами:"
|
||||
I18N[preflight_needed]="GoTelegram нужны порты:"
|
||||
I18N[preflight_hint_header]="Рекомендации:"
|
||||
I18N[preflight_hint1]="Остановите и удалите конфликтующие сервисы (systemctl stop ...)"
|
||||
I18N[preflight_hint2]="Либо возьмите чистый VPS без других прокси"
|
||||
I18N[preflight_hint3]="Установка поверх, скорее всего, завершится некорректно"
|
||||
I18N[preflight_skip_hint]="Обойти проверку: GOTELEGRAM_SKIP_PREFLIGHT=1 gotelegram"
|
||||
I18N[preflight_proceed]="Продолжить установку всё равно (скорее всего не заработает)?"
|
||||
I18N[preflight_forced]="Установка продолжена вопреки конфликтам — возможны ошибки"
|
||||
I18N[preflight_aborted]="Установка отменена из-за конфликтов портов"
|
||||
|
||||
# ── Errors / misc ───────────────────────────────────────────────────────
|
||||
I18N[err_need_root]="Запустите скрипт с sudo / от root"
|
||||
I18N[err_os_unknown]="Не удалось определить ОС. Требуется Linux."
|
||||
@@ -373,3 +414,68 @@ I18N[v1_migration_cancelled]="Миграция отменена. v1 оставл
|
||||
I18N[v1_stopping]="Остановка v1 контейнера..."
|
||||
I18N[v1_config_saved]="Конфиг v1 сохранён в %s"
|
||||
I18N[v1_port_freed]="v1 остановлен. Порт %s освобождён."
|
||||
|
||||
# ── v2.4.9: UBF v2.0 backup + manual secret recovery ─────────────────────
|
||||
I18N[install_source_title]="Источник установки"
|
||||
I18N[install_source_choice]="Выберите источник [1-3]:"
|
||||
I18N[install_menu_new]="Новая установка"
|
||||
I18N[install_menu_new_desc]="Сгенерировать новый ключ и настроить с нуля"
|
||||
I18N[install_menu_restore]="Восстановить из бекапа"
|
||||
I18N[install_menu_restore_desc]="Полное восстановление из файла .tar.gz[.enc]"
|
||||
I18N[install_menu_existing_key]="Использовать существующий ключ"
|
||||
I18N[install_menu_existing_key_desc]="Ввести ссылку tg://proxy или ключ вручную"
|
||||
I18N[install_hint_pro_mode]="Ключ содержит домен — обычно это Pro режим"
|
||||
I18N[install_reuse_secret]="Используется переданный ключ"
|
||||
I18N[install_reuse_domain]="Используется домен из ключа"
|
||||
I18N[install_reuse_port]="Используется порт из ключа"
|
||||
|
||||
I18N[manual_secret_title]="Ввод существующего ключа"
|
||||
I18N[manual_secret_help1]="Поддерживаются форматы:"
|
||||
I18N[manual_secret_prompt]="Вставьте ключ"
|
||||
I18N[manual_secret_empty]="Ключ не введён"
|
||||
I18N[manual_secret_bad]="Не удалось распознать формат ключа"
|
||||
I18N[manual_secret_parsed]="Ключ распознан"
|
||||
|
||||
I18N[backup_id_label]="Backup ID"
|
||||
I18N[backup_file_label]="Файл"
|
||||
I18N[backup_size_label]="Размер"
|
||||
I18N[backup_key_label]="Ключ в бекапе (fingerprint)"
|
||||
I18N[backup_format_label]="Формат"
|
||||
I18N[backup_mode_label]="Режим"
|
||||
I18N[backup_lang_label]="Язык"
|
||||
I18N[backup_date_label]="Дата"
|
||||
I18N[backup_label]="Бекап"
|
||||
I18N[backup_ssl_included]="SSL-сертификаты включены (+ chain + renewal)"
|
||||
I18N[backup_site_included]="Шаблон сайта включён"
|
||||
I18N[backup_bot_included]="Конфиг Telegram-бота включён"
|
||||
I18N[backup_restored_bot]="Конфиг Telegram-бота восстановлен"
|
||||
I18N[backup_automigrate]="Конвертирую старый бекап в UBF v2.0..."
|
||||
I18N[backup_migrated]="Свежий UBF v2.0 бекап сохранён"
|
||||
I18N[backup_collecting]="Собираю конфигурацию..."
|
||||
I18N[backup_archive_err]="Ошибка создания архива"
|
||||
I18N[backup_archive_missing]="Архив не создан"
|
||||
I18N[backup_encrypt_err]="Ошибка шифрования"
|
||||
I18N[backup_encrypted]="Бекап зашифрован (AES-256-CBC)"
|
||||
I18N[backup_created]="Бекап создан"
|
||||
I18N[backup_enter_pass]="Введите пароль"
|
||||
I18N[backup_repeat_pass]="Повторите пароль"
|
||||
I18N[backup_pass_mismatch]="Пароли не совпадают"
|
||||
I18N[backup_pass_short]="Пароль слишком короткий (минимум 6 символов)"
|
||||
I18N[backup_bad_pass]="Неверный пароль или повреждённый файл"
|
||||
I18N[backup_extract_err]="Ошибка распаковки архива"
|
||||
I18N[backup_confirm_restore]="Восстановить конфигурацию? Текущие настройки будут перезаписаны."
|
||||
I18N[backup_restored_telemt]="telemt конфиг восстановлен"
|
||||
I18N[backup_restored_gotelegram]="GoTelegram конфиг восстановлен"
|
||||
I18N[backup_restored_lang]="Язык интерфейса восстановлен"
|
||||
I18N[backup_restored_nginx]="nginx конфиг восстановлен"
|
||||
I18N[backup_restored_ssl]="SSL сертификаты восстановлены"
|
||||
I18N[backup_restored_site]="Шаблон сайта восстановлен"
|
||||
I18N[backup_restore_done]="Восстановление завершено!"
|
||||
I18N[backup_create_title]="Создание бекапа"
|
||||
I18N[backup_encrypt_prompt]="Зашифровать бекап паролем?"
|
||||
I18N[backup_none]="Бекапов нет"
|
||||
I18N[backup_list_title]="Доступные бекапы"
|
||||
I18N[backup_pick_prompt]="Номер бекапа (или путь к файлу)"
|
||||
I18N[backup_not_found]="Бекап не найден"
|
||||
I18N[backup_file_not_found_fmt]="Файл не найден: %s"
|
||||
I18N[backup_cleanup_fmt]="Удалено %s старых бекапов (оставлено %s)"
|
||||
|
||||
53
lib/stats.sh
53
lib/stats.sh
@@ -243,36 +243,49 @@ show_traffic_stats() {
|
||||
local site_rates=$(stats_calculate_rates "site")
|
||||
IFS='|' read -r s1m s1mr s5m s5mr s60m s60mr s1d s1dr s7d s7dr s30d s30dr s365d s365dr <<< "$site_rates"
|
||||
|
||||
# i18n labels (fall back to English if t() not loaded)
|
||||
local lbl_proxy; lbl_proxy="$(_t_or stats_sh_proxy 'Proxy (telemt, port 443)')"
|
||||
local lbl_site; lbl_site="$(_t_or stats_sh_site 'Site (nginx, port 8443)')"
|
||||
local lbl_hdr; lbl_hdr="$(_t_or stats_sh_hdr_period 'Period') │ $(_t_or stats_sh_hdr_inbound 'Inbound') │ $(_t_or stats_sh_hdr_rate 'Rate')"
|
||||
local lbl_pkts; lbl_pkts="$(_t_or stats_sh_packets 'Packets')"
|
||||
local l1m; l1m="$(_t_or stats_sh_1min '1 min')"
|
||||
local l5m; l5m="$(_t_or stats_sh_5min '5 min')"
|
||||
local l60m; l60m="$(_t_or stats_sh_60min '60 min')"
|
||||
local l1d; l1d="$(_t_or stats_sh_1day '1 day')"
|
||||
local l7d; l7d="$(_t_or stats_sh_7days '7 days')"
|
||||
local l30d; l30d="$(_t_or stats_sh_30days '30 days')"
|
||||
local l365d; l365d="$(_t_or stats_sh_365days '365 days')"
|
||||
|
||||
# Display proxy stats
|
||||
{
|
||||
echo ""
|
||||
echo -e "${BLUE} Proxy (telemt, порт 443):${NC}"
|
||||
echo -e "${BLUE} ${lbl_proxy}:${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
echo -e "${BLUE} Период │ Входящий │ Скорость${NC}"
|
||||
echo -e "${BLUE} ${lbl_hdr}${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " %-9s │ %14s │ %s\n" "1 мин" "$p1m" "$p1mr"
|
||||
printf " %-9s │ %14s │ %s\n" "5 мин" "$p5m" "$p5mr"
|
||||
printf " %-9s │ %14s │ %s\n" "60 мин" "$p60m" "$p60mr"
|
||||
printf " %-9s │ %14s │ %s\n" "1 день" "$p1d" "$p1dr"
|
||||
printf " %-9s │ %14s │ %s\n" "7 дней" "$p7d" "$p7dr"
|
||||
printf " %-9s │ %14s │ %s\n" "30 дней" "$p30d" "$p30dr"
|
||||
printf " %-9s │ %14s │ %s\n" "365 дней" "$p365d" "$p365dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l1m" "$p1m" "$p1mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l5m" "$p5m" "$p5mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l60m" "$p60m" "$p60mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l1d" "$p1d" "$p1dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l7d" "$p7d" "$p7dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l30d" "$p30d" "$p30dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l365d" "$p365d" "$p365dr"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " Пакетов: %d\n\n" "$proxy_pkts"
|
||||
printf " %s: %d\n\n" "$lbl_pkts" "$proxy_pkts"
|
||||
|
||||
echo -e "${BLUE} Сайт (nginx, порт 8443):${NC}"
|
||||
echo -e "${BLUE} ${lbl_site}:${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
echo -e "${BLUE} Период │ Входящий │ Скорость${NC}"
|
||||
echo -e "${BLUE} ${lbl_hdr}${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " %-9s │ %14s │ %s\n" "1 мин" "$s1m" "$s1mr"
|
||||
printf " %-9s │ %14s │ %s\n" "5 мин" "$s5m" "$s5mr"
|
||||
printf " %-9s │ %14s │ %s\n" "60 мин" "$s60m" "$s60mr"
|
||||
printf " %-9s │ %14s │ %s\n" "1 день" "$s1d" "$s1dr"
|
||||
printf " %-9s │ %14s │ %s\n" "7 дней" "$s7d" "$s7dr"
|
||||
printf " %-9s │ %14s │ %s\n" "30 дней" "$s30d" "$s30dr"
|
||||
printf " %-9s │ %14s │ %s\n" "365 дней" "$s365d" "$s365dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l1m" "$s1m" "$s1mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l5m" "$s5m" "$s5mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l60m" "$s60m" "$s60mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l1d" "$s1d" "$s1dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l7d" "$s7d" "$s7dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l30d" "$s30d" "$s30dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l365d" "$s365d" "$s365dr"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " Пакетов: %d\n" "$site_pkts"
|
||||
printf " %s: %d\n" "$lbl_pkts" "$site_pkts"
|
||||
echo ""
|
||||
} >&2
|
||||
}
|
||||
|
||||
87
lib/telemt.sh
Executable file → Normal file
87
lib/telemt.sh
Executable file → Normal file
@@ -19,27 +19,55 @@ get_latest_telemt_version() {
|
||||
}
|
||||
|
||||
get_telemt_download_url() {
|
||||
local arch
|
||||
# 1) Сначала пробуем GitHub Releases API — он отдаёт точное имя ассета
|
||||
# последнего релиза (в т.ч. если в репо есть несколько архитектур,
|
||||
# pre-release и т.д.). Это наш предпочтительный путь.
|
||||
local resp url arch
|
||||
arch=$(get_arch)
|
||||
local resp
|
||||
resp=$(curl -s --max-time 10 "$TELEMT_RELEASE_API" 2>/dev/null)
|
||||
if [ -z "$resp" ]; then return 1; fi
|
||||
if [ -n "$resp" ]; then
|
||||
url=$(echo "$resp" | jq -r --arg a "$arch" '
|
||||
.assets[]?.browser_download_url
|
||||
| select(test("linux"))
|
||||
| select(
|
||||
($a == "amd64" and (test("x86_64|amd64"))) or
|
||||
($a == "arm64" and (test("aarch64|arm64"))) or
|
||||
($a == "armv7" and (test("armv7"))) or
|
||||
(test($a))
|
||||
)
|
||||
| select(test("gnu"))
|
||||
' 2>/dev/null | head -1)
|
||||
if [ -n "$url" ] && [ "$url" != "null" ]; then
|
||||
echo "$url"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# URL format: telemt-x86_64-linux-gnu.tar.gz (arch BEFORE linux)
|
||||
local arch_pattern
|
||||
# 2) Fallback: API не ответил / отдал 403 (rate limit на shared-IP VPS),
|
||||
# отдал пустой JSON, или jq не нашёл подходящий ассет.
|
||||
# Берём прямой "magic redirect" CDN-URL — он не считается в API rate
|
||||
# limit и всегда указывает на последний релиз.
|
||||
local arch_name
|
||||
case "$arch" in
|
||||
amd64) arch_pattern="(amd64|x86_64)" ;;
|
||||
arm64) arch_pattern="(arm64|aarch64)" ;;
|
||||
armv7) arch_pattern="(armv7|arm)" ;;
|
||||
*) arch_pattern="${arch}" ;;
|
||||
amd64) arch_name="x86_64" ;;
|
||||
arm64) arch_name="aarch64" ;;
|
||||
armv7) arch_name="armv7" ;;
|
||||
*) arch_name="$arch" ;;
|
||||
esac
|
||||
echo "https://github.com/${TELEMT_GITHUB}/releases/latest/download/telemt-${arch_name}-linux-gnu.tar.gz"
|
||||
}
|
||||
|
||||
echo "$resp" | jq -r ".assets[].browser_download_url" 2>/dev/null \
|
||||
| grep -iE "$arch_pattern" \
|
||||
| grep -i "linux" \
|
||||
| grep -v "sha256" \
|
||||
| grep "gnu" \
|
||||
| head -1
|
||||
# Fallback URL using musl libc (some minimal distros / older glibc)
|
||||
get_telemt_download_url_musl() {
|
||||
local arch arch_name
|
||||
arch=$(get_arch)
|
||||
case "$arch" in
|
||||
amd64) arch_name="x86_64" ;;
|
||||
arm64) arch_name="aarch64" ;;
|
||||
armv7) arch_name="armv7" ;;
|
||||
*) arch_name="$arch" ;;
|
||||
esac
|
||||
echo "https://github.com/${TELEMT_GITHUB}/releases/latest/download/telemt-${arch_name}-linux-musl.tar.gz"
|
||||
}
|
||||
|
||||
# ── Установленная версия ─────────────────────────────────────────────────────
|
||||
@@ -69,18 +97,35 @@ download_telemt() {
|
||||
log_info "Скачивание: $url"
|
||||
|
||||
if ! curl -L -s --max-time 120 -o "$tmp_file" "$url"; then
|
||||
log_error "Ошибка скачивания telemt"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
log_warning "Не удалось скачать gnu-сборку, пробую musl..."
|
||||
url=$(get_telemt_download_url_musl)
|
||||
log_info "Скачивание: $url"
|
||||
if ! curl -L -s --max-time 120 -o "$tmp_file" "$url"; then
|
||||
log_error "Ошибка скачивания telemt (gnu и musl)"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Проверяем что файл не пустой и не HTML
|
||||
local file_size
|
||||
file_size=$(stat -c%s "$tmp_file" 2>/dev/null || echo 0)
|
||||
if [ "$file_size" -lt 1000 ]; then
|
||||
log_error "Скачанный файл слишком маленький ($file_size байт) — возможна ошибка сети"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
# Try musl as fallback if gnu came back empty/error-html
|
||||
log_warning "Файл подозрительно мал ($file_size байт), пробую musl-сборку..."
|
||||
url=$(get_telemt_download_url_musl)
|
||||
log_info "Скачивание: $url"
|
||||
if ! curl -L -s --max-time 120 -o "$tmp_file" "$url"; then
|
||||
log_error "Ошибка скачивания telemt (musl fallback)"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
fi
|
||||
file_size=$(stat -c%s "$tmp_file" 2>/dev/null || echo 0)
|
||||
if [ "$file_size" -lt 1000 ]; then
|
||||
log_error "Скачанный файл слишком маленький ($file_size байт) — возможна ошибка сети"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Определяем тип файла и распаковываем
|
||||
|
||||
44
lib/telemt_config.sh
Executable file → Normal file
44
lib/telemt_config.sh
Executable file → Normal file
@@ -285,26 +285,26 @@ show_proxy_info() {
|
||||
|
||||
local status_icon status_text
|
||||
case "$status" in
|
||||
running) status_icon="✅"; status_text="Работает" ;;
|
||||
stopped) status_icon="⏸️"; status_text="Остановлен" ;;
|
||||
*) status_icon="❌"; status_text="Не установлен" ;;
|
||||
running) status_icon="✅"; status_text="$(t info_status_running)" ;;
|
||||
stopped) status_icon="⏸️"; status_text="$(t info_status_stopped)" ;;
|
||||
*) status_icon="❌"; status_text="$(t info_status_not_installed)" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}${status_icon} Статус прокси: ${status_text}${NC}"
|
||||
echo -e " ${BOLD}${WHITE}${status_icon} $(t info_proxy_status): ${status_text}${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
|
||||
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
|
||||
echo -e " ${WHITE}$(t info_engine):${NC} telemt (Rust)"
|
||||
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||
echo -e " ${WHITE}Домен:${NC} ${CYAN}${domain}${NC}"
|
||||
echo -e " ${WHITE}$(t info_domain):${NC} ${CYAN}${domain}${NC}"
|
||||
else
|
||||
echo -e " ${WHITE}IP:${NC} ${CYAN}${ip}${NC}"
|
||||
echo -e " ${WHITE}$(t info_ip):${NC} ${CYAN}${ip}${NC}"
|
||||
fi
|
||||
echo -e " ${WHITE}Порт:${NC} ${CYAN}${port}${NC}"
|
||||
echo -e " ${WHITE}Режим:${NC} ${CYAN}${mode}${NC}"
|
||||
echo -e " ${WHITE}Маскировка:${NC} ${CYAN}${mask_host}${NC}"
|
||||
echo -e " ${WHITE}Secret:${NC} ${CYAN}${secret:0:16}...${NC}"
|
||||
echo -e " ${WHITE}$(t info_port):${NC} ${CYAN}${port}${NC}"
|
||||
echo -e " ${WHITE}$(t info_mode):${NC} ${CYAN}${mode}${NC}"
|
||||
echo -e " ${WHITE}$(t info_mask):${NC} ${CYAN}${mask_host}${NC}"
|
||||
echo -e " ${WHITE}$(t info_secret):${NC} ${CYAN}${secret:0:16}...${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
|
||||
echo -e " ${WHITE}Ссылка:${NC}"
|
||||
echo -e " ${WHITE}$(t info_link):${NC}"
|
||||
echo -e " ${GREEN}${link}${NC}"
|
||||
echo ""
|
||||
|
||||
@@ -323,20 +323,20 @@ show_proxy_info_pro() {
|
||||
local link="tg://proxy?server=${domain}&port=443&secret=${faketls_secret}"
|
||||
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}✅ Pro-прокси настроен${NC}"
|
||||
echo -e " ${BOLD}${WHITE}✅ $(t info_proxy_status): $(t info_status_running) (Pro)${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
|
||||
echo -e " ${WHITE}Домен:${NC} ${CYAN}${domain}${NC}"
|
||||
echo -e " ${WHITE}Порт:${NC} ${CYAN}443${NC} (внешний, telemt)"
|
||||
echo -e " ${WHITE}Режим:${NC} ${MAGENTA}Pro (fake-TLS)${NC}"
|
||||
echo -e " ${WHITE}nginx:${NC} ${CYAN}127.0.0.1:8443${NC} (внутренний)"
|
||||
echo -e " ${WHITE}Secret:${NC} ${CYAN}${faketls_secret:0:20}...${NC}"
|
||||
echo -e " ${WHITE}$(t info_engine):${NC} telemt (Rust)"
|
||||
echo -e " ${WHITE}$(t info_domain):${NC} ${CYAN}${domain}${NC}"
|
||||
echo -e " ${WHITE}$(t info_port):${NC} ${CYAN}443${NC} (telemt)"
|
||||
echo -e " ${WHITE}$(t info_mode):${NC} ${MAGENTA}Pro (fake-TLS)${NC}"
|
||||
echo -e " ${WHITE}nginx:${NC} ${CYAN}127.0.0.1:8443${NC}"
|
||||
echo -e " ${WHITE}$(t info_secret):${NC} ${CYAN}${faketls_secret:0:20}...${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||
echo -e " ${WHITE}Ссылка для Telegram:${NC}"
|
||||
echo -e " ${WHITE}$(t info_link):${NC}"
|
||||
echo -e " ${GREEN}${link}${NC}"
|
||||
echo ""
|
||||
echo -e " ${DIM}Провайдер видит: HTTPS-трафик к ${domain}:443${NC}"
|
||||
echo -e " ${DIM}Telegram-клиент маскирует соединение под TLS${NC}"
|
||||
echo -e " ${DIM}ISP sees: HTTPS → ${domain}:443${NC}"
|
||||
echo -e " ${DIM}Telegram client masquerades as TLS${NC}"
|
||||
echo ""
|
||||
|
||||
# QR если доступен
|
||||
|
||||
Reference in New Issue
Block a user