Connexion : formulaire utilisable sans JavaScript, refus expliqués dans le Terminal, rtg mot-de-passe --tester ; nginx : location ^~ /admin/
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
2c2530a3dc
commit
4852361ea6
6 files changed
+46
-11
No files matched your search
@@ -161,7 +161,7 @@ Pour administrer le site depuis son adresse publique (bouton « Connexion » en
|
|||||||
- Traefik seul (fichier de configuration dynamique) : copier `exemples/traefik-rtg-admin.yml` dans le dossier de configuration dynamique de Traefik, en y mettant les mêmes `entryPoints` et `certResolver` que pour le site. Le fichier indique l'adresse à mettre selon que Traefik tourne dans Docker sur le Mac, directement sur le Mac, ou sur une autre machine (dans ce dernier cas, lancer RTG avec `--ecoute 0.0.0.0` en plus).
|
- Traefik seul (fichier de configuration dynamique) : copier `exemples/traefik-rtg-admin.yml` dans le dossier de configuration dynamique de Traefik, en y mettant les mêmes `entryPoints` et `certResolver` que pour le site. Le fichier indique l'adresse à mettre selon que Traefik tourne dans Docker sur le Mac, directement sur le Mac, ou sur une autre machine (dans ce dernier cas, lancer RTG avec `--ecoute 0.0.0.0` en plus).
|
||||||
- nginx, dans le bloc `server` du site en HTTPS :
|
- nginx, dans le bloc `server` du site en HTTPS :
|
||||||
```nginx
|
```nginx
|
||||||
location /admin/ {
|
location ^~ /admin/ {
|
||||||
proxy_pass http://127.0.0.1:8765;
|
proxy_pass http://127.0.0.1:8765;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
# extra_hosts:
|
# extra_hosts:
|
||||||
# - "host.docker.internal:host-gateway"
|
# - "host.docker.internal:host-gateway"
|
||||||
|
|
||||||
location /admin/ {
|
# « ^~ » : sinon une règle du type « location ~* \.(js|css|png)$ » déjà
|
||||||
|
# présente dans nginx.conf prendrait les fichiers de l'interface pour ceux du site.
|
||||||
|
location ^~ /admin/ {
|
||||||
proxy_pass http://host.docker.internal:8765;
|
proxy_pass http://host.docker.internal:8765;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
# Traefik a déjà noté l'adresse du visiteur et le https : on les transmet tels quels.
|
# Traefik a déjà noté l'adresse du visiteur et le https : on les transmet tels quels.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import hashlib
|
|||||||
import hmac
|
import hmac
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
from rtg.config import ecrire_toml, lire_toml
|
from rtg.config import ecrire_toml, lire_toml
|
||||||
|
|
||||||
@@ -26,6 +27,8 @@ def chemin_acces():
|
|||||||
|
|
||||||
|
|
||||||
def _empreinte(mdp, sel, n, r, p):
|
def _empreinte(mdp, sel, n, r, p):
|
||||||
|
# NFC : un « é » tapé dans le Terminal ou dans le navigateur donne les mêmes octets.
|
||||||
|
mdp = unicodedata.normalize("NFC", mdp)
|
||||||
return hashlib.scrypt(mdp.encode("utf-8"), salt=sel, n=n, r=r, p=p,
|
return hashlib.scrypt(mdp.encode("utf-8"), salt=sel, n=n, r=r, p=p,
|
||||||
maxmem=128 * r * n * 2, dklen=32)
|
maxmem=128 * r * n * 2, dklen=32)
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ def main(argv=None):
|
|||||||
|
|
||||||
mp = sp.add_parser("mot-de-passe", help="définir ou supprimer le mot de passe de l'interface")
|
mp = sp.add_parser("mot-de-passe", help="définir ou supprimer le mot de passe de l'interface")
|
||||||
mp.add_argument("--supprimer", action="store_true")
|
mp.add_argument("--supprimer", action="store_true")
|
||||||
|
mp.add_argument("--tester", action="store_true", help="vérifier un mot de passe sans le changer")
|
||||||
|
|
||||||
args = p.parse_args(argv)
|
args = p.parse_args(argv)
|
||||||
|
|
||||||
@@ -70,6 +71,13 @@ def main(argv=None):
|
|||||||
print("✅ Mot de passe supprimé." if acces.supprimer() else "ℹ️ Aucun mot de passe défini.")
|
print("✅ Mot de passe supprimé." if acces.supprimer() else "ℹ️ Aucun mot de passe défini.")
|
||||||
return
|
return
|
||||||
import getpass
|
import getpass
|
||||||
|
if args.tester:
|
||||||
|
if not acces.defini():
|
||||||
|
sys.exit(f"❌ Aucun mot de passe défini ({acces.chemin_acces()}).")
|
||||||
|
ok = acces.verifier(getpass.getpass("Mot de passe à vérifier : "))
|
||||||
|
print(f"✅ C'est bien le mot de passe enregistré ({acces.chemin_acces()})." if ok
|
||||||
|
else f"❌ Ce n'est pas le mot de passe enregistré ({acces.chemin_acces()}).")
|
||||||
|
sys.exit(0 if ok else 1)
|
||||||
mdp = getpass.getpass("Nouveau mot de passe : ")
|
mdp = getpass.getpass("Nouveau mot de passe : ")
|
||||||
if mdp != getpass.getpass("Le même, encore une fois : "):
|
if mdp != getpass.getpass("Le même, encore une fois : "):
|
||||||
sys.exit("❌ Les deux saisies diffèrent.")
|
sys.exit("❌ Les deux saisies diffèrent.")
|
||||||
|
|||||||
@@ -20,12 +20,12 @@
|
|||||||
elle contient la clé d'accès de cette session{% if mdp %}, ou entrez le mot de passe{% endif %}.</p>
|
elle contient la clé d'accès de cette session{% if mdp %}, ou entrez le mot de passe{% endif %}.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if mdp %}
|
{% if mdp %}
|
||||||
<form hx-post="{{ base }}/connexion" hx-target="#retour-connexion">
|
<form method="post" action="{{ base }}/connexion" hx-post="{{ base }}/connexion" hx-target="#retour-connexion">
|
||||||
<label>Mot de passe
|
<label>Mot de passe
|
||||||
<input type="password" name="mot_de_passe" autocomplete="current-password" required autofocus>
|
<input type="password" name="mot_de_passe" autocomplete="current-password" required autofocus>
|
||||||
</label>
|
</label>
|
||||||
<button class="principal">Entrer</button>
|
<button class="principal">Entrer</button>
|
||||||
<div id="retour-connexion">{% if attente %}<div class="erreur">Trop d'essais : réessayez dans {{ attente }} s.</div>{% endif %}</div>
|
<div id="retour-connexion">{% if message %}<div class="erreur">{{ message }}</div>{% elif attente %}<div class="erreur">Trop d'essais : réessayez dans {{ attente }} s.</div>{% endif %}</div>
|
||||||
</form>
|
</form>
|
||||||
{% elif not distant %}
|
{% elif not distant %}
|
||||||
<p class="aide">Pour entrer aussi avec un mot de passe (depuis un autre navigateur ou une autre machine) :
|
<p class="aide">Pour entrer aussi avec un mot de passe (depuis un autre navigateur ou une autre machine) :
|
||||||
|
|||||||
@@ -696,14 +696,18 @@ def creer_gestionnaire(app, port, gardien=None):
|
|||||||
connecte = gardien.session_valide(self.session())
|
connecte = gardien.session_valide(self.session())
|
||||||
form = {}
|
form = {}
|
||||||
if methode == "POST":
|
if methode == "POST":
|
||||||
if self.headers.get("HX-Request") != "true":
|
# La connexion marche aussi sans JavaScript (formulaire classique),
|
||||||
|
# à condition de venir d'une page de ce site.
|
||||||
|
if self.headers.get("HX-Request") != "true" and not (
|
||||||
|
chemin == "/connexion" and self.meme_origine()):
|
||||||
raise Erreur(403, "Requête refusée")
|
raise Erreur(403, "Requête refusée")
|
||||||
n = int(self.headers.get("Content-Length") or 0)
|
n = int(self.headers.get("Content-Length") or 0)
|
||||||
if n > 2_000_000:
|
if n > 2_000_000:
|
||||||
raise Erreur(413, "Requête trop grosse")
|
raise Erreur(413, "Requête trop grosse")
|
||||||
form = parse_qs(self.rfile.read(n).decode("utf-8"), keep_blank_values=True)
|
form = parse_qs(self.rfile.read(n).decode("utf-8"), keep_blank_values=True)
|
||||||
if chemin in PUBLIQUES:
|
if chemin in PUBLIQUES:
|
||||||
return self.envoyer(self.connexion(methode, form, connecte), tete)
|
rep = self.connexion(methode, form, connecte)
|
||||||
|
return self.envoyer(rep, tete) if rep else None
|
||||||
if not connecte:
|
if not connecte:
|
||||||
if self.headers.get("HX-Request") == "true":
|
if self.headers.get("HX-Request") == "true":
|
||||||
return self.envoyer(Reponse("", entetes={"HX-Redirect": P + "/connexion"}), tete)
|
return self.envoyer(Reponse("", entetes={"HX-Redirect": P + "/connexion"}), tete)
|
||||||
@@ -741,18 +745,36 @@ def creer_gestionnaire(app, port, gardien=None):
|
|||||||
def https(self):
|
def https(self):
|
||||||
return gardien.https or (gardien.proxy and self.headers.get("X-Forwarded-Proto") == "https")
|
return gardien.https or (gardien.proxy and self.headers.get("X-Forwarded-Proto") == "https")
|
||||||
|
|
||||||
|
def meme_origine(self):
|
||||||
|
origine = self.headers.get("Origin")
|
||||||
|
if not origine:
|
||||||
|
return True # navigateurs anciens : pas d'en-tête Origin
|
||||||
|
hote = self.headers.get("X-Forwarded-Host") if gardien.proxy else None
|
||||||
|
return urlsplit(origine).netloc in {hote or self.headers.get("Host", ""), *hotes}
|
||||||
|
|
||||||
def connexion(self, methode, form, connecte):
|
def connexion(self, methode, form, connecte):
|
||||||
ip = self.ip()
|
ip = self.ip()
|
||||||
|
htmx = self.headers.get("HX-Request") == "true"
|
||||||
|
message = ""
|
||||||
if methode == "POST":
|
if methode == "POST":
|
||||||
ok, message = gardien.essayer(ip, _valeur(form, "mot_de_passe", ""))
|
mdp = _valeur(form, "mot_de_passe", "")
|
||||||
|
ok, message = gardien.essayer(ip, mdp)
|
||||||
if ok:
|
if ok:
|
||||||
|
print(f"🔓 Connexion à l'interface depuis {ip}")
|
||||||
sid = gardien.ouvrir_session()
|
sid = gardien.ouvrir_session()
|
||||||
return Reponse("", entetes={"HX-Redirect": P + "/",
|
cookie = gardien.entete_cookie(sid, self.https())
|
||||||
"Set-Cookie": gardien.entete_cookie(sid, self.https())})
|
if htmx:
|
||||||
return Reponse(f'<div class="erreur">{_echapper(message)}</div>')
|
return Reponse("", entetes={"HX-Redirect": P + "/", "Set-Cookie": cookie})
|
||||||
|
self.rediriger(P + "/", cookie)
|
||||||
|
return None
|
||||||
|
# Dans le Terminal, pour comprendre un refus (jamais le mot de passe).
|
||||||
|
raison = "champ vide (formulaire mal transmis ?)" if not mdp else message
|
||||||
|
print(f"🔒 Connexion refusée depuis {ip} : {raison}")
|
||||||
|
if htmx:
|
||||||
|
return Reponse(f'<div class="erreur">{_echapper(message)}</div>')
|
||||||
return page("connexion.html", mdp=acces.defini(), connecte=connecte,
|
return page("connexion.html", mdp=acces.defini(), connecte=connecte,
|
||||||
distant=gardien.distant or gardien.proxy, attente=gardien.attente(ip),
|
distant=gardien.distant or gardien.proxy, attente=gardien.attente(ip),
|
||||||
site=self.adresse_site())
|
site=self.adresse_site(), message=message)
|
||||||
|
|
||||||
def adresse_site(self):
|
def adresse_site(self):
|
||||||
"""Lien « Retour au site » de la page de connexion, derrière le serveur web."""
|
"""Lien « Retour au site » de la page de connexion, derrière le serveur web."""
|
||||||
|
|||||||
Reference in new issue
Block a user