Appairage du téléphone, en-têtes de licence (2.1.0)
Recopier un jeton de trente-trois caractères sur un clavier tactile n'était pas un réglage, c'était une corvée. On ouvre désormais une fenêtre depuis le serveur, on appuie sur Appairer côté téléphone, et c'est fait. Le jeton n'est pas pour autant distribué : ouvrir la fenêtre l'exige déjà, elle dure deux minutes, se referme dès qu'un appareil s'est servi, et ne répond qu'à une adresse privée. Le champ de saisie reste là pour qui préfère le coller. Les cinquante fichiers de code portent en outre leur notice de licence, comme la GPL le recommande. Elle est en français, comme le reste ; le texte qui fait foi demeure celui de LICENSE, non traduit.
This commit is contained in:
1 parent
b3ddc2117b
commit
51b60cda70
53 files changed
+717
-4
No files matched your search
@@ -1,3 +1,13 @@
|
||||
# Noxely — collecte côté serveur, écoute hors connexion côté mobile.
|
||||
# Copyright © 2026 Johnny (tuxgyver)
|
||||
#
|
||||
# Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
|
||||
# selon les termes de la GNU General Public License, version 3 ou ultérieure,
|
||||
# telle que publiée par la Free Software Foundation.
|
||||
#
|
||||
# Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
|
||||
# Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
|
||||
|
||||
"""API HTTP du serveur Noxely.
|
||||
|
||||
Le client Android ne consomme que `/api/health`, `/api/library` et `/api/media/{id}/file` :
|
||||
@@ -7,6 +17,8 @@ mobile doit pouvoir se contenter d'un sondage bref, c'est là qu'est le gain de
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
@@ -19,6 +31,7 @@ from .storage import Storage
|
||||
from .workers import Downloader, Watcher, safe_name
|
||||
|
||||
VERSION = "1.0.0"
|
||||
PAIRING_WINDOW_SECONDS = 120
|
||||
WEB_DIR = Path(__file__).parent / "web"
|
||||
|
||||
config = config_module.load()
|
||||
@@ -180,6 +193,53 @@ def queue_state() -> dict:
|
||||
return {"items": [m.public() for m in pending if m.state != "ready"][:50]}
|
||||
|
||||
|
||||
#: Fenêtre d'appairage : instant d'expiration, ou None si elle est fermée.
|
||||
_pairing_until: float | None = None
|
||||
|
||||
|
||||
def _is_private(host: str) -> bool:
|
||||
"""Le réseau local, et lui seul."""
|
||||
try:
|
||||
address = ipaddress.ip_address(host)
|
||||
return address.is_private or address.is_loopback
|
||||
except ValueError:
|
||||
return host in ("localhost", "")
|
||||
|
||||
|
||||
@app.post("/api/pair/open", dependencies=[Depends(authorised)])
|
||||
def open_pairing() -> dict:
|
||||
"""Ouvre une fenêtre de deux minutes pendant laquelle un appareil du réseau local peut
|
||||
réclamer le jeton. Fermée le reste du temps : sinon le jeton ne protégerait plus rien."""
|
||||
global _pairing_until
|
||||
_pairing_until = time.time() + PAIRING_WINDOW_SECONDS
|
||||
return {"open": True, "seconds": PAIRING_WINDOW_SECONDS}
|
||||
|
||||
|
||||
@app.get("/api/pair/state", dependencies=[Depends(authorised)])
|
||||
def pairing_state() -> dict:
|
||||
remaining = int(_pairing_until - time.time()) if _pairing_until else 0
|
||||
return {"open": remaining > 0, "seconds": max(0, remaining)}
|
||||
|
||||
|
||||
@app.post("/api/pair")
|
||||
def pair(request: Request) -> dict:
|
||||
"""Livre le jeton à un appareil du réseau local, pendant la fenêtre d'appairage.
|
||||
|
||||
La fenêtre se referme dès qu'un appareil s'est servi : une ouverture, un appairage.
|
||||
"""
|
||||
global _pairing_until
|
||||
client = request.client.host if request.client else ""
|
||||
if not _pairing_until or time.time() > _pairing_until:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Aucun appairage en cours. Ouvrez-en un depuis l'interface du serveur.",
|
||||
)
|
||||
if not _is_private(client):
|
||||
raise HTTPException(status_code=403, detail="Appairage réservé au réseau local")
|
||||
_pairing_until = None
|
||||
return {"token": config.token, "name": "Noxely", "version": VERSION}
|
||||
|
||||
|
||||
@app.get("/api/bootstrap")
|
||||
def bootstrap(request: Request) -> dict:
|
||||
"""Livre le jeton — uniquement à une requête venue de la machine elle-même.
|
||||
|
||||
Reference in new issue
Block a user