feat: add admin web basic auth

This commit is contained in:
Андрей Бобырев
2026-06-06 12:36:37 +03:00
parent f6eb56b88d
commit a01e0ddfba
2 changed files with 69 additions and 1 deletions

View File

@@ -50,6 +50,33 @@ ssh root@SERVER 'chmod +x /opt/gotelegram/install.sh /opt/gotelegram/install_got
| `GOTELEGRAM_BASE` | `https://raw.githubusercontent.com/andrey271192/gotelegram/main` | база загрузки файлов |
| `GOTELEGRAM_INSTALL_DIR` | `/opt/gotelegram` | путь установки |
| `GITHUB_TOKEN` / `GH_TOKEN` | пусто | token для private GitHub raw downloads |
| `GOTELEGRAM_ADMIN_HOST` | `127.0.0.1` | bind web-admin |
| `GOTELEGRAM_ADMIN_PORT` | `1984` | port web-admin |
| `GOTELEGRAM_ADMIN_USER` | `admin` | Basic Auth login |
| `GOTELEGRAM_ADMIN_PASSWORD` | пусто | Basic Auth password; обязателен при public bind |
## Web-admin
После первого запуска `gotelegram` локальная web-admin панель ставится как `gotelegram-admin.service`.
Безопасный локальный доступ через SSH tunnel:
```bash
ssh -L 1984:127.0.0.1:1984 root@SERVER
open http://127.0.0.1:1984/
```
Для public bind сначала задайте пароль через systemd override или reverse proxy:
```ini
[Service]
Environment=GOTELEGRAM_ADMIN_HOST=0.0.0.0
Environment=GOTELEGRAM_ADMIN_PORT=1984
Environment=GOTELEGRAM_ADMIN_USER=admin
Environment=GOTELEGRAM_ADMIN_PASSWORD=change-me
```
Без HTTPS public Basic Auth гонит пароль открытым текстом. Для постоянного доступа лучше reverse proxy с TLS.
## Проверки

View File

@@ -3,14 +3,17 @@
goTelegram Pro local web admin.
The service is intentionally bound to 127.0.0.1:1984. Operators reach it
through an SSH tunnel; it must never be exposed directly on the public network.
through an SSH tunnel by default. If exposed through a reverse proxy or public
bind, enable GOTELEGRAM_ADMIN_PASSWORD.
"""
from __future__ import annotations
import base64
import csv
import fcntl
import hashlib
import hmac
import json
import mimetypes
import os
@@ -48,6 +51,9 @@ BACKUP_RESTORE_LOG = Path(os.getenv("GOTELEGRAM_BACKUP_RESTORE_LOG", "/var/log/g
HOST = os.getenv("GOTELEGRAM_ADMIN_HOST", "127.0.0.1")
PORT = int(os.getenv("GOTELEGRAM_ADMIN_PORT", "1984"))
ADMIN_USER = os.getenv("GOTELEGRAM_ADMIN_USER", "admin")
ADMIN_PASSWORD = os.getenv("GOTELEGRAM_ADMIN_PASSWORD", "")
ADMIN_REALM = os.getenv("GOTELEGRAM_ADMIN_REALM", "GoTelegram")
VERSION = "2.5.0"
USER_RE = re.compile(r"^[A-Za-z0-9_.-]{1,48}$")
LANG_RE = re.compile(r"^(en|ru)$")
@@ -1281,6 +1287,35 @@ class AdminHandler(BaseHTTPRequestHandler):
def log_message(self, fmt: str, *args: Any) -> None:
print("%s - %s" % (self.address_string(), fmt % args))
def auth_enabled(self) -> bool:
return bool(ADMIN_PASSWORD)
def is_authorized(self) -> bool:
if not self.auth_enabled():
return True
header = self.headers.get("Authorization", "")
if not header.startswith("Basic "):
return False
try:
decoded = base64.b64decode(header[6:].strip(), validate=True).decode("utf-8")
username, password = decoded.split(":", 1)
except Exception:
return False
return hmac.compare_digest(username, ADMIN_USER) and hmac.compare_digest(password, ADMIN_PASSWORD)
def require_auth(self) -> bool:
if self.is_authorized():
return True
body = b"Authentication required\n"
self.send_response(401)
self.send_header("WWW-Authenticate", f'Basic realm="{ADMIN_REALM}"')
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
return False
def send_json(self, payload: Any, status: int = 200) -> None:
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
@@ -1632,6 +1667,8 @@ class AdminHandler(BaseHTTPRequestHandler):
self.wfile.write(body)
def do_GET(self) -> None:
if not self.require_auth():
return
parsed = urllib.parse.urlparse(self.path)
if parsed.path.startswith("/api/"):
self.route_get_api(parsed)
@@ -1639,6 +1676,8 @@ class AdminHandler(BaseHTTPRequestHandler):
self.send_static(parsed)
def do_POST(self) -> None:
if not self.require_auth():
return
parsed = urllib.parse.urlparse(self.path)
if parsed.path.startswith("/api/"):
self.route_post_api(parsed)
@@ -1646,6 +1685,8 @@ class AdminHandler(BaseHTTPRequestHandler):
self.send_error(404)
def do_DELETE(self) -> None:
if not self.require_auth():
return
parsed = urllib.parse.urlparse(self.path)
if parsed.path.startswith("/api/"):
self.route_delete_api(parsed)