This commit is contained in:
2026-06-01 23:22:18 +02:00
parent f061da654d
commit d23d0c15be
14 changed files with 1858 additions and 202 deletions
+483 -47
View File
@@ -2,6 +2,7 @@ from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget,
QLabel, QLineEdit, QPushButton, QSpinBox, QCheckBox,
QGroupBox, QFormLayout, QTextEdit, QMessageBox, QFrame,
QFileDialog, QSizePolicy,
)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont
@@ -127,12 +128,14 @@ class AdminWindow(QDialog):
font-weight: bold; }
QTabBar::tab:hover { background: #253545; color: white; }
""")
tabs.addTab(self._tab_wireguard(), "WireGuard")
tabs.addTab(self._tab_keys(), "Clés")
tabs.addTab(self._tab_mfa(), "MFA")
tabs.addTab(self._tab_test(), "Test connexion")
tabs.addTab(self._tab_admin(), "Sécurité")
tabs.addTab(self._tab_about(), "À propos")
tabs.addTab(self._tab_wireguard(), "WireGuard")
tabs.addTab(self._tab_keys(), "Clés")
tabs.addTab(self._tab_mfa(), "MFA")
tabs.addTab(self._tab_test(), "Test connexion")
tabs.addTab(self._tab_profiles(), "Profils")
tabs.addTab(self._tab_settings(), "Paramètres")
tabs.addTab(self._tab_admin(), "Sécurité")
tabs.addTab(self._tab_about(), "À propos")
layout.addWidget(tabs)
# Barre de boutons
@@ -165,41 +168,79 @@ class AdminWindow(QDialog):
# ------------------------------------------------------------------ #
# 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:
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")
form = QFormLayout(grp)
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
self._srv_endpoint = QLineEdit()
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.setRange(1, 65535)
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.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)
grp2 = QGroupBox("Interface client")
form2 = QFormLayout(grp2)
form2.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
self._iface_name = QLineEdit()
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.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.setPlaceholderText("1.1.1.1")
form2.addRow("DNS :", self._dns)
self._row(form2, "DNS :", self._dns)
self._allowed_ips = QLineEdit()
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.setRange(0, 300)
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.addStretch()
return w
@@ -208,15 +249,18 @@ class AdminWindow(QDialog):
# Onglet Clés
# ------------------------------------------------------------------ #
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")
form = QFormLayout(grp)
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
self._priv_key = QLineEdit()
self._priv_key.setEchoMode(QLineEdit.EchoMode.Password)
self._priv_key.setPlaceholderText("(générer ou coller)")
self._priv_key.setFixedHeight(28)
show_priv = QPushButton("Afficher")
show_priv.setFixedWidth(80)
show_priv.setFixedSize(70, 28)
show_priv.setCheckable(True)
show_priv.toggled.connect(
lambda checked: self._priv_key.setEchoMode(
@@ -224,16 +268,19 @@ class AdminWindow(QDialog):
)
)
priv_row = QHBoxLayout()
priv_row.setSpacing(6)
priv_row.addWidget(self._priv_key)
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.setReadOnly(True)
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.clicked.connect(self._generate_keys)
form.addRow("", btn_gen)
btn_gen.setFixedHeight(28)
self._row(form, "", btn_gen)
lay.addWidget(grp)
grp2 = QGroupBox("Aperçu config WireGuard")
@@ -241,12 +288,28 @@ class AdminWindow(QDialog):
self._config_preview = QTextEdit()
self._config_preview.setReadOnly(True)
self._config_preview.setFont(QFont("Courier", 9))
self._config_preview.setFixedHeight(150)
self._config_preview.setFixedHeight(130)
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)
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)
# 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()
return w
@@ -254,7 +317,7 @@ class AdminWindow(QDialog):
# Onglet MFA
# ------------------------------------------------------------------ #
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.setStyleSheet("color: white; font-weight: bold;")
@@ -267,12 +330,13 @@ class AdminWindow(QDialog):
grp = QGroupBox("Secret TOTP")
form = QFormLayout(grp)
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
self._mfa_secret = QLineEdit()
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.clicked.connect(self._generate_mfa_secret)
form.addRow("", btn_gen_secret)
self._row(form, "", btn_gen_secret)
lay.addWidget(grp)
grp2 = QGroupBox("QR Code — Scanner avec Google Authenticator / Aegis")
@@ -296,39 +360,227 @@ class AdminWindow(QDialog):
# Onglet Test connexion
# ------------------------------------------------------------------ #
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(
"Teste la joignabilité du serveur WireGuard (port UDP) avant d'établir le tunnel."
)
info.setWordWrap(True)
info.setStyleSheet("color: white; font-size: 13px;")
lay.addWidget(info)
btn_test = QPushButton("Lancer le test de connexion")
# Test UDP serveur
grp1 = QGroupBox("Test UDP serveur")
g1 = QVBoxLayout(grp1)
g1.addWidget(QLabel("Vérifie la joignabilité UDP du serveur WireGuard."))
btn_test = QPushButton("🚀 Lancer le test de connexion")
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; }"
"QPushButton:hover { background: #27ae60; }"
)
btn_test.clicked.connect(self._run_test)
lay.addWidget(btn_test)
g1.addWidget(btn_test)
self._test_result = QLabel("")
self._test_result.setWordWrap(True)
self._test_result.setAlignment(Qt.AlignmentFlag.AlignCenter)
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()
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é
# ------------------------------------------------------------------ #
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(
"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")
form = QFormLayout(grp)
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
self._admin_pw1 = QLineEdit()
self._admin_pw1.setEchoMode(QLineEdit.EchoMode.Password)
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.setEchoMode(QLineEdit.EchoMode.Password)
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.setStyleSheet(
"QPushButton { padding: 8px; background: #922b21; color: white;"
"QPushButton { background: #922b21; color: white;"
" border-radius: 5px; border: none; }"
"QPushButton:hover { background: #c0392b; }"
)
btn_set_pw.clicked.connect(self._set_admin_password)
form.addRow("", btn_set_pw)
self._row(form, "", btn_set_pw)
lay.addWidget(grp)
lay.addStretch()
return w
@@ -385,7 +641,7 @@ class AdminWindow(QDialog):
name_lbl.setStyleSheet("color: white;")
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.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 13px;")
layout.addWidget(version_lbl)
@@ -468,6 +724,54 @@ class AdminWindow(QDialog):
# ------------------------------------------------------------------ #
# 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):
priv, pub = wg_core.generate_keypair()
self._priv_key.setText(priv)
@@ -522,6 +826,138 @@ class AdminWindow(QDialog):
"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):
p1 = self._admin_pw1.text()
p2 = self._admin_pw2.text()
+98
View File
@@ -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()
+158
View File
@@ -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
View File
@@ -1,26 +1,52 @@
from __future__ import annotations
from PyQt6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QFrame, QMessageBox, QInputDialog,
QLineEdit,
QLineEdit, QListWidget, QListWidgetItem, QApplication,
QComboBox, QSizePolicy,
)
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QFont, QCloseEvent, QPalette, QColor
from PyQt6.QtCore import Qt, QTimer, QDateTime
from PyQt6.QtGui import QFont, QCloseEvent, QColor
from app.core.config import Config
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.admin_window import AdminWindow
from app.ui.bw_graph import BandwidthGraph
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):
def __init__(self, config: Config, parent=None):
super().__init__(parent)
self._cfg = config
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.setFixedSize(400, 520)
self.setFixedSize(430, 680)
self.setWindowFlags(
Qt.WindowType.Window
| Qt.WindowType.WindowTitleHint
@@ -29,147 +55,364 @@ class MainWindow(QMainWindow):
)
self.setWindowIcon(icons.icon_app())
self._build_ui()
# Timers
self._status_timer = QTimer(self)
self._status_timer.timeout.connect(self._refresh_status)
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._reload_log()
self._reload_profiles()
# ------------------------------------------------------------------ #
# Construction UI
# ------------------------------------------------------------------ #
def _build_ui(self):
central = QWidget()
central.setStyleSheet(f"background: {_DARK};")
self.setCentralWidget(central)
layout = QVBoxLayout(central)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
root = QVBoxLayout(central)
root.setContentsMargins(0, 0, 0, 0)
root.setSpacing(0)
# -- En-tête --------------------------------------------------
# ── En-tête ──────────────────────────────────────────────────
header = QWidget()
header.setFixedHeight(72)
header.setStyleSheet("background: #2c3e50;")
h_layout = QHBoxLayout(header)
h_layout.setContentsMargins(16, 10, 16, 10)
header.setStyleSheet(f"background: {_DARK2};")
h = QHBoxLayout(header)
h.setContentsMargins(14, 8, 14, 8)
self._icon_label = QLabel()
self._icon_label.setFixedSize(44, 44)
self._icon_label.setFixedSize(40, 40)
self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
h_layout.addWidget(self._icon_label)
h.addWidget(self._icon_label)
title_col = QVBoxLayout()
title_col.setSpacing(2)
app_title = QLabel("WGSecure")
app_title.setStyleSheet("color: white; font-size: 17px; font-weight: bold; background: transparent;")
version_label = QLabel("v0.1.0 — WireGuard + MFA")
version_label.setStyleSheet("color: #95a5a6; font-size: 10px; background: transparent;")
title_col.addWidget(app_title)
title_col.addWidget(version_label)
h_layout.addLayout(title_col)
h_layout.addStretch()
layout.addWidget(header)
title_col.setSpacing(1)
title_lbl = QLabel("WGSecure")
title_lbl.setStyleSheet(
"color: white; font-size: 16px; font-weight: bold; background: transparent;"
)
self._profile_combo = QComboBox()
self._profile_combo.setFixedHeight(22)
self._profile_combo.setStyleSheet(
"QComboBox { background: #2e4057; color: #aed6f1; border: none;"
" 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.setFixedHeight(90)
status_area.setStyleSheet("background: #ecf0f1;")
s_layout = QVBoxLayout(status_area)
s_layout.setContentsMargins(10, 10, 10, 10)
s_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
status_area.setFixedHeight(80)
status_area.setStyleSheet(f"background: {_CARD};")
s = QVBoxLayout(status_area)
s.setContentsMargins(10, 6, 10, 6)
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.setStyleSheet("color: #e74c3c; font-size: 16px; font-weight: bold; background: transparent;")
s_layout.addWidget(self._status_badge)
self._status_badge.setStyleSheet(
"color: #e74c3c; font-size: 15px; font-weight: bold; background: transparent;"
)
s.addWidget(self._status_badge)
self._status_sub = QLabel("")
self._status_sub.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._status_sub.setStyleSheet("color: #7f8c8d; font-size: 11px; background: transparent;")
s_layout.addWidget(self._status_sub)
layout.addWidget(status_area)
self._duration_label = QLabel("")
self._duration_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._duration_label.setStyleSheet(
"color: #5d6d7e; font-size: 11px; background: transparent;"
)
s.addWidget(self._duration_label)
root.addWidget(status_area)
# -- Séparateur -----------------------------------------------
sep = QFrame()
sep.setFrameShape(QFrame.Shape.HLine)
sep.setStyleSheet("color: #bdc3c7;")
layout.addWidget(sep)
root.addWidget(self._hsep())
# -- Section infos --------------------------------------------
# ── Infos ─────────────────────────────────────────────────────
info_frame = QFrame()
info_frame.setFrameShape(QFrame.Shape.NoFrame)
info_frame.setStyleSheet("background-color: #ffffff;")
info_frame.setStyleSheet(f"background: {_DARK};")
info_frame.setAutoFillBackground(True)
i_layout = QVBoxLayout(info_frame)
i_layout.setContentsMargins(16, 12, 16, 12)
i_layout.setSpacing(8)
il = QVBoxLayout(info_frame)
il.setContentsMargins(14, 8, 14, 8)
il.setSpacing(5)
self._info_labels: dict[str, QLabel] = {}
rows = [
("server", "Serveur"),
("iface", "Interface"),
("addr", "Adresse IP"),
("mfa", "MFA"),
("rx_tx", "Transfert"),
("handshake", "Dernier handshake"),
]
for key, label in rows:
row_w = QWidget()
row_w.setStyleSheet("background: transparent;")
row_layout = QHBoxLayout(row_w)
row_layout.setContentsMargins(0, 0, 0, 0)
row_layout.setSpacing(8)
for key, icon_txt, label in [
("server", "🌐", "Serveur"),
("iface", "🔌", "Interface"),
("addr", "📍", "Adresse IP"),
("mfa", "🔐", "MFA"),
("rx_tx", "↕️ ", "Transfert"),
("handshake", "🤝", "Handshake"),
]:
rw = QWidget()
rw.setStyleSheet("background: transparent;")
rl = QHBoxLayout(rw)
rl.setContentsMargins(0, 0, 0, 0)
rl.setSpacing(5)
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} :")
lbl_key.setFixedWidth(130)
lbl_key.setStyleSheet("color: #7f8c8d; font-size: 12px; background: transparent;")
root.addWidget(info_frame)
root.addWidget(self._hsep())
lbl_val = QLabel("")
lbl_val.setStyleSheet("color: #2c3e50; font-size: 12px; background: transparent;")
lbl_val.setWordWrap(True)
self._info_labels[key] = lbl_val
# ── Graphique bande passante ───────────────────────────────────
bw_header = self._section_header("📊 Bande passante")
root.addWidget(bw_header)
row_layout.addWidget(lbl_key)
row_layout.addWidget(lbl_val, 1)
i_layout.addWidget(row_w)
self._bw_graph = BandwidthGraph()
self._bw_graph.setFixedHeight(75)
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 -------------------------------------------
sep2 = QFrame()
sep2.setFrameShape(QFrame.Shape.HLine)
sep2.setStyleSheet("color: #bdc3c7;")
layout.addWidget(sep2)
self._log_list = QListWidget()
self._log_list.setFixedHeight(78)
self._log_list.setStyleSheet(
f"QListWidget {{ background: {_DARK2}; color: #aed6f1;"
" 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.setStyleSheet("background: #f8f9fa;")
b_layout = QVBoxLayout(btn_area)
b_layout.setContentsMargins(16, 12, 16, 12)
b_layout.setSpacing(8)
btn_area.setStyleSheet(f"background: {_DARK2};")
bl = QVBoxLayout(btn_area)
bl.setContentsMargins(14, 10, 14, 12)
bl.setSpacing(7)
self._btn_connect = QPushButton("Se connecter")
self._btn_connect.setFixedHeight(42)
self._btn_connect.setStyleSheet(
"QPushButton { background: #27ae60; color: white; font-size: 14px; "
"font-weight: bold; border-radius: 6px; border: none; }"
"QPushButton:hover { background: #219a52; }"
"QPushButton:disabled { background: #bdc3c7; color: #ecf0f1; }"
"QPushButton { background: #1e8449; color: white; font-size: 14px;"
" font-weight: bold; border-radius: 6px; border: none; }"
"QPushButton:hover { background: #27ae60; }"
"QPushButton:disabled { background: #2e4057; color: #5d6d7e; }"
)
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.setFixedHeight(34)
btn_admin = QPushButton("⚙️ Panneau Administrateur")
btn_admin.setFixedHeight(30)
btn_admin.setStyleSheet(
"QPushButton { background: #dde1e7; color: #2c3e50; border-radius: 6px; "
"font-size: 12px; border: none; }"
"QPushButton:hover { background: #bdc3c7; color: #2c3e50; }"
"QPushButton { background: #2e4057; color: white; border-radius: 6px;"
" font-size: 11px; border: none; }"
"QPushButton:hover { background: #3d5166; }"
)
btn_admin.clicked.connect(self._open_admin)
b_layout.addWidget(btn_admin)
layout.addWidget(btn_area)
bl.addWidget(btn_admin)
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):
wg = self._cfg.wg
@@ -181,6 +424,15 @@ class MainWindow(QMainWindow):
self._info_labels["mfa"].setText(
"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:
info = wg_core.get_status_info(self._cfg)
@@ -190,36 +442,47 @@ class MainWindow(QMainWindow):
info = {}
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(
"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.setStyleSheet(
"QPushButton { background: #e74c3c; color: white; font-size: 14px; "
"font-weight: bold; border-radius: 6px; border: none; }"
"QPushButton { background: #922b21; color: white; font-size: 14px;"
" font-weight: bold; border-radius: 6px; border: none; }"
"QPushButton:hover { background: #c0392b; }"
)
self._info_labels["rx_tx"].setText(
f"{info.get('rx_bytes','')}{info.get('tx_bytes','')}"
)
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())
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(
"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.setStyleSheet(
"QPushButton { background: #27ae60; color: white; font-size: 14px; "
"font-weight: bold; border-radius: 6px; border: none; }"
"QPushButton:hover { background: #219a52; }"
"QPushButton { background: #1e8449; color: white; font-size: 14px;"
" font-weight: bold; border-radius: 6px; border: none; }"
"QPushButton:hover { background: #27ae60; }"
)
self._info_labels["rx_tx"].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())
pix = (icons.icon_connected() if connected else icons.icon_disconnected()).pixmap(36, 36)
@@ -228,6 +491,12 @@ class MainWindow(QMainWindow):
# ------------------------------------------------------------------ #
# 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):
if self._connecting:
return
@@ -238,37 +507,48 @@ class MainWindow(QMainWindow):
if connected:
ok, msg = wg_core.disconnect(self._cfg)
if not ok:
QMessageBox.warning(self, "Erreur", f"Déconnexion échouée :\n{msg}")
if ok:
self._add_log("Tunnel WireGuard désactivé", "warning")
else:
self._add_log(f"Erreur déconnexion : {msg}", "error")
QMessageBox.warning(self, "Erreur", msg)
else:
if not self._cfg.configured:
QMessageBox.information(
self, "Configuration manquante",
"WireGuard n'est pas encore configuré.\n"
"Ouvrez le panneau Administrateur pour paramétrer la connexion."
"Ouvrez le panneau Administrateur."
)
return
if self._cfg.mfa_enabled:
if not self._cfg.mfa_secret:
QMessageBox.warning(
self, "MFA non configuré",
"Le MFA est activé mais aucun secret n'est configuré."
)
QMessageBox.warning(self, "MFA non configuré",
"MFA activé mais aucun secret défini.")
return
dlg = MFADialog(self._cfg.mfa_secret, self)
if dlg.exec() != MFADialog.DialogCode.Accepted or not dlg.is_verified():
self._add_log("MFA annulé ou échoué", "warning")
return
self._add_log("Authentification MFA réussie ✓", "success")
self._btn_connect.setEnabled(False)
self._btn_connect.setText("Connexion…")
self._connecting = True
from PyQt6.QtWidgets import QApplication
QApplication.processEvents()
ok, msg = wg_core.connect(self._cfg)
self._connecting = False
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)
self._refresh_status()
@@ -287,6 +567,7 @@ class MainWindow(QMainWindow):
return
dlg = AdminWindow(self._cfg, self)
dlg.exec()
self._reload_profiles()
self._refresh_status()
def closeEvent(self, event: QCloseEvent):
+14
View File
@@ -70,6 +70,7 @@ class SystemTray(QSystemTrayIcon):
self._win.activateWindow()
def _update_status(self):
prev = self._connected
try:
self._connected = wg_core.is_connected(self._cfg)
except Exception:
@@ -81,12 +82,25 @@ class SystemTray(QSystemTrayIcon):
self._action_status.setText("● Connecté")
self._action_toggle.setIcon(icons.icon_connected())
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:
self.setIcon(icons.icon_disconnected())
self.setToolTip("WGSecure — Déconnecté")
self._action_status.setText("● Déconnecté")
self._action_toggle.setIcon(icons.icon_disconnected())
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):
self._win._on_connect()