Update
This commit is contained in:
+42
-10
@@ -6,6 +6,40 @@ Ce projet suit le [Versionnage Sémantique](https://semver.org/lang/fr/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.2.0] — 2026-06-01
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
- **Onglet "À propos"** dans le panneau admin : logo bouclier, version, branding JT-Tools by Johnny
|
||||||
|
- **Icône bouclier vectorielle** générée par QPainter (7 résolutions : 16→256 px)
|
||||||
|
- Vert connecté / Rouge déconnecté / Orange connexion en cours / Bleu app / Violet admin
|
||||||
|
- **Icône barre des tâches** : installation automatique de `~/.local/share/icons/wgsecure.png`
|
||||||
|
et `~/.local/share/applications/wgsecure.desktop` au premier démarrage
|
||||||
|
- **Thème sombre uniforme** sur les 6 onglets du panneau admin (CSS partagé `_TAB_CSS`)
|
||||||
|
- Fond `#1c2833`, texte blanc, champs semi-transparents
|
||||||
|
- Bandeau coloré par onglet (bleu/navy/teal/bleu/rouge)
|
||||||
|
- TabBar cohérente avec onglet actif mis en évidence
|
||||||
|
- **Makefile** : compilation binaire Linux (`make linux`) et Windows (`make windows` via Wine)
|
||||||
|
- **README** enrichi avec badges shields.io (version, Python, PyQt6, plateformes) et emojis
|
||||||
|
|
||||||
|
### Corrigé
|
||||||
|
- **Bug MDP vide** : `set_admin_password("")` effaçait incorrectement — hashait la chaîne vide,
|
||||||
|
forçant une demande de mot de passe vide après suppression
|
||||||
|
- **Menu systray incomplet** : `QAction` en variables locales GC'd par Python avant affichage →
|
||||||
|
toutes les actions passent désormais `self` comme parent Qt
|
||||||
|
- **Section infos invisible** : `QWidget` sans `autoFillBackground` ne peignait pas son fond ;
|
||||||
|
remplacé par `QFrame` avec `setAutoFillBackground(True)` et couleurs explicites
|
||||||
|
- **Bouton agrandir persistant** : `setWindowFlag` seul ignoré par Mutter/XWayland →
|
||||||
|
`setWindowFlags()` complet sans `WindowMaximizeButtonHint`
|
||||||
|
|
||||||
|
### Amélioré
|
||||||
|
- Fenêtre principale agrandie 380×460 → 400×520 pour afficher les 6 lignes d'infos
|
||||||
|
- Police des labels infos : 11 → 12 px, couleurs explicites `#2c3e50`
|
||||||
|
- Panneau admin : largeur 620 → 700 px pour éviter le débordement des onglets
|
||||||
|
- `app.setWindowIcon()` posé sur `QApplication` (multi-tailles) + `setDesktopFileName()`
|
||||||
|
- Page About : icône 96 → 86 px (−10 %), texte en blanc sur fond sombre `#1c2833`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.1.0] — 2026-06-01
|
## [0.1.0] — 2026-06-01
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
@@ -14,23 +48,21 @@ Ce projet suit le [Versionnage Sémantique](https://semver.org/lang/fr/).
|
|||||||
- **Mode Admin** (`--admin` ou via le menu) protégé par mot de passe
|
- **Mode Admin** (`--admin` ou via le menu) protégé par mot de passe
|
||||||
- Panneau Admin avec 5 onglets :
|
- Panneau Admin avec 5 onglets :
|
||||||
- **WireGuard** : configuration serveur (adresse, port UDP, clé publique, DNS, IP client)
|
- **WireGuard** : configuration serveur (adresse, port UDP, clé publique, DNS, IP client)
|
||||||
- **Clés** : génération de paires Curve25519 (clé privée / publique), aperçu de la config exportable
|
- **Clés** : génération de paires Curve25519 (clé privée / publique), aperçu config exportable
|
||||||
- **MFA** : génération de secret TOTP, QR Code compatible Google Authenticator / Aegis
|
- **MFA** : génération de secret TOTP, QR Code compatible Google Authenticator / Aegis
|
||||||
- **Test connexion** : vérification UDP du serveur avant tunnel
|
- **Test connexion** : vérification UDP du serveur avant tunnel
|
||||||
- **Sécurité** : gestion du mot de passe administrateur (SHA-256 + salt)
|
- **Sécurité** : gestion du mot de passe administrateur (SHA-256 + salt)
|
||||||
- Surcouche MFA : dialogue TOTP avec minuterie de validité du code (fenêtre glissante ±1)
|
- Surcouche MFA : dialogue TOTP avec minuterie de validité (fenêtre glissante ±1)
|
||||||
- Génération de clés WireGuard via la bibliothèque `cryptography` (X25519, sans dépendance à `wg`)
|
- Génération de clés WireGuard via `cryptography` (X25519, sans `wg` binaire)
|
||||||
- Écriture automatique du fichier `.conf` WireGuard avec permissions `0600`
|
- Écriture automatique du fichier `.conf` WireGuard avec permissions `0600`
|
||||||
- Connexion via `wg-quick` (Linux) ou `wireguard /installtunnel` (Windows)
|
- Connexion via `wg-quick` (Linux) ou `wireguard /installtunnel` (Windows)
|
||||||
- Statut temps réel : trafic RX/TX, dernier handshake
|
- Statut temps réel : trafic RX/TX, dernier handshake
|
||||||
- Icônes générées programmatiquement (vert/rouge/orange) sans fichier binaire externe
|
|
||||||
- Réduction dans le systray à la fermeture de la fenêtre principale
|
- Réduction dans le systray à la fermeture de la fenêtre principale
|
||||||
- Option `--no-tray` pour fonctionner sans systray
|
- Configuration persistante JSON (`~/.wgsecure/config.json`)
|
||||||
- Configuration persistante en JSON (`~/.wgsecure/config.json`)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Prochaines versions planifiées :*
|
*Versions planifiées :*
|
||||||
- `0.2.0` — Import/export de profils WireGuard (.conf), support multi-profils
|
- `0.3.0` — Import/export de profils WireGuard (.conf), support multi-profils
|
||||||
- `0.3.0` — MFA par clé matérielle (FIDO2/YubiKey)
|
- `0.4.0` — MFA par clé matérielle (FIDO2/YubiKey)
|
||||||
- `0.4.0` — Paquet installable (Windows .exe / Linux .deb)
|
- `0.5.0` — Paquet installable (Windows .exe / Linux .deb)
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
## ──────────────────────────────────────────────
|
||||||
|
## WGSecure (WGS) — Makefile
|
||||||
|
## Compilation Linux & Windows via PyInstaller
|
||||||
|
## ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
APP := wgsecure
|
||||||
|
VERSION := 0.4.0
|
||||||
|
PYTHON := python3
|
||||||
|
SRC := main.py
|
||||||
|
DIST := dist
|
||||||
|
BUILD := build
|
||||||
|
ICON := /tmp/wgsecure_icon_build.png
|
||||||
|
|
||||||
|
# Options PyInstaller communes aux deux plateformes
|
||||||
|
PI_OPTS := \
|
||||||
|
--noconfirm \
|
||||||
|
--clean \
|
||||||
|
--name $(APP) \
|
||||||
|
--hidden-import pyotp \
|
||||||
|
--hidden-import qrcode \
|
||||||
|
--hidden-import qrcode.image.pil \
|
||||||
|
--hidden-import PIL \
|
||||||
|
--hidden-import PIL.Image \
|
||||||
|
--hidden-import cryptography \
|
||||||
|
--hidden-import cryptography.hazmat.primitives.asymmetric.x25519 \
|
||||||
|
--collect-submodules PyQt6
|
||||||
|
|
||||||
|
.PHONY: all linux windows release install run run-admin icon clean help
|
||||||
|
|
||||||
|
# ── Cible par défaut ────────────────────────────────────────────────────────
|
||||||
|
all: linux
|
||||||
|
|
||||||
|
# ── Génération de l'icône PNG (utilisée par PyInstaller) ────────────────────
|
||||||
|
icon:
|
||||||
|
@$(PYTHON) -c "\
|
||||||
|
import sys; sys.path.insert(0,'.');\
|
||||||
|
from PyQt6.QtWidgets import QApplication; app=QApplication(sys.argv);\
|
||||||
|
from app.ui.icons import icon_app;\
|
||||||
|
pix=icon_app().pixmap(256,256); pix.save('$(ICON)','PNG');\
|
||||||
|
print(' Icône générée →', '$(ICON)')" 2>/dev/null
|
||||||
|
@test -f $(ICON) || (echo " ❌ Génération icône échouée"; exit 1)
|
||||||
|
|
||||||
|
# ── Binaire Linux ────────────────────────────────────────────────────────────
|
||||||
|
linux: icon
|
||||||
|
@echo ""
|
||||||
|
@echo " 🐧 Compilation Linux (PyInstaller onefile)…"
|
||||||
|
@echo ""
|
||||||
|
$(PYTHON) -m PyInstaller $(PI_OPTS) \
|
||||||
|
--onefile \
|
||||||
|
--icon=$(ICON) \
|
||||||
|
$(SRC)
|
||||||
|
@echo ""
|
||||||
|
@echo " ✅ Binaire Linux → $(DIST)/$(APP)"
|
||||||
|
@echo ""
|
||||||
|
|
||||||
|
# ── Binaire Windows (via Wine + Python Windows) ──────────────────────────────
|
||||||
|
windows: icon
|
||||||
|
@echo ""
|
||||||
|
@echo " 🪟 Compilation Windows (Wine + PyInstaller)…"
|
||||||
|
@echo ""
|
||||||
|
@which wine > /dev/null 2>&1 \
|
||||||
|
|| (echo " ❌ Wine non installé → sudo apt install wine"; exit 1)
|
||||||
|
@wine python -m PyInstaller $(PI_OPTS) \
|
||||||
|
--onefile \
|
||||||
|
--windowed \
|
||||||
|
--icon=$(ICON) \
|
||||||
|
$(SRC) \
|
||||||
|
|| (echo ""; \
|
||||||
|
echo " ❌ Échec : Python Windows introuvable dans Wine."; \
|
||||||
|
echo " → Installez-le avec : make setup-wine"; \
|
||||||
|
echo ""; exit 1)
|
||||||
|
@echo ""
|
||||||
|
@echo " ✅ Binaire Windows → $(DIST)/$(APP).exe"
|
||||||
|
@echo ""
|
||||||
|
|
||||||
|
# ── Installation de Python dans Wine ─────────────────────────────────────────
|
||||||
|
setup-wine:
|
||||||
|
@echo " 📦 Installation Python + pip dans Wine…"
|
||||||
|
winetricks python3
|
||||||
|
wine pip install -r requirements.txt
|
||||||
|
wine pip install pyinstaller
|
||||||
|
|
||||||
|
# ── Release (Linux renommé avec version) ─────────────────────────────────────
|
||||||
|
release: clean linux
|
||||||
|
@mv $(DIST)/$(APP) $(DIST)/$(APP)-$(VERSION)-linux-x86_64
|
||||||
|
@echo " 📦 Release → $(DIST)/$(APP)-$(VERSION)-linux-x86_64"
|
||||||
|
|
||||||
|
# ── Dépendances Python ────────────────────────────────────────────────────────
|
||||||
|
install:
|
||||||
|
$(PYTHON) -m pip install -r requirements.txt
|
||||||
|
$(PYTHON) -m pip install pyinstaller
|
||||||
|
@echo " ✅ Dépendances installées"
|
||||||
|
|
||||||
|
# ── Lancer l'application ─────────────────────────────────────────────────────
|
||||||
|
run:
|
||||||
|
DISPLAY=:0 $(PYTHON) $(SRC)
|
||||||
|
|
||||||
|
run-admin:
|
||||||
|
DISPLAY=:0 $(PYTHON) $(SRC) --admin
|
||||||
|
|
||||||
|
# ── Nettoyage ────────────────────────────────────────────────────────────────
|
||||||
|
clean:
|
||||||
|
@echo " 🧹 Nettoyage…"
|
||||||
|
@rm -rf $(BUILD) $(DIST) *.spec
|
||||||
|
@find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null; true
|
||||||
|
@find . -name "*.pyc" -delete 2>/dev/null; true
|
||||||
|
@echo " ✅ Artefacts supprimés"
|
||||||
|
|
||||||
|
# ── Aide ─────────────────────────────────────────────────────────────────────
|
||||||
|
help:
|
||||||
|
@echo ""
|
||||||
|
@echo " ╔════════════════════════════════════════════╗"
|
||||||
|
@echo " ║ 🛡️ WGSecure v$(VERSION) — Makefile ║"
|
||||||
|
@echo " ╠════════════════════════════════════════════╣"
|
||||||
|
@echo " ║ make install Installe les dépendances ║"
|
||||||
|
@echo " ║ make linux Binaire Linux (onefile) ║"
|
||||||
|
@echo " ║ make windows Binaire Windows (Wine) ║"
|
||||||
|
@echo " ║ make release Linux + nommage release ║"
|
||||||
|
@echo " ║ make setup-wine Python Windows dans Wine ║"
|
||||||
|
@echo " ║ make run Lance l'application ║"
|
||||||
|
@echo " ║ make run-admin Lance en mode admin ║"
|
||||||
|
@echo " ║ make clean Supprime les artefacts ║"
|
||||||
|
@echo " ╚════════════════════════════════════════════╝"
|
||||||
|
@echo ""
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# 🛡️ WGSecure (WGS)
|
# 🛡️ WGSecure (WGS)
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
@@ -135,4 +135,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 :  — Juin 2026
|
Version :  — Juin 2026
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
__version__ = "0.1.0"
|
__version__ = "0.4.0"
|
||||||
APP_NAME = "WGSecure"
|
APP_NAME = "WGSecure"
|
||||||
APP_SHORT = "WGS"
|
APP_SHORT = "WGS"
|
||||||
|
|||||||
+121
-23
@@ -2,33 +2,40 @@ import json
|
|||||||
import os
|
import os
|
||||||
import hashlib
|
import hashlib
|
||||||
import secrets
|
import secrets
|
||||||
|
from copy import deepcopy
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from app.utils.platform_utils import get_config_dir
|
from app.utils.platform_utils import get_config_dir
|
||||||
|
|
||||||
|
|
||||||
_CONFIG_FILE = "config.json"
|
_CONFIG_FILE = "config.json"
|
||||||
|
|
||||||
|
_WG_DEFAULT: dict[str, Any] = {
|
||||||
|
"interface_name": "wgs0",
|
||||||
|
"server_endpoint": "",
|
||||||
|
"server_port": 51820,
|
||||||
|
"server_public_key": "",
|
||||||
|
"client_private_key": "",
|
||||||
|
"client_public_key": "",
|
||||||
|
"client_address": "10.8.0.2/24",
|
||||||
|
"dns": "1.1.1.1",
|
||||||
|
"allowed_ips": "0.0.0.0/0",
|
||||||
|
"keepalive": 25,
|
||||||
|
}
|
||||||
|
|
||||||
_DEFAULT: dict[str, Any] = {
|
_DEFAULT: dict[str, Any] = {
|
||||||
"version": "0.1.0",
|
"version": "0.4.0",
|
||||||
"admin_password_hash": "",
|
"admin_password_hash": "",
|
||||||
"admin_salt": "",
|
"admin_salt": "",
|
||||||
"mfa_enabled": False,
|
"mfa_enabled": False,
|
||||||
"mfa_secret": "",
|
"mfa_secret": "",
|
||||||
"wg": {
|
"active_profile": "Défaut",
|
||||||
"interface_name": "wgs0",
|
"profiles": {},
|
||||||
"server_endpoint": "",
|
"wg": deepcopy(_WG_DEFAULT),
|
||||||
"server_port": 51820,
|
|
||||||
"server_public_key": "",
|
|
||||||
"client_private_key": "",
|
|
||||||
"client_public_key": "",
|
|
||||||
"client_address": "10.8.0.2/24",
|
|
||||||
"dns": "1.1.1.1",
|
|
||||||
"allowed_ips": "0.0.0.0/0",
|
|
||||||
"keepalive": 25,
|
|
||||||
},
|
|
||||||
"ui": {
|
"ui": {
|
||||||
"minimize_to_tray": True,
|
"minimize_to_tray": True,
|
||||||
"autostart": False,
|
"autostart": False,
|
||||||
"theme": "auto",
|
"auto_reconnect": False,
|
||||||
|
"reconnect_interval": 30,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,9 +53,9 @@ class Config:
|
|||||||
saved = json.load(f)
|
saved = json.load(f)
|
||||||
self._data = self._merge(_DEFAULT, saved)
|
self._data = self._merge(_DEFAULT, saved)
|
||||||
except Exception:
|
except Exception:
|
||||||
self._data = dict(_DEFAULT)
|
self._data = deepcopy(_DEFAULT)
|
||||||
else:
|
else:
|
||||||
self._data = dict(_DEFAULT)
|
self._data = deepcopy(_DEFAULT)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
os.makedirs(os.path.dirname(self._path), exist_ok=True)
|
os.makedirs(os.path.dirname(self._path), exist_ok=True)
|
||||||
@@ -79,11 +86,10 @@ class Config:
|
|||||||
node = node.setdefault(k, {})
|
node = node.setdefault(k, {})
|
||||||
node[keys[-1]] = value
|
node[keys[-1]] = value
|
||||||
|
|
||||||
# -- Admin password --
|
# ── Mot de passe admin ────────────────────────────────────────────────
|
||||||
|
|
||||||
def set_admin_password(self, password: str):
|
def set_admin_password(self, password: str):
|
||||||
if not password:
|
if not password:
|
||||||
# Mot de passe vide = suppression de la protection
|
|
||||||
self._data["admin_salt"] = ""
|
self._data["admin_salt"] = ""
|
||||||
self._data["admin_password_hash"] = ""
|
self._data["admin_password_hash"] = ""
|
||||||
else:
|
else:
|
||||||
@@ -97,19 +103,26 @@ class Config:
|
|||||||
salt = self._data.get("admin_salt", "")
|
salt = self._data.get("admin_salt", "")
|
||||||
stored = self._data.get("admin_password_hash", "")
|
stored = self._data.get("admin_password_hash", "")
|
||||||
if not stored:
|
if not stored:
|
||||||
return True # Pas encore de mot de passe configuré
|
return True
|
||||||
candidate = hashlib.sha256((salt + password).encode()).hexdigest()
|
candidate = hashlib.sha256((salt + password).encode()).hexdigest()
|
||||||
return secrets.compare_digest(candidate, stored)
|
return secrets.compare_digest(candidate, stored)
|
||||||
|
|
||||||
def has_admin_password(self) -> bool:
|
def has_admin_password(self) -> bool:
|
||||||
return bool(self._data.get("admin_password_hash", ""))
|
return bool(self._data.get("admin_password_hash", ""))
|
||||||
|
|
||||||
# -- Propriétés WireGuard --
|
# ── WireGuard ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wg(self) -> dict:
|
def wg(self) -> dict:
|
||||||
return self._data["wg"]
|
return self._data["wg"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def configured(self) -> bool:
|
||||||
|
wg = self._data["wg"]
|
||||||
|
return bool(wg.get("server_endpoint") and wg.get("client_private_key"))
|
||||||
|
|
||||||
|
# ── MFA ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mfa_enabled(self) -> bool:
|
def mfa_enabled(self) -> bool:
|
||||||
return bool(self._data.get("mfa_enabled", False))
|
return bool(self._data.get("mfa_enabled", False))
|
||||||
@@ -126,7 +139,92 @@ class Config:
|
|||||||
def mfa_secret(self, value: str):
|
def mfa_secret(self, value: str):
|
||||||
self._data["mfa_secret"] = value
|
self._data["mfa_secret"] = value
|
||||||
|
|
||||||
|
# ── Multi-profils ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def configured(self) -> bool:
|
def active_profile(self) -> str:
|
||||||
wg = self._data["wg"]
|
return self._data.get("active_profile", "Défaut")
|
||||||
return bool(wg.get("server_endpoint") and wg.get("client_private_key"))
|
|
||||||
|
def list_profiles(self) -> list[str]:
|
||||||
|
return list(self._data.get("profiles", {}).keys())
|
||||||
|
|
||||||
|
def save_profile(self, name: str) -> None:
|
||||||
|
"""Sauvegarde la config WireGuard courante sous un nom de profil."""
|
||||||
|
self._data.setdefault("profiles", {})[name] = deepcopy(self._data["wg"])
|
||||||
|
self._data["active_profile"] = name
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def load_profile(self, name: str) -> bool:
|
||||||
|
"""Charge un profil sauvegardé dans la config active."""
|
||||||
|
profiles = self._data.get("profiles", {})
|
||||||
|
if name not in profiles:
|
||||||
|
return False
|
||||||
|
self._data["wg"] = deepcopy(profiles[name])
|
||||||
|
self._data["active_profile"] = name
|
||||||
|
self.save()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def delete_profile(self, name: str) -> bool:
|
||||||
|
profiles = self._data.get("profiles", {})
|
||||||
|
if name not in profiles:
|
||||||
|
return False
|
||||||
|
del profiles[name]
|
||||||
|
if self._data.get("active_profile") == name:
|
||||||
|
self._data["active_profile"] = "Défaut"
|
||||||
|
self.save()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def rename_profile(self, old: str, new: str) -> bool:
|
||||||
|
profiles = self._data.get("profiles", {})
|
||||||
|
if old not in profiles or new in profiles:
|
||||||
|
return False
|
||||||
|
profiles[new] = profiles.pop(old)
|
||||||
|
if self._data.get("active_profile") == old:
|
||||||
|
self._data["active_profile"] = new
|
||||||
|
self.save()
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ── Autostart ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def set_autostart(self, enabled: bool) -> None:
|
||||||
|
self._data["ui"]["autostart"] = enabled
|
||||||
|
self._apply_autostart(enabled)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def _apply_autostart(self, enabled: bool) -> None:
|
||||||
|
import sys
|
||||||
|
from app.utils.platform_utils import is_linux, is_windows
|
||||||
|
script = os.path.abspath(os.path.join(
|
||||||
|
os.path.dirname(__file__), "..", "..", "main.py"
|
||||||
|
))
|
||||||
|
if is_linux():
|
||||||
|
autostart_dir = os.path.expanduser("~/.config/autostart")
|
||||||
|
desktop = os.path.join(autostart_dir, "wgsecure.desktop")
|
||||||
|
if enabled:
|
||||||
|
os.makedirs(autostart_dir, exist_ok=True)
|
||||||
|
content = (
|
||||||
|
"[Desktop Entry]\nType=Application\nName=WGSecure\n"
|
||||||
|
f"Exec=python3 {script}\nIcon=wgsecure\n"
|
||||||
|
"Hidden=false\nX-GNOME-Autostart-enabled=true\n"
|
||||||
|
)
|
||||||
|
with open(desktop, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
os.unlink(desktop)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
elif is_windows():
|
||||||
|
import winreg
|
||||||
|
key = r"Software\Microsoft\Windows\CurrentVersion\Run"
|
||||||
|
try:
|
||||||
|
reg = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key, 0,
|
||||||
|
winreg.KEY_SET_VALUE)
|
||||||
|
if enabled:
|
||||||
|
winreg.SetValueEx(reg, "WGSecure", 0, winreg.REG_SZ,
|
||||||
|
f'pythonw "{script}"')
|
||||||
|
else:
|
||||||
|
winreg.DeleteValue(reg, "WGSecure")
|
||||||
|
winreg.CloseKey(reg)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"""Historique des sessions de connexion WGSecure."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from app.utils.platform_utils import get_config_dir
|
||||||
|
|
||||||
|
_FILE = "sessions.json"
|
||||||
|
_MAX = 200
|
||||||
|
|
||||||
|
|
||||||
|
def _path() -> str:
|
||||||
|
return os.path.join(get_config_dir(), _FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def start_session(server: str, profile: str) -> int:
|
||||||
|
"""Démarre une nouvelle session, retourne son id."""
|
||||||
|
sid = int(datetime.now().timestamp())
|
||||||
|
sessions = _load()
|
||||||
|
sessions.append({
|
||||||
|
"id": sid,
|
||||||
|
"start": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"end": None,
|
||||||
|
"duration": None,
|
||||||
|
"server": server,
|
||||||
|
"profile": profile,
|
||||||
|
"rx": 0,
|
||||||
|
"tx": 0,
|
||||||
|
})
|
||||||
|
_save(sessions)
|
||||||
|
return sid
|
||||||
|
|
||||||
|
|
||||||
|
def end_session(sid: int, rx: int = 0, tx: int = 0) -> None:
|
||||||
|
sessions = _load()
|
||||||
|
for s in sessions:
|
||||||
|
if s.get("id") == sid:
|
||||||
|
start_dt = datetime.strptime(s["start"], "%Y-%m-%d %H:%M:%S")
|
||||||
|
secs = int((datetime.now() - start_dt).total_seconds())
|
||||||
|
s["end"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
s["duration"] = secs
|
||||||
|
s["rx"] = rx
|
||||||
|
s["tx"] = tx
|
||||||
|
break
|
||||||
|
_save(sessions)
|
||||||
|
|
||||||
|
|
||||||
|
def get_sessions(n: int = 50) -> list[dict]:
|
||||||
|
return list(reversed(_load()[-n:]))
|
||||||
|
|
||||||
|
|
||||||
|
def clear() -> None:
|
||||||
|
_save([])
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_duration(secs: int | None) -> str:
|
||||||
|
if secs is None:
|
||||||
|
return "—"
|
||||||
|
h, r = divmod(int(secs), 3600)
|
||||||
|
m, s = divmod(r, 60)
|
||||||
|
if h:
|
||||||
|
return f"{h}h {m:02d}m"
|
||||||
|
if m:
|
||||||
|
return f"{m}m {s:02d}s"
|
||||||
|
return f"{s}s"
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_bytes(n: int) -> str:
|
||||||
|
for unit, div in [("GB", 1_073_741_824), ("MB", 1_048_576), ("KB", 1024)]:
|
||||||
|
if n >= div:
|
||||||
|
return f"{n/div:.1f} {unit}"
|
||||||
|
return f"{n} B"
|
||||||
|
|
||||||
|
|
||||||
|
def _load() -> list[dict]:
|
||||||
|
p = _path()
|
||||||
|
if not os.path.exists(p):
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
with open(p, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _save(sessions: list[dict]) -> None:
|
||||||
|
try:
|
||||||
|
with open(_path(), "w", encoding="utf-8") as f:
|
||||||
|
json.dump(sessions[-_MAX:], f, indent=2, ensure_ascii=False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""Journal des événements de connexion WGSecure."""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from app.utils.platform_utils import get_config_dir
|
||||||
|
|
||||||
|
_FILE = "events.json"
|
||||||
|
_MAX_ENTRIES = 200
|
||||||
|
|
||||||
|
|
||||||
|
def _path() -> str:
|
||||||
|
return os.path.join(get_config_dir(), _FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def log_event(msg: str, kind: str = "info") -> None:
|
||||||
|
"""Ajoute un événement horodaté au journal."""
|
||||||
|
events = _load()
|
||||||
|
events.append({
|
||||||
|
"ts": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"kind": kind, # info | success | error | warning
|
||||||
|
"msg": msg,
|
||||||
|
})
|
||||||
|
_save(events[-_MAX_ENTRIES:])
|
||||||
|
|
||||||
|
|
||||||
|
def get_events(n: int = 50) -> list[dict]:
|
||||||
|
return _load()[-n:]
|
||||||
|
|
||||||
|
|
||||||
|
def clear() -> None:
|
||||||
|
_save([])
|
||||||
|
|
||||||
|
|
||||||
|
def _load() -> list[dict]:
|
||||||
|
p = _path()
|
||||||
|
if not os.path.exists(p):
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
with open(p, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _save(events: list[dict]) -> None:
|
||||||
|
try:
|
||||||
|
with open(_path(), "w", encoding="utf-8") as f:
|
||||||
|
json.dump(events, f, ensure_ascii=False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
@@ -143,6 +143,281 @@ def test_connection(cfg: Config, timeout: int = 5) -> tuple[bool, str]:
|
|||||||
return False, str(e)
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_conf_file(path: str) -> dict | None:
|
||||||
|
"""Parse un fichier .conf WireGuard et retourne les valeurs extraites."""
|
||||||
|
result: dict = {}
|
||||||
|
section = None
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
for raw in f:
|
||||||
|
line = raw.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if line.startswith("["):
|
||||||
|
section = line.strip("[]").lower()
|
||||||
|
continue
|
||||||
|
if "=" not in line:
|
||||||
|
continue
|
||||||
|
key, val = (x.strip() for x in line.split("=", 1))
|
||||||
|
if section == "interface":
|
||||||
|
if key == "PrivateKey":
|
||||||
|
result["client_private_key"] = val
|
||||||
|
elif key == "Address":
|
||||||
|
result["client_address"] = val.split(",")[0].strip()
|
||||||
|
elif key == "DNS":
|
||||||
|
result["dns"] = val.split(",")[0].strip()
|
||||||
|
elif section == "peer":
|
||||||
|
if key == "PublicKey":
|
||||||
|
result["server_public_key"] = val
|
||||||
|
elif key == "Endpoint" and ":" in val:
|
||||||
|
host, port = val.rsplit(":", 1)
|
||||||
|
result["server_endpoint"] = host.strip("[]")
|
||||||
|
try:
|
||||||
|
result["server_port"] = int(port)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
elif key == "AllowedIPs":
|
||||||
|
result["allowed_ips"] = val
|
||||||
|
elif key == "PersistentKeepalive":
|
||||||
|
try:
|
||||||
|
result["keepalive"] = int(val)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
# Dériver la clé publique depuis la clé privée importée
|
||||||
|
if "client_private_key" in result and "client_public_key" not in result:
|
||||||
|
try:
|
||||||
|
priv_bytes = base64.b64decode(result["client_private_key"])
|
||||||
|
priv = X25519PrivateKey.from_private_bytes(priv_bytes)
|
||||||
|
pub_bytes = priv.public_key().public_bytes_raw()
|
||||||
|
result["client_public_key"] = base64.b64encode(pub_bytes).decode()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return result or None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def export_conf_file(cfg: Config, dest_path: str) -> tuple[bool, str]:
|
||||||
|
"""Exporte la configuration courante vers un fichier .conf."""
|
||||||
|
try:
|
||||||
|
content = build_client_config(cfg)
|
||||||
|
with open(dest_path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
if not is_windows():
|
||||||
|
os.chmod(dest_path, 0o600)
|
||||||
|
return True, dest_path
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def get_interface_bytes(iface: str) -> tuple[int, int] | None:
|
||||||
|
"""Retourne (rx_bytes, tx_bytes) depuis /proc/net/dev (Linux)."""
|
||||||
|
try:
|
||||||
|
with open("/proc/net/dev") as f:
|
||||||
|
for line in f:
|
||||||
|
if iface in line:
|
||||||
|
parts = line.split()
|
||||||
|
return int(parts[1]), int(parts[9])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ping_server(host: str, timeout: int = 2) -> int | None:
|
||||||
|
"""Ping ICMP du serveur. Retourne la latence en ms, ou None si inaccessible."""
|
||||||
|
if not host:
|
||||||
|
return None
|
||||||
|
if is_windows():
|
||||||
|
code, out, _ = run_command(
|
||||||
|
["ping", "-n", "1", "-w", str(timeout * 1000), host], timeout + 2
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
code, out, _ = run_command(
|
||||||
|
["ping", "-c", "1", "-W", str(timeout), host], timeout + 2
|
||||||
|
)
|
||||||
|
if code != 0:
|
||||||
|
return None
|
||||||
|
# Parser "time=X.X ms" ou "temps=X.X ms"
|
||||||
|
import re
|
||||||
|
m = re.search(r"[Tt]ime[<=]([\d.]+)\s*ms", out)
|
||||||
|
if m:
|
||||||
|
return int(float(m.group(1)))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ── Kill switch ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_KS_CHAIN = "WGS_KILLSWITCH"
|
||||||
|
|
||||||
|
|
||||||
|
def enable_kill_switch(cfg: Config) -> tuple[bool, str]:
|
||||||
|
"""Active le kill switch : bloque tout trafic hors du tunnel WireGuard."""
|
||||||
|
if is_windows():
|
||||||
|
return False, "Kill switch non disponible sur Windows (utilisez le pare-feu Windows)"
|
||||||
|
|
||||||
|
iface = cfg.wg.get("interface_name", "wgs0")
|
||||||
|
server = cfg.wg.get("server_endpoint", "")
|
||||||
|
port = str(cfg.wg.get("server_port", 51820))
|
||||||
|
|
||||||
|
# Créer la chaîne dédiée (idempotent)
|
||||||
|
run_privileged(["iptables", "-N", _KS_CHAIN])
|
||||||
|
run_privileged(["iptables", "-F", _KS_CHAIN])
|
||||||
|
|
||||||
|
rules = [
|
||||||
|
["iptables", "-A", _KS_CHAIN, "-o", "lo", "-j", "ACCEPT"],
|
||||||
|
["iptables", "-A", _KS_CHAIN, "-o", iface, "-j", "ACCEPT"],
|
||||||
|
["iptables", "-A", _KS_CHAIN, "-m", "state",
|
||||||
|
"--state", "ESTABLISHED,RELATED", "-j", "ACCEPT"],
|
||||||
|
]
|
||||||
|
if server:
|
||||||
|
rules.append(["iptables", "-A", _KS_CHAIN,
|
||||||
|
"-d", server, "-p", "udp", "--dport", port, "-j", "ACCEPT"])
|
||||||
|
rules.append(["iptables", "-A", _KS_CHAIN, "-j", "REJECT",
|
||||||
|
"--reject-with", "icmp-net-unreachable"])
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
code, _, err = run_privileged(rule)
|
||||||
|
if code != 0:
|
||||||
|
disable_kill_switch(cfg)
|
||||||
|
return False, f"Erreur iptables : {err}"
|
||||||
|
|
||||||
|
# Insérer la chaîne dans OUTPUT (évite les doublons)
|
||||||
|
run_privileged(["iptables", "-D", "OUTPUT", "-j", _KS_CHAIN])
|
||||||
|
code, _, err = run_privileged(["iptables", "-I", "OUTPUT", "-j", _KS_CHAIN])
|
||||||
|
if code != 0:
|
||||||
|
disable_kill_switch(cfg)
|
||||||
|
return False, err
|
||||||
|
|
||||||
|
return True, f"Kill switch activé (interface {iface})"
|
||||||
|
|
||||||
|
|
||||||
|
def disable_kill_switch(cfg: Config) -> tuple[bool, str]:
|
||||||
|
"""Désactive le kill switch."""
|
||||||
|
if is_windows():
|
||||||
|
return True, "N/A"
|
||||||
|
run_privileged(["iptables", "-D", "OUTPUT", "-j", _KS_CHAIN])
|
||||||
|
run_privileged(["iptables", "-F", _KS_CHAIN])
|
||||||
|
run_privileged(["iptables", "-X", _KS_CHAIN])
|
||||||
|
return True, "Kill switch désactivé"
|
||||||
|
|
||||||
|
|
||||||
|
def kill_switch_active() -> bool:
|
||||||
|
if is_windows():
|
||||||
|
return False
|
||||||
|
code, out, _ = run_command(["iptables", "-L", "OUTPUT", "-n"])
|
||||||
|
return _KS_CHAIN in out
|
||||||
|
|
||||||
|
|
||||||
|
# ── Test DNS leak ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def dns_leak_test(cfg: Config) -> dict:
|
||||||
|
"""
|
||||||
|
Teste si le DNS fuit en dehors du tunnel.
|
||||||
|
Retourne {"status": ok|leak|unknown, "resolvers": [...], "expected": str}
|
||||||
|
"""
|
||||||
|
expected_dns = cfg.wg.get("dns", "")
|
||||||
|
resolvers: list[str] = []
|
||||||
|
|
||||||
|
# 1. Lire /etc/resolv.conf (Linux) ou ipconfig /all (Windows)
|
||||||
|
if not is_windows():
|
||||||
|
try:
|
||||||
|
with open("/etc/resolv.conf") as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith("nameserver"):
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
resolvers.append(parts[1])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
code, out, _ = run_command(["ipconfig", "/all"])
|
||||||
|
if code == 0:
|
||||||
|
import re
|
||||||
|
resolvers = re.findall(r"DNS Servers.*?:(.*?)(?:\n\S|\Z)",
|
||||||
|
out, re.DOTALL)
|
||||||
|
|
||||||
|
# 2. Résoudre un domaine test via dig/nslookup pour voir quel serveur répond
|
||||||
|
probe_ip: str = ""
|
||||||
|
code, out, _ = run_command(["dig", "+short", "+time=3", "whoami.akamai.net"], 6)
|
||||||
|
if code == 0:
|
||||||
|
lines = [l.strip() for l in out.splitlines() if l.strip()]
|
||||||
|
if lines:
|
||||||
|
probe_ip = lines[-1]
|
||||||
|
|
||||||
|
if not resolvers:
|
||||||
|
return {"status": "unknown", "resolvers": [], "expected": expected_dns,
|
||||||
|
"probe_ip": probe_ip}
|
||||||
|
|
||||||
|
# 3. Comparer avec le DNS configuré
|
||||||
|
if expected_dns and any(expected_dns in r for r in resolvers):
|
||||||
|
status = "ok"
|
||||||
|
elif expected_dns:
|
||||||
|
status = "leak"
|
||||||
|
else:
|
||||||
|
status = "unknown"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": status,
|
||||||
|
"resolvers": resolvers,
|
||||||
|
"expected": expected_dns,
|
||||||
|
"probe_ip": probe_ip,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Génération config serveur ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def generate_server_config(
|
||||||
|
server_port: int = 51820,
|
||||||
|
server_address: str = "10.8.0.1/24",
|
||||||
|
client_address: str = "10.8.0.2/24",
|
||||||
|
client_allowed_ips: str = "10.8.0.2/32",
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Génère une paire complète (server_conf, client_conf) avec de nouvelles clés.
|
||||||
|
Retourne un dict avec server_priv/pub, client_priv/pub, server_conf, client_conf.
|
||||||
|
"""
|
||||||
|
srv_priv, srv_pub = generate_keypair()
|
||||||
|
cli_priv, cli_pub = generate_keypair()
|
||||||
|
psk = generate_preshared_key()
|
||||||
|
|
||||||
|
server_conf = "\n".join([
|
||||||
|
"[Interface]",
|
||||||
|
f"PrivateKey = {srv_priv}",
|
||||||
|
f"Address = {server_address}",
|
||||||
|
f"ListenPort = {server_port}",
|
||||||
|
"PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE",
|
||||||
|
"PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE",
|
||||||
|
"",
|
||||||
|
"# === Peer client ===",
|
||||||
|
"[Peer]",
|
||||||
|
f"PublicKey = {cli_pub}",
|
||||||
|
f"PresharedKey = {psk}",
|
||||||
|
f"AllowedIPs = {client_allowed_ips}",
|
||||||
|
])
|
||||||
|
|
||||||
|
client_conf = "\n".join([
|
||||||
|
"[Interface]",
|
||||||
|
f"PrivateKey = {cli_priv}",
|
||||||
|
f"Address = {client_address}",
|
||||||
|
"DNS = 1.1.1.1",
|
||||||
|
"",
|
||||||
|
"[Peer]",
|
||||||
|
f"PublicKey = {srv_pub}",
|
||||||
|
f"PresharedKey = {psk}",
|
||||||
|
"AllowedIPs = 0.0.0.0/0",
|
||||||
|
"Endpoint = <SERVER_IP>:" + str(server_port),
|
||||||
|
"PersistentKeepalive = 25",
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"server_priv": srv_priv, "server_pub": srv_pub,
|
||||||
|
"client_priv": cli_priv, "client_pub": cli_pub,
|
||||||
|
"psk": psk,
|
||||||
|
"server_conf": server_conf,
|
||||||
|
"client_conf": client_conf,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_status_info(cfg: Config) -> dict:
|
def get_status_info(cfg: Config) -> dict:
|
||||||
name = cfg.wg.get("interface_name", "wgs0")
|
name = cfg.wg.get("interface_name", "wgs0")
|
||||||
info = {
|
info = {
|
||||||
|
|||||||
+483
-47
@@ -2,6 +2,7 @@ from PyQt6.QtWidgets import (
|
|||||||
QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget,
|
QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget,
|
||||||
QLabel, QLineEdit, QPushButton, QSpinBox, QCheckBox,
|
QLabel, QLineEdit, QPushButton, QSpinBox, QCheckBox,
|
||||||
QGroupBox, QFormLayout, QTextEdit, QMessageBox, QFrame,
|
QGroupBox, QFormLayout, QTextEdit, QMessageBox, QFrame,
|
||||||
|
QFileDialog, QSizePolicy,
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt6.QtGui import QFont
|
from PyQt6.QtGui import QFont
|
||||||
@@ -127,12 +128,14 @@ class AdminWindow(QDialog):
|
|||||||
font-weight: bold; }
|
font-weight: bold; }
|
||||||
QTabBar::tab:hover { background: #253545; color: white; }
|
QTabBar::tab:hover { background: #253545; color: white; }
|
||||||
""")
|
""")
|
||||||
tabs.addTab(self._tab_wireguard(), "WireGuard")
|
tabs.addTab(self._tab_wireguard(), "WireGuard")
|
||||||
tabs.addTab(self._tab_keys(), "Clés")
|
tabs.addTab(self._tab_keys(), "Clés")
|
||||||
tabs.addTab(self._tab_mfa(), "MFA")
|
tabs.addTab(self._tab_mfa(), "MFA")
|
||||||
tabs.addTab(self._tab_test(), "Test connexion")
|
tabs.addTab(self._tab_test(), "Test connexion")
|
||||||
tabs.addTab(self._tab_admin(), "Sécurité")
|
tabs.addTab(self._tab_profiles(), "Profils")
|
||||||
tabs.addTab(self._tab_about(), "À propos")
|
tabs.addTab(self._tab_settings(), "Paramètres")
|
||||||
|
tabs.addTab(self._tab_admin(), "Sécurité")
|
||||||
|
tabs.addTab(self._tab_about(), "À propos")
|
||||||
layout.addWidget(tabs)
|
layout.addWidget(tabs)
|
||||||
|
|
||||||
# Barre de boutons
|
# Barre de boutons
|
||||||
@@ -165,41 +168,79 @@ class AdminWindow(QDialog):
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Onglet WireGuard
|
# Onglet WireGuard
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
@staticmethod
|
||||||
|
def _row(form: QFormLayout, text: str, widget,
|
||||||
|
label_w: int = 148, field_h: int = 28) -> None:
|
||||||
|
"""Ligne de formulaire : label largeur fixe + champ hauteur uniforme.
|
||||||
|
widget peut être un QWidget ou un QHBoxLayout (sans setFixedHeight)."""
|
||||||
|
from PyQt6.QtWidgets import QLayout
|
||||||
|
lbl = QLabel(text)
|
||||||
|
lbl.setFixedWidth(label_w)
|
||||||
|
lbl.setFixedHeight(field_h)
|
||||||
|
lbl.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||||
|
lbl.setStyleSheet("color: rgba(255,255,255,0.75); background: transparent;")
|
||||||
|
if not isinstance(widget, QLayout):
|
||||||
|
widget.setFixedHeight(field_h)
|
||||||
|
form.addRow(lbl, widget)
|
||||||
|
|
||||||
def _tab_wireguard(self) -> QWidget:
|
def _tab_wireguard(self) -> QWidget:
|
||||||
w, lay = self._dark_page("Configuration WireGuard", "#154360")
|
w, lay = self._dark_page("🛡️ Configuration WireGuard", "#154360")
|
||||||
|
|
||||||
|
# Barre import / export
|
||||||
|
io_row = QHBoxLayout()
|
||||||
|
btn_import = QPushButton("📂 Importer un .conf")
|
||||||
|
btn_import.clicked.connect(self._import_conf)
|
||||||
|
btn_export = QPushButton("💾 Exporter en .conf")
|
||||||
|
btn_export.clicked.connect(self._export_conf)
|
||||||
|
io_row.addWidget(btn_import)
|
||||||
|
io_row.addWidget(btn_export)
|
||||||
|
io_row.addStretch()
|
||||||
|
lay.addLayout(io_row)
|
||||||
|
|
||||||
grp = QGroupBox("Serveur WireGuard")
|
grp = QGroupBox("Serveur WireGuard")
|
||||||
form = QFormLayout(grp)
|
form = QFormLayout(grp)
|
||||||
|
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
||||||
|
|
||||||
self._srv_endpoint = QLineEdit()
|
self._srv_endpoint = QLineEdit()
|
||||||
self._srv_endpoint.setPlaceholderText("vpn.exemple.com ou 1.2.3.4")
|
self._srv_endpoint.setPlaceholderText("vpn.exemple.com ou 1.2.3.4")
|
||||||
form.addRow("Adresse serveur :", self._srv_endpoint)
|
self._row(form, "Adresse serveur :", self._srv_endpoint)
|
||||||
|
|
||||||
self._srv_port = QSpinBox()
|
self._srv_port = QSpinBox()
|
||||||
self._srv_port.setRange(1, 65535)
|
self._srv_port.setRange(1, 65535)
|
||||||
self._srv_port.setValue(51820)
|
self._srv_port.setValue(51820)
|
||||||
form.addRow("Port UDP :", self._srv_port)
|
self._srv_port.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||||
|
self._row(form, "Port UDP :", self._srv_port)
|
||||||
|
|
||||||
self._srv_pubkey = QLineEdit()
|
self._srv_pubkey = QLineEdit()
|
||||||
self._srv_pubkey.setPlaceholderText("Clé publique du serveur (base64)")
|
self._srv_pubkey.setPlaceholderText("Clé publique du serveur (base64)")
|
||||||
form.addRow("Clé publique serveur :", self._srv_pubkey)
|
self._row(form, "Clé publique serveur :", self._srv_pubkey)
|
||||||
lay.addWidget(grp)
|
lay.addWidget(grp)
|
||||||
|
|
||||||
grp2 = QGroupBox("Interface client")
|
grp2 = QGroupBox("Interface client")
|
||||||
form2 = QFormLayout(grp2)
|
form2 = QFormLayout(grp2)
|
||||||
|
form2.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
||||||
|
|
||||||
self._iface_name = QLineEdit()
|
self._iface_name = QLineEdit()
|
||||||
self._iface_name.setPlaceholderText("wgs0")
|
self._iface_name.setPlaceholderText("wgs0")
|
||||||
form2.addRow("Nom interface :", self._iface_name)
|
self._row(form2, "Nom interface :", self._iface_name)
|
||||||
|
|
||||||
self._client_addr = QLineEdit()
|
self._client_addr = QLineEdit()
|
||||||
self._client_addr.setPlaceholderText("10.8.0.2/24")
|
self._client_addr.setPlaceholderText("10.8.0.2/24")
|
||||||
form2.addRow("Adresse IP client :", self._client_addr)
|
self._row(form2, "Adresse IP client :", self._client_addr)
|
||||||
|
|
||||||
self._dns = QLineEdit()
|
self._dns = QLineEdit()
|
||||||
self._dns.setPlaceholderText("1.1.1.1")
|
self._dns.setPlaceholderText("1.1.1.1")
|
||||||
form2.addRow("DNS :", self._dns)
|
self._row(form2, "DNS :", self._dns)
|
||||||
|
|
||||||
self._allowed_ips = QLineEdit()
|
self._allowed_ips = QLineEdit()
|
||||||
self._allowed_ips.setPlaceholderText("0.0.0.0/0")
|
self._allowed_ips.setPlaceholderText("0.0.0.0/0")
|
||||||
form2.addRow("IPs autorisées :", self._allowed_ips)
|
self._row(form2, "IPs autorisées :", self._allowed_ips)
|
||||||
|
|
||||||
self._keepalive = QSpinBox()
|
self._keepalive = QSpinBox()
|
||||||
self._keepalive.setRange(0, 300)
|
self._keepalive.setRange(0, 300)
|
||||||
self._keepalive.setValue(25)
|
self._keepalive.setValue(25)
|
||||||
form2.addRow("Keepalive (s) :", self._keepalive)
|
self._keepalive.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||||
|
self._row(form2, "Keepalive (s) :", self._keepalive)
|
||||||
lay.addWidget(grp2)
|
lay.addWidget(grp2)
|
||||||
lay.addStretch()
|
lay.addStretch()
|
||||||
return w
|
return w
|
||||||
@@ -208,15 +249,18 @@ class AdminWindow(QDialog):
|
|||||||
# Onglet Clés
|
# Onglet Clés
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
def _tab_keys(self) -> QWidget:
|
def _tab_keys(self) -> QWidget:
|
||||||
w, lay = self._dark_page("Gestion des clés Curve25519", "#1a3a52")
|
w, lay = self._dark_page("🔑 Gestion des clés Curve25519", "#1a3a52")
|
||||||
|
|
||||||
grp = QGroupBox("Paire de clés client")
|
grp = QGroupBox("Paire de clés client")
|
||||||
form = QFormLayout(grp)
|
form = QFormLayout(grp)
|
||||||
|
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
||||||
|
|
||||||
self._priv_key = QLineEdit()
|
self._priv_key = QLineEdit()
|
||||||
self._priv_key.setEchoMode(QLineEdit.EchoMode.Password)
|
self._priv_key.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
self._priv_key.setPlaceholderText("(générer ou coller)")
|
self._priv_key.setPlaceholderText("(générer ou coller)")
|
||||||
|
self._priv_key.setFixedHeight(28)
|
||||||
show_priv = QPushButton("Afficher")
|
show_priv = QPushButton("Afficher")
|
||||||
show_priv.setFixedWidth(80)
|
show_priv.setFixedSize(70, 28)
|
||||||
show_priv.setCheckable(True)
|
show_priv.setCheckable(True)
|
||||||
show_priv.toggled.connect(
|
show_priv.toggled.connect(
|
||||||
lambda checked: self._priv_key.setEchoMode(
|
lambda checked: self._priv_key.setEchoMode(
|
||||||
@@ -224,16 +268,19 @@ class AdminWindow(QDialog):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
priv_row = QHBoxLayout()
|
priv_row = QHBoxLayout()
|
||||||
|
priv_row.setSpacing(6)
|
||||||
priv_row.addWidget(self._priv_key)
|
priv_row.addWidget(self._priv_key)
|
||||||
priv_row.addWidget(show_priv)
|
priv_row.addWidget(show_priv)
|
||||||
form.addRow("Clé privée :", priv_row)
|
self._row(form, "Clé privée :", priv_row)
|
||||||
|
|
||||||
self._pub_key = QLineEdit()
|
self._pub_key = QLineEdit()
|
||||||
self._pub_key.setReadOnly(True)
|
self._pub_key.setReadOnly(True)
|
||||||
self._pub_key.setPlaceholderText("(dérivée automatiquement)")
|
self._pub_key.setPlaceholderText("(dérivée automatiquement)")
|
||||||
form.addRow("Clé publique :", self._pub_key)
|
self._row(form, "Clé publique :", self._pub_key)
|
||||||
|
|
||||||
btn_gen = QPushButton("Générer une nouvelle paire de clés")
|
btn_gen = QPushButton("Générer une nouvelle paire de clés")
|
||||||
btn_gen.clicked.connect(self._generate_keys)
|
btn_gen.setFixedHeight(28)
|
||||||
form.addRow("", btn_gen)
|
self._row(form, "", btn_gen)
|
||||||
lay.addWidget(grp)
|
lay.addWidget(grp)
|
||||||
|
|
||||||
grp2 = QGroupBox("Aperçu config WireGuard")
|
grp2 = QGroupBox("Aperçu config WireGuard")
|
||||||
@@ -241,12 +288,28 @@ class AdminWindow(QDialog):
|
|||||||
self._config_preview = QTextEdit()
|
self._config_preview = QTextEdit()
|
||||||
self._config_preview.setReadOnly(True)
|
self._config_preview.setReadOnly(True)
|
||||||
self._config_preview.setFont(QFont("Courier", 9))
|
self._config_preview.setFont(QFont("Courier", 9))
|
||||||
self._config_preview.setFixedHeight(150)
|
self._config_preview.setFixedHeight(130)
|
||||||
v2.addWidget(self._config_preview)
|
v2.addWidget(self._config_preview)
|
||||||
btn_preview = QPushButton("Rafraîchir l'aperçu")
|
btn_row2 = QHBoxLayout()
|
||||||
|
btn_preview = QPushButton("🔄 Rafraîchir")
|
||||||
btn_preview.clicked.connect(self._refresh_preview)
|
btn_preview.clicked.connect(self._refresh_preview)
|
||||||
v2.addWidget(btn_preview)
|
btn_qr = QPushButton("📱 Exporter QR Code")
|
||||||
|
btn_qr.clicked.connect(self._export_qr)
|
||||||
|
btn_row2.addWidget(btn_preview)
|
||||||
|
btn_row2.addWidget(btn_qr)
|
||||||
|
v2.addLayout(btn_row2)
|
||||||
lay.addWidget(grp2)
|
lay.addWidget(grp2)
|
||||||
|
|
||||||
|
# Config serveur
|
||||||
|
grp3 = QGroupBox("🖥️ Génération config serveur + client")
|
||||||
|
v3 = QVBoxLayout(grp3)
|
||||||
|
note = QLabel("Génère une paire complète prête à déployer (nouvelles clés, PSK).")
|
||||||
|
note.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 11px;")
|
||||||
|
v3.addWidget(note)
|
||||||
|
btn_gen_srv = QPushButton("⚡ Générer config serveur + client")
|
||||||
|
btn_gen_srv.clicked.connect(self._generate_server_config)
|
||||||
|
v3.addWidget(btn_gen_srv)
|
||||||
|
lay.addWidget(grp3)
|
||||||
lay.addStretch()
|
lay.addStretch()
|
||||||
return w
|
return w
|
||||||
|
|
||||||
@@ -254,7 +317,7 @@ class AdminWindow(QDialog):
|
|||||||
# Onglet MFA
|
# Onglet MFA
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
def _tab_mfa(self) -> QWidget:
|
def _tab_mfa(self) -> QWidget:
|
||||||
w, lay = self._dark_page("Authentification Multi-Facteurs (MFA / TOTP)", "#0e6655")
|
w, lay = self._dark_page("🔐 Authentification Multi-Facteurs (MFA / TOTP)", "#0e6655")
|
||||||
|
|
||||||
self._mfa_enabled_cb = QCheckBox("Activer la vérification MFA avant connexion")
|
self._mfa_enabled_cb = QCheckBox("Activer la vérification MFA avant connexion")
|
||||||
self._mfa_enabled_cb.setStyleSheet("color: white; font-weight: bold;")
|
self._mfa_enabled_cb.setStyleSheet("color: white; font-weight: bold;")
|
||||||
@@ -267,12 +330,13 @@ class AdminWindow(QDialog):
|
|||||||
|
|
||||||
grp = QGroupBox("Secret TOTP")
|
grp = QGroupBox("Secret TOTP")
|
||||||
form = QFormLayout(grp)
|
form = QFormLayout(grp)
|
||||||
|
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
||||||
self._mfa_secret = QLineEdit()
|
self._mfa_secret = QLineEdit()
|
||||||
self._mfa_secret.setPlaceholderText("(générer ou coller votre secret TOTP)")
|
self._mfa_secret.setPlaceholderText("(générer ou coller votre secret TOTP)")
|
||||||
form.addRow("Secret :", self._mfa_secret)
|
self._row(form, "Secret :", self._mfa_secret)
|
||||||
btn_gen_secret = QPushButton("Générer un nouveau secret")
|
btn_gen_secret = QPushButton("Générer un nouveau secret")
|
||||||
btn_gen_secret.clicked.connect(self._generate_mfa_secret)
|
btn_gen_secret.clicked.connect(self._generate_mfa_secret)
|
||||||
form.addRow("", btn_gen_secret)
|
self._row(form, "", btn_gen_secret)
|
||||||
lay.addWidget(grp)
|
lay.addWidget(grp)
|
||||||
|
|
||||||
grp2 = QGroupBox("QR Code — Scanner avec Google Authenticator / Aegis")
|
grp2 = QGroupBox("QR Code — Scanner avec Google Authenticator / Aegis")
|
||||||
@@ -296,39 +360,227 @@ class AdminWindow(QDialog):
|
|||||||
# Onglet Test connexion
|
# Onglet Test connexion
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
def _tab_test(self) -> QWidget:
|
def _tab_test(self) -> QWidget:
|
||||||
w, lay = self._dark_page("Test de connectivité réseau", "#1a4a7a")
|
w, lay = self._dark_page("📡 Test de connectivité réseau", "#1a4a7a")
|
||||||
|
|
||||||
info = QLabel(
|
# Test UDP serveur
|
||||||
"Teste la joignabilité du serveur WireGuard (port UDP) avant d'établir le tunnel."
|
grp1 = QGroupBox("Test UDP serveur")
|
||||||
)
|
g1 = QVBoxLayout(grp1)
|
||||||
info.setWordWrap(True)
|
g1.addWidget(QLabel("Vérifie la joignabilité UDP du serveur WireGuard."))
|
||||||
info.setStyleSheet("color: white; font-size: 13px;")
|
btn_test = QPushButton("🚀 Lancer le test de connexion")
|
||||||
lay.addWidget(info)
|
|
||||||
|
|
||||||
btn_test = QPushButton("Lancer le test de connexion")
|
|
||||||
btn_test.setStyleSheet(
|
btn_test.setStyleSheet(
|
||||||
"QPushButton { padding: 10px; background: #1e8449; color: white;"
|
"QPushButton { padding: 9px; background: #1e8449; color: white;"
|
||||||
" border-radius: 5px; font-weight: bold; border: none; }"
|
" border-radius: 5px; font-weight: bold; border: none; }"
|
||||||
"QPushButton:hover { background: #27ae60; }"
|
"QPushButton:hover { background: #27ae60; }"
|
||||||
)
|
)
|
||||||
btn_test.clicked.connect(self._run_test)
|
btn_test.clicked.connect(self._run_test)
|
||||||
lay.addWidget(btn_test)
|
g1.addWidget(btn_test)
|
||||||
|
|
||||||
self._test_result = QLabel("")
|
self._test_result = QLabel("")
|
||||||
self._test_result.setWordWrap(True)
|
self._test_result.setWordWrap(True)
|
||||||
self._test_result.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self._test_result.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self._test_result.setStyleSheet(
|
self._test_result.setStyleSheet(
|
||||||
"color: white; padding: 12px; border-radius: 6px; font-size: 13px;"
|
"color: white; padding: 8px; border-radius: 6px; font-size: 12px;"
|
||||||
)
|
)
|
||||||
lay.addWidget(self._test_result)
|
g1.addWidget(self._test_result)
|
||||||
|
lay.addWidget(grp1)
|
||||||
|
|
||||||
|
# Test DNS leak
|
||||||
|
grp2 = QGroupBox("🔍 Test de fuite DNS")
|
||||||
|
g2 = QVBoxLayout(grp2)
|
||||||
|
g2.addWidget(QLabel("Vérifie que le DNS passe bien par le tunnel VPN."))
|
||||||
|
btn_dns = QPushButton("🔍 Analyser le DNS")
|
||||||
|
btn_dns.clicked.connect(self._run_dns_test)
|
||||||
|
g2.addWidget(btn_dns)
|
||||||
|
self._dns_result = QLabel("")
|
||||||
|
self._dns_result.setWordWrap(True)
|
||||||
|
self._dns_result.setStyleSheet(
|
||||||
|
"color: white; padding: 8px; border-radius: 6px; font-size: 11px;"
|
||||||
|
)
|
||||||
|
g2.addWidget(self._dns_result)
|
||||||
|
lay.addWidget(grp2)
|
||||||
lay.addStretch()
|
lay.addStretch()
|
||||||
return w
|
return w
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Onglet Profils
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def _tab_profiles(self) -> QWidget:
|
||||||
|
from PyQt6.QtWidgets import QListWidget, QListWidgetItem, QInputDialog
|
||||||
|
w, lay = self._dark_page("👤 Gestion des profils", "#1a4a2a")
|
||||||
|
|
||||||
|
info = QLabel(
|
||||||
|
"Sauvegardez la configuration WireGuard courante sous un nom de profil,\n"
|
||||||
|
"puis basculez entre profils sans passer par la configuration."
|
||||||
|
)
|
||||||
|
info.setWordWrap(True)
|
||||||
|
info.setStyleSheet("color: rgba(255,255,255,0.75); font-size: 12px;")
|
||||||
|
lay.addWidget(info)
|
||||||
|
|
||||||
|
grp = QGroupBox("Profils sauvegardés")
|
||||||
|
gv = QVBoxLayout(grp)
|
||||||
|
|
||||||
|
self._profile_list = QListWidget()
|
||||||
|
self._profile_list.setFixedHeight(130)
|
||||||
|
self._profile_list.setStyleSheet(
|
||||||
|
"QListWidget { background: rgba(255,255,255,0.07); color: white;"
|
||||||
|
" border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; }"
|
||||||
|
"QListWidget::item:selected { background: #2471a3; }"
|
||||||
|
)
|
||||||
|
gv.addWidget(self._profile_list)
|
||||||
|
|
||||||
|
btn_row = QHBoxLayout()
|
||||||
|
btn_save_p = QPushButton("💾 Sauvegarder sous…")
|
||||||
|
btn_save_p.clicked.connect(self._save_profile)
|
||||||
|
btn_load_p = QPushButton("✅ Charger")
|
||||||
|
btn_load_p.clicked.connect(self._load_profile)
|
||||||
|
btn_del_p = QPushButton("🗑️ Supprimer")
|
||||||
|
btn_del_p.setStyleSheet(
|
||||||
|
"QPushButton { background: #6e2e1c; color: white; border-radius: 5px;"
|
||||||
|
" padding: 7px 12px; border: none; }"
|
||||||
|
"QPushButton:hover { background: #922b21; }"
|
||||||
|
)
|
||||||
|
btn_del_p.clicked.connect(self._delete_profile)
|
||||||
|
btn_row.addWidget(btn_save_p)
|
||||||
|
btn_row.addWidget(btn_load_p)
|
||||||
|
btn_row.addWidget(btn_del_p)
|
||||||
|
gv.addLayout(btn_row)
|
||||||
|
lay.addWidget(grp)
|
||||||
|
lay.addStretch()
|
||||||
|
|
||||||
|
self._refresh_profile_list()
|
||||||
|
return w
|
||||||
|
|
||||||
|
def _refresh_profile_list(self):
|
||||||
|
self._profile_list.clear()
|
||||||
|
active = self._cfg.active_profile
|
||||||
|
for name in self._cfg.list_profiles():
|
||||||
|
marker = "★" if name == active else " "
|
||||||
|
self._profile_list.addItem(f"{marker} {name}")
|
||||||
|
|
||||||
|
def _save_profile(self):
|
||||||
|
from PyQt6.QtWidgets import QInputDialog
|
||||||
|
self._save_values()
|
||||||
|
name, ok = QInputDialog.getText(self, "Sauvegarder le profil",
|
||||||
|
"Nom du profil :")
|
||||||
|
if not ok or not name.strip():
|
||||||
|
return
|
||||||
|
self._cfg.save_profile(name.strip())
|
||||||
|
self._refresh_profile_list()
|
||||||
|
QMessageBox.information(self, "Profil sauvegardé",
|
||||||
|
f"Profil « {name.strip()} » sauvegardé.")
|
||||||
|
|
||||||
|
def _load_profile(self):
|
||||||
|
item = self._profile_list.currentItem()
|
||||||
|
if not item:
|
||||||
|
return
|
||||||
|
name = item.text().lstrip("★ ").strip()
|
||||||
|
if self._cfg.load_profile(name):
|
||||||
|
self._load_values()
|
||||||
|
self._refresh_profile_list()
|
||||||
|
QMessageBox.information(self, "Profil chargé",
|
||||||
|
f"Profil « {name} » chargé.")
|
||||||
|
|
||||||
|
def _delete_profile(self):
|
||||||
|
item = self._profile_list.currentItem()
|
||||||
|
if not item:
|
||||||
|
return
|
||||||
|
name = item.text().lstrip("★ ").strip()
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self, "Confirmer",
|
||||||
|
f"Supprimer le profil « {name} » ?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
|
)
|
||||||
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
|
self._cfg.delete_profile(name)
|
||||||
|
self._refresh_profile_list()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Onglet Paramètres
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def _tab_settings(self) -> QWidget:
|
||||||
|
w, lay = self._dark_page("⚙️ Paramètres de l'application", "#2a1a4a")
|
||||||
|
|
||||||
|
grp1 = QGroupBox("Comportement")
|
||||||
|
form1 = QFormLayout(grp1)
|
||||||
|
|
||||||
|
self._chk_tray = QCheckBox("Réduire dans le systray à la fermeture")
|
||||||
|
self._chk_tray.setStyleSheet("color: white;")
|
||||||
|
self._chk_tray.setChecked(bool(self._cfg.get("ui", "minimize_to_tray")))
|
||||||
|
form1.addRow("", self._chk_tray)
|
||||||
|
|
||||||
|
self._chk_autostart = QCheckBox("Lancer WGSecure au démarrage du système")
|
||||||
|
self._chk_autostart.setStyleSheet("color: white;")
|
||||||
|
self._chk_autostart.setChecked(bool(self._cfg.get("ui", "autostart")))
|
||||||
|
form1.addRow("", self._chk_autostart)
|
||||||
|
|
||||||
|
lay.addWidget(grp1)
|
||||||
|
|
||||||
|
grp_ks = QGroupBox("🔒 Kill Switch (Linux)")
|
||||||
|
g_ks = QVBoxLayout(grp_ks)
|
||||||
|
note_ks = QLabel(
|
||||||
|
"Bloque tout trafic internet si le tunnel VPN tombe.\n"
|
||||||
|
"Nécessite des droits root (iptables)."
|
||||||
|
)
|
||||||
|
note_ks.setWordWrap(True)
|
||||||
|
note_ks.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 11px;")
|
||||||
|
g_ks.addWidget(note_ks)
|
||||||
|
ks_row = QHBoxLayout()
|
||||||
|
self._btn_ks_enable = QPushButton("🔒 Activer")
|
||||||
|
self._btn_ks_enable.clicked.connect(self._enable_kill_switch)
|
||||||
|
self._btn_ks_disable = QPushButton("🔓 Désactiver")
|
||||||
|
self._btn_ks_disable.setStyleSheet(
|
||||||
|
"QPushButton { background: #6e2e1c; border-radius: 5px; padding: 7px 12px;"
|
||||||
|
" color: white; border: none; } QPushButton:hover { background: #922b21; }"
|
||||||
|
)
|
||||||
|
self._btn_ks_disable.clicked.connect(self._disable_kill_switch)
|
||||||
|
self._ks_status = QLabel("—")
|
||||||
|
self._ks_status.setStyleSheet("font-size: 11px; color: #aed6f1;")
|
||||||
|
ks_row.addWidget(self._btn_ks_enable)
|
||||||
|
ks_row.addWidget(self._btn_ks_disable)
|
||||||
|
ks_row.addWidget(self._ks_status)
|
||||||
|
ks_row.addStretch()
|
||||||
|
g_ks.addLayout(ks_row)
|
||||||
|
lay.addWidget(grp_ks)
|
||||||
|
self._refresh_ks_status()
|
||||||
|
|
||||||
|
grp2 = QGroupBox("Auto-reconnexion")
|
||||||
|
form2 = QFormLayout(grp2)
|
||||||
|
|
||||||
|
self._chk_autorecon = QCheckBox("Reconnecter automatiquement si le tunnel tombe")
|
||||||
|
self._chk_autorecon.setStyleSheet("color: white;")
|
||||||
|
self._chk_autorecon.setChecked(bool(self._cfg.get("ui", "auto_reconnect")))
|
||||||
|
form2.addRow("", self._chk_autorecon)
|
||||||
|
|
||||||
|
self._spin_interval = QSpinBox()
|
||||||
|
self._spin_interval.setRange(10, 300)
|
||||||
|
self._spin_interval.setValue(int(self._cfg.get("ui", "reconnect_interval") or 30))
|
||||||
|
self._spin_interval.setSuffix(" s")
|
||||||
|
self._spin_interval.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||||
|
self._row(form2, "Intervalle :", self._spin_interval)
|
||||||
|
|
||||||
|
lay.addWidget(grp2)
|
||||||
|
|
||||||
|
btn_apply = QPushButton("✅ Appliquer les paramètres")
|
||||||
|
btn_apply.clicked.connect(self._apply_settings)
|
||||||
|
lay.addWidget(btn_apply)
|
||||||
|
lay.addStretch()
|
||||||
|
return w
|
||||||
|
|
||||||
|
def _apply_settings(self):
|
||||||
|
self._cfg.set("ui", "minimize_to_tray", self._chk_tray.isChecked())
|
||||||
|
self._cfg.set("ui", "auto_reconnect", self._chk_autorecon.isChecked())
|
||||||
|
self._cfg.set("ui", "reconnect_interval", self._spin_interval.value())
|
||||||
|
autostart = self._chk_autostart.isChecked()
|
||||||
|
if autostart != bool(self._cfg.get("ui", "autostart")):
|
||||||
|
self._cfg.set_autostart(autostart)
|
||||||
|
else:
|
||||||
|
self._cfg.save()
|
||||||
|
QMessageBox.information(self, "Paramètres", "Paramètres enregistrés.")
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Onglet Sécurité
|
# Onglet Sécurité
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
def _tab_admin(self) -> QWidget:
|
def _tab_admin(self) -> QWidget:
|
||||||
w, lay = self._dark_page("Sécurité — Accès Administrateur", "#6e2e1c")
|
w, lay = self._dark_page("🔒 Sécurité — Accès Administrateur", "#6e2e1c")
|
||||||
|
|
||||||
note = QLabel(
|
note = QLabel(
|
||||||
"Définissez un mot de passe pour protéger l'accès au panneau Administrateur.\n"
|
"Définissez un mot de passe pour protéger l'accès au panneau Administrateur.\n"
|
||||||
@@ -340,22 +592,26 @@ class AdminWindow(QDialog):
|
|||||||
|
|
||||||
grp = QGroupBox("Mot de passe administrateur")
|
grp = QGroupBox("Mot de passe administrateur")
|
||||||
form = QFormLayout(grp)
|
form = QFormLayout(grp)
|
||||||
|
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
||||||
|
|
||||||
self._admin_pw1 = QLineEdit()
|
self._admin_pw1 = QLineEdit()
|
||||||
self._admin_pw1.setEchoMode(QLineEdit.EchoMode.Password)
|
self._admin_pw1.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
self._admin_pw1.setPlaceholderText("Nouveau mot de passe")
|
self._admin_pw1.setPlaceholderText("Nouveau mot de passe")
|
||||||
form.addRow("Mot de passe :", self._admin_pw1)
|
self._row(form, "Mot de passe :", self._admin_pw1)
|
||||||
|
|
||||||
self._admin_pw2 = QLineEdit()
|
self._admin_pw2 = QLineEdit()
|
||||||
self._admin_pw2.setEchoMode(QLineEdit.EchoMode.Password)
|
self._admin_pw2.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
self._admin_pw2.setPlaceholderText("Confirmer")
|
self._admin_pw2.setPlaceholderText("Confirmer")
|
||||||
form.addRow("Confirmation :", self._admin_pw2)
|
self._row(form, "Confirmation :", self._admin_pw2)
|
||||||
|
|
||||||
btn_set_pw = QPushButton("Définir le mot de passe")
|
btn_set_pw = QPushButton("Définir le mot de passe")
|
||||||
btn_set_pw.setStyleSheet(
|
btn_set_pw.setStyleSheet(
|
||||||
"QPushButton { padding: 8px; background: #922b21; color: white;"
|
"QPushButton { background: #922b21; color: white;"
|
||||||
" border-radius: 5px; border: none; }"
|
" border-radius: 5px; border: none; }"
|
||||||
"QPushButton:hover { background: #c0392b; }"
|
"QPushButton:hover { background: #c0392b; }"
|
||||||
)
|
)
|
||||||
btn_set_pw.clicked.connect(self._set_admin_password)
|
btn_set_pw.clicked.connect(self._set_admin_password)
|
||||||
form.addRow("", btn_set_pw)
|
self._row(form, "", btn_set_pw)
|
||||||
lay.addWidget(grp)
|
lay.addWidget(grp)
|
||||||
lay.addStretch()
|
lay.addStretch()
|
||||||
return w
|
return w
|
||||||
@@ -385,7 +641,7 @@ class AdminWindow(QDialog):
|
|||||||
name_lbl.setStyleSheet("color: white;")
|
name_lbl.setStyleSheet("color: white;")
|
||||||
layout.addWidget(name_lbl)
|
layout.addWidget(name_lbl)
|
||||||
|
|
||||||
version_lbl = QLabel("Version 0.1.0")
|
version_lbl = QLabel("Version 0.3.0")
|
||||||
version_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
version_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
version_lbl.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 13px;")
|
version_lbl.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 13px;")
|
||||||
layout.addWidget(version_lbl)
|
layout.addWidget(version_lbl)
|
||||||
@@ -468,6 +724,54 @@ class AdminWindow(QDialog):
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Actions
|
# Actions
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
def _import_conf(self):
|
||||||
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self, "Importer un fichier WireGuard",
|
||||||
|
"", "WireGuard Config (*.conf);;Tous les fichiers (*)"
|
||||||
|
)
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
values = wg_core.parse_conf_file(path)
|
||||||
|
if not values:
|
||||||
|
QMessageBox.warning(self, "Erreur", "Impossible de lire ce fichier .conf")
|
||||||
|
return
|
||||||
|
mapping = {
|
||||||
|
"server_endpoint": self._srv_endpoint,
|
||||||
|
"server_public_key": self._srv_pubkey,
|
||||||
|
"client_address": self._client_addr,
|
||||||
|
"dns": self._dns,
|
||||||
|
"allowed_ips": self._allowed_ips,
|
||||||
|
"client_private_key": self._priv_key,
|
||||||
|
"client_public_key": self._pub_key,
|
||||||
|
}
|
||||||
|
for k, widget in mapping.items():
|
||||||
|
if k in values:
|
||||||
|
widget.setText(str(values[k]))
|
||||||
|
if "server_port" in values:
|
||||||
|
self._srv_port.setValue(int(values["server_port"]))
|
||||||
|
if "keepalive" in values:
|
||||||
|
self._keepalive.setValue(int(values["keepalive"]))
|
||||||
|
self._save_values()
|
||||||
|
QMessageBox.information(
|
||||||
|
self, "Import réussi",
|
||||||
|
f"Configuration importée depuis :\n{path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _export_conf(self):
|
||||||
|
self._save_values()
|
||||||
|
path, _ = QFileDialog.getSaveFileName(
|
||||||
|
self, "Exporter la configuration WireGuard",
|
||||||
|
f"{self._cfg.wg.get('interface_name','wgs0')}.conf",
|
||||||
|
"WireGuard Config (*.conf)"
|
||||||
|
)
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
ok, result = wg_core.export_conf_file(self._cfg, path)
|
||||||
|
if ok:
|
||||||
|
QMessageBox.information(self, "Export réussi", f"Config exportée :\n{result}")
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(self, "Erreur", f"Export échoué :\n{result}")
|
||||||
|
|
||||||
def _generate_keys(self):
|
def _generate_keys(self):
|
||||||
priv, pub = wg_core.generate_keypair()
|
priv, pub = wg_core.generate_keypair()
|
||||||
self._priv_key.setText(priv)
|
self._priv_key.setText(priv)
|
||||||
@@ -522,6 +826,138 @@ class AdminWindow(QDialog):
|
|||||||
"background: rgba(231,76,60,0.25); color: #f1948a;"
|
"background: rgba(231,76,60,0.25); color: #f1948a;"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _export_qr(self):
|
||||||
|
"""Exporte la config client comme QR Code pour import mobile."""
|
||||||
|
self._save_values()
|
||||||
|
if not self._cfg.configured:
|
||||||
|
QMessageBox.warning(self, "Erreur", "Config WireGuard incomplète.")
|
||||||
|
return
|
||||||
|
from PyQt6.QtWidgets import QDialog, QLabel, QVBoxLayout
|
||||||
|
import qrcode, io
|
||||||
|
from PyQt6.QtGui import QImage, QPixmap
|
||||||
|
|
||||||
|
content = wg_core.build_client_config(self._cfg)
|
||||||
|
qr = qrcode.QRCode(box_size=5, border=2,
|
||||||
|
error_correction=qrcode.constants.ERROR_CORRECT_L)
|
||||||
|
qr.add_data(content)
|
||||||
|
qr.make(fit=True)
|
||||||
|
img = qr.make_image(fill_color="black", back_color="white")
|
||||||
|
buf = io.BytesIO()
|
||||||
|
img.save(buf, "PNG")
|
||||||
|
buf.seek(0)
|
||||||
|
pix = QPixmap.fromImage(QImage.fromData(buf.read()))
|
||||||
|
|
||||||
|
dlg = QDialog(self)
|
||||||
|
dlg.setWindowTitle("QR Code — Config WireGuard client")
|
||||||
|
dlg.setStyleSheet("background: #1c2833; color: white;")
|
||||||
|
v = QVBoxLayout(dlg)
|
||||||
|
lbl = QLabel()
|
||||||
|
lbl.setPixmap(pix.scaled(300, 300, Qt.AspectRatioMode.KeepAspectRatio,
|
||||||
|
Qt.TransformationMode.SmoothTransformation))
|
||||||
|
lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
note = QLabel("Scannez avec l'application WireGuard sur mobile.")
|
||||||
|
note.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
note.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 11px;")
|
||||||
|
v.addWidget(lbl)
|
||||||
|
v.addWidget(note)
|
||||||
|
dlg.exec()
|
||||||
|
|
||||||
|
def _generate_server_config(self):
|
||||||
|
"""Génère et affiche une paire complète server + client."""
|
||||||
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QTabWidget, QTextEdit, QPushButton
|
||||||
|
result = wg_core.generate_server_config(
|
||||||
|
server_port=self._srv_port.value() if hasattr(self, '_srv_port') else 51820
|
||||||
|
)
|
||||||
|
dlg = QDialog(self)
|
||||||
|
dlg.setWindowTitle("Config générée — Server + Client")
|
||||||
|
dlg.setMinimumSize(600, 420)
|
||||||
|
dlg.setStyleSheet("background: #1c2833; color: white;")
|
||||||
|
v = QVBoxLayout(dlg)
|
||||||
|
tabs = QTabWidget()
|
||||||
|
tabs.setStyleSheet(
|
||||||
|
"QTabBar::tab { background:#2e4057; color:white; padding:6px 14px; }"
|
||||||
|
"QTabBar::tab:selected { background:#2471a3; }"
|
||||||
|
)
|
||||||
|
for title, content in [("🖥️ Serveur", result["server_conf"]),
|
||||||
|
("💻 Client", result["client_conf"])]:
|
||||||
|
te = QTextEdit()
|
||||||
|
te.setReadOnly(True)
|
||||||
|
te.setFont(QFont("Courier", 9))
|
||||||
|
te.setPlainText(content)
|
||||||
|
te.setStyleSheet(
|
||||||
|
"background: #17202a; color: #a8d8ea; border: none; font-size: 11px;"
|
||||||
|
)
|
||||||
|
tabs.addTab(te, title)
|
||||||
|
v.addWidget(tabs)
|
||||||
|
|
||||||
|
note = QLabel(
|
||||||
|
f"🔑 Clé pub. serveur : {result['server_pub'][:32]}…\n"
|
||||||
|
f"🔑 Clé pub. client : {result['client_pub'][:32]}…\n"
|
||||||
|
"⚠️ Remplacez <SERVER_IP> dans la config client par l'IP publique du serveur."
|
||||||
|
)
|
||||||
|
note.setWordWrap(True)
|
||||||
|
note.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 10px; padding: 6px;")
|
||||||
|
v.addWidget(note)
|
||||||
|
|
||||||
|
btn_close = QPushButton("Fermer")
|
||||||
|
btn_close.setStyleSheet(
|
||||||
|
"QPushButton { background:#2471a3; color:white; border-radius:5px;"
|
||||||
|
" padding:7px 16px; border:none; } QPushButton:hover { background:#1a5276; }"
|
||||||
|
)
|
||||||
|
btn_close.clicked.connect(dlg.accept)
|
||||||
|
v.addWidget(btn_close)
|
||||||
|
dlg.exec()
|
||||||
|
|
||||||
|
def _run_dns_test(self):
|
||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
self._dns_result.setText("Analyse en cours…")
|
||||||
|
QApplication.processEvents()
|
||||||
|
r = wg_core.dns_leak_test(self._cfg)
|
||||||
|
lines = []
|
||||||
|
status = r["status"]
|
||||||
|
if status == "ok":
|
||||||
|
lines.append("✅ Aucune fuite DNS détectée")
|
||||||
|
css = "background: rgba(39,174,96,0.2); color: #a9dfbf;"
|
||||||
|
elif status == "leak":
|
||||||
|
lines.append("⚠️ Fuite DNS potentielle !")
|
||||||
|
css = "background: rgba(231,76,60,0.2); color: #f1948a;"
|
||||||
|
else:
|
||||||
|
lines.append("❓ Statut inconnu")
|
||||||
|
css = "color: #aed6f1;"
|
||||||
|
if r["resolvers"]:
|
||||||
|
lines.append("Serveurs DNS actifs : " + " • ".join(r["resolvers"]))
|
||||||
|
if r["expected"]:
|
||||||
|
lines.append(f"DNS configuré (VPN) : {r['expected']}")
|
||||||
|
if r.get("probe_ip"):
|
||||||
|
lines.append(f"IP retournée par whoami.akamai.net : {r['probe_ip']}")
|
||||||
|
self._dns_result.setText("\n".join(lines))
|
||||||
|
self._dns_result.setStyleSheet(
|
||||||
|
f"padding: 8px; border-radius: 6px; font-size: 11px; {css}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _refresh_ks_status(self):
|
||||||
|
active = wg_core.kill_switch_active()
|
||||||
|
if active:
|
||||||
|
self._ks_status.setText("🔒 Actif")
|
||||||
|
self._ks_status.setStyleSheet("color: #a9dfbf; font-size: 11px;")
|
||||||
|
else:
|
||||||
|
self._ks_status.setText("🔓 Inactif")
|
||||||
|
self._ks_status.setStyleSheet("color: #f1948a; font-size: 11px;")
|
||||||
|
|
||||||
|
def _enable_kill_switch(self):
|
||||||
|
self._save_values()
|
||||||
|
ok, msg = wg_core.enable_kill_switch(self._cfg)
|
||||||
|
self._refresh_ks_status()
|
||||||
|
if ok:
|
||||||
|
QMessageBox.information(self, "Kill Switch", msg)
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(self, "Kill Switch", msg)
|
||||||
|
|
||||||
|
def _disable_kill_switch(self):
|
||||||
|
ok, msg = wg_core.disable_kill_switch(self._cfg)
|
||||||
|
self._refresh_ks_status()
|
||||||
|
QMessageBox.information(self, "Kill Switch", msg)
|
||||||
|
|
||||||
def _set_admin_password(self):
|
def _set_admin_password(self):
|
||||||
p1 = self._admin_pw1.text()
|
p1 = self._admin_pw1.text()
|
||||||
p2 = self._admin_pw2.text()
|
p2 = self._admin_pw2.text()
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""Widget graphique bande passante RX/TX (QPainter, sans dépendance externe)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
from collections import deque
|
||||||
|
from PyQt6.QtWidgets import QWidget
|
||||||
|
from PyQt6.QtGui import QPainter, QColor, QPen, QBrush, QFont, QPainterPath
|
||||||
|
from PyQt6.QtCore import Qt, QRect, QPointF
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt(bps: float) -> str:
|
||||||
|
if bps >= 1_048_576:
|
||||||
|
return f"{bps/1_048_576:.1f} MB/s"
|
||||||
|
if bps >= 1_024:
|
||||||
|
return f"{bps/1_024:.0f} KB/s"
|
||||||
|
return f"{bps:.0f} B/s"
|
||||||
|
|
||||||
|
|
||||||
|
class BandwidthGraph(QWidget):
|
||||||
|
"""Affiche RX (bleu) et TX (vert) sur les N dernières secondes."""
|
||||||
|
|
||||||
|
POINTS = 30 # nombre de points conservés
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._rx: deque[float] = deque([0.0] * self.POINTS, maxlen=self.POINTS)
|
||||||
|
self._tx: deque[float] = deque([0.0] * self.POINTS, maxlen=self.POINTS)
|
||||||
|
self.setMinimumHeight(70)
|
||||||
|
self.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
def push(self, rx_bps: float, tx_bps: float) -> None:
|
||||||
|
self._rx.append(max(0.0, rx_bps))
|
||||||
|
self._tx.append(max(0.0, tx_bps))
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self._rx = deque([0.0] * self.POINTS, maxlen=self.POINTS)
|
||||||
|
self._tx = deque([0.0] * self.POINTS, maxlen=self.POINTS)
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def paintEvent(self, _):
|
||||||
|
p = QPainter(self)
|
||||||
|
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
|
w, h = self.width(), self.height()
|
||||||
|
pad_l, pad_r, pad_t, pad_b = 42, 8, 6, 20
|
||||||
|
|
||||||
|
# Fond
|
||||||
|
p.fillRect(0, 0, w, h, QColor("#17202a"))
|
||||||
|
|
||||||
|
plot_w = w - pad_l - pad_r
|
||||||
|
plot_h = h - pad_t - pad_b
|
||||||
|
|
||||||
|
max_val = max(max(self._rx), max(self._tx), 1.0)
|
||||||
|
|
||||||
|
def _path(data: deque, color: str, fill: str):
|
||||||
|
pts = list(data)
|
||||||
|
path = QPainterPath()
|
||||||
|
xs = [pad_l + i * plot_w / (len(pts) - 1) for i in range(len(pts))]
|
||||||
|
ys = [pad_t + plot_h - (v / max_val) * plot_h for v in pts]
|
||||||
|
path.moveTo(QPointF(xs[0], pad_t + plot_h))
|
||||||
|
path.lineTo(QPointF(xs[0], ys[0]))
|
||||||
|
for x, y in zip(xs[1:], ys[1:]):
|
||||||
|
path.lineTo(QPointF(x, y))
|
||||||
|
path.lineTo(QPointF(xs[-1], pad_t + plot_h))
|
||||||
|
path.closeSubpath()
|
||||||
|
p.fillPath(path, QBrush(QColor(fill)))
|
||||||
|
pen = QPen(QColor(color))
|
||||||
|
pen.setWidthF(1.5)
|
||||||
|
p.setPen(pen)
|
||||||
|
line = QPainterPath()
|
||||||
|
line.moveTo(QPointF(xs[0], ys[0]))
|
||||||
|
for x, y in zip(xs[1:], ys[1:]):
|
||||||
|
line.lineTo(QPointF(x, y))
|
||||||
|
p.drawPath(line)
|
||||||
|
|
||||||
|
_path(self._rx, "#5dade2", "#1a3a52") # RX bleu
|
||||||
|
_path(self._tx, "#58d68d", "#1a3d2b") # TX vert
|
||||||
|
|
||||||
|
# Axes
|
||||||
|
p.setPen(QPen(QColor("#2e4057")))
|
||||||
|
p.drawLine(pad_l, pad_t, pad_l, pad_t + plot_h)
|
||||||
|
p.drawLine(pad_l, pad_t + plot_h, w - pad_r, pad_t + plot_h)
|
||||||
|
|
||||||
|
# Labels Y
|
||||||
|
font = QFont("Arial", 8)
|
||||||
|
p.setFont(font)
|
||||||
|
p.setPen(QPen(QColor("#5d6d7e")))
|
||||||
|
for frac, label in [(0.0, _fmt(max_val)), (0.5, _fmt(max_val / 2)), (1.0, "0")]:
|
||||||
|
y = int(pad_t + frac * plot_h)
|
||||||
|
p.drawText(QRect(0, y - 8, pad_l - 2, 16),
|
||||||
|
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
|
||||||
|
label)
|
||||||
|
|
||||||
|
# Légende
|
||||||
|
p.setPen(QPen(QColor("#5dade2")))
|
||||||
|
p.drawText(pad_l + 4, pad_t + 12, f"↓ {_fmt(self._rx[-1])}")
|
||||||
|
p.setPen(QPen(QColor("#58d68d")))
|
||||||
|
p.drawText(pad_l + 90, pad_t + 12, f"↑ {_fmt(self._tx[-1])}")
|
||||||
|
|
||||||
|
p.end()
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
"""Fenêtre d'historique des sessions WGSecure."""
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QDialog, QVBoxLayout, QHBoxLayout, QTableWidget,
|
||||||
|
QTableWidgetItem, QPushButton, QLabel, QHeaderView,
|
||||||
|
QMessageBox,
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
from PyQt6.QtGui import QColor, QFont
|
||||||
|
|
||||||
|
from app.core import history as hist
|
||||||
|
|
||||||
|
|
||||||
|
_DARK = "#1c2833"
|
||||||
|
_DARK2 = "#17202a"
|
||||||
|
|
||||||
|
_CSS = f"""
|
||||||
|
QDialog {{ background: {_DARK2}; }}
|
||||||
|
QLabel {{ color: white; background: transparent; }}
|
||||||
|
QTableWidget {{
|
||||||
|
background: {_DARK}; color: white; gridline-color: #2e4057;
|
||||||
|
border: none; font-size: 11px;
|
||||||
|
}}
|
||||||
|
QHeaderView::section {{
|
||||||
|
background: #2e4057; color: white; padding: 5px;
|
||||||
|
border: none; font-weight: bold; font-size: 11px;
|
||||||
|
}}
|
||||||
|
QTableWidget::item:selected {{ background: #2471a3; }}
|
||||||
|
QPushButton {{
|
||||||
|
background: #2e4057; color: white; border-radius: 5px;
|
||||||
|
padding: 6px 14px; border: none;
|
||||||
|
}}
|
||||||
|
QPushButton:hover {{ background: #3d5166; }}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryDialog(QDialog):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("WGSecure — Historique des sessions")
|
||||||
|
self.setMinimumSize(680, 420)
|
||||||
|
self.setStyleSheet(_CSS)
|
||||||
|
self._build_ui()
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
|
||||||
|
# Bannière
|
||||||
|
banner = QLabel(" 📊 Historique des sessions VPN")
|
||||||
|
banner.setFixedHeight(40)
|
||||||
|
banner.setStyleSheet(
|
||||||
|
f"background: {_DARK2}; color: white; font-size: 14px; font-weight: bold;"
|
||||||
|
)
|
||||||
|
layout.addWidget(banner)
|
||||||
|
|
||||||
|
# Tableau
|
||||||
|
self._table = QTableWidget(0, 6)
|
||||||
|
self._table.setHorizontalHeaderLabels([
|
||||||
|
"Date début", "Serveur", "Profil", "Durée", "↓ Reçu", "↑ Envoyé"
|
||||||
|
])
|
||||||
|
self._table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||||
|
self._table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
self._table.verticalHeader().setVisible(False)
|
||||||
|
self._table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||||
|
self._table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||||
|
self._table.setAlternatingRowColors(True)
|
||||||
|
self._table.setStyleSheet(
|
||||||
|
_CSS +
|
||||||
|
"QTableWidget { alternate-background-color: #212f3d; }"
|
||||||
|
)
|
||||||
|
layout.addWidget(self._table)
|
||||||
|
|
||||||
|
# Barre infos + boutons
|
||||||
|
bar = QLabel("")
|
||||||
|
bar.setFixedHeight(1)
|
||||||
|
bar.setStyleSheet(f"background: #2e4057;")
|
||||||
|
layout.addWidget(bar)
|
||||||
|
|
||||||
|
btn_row_w = QVBoxLayout()
|
||||||
|
btn_row_w.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
bottom = QHBoxLayout()
|
||||||
|
bottom.setContentsMargins(14, 8, 14, 12)
|
||||||
|
|
||||||
|
self._summary_lbl = QLabel("")
|
||||||
|
self._summary_lbl.setStyleSheet("color: #5d6d7e; font-size: 11px;")
|
||||||
|
bottom.addWidget(self._summary_lbl)
|
||||||
|
bottom.addStretch()
|
||||||
|
|
||||||
|
btn_clear = QPushButton("🗑️ Effacer l'historique")
|
||||||
|
btn_clear.setStyleSheet(
|
||||||
|
"QPushButton { background: #6e2e1c; color: white; border-radius: 5px;"
|
||||||
|
" padding: 6px 14px; border: none; }"
|
||||||
|
"QPushButton:hover { background: #922b21; }"
|
||||||
|
)
|
||||||
|
btn_clear.clicked.connect(self._clear)
|
||||||
|
btn_close = QPushButton("Fermer")
|
||||||
|
btn_close.clicked.connect(self.accept)
|
||||||
|
bottom.addWidget(btn_clear)
|
||||||
|
bottom.addWidget(btn_close)
|
||||||
|
|
||||||
|
bottom_w = QWidget()
|
||||||
|
bottom_w.setStyleSheet(f"background: {_DARK2};")
|
||||||
|
bottom_w.setLayout(bottom)
|
||||||
|
layout.addWidget(bottom_w)
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
self._table.setRowCount(0)
|
||||||
|
sessions = hist.get_sessions(100)
|
||||||
|
total_rx = total_tx = total_s = 0
|
||||||
|
|
||||||
|
for row, s in enumerate(sessions):
|
||||||
|
self._table.insertRow(row)
|
||||||
|
end = s.get("end")
|
||||||
|
color = "#a9dfbf" if end else "#f9e79f" # vert=terminée, jaune=en cours
|
||||||
|
|
||||||
|
cells = [
|
||||||
|
s.get("start", "—"),
|
||||||
|
s.get("server", "—"),
|
||||||
|
s.get("profile", "Défaut"),
|
||||||
|
hist.fmt_duration(s.get("duration")),
|
||||||
|
hist.fmt_bytes(s.get("rx", 0)),
|
||||||
|
hist.fmt_bytes(s.get("tx", 0)),
|
||||||
|
]
|
||||||
|
for col, text in enumerate(cells):
|
||||||
|
item = QTableWidgetItem(text)
|
||||||
|
item.setForeground(QColor(color))
|
||||||
|
item.setTextAlignment(
|
||||||
|
Qt.AlignmentFlag.AlignVCenter |
|
||||||
|
(Qt.AlignmentFlag.AlignRight if col >= 3
|
||||||
|
else Qt.AlignmentFlag.AlignLeft)
|
||||||
|
)
|
||||||
|
self._table.setItem(row, col, item)
|
||||||
|
|
||||||
|
total_rx += s.get("rx", 0)
|
||||||
|
total_tx += s.get("tx", 0)
|
||||||
|
if s.get("duration"):
|
||||||
|
total_s += s["duration"]
|
||||||
|
|
||||||
|
n = len(sessions)
|
||||||
|
self._summary_lbl.setText(
|
||||||
|
f"{n} session{'s' if n > 1 else ''} • "
|
||||||
|
f"Total ↓ {hist.fmt_bytes(total_rx)} "
|
||||||
|
f"↑ {hist.fmt_bytes(total_tx)} • "
|
||||||
|
f"Durée cumulée : {hist.fmt_duration(total_s)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _clear(self):
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self, "Confirmer",
|
||||||
|
"Effacer tout l'historique des sessions ?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
|
)
|
||||||
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
|
hist.clear()
|
||||||
|
self._load()
|
||||||
+398
-117
@@ -1,26 +1,52 @@
|
|||||||
|
from __future__ import annotations
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
QLabel, QPushButton, QFrame, QMessageBox, QInputDialog,
|
QLabel, QPushButton, QFrame, QMessageBox, QInputDialog,
|
||||||
QLineEdit,
|
QLineEdit, QListWidget, QListWidgetItem, QApplication,
|
||||||
|
QComboBox, QSizePolicy,
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QTimer
|
from PyQt6.QtCore import Qt, QTimer, QDateTime
|
||||||
from PyQt6.QtGui import QFont, QCloseEvent, QPalette, QColor
|
from PyQt6.QtGui import QFont, QCloseEvent, QColor
|
||||||
|
|
||||||
from app.core.config import Config
|
from app.core.config import Config
|
||||||
from app.core import wireguard as wg_core
|
from app.core import wireguard as wg_core
|
||||||
from app.core import mfa as mfa_core
|
from app.core import log as conn_log
|
||||||
from app.ui.mfa_dialog import MFADialog
|
from app.ui.mfa_dialog import MFADialog
|
||||||
from app.ui.admin_window import AdminWindow
|
from app.ui.admin_window import AdminWindow
|
||||||
|
from app.ui.bw_graph import BandwidthGraph
|
||||||
from app.ui import icons
|
from app.ui import icons
|
||||||
|
|
||||||
|
_DARK = "#1c2833"
|
||||||
|
_DARK2 = "#17202a"
|
||||||
|
_CARD = "#212f3d"
|
||||||
|
|
||||||
|
_LOG_COLORS = {
|
||||||
|
"success": "#a9dfbf",
|
||||||
|
"error": "#f1948a",
|
||||||
|
"warning": "#f9e79f",
|
||||||
|
"info": "#aed6f1",
|
||||||
|
}
|
||||||
|
|
||||||
|
_PING_CSS = {
|
||||||
|
"good": "background:#1e8449; color:white;",
|
||||||
|
"medium": "background:#d4ac0d; color:black;",
|
||||||
|
"bad": "background:#922b21; color:white;",
|
||||||
|
"offline": "background:#2e4057; color:#5d6d7e;",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
def __init__(self, config: Config, parent=None):
|
def __init__(self, config: Config, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._cfg = config
|
self._cfg = config
|
||||||
self._connecting = False
|
self._connecting = False
|
||||||
|
self._connected_since: QDateTime | None = None
|
||||||
|
self._prev_rx: int | None = None
|
||||||
|
self._prev_tx: int | None = None
|
||||||
|
self._auto_reconnect_active = False
|
||||||
|
|
||||||
self.setWindowTitle("WGSecure")
|
self.setWindowTitle("WGSecure")
|
||||||
self.setFixedSize(400, 520)
|
self.setFixedSize(430, 680)
|
||||||
self.setWindowFlags(
|
self.setWindowFlags(
|
||||||
Qt.WindowType.Window
|
Qt.WindowType.Window
|
||||||
| Qt.WindowType.WindowTitleHint
|
| Qt.WindowType.WindowTitleHint
|
||||||
@@ -29,147 +55,364 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
self.setWindowIcon(icons.icon_app())
|
self.setWindowIcon(icons.icon_app())
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
|
|
||||||
|
# Timers
|
||||||
self._status_timer = QTimer(self)
|
self._status_timer = QTimer(self)
|
||||||
self._status_timer.timeout.connect(self._refresh_status)
|
self._status_timer.timeout.connect(self._refresh_status)
|
||||||
self._status_timer.start(3000)
|
self._status_timer.start(3000)
|
||||||
|
|
||||||
|
self._clock_timer = QTimer(self)
|
||||||
|
self._clock_timer.timeout.connect(self._tick_duration)
|
||||||
|
self._clock_timer.start(1000)
|
||||||
|
|
||||||
|
self._bw_timer = QTimer(self)
|
||||||
|
self._bw_timer.timeout.connect(self._update_bw)
|
||||||
|
self._bw_timer.start(3000)
|
||||||
|
|
||||||
|
self._ping_timer = QTimer(self)
|
||||||
|
self._ping_timer.timeout.connect(self._update_ping)
|
||||||
|
self._ping_timer.start(10000)
|
||||||
|
|
||||||
|
self._reconnect_timer = QTimer(self)
|
||||||
|
self._reconnect_timer.timeout.connect(self._check_reconnect)
|
||||||
|
|
||||||
self._refresh_status()
|
self._refresh_status()
|
||||||
|
self._reload_log()
|
||||||
|
self._reload_profiles()
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Construction UI
|
# Construction UI
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
def _build_ui(self):
|
def _build_ui(self):
|
||||||
central = QWidget()
|
central = QWidget()
|
||||||
|
central.setStyleSheet(f"background: {_DARK};")
|
||||||
self.setCentralWidget(central)
|
self.setCentralWidget(central)
|
||||||
layout = QVBoxLayout(central)
|
root = QVBoxLayout(central)
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
root.setContentsMargins(0, 0, 0, 0)
|
||||||
layout.setSpacing(0)
|
root.setSpacing(0)
|
||||||
|
|
||||||
# -- En-tête --------------------------------------------------
|
# ── En-tête ──────────────────────────────────────────────────
|
||||||
header = QWidget()
|
header = QWidget()
|
||||||
header.setFixedHeight(72)
|
header.setFixedHeight(72)
|
||||||
header.setStyleSheet("background: #2c3e50;")
|
header.setStyleSheet(f"background: {_DARK2};")
|
||||||
h_layout = QHBoxLayout(header)
|
h = QHBoxLayout(header)
|
||||||
h_layout.setContentsMargins(16, 10, 16, 10)
|
h.setContentsMargins(14, 8, 14, 8)
|
||||||
|
|
||||||
self._icon_label = QLabel()
|
self._icon_label = QLabel()
|
||||||
self._icon_label.setFixedSize(44, 44)
|
self._icon_label.setFixedSize(40, 40)
|
||||||
self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
h_layout.addWidget(self._icon_label)
|
h.addWidget(self._icon_label)
|
||||||
|
|
||||||
title_col = QVBoxLayout()
|
title_col = QVBoxLayout()
|
||||||
title_col.setSpacing(2)
|
title_col.setSpacing(1)
|
||||||
app_title = QLabel("WGSecure")
|
title_lbl = QLabel("WGSecure")
|
||||||
app_title.setStyleSheet("color: white; font-size: 17px; font-weight: bold; background: transparent;")
|
title_lbl.setStyleSheet(
|
||||||
version_label = QLabel("v0.1.0 — WireGuard + MFA")
|
"color: white; font-size: 16px; font-weight: bold; background: transparent;"
|
||||||
version_label.setStyleSheet("color: #95a5a6; font-size: 10px; background: transparent;")
|
)
|
||||||
title_col.addWidget(app_title)
|
self._profile_combo = QComboBox()
|
||||||
title_col.addWidget(version_label)
|
self._profile_combo.setFixedHeight(22)
|
||||||
h_layout.addLayout(title_col)
|
self._profile_combo.setStyleSheet(
|
||||||
h_layout.addStretch()
|
"QComboBox { background: #2e4057; color: #aed6f1; border: none;"
|
||||||
layout.addWidget(header)
|
" border-radius: 3px; font-size: 10px; padding: 0 6px; }"
|
||||||
|
"QComboBox::drop-down { border: none; }"
|
||||||
|
"QComboBox QAbstractItemView { background: #2e4057; color: white; }"
|
||||||
|
)
|
||||||
|
self._profile_combo.currentTextChanged.connect(self._on_profile_changed)
|
||||||
|
title_col.addWidget(title_lbl)
|
||||||
|
title_col.addWidget(self._profile_combo)
|
||||||
|
h.addLayout(title_col)
|
||||||
|
h.addStretch()
|
||||||
|
|
||||||
# -- Zone statut ----------------------------------------------
|
# Ping badge
|
||||||
|
self._ping_badge = QLabel("— ms")
|
||||||
|
self._ping_badge.setFixedSize(62, 22)
|
||||||
|
self._ping_badge.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self._ping_badge.setStyleSheet(
|
||||||
|
f"border-radius: 11px; font-size: 10px; font-weight: bold; {_PING_CSS['offline']}"
|
||||||
|
)
|
||||||
|
h.addWidget(self._ping_badge)
|
||||||
|
|
||||||
|
# Copier clé publique
|
||||||
|
self._btn_copy_key = QPushButton("📋 Clé pub.")
|
||||||
|
self._btn_copy_key.setFixedHeight(26)
|
||||||
|
self._btn_copy_key.setStyleSheet(
|
||||||
|
"QPushButton { background: #2471a3; color: white; border-radius: 5px;"
|
||||||
|
" font-size: 10px; padding: 0 8px; border: none; }"
|
||||||
|
"QPushButton:hover { background: #1a5276; }"
|
||||||
|
"QPushButton:disabled { background: #2e4057; color: #5d6d7e; }"
|
||||||
|
)
|
||||||
|
self._btn_copy_key.clicked.connect(self._copy_public_key)
|
||||||
|
h.addWidget(self._btn_copy_key)
|
||||||
|
root.addWidget(header)
|
||||||
|
|
||||||
|
# ── Statut ───────────────────────────────────────────────────
|
||||||
status_area = QWidget()
|
status_area = QWidget()
|
||||||
status_area.setFixedHeight(90)
|
status_area.setFixedHeight(80)
|
||||||
status_area.setStyleSheet("background: #ecf0f1;")
|
status_area.setStyleSheet(f"background: {_CARD};")
|
||||||
s_layout = QVBoxLayout(status_area)
|
s = QVBoxLayout(status_area)
|
||||||
s_layout.setContentsMargins(10, 10, 10, 10)
|
s.setContentsMargins(10, 6, 10, 6)
|
||||||
s_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
s.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
self._status_badge = QLabel("● Déconnecté")
|
self._status_badge = QLabel("● Déconnecté")
|
||||||
self._status_badge.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self._status_badge.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self._status_badge.setStyleSheet("color: #e74c3c; font-size: 16px; font-weight: bold; background: transparent;")
|
self._status_badge.setStyleSheet(
|
||||||
s_layout.addWidget(self._status_badge)
|
"color: #e74c3c; font-size: 15px; font-weight: bold; background: transparent;"
|
||||||
|
)
|
||||||
|
s.addWidget(self._status_badge)
|
||||||
|
|
||||||
self._status_sub = QLabel("")
|
self._duration_label = QLabel("")
|
||||||
self._status_sub.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self._duration_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self._status_sub.setStyleSheet("color: #7f8c8d; font-size: 11px; background: transparent;")
|
self._duration_label.setStyleSheet(
|
||||||
s_layout.addWidget(self._status_sub)
|
"color: #5d6d7e; font-size: 11px; background: transparent;"
|
||||||
layout.addWidget(status_area)
|
)
|
||||||
|
s.addWidget(self._duration_label)
|
||||||
|
root.addWidget(status_area)
|
||||||
|
|
||||||
# -- Séparateur -----------------------------------------------
|
root.addWidget(self._hsep())
|
||||||
sep = QFrame()
|
|
||||||
sep.setFrameShape(QFrame.Shape.HLine)
|
|
||||||
sep.setStyleSheet("color: #bdc3c7;")
|
|
||||||
layout.addWidget(sep)
|
|
||||||
|
|
||||||
# -- Section infos --------------------------------------------
|
# ── Infos ─────────────────────────────────────────────────────
|
||||||
info_frame = QFrame()
|
info_frame = QFrame()
|
||||||
info_frame.setFrameShape(QFrame.Shape.NoFrame)
|
info_frame.setStyleSheet(f"background: {_DARK};")
|
||||||
info_frame.setStyleSheet("background-color: #ffffff;")
|
|
||||||
info_frame.setAutoFillBackground(True)
|
info_frame.setAutoFillBackground(True)
|
||||||
i_layout = QVBoxLayout(info_frame)
|
il = QVBoxLayout(info_frame)
|
||||||
i_layout.setContentsMargins(16, 12, 16, 12)
|
il.setContentsMargins(14, 8, 14, 8)
|
||||||
i_layout.setSpacing(8)
|
il.setSpacing(5)
|
||||||
|
|
||||||
self._info_labels: dict[str, QLabel] = {}
|
self._info_labels: dict[str, QLabel] = {}
|
||||||
rows = [
|
for key, icon_txt, label in [
|
||||||
("server", "Serveur"),
|
("server", "🌐", "Serveur"),
|
||||||
("iface", "Interface"),
|
("iface", "🔌", "Interface"),
|
||||||
("addr", "Adresse IP"),
|
("addr", "📍", "Adresse IP"),
|
||||||
("mfa", "MFA"),
|
("mfa", "🔐", "MFA"),
|
||||||
("rx_tx", "Transfert"),
|
("rx_tx", "↕️ ", "Transfert"),
|
||||||
("handshake", "Dernier handshake"),
|
("handshake", "🤝", "Handshake"),
|
||||||
]
|
]:
|
||||||
for key, label in rows:
|
rw = QWidget()
|
||||||
row_w = QWidget()
|
rw.setStyleSheet("background: transparent;")
|
||||||
row_w.setStyleSheet("background: transparent;")
|
rl = QHBoxLayout(rw)
|
||||||
row_layout = QHBoxLayout(row_w)
|
rl.setContentsMargins(0, 0, 0, 0)
|
||||||
row_layout.setContentsMargins(0, 0, 0, 0)
|
rl.setSpacing(5)
|
||||||
row_layout.setSpacing(8)
|
rl.addWidget(self._lbl(icon_txt, fixed=20, color="#5d6d7e", size=11))
|
||||||
|
rl.addWidget(self._lbl(f"{label} :", fixed=100, color="#5d6d7e", size=12))
|
||||||
|
val = self._lbl("—", color="#aed6f1", size=12)
|
||||||
|
val.setWordWrap(True)
|
||||||
|
self._info_labels[key] = val
|
||||||
|
rl.addWidget(val, 1)
|
||||||
|
il.addWidget(rw)
|
||||||
|
|
||||||
lbl_key = QLabel(f"{label} :")
|
root.addWidget(info_frame)
|
||||||
lbl_key.setFixedWidth(130)
|
root.addWidget(self._hsep())
|
||||||
lbl_key.setStyleSheet("color: #7f8c8d; font-size: 12px; background: transparent;")
|
|
||||||
|
|
||||||
lbl_val = QLabel("—")
|
# ── Graphique bande passante ───────────────────────────────────
|
||||||
lbl_val.setStyleSheet("color: #2c3e50; font-size: 12px; background: transparent;")
|
bw_header = self._section_header("📊 Bande passante")
|
||||||
lbl_val.setWordWrap(True)
|
root.addWidget(bw_header)
|
||||||
self._info_labels[key] = lbl_val
|
|
||||||
|
|
||||||
row_layout.addWidget(lbl_key)
|
self._bw_graph = BandwidthGraph()
|
||||||
row_layout.addWidget(lbl_val, 1)
|
self._bw_graph.setFixedHeight(75)
|
||||||
i_layout.addWidget(row_w)
|
root.addWidget(self._bw_graph)
|
||||||
|
root.addWidget(self._hsep())
|
||||||
|
|
||||||
layout.addWidget(info_frame, 1) # stretch factor 1 — prend l'espace disponible
|
# ── Journal ───────────────────────────────────────────────────
|
||||||
|
log_hdr = self._section_header("📋 Journal", btn_text="Effacer",
|
||||||
|
btn_slot=self._clear_log)
|
||||||
|
root.addWidget(log_hdr)
|
||||||
|
|
||||||
# -- Séparateur bas -------------------------------------------
|
self._log_list = QListWidget()
|
||||||
sep2 = QFrame()
|
self._log_list.setFixedHeight(78)
|
||||||
sep2.setFrameShape(QFrame.Shape.HLine)
|
self._log_list.setStyleSheet(
|
||||||
sep2.setStyleSheet("color: #bdc3c7;")
|
f"QListWidget {{ background: {_DARK2}; color: #aed6f1;"
|
||||||
layout.addWidget(sep2)
|
" border: none; font-size: 10px; font-family: Courier; }}"
|
||||||
|
"QListWidget::item { padding: 1px 6px; }"
|
||||||
|
)
|
||||||
|
self._log_list.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||||
|
root.addWidget(self._log_list)
|
||||||
|
root.addWidget(self._hsep())
|
||||||
|
|
||||||
# -- Boutons --------------------------------------------------
|
# ── Boutons ───────────────────────────────────────────────────
|
||||||
btn_area = QWidget()
|
btn_area = QWidget()
|
||||||
btn_area.setStyleSheet("background: #f8f9fa;")
|
btn_area.setStyleSheet(f"background: {_DARK2};")
|
||||||
b_layout = QVBoxLayout(btn_area)
|
bl = QVBoxLayout(btn_area)
|
||||||
b_layout.setContentsMargins(16, 12, 16, 12)
|
bl.setContentsMargins(14, 10, 14, 12)
|
||||||
b_layout.setSpacing(8)
|
bl.setSpacing(7)
|
||||||
|
|
||||||
self._btn_connect = QPushButton("Se connecter")
|
self._btn_connect = QPushButton("Se connecter")
|
||||||
self._btn_connect.setFixedHeight(42)
|
self._btn_connect.setFixedHeight(42)
|
||||||
self._btn_connect.setStyleSheet(
|
self._btn_connect.setStyleSheet(
|
||||||
"QPushButton { background: #27ae60; color: white; font-size: 14px; "
|
"QPushButton { background: #1e8449; color: white; font-size: 14px;"
|
||||||
"font-weight: bold; border-radius: 6px; border: none; }"
|
" font-weight: bold; border-radius: 6px; border: none; }"
|
||||||
"QPushButton:hover { background: #219a52; }"
|
"QPushButton:hover { background: #27ae60; }"
|
||||||
"QPushButton:disabled { background: #bdc3c7; color: #ecf0f1; }"
|
"QPushButton:disabled { background: #2e4057; color: #5d6d7e; }"
|
||||||
)
|
)
|
||||||
self._btn_connect.clicked.connect(self._on_connect)
|
self._btn_connect.clicked.connect(self._on_connect)
|
||||||
b_layout.addWidget(self._btn_connect)
|
bl.addWidget(self._btn_connect)
|
||||||
|
|
||||||
btn_admin = QPushButton("Panneau Administrateur")
|
btn_admin = QPushButton("⚙️ Panneau Administrateur")
|
||||||
btn_admin.setFixedHeight(34)
|
btn_admin.setFixedHeight(30)
|
||||||
btn_admin.setStyleSheet(
|
btn_admin.setStyleSheet(
|
||||||
"QPushButton { background: #dde1e7; color: #2c3e50; border-radius: 6px; "
|
"QPushButton { background: #2e4057; color: white; border-radius: 6px;"
|
||||||
"font-size: 12px; border: none; }"
|
" font-size: 11px; border: none; }"
|
||||||
"QPushButton:hover { background: #bdc3c7; color: #2c3e50; }"
|
"QPushButton:hover { background: #3d5166; }"
|
||||||
)
|
)
|
||||||
btn_admin.clicked.connect(self._open_admin)
|
btn_admin.clicked.connect(self._open_admin)
|
||||||
b_layout.addWidget(btn_admin)
|
bl.addWidget(btn_admin)
|
||||||
layout.addWidget(btn_area)
|
root.addWidget(btn_area)
|
||||||
|
|
||||||
|
# ── Helpers UI ───────────────────────────────────────────────────────
|
||||||
|
def _lbl(self, text: str, fixed: int = 0, color: str = "white",
|
||||||
|
size: int = 12) -> QLabel:
|
||||||
|
lbl = QLabel(text)
|
||||||
|
lbl.setStyleSheet(
|
||||||
|
f"color: {color}; font-size: {size}px; background: transparent;"
|
||||||
|
)
|
||||||
|
if fixed:
|
||||||
|
lbl.setFixedWidth(fixed)
|
||||||
|
return lbl
|
||||||
|
|
||||||
|
def _hsep(self) -> QFrame:
|
||||||
|
f = QFrame()
|
||||||
|
f.setFrameShape(QFrame.Shape.HLine)
|
||||||
|
f.setFixedHeight(1)
|
||||||
|
f.setStyleSheet(f"background: {_DARK2};")
|
||||||
|
return f
|
||||||
|
|
||||||
|
def _section_header(self, title: str, btn_text: str = "",
|
||||||
|
btn_slot=None) -> QWidget:
|
||||||
|
w = QWidget()
|
||||||
|
w.setFixedHeight(24)
|
||||||
|
w.setStyleSheet(f"background: {_DARK2};")
|
||||||
|
hl = QHBoxLayout(w)
|
||||||
|
hl.setContentsMargins(12, 0, 12, 0)
|
||||||
|
t = QLabel(title)
|
||||||
|
t.setStyleSheet("color: #5d6d7e; font-size: 10px; font-weight: bold;"
|
||||||
|
" background: transparent;")
|
||||||
|
hl.addWidget(t)
|
||||||
|
hl.addStretch()
|
||||||
|
if btn_text and btn_slot:
|
||||||
|
b = QPushButton(btn_text)
|
||||||
|
b.setFixedHeight(18)
|
||||||
|
b.setStyleSheet(
|
||||||
|
"QPushButton { background: transparent; color: #5d6d7e;"
|
||||||
|
" font-size: 9px; border: none; padding: 0 4px; }"
|
||||||
|
"QPushButton:hover { color: white; }"
|
||||||
|
)
|
||||||
|
b.clicked.connect(btn_slot)
|
||||||
|
hl.addWidget(b)
|
||||||
|
return w
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Rafraîchissement du statut
|
# Profils
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def _reload_profiles(self):
|
||||||
|
self._profile_combo.blockSignals(True)
|
||||||
|
self._profile_combo.clear()
|
||||||
|
self._profile_combo.addItem("⭐ Défaut (actif)")
|
||||||
|
for name in self._cfg.list_profiles():
|
||||||
|
marker = "✓ " if name == self._cfg.active_profile else " "
|
||||||
|
self._profile_combo.addItem(f"{marker}{name}")
|
||||||
|
self._profile_combo.blockSignals(False)
|
||||||
|
|
||||||
|
def _on_profile_changed(self, text: str):
|
||||||
|
if "Défaut" in text:
|
||||||
|
return
|
||||||
|
name = text.lstrip("✓ ").strip()
|
||||||
|
if not name or name == self._cfg.active_profile:
|
||||||
|
return
|
||||||
|
if self._cfg.load_profile(name):
|
||||||
|
self._add_log(f"Profil chargé : {name}", "info")
|
||||||
|
self._reload_profiles()
|
||||||
|
self._refresh_status()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Journal
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def _reload_log(self):
|
||||||
|
self._log_list.clear()
|
||||||
|
for ev in conn_log.get_events(30):
|
||||||
|
self._add_log_item(ev["ts"], ev["msg"], ev["kind"])
|
||||||
|
self._log_list.scrollToBottom()
|
||||||
|
|
||||||
|
def _clear_log(self):
|
||||||
|
conn_log.clear()
|
||||||
|
self._log_list.clear()
|
||||||
|
|
||||||
|
def _add_log(self, msg: str, kind: str = "info"):
|
||||||
|
conn_log.log_event(msg, kind)
|
||||||
|
ts = QDateTime.currentDateTime().toString("yyyy-MM-dd hh:mm:ss")
|
||||||
|
self._add_log_item(ts, msg, kind)
|
||||||
|
|
||||||
|
def _add_log_item(self, ts: str, msg: str, kind: str):
|
||||||
|
color = _LOG_COLORS.get(kind, "#aed6f1")
|
||||||
|
item = QListWidgetItem(f"[{ts}] {msg}")
|
||||||
|
item.setForeground(QColor(color))
|
||||||
|
self._log_list.addItem(item)
|
||||||
|
self._log_list.scrollToBottom()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Timers
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def _tick_duration(self):
|
||||||
|
if self._connected_since is None:
|
||||||
|
return
|
||||||
|
secs = self._connected_since.secsTo(QDateTime.currentDateTime())
|
||||||
|
h, rem = divmod(secs, 3600)
|
||||||
|
m, s = divmod(rem, 60)
|
||||||
|
self._duration_label.setText(f"Connecté depuis {h:02d}h {m:02d}m {s:02d}s")
|
||||||
|
|
||||||
|
def _update_bw(self):
|
||||||
|
iface = self._cfg.wg.get("interface_name", "wgs0")
|
||||||
|
result = wg_core.get_interface_bytes(iface)
|
||||||
|
if result is None or self._prev_rx is None:
|
||||||
|
self._prev_rx, self._prev_tx = (result or (0, 0))
|
||||||
|
return
|
||||||
|
rx_now, tx_now = result
|
||||||
|
dt = 3.0
|
||||||
|
rx_bps = max(0, rx_now - self._prev_rx) / dt
|
||||||
|
tx_bps = max(0, tx_now - self._prev_tx) / dt
|
||||||
|
self._prev_rx, self._prev_tx = rx_now, tx_now
|
||||||
|
if wg_core.is_connected(self._cfg):
|
||||||
|
self._bw_graph.push(rx_bps, tx_bps)
|
||||||
|
else:
|
||||||
|
self._bw_graph.push(0, 0)
|
||||||
|
|
||||||
|
def _update_ping(self):
|
||||||
|
host = self._cfg.wg.get("server_endpoint", "")
|
||||||
|
if not host or not wg_core.is_connected(self._cfg):
|
||||||
|
self._ping_badge.setText("— ms")
|
||||||
|
self._ping_badge.setStyleSheet(
|
||||||
|
f"border-radius: 11px; font-size: 10px; font-weight: bold; {_PING_CSS['offline']}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
ms = wg_core.ping_server(host)
|
||||||
|
if ms is None:
|
||||||
|
css = _PING_CSS["bad"]
|
||||||
|
txt = "hors ligne"
|
||||||
|
elif ms < 50:
|
||||||
|
css = _PING_CSS["good"]
|
||||||
|
txt = f"{ms} ms"
|
||||||
|
elif ms < 200:
|
||||||
|
css = _PING_CSS["medium"]
|
||||||
|
txt = f"{ms} ms"
|
||||||
|
else:
|
||||||
|
css = _PING_CSS["bad"]
|
||||||
|
txt = f"{ms} ms"
|
||||||
|
self._ping_badge.setText(txt)
|
||||||
|
self._ping_badge.setStyleSheet(
|
||||||
|
f"border-radius: 11px; font-size: 10px; font-weight: bold; {css}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_reconnect(self):
|
||||||
|
if not self._cfg.get("ui", "auto_reconnect"):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
connected = wg_core.is_connected(self._cfg)
|
||||||
|
except Exception:
|
||||||
|
connected = False
|
||||||
|
if not connected and self._cfg.configured:
|
||||||
|
self._add_log("Auto-reconnexion…", "warning")
|
||||||
|
self._on_connect()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Statut WireGuard
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
def _refresh_status(self):
|
def _refresh_status(self):
|
||||||
wg = self._cfg.wg
|
wg = self._cfg.wg
|
||||||
@@ -181,6 +424,15 @@ class MainWindow(QMainWindow):
|
|||||||
self._info_labels["mfa"].setText(
|
self._info_labels["mfa"].setText(
|
||||||
"Activé ✓" if self._cfg.mfa_enabled else "Désactivé"
|
"Activé ✓" if self._cfg.mfa_enabled else "Désactivé"
|
||||||
)
|
)
|
||||||
|
self._btn_copy_key.setEnabled(bool(wg.get("client_public_key", "")))
|
||||||
|
|
||||||
|
# Auto-reconnect timer
|
||||||
|
if self._cfg.get("ui", "auto_reconnect"):
|
||||||
|
interval = int(self._cfg.get("ui", "reconnect_interval") or 30) * 1000
|
||||||
|
if not self._reconnect_timer.isActive():
|
||||||
|
self._reconnect_timer.start(interval)
|
||||||
|
else:
|
||||||
|
self._reconnect_timer.stop()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
info = wg_core.get_status_info(self._cfg)
|
info = wg_core.get_status_info(self._cfg)
|
||||||
@@ -190,36 +442,47 @@ class MainWindow(QMainWindow):
|
|||||||
info = {}
|
info = {}
|
||||||
|
|
||||||
if connected:
|
if connected:
|
||||||
self._status_badge.setText("● Connecté")
|
if self._connected_since is None:
|
||||||
|
self._connected_since = QDateTime.currentDateTime()
|
||||||
|
self._status_badge.setText("● Connecté")
|
||||||
self._status_badge.setStyleSheet(
|
self._status_badge.setStyleSheet(
|
||||||
"color: #27ae60; font-size: 16px; font-weight: bold; background: transparent;"
|
"color: #27ae60; font-size: 15px; font-weight: bold; background: transparent;"
|
||||||
)
|
)
|
||||||
self._btn_connect.setText("Se déconnecter")
|
self._btn_connect.setText("Se déconnecter")
|
||||||
self._btn_connect.setStyleSheet(
|
self._btn_connect.setStyleSheet(
|
||||||
"QPushButton { background: #e74c3c; color: white; font-size: 14px; "
|
"QPushButton { background: #922b21; color: white; font-size: 14px;"
|
||||||
"font-weight: bold; border-radius: 6px; border: none; }"
|
" font-weight: bold; border-radius: 6px; border: none; }"
|
||||||
"QPushButton:hover { background: #c0392b; }"
|
"QPushButton:hover { background: #c0392b; }"
|
||||||
)
|
)
|
||||||
self._info_labels["rx_tx"].setText(
|
self._info_labels["rx_tx"].setText(
|
||||||
f"↓ {info.get('rx_bytes','—')} ↑ {info.get('tx_bytes','—')}"
|
f"↓ {info.get('rx_bytes','—')} ↑ {info.get('tx_bytes','—')}"
|
||||||
)
|
)
|
||||||
self._info_labels["handshake"].setText(info.get("last_handshake", "—"))
|
self._info_labels["handshake"].setText(info.get("last_handshake", "—"))
|
||||||
self._status_sub.setText(f"Interface active : {info.get('interface','—')}")
|
self._duration_label.setStyleSheet(
|
||||||
|
"color: #a9dfbf; font-size: 11px; background: transparent;"
|
||||||
|
)
|
||||||
self.setWindowIcon(icons.icon_connected())
|
self.setWindowIcon(icons.icon_connected())
|
||||||
else:
|
else:
|
||||||
self._status_badge.setText("● Déconnecté")
|
if self._connected_since is not None:
|
||||||
|
self._connected_since = None
|
||||||
|
self._duration_label.setText("")
|
||||||
|
self._bw_graph.reset()
|
||||||
|
self._prev_rx = self._prev_tx = None
|
||||||
|
self._status_badge.setText("● Déconnecté")
|
||||||
self._status_badge.setStyleSheet(
|
self._status_badge.setStyleSheet(
|
||||||
"color: #e74c3c; font-size: 16px; font-weight: bold; background: transparent;"
|
"color: #e74c3c; font-size: 15px; font-weight: bold; background: transparent;"
|
||||||
)
|
)
|
||||||
self._btn_connect.setText("Se connecter")
|
self._btn_connect.setText("Se connecter")
|
||||||
self._btn_connect.setStyleSheet(
|
self._btn_connect.setStyleSheet(
|
||||||
"QPushButton { background: #27ae60; color: white; font-size: 14px; "
|
"QPushButton { background: #1e8449; color: white; font-size: 14px;"
|
||||||
"font-weight: bold; border-radius: 6px; border: none; }"
|
" font-weight: bold; border-radius: 6px; border: none; }"
|
||||||
"QPushButton:hover { background: #219a52; }"
|
"QPushButton:hover { background: #27ae60; }"
|
||||||
)
|
)
|
||||||
self._info_labels["rx_tx"].setText("—")
|
self._info_labels["rx_tx"].setText("—")
|
||||||
self._info_labels["handshake"].setText("—")
|
self._info_labels["handshake"].setText("—")
|
||||||
self._status_sub.setText("")
|
self._duration_label.setStyleSheet(
|
||||||
|
"color: #5d6d7e; font-size: 11px; background: transparent;"
|
||||||
|
)
|
||||||
self.setWindowIcon(icons.icon_disconnected())
|
self.setWindowIcon(icons.icon_disconnected())
|
||||||
|
|
||||||
pix = (icons.icon_connected() if connected else icons.icon_disconnected()).pixmap(36, 36)
|
pix = (icons.icon_connected() if connected else icons.icon_disconnected()).pixmap(36, 36)
|
||||||
@@ -228,6 +491,12 @@ class MainWindow(QMainWindow):
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Actions
|
# Actions
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
def _copy_public_key(self):
|
||||||
|
pub = self._cfg.wg.get("client_public_key", "")
|
||||||
|
if pub:
|
||||||
|
QApplication.clipboard().setText(pub)
|
||||||
|
self._add_log("Clé publique copiée dans le presse-papiers", "info")
|
||||||
|
|
||||||
def _on_connect(self):
|
def _on_connect(self):
|
||||||
if self._connecting:
|
if self._connecting:
|
||||||
return
|
return
|
||||||
@@ -238,37 +507,48 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
if connected:
|
if connected:
|
||||||
ok, msg = wg_core.disconnect(self._cfg)
|
ok, msg = wg_core.disconnect(self._cfg)
|
||||||
if not ok:
|
if ok:
|
||||||
QMessageBox.warning(self, "Erreur", f"Déconnexion échouée :\n{msg}")
|
self._add_log("Tunnel WireGuard désactivé", "warning")
|
||||||
|
else:
|
||||||
|
self._add_log(f"Erreur déconnexion : {msg}", "error")
|
||||||
|
QMessageBox.warning(self, "Erreur", msg)
|
||||||
else:
|
else:
|
||||||
if not self._cfg.configured:
|
if not self._cfg.configured:
|
||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
self, "Configuration manquante",
|
self, "Configuration manquante",
|
||||||
"WireGuard n'est pas encore configuré.\n"
|
"WireGuard n'est pas encore configuré.\n"
|
||||||
"Ouvrez le panneau Administrateur pour paramétrer la connexion."
|
"Ouvrez le panneau Administrateur."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._cfg.mfa_enabled:
|
if self._cfg.mfa_enabled:
|
||||||
if not self._cfg.mfa_secret:
|
if not self._cfg.mfa_secret:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(self, "MFA non configuré",
|
||||||
self, "MFA non configuré",
|
"MFA activé mais aucun secret défini.")
|
||||||
"Le MFA est activé mais aucun secret n'est configuré."
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
dlg = MFADialog(self._cfg.mfa_secret, self)
|
dlg = MFADialog(self._cfg.mfa_secret, self)
|
||||||
if dlg.exec() != MFADialog.DialogCode.Accepted or not dlg.is_verified():
|
if dlg.exec() != MFADialog.DialogCode.Accepted or not dlg.is_verified():
|
||||||
|
self._add_log("MFA annulé ou échoué", "warning")
|
||||||
return
|
return
|
||||||
|
self._add_log("Authentification MFA réussie ✓", "success")
|
||||||
|
|
||||||
self._btn_connect.setEnabled(False)
|
self._btn_connect.setEnabled(False)
|
||||||
self._btn_connect.setText("Connexion…")
|
self._btn_connect.setText("Connexion…")
|
||||||
self._connecting = True
|
self._connecting = True
|
||||||
from PyQt6.QtWidgets import QApplication
|
|
||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
|
|
||||||
ok, msg = wg_core.connect(self._cfg)
|
ok, msg = wg_core.connect(self._cfg)
|
||||||
self._connecting = False
|
self._connecting = False
|
||||||
self._btn_connect.setEnabled(True)
|
self._btn_connect.setEnabled(True)
|
||||||
if not ok:
|
|
||||||
|
if ok:
|
||||||
|
self._connected_since = QDateTime.currentDateTime()
|
||||||
|
self._add_log(
|
||||||
|
f"Connexion établie → {self._cfg.wg.get('server_endpoint','')}", "success"
|
||||||
|
)
|
||||||
|
QTimer.singleShot(5000, self._update_ping)
|
||||||
|
else:
|
||||||
|
self._add_log(f"Erreur : {msg}", "error")
|
||||||
QMessageBox.warning(self, "Erreur de connexion", msg)
|
QMessageBox.warning(self, "Erreur de connexion", msg)
|
||||||
|
|
||||||
self._refresh_status()
|
self._refresh_status()
|
||||||
@@ -287,6 +567,7 @@ class MainWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
dlg = AdminWindow(self._cfg, self)
|
dlg = AdminWindow(self._cfg, self)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
|
self._reload_profiles()
|
||||||
self._refresh_status()
|
self._refresh_status()
|
||||||
|
|
||||||
def closeEvent(self, event: QCloseEvent):
|
def closeEvent(self, event: QCloseEvent):
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class SystemTray(QSystemTrayIcon):
|
|||||||
self._win.activateWindow()
|
self._win.activateWindow()
|
||||||
|
|
||||||
def _update_status(self):
|
def _update_status(self):
|
||||||
|
prev = self._connected
|
||||||
try:
|
try:
|
||||||
self._connected = wg_core.is_connected(self._cfg)
|
self._connected = wg_core.is_connected(self._cfg)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -81,12 +82,25 @@ class SystemTray(QSystemTrayIcon):
|
|||||||
self._action_status.setText("● Connecté")
|
self._action_status.setText("● Connecté")
|
||||||
self._action_toggle.setIcon(icons.icon_connected())
|
self._action_toggle.setIcon(icons.icon_connected())
|
||||||
self._action_toggle.setText("Se déconnecter")
|
self._action_toggle.setText("Se déconnecter")
|
||||||
|
if not prev:
|
||||||
|
srv = self._cfg.wg.get("server_endpoint", "")
|
||||||
|
self.showMessage(
|
||||||
|
"WGSecure — Connecté ✓",
|
||||||
|
f"Tunnel WireGuard actif{(' → ' + srv) if srv else ''}",
|
||||||
|
QSystemTrayIcon.MessageIcon.Information, 3000,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.setIcon(icons.icon_disconnected())
|
self.setIcon(icons.icon_disconnected())
|
||||||
self.setToolTip("WGSecure — Déconnecté")
|
self.setToolTip("WGSecure — Déconnecté")
|
||||||
self._action_status.setText("● Déconnecté")
|
self._action_status.setText("● Déconnecté")
|
||||||
self._action_toggle.setIcon(icons.icon_disconnected())
|
self._action_toggle.setIcon(icons.icon_disconnected())
|
||||||
self._action_toggle.setText("Se connecter")
|
self._action_toggle.setText("Se connecter")
|
||||||
|
if prev:
|
||||||
|
self.showMessage(
|
||||||
|
"WGSecure — Déconnecté",
|
||||||
|
"Le tunnel WireGuard a été désactivé.",
|
||||||
|
QSystemTrayIcon.MessageIcon.Warning, 3000,
|
||||||
|
)
|
||||||
|
|
||||||
def _toggle_connection(self):
|
def _toggle_connection(self):
|
||||||
self._win._on_connect()
|
self._win._on_connect()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
WGSecure (WGS) — Interface graphique WireGuard avec MFA
|
WGSecure (WGS) — Interface graphique WireGuard avec MFA
|
||||||
Version 0.1.0
|
Version 0.4.0
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
@@ -72,7 +72,7 @@ def main():
|
|||||||
)
|
)
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
app.setApplicationName("WGSecure")
|
app.setApplicationName("WGSecure")
|
||||||
app.setApplicationVersion("0.1.0")
|
app.setApplicationVersion("0.4.0")
|
||||||
app.setOrganizationName("WGS")
|
app.setOrganizationName("WGS")
|
||||||
# Icône globale multi-tailles — barre des tâches + alt-tab
|
# Icône globale multi-tailles — barre des tâches + alt-tab
|
||||||
app_icon = icons.icon_app()
|
app_icon = icons.icon_app()
|
||||||
|
|||||||
Reference in New Issue
Block a user