Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99e1db5880 | ||
|
|
9e72fa4155 |
@@ -6,6 +6,17 @@ Ce projet suit le [Versionnage Sémantique](https://semver.org/lang/fr/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.12.0] — 2026-09-08
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
- **Une saisie de code réussie se voit enfin à l'écran.** Le message du serveur partait dans le seul journal, pendant que la fenêtre de saisie se refermait : rien ne distinguait un code accepté d'un code ignoré, et la durée obtenue — précisément ce que l'utilisateur venait de demander — restait invisible. Une confirmation l'annonce désormais avec son échéance et le temps restant.
|
||||||
|
- **Le client vérifie ensuite que le trafic atteint réellement le réseau distant.** Le serveur peut enregistrer l'autorisation sans que la règle de pare-feu s'applique ; le réseau reste alors muet malgré un code accepté, et l'utilisateur ne pouvait que le découvrir seul, plus tard. La vérification observe ce qui passe, là où le sondage se contente de demander au serveur ce qu'il a enregistré. Son verdict rejoint le journal, succès comme échec, en nommant la cible et en renvoyant à l'administrateur.
|
||||||
|
|
||||||
|
### Corrigé
|
||||||
|
- **Un refus du serveur pouvait faire lever une exception au lieu de s'afficher.** Contre un serveur antérieur à la v0.3.2, qui exige encore un identifiant que ce poste ne transmet plus, `POST /api/session` répond 422 — et le `detail` d'une erreur de validation FastAPI est une *liste*, non une phrase. Elle atterrissait telle quelle dans le champ `message` de la réponse, que l'interface passe ensuite à Qt : le refus n'était pas montré, il interrompait le traitement de la réponse. Seule une chaîne est désormais retenue comme message, et ce cas précis explique ce qu'il faut faire — mettre le serveur à jour. Un corps JSON qui n'est pas un objet, qui provoquait la même interruption un cran plus tôt, est écarté de même.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.11.0] — 2026-09-08
|
## [0.11.0] — 2026-09-08
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
## ──────────────────────────────────────────────
|
## ──────────────────────────────────────────────
|
||||||
|
|
||||||
APP := wgsecure
|
APP := wgsecure
|
||||||
VERSION := 0.11.0
|
VERSION := 0.12.0
|
||||||
VENV := .venv
|
VENV := .venv
|
||||||
PYTHON := $(VENV)/bin/python3
|
PYTHON := $(VENV)/bin/python3
|
||||||
PIP := $(VENV)/bin/pip
|
PIP := $(VENV)/bin/pip
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 🛡️ WGSecure (WGS)
|
# 🛡️ WGSecure (WGS)
|
||||||
|
|
||||||
🚀 v0.11.0 · 🐍 · 🪟🐧 · 🔐 Accès validé par le serveur · 🛡️ WireGuard
|
🚀 v0.12.0 · 🐍 · 🪟🐧 · 🔐 Accès validé par le serveur · 🛡️ WireGuard
|
||||||
|
|
||||||
**WGSecure** est une interface graphique multiplateforme (Windows & Linux) pour gérer une connexion WireGuard dont l'accès aux ressources distantes est ouvert par un code à 6 chiffres validé **par le serveur**.
|
**WGSecure** est une interface graphique multiplateforme (Windows & Linux) pour gérer une connexion WireGuard dont l'accès aux ressources distantes est ouvert par un code à 6 chiffres validé **par le serveur**.
|
||||||
|
|
||||||
@@ -210,4 +210,4 @@ WGSecure/
|
|||||||
## 👤 Auteur
|
## 👤 Auteur
|
||||||
|
|
||||||
Développé par **Johnny** — [JT-Tools](https://github.com/JT-Tools)
|
Développé par **Johnny** — [JT-Tools](https://github.com/JT-Tools)
|
||||||
Version :  — Septembre 2026
|
Version :  — Septembre 2026
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
__version__ = "0.11.0"
|
__version__ = "0.12.0"
|
||||||
APP_NAME = "WGSecure"
|
APP_NAME = "WGSecure"
|
||||||
APP_SHORT = "WGS"
|
APP_SHORT = "WGS"
|
||||||
|
|||||||
+30
-10
@@ -101,6 +101,23 @@ def _parse_until(raw: object) -> datetime | None:
|
|||||||
return moment if moment.tzinfo else moment.replace(tzinfo=timezone.utc)
|
return moment if moment.tzinfo else moment.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def _error_detail(exc: urllib.error.HTTPError) -> str:
|
||||||
|
"""Le motif du refus tel que le serveur l'énonce, ou "" s'il n'en dit rien.
|
||||||
|
|
||||||
|
FastAPI répond aux erreurs de validation (422) par un `detail` qui est une
|
||||||
|
*liste* d'erreurs, pas une phrase. Relayée telle quelle, elle atterrissait
|
||||||
|
dans un champ `message` que l'interface passe ensuite à Qt : le refus
|
||||||
|
n'était alors pas affiché, il levait une exception au moment de le lire.
|
||||||
|
Seule une chaîne est un message ; tout autre corps ne dit rien
|
||||||
|
d'affichable, et le message générique de l'appelant vaut mieux.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
detail = json.loads(exc.read().decode("utf-8")).get("detail", "")
|
||||||
|
except (ValueError, OSError, AttributeError):
|
||||||
|
return "" # corps illisible, ou JSON sans objet
|
||||||
|
return detail if isinstance(detail, str) else ""
|
||||||
|
|
||||||
|
|
||||||
def api_base_url(cfg: Config) -> str:
|
def api_base_url(cfg: Config) -> str:
|
||||||
"""URL de l'API du serveur, telle qu'atteignable depuis le tunnel.
|
"""URL de l'API du serveur, telle qu'atteignable depuis le tunnel.
|
||||||
|
|
||||||
@@ -179,7 +196,7 @@ def _routed_through_tunnel(cfg: Config, target: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _remote_network_reachable(cfg: Config) -> bool:
|
def remote_network_reachable(cfg: Config) -> bool:
|
||||||
"""Le trafic passe-t-il réellement vers le réseau distant ?
|
"""Le trafic passe-t-il réellement vers le réseau distant ?
|
||||||
|
|
||||||
Repli pour les serveurs antérieurs à la v0.3.0, qui n'exposent pas
|
Repli pour les serveurs antérieurs à la v0.3.0, qui n'exposent pas
|
||||||
@@ -243,11 +260,8 @@ def session_status(cfg: Config) -> AuthResult:
|
|||||||
return AuthResult(True, "Accès réseau déjà ouvert jusqu'à "
|
return AuthResult(True, "Accès réseau déjà ouvert jusqu'à "
|
||||||
f"{until.astimezone():%d/%m/%Y %H:%M}", until)
|
f"{until.astimezone():%d/%m/%Y %H:%M}", until)
|
||||||
except urllib.error.HTTPError as exc:
|
except urllib.error.HTTPError as exc:
|
||||||
try:
|
detail = _error_detail(exc)
|
||||||
detail = json.loads(exc.read().decode("utf-8")).get("detail", "")
|
if exc.code == 404 and remote_network_reachable(cfg):
|
||||||
except (ValueError, OSError):
|
|
||||||
detail = ""
|
|
||||||
if exc.code == 404 and _remote_network_reachable(cfg):
|
|
||||||
# Serveur antérieur à la v0.3.0 : la route n'existe pas, mais le
|
# Serveur antérieur à la v0.3.0 : la route n'existe pas, mais le
|
||||||
# trafic atteint le réseau distant — l'accès est donc bien ouvert.
|
# trafic atteint le réseau distant — l'accès est donc bien ouvert.
|
||||||
# Aucune échéance en revanche : seul le serveur la connaît, et il
|
# Aucune échéance en revanche : seul le serveur la connaît, et il
|
||||||
@@ -324,10 +338,16 @@ def authenticate(cfg: Config, code: str) -> AuthResult:
|
|||||||
# Le serveur explique lui-même le refus (code invalide, compte
|
# Le serveur explique lui-même le refus (code invalide, compte
|
||||||
# verrouillé, expiré…) : le relayer tel quel vaut mieux qu'un
|
# verrouillé, expiré…) : le relayer tel quel vaut mieux qu'un
|
||||||
# message générique qui obligerait à consulter les journaux.
|
# message générique qui obligerait à consulter les journaux.
|
||||||
try:
|
detail = _error_detail(exc)
|
||||||
detail = json.loads(exc.read().decode("utf-8")).get("detail", "")
|
if exc.code == 422:
|
||||||
except (ValueError, OSError):
|
# Serveur antérieur à la v0.3.2 : il exige encore un identifiant
|
||||||
detail = ""
|
# dans le corps, que ce poste ne transmet plus. Son `detail` est
|
||||||
|
# la liste d'erreurs de validation de FastAPI — `_error_detail`
|
||||||
|
# l'écarte, et il n'en resterait qu'un « code 422 » énigmatique.
|
||||||
|
return AuthResult(False, "Ce serveur attend un identifiant VPN "
|
||||||
|
"que ce poste ne transmet plus. Mettez "
|
||||||
|
"le serveur à jour (v0.3.2 ou "
|
||||||
|
"supérieur).")
|
||||||
return AuthResult(False, detail
|
return AuthResult(False, detail
|
||||||
or f"Authentification refusée (code {exc.code})")
|
or f"Authentification refusée (code {exc.code})")
|
||||||
except urllib.error.URLError as exc:
|
except urllib.error.URLError as exc:
|
||||||
|
|||||||
+54
-1
@@ -88,6 +88,10 @@ class MainWindow(QMainWindow):
|
|||||||
# déclenché sans invite est déjà parti.
|
# déclenché sans invite est déjà parti.
|
||||||
self._probe_worker: ValueWorker | None = None
|
self._probe_worker: ValueWorker | None = None
|
||||||
self._probe_prompt = False
|
self._probe_prompt = False
|
||||||
|
# Vérification, après un code accepté, que le trafic atteint bien le
|
||||||
|
# réseau distant. Distincte du sondage : celui-ci demande au serveur
|
||||||
|
# ce qu'il a enregistré, celle-là observe ce qui passe réellement.
|
||||||
|
self._reach_worker: ValueWorker | None = None
|
||||||
# Date du dernier sondage abouti, et nombre de sondages consécutifs
|
# Date du dernier sondage abouti, et nombre de sondages consécutifs
|
||||||
# ayant trouvé fermé un accès qu'on croyait ouvert (cf.
|
# ayant trouvé fermé un accès qu'on croyait ouvert (cf.
|
||||||
# `_access_stale` et `_on_probe_reply`). `_access_message` évite de
|
# `_access_stale` et `_on_probe_reply`). `_access_message` évite de
|
||||||
@@ -1369,6 +1373,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._access_denials = 0
|
self._access_denials = 0
|
||||||
self._probe_retried = False
|
self._probe_retried = False
|
||||||
self._add_log(message, "success")
|
self._add_log(message, "success")
|
||||||
|
self._announce_access_granted()
|
||||||
else:
|
else:
|
||||||
# Abandon : l'état d'accès n'est pas touché. Il l'était, et à tort
|
# Abandon : l'état d'accès n'est pas touché. Il l'était, et à tort
|
||||||
# — un sondage abouti pendant que la fenêtre était ouverte (les
|
# — un sondage abouti pendant que la fenêtre était ouverte (les
|
||||||
@@ -1383,6 +1388,53 @@ class MainWindow(QMainWindow):
|
|||||||
"accès distant fermé", "warning")
|
"accès distant fermé", "warning")
|
||||||
self._refresh_status()
|
self._refresh_status()
|
||||||
|
|
||||||
|
def _announce_access_granted(self):
|
||||||
|
"""Confirme la validation, en disant jusqu'à quand l'accès tient.
|
||||||
|
|
||||||
|
Le message du serveur partait jusqu'ici dans le seul journal, pendant
|
||||||
|
que la fenêtre se refermait : rien ne distinguait à l'écran un code
|
||||||
|
accepté d'un code ignoré, et la durée obtenue restait invisible alors
|
||||||
|
que c'est précisément ce que l'utilisateur vient de demander.
|
||||||
|
"""
|
||||||
|
if self._access_until is not None:
|
||||||
|
fin = self._access_until.astimezone()
|
||||||
|
texte = (f"Code accepté.\n\nAccès au réseau distant ouvert "
|
||||||
|
f"jusqu'au {fin:%d/%m/%Y à %H:%M} "
|
||||||
|
f"(soit {self._remaining_text()}).")
|
||||||
|
else:
|
||||||
|
texte = ("Code accepté.\n\nAccès au réseau distant ouvert. Le "
|
||||||
|
"serveur n'a pas annoncé d'échéance.")
|
||||||
|
QMessageBox.information(self, "Accès ouvert", texte)
|
||||||
|
# Le serveur peut avoir enregistré l'autorisation sans que le trafic
|
||||||
|
# passe pour autant — règle de pare-feu non appliquée, par exemple.
|
||||||
|
# Le constater vaut mieux que laisser l'utilisateur découvrir seul un
|
||||||
|
# réseau resté muet après un code pourtant accepté.
|
||||||
|
self._verify_access_effective()
|
||||||
|
|
||||||
|
def _verify_access_effective(self):
|
||||||
|
"""Vérifie en fond que le trafic atteint réellement le réseau distant."""
|
||||||
|
if self._reach_worker is not None:
|
||||||
|
return
|
||||||
|
self._reach_worker = ValueWorker(vpn_session.remote_network_reachable,
|
||||||
|
self._cfg, parent=self)
|
||||||
|
self._reach_worker.done.connect(self._on_reach_reply)
|
||||||
|
self._reach_worker.finished.connect(self._reach_worker.deleteLater)
|
||||||
|
self._reach_worker.start()
|
||||||
|
|
||||||
|
def _on_reach_reply(self, joignable: bool):
|
||||||
|
self._reach_worker = None
|
||||||
|
cible = (self._cfg.wg.get("split_dns_server") or "").strip()
|
||||||
|
if not cible:
|
||||||
|
return # rien à sonder, donc rien à conclure
|
||||||
|
if joignable:
|
||||||
|
self._add_log(f"Trafic vers le réseau distant confirmé ({cible})",
|
||||||
|
"success")
|
||||||
|
return
|
||||||
|
self._add_log(
|
||||||
|
f"Code accepté, mais {cible} reste injoignable : le serveur a "
|
||||||
|
"enregistré l'autorisation sans que le trafic passe. Prévenez "
|
||||||
|
"l'administrateur.", "warning")
|
||||||
|
|
||||||
def _begin_task(self, label: str):
|
def _begin_task(self, label: str):
|
||||||
self._connecting = True
|
self._connecting = True
|
||||||
self._btn_connect.setEnabled(False)
|
self._btn_connect.setEnabled(False)
|
||||||
@@ -1505,7 +1557,7 @@ class MainWindow(QMainWindow):
|
|||||||
# Laisser les threads en cours se terminer avant de démonter le tunnel :
|
# Laisser les threads en cours se terminer avant de démonter le tunnel :
|
||||||
# une connexion et une déconnexion simultanées laisseraient un état mixte.
|
# une connexion et une déconnexion simultanées laisseraient un état mixte.
|
||||||
for worker in (self._worker, self._ping_worker, self._probe_worker,
|
for worker in (self._worker, self._ping_worker, self._probe_worker,
|
||||||
self._update_worker):
|
self._reach_worker, self._update_worker):
|
||||||
try:
|
try:
|
||||||
if worker is not None and worker.isRunning():
|
if worker is not None and worker.isRunning():
|
||||||
worker.wait(15000)
|
worker.wait(15000)
|
||||||
@@ -1520,6 +1572,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._worker = None
|
self._worker = None
|
||||||
self._ping_worker = None
|
self._ping_worker = None
|
||||||
self._probe_worker = None
|
self._probe_worker = None
|
||||||
|
self._reach_worker = None
|
||||||
self._update_worker = None
|
self._update_worker = None
|
||||||
|
|
||||||
if self._session_id is not None:
|
if self._session_id is not None:
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
; ──────────────────────────────────────────────
|
; ──────────────────────────────────────────────
|
||||||
|
|
||||||
#define MyAppName "WGSecure"
|
#define MyAppName "WGSecure"
|
||||||
#define MyAppVersion "0.11.0"
|
#define MyAppVersion "0.12.0"
|
||||||
#define MyAppPublisher "WGSecure"
|
#define MyAppPublisher "WGSecure"
|
||||||
#define MyAppExeName "wgsecure.exe"
|
#define MyAppExeName "wgsecure.exe"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user