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