From 0a1e3341693fb9eb23d387846bb5896515b8302b Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 31 Oct 2026 18:06:11 +0100 Subject: [PATCH] =?UTF-8?q?fix(satellite):=20FritzBox-Tastenbestaetigung?= =?UTF-8?q?=20=E2=80=94=20Token=20im=20SOAP-Header=20mitschicken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An der Kellerbox kam nach dem Tastendruck "stopped" statt "authenticated". Laut AVM "TR-064 Support – Authentication" (Kap. 6.4) muss der Token aus X_AVM-DE_Auth SetConfig(start) bei GetState UND beim Wiederholen der Aktion als im SOAP-Header stehen — ohne ihn ordnet die Box die Bestaetigung nicht unserem Vorgang zu. - _tr064(token=…) setzt den Header. - GetState und die wiederholte Aktion laufen mit dem Token; Stop ebenso. - Zustaende werden geloggt und erklaert (stopped/blocked/ anotherauthprocess/failure); 867 (gesperrt, bis 60 min) und 868 (beschaeftigt, ~2 min) mit verstaendlicher Meldung. Getestet gegen eine nachgebaute Box, die sich wie in AVMs Doku verhaelt (ohne Token "stopped", mit Token "authenticated"). Co-Authored-By: Claude Opus 5.5 --- satellite/satellite.py | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/satellite/satellite.py b/satellite/satellite.py index f4d5c80..69e4ec5 100644 --- a/satellite/satellite.py +++ b/satellite/satellite.py @@ -1213,7 +1213,7 @@ def _do_snmp_info(params: dict) -> dict: # schlanker Reader. Braucht FritzBox-Login (Credential-Store, Typ 'fritzbox'). def _tr064(ip: str, user: str, pw: str, service: str, control: str, - action: str, args: Optional[dict] = None) -> dict: + action: str, args: Optional[dict] = None, token: str = "") -> dict: """Ein TR-064-SOAP-Call. Gibt {ok, fields|error}. fields = alle -Tags.""" import requests from requests.auth import HTTPDigestAuth @@ -1221,7 +1221,14 @@ def _tr064(ip: str, user: str, pw: str, service: str, control: str, envelope = ( '' '' + 's:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' + # Zusatzbestaetigung: der Token aus X_AVM-DE_Auth SetConfig(start) MUSS + # bei GetState und beim Wiederholen der Aktion im SOAP-Header stehen + # (AVM "TR-064 Support – Authentication", Kap. 6.4) — sonst ordnet die + # Box den Tastendruck nicht zu und meldet "stopped". + + (f'{token}' + if token else '') + + '' f'{body}' '' ) @@ -1374,18 +1381,27 @@ def _fb_second_factor(ip: str, user: str, pw: str) -> dict: if not st.get("ok"): return {"ok": False, "error": f"Bestaetigung nicht startbar: {st.get('error')}"} methods = st["fields"].get("NewMethods", "") + token = st["fields"].get("NewToken", "") logger.info("[fritzbox] %s: warte bis %.0fs auf Bestaetigung am Geraet (%s)", ip, FB_BUTTON_WAIT_SEC, methods) deadline = time.time() + FB_BUTTON_WAIT_SEC state = st["fields"].get("NewState", "") + reasons = {"stopped": "die Box hat den Vorgang beendet, ohne Bestaetigung (Zeit abgelaufen?)", + "blocked": "zu viele Versuche — die Box sperrt bis zu 60 Minuten", + "anotherauthprocess": "es laeuft schon eine Bestaetigung fuer einen anderen Benutzer", + "failure": "interner Fehler der Box", "disabled": "die Zusatzbestaetigung ist aus"} while time.time() < deadline: if state == "authenticated": - return {"ok": True} - if state in ("blocked", "failure", "stopped"): - return {"ok": False, "error": f"Bestaetigung abgebrochen (Box meldet '{state}')."} + logger.info("[fritzbox] %s: bestaetigt", ip) + return {"ok": True, "token": token} + if state in reasons and state != "disabled": + return {"ok": False, "error": f"Bestaetigung nicht erfolgt — Box meldet '{state}': {reasons[state]}."} time.sleep(2) - g = _tr064(ip, user, pw, auth["type"], auth["control"], "GetState") - state = g.get("fields", {}).get("NewState", state) if g.get("ok") else state - _tr064(ip, user, pw, auth["type"], auth["control"], "SetConfig", {"NewAction": "stop"}) + g = _tr064(ip, user, pw, auth["type"], auth["control"], "GetState", token=token) + new = g.get("fields", {}).get("NewState", state) if g.get("ok") else state + if new != state: + logger.info("[fritzbox] %s: Bestaetigung %s → %s", ip, state, new) + state = new + _tr064(ip, user, pw, auth["type"], auth["control"], "SetConfig", {"NewAction": "stop"}, token=token) return {"ok": False, "timeout": True, "error": f"Timeout: Die Taste an der Box wurde nicht innerhalb von {FB_BUTTON_WAIT_SEC:.0f} s " f"gedrueckt — abgebrochen, nichts ausgefuehrt." @@ -1426,7 +1442,12 @@ def _fb_call(ip: str, user: str, pw: str, params: dict) -> dict: sf = _fb_second_factor(ip, user, pw) if not sf.get("ok"): return sf - r = _tr064(ip, user, pw, svc["type"], svc["control"], action, args) + # Gleiche Aktion, gleiche Werte — jetzt mit dem Token im SOAP-Header + r = _tr064(ip, user, pw, svc["type"], svc["control"], action, args, token=sf.get("token", "")) + if not r.get("ok") and r.get("upnp_error") == "867": + return {"ok": False, "error": "Die Box sperrt die Bestaetigung (zu viele Versuche) — bis zu 60 Minuten warten."} + if not r.get("ok") and r.get("upnp_error") == "868": + return {"ok": False, "error": "Die Box ist mit einer anderen Bestaetigung beschaeftigt — in etwa 2 Minuten nochmal."} if not r.get("ok"): return r # Geheimnisse, die die Box auf Lese-Anfragen herausgibt (WLAN-Schluessel,