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
94 lines
3.2 KiB
Python
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."
|