From 9d23cd8ac7cd42478eed41e6cc5c8dde71e89a2c Mon Sep 17 00:00:00 2001 From: Nirvana Date: Thu, 22 Oct 2026 07:47:13 +0200 Subject: [PATCH] eon: use saved session --- lib/streaming_providers/providers/eon/auth.py | 1 - .../providers/eon/persistence.py | 84 ++++++++++++++----- 2 files changed, 61 insertions(+), 24 deletions(-) diff --git a/lib/streaming_providers/providers/eon/auth.py b/lib/streaming_providers/providers/eon/auth.py index d16ccd8..049a415 100644 --- a/lib/streaming_providers/providers/eon/auth.py +++ b/lib/streaming_providers/providers/eon/auth.py @@ -51,7 +51,6 @@ typed errors only, nothing secret in logs (lengths at most). """ import hashlib -import json import secrets import string import threading diff --git a/lib/streaming_providers/providers/eon/persistence.py b/lib/streaming_providers/providers/eon/persistence.py index e156099..63342e5 100644 --- a/lib/streaming_providers/providers/eon/persistence.py +++ b/lib/streaming_providers/providers/eon/persistence.py @@ -4,7 +4,10 @@ Glue between EonAuth and the framework's storage managers. SessionStore -- EonStore on top of SessionManager: one dict at session.json[][] (country LOWER-case, as - the managers key by it). Read once, written through. + the managers key by it). STATELESS view: every get / set / + delete re-reads through SessionManager, so keys this process + did not write are preserved and the store never overwrites + the on-disk entry with a stale snapshot. default_persistence() -- finds a SessionManager and a credentials loader the way BaseAuthenticator does (injected settings manager, else a SettingsManager of its own); anything missing degrades to @@ -15,6 +18,20 @@ SessionManager logs every value of keys it does not know at INFO (`Saving session data ...`): never hand this store a secret that can be re-derived. EonAuth persists device identity and the refresh token only (the refresh token is on SessionManager's mask list). + +Why there is NO local cache here +-------------------------------- +SessionManager.load_session re-reads session.json on every call and +save_session REPLACES the dict at session.json[provider][country]. A local +cache that is populated on the first read and written through on set would +therefore: + + * hide keys written to disk after the cache was populated (e.g. another + provider, another process, or the same provider in a previous run), and + * on the first set(), save the stale cache back and ERASE those keys. + +That is exactly how an existing eon device identity got wiped and a new device +was registered (auth_device_forbidden). Read-modify-write on every mutation. """ import threading @@ -25,51 +42,66 @@ from .auth import EonStore, MemoryStore class SessionStore: + """EonStore backed by SessionManager, with no local state. + + Each operation goes to SessionManager, which reads session.json and (for + writes) replaces the provider/country sub-dict. The re-read inside + set_many / delete makes the write a merge into the CURRENT on-disk state + instead of a write-through of a snapshot taken at process start. + """ + def __init__(self, session_manager: Any, provider: str, country: Optional[str]): self._sm = session_manager self._provider = provider - self._country = country or None + # Normalise here: a caller passing "BA" must not open a parallel + # eon.BA subtree next to the eon.ba one the managers use. + self._country = country.lower() if country else None self._lock = threading.RLock() - self._cache: Optional[Dict[str, str]] = None - def _data(self) -> Dict[str, str]: - if self._cache is None: - loaded = self._sm.load_session(self._provider, self._country) - self._cache = {k: v for k, v in (loaded or {}).items() if isinstance(v, str)} - return self._cache + # ------------------------------------------------------------------ + def _read(self) -> Dict[str, str]: + """Current on-disk dict for provider/country (never cached).""" + loaded = self._sm.load_session(self._provider, self._country) + if not loaded: + return {} + return {k: v for k, v in loaded.items() if isinstance(v, str)} def get(self, key: str) -> Optional[str]: with self._lock: - return self._data().get(key) + return self._read().get(key) def set(self, key: str, value: str) -> None: self.set_many({key: value}) def set_many(self, values: Dict[str, str]) -> None: with self._lock: - data = self._data() + data = self._read() if all(data.get(k) == v for k, v in values.items()): return data.update(values) - self._save() + if not self._sm.save_session(self._provider, data, self._country): + logger.warning(f"{self._provider}: could not persist the session") def delete(self, key: str) -> None: with self._lock: - data = self._data() - if key in data: - del data[key] - self._save() - - def _save(self) -> None: - # save_session REPLACES the dict at that path: write the whole cache. - if not self._sm.save_session(self._provider, dict(self._data()), self._country): - logger.warning(f"{self._provider}: could not persist the session") + data = self._read() + if key not in data: + return + del data[key] + if not self._sm.save_session(self._provider, data, self._country): + logger.warning(f"{self._provider}: could not persist the session") def default_persistence( *, provider: str, country: str, settings_manager: Any = None, ) -> Tuple[EonStore, Optional[Callable[[], Any]]]: - """(store, credentials_loader); never raises.""" + """(store, credentials_loader); never raises. + + The SessionManager is the SHARED one when the settings manager exposes it; + a fresh SessionManager() is only a last resort (it still reads the same + session.json via VFS) and is logged, because a silently different store is + how a provider ends up registering a brand-new device identity. + """ manager = settings_manager if manager is None: try: @@ -83,13 +115,19 @@ def default_persistence( except Exception as exc: logger.warning(f"{provider}: could not register with the settings manager: {exc!r}") - session_manager = getattr(manager, "session_manager", None) + session_manager = getattr(manager, "session_manager", None) if manager is not None else None if session_manager is None: + logger.warning( + f"{provider}: settings manager exposes no session_manager; " + f"creating a standalone SessionManager (same session.json, different instance)" + ) try: from ...base.auth.session_manager import SessionManager session_manager = SessionManager() except Exception as exc: - logger.warning(f"{provider}: no SessionManager ({exc!r}); device identity is NOT persisted") + logger.warning( + f"{provider}: no SessionManager ({exc!r}); device identity is NOT persisted" + ) store: EonStore = ( SessionStore(session_manager, provider, country) if session_manager is not None