diff --git a/CHANGELOG.md b/CHANGELOG.md index ba1fea2..01a317d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ Format inspiré de [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/). +## [1.0.9] — 2026-09-13 + +### Ajouté +- **Imitation d'empreinte TLS sur la seconde tentative.** Les protections + modernes identifient un client par son empreinte TLS et HTTP/2 avant même de + lire ses en-têtes — c'est pourquoi les en-têtes seuls n'avaient récupéré que + 3 sites sur 506. Maigret embarque `CurlCffiChecker`, qui imite l'empreinte + d'un vrai navigateur, mais ne l'active que pour les 108 sites portant + `tls_fingerprint` en amont. Limier l'étend désormais à tous les sites rejoués + en seconde passe. Désactivable par `LIMIER_TLS_IMPERSONATION_RETRY=false`. + + Plus lent qu'aiohttp, donc réservé aux sites ayant déjà refusé la première + requête — le coût ne porte que là où il peut rapporter. + ## [1.0.8] — 2026-09-12 ### Modifié diff --git a/VERSION b/VERSION index b0f3d96..66c4c22 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.8 +1.0.9 diff --git a/backend/pyproject.toml b/backend/pyproject.toml index ce5a90d..98f86b7 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "limier" -version = "1.0.8" +version = "1.0.9" description = "Plateforme OSINT auto-hébergée : pseudonyme, nom, e-mail, domaine" requires-python = ">=3.12,<3.14" license = { text = "MIT" } diff --git a/backend/src/limier/__init__.py b/backend/src/limier/__init__.py index 24a6ec6..c450f00 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.8" +__version__ = "1.0.9" diff --git a/backend/src/limier/config.py b/backend/src/limier/config.py index d5cf8ae..0eb0742 100644 --- a/backend/src/limier/config.py +++ b/backend/src/limier/config.py @@ -127,6 +127,15 @@ class Settings(BaseSettings): "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0" ) + tls_impersonation_retry: bool = True + """Imiter l'empreinte TLS d'un navigateur lors de la seconde tentative. + + S'appuie sur ``curl_cffi``, déjà embarqué par Maigret. C'est le levier qui + agit sur les protections modernes : elles identifient un client par son + empreinte TLS bien avant de lire ses en-têtes. Plus lent qu'aiohttp, donc + réservé aux sites ayant déjà refusé la première requête. + """ + outbound_browser_headers: bool = True """Présenter un jeu d'en-têtes de navigateur sur les requêtes sortantes. diff --git a/backend/src/limier/engines/username/database.py b/backend/src/limier/engines/username/database.py index a343148..83b04bd 100644 --- a/backend/src/limier/engines/username/database.py +++ b/backend/src/limier/engines/username/database.py @@ -173,8 +173,30 @@ def selectionner_sites( ) -def dupliquer_avec_entetes(sites: dict) -> dict: - """Copie des sites donnés, avec les en-têtes de navigateur ajoutés. +def dupliquer_pour_seconde_passe(sites: dict) -> dict: + """Copie des sites donnés, préparée pour une seconde tentative renforcée. + + Deux renforts sont appliqués, du moins au plus efficace : + + **Les en-têtes de navigateur**, pour les sites qui n'en déclarent pas. Gain + mesuré en conditions réelles : 3 sites récupérés sur 506. Faible, mais réel. + + **L'imitation d'empreinte TLS**, qui est le vrai levier. Les protections + modernes identifient un client par son empreinte TLS et HTTP/2 avant même + de lire ses en-têtes : annoncer Firefox tout en présentant l'empreinte de + Python est précisément ce qui déclenche le blocage. Maigret embarque + ``CurlCffiChecker``, qui imite l'empreinte d'un vrai navigateur, mais ne + l'active que pour les sites portant ``tls_fingerprint`` dans leur champ + ``protection`` — 108 sites sur 3 302 en amont. On l'étend ici à tous les + sites de la seconde passe. + + Ce renfort n'est pas gratuit : curl_cffi est plus lent qu'aiohttp et ouvre + ses propres connexions. Le réserver aux sites ayant déjà échoué limite ce + coût aux seuls cas où il peut rapporter quelque chose. + + Note : pour ces sites, curl_cffi retire le User-Agent afin d'utiliser celui + du navigateur imité — c'est voulu, la cohérence entre empreinte et en-têtes + est justement ce qu'on cherche. **Duplication et non modification sur place.** Les objets rendus par ``ranked_sites_dict`` proviennent de la base en cache, partagée par tous les @@ -186,12 +208,26 @@ def dupliquer_avec_entetes(sites: dict) -> dict: """ from maigret.sites import MaigretSite + from limier.config import get_settings + + settings = get_settings() entetes = entetes_navigateur() copies = {} + for nom, site in sites.items(): copie = MaigretSite(site.name, dict(site.json)) - copie.headers = dict(site.headers) if site.headers else dict(entetes) + + if settings.outbound_browser_headers: + copie.headers = dict(site.headers) if site.headers else dict(entetes) + + if settings.tls_impersonation_retry: + protection = list(site.protection or []) + if "tls_fingerprint" not in protection: + protection.append("tls_fingerprint") + copie.protection = protection + copies[nom] = copie + return copies diff --git a/backend/src/limier/engines/username/engine.py b/backend/src/limier/engines/username/engine.py index 9d45501..262f5ac 100644 --- a/backend/src/limier/engines/username/engine.py +++ b/backend/src/limier/engines/username/engine.py @@ -347,8 +347,12 @@ class MoteurPseudonyme(Engine): module=self.name, level="info", message=( - f"Seconde tentative sur {len(a_rejouer)} site(s) en échec, " - "avec des en-têtes de navigateur." + f"Seconde tentative sur {len(a_rejouer)} site(s) en échec" + + ( + ", avec imitation d'empreinte TLS." + if self.settings.tls_impersonation_retry + else ", avec des en-têtes de navigateur." + ) ), ) # Le total inclut désormais cette seconde passe, sinon la barre de @@ -357,7 +361,7 @@ class MoteurPseudonyme(Engine): recuperes_avant = len(deja_vus) async for evenement in self._chercher_un( pseudo=pseudo, - sites=database.dupliquer_avec_entetes(a_rejouer), + sites=database.dupliquer_pour_seconde_passe(a_rejouer), compteur=compteur, label=f"{label} — seconde tentative", deja_vus=deja_vus, diff --git a/deploy/.env.example b/deploy/.env.example index 4d4b6f2..81b802a 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -7,7 +7,7 @@ # --------------------------------------------------------------------------- REGISTRY=registry.tips-of-mine.com -TAG=1.0.8 +TAG=1.0.9 LIMIER_HOST=limier.tips-of-mine.com LIMIER_PUBLIC_URL=https://limier.tips-of-mine.com LIMIER_ENVIRONMENT=prod @@ -76,4 +76,9 @@ LIMIER_DNS_RESOLVERS=1.1.1.1,9.9.9.9 # refusé la première requête. Mettre à false pour s'en tenir au comportement # natif de Maigret et comparer les taux de blocage. LIMIER_OUTBOUND_BROWSER_HEADERS=true + +# Imitation d'empreinte TLS sur la seconde tentative (curl_cffi). C'est le +# levier qui agit réellement sur les protections anti-robot. Mettre à false +# pour mesurer ce qu'il apporte. +LIMIER_TLS_IMPERSONATION_RETRY=true LIMIER_WORKER_MAX_JOBS=4 diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 9938fe3..a02f594 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.8} + image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-backend:${TAG:-1.0.9} 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.8} + image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-backend:${TAG:-1.0.9} container_name: limier-worker restart: unless-stopped command: ["limier-worker"] @@ -131,7 +131,7 @@ services: - "com.centurylinklabs.watchtower.enable=true" limier-web: - image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-frontend:${TAG:-1.0.8} + image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-frontend:${TAG:-1.0.9} container_name: limier-web restart: unless-stopped volumes: @@ -214,7 +214,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.8} + image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-holehe:${TAG:-1.0.9} container_name: limier-holehe restart: unless-stopped profiles: ["holehe"] diff --git a/deploy/essai/LISEZMOI.md b/deploy/essai/LISEZMOI.md index 03b2d8a..8fa22cf 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.8 \ + registry.tips-of-mine.com/limier/limier-backend:1.0.9 \ 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.8 \ + registry.tips-of-mine.com/limier/limier-backend:1.0.9 \ 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.8 \ + registry.tips-of-mine.com/limier/limier-backend:1.0.9 \ python /tmp/essai.py name "Hubert Cornet" ``` diff --git a/frontend/package.json b/frontend/package.json index 2117804..0c75718 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "limier-frontend", - "version": "1.0.8", + "version": "1.0.9", "private": true, "type": "module", "scripts": {