diff --git a/DOCS_AI.md b/DOCS_AI.md index 8e8d3f3..d471453 100644 --- a/DOCS_AI.md +++ b/DOCS_AI.md @@ -445,12 +445,12 @@ switch_language ru|en - frontend: `admin-web/static/`, vanilla JS/CSS, canvas-график без CDN; - systemd service: `gotelegram-admin`; - bind: `127.0.0.1:1984`, доступ только через SSH tunnel; -- token: `/opt/gotelegram-admin/token`, HTTP cookie `gtauth` выставляется при открытии `/?token=...`; -- API требует cookie/Bearer token, а write-запросы дополнительно требуют `X-GoTelegram-Admin: 1`. +- токена нет: после SSH tunnel открывается `http://127.0.0.1:1984/`; +- write-запросы дополнительно требуют `X-GoTelegram-Admin: 1`, фронтенд добавляет его автоматически. Функции: overview, service status/restart, чтение/запись `[access.users]`, генерация proxy links, traffic history из `/opt/gotelegram/stats_history.csv`, current stats из `/run/gotelegram/stats_current.json`, список/создание backup, journal logs. -`install_admin_web` вызывается при установке Telegram-бота. `auto_install_admin_web_if_possible` подхватывает админку после bootstrap/update, если Python уже установлен и файлы отличаются. Backup v1.3 сохраняет `admin_web/token`, `server.py` и `static/`, restore возвращает их и пробует перезапустить `gotelegram-admin`. +`install_admin_web` вызывается при установке Telegram-бота. `auto_install_admin_web_if_possible` подхватывает админку после bootstrap/update, если Python уже установлен и файлы отличаются. Backup v1.3 сохраняет `admin_web/server.py` и `admin_web/static/`, restore возвращает их, удаляет legacy `admin_web/token` и пробует перезапустить `gotelegram-admin`. ### 13.2 Upgrade migration (v2.5.0) @@ -626,7 +626,7 @@ with socket.create_connection(("95.163.176.222", 443), timeout=5) as s: ## 17. Changelog -- **2.5.0 (2026-04-24)** — крупный maintenance pass в ветке `codex`: единая версия `2.5.0` в runtime и документации; удалён дефолтный PAT из `bootstrap.sh` (токен теперь только через `GOTELEGRAM_PAT`); `generate_telemt_toml` добавляет `[server.api]` на `127.0.0.1:9091` и metrics на `127.0.0.1:9090`, что нужно для управления пользователями и статистики; Telegram-бот получил меню `🔑 Keys` для `[access.users]` (добавить/удалить/показать ссылку/runtime info); добавлена локальная web-админка `gotelegram-admin` на `127.0.0.1:1984` с SSH-tunnel инструкцией в боте; исправлено чтение traffic CSV в боте (header больше не ломает parsing); бот сам делает `stats_collect` перед показом статистики; `iptables` добавлен в optional deps и stats collector пытается установить его; CLI-смена шаблона теперь обновляет `config.json.template_id`, чтобы бот не показывал первый установленный шаблон; backup/restore версии `1.3` сохраняет bot `.env`, bot lang files, web-admin token/static, custom templates, templates catalog, stats history и полноценную структуру Let's Encrypt (`live/archive/renewal`) для переезда на новый сервер; добавлен безопасный детект 3x-ui/Xray на 443 и генерируется `/opt/gotelegram/shared-443-3xui.md` с объяснением shared-443 ограничений. +- **2.5.0 (2026-04-24)** — крупный maintenance pass в ветке `codex`: единая версия `2.5.0` в runtime и документации; удалён дефолтный PAT из `bootstrap.sh` (токен теперь только через `GOTELEGRAM_PAT`); `generate_telemt_toml` добавляет `[server.api]` на `127.0.0.1:9091` и metrics на `127.0.0.1:9090`, что нужно для управления пользователями и статистики; Telegram-бот получил меню `🔑 Keys` для `[access.users]` (добавить/удалить/показать ссылку/runtime info); добавлена локальная web-админка `gotelegram-admin` на `127.0.0.1:1984` с SSH-tunnel инструкцией в боте без отдельного web-admin токена; исправлено чтение traffic CSV в боте (header больше не ломает parsing); бот сам делает `stats_collect` перед показом статистики; `iptables` добавлен в optional deps и stats collector пытается установить его; CLI-смена шаблона теперь обновляет `config.json.template_id`, чтобы бот не показывал первый установленный шаблон; backup/restore версии `1.3` сохраняет bot `.env`, bot lang files, web-admin server/static, custom templates, templates catalog, stats history и полноценную структуру Let's Encrypt (`live/archive/renewal`) для переезда на новый сервер; добавлен безопасный детект 3x-ui/Xray на 443 и генерируется `/opt/gotelegram/shared-443-3xui.md` с объяснением shared-443 ограничений. - **2.4.6 (2026-04-10)** — universal `apt_lock_wait` helper: ожидание dpkg/apt lock при unattended-upgrades, исправляет установку nginx/certbot/python на свежих VPS. - **2.4.3 (2026-04-10)** — iter3-фикс: `bot_action_dispatch` оборачивается во `flock -w 30` на `/var/lock/gotelegram-bot-action.lock`. Обнаружена гонка: параллельные `change-lite-domain` получали `"no secret in config"`, потому что один процесс читал `config.json`, пока другой делал `jq ... > tmp && mv`. `util-linux` (содержит `flock`) добавлен в `critical` deps, `check_deps_present` и маппинги `apt_pkg_for_cmd`/`dnf_pkg_for_cmd`. - **2.4.2 (2026-04-10)** — реализация non-interactive `bot_action_*` в install.sh (change-template + change-lite-domain с JSON-ответом). bot.py подключает `run_bot_action()` и делает реальную работу вместо stub'ов. Критфиксы: (a) `safe_edit_message` принимает `disable_web_page_preview` (иначе TypeError в success-пути cb_pro_confirm); (b) чтение/запись `config['template_id']` вместо `config['template']` (save_gotelegram_config всегда писал `template_id`, бот смотрел не туда); (c) `bot_update_config_field` использует shell `date -Iseconds` вместо `jq now|todate` (jq 1.5 совместимость для Debian 10); (d) `asyncio.Lock _BOT_ACTION_LOCK` сериализует callback'и в процессе бота; (e) валидация `_TPL_ID_RE`/`_DOMAIN_RE` до subprocess. Полный аудит и автоустановка зависимостей: `ensure_deps` разделяет critical/optional, дедуплицирует пакеты, re-verify после install; `apt_pkg_for_cmd` и `dnf_pkg_for_cmd` мапят команды на пакеты (dig→dnsutils/bind-utils, xxd→xxd/vim-common, flock→util-linux). `check_deps_present` — быстрый чек без `apt-get update`. diff --git a/DOCS_HUMAN.md b/DOCS_HUMAN.md index 83892f5..fc37afa 100644 --- a/DOCS_HUMAN.md +++ b/DOCS_HUMAN.md @@ -140,7 +140,7 @@ CLI и бот переведены на русский и английский. - systemd service: `gotelegram-admin`; - слушает только `127.0.0.1:1984`; - наружу не публикуется и рассчитана на доступ через SSH tunnel; -- токен доступа хранится в `/opt/gotelegram-admin/token`; +- после туннеля открывается обычным URL `http://127.0.0.1:1984/`; - Telegram-бот показывает инструкцию для Termius и обычную команду `ssh -L 1984:127.0.0.1:1984 root@SERVER`. В админке есть dashboard, статус сервисов, управление `[access.users]`, генерация ссылок, график трафика, список бекапов и просмотр логов. @@ -236,7 +236,7 @@ A: Сам MTProxy — да, это публичная технология из ## Changelog (коротко) -- **2.5.0** — единая версия по коду и документации; удалён дефолтный PAT из `bootstrap.sh`; исправлена статистика в боте (CSV header больше не ломает чтение истории, бот сам обновляет snapshot); CLI-смена шаблона теперь обновляет `config.json.template_id`, поэтому бот показывает текущий шаблон; telemt TOML включает локальный API `127.0.0.1:9091` и metrics `127.0.0.1:9090`; добавлено меню Telegram-бота для отдельных ключей пользователей (`[access.users]`): список, добавление, удаление, ссылка и runtime/API-информация; добавлена локальная web-админка на `127.0.0.1:1984` под SSH tunnel; backup/restore сохраняет bot `.env`, языки бота, web-admin token/static, custom templates, stats history и структуру Let's Encrypt для переезда на новый VPS; добавлен безопасный детект 3x-ui/Xray на 443 с предупреждением и заметкой по shared-443. +- **2.5.0** — единая версия по коду и документации; удалён дефолтный PAT из `bootstrap.sh`; исправлена статистика в боте (CSV header больше не ломает чтение истории, бот сам обновляет snapshot); CLI-смена шаблона теперь обновляет `config.json.template_id`, поэтому бот показывает текущий шаблон; telemt TOML включает локальный API `127.0.0.1:9091` и metrics `127.0.0.1:9090`; добавлено меню Telegram-бота для отдельных ключей пользователей (`[access.users]`): список, добавление, удаление, ссылка и runtime/API-информация; добавлена локальная web-админка на `127.0.0.1:1984` под SSH tunnel без отдельного токена; backup/restore сохраняет bot `.env`, языки бота, web-admin server/static, custom templates, stats history и структуру Let's Encrypt для переезда на новый VPS; добавлен безопасный детект 3x-ui/Xray на 443 с предупреждением и заметкой по shared-443. - **2.4.6** — ожидание apt/dpkg lock на свежих Ubuntu/Debian, чтобы установка nginx/certbot/Python не падала во время unattended-upgrades. - **2.4.3** — фикс гонки в `bot_action_dispatch`: параллельные вызовы `change-lite-domain`/`change-template` (например, два пользователя бота одновременно) могли получить ошибку «no secret in config», если один процесс читал `config.json` в момент, когда другой его перезаписывал через `jq`. Теперь диспетчер оборачивается в `flock(1)` с таймаутом 30 с; `util-linux` (содержит `flock`) добавлен в критические зависимости. - **2.4.2** — смена шаблона и домена маскировки **прямо из Telegram-бота** без SSH. Раньше эти пункты меню показывали сообщение «сделай через CLI», теперь бот вызывает `install.sh --action=change-template --json` / `--action=change-lite-domain --json` и разбирает ответ. Плюс: безопасный `safe_edit_message` принимает `disable_web_page_preview`; поле статуса шаблона наконец-то отображается (раньше читалось не из того ключа JSON); полный аудит и автоустановка системных зависимостей при первом запуске (`curl jq openssl git xxd tar dig flock` + опциональные `qrencode bc`); `asyncio.Lock` в боте сериализует параллельные callback'и; валидация tpl\_id (`[A-Za-z0-9_-]{1,64}`) и домена до subprocess. diff --git a/admin-web/server.py b/admin-web/server.py index acebd8c..87932e8 100644 --- a/admin-web/server.py +++ b/admin-web/server.py @@ -10,7 +10,6 @@ from __future__ import annotations import csv import hashlib -import http.cookies import json import mimetypes import os @@ -30,7 +29,6 @@ from typing import Any ADMIN_DIR = Path(os.getenv("GOTELEGRAM_ADMIN_DIR", "/opt/gotelegram-admin")) STATIC_DIR = Path(os.getenv("GOTELEGRAM_ADMIN_STATIC", str(ADMIN_DIR / "static"))) -TOKEN_FILE = Path(os.getenv("GOTELEGRAM_ADMIN_TOKEN_FILE", str(ADMIN_DIR / "token"))) GOTELEGRAM_CONFIG = Path(os.getenv("GOTELEGRAM_CONFIG", "/opt/gotelegram/config.json")) TELEMT_CONFIG = Path(os.getenv("TELEMT_CONFIG", "/etc/telemt/config.toml")) @@ -63,21 +61,6 @@ def run(cmd: list[str], timeout: int = 8) -> tuple[int, str, str]: return 125, "", str(exc) -def ensure_token() -> str: - ADMIN_DIR.mkdir(parents=True, exist_ok=True) - if not TOKEN_FILE.exists() or TOKEN_FILE.stat().st_size < 24: - TOKEN_FILE.write_text(secrets.token_urlsafe(36) + "\n", encoding="utf-8") - os.chmod(TOKEN_FILE, 0o600) - return TOKEN_FILE.read_text(encoding="utf-8").strip() - - -def current_token() -> str: - try: - return ensure_token() - except OSError: - return "" - - def load_json(path: Path, fallback: Any = None) -> Any: try: with path.open("r", encoding="utf-8") as fh: @@ -353,27 +336,6 @@ class AdminHandler(BaseHTTPRequestHandler): def log_message(self, fmt: str, *args: Any) -> None: print("%s - %s" % (self.address_string(), fmt % args)) - def token_from_request(self) -> str: - parsed = urllib.parse.urlparse(self.path) - qs = urllib.parse.parse_qs(parsed.query) - if qs.get("token", [""])[0]: - return qs["token"][0] - auth = self.headers.get("Authorization", "") - if auth.startswith("Bearer "): - return auth[len("Bearer "):].strip() - cookie_header = self.headers.get("Cookie", "") - if cookie_header: - cookie = http.cookies.SimpleCookie() - cookie.load(cookie_header) - if "gtauth" in cookie: - return cookie["gtauth"].value - return "" - - def is_authorized(self) -> bool: - token = current_token() - candidate = self.token_from_request() - return bool(token and candidate and secrets.compare_digest(token, candidate)) - def send_json(self, payload: Any, status: int = 200) -> None: body = json.dumps(payload, ensure_ascii=False).encode("utf-8") self.send_response(status) @@ -394,12 +356,6 @@ class AdminHandler(BaseHTTPRequestHandler): return {} return json.loads(self.rfile.read(length).decode("utf-8")) - def require_auth(self) -> bool: - if self.is_authorized(): - return True - self.send_error_json(401, "unauthorized") - return False - def require_write_guard(self) -> bool: if self.command in {"POST", "PUT", "PATCH", "DELETE"} and self.headers.get("X-GoTelegram-Admin") != "1": self.send_error_json(403, "missing write guard") @@ -407,8 +363,6 @@ class AdminHandler(BaseHTTPRequestHandler): return True def route_get_api(self, parsed: urllib.parse.ParseResult) -> None: - if not self.require_auth(): - return path = parsed.path if path == "/api/overview": self.send_json({"ok": True, "data": overview_payload()}) @@ -437,7 +391,7 @@ class AdminHandler(BaseHTTPRequestHandler): self.send_error_json(404, "not found") def route_post_api(self, parsed: urllib.parse.ParseResult) -> None: - if not self.require_auth() or not self.require_write_guard(): + if not self.require_write_guard(): return path = parsed.path try: @@ -480,7 +434,7 @@ class AdminHandler(BaseHTTPRequestHandler): self.send_error_json(404, "not found") def route_delete_api(self, parsed: urllib.parse.ParseResult) -> None: - if not self.require_auth() or not self.require_write_guard(): + if not self.require_write_guard(): return path = parsed.path if not path.startswith("/api/users/"): @@ -504,15 +458,6 @@ class AdminHandler(BaseHTTPRequestHandler): self.send_json({"ok": True, "restarted": restarted}) def send_static(self, parsed: urllib.parse.ParseResult) -> None: - qs = urllib.parse.parse_qs(parsed.query) - if qs.get("token", [""])[0] and self.is_authorized(): - self.send_response(302) - self.send_header("Location", "/") - self.send_header("Set-Cookie", "gtauth=%s; Path=/; HttpOnly; SameSite=Strict" % qs["token"][0]) - self.send_header("Cache-Control", "no-store") - self.end_headers() - return - rel = parsed.path.lstrip("/") or "index.html" if rel.startswith("api/") or ".." in rel.split("/"): self.send_error(404) @@ -558,7 +503,6 @@ class AdminHandler(BaseHTTPRequestHandler): def main() -> None: - ensure_token() if not STATIC_DIR.exists(): raise SystemExit(f"static dir not found: {STATIC_DIR}") httpd = ThreadingHTTPServer((HOST, PORT), AdminHandler) diff --git a/admin-web/static/app.js b/admin-web/static/app.js index 0865b40..9bb519a 100644 --- a/admin-web/static/app.js +++ b/admin-web/static/app.js @@ -36,10 +36,6 @@ async function api(path, options = {}) { }; if (options.body && !headers["Content-Type"]) headers["Content-Type"] = "application/json"; const res = await fetch(path, { ...options, headers, credentials: "same-origin" }); - if (res.status === 401) { - $("#authLock").classList.remove("hidden"); - throw new Error("Unauthorized"); - } const data = await res.json().catch(() => ({})); if (!res.ok || data.ok === false) throw new Error(data.error || `HTTP ${res.status}`); return data.data ?? data; diff --git a/admin-web/static/index.html b/admin-web/static/index.html index 2b0970c..a844338 100644 --- a/admin-web/static/index.html +++ b/admin-web/static/index.html @@ -7,14 +7,6 @@ - -