v2.5.0: harden telemt user reloads

This commit is contained in:
Виталий Литвинов
2026-04-25 17:50:06 +03:00
parent 5a6bc9f614
commit 2f3607e1e6
5 changed files with 85 additions and 2 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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}"

View File

@@ -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()