From eb38aa9c11aef0f9c6d03200b92c8e8e1c5d22b4 Mon Sep 17 00:00:00 2001 From: hcornet Date: Sat, 12 Sep 2026 19:08:15 +0200 Subject: [PATCH] =?UTF-8?q?1.0.4=20=E2=80=94=20contournement=20Cloudflare?= =?UTF-8?q?=20reellement=20applique?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 26 ++++++++ VERSION | 2 +- backend/pyproject.toml | 6 +- backend/src/limier/__init__.py | 2 +- .../src/limier/engines/username/database.py | 59 +++++++++++++++++-- backend/src/limier/engines/username/engine.py | 39 +++++++++++- backend/src/limier/entrypoints/worker.py | 19 +++++- backend/src/limier/jobs/tasks.py | 4 ++ deploy/.env.example | 5 +- deploy/docker-compose.yml | 8 +-- deploy/essai/LISEZMOI.md | 6 +- frontend/package.json | 2 +- 12 files changed, 155 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a900200..970efe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ Format inspiré de [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/). +## [1.0.4] — 2026-09-12 + +### Corrigé +- **Le contournement Cloudflare n'était jamais appliqué.** La configuration + passée à Maigret n'avait pas la forme attendue par `CloudflareWebgateChecker` + (`trigger_protection`, `modules`, `session_prefix`). Une structure invalide + est ignorée **sans erreur** : FlareSolverr paraissait actif et ne servait + jamais. Le suffixe `/v1` de l'API JSON est désormais ajouté automatiquement. +- Seuil d'alerte sur les sites non vérifiables abaissé de 30 % à 15 %. Maigret + alerte dès 10 % ; à 30 %, une recherche amputée du quart de sa couverture + était présentée comme complète. + +## [1.0.3] — 2026-09-12 + +### Corrigé +- **La base de sites restait figée sur celle livrée dans le paquet** — 3 302 + sites, là où la base amont en compte plus de 5 400. Maigret ne déclenche sa + mise à jour que depuis son CLI ; en bibliothèque, `load_from_path` ne fait + rien. `database.synchroniser()` la déclenche désormais explicitement, au + démarrage du worker et avant l'auto-contrôle nocturne, puis recopie la base + obtenue dans le volume persistant. L'écriture est atomique et l'échec reste + un avertissement : sans accès sortant, on continue avec la base disponible. +- **Brotli manquait dans l'image.** aiohttp annonce accepter le codage `br` + mais ne sait pas le décoder sans ce paquet : les sites répondant en Brotli + étaient perdus avec « Can not decode content-encoding: brotli ». + ## [1.0.2] — 2026-09-12 Correctifs de déploiement uniquement : le code applicatif et les images sont diff --git a/VERSION b/VERSION index 6d7de6e..ee90284 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.2 +1.0.4 diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 77ee780..91025d7 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "limier" -version = "1.0.2" +version = "1.0.4" description = "Plateforme OSINT auto-hébergée : pseudonyme, nom, e-mail, domaine" requires-python = ">=3.12,<3.14" license = { text = "MIT" } @@ -29,6 +29,10 @@ dependencies = [ "structlog==26.1.0", "prometheus-client==0.26.0", "maigret==0.6.5", + # aiohttp (via maigret) annonce accepter le codage « br » dans ses en-têtes. + # Sans ce paquet il ne sait pas le décoder, et les sites qui répondent en + # Brotli sont perdus avec « Can not decode content-encoding: brotli ». + "Brotli==1.2.0", ] [project.optional-dependencies] diff --git a/backend/src/limier/__init__.py b/backend/src/limier/__init__.py index d1c289e..9c16935 100644 --- a/backend/src/limier/__init__.py +++ b/backend/src/limier/__init__.py @@ -1,3 +1,3 @@ """Limier — plateforme OSINT auto-hébergée.""" -__version__ = "1.0.2" +__version__ = "1.0.4" diff --git a/backend/src/limier/engines/username/database.py b/backend/src/limier/engines/username/database.py index 9941091..ea9dfff 100644 --- a/backend/src/limier/engines/username/database.py +++ b/backend/src/limier/engines/username/database.py @@ -6,10 +6,19 @@ automatiquement quand le fichier sur disque change, ce qui permet au travail planifié d'auto-contrôle (``jobs.tasks.selfcheck``) de publier une base corrigée sans redémarrer les workers. -Maigret sait se mettre à jour seul depuis GitHub une fois par 24 h. On garde ce -comportement mais on le rend explicite et coupable via ``LIMIER_`` : dans un -conteneur sans accès sortant direct, l'échec de mise à jour doit être un -avertissement, pas une erreur fatale. +Maigret sait se mettre à jour seul depuis GitHub une fois par 24 h, mais +uniquement quand il est lancé par son CLI. En bibliothèque, ``load_from_path`` +ne déclenche rien : l'application resterait indéfiniment sur la base figée dans +le paquet installé. + +L'écart est loin d'être anecdotique — la base livrée avec maigret 0.6.5 compte +3 302 sites là où la base amont en compte plus de 5 400. C'est autant de +plateformes jamais interrogées. + +``synchroniser()`` déclenche donc explicitement le mécanisme de mise à jour, puis +recopie la base obtenue dans le volume persistant (``LIMIER_SITES_DB_PATH``). +L'échec reste un avertissement : dans un conteneur sans accès sortant, on +continue avec la base disponible plutôt que de refuser de démarrer. """ from __future__ import annotations @@ -52,6 +61,48 @@ def chemin_base() -> str: return _chemin_base_par_defaut() +def synchroniser(forcer: bool = False) -> dict: + """Met la base de sites à jour depuis l'amont, puis la place dans le volume. + + Retourne un résumé : ``{"mise_a_jour": bool, "sites": int, "erreur": str|None}``. + + Appelée au démarrage du worker et avant l'auto-contrôle nocturne. Jamais + depuis l'API : une requête HTTP n'a pas à attendre un téléchargement. + """ + import shutil + + from maigret.db_updater import resolve_db_path + + cible = os.environ.get("LIMIER_SITES_DB_PATH", "").strip() + + try: + # resolve_db_path télécharge la base amont si elle est plus récente et + # retourne le chemin à utiliser. Le nom par défaut est la valeur qui + # déclenche la mise à jour ; tout autre argument la désactive. + chemin_frais = resolve_db_path("resources/data.json", no_autoupdate=False, color=False) + except Exception as exc: + log.warning("maj_base_sites_impossible", erreur=str(exc)) + return {"mise_a_jour": False, "sites": len(charger_base().sites), "erreur": str(exc)} + + if cible and chemin_frais and os.path.isfile(chemin_frais): + try: + os.makedirs(os.path.dirname(cible), exist_ok=True) + frais_mtime = os.path.getmtime(chemin_frais) + cible_mtime = os.path.getmtime(cible) if os.path.isfile(cible) else 0.0 + if frais_mtime > cible_mtime: + # Écriture atomique : un worker qui recharge pendant la copie ne + # doit jamais lire un fichier tronqué. + temporaire = f"{cible}.tmp" + shutil.copy2(chemin_frais, temporaire) + os.replace(temporaire, cible) + log.info("base_sites_synchronisee", source=chemin_frais, cible=cible) + except OSError as exc: + log.warning("copie_base_sites_impossible", erreur=str(exc)) + + db = charger_base(forcer=True) + return {"mise_a_jour": True, "sites": len(db.sites), "erreur": None} + + def charger_base(forcer: bool = False): """Retourne l'objet ``MaigretDatabase``, en le rechargeant si nécessaire.""" from maigret.sites import MaigretDatabase diff --git a/backend/src/limier/engines/username/engine.py b/backend/src/limier/engines/username/engine.py index e3ed1c1..a284388 100644 --- a/backend/src/limier/engines/username/engine.py +++ b/backend/src/limier/engines/username/engine.py @@ -49,6 +49,14 @@ _logger_maigret.setLevel(logging.WARNING) TAILLE_FILE = 2000 +SEUIL_ALERTE_BLOCAGE = 0.15 +"""Part de sites non vérifiables au-delà de laquelle on prévient l'utilisateur. + +Calibré sur le comportement de Maigret, qui alerte dès 10 % de blocages par +protection anti-robot. Un seuil à 30 % laissait passer sans un mot une recherche +amputée du quart de sa couverture. +""" + class MoteurPseudonyme(Engine): name = "username" @@ -274,7 +282,7 @@ class MoteurPseudonyme(Engine): if erreurs_reseau: taux = erreurs_reseau / max(1, len(sites)) - if taux > 0.30: + if taux > SEUIL_ALERTE_BLOCAGE: await self.proxies.signaler_echec(proxy) yield NoticeEvent( module=self.name, @@ -298,10 +306,35 @@ class MoteurPseudonyme(Engine): ) def _config_cloudflare(self) -> dict[str, Any] | None: - url = self.settings.flaresolverr_url.strip() + """Configuration de contournement attendue par Maigret. + + Structure imposée par ``CloudflareWebgateChecker`` : une liste de + protections déclenchant le contournement, une liste ordonnée de modules, + et un préfixe de session. Un dictionnaire d'une autre forme est ignoré + **sans erreur** — le contournement paraît actif mais ne s'applique jamais. + + L'API JSON de FlareSolverr est exposée sur ``/v1`` ; on complète l'URL + si l'utilisateur ne l'a pas fait, la configuration nommant d'ordinaire + le service seul. + """ + url = self.settings.flaresolverr_url.strip().rstrip("/") if not url: return None - return {"enabled": True, "backend": "flaresolverr", "url": url} + if not url.endswith("/v1"): + url = f"{url}/v1" + + return { + "trigger_protection": ["cf_js_challenge", "cf_firewall", "webgate"], + "modules": [ + { + "name": "flaresolverr", + "method": "json_api", + "url": url, + "max_timeout_ms": 60000, + } + ], + "session_prefix": "limier", + } def _construire_resultat( self, diff --git a/backend/src/limier/entrypoints/worker.py b/backend/src/limier/entrypoints/worker.py index 75c27c5..ce45f46 100644 --- a/backend/src/limier/entrypoints/worker.py +++ b/backend/src/limier/entrypoints/worker.py @@ -28,11 +28,24 @@ from limier.logging import configurer as configurer_journal async def au_demarrage(ctx: dict) -> None: configurer_journal() + import asyncio + import structlog - structlog.get_logger(__name__).info( - "worker_demarre", concurrence=get_settings().worker_max_jobs - ) + from limier.engines.username import database + + log = structlog.get_logger(__name__) + + # Synchronisation de la base de sites avant d'accepter le premier travail. + # En fil séparé : resolve_db_path fait un téléchargement bloquant, il ne + # doit pas figer la boucle d'évènements du worker. + try: + resume = await asyncio.to_thread(database.synchroniser) + log.info("base_sites_prete", sites=resume["sites"], erreur=resume["erreur"]) + except Exception as exc: + log.warning("synchronisation_base_echouee", erreur=str(exc)) + + log.info("worker_demarre", concurrence=get_settings().worker_max_jobs) async def a_l_arret(ctx: dict) -> None: diff --git a/backend/src/limier/jobs/tasks.py b/backend/src/limier/jobs/tasks.py index 1523285..6f019b4 100644 --- a/backend/src/limier/jobs/tasks.py +++ b/backend/src/limier/jobs/tasks.py @@ -223,6 +223,10 @@ async def auto_controle_sites(ctx: dict[str, Any], auto_desactiver: bool = True) from limier.engines.username import database settings = get_settings() + # On part de la base amont la plus fraîche : contrôler une base périmée + # reviendrait à désactiver des sites déjà corrigés en amont. + resume = await asyncio.to_thread(database.synchroniser, True) + log.info("base_sites_synchronisee_avant_controle", sites=resume["sites"]) db = database.charger_base(forcer=True) sites = db.ranked_sites_dict(top=settings.username_top_sites_deep, disabled=True) diff --git a/deploy/.env.example b/deploy/.env.example index 04de879..ebb5dc8 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -7,7 +7,7 @@ # --------------------------------------------------------------------------- REGISTRY=registry.tips-of-mine.com -TAG=1.0.2 +TAG=1.0.4 LIMIER_HOST=limier.tips-of-mine.com LIMIER_PUBLIC_URL=https://limier.tips-of-mine.com LIMIER_ENVIRONMENT=prod @@ -53,7 +53,8 @@ LIMIER_RETENTION_DAYS=30 LIMIER_PROXY_STRATEGY=none LIMIER_PROXY_URLS= -# Contournement Cloudflare (profil flaresolverr) +# Contournement Cloudflare (profil flaresolverr). Le suffixe /v1 est ajouté +# automatiquement s'il manque : c'est là qu'est l'API JSON du service. # LIMIER_FLARESOLVERR_URL=http://limier-flaresolverr:8191 LIMIER_FLARESOLVERR_URL= diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index a3662de..8287a73 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -56,7 +56,7 @@ x-limier-env: &limier-env services: limier-api: - image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-backend:${TAG:-1.0.2} + image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-backend:${TAG:-1.0.4} container_name: limier-api restart: unless-stopped command: ["limier-api"] @@ -99,7 +99,7 @@ services: - "homepage.description=Plateforme OSINT" limier-worker: - image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-backend:${TAG:-1.0.2} + image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-backend:${TAG:-1.0.4} container_name: limier-worker restart: unless-stopped command: ["limier-worker"] @@ -122,7 +122,7 @@ services: - "com.centurylinklabs.watchtower.enable=true" limier-web: - image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-frontend:${TAG:-1.0.2} + image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-frontend:${TAG:-1.0.4} container_name: limier-web restart: unless-stopped volumes: @@ -205,7 +205,7 @@ services: # ``--profile``, pour que l'installation par défaut reste minimale. limier-holehe: - image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-holehe:${TAG:-1.0.2} + image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-holehe:${TAG:-1.0.4} container_name: limier-holehe restart: unless-stopped profiles: ["holehe"] diff --git a/deploy/essai/LISEZMOI.md b/deploy/essai/LISEZMOI.md index 146fdac..b500354 100644 --- a/deploy/essai/LISEZMOI.md +++ b/deploy/essai/LISEZMOI.md @@ -12,7 +12,7 @@ Ni base de données, ni Redis, ni Authentik. On appelle le moteur directement. cd /opt # ou n'importe où, il suffit d'avoir essai_moteur.py sous la main docker run --rm -v "$PWD/essai_moteur.py:/tmp/essai.py" \ - registry.tips-of-mine.com/limier/limier-backend:1.0.2 \ + registry.tips-of-mine.com/limier/limier-backend:1.0.4 \ python /tmp/essai.py domain tips-of-mine.com ``` @@ -24,7 +24,7 @@ Le moteur pseudonyme, en version courte : ```bash docker run --rm -v "$PWD/essai_moteur.py:/tmp/essai.py" \ -e LIMIER_USERNAME_TOP_SITES=80 \ - registry.tips-of-mine.com/limier/limier-backend:1.0.2 \ + registry.tips-of-mine.com/limier/limier-backend:1.0.4 \ python /tmp/essai.py username soxoj ``` @@ -33,7 +33,7 @@ Et la recherche par nom, qui montre les variantes déduites : ```bash docker run --rm -v "$PWD/essai_moteur.py:/tmp/essai.py" \ -e LIMIER_USERNAME_TOP_SITES=60 \ - registry.tips-of-mine.com/limier/limier-backend:1.0.2 \ + registry.tips-of-mine.com/limier/limier-backend:1.0.4 \ python /tmp/essai.py name "Hubert Cornet" ``` diff --git a/frontend/package.json b/frontend/package.json index f73d300..b37484a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "limier-frontend", - "version": "1.0.2", + "version": "1.0.4", "private": true, "type": "module", "scripts": {