Files
FabriceandClaude Opus 5.5 4366db94ed Administration depuis le site : bouton Connexion sur l'accueil
site.toml [administration] adresse : l'accueil affiche un bouton
« Connexion » vers l'interface. Celle-ci peut tourner derrière le serveur
web du site (rtg interface --proxy --prefixe /admin) : adresses
préfixées, mot de passe obligatoire, visiteur identifié par
X-Forwarded-For, cookie Secure limité au préfixe quand le site est en
HTTPS. Exemples nginx et Apache dans le README. Le bouton Arrêter ne
remplace plus le bloc de la tâche en même temps que la relève.

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

95 lines
3.4 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, 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."