"""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, proxy=False, prefixe=""): self.jeton = secrets.token_urlsafe(24) self.cookie = f"rtg_{port}" self.distant, self.https, self.proxy = distant, https, proxy self.chemin_cookie = prefixe + "/" if prefixe else "/" 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, https=None, effacer=False): valeur = f"{self.cookie}={'' if effacer else sid}; Path={self.chemin_cookie}; HttpOnly; SameSite=Strict" if effacer: valeur += "; Max-Age=0" if self.https if https is None else 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."