mirror of
https://github.com/andrey271192/PCAtelegram_web.git
synced 2026-09-20 11:55:32 +00:00
feat: add admin web basic auth
This commit is contained in:
27
README.md
27
README.md
@@ -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_BASE` | `https://raw.githubusercontent.com/andrey271192/gotelegram/main` | база загрузки файлов |
|
||||||
| `GOTELEGRAM_INSTALL_DIR` | `/opt/gotelegram` | путь установки |
|
| `GOTELEGRAM_INSTALL_DIR` | `/opt/gotelegram` | путь установки |
|
||||||
| `GITHUB_TOKEN` / `GH_TOKEN` | пусто | token для private GitHub raw downloads |
|
| `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.
|
||||||
|
|
||||||
## Проверки
|
## Проверки
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,17 @@
|
|||||||
goTelegram Pro local web admin.
|
goTelegram Pro local web admin.
|
||||||
|
|
||||||
The service is intentionally bound to 127.0.0.1:1984. Operators reach it
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
import csv
|
import csv
|
||||||
import fcntl
|
import fcntl
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import hmac
|
||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
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")
|
HOST = os.getenv("GOTELEGRAM_ADMIN_HOST", "127.0.0.1")
|
||||||
PORT = int(os.getenv("GOTELEGRAM_ADMIN_PORT", "1984"))
|
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"
|
VERSION = "2.5.0"
|
||||||
USER_RE = re.compile(r"^[A-Za-z0-9_.-]{1,48}$")
|
USER_RE = re.compile(r"^[A-Za-z0-9_.-]{1,48}$")
|
||||||
LANG_RE = re.compile(r"^(en|ru)$")
|
LANG_RE = re.compile(r"^(en|ru)$")
|
||||||
@@ -1281,6 +1287,35 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
def log_message(self, fmt: str, *args: Any) -> None:
|
def log_message(self, fmt: str, *args: Any) -> None:
|
||||||
print("%s - %s" % (self.address_string(), fmt % args))
|
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:
|
def send_json(self, payload: Any, status: int = 200) -> None:
|
||||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||||
self.send_response(status)
|
self.send_response(status)
|
||||||
@@ -1632,6 +1667,8 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
self.wfile.write(body)
|
self.wfile.write(body)
|
||||||
|
|
||||||
def do_GET(self) -> None:
|
def do_GET(self) -> None:
|
||||||
|
if not self.require_auth():
|
||||||
|
return
|
||||||
parsed = urllib.parse.urlparse(self.path)
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
if parsed.path.startswith("/api/"):
|
if parsed.path.startswith("/api/"):
|
||||||
self.route_get_api(parsed)
|
self.route_get_api(parsed)
|
||||||
@@ -1639,6 +1676,8 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
self.send_static(parsed)
|
self.send_static(parsed)
|
||||||
|
|
||||||
def do_POST(self) -> None:
|
def do_POST(self) -> None:
|
||||||
|
if not self.require_auth():
|
||||||
|
return
|
||||||
parsed = urllib.parse.urlparse(self.path)
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
if parsed.path.startswith("/api/"):
|
if parsed.path.startswith("/api/"):
|
||||||
self.route_post_api(parsed)
|
self.route_post_api(parsed)
|
||||||
@@ -1646,6 +1685,8 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
self.send_error(404)
|
self.send_error(404)
|
||||||
|
|
||||||
def do_DELETE(self) -> None:
|
def do_DELETE(self) -> None:
|
||||||
|
if not self.require_auth():
|
||||||
|
return
|
||||||
parsed = urllib.parse.urlparse(self.path)
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
if parsed.path.startswith("/api/"):
|
if parsed.path.startswith("/api/"):
|
||||||
self.route_delete_api(parsed)
|
self.route_delete_api(parsed)
|
||||||
|
|||||||
Reference in New Issue
Block a user