fix(ui): ne plus réclamer un code déjà validé après une reconnexion (v0.8.6)

Le client tenait pour acquis que l'autorisation ne survivait pas à la chute
du tunnel — un commentaire l'affirmait noir sur blanc. C'est l'inverse : le
serveur autorise l'adresse de tunnel du pair jusqu'à l'échéance enregistrée,
et cette adresse ne change pas d'une reconnexion à l'autre. Se reconnecter
ne révoque rien ; seul le client oubliait, et redemandait un code que le
serveur avait déjà accepté.

Il pose maintenant la question au lieu de la supposer, via GET /api/session
(serveur >= 0.3.0), dès que le tunnel monte : démarrage sur un tunnel déjà
actif, reconnexion automatique, ou avant toute invite de saisie. Le serveur
identifie l'appelant à l'IP source, aucun identifiant ne circule.

Serveur injoignable, réponse illisible ou serveur antérieur à la 0.3.0 ne
valent pas « accès ouvert » : sans information, le client ne conclut rien et
laisse le bouton disponible — le sens sûr, et le comportement de la 0.8.5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 18:27:32 +02:00
co-authored by Claude Opus 5
parent 4cf3e26dff
commit 306e512aab
7 changed files with 127 additions and 14 deletions
+42
View File
@@ -86,6 +86,48 @@ def api_base_url(cfg: Config) -> str:
return f"http://{gateway}:8000"
def session_status(cfg: Config) -> AuthResult:
"""Demande au serveur si l'accès de ce poste est déjà ouvert.
Un tunnel qui tombe et remonte ne révoque rien côté serveur :
l'autorisation porte sur l'adresse du pair, et cette adresse ne change
pas. Le client, lui, oubliait tout à chaque reconnexion et réclamait un
code déjà validé — c'est ce que cette question évite.
Aucun identifiant n'est envoyé : le serveur reconnaît l'appelant à l'IP
source du tunnel, et ne renseigne donc jamais sur un autre compte que
celui qui parle. Rien à demander avant que le tunnel ne soit monté,
l'API n'étant joignable que par l'intérieur.
Un `ok` faux ne distingue pas « accès fermé » de « serveur injoignable » :
l'appelant traite les deux pareil — il redemande un code, ce qui est le
comportement sûr dans le doute.
"""
base = api_base_url(cfg)
if not base:
return AuthResult(False, "Adresse du serveur d'authentification inconnue.")
request = urllib.request.Request(f"{base}/api/session", method="GET")
try:
with urllib.request.urlopen(request, timeout=_TIMEOUT) as response:
body = json.loads(response.read().decode("utf-8"))
until = _parse_until(body.get("access_until"))
if until is None:
return AuthResult(True, "Accès réseau déjà ouvert")
return AuthResult(True, "Accès réseau déjà ouvert jusqu'à "
f"{until.astimezone():%d/%m/%Y %H:%M}", until)
except urllib.error.HTTPError as exc:
try:
detail = json.loads(exc.read().decode("utf-8")).get("detail", "")
except (ValueError, OSError):
detail = ""
return AuthResult(False, detail or f"Accès fermé (code {exc.code})")
except (urllib.error.URLError, OSError, ValueError) as exc:
# Serveur ancien (route absente), injoignable, ou réponse illisible :
# aucune information, donc aucune conclusion — on ne prétend pas que
# l'accès est ouvert.
return AuthResult(False, f"État de l'accès inconnu : {exc}")
def authenticate(cfg: Config, code: str, username: str = "") -> AuthResult:
"""Valide un code TOTP auprès du serveur et ouvre l'accès réseau.