Files
RoadTripsGenerator/rtg/interface/securite.py
T
FabriceandClaude Opus 5.5 71779f6f53 Interface : corrections à la souris et accès protégé
Onglet « Carte et corrections » : glisser une photo ou une vidéo, placer
les médias sans position, masquer des zones de trace (pour un voyage, ou
zone privée pour tous). Corrections notées dans voyage.toml ([positions],
[corrections]) ou site.toml ([confidentialite]), appliquées par les pages ;
les originaux ne changent pas. L'extraction garde les médias datés sans
position pour qu'on puisse les placer.

Accès : clé d'accès aléatoire à chaque lancement, session par cookie
HttpOnly/SameSite=Strict, mot de passe facultatif (rtg mot-de-passe,
empreinte scrypt hors du site, blocage après 5 échecs), accès réseau
seulement avec mot de passe (--distant), HTTPS possible (--certificat,
--cle), Content-Security-Policy et interdiction des cadres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mf2kATMtqVABHSYdxJMnim
2026-10-09 14:35:43 +00:00

94 lines
3.2 KiB
Python

"""Contrôle d'accès à l'interface.
Deux façons d'entrer :
- la clé d'accès (jeton) tirée au hasard à chaque lancement et ajoutée à
l'adresse que RTG ouvre dans le navigateur (et affiche dans le Terminal) :
seul celui qui voit ce Terminal peut entrer ;
- le mot de passe défini par `rtg mot-de-passe`, indispensable pour un accès
depuis une autre machine (`rtg interface --distant`).
Une fois entré, le navigateur garde un cookie de session (HttpOnly,
SameSite=Strict), valable 12 heures après la dernière requête et perdu à
l'arrêt de l'interface. Après 5 mots de passe faux, une adresse IP est
bloquée une minute, puis deux, quatre… jusqu'à une heure.
"""
import hmac
import secrets
import threading
import time
from rtg import acces
DUREE_SESSION = 12 * 3600
ESSAIS_LIBRES = 5
class Gardien:
def __init__(self, port, distant=False, https=False):
self.jeton = secrets.token_urlsafe(24)
self.cookie = f"rtg_{port}"
self.distant, self.https = distant, https
self._sessions = {}
self._echecs = {} # ip → (nombre d'échecs, heure du dernier)
self._verrou = threading.Lock()
# --- sessions
def ouvrir_session(self):
sid = secrets.token_urlsafe(32)
with self._verrou:
self._sessions[sid] = time.time() + DUREE_SESSION
return sid
def session_valide(self, sid):
if not sid:
return False
maintenant = time.time()
with self._verrou:
fin = self._sessions.get(sid)
if not fin or fin < maintenant:
self._sessions.pop(sid, None)
return False
self._sessions[sid] = maintenant + DUREE_SESSION
return True
def fermer_session(self, sid):
with self._verrou:
self._sessions.pop(sid, None)
def entete_cookie(self, sid, effacer=False):
valeur = f"{self.cookie}={'' if effacer else sid}; Path=/; HttpOnly; SameSite=Strict"
if effacer:
valeur += "; Max-Age=0"
if self.https:
valeur += "; Secure"
return valeur
# --- clé d'accès et mot de passe
def jeton_valide(self, jeton):
return bool(jeton) and hmac.compare_digest(jeton.encode(), self.jeton.encode())
def attente(self, ip):
"""Secondes à attendre avant un nouvel essai (0 si libre)."""
with self._verrou:
n, dernier = self._echecs.get(ip, (0, 0))
if n < ESSAIS_LIBRES:
return 0
blocage = min(60 * 2 ** (n - ESSAIS_LIBRES), 3600)
return max(0, int(dernier + blocage - time.time()))
def essayer(self, ip, mdp):
"""Vérifie le mot de passe. Renvoie (ok, message d'erreur)."""
reste = self.attente(ip)
if reste:
return False, f"Trop d'essais : réessayez dans {reste} s."
if not acces.defini():
return False, "Aucun mot de passe n'est défini (commande rtg mot-de-passe)."
if acces.verifier(mdp):
with self._verrou:
self._echecs.pop(ip, None)
return True, ""
with self._verrou:
n, _ = self._echecs.get(ip, (0, 0))
self._echecs[ip] = (n + 1, time.time())
return False, "Mot de passe incorrect."