La version s'affiche déjà dynamiquement depuis app.__version__ ; seul le contenu curaté (FEATURES) devait suivre les apports réels de la 0.7.4 : diagnostic du split-DNS et verrouillage des champs après import .conf.
148 lines
6.3 KiB
Python
148 lines
6.3 KiB
Python
"""Contenu de la page « À propos ».
|
|
|
|
Source de vérité unique pour les textes de la page : l'onglet Qt
|
|
(app/ui/admin_window.py, méthode `_tab_about`) ne fait que les mettre en
|
|
forme. La version, elle, vient de `app.__version__` (app/__init__.py), le
|
|
seul endroit où elle est bumpée à chaque release — jamais recopiée en dur ici.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
from app import __version__
|
|
|
|
# En développement, CHANGELOG.md vit à la racine du dépôt, deux niveaux
|
|
# au-dessus de ce fichier. Une fois empaqueté par PyInstaller (--onefile),
|
|
# `__file__` pointe vers l'intérieur de l'exécutable et ce chemin n'existe
|
|
# plus : le fichier doit alors être cherché dans `sys._MEIPASS`, le dossier
|
|
# temporaire où l'exe extrait les données embarquées via `--add-data`.
|
|
if getattr(sys, "frozen", False):
|
|
_CHANGELOG_PATH = os.path.join(getattr(sys, "_MEIPASS", ""), "CHANGELOG.md")
|
|
else:
|
|
_CHANGELOG_PATH = os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "CHANGELOG.md"
|
|
)
|
|
|
|
APP_NAME = "WGSecure"
|
|
TAGLINE = "Client WireGuard avec surcouche MFA TOTP, pour Linux et Windows"
|
|
VERSION = __version__
|
|
LICENSE = "Usage personnel"
|
|
REPO_URL = "https://git.tuxtech.fr/tuxgyver/WGSecure"
|
|
AUTHOR_NAME = "Johnny"
|
|
AUTHOR_ROLE = "Auteur"
|
|
|
|
FEATURES = [
|
|
(
|
|
"Connexion en un clic",
|
|
"Icône dans la barre système, montage/démontage du tunnel WireGuard "
|
|
"sans quitter le systray.",
|
|
),
|
|
(
|
|
"Suivi en direct",
|
|
"Statut RX/TX, dernier handshake et latence affichés en continu ; "
|
|
"graphique de bande passante sur les 30 dernières secondes.",
|
|
),
|
|
(
|
|
"Journal et historique",
|
|
"Horodatage de chaque connexion, déconnexion et erreur ; historique "
|
|
"des sessions passées (durée, volume transféré, serveur).",
|
|
),
|
|
(
|
|
"Multi-profils",
|
|
"Plusieurs configurations WireGuard sauvegardées et basculables, "
|
|
"avec import/export de fichiers .conf standards. Les champs issus "
|
|
"d'un import se verrouillent pour éviter une divergence silencieuse "
|
|
"avec le pair déjà configuré côté serveur.",
|
|
),
|
|
(
|
|
"Génération de clés",
|
|
"Paire de clés Curve25519 générée nativement en Python, sans "
|
|
"dépendre du binaire `wg` pour cette étape.",
|
|
),
|
|
(
|
|
"Mode Admin protégé",
|
|
"Configuration complète (serveur, DNS, IP, keepalive, MTU) derrière "
|
|
"un mot de passe et, en option, une double authentification TOTP.",
|
|
),
|
|
(
|
|
"Diagnostic réseau",
|
|
"Test de handshake réel sans monter le tunnel, détection du "
|
|
"filtrage sur port source, et test de fuite DNS.",
|
|
),
|
|
(
|
|
"Auto-reconnexion",
|
|
"Surveillance du tunnel et reconnexion automatique en cas de "
|
|
"coupure, à l'intervalle choisi.",
|
|
),
|
|
(
|
|
"Split-DNS",
|
|
"Configuration d'un serveur DNS dédié pour les noms du réseau distant "
|
|
"(ex: 192.168.1.210 pour .local), tout en utilisant le DNS système "
|
|
"pour le reste du trafic internet. Le test de connexion vérifie que "
|
|
"les domaines configurés s'y résolvent réellement, et un outil de "
|
|
"résolution à la demande permet de tester n'importe quel nom précis.",
|
|
),
|
|
]
|
|
|
|
GUARANTEES_INTRO = "Ce que l'application garantit, quel que soit le profil configuré :"
|
|
GUARANTEES = [
|
|
"Le mot de passe du mode Admin n'est jamais stocké en clair : il est "
|
|
"haché avec bcrypt (12 tours) avant d'être écrit dans la configuration.",
|
|
"La clé privée WireGuard est toujours écrite avec des permissions "
|
|
"restrictives (600) — aussi bien dans la configuration locale que dans "
|
|
"le fichier installé sous /etc/wireguard.",
|
|
"Le test de handshake et le sondage de statut (dernier handshake, "
|
|
"RX/TX) n'ouvrent jamais de tunnel et ne modifient aucun fichier : ce "
|
|
"sont des lectures ou un simple datagramme UDP jetable.",
|
|
"Les commandes système nécessitant les privilèges root sont exécutées "
|
|
"une par une, jamais l'application entière : `make setup-sudoers` "
|
|
"autorise sans mot de passe uniquement wg-quick, wg, et la suppression "
|
|
"d'interface/DNS orpheline — jamais un accès root généralisé.",
|
|
"Le sondage périodique de l'état du tunnel (toutes les 3 secondes) "
|
|
"n'ouvre jamais de dialogue de mot de passe ou d'UAC : il échoue "
|
|
"silencieusement si les privilèges nécessaires ne sont pas déjà "
|
|
"accordés.",
|
|
"Le test de fuite DNS vérifie que les requêtes sortent bien par le "
|
|
"resolver attendu une fois le tunnel monté, plutôt que de supposer la "
|
|
"configuration correcte.",
|
|
]
|
|
|
|
TECH_STACK = [
|
|
("Interface", "PyQt6"),
|
|
("Cryptographie", "cryptography (X25519, ChaCha20-Poly1305, bcrypt)"),
|
|
("MFA", "pyotp (TOTP) + qrcode/Pillow pour le QR code"),
|
|
("Tunnel", "wg-quick / wg (Linux), WireGuard for Windows"),
|
|
("Élévation de privilèges", "sudo NOPASSWD ciblé, repli askpass puis pkexec"),
|
|
("Empaquetage", "PyInstaller (Linux/Windows) + Inno Setup (installeur Windows)"),
|
|
]
|
|
|
|
|
|
def parse_changelog() -> list[dict]:
|
|
"""Lit CHANGELOG.md : titres `## [x.y.z] — date`, sous-titres `### ...`
|
|
et puces `- ...` (avec leurs lignes de continuation) uniquement — le
|
|
format effectivement utilisé par ce fichier, sans dépendance Markdown.
|
|
"""
|
|
entries: list[dict] = []
|
|
version = None
|
|
section = None
|
|
try:
|
|
with open(_CHANGELOG_PATH, encoding="utf-8") as f:
|
|
for raw in f:
|
|
line = raw.rstrip("\n")
|
|
if line.startswith("## "):
|
|
version = {"title": line[3:].strip(), "sections": []}
|
|
entries.append(version)
|
|
section = None
|
|
elif line.startswith("### ") and version is not None:
|
|
section = {"name": line[4:].strip(), "items": []}
|
|
version["sections"].append(section)
|
|
elif line.startswith("- ") and section is not None:
|
|
section["items"].append(line[2:].strip())
|
|
elif (line.strip() and section is not None and section["items"]
|
|
and not line.startswith(("#", "---"))):
|
|
# Ligne de continuation d'une puce étalée sur plusieurs lignes.
|
|
section["items"][-1] += " " + line.strip()
|
|
except FileNotFoundError:
|
|
pass
|
|
return entries
|