From 2f3607e1e63b22159cc1169582077fe09ec5287a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B8?= =?UTF-8?q?=D1=82=D0=B2=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 25 Apr 2026 17:50:06 +0300 Subject: [PATCH] v2.5.0: harden telemt user reloads --- admin-web/server.py | 11 +++++++++ gotelegram-bot/bot.py | 12 +++++++++- install.sh | 2 +- lib/telemt_config.sh | 19 ++++++++++++++++ tests/test_admin_features.py | 43 ++++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 2 deletions(-) diff --git a/admin-web/server.py b/admin-web/server.py index 77996d7..43ffb3b 100644 --- a/admin-web/server.py +++ b/admin-web/server.py @@ -54,6 +54,8 @@ LANG_RE = re.compile(r"^(en|ru)$") SENSITIVE_CONFIG_KEYS = {"secret"} BACKUP_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]+\.tar\.gz(\.enc)?$") MAX_UNIQUE_IP_LIMIT = 1000000 +TELEMT_RESTART_DEBOUNCE_SECONDS = float(os.getenv("GOTELEGRAM_TELEMT_RESTART_DEBOUNCE", "8")) +_LAST_TELEMT_RESTART = 0.0 TRAFFIC_WINDOWS = { "15m": 15 * 60, "1h": 60 * 60, @@ -405,6 +407,15 @@ def restart_service(name: str) -> bool: def request_service_restart(name: str) -> bool: + global _LAST_TELEMT_RESTART + if name == "telemt": + now = time.monotonic() + if _LAST_TELEMT_RESTART > 0 and now - _LAST_TELEMT_RESTART < TELEMT_RESTART_DEBOUNCE_SECONDS: + status = service_status(name) + if status in {"running", "activating"}: + return True + run(["systemctl", "reset-failed", name], timeout=5) + _LAST_TELEMT_RESTART = now code, _, _ = run(["systemctl", "--no-block", "restart", name], timeout=5) return code == 0 diff --git a/gotelegram-bot/bot.py b/gotelegram-bot/bot.py index 3e071a4..014a8ef 100644 --- a/gotelegram-bot/bot.py +++ b/gotelegram-bot/bot.py @@ -279,6 +279,8 @@ _DOMAIN_RE = re.compile( ) _USER_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]{1,48}$") MAX_UNIQUE_IP_LIMIT = 1000000 +TELEMT_RESTART_DEBOUNCE_SECONDS = float(os.getenv("GOTELEGRAM_TELEMT_RESTART_DEBOUNCE", "8")) +_LAST_TELEMT_RESTART = 0.0 class FileLock: @@ -1638,7 +1640,15 @@ def save_telemt_users(users: Dict[str, str]) -> bool: async def refresh_telemt_after_user_change() -> bool: - """Restart telemt after config user changes.""" + """Restart telemt after config user changes, coalescing rapid UI clicks.""" + global _LAST_TELEMT_RESTART + now = time.monotonic() + if _LAST_TELEMT_RESTART > 0 and now - _LAST_TELEMT_RESTART < TELEMT_RESTART_DEBOUNCE_SECONDS: + code, stdout, _ = await sh("systemctl", "is-active", TELEMT_SERVICE, timeout=5) + if code == 0 and stdout.strip() == "active": + return True + await sh("systemctl", "reset-failed", TELEMT_SERVICE, timeout=5) + _LAST_TELEMT_RESTART = now code, _, _ = await sh("systemctl", "--no-block", "restart", TELEMT_SERVICE, timeout=5) return code == 0 diff --git a/install.sh b/install.sh index a8134d2..0211851 100755 --- a/install.sh +++ b/install.sh @@ -365,7 +365,7 @@ auto_migrate_legacy_state() { [ -z "$secret" ] && secret=$(first_telemt_user_secret "$TELEMT_CONFIG" 2>/dev/null || echo "") [ -z "$secret" ] && secret=$(generate_hex 32) - if [ -n "$users_block" ] && ! printf '%s\n' "$users_block" | grep -qE '^[[:space:]]*main[[:space:]]*='; then + if [ -n "$users_block" ] && ! telemt_users_block_has_main "$users_block"; then users_block=$(printf 'main = "%s"\n%s\n' "$secret" "$users_block") users_block_needs_write=1 fi diff --git a/lib/telemt_config.sh b/lib/telemt_config.sh index d5738fb..a4bdd76 100755 --- a/lib/telemt_config.sh +++ b/lib/telemt_config.sh @@ -210,6 +210,25 @@ first_telemt_user_secret() { get_telemt_users_block "$config" | head -1 | sed 's/^[^=]*=[[:space:]]*//; s/^"//; s/".*$//' | tr -d ' ' } +telemt_users_block_has_main() { + local users_block="$1" + printf '%s\n' "$users_block" | awk -F= ' + /^[[:space:]]*#/ || ! /=/ { next } + { + key=$1 + gsub(/^[[:space:]]+|[[:space:]]+$/, "", key) + if (key ~ /^".*"$/ || key ~ /^\047.*\047$/) { + key=substr(key, 2, length(key) - 2) + } + if (key == "main") { + found=1 + exit + } + } + END { exit found ? 0 : 1 } + ' +} + replace_telemt_users_block() { local users_block="$1" local config="${2:-$TELEMT_CONFIG}" diff --git a/tests/test_admin_features.py b/tests/test_admin_features.py index f5bd4d5..9e4ce74 100644 --- a/tests/test_admin_features.py +++ b/tests/test_admin_features.py @@ -243,6 +243,49 @@ class AdminFeatureTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.strip(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + def test_telemt_users_block_detects_quoted_main_user(self): + script = "\n".join([ + "set -e", + f"source {shlex.quote(str(ROOT / 'lib' / 'telemt_config.sh'))}", + "block=$'\"main\" = \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\\n\"client\" = \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"'", + "telemt_users_block_has_main \"$block\"", + ]) + + result = subprocess.run( + ["bash", "-lc", script], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + + def test_telemt_restart_requests_are_debounced(self): + with tempfile.TemporaryDirectory() as raw: + server = load_server(Path(raw)) + calls = [] + original_run = server.run + original_status = server.service_status + try: + server._LAST_TELEMT_RESTART = 0.0 + server.TELEMT_RESTART_DEBOUNCE_SECONDS = 30.0 + server.service_status = lambda name: "running" + + def fake_run(cmd, timeout=8): + calls.append(cmd) + return 0, "", "" + + server.run = fake_run + self.assertTrue(server.request_service_restart("telemt")) + self.assertTrue(server.request_service_restart("telemt")) + finally: + server.run = original_run + server.service_status = original_status + + restart_calls = [cmd for cmd in calls if cmd[:3] == ["systemctl", "--no-block", "restart"]] + self.assertEqual(len(restart_calls), 1) + if __name__ == "__main__": unittest.main()