mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 14:36:05 +00:00
v2.5.0: harden admin key and traffic flows
This commit is contained in:
@@ -8,6 +8,7 @@ Supports EN/RU UI with per-user language preferences.
|
||||
|
||||
import asyncio
|
||||
import csv
|
||||
import fcntl
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
@@ -104,6 +105,7 @@ logger = logging.getLogger(__name__)
|
||||
GOTELEGRAM_VERSION = "2.5.0"
|
||||
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
||||
DISABLED_USERS_FILE = "/opt/gotelegram/disabled_users.json"
|
||||
USER_LOCK_FILE = "/run/gotelegram/admin-users.lock"
|
||||
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
||||
TELEMT_SERVICE = "telemt"
|
||||
WEBSITE_ROOT = "/var/www/gotelegram-site"
|
||||
@@ -264,6 +266,23 @@ _DOMAIN_RE = re.compile(
|
||||
_USER_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]{1,48}$")
|
||||
|
||||
|
||||
class FileLock:
|
||||
def __init__(self, path: str):
|
||||
self.path = Path(path)
|
||||
self.handle = None
|
||||
|
||||
def __enter__(self):
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.handle = self.path.open("w", encoding="utf-8")
|
||||
fcntl.flock(self.handle.fileno(), fcntl.LOCK_EX)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
if self.handle:
|
||||
fcntl.flock(self.handle.fileno(), fcntl.LOCK_UN)
|
||||
self.handle.close()
|
||||
|
||||
|
||||
async def run_bot_action(action: str, timeout: int = 300, **params) -> Dict:
|
||||
"""Invoke install.sh --action=X --json and parse the JSON result.
|
||||
|
||||
@@ -1401,6 +1420,19 @@ async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
# PROXY LINK & SHARE
|
||||
# ============================================================================
|
||||
|
||||
def quote_toml_key(name: str) -> str:
|
||||
escaped = name.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def ordered_user_lines(users: Dict[str, str]) -> List[str]:
|
||||
names: List[str] = []
|
||||
if "main" in users:
|
||||
names.append("main")
|
||||
names.extend(sorted(name for name in users if name != "main"))
|
||||
return [f'{quote_toml_key(name)} = "{users[name]}"' for name in names]
|
||||
|
||||
|
||||
def load_telemt_users() -> Dict[str, str]:
|
||||
"""Return users from [access.users] in telemt config."""
|
||||
telemt_cfg = load_toml(TELEMT_CONFIG) or {}
|
||||
@@ -1458,14 +1490,39 @@ def load_user_records() -> Dict[str, Dict[str, Any]]:
|
||||
|
||||
def save_telemt_users(users: Dict[str, str]) -> bool:
|
||||
"""Persist [access.users] while keeping the rest of the TOML structure."""
|
||||
telemt_cfg = load_toml(TELEMT_CONFIG) or {}
|
||||
access = telemt_cfg.setdefault("access", {})
|
||||
access["users"] = dict(sorted(users.items()))
|
||||
try:
|
||||
os.makedirs(os.path.dirname(TELEMT_CONFIG), exist_ok=True)
|
||||
with open(TELEMT_CONFIG, "w") as f:
|
||||
toml.dump(telemt_cfg, f)
|
||||
os.chmod(TELEMT_CONFIG, 0o600)
|
||||
if os.path.exists(TELEMT_CONFIG):
|
||||
with open(TELEMT_CONFIG, "r", encoding="utf-8", errors="ignore") as f:
|
||||
lines = f.read().splitlines()
|
||||
else:
|
||||
lines = []
|
||||
rendered = ordered_user_lines(users)
|
||||
out: List[str] = []
|
||||
in_users = False
|
||||
found = False
|
||||
for raw in lines:
|
||||
if raw.strip() == "[access.users]":
|
||||
found = True
|
||||
in_users = True
|
||||
out.append(raw)
|
||||
out.extend(rendered)
|
||||
continue
|
||||
if in_users and raw.strip().startswith("["):
|
||||
in_users = False
|
||||
if in_users:
|
||||
continue
|
||||
out.append(raw)
|
||||
if not found:
|
||||
if out and out[-1].strip():
|
||||
out.append("")
|
||||
out.append("[access.users]")
|
||||
out.extend(rendered)
|
||||
tmp = f"{TELEMT_CONFIG}.tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(out).rstrip() + "\n")
|
||||
os.chmod(tmp, 0o600)
|
||||
os.replace(tmp, TELEMT_CONFIG)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save telemt users: {e}")
|
||||
@@ -1474,7 +1531,7 @@ def save_telemt_users(users: Dict[str, str]) -> bool:
|
||||
|
||||
async def refresh_telemt_after_user_change() -> bool:
|
||||
"""Restart telemt after config user changes."""
|
||||
code, _, _ = await sh("systemctl", "restart", TELEMT_SERVICE, timeout=20)
|
||||
code, _, _ = await sh("systemctl", "--no-block", "restart", TELEMT_SERVICE, timeout=5)
|
||||
return code == 0
|
||||
|
||||
|
||||
@@ -1783,25 +1840,26 @@ async def cb_user_toggle(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
if name == "main":
|
||||
await query.answer("main нельзя отключить", show_alert=True)
|
||||
return
|
||||
active = load_telemt_users()
|
||||
disabled = load_disabled_users()
|
||||
records = load_user_records()
|
||||
record = records.get(name)
|
||||
if not record:
|
||||
await query.answer("Ключ не найден", show_alert=True)
|
||||
return
|
||||
enabled = not bool(record.get("enabled"))
|
||||
secret = str(record.get("secret", ""))
|
||||
if enabled:
|
||||
disabled.pop(name, None)
|
||||
active[name] = secret
|
||||
else:
|
||||
active.pop(name, None)
|
||||
disabled[name] = secret
|
||||
if enabled:
|
||||
saved = save_telemt_users(active) and save_disabled_users(disabled)
|
||||
else:
|
||||
saved = save_disabled_users(disabled) and save_telemt_users(active)
|
||||
with FileLock(USER_LOCK_FILE):
|
||||
active = load_telemt_users()
|
||||
disabled = load_disabled_users()
|
||||
records = load_user_records()
|
||||
record = records.get(name)
|
||||
if not record:
|
||||
await query.answer("Ключ не найден", show_alert=True)
|
||||
return
|
||||
enabled = not bool(record.get("enabled"))
|
||||
secret = str(record.get("secret", ""))
|
||||
if enabled:
|
||||
disabled.pop(name, None)
|
||||
active[name] = secret
|
||||
else:
|
||||
active.pop(name, None)
|
||||
disabled[name] = secret
|
||||
if enabled:
|
||||
saved = save_telemt_users(active) and save_disabled_users(disabled)
|
||||
else:
|
||||
saved = save_disabled_users(disabled) and save_telemt_users(active)
|
||||
if not saved:
|
||||
await safe_edit_message(query, "❌ Не удалось сохранить состояние ключа")
|
||||
return
|
||||
@@ -1835,15 +1893,17 @@ async def cb_user_delete_confirm(update: Update, context: ContextTypes.DEFAULT_T
|
||||
await query.answer()
|
||||
user_id = _uid(update)
|
||||
name = query.data.removeprefix("user_del_yes_")
|
||||
active = load_telemt_users()
|
||||
disabled = load_disabled_users()
|
||||
records = load_user_records()
|
||||
if name == "main" or name not in records:
|
||||
await query.answer("Нельзя удалить этот ключ", show_alert=True)
|
||||
return
|
||||
active.pop(name, None)
|
||||
disabled.pop(name, None)
|
||||
if not save_telemt_users(active) or not save_disabled_users(disabled):
|
||||
with FileLock(USER_LOCK_FILE):
|
||||
active = load_telemt_users()
|
||||
disabled = load_disabled_users()
|
||||
records = load_user_records()
|
||||
if name == "main" or name not in records:
|
||||
await query.answer("Нельзя удалить этот ключ", show_alert=True)
|
||||
return
|
||||
active.pop(name, None)
|
||||
disabled.pop(name, None)
|
||||
saved = save_telemt_users(active) and save_disabled_users(disabled)
|
||||
if not saved:
|
||||
await safe_edit_message(query, "❌ Не удалось сохранить config.toml")
|
||||
return
|
||||
await refresh_telemt_after_user_change()
|
||||
@@ -1860,14 +1920,16 @@ async def create_user_from_text(update: Update, context: ContextTypes.DEFAULT_TY
|
||||
if not _USER_NAME_RE.match(name):
|
||||
await update.message.reply_text("❌ Некорректное имя. Используйте латиницу, цифры, _ . - и до 48 символов.")
|
||||
return
|
||||
records = load_user_records()
|
||||
if name in records:
|
||||
await update.message.reply_text("❌ Такой пользователь уже есть.")
|
||||
return
|
||||
users = load_telemt_users()
|
||||
secret = hashlib.sha256(f"{name}:{time.time()}:{os.urandom(16).hex()}".encode()).hexdigest()[:32]
|
||||
users[name] = secret
|
||||
if not save_telemt_users(users):
|
||||
with FileLock(USER_LOCK_FILE):
|
||||
records = load_user_records()
|
||||
if name in records:
|
||||
await update.message.reply_text("❌ Такой пользователь уже есть.")
|
||||
return
|
||||
users = load_telemt_users()
|
||||
secret = hashlib.sha256(f"{name}:{time.time()}:{os.urandom(16).hex()}".encode()).hexdigest()[:32]
|
||||
users[name] = secret
|
||||
saved = save_telemt_users(users)
|
||||
if not saved:
|
||||
await update.message.reply_text("❌ Не удалось сохранить /etc/telemt/config.toml")
|
||||
return
|
||||
await refresh_telemt_after_user_change()
|
||||
|
||||
Reference in New Issue
Block a user