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:
+69
-10
@@ -18,6 +18,7 @@ from app.core import dns as dns_util
|
||||
from app.core import log as conn_log
|
||||
from app.core import history as hist
|
||||
from app.core import shutdown as shutdown_guard
|
||||
from app.core import vpn_session
|
||||
from app.ui.worker import TaskWorker, ValueWorker
|
||||
from app.ui.access_code_dialog import AccessCodeDialog
|
||||
from app.ui.admin_window import AdminWindow
|
||||
@@ -45,6 +46,12 @@ class MainWindow(QMainWindow):
|
||||
# signifie « aucune échéance connue » — soit aucun accès ouvert, soit
|
||||
# un serveur qui n'en annonce pas — et jamais « expire maintenant ».
|
||||
self._access_until: datetime | None = None
|
||||
# Sondage de l'état de l'accès auprès du serveur. `_probe_prompt`
|
||||
# retient qu'une saisie devra être proposée si le sondage en cours
|
||||
# revient « fermé » : la demande peut arriver alors qu'un sondage
|
||||
# déclenché sans invite est déjà parti.
|
||||
self._probe_worker: ValueWorker | None = None
|
||||
self._probe_prompt = False
|
||||
self._reconnect_failures = 0
|
||||
self._quitting = False
|
||||
self._warned_foreign: set[str] = set()
|
||||
@@ -663,12 +670,23 @@ class MainWindow(QMainWindow):
|
||||
except Exception:
|
||||
connected = False
|
||||
info = {}
|
||||
was_connected = self._is_connected
|
||||
self._is_connected = connected
|
||||
if connected:
|
||||
self._reconnect_failures = 0
|
||||
if not was_connected:
|
||||
# Tunnel monté à l'instant — démarrage de l'application sur un
|
||||
# tunnel déjà actif, reconnexion automatique, ou montage fait
|
||||
# hors de l'application. Demander au serveur avant de conclure
|
||||
# quoi que ce soit : lui seul sait si l'accès est encore ouvert.
|
||||
self._probe_access(prompt_if_closed=False)
|
||||
else:
|
||||
# Tunnel tombé : l'autorisation obtenue auprès du serveur ne vaut
|
||||
# que pour l'adresse du pair, elle ne survit pas à la déconnexion.
|
||||
# Tunnel tombé : le client oublie ce qu'il croyait savoir. Le
|
||||
# serveur, lui, n'oublie rien — l'autorisation porte sur l'adresse
|
||||
# du pair, qui ne change pas d'une reconnexion à l'autre, et elle
|
||||
# court jusqu'à son échéance. D'où le sondage au retour du tunnel
|
||||
# plutôt qu'une remise à zéro tenue pour définitive : c'est cette
|
||||
# confusion qui faisait réclamer un code déjà validé.
|
||||
self._access_open = False
|
||||
self._access_until = None
|
||||
self._info_labels["access"].setText(self._access_state())
|
||||
@@ -829,16 +847,57 @@ class MainWindow(QMainWindow):
|
||||
ne vaut que pour l'invite : la saisie reste atteignable à la demande,
|
||||
par le bouton et par le systray, quelle que soit la configuration.
|
||||
"""
|
||||
if not (self._cfg.wg.get("vpn_username") or "").strip():
|
||||
# L'invite ne part qu'après la réponse du serveur : le tunnel qui
|
||||
# remonte ne révoque pas l'autorisation en cours, et réclamer un code
|
||||
# sans avoir demandé revenait à en redemander un déjà validé.
|
||||
#
|
||||
# Une reconnexion automatique ne fait jamais surgir de fenêtre modale
|
||||
# — personne n'est devant l'écran, l'application resterait figée ; le
|
||||
# bouton prend le relais. Sans identifiant VPN configuré non plus :
|
||||
# une installation sans serveur d'authentification n'a pas à voir
|
||||
# cette fenêtre à chaque connexion.
|
||||
known = bool((self._cfg.wg.get("vpn_username") or "").strip())
|
||||
self._probe_access(prompt_if_closed=(not silent) and known)
|
||||
|
||||
def _probe_access(self, prompt_if_closed: bool):
|
||||
"""Interroge le serveur sur l'état de l'accès, sans bloquer l'écran.
|
||||
|
||||
Un seul sondage à la fois : les deux déclencheurs — front montant du
|
||||
tunnel et fin d'une connexion — se produisent souvent coup sur coup.
|
||||
L'intention d'inviter à saisir un code survit à cette fusion, sans
|
||||
quoi une connexion manuelle pourrait n'ouvrir aucune fenêtre parce
|
||||
qu'un sondage silencieux serait parti une fraction de seconde plus tôt.
|
||||
"""
|
||||
self._probe_prompt = self._probe_prompt or prompt_if_closed
|
||||
if self._probe_worker is not None:
|
||||
return
|
||||
if silent:
|
||||
# Reconnexion automatique : ouvrir une fenêtre modale sans
|
||||
# personne devant l'écran laisserait l'application figée. Le
|
||||
# bouton « Saisir le code d'accès » prend le relais.
|
||||
self._add_log("Code d'accès requis — bouton « Saisir le code "
|
||||
"d'accès »", "warning")
|
||||
self._probe_worker = ValueWorker(vpn_session.session_status, self._cfg,
|
||||
parent=self)
|
||||
self._probe_worker.done.connect(self._on_probe_reply)
|
||||
self._probe_worker.finished.connect(self._probe_worker.deleteLater)
|
||||
self._probe_worker.start()
|
||||
|
||||
def _on_probe_reply(self, result):
|
||||
"""Adopte l'état renvoyé par le serveur, ou réclame un code."""
|
||||
self._probe_worker = None
|
||||
prompt = self._probe_prompt
|
||||
self._probe_prompt = False
|
||||
|
||||
if result is not None and result.ok:
|
||||
self._access_open = True
|
||||
self._access_until = result.access_until
|
||||
self._add_log(result.message, "success")
|
||||
self._refresh_status()
|
||||
return
|
||||
self._open_access_dialog()
|
||||
|
||||
# Accès fermé, serveur injoignable ou route absente : dans tous les
|
||||
# cas on ne prétend rien et un code reste à saisir.
|
||||
if prompt:
|
||||
self._open_access_dialog()
|
||||
return
|
||||
self._add_log("Code d'accès requis — bouton « Saisir le code "
|
||||
"d'accès »", "warning")
|
||||
self._refresh_status()
|
||||
|
||||
def _open_access_dialog(self):
|
||||
"""Ouvre la fenêtre de saisie et journalise la réponse du serveur."""
|
||||
|
||||
Reference in New Issue
Block a user