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
This commit is contained in:
1 parent
06153a227e
commit
71779f6f53
17 files changed
+1047
-38
No files matched your search
+80
-2
@@ -10,7 +10,9 @@ from http.server import ThreadingHTTPServer
|
||||
import pytest
|
||||
|
||||
from rtg import config, sources
|
||||
from rtg import acces, corrections
|
||||
from rtg.interface import serveur
|
||||
from rtg.interface.securite import Gardien
|
||||
from test_chaine import _photo, pytestmark # noqa: F401 (mêmes prérequis)
|
||||
|
||||
|
||||
@@ -64,19 +66,24 @@ def client(voyage_sans_heures):
|
||||
app = serveur.Application(str(racine))
|
||||
srv = ThreadingHTTPServer(("127.0.0.1", 0), None)
|
||||
port = srv.server_address[1]
|
||||
srv.RequestHandlerClass = serveur.creer_gestionnaire(app, port)
|
||||
gardien = Gardien(port)
|
||||
srv.RequestHandlerClass = serveur.creer_gestionnaire(app, port, gardien)
|
||||
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
||||
sid = gardien.ouvrir_session()
|
||||
|
||||
def appel(chemin, donnees=None, htmx=True):
|
||||
def appel(chemin, donnees=None, htmx=True, session=True):
|
||||
req = urllib.request.Request(f"http://127.0.0.1:{port}{chemin}",
|
||||
data=urllib.parse.urlencode(donnees).encode() if donnees else None)
|
||||
if htmx:
|
||||
req.add_header("HX-Request", "true")
|
||||
if session:
|
||||
req.add_header("Cookie", f"{gardien.cookie}={sid}")
|
||||
try:
|
||||
with urllib.request.urlopen(req) as r:
|
||||
return r.status, r.read().decode()
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode()
|
||||
appel.port, appel.gardien = port, gardien
|
||||
yield appel, racine, voyage
|
||||
srv.shutdown()
|
||||
|
||||
@@ -98,3 +105,74 @@ def test_serveur(client):
|
||||
# Voyage hors du site : refusé
|
||||
code, html = appel("/voyage/analyse?v=../../etc")
|
||||
assert "introuvable" in html.lower()
|
||||
|
||||
|
||||
class _SansRedirection(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, *a, **k):
|
||||
return None
|
||||
|
||||
|
||||
def test_acces(client, tmp_path, monkeypatch):
|
||||
appel, racine, voyage = client
|
||||
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config"))
|
||||
monkeypatch.setenv("APPDATA", str(tmp_path / "config"))
|
||||
base = f"http://127.0.0.1:{appel.port}"
|
||||
ouvreur = urllib.request.build_opener(_SansRedirection)
|
||||
|
||||
def brut(chemin):
|
||||
try:
|
||||
return ouvreur.open(base + chemin)
|
||||
except urllib.error.HTTPError as e:
|
||||
return e
|
||||
# Sans session : renvoyé vers la connexion, y compris pour les fichiers du site
|
||||
for chemin in ("/", "/site/site.toml", "/voyage?v=Ecosse-2027"):
|
||||
r = brut(chemin)
|
||||
assert r.status == 303 and r.headers["Location"] == "/connexion", chemin
|
||||
assert appel("/", session=False)[0] == 200 # htmx : HX-Redirect, corps vide
|
||||
# La clé d'accès ouvre une session (cookie HttpOnly), une fausse clé non
|
||||
r = brut("/?jeton=" + appel.gardien.jeton)
|
||||
assert r.status == 303 and "HttpOnly" in r.headers["Set-Cookie"] and "SameSite=Strict" in r.headers["Set-Cookie"]
|
||||
assert brut("/?jeton=faux").headers["Location"] == "/connexion"
|
||||
# Mot de passe : faux, juste, puis blocage après 5 échecs
|
||||
acces.definir("un mot de passe solide")
|
||||
code, html = appel("/connexion", {"mot_de_passe": "non"}, session=False)
|
||||
assert "incorrect" in html
|
||||
ok, _ = appel.gardien.essayer("10.0.0.1", "un mot de passe solide")
|
||||
assert ok
|
||||
for _ in range(5):
|
||||
appel.gardien.essayer("10.0.0.2", "faux")
|
||||
ok, message = appel.gardien.essayer("10.0.0.2", "un mot de passe solide")
|
||||
assert not ok and "Trop d'essais" in message
|
||||
# En-têtes de sécurité
|
||||
r = urllib.request.urlopen(urllib.request.Request(base + "/connexion"))
|
||||
assert r.headers["X-Frame-Options"] == "DENY" and "frame-ancestors 'none'" in r.headers["Content-Security-Policy"]
|
||||
|
||||
|
||||
def test_corrections(client):
|
||||
appel, racine, voyage = client
|
||||
# Médias : une photo localisée, une sans position
|
||||
with open(voyage / "export_photos.csv", "w") as f:
|
||||
f.write('"P1.jpg",2027:06:11 10:00:00,56.75,2.30,\n"P2.jpg",2027:06:11 11:00:00,,,\n')
|
||||
code, texte = appel("/voyage/position", {"v": "Ecosse-2027", "fichier": "P2.jpg", "lat": "56.8", "lon": "2.36"})
|
||||
assert '"deplace": true' in texte
|
||||
cfg = config.reglages_voyage(str(voyage), config.reglages_site(str(racine)))
|
||||
assert corrections.positions(cfg) == {"P2.jpg": (56.8, 2.36)}
|
||||
# Zone masquée du voyage, puis zone privée du site
|
||||
appel("/voyage/masque", {"v": "Ecosse-2027", "action": "ajouter", "lat": "56.7", "lon": "2.24", "rayon": "3000"})
|
||||
appel("/voyage/masque", {"v": "Ecosse-2027", "action": "ajouter", "portee": "site",
|
||||
"lat": "48.8", "lon": "2.1", "rayon": "500"})
|
||||
cfg = config.reglages_voyage(str(voyage), config.reglages_site(str(racine)))
|
||||
assert corrections.masques(cfg) == [(56.7, 2.24, 3000.0), (48.8, 2.1, 500.0)]
|
||||
# Génération : la photo placée apparaît, le début d'Etape 2 est retiré
|
||||
config.modifier_toml(str(voyage / "voyage.toml"), {"dates_traces": {"Etape 1.gpx": "2027-06-10",
|
||||
"Etape 2.gpx": "2027-06-11"}})
|
||||
from rtg import pages, sources
|
||||
cfg = config.reglages_voyage(str(voyage), config.reglages_site(str(racine)))
|
||||
sources.ranger(str(voyage), cfg, sources.analyser(str(voyage), cfg))
|
||||
v = pages.Voyage(str(voyage), cfg)
|
||||
assert set(v.df_p["Fichier"]) == {"P1.jpg", "P2.jpg"}
|
||||
v.lire_traces()
|
||||
assert v.points_masques > 0
|
||||
for pistes in v.traces.values():
|
||||
for piste, pts in pistes:
|
||||
assert all(corrections.distance_m(la, lo, 56.7, 2.24) > 3000 for la, lo in pts)
|
||||
Reference in new issue
Block a user