1.0.4 — contournement Cloudflare reellement applique
CI / Backend — lint, types, tests (push) Successful in 36s
CI / Interface — types et compilation (push) Successful in 15s
CI / Construction des images (sans publication) (push) Successful in 1m43s
Publication / Images vers Harbor (push) Successful in 2m55s

This commit is contained in:
hcornet committed 2026-09-12 19:08:15 +02:00
1 parent 530c61c814
commit eb38aa9c11
12 files changed
+155 -23

No files matched your search

+26
View File
@@ -2,6 +2,32 @@
Format inspiré de [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/). 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 ## [1.0.2] — 2026-09-12
Correctifs de déploiement uniquement : le code applicatif et les images sont Correctifs de déploiement uniquement : le code applicatif et les images sont
+1 -1
View File
@@ -1 +1 @@
1.0.2 1.0.4
+5 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "limier" name = "limier"
version = "1.0.2" version = "1.0.4"
description = "Plateforme OSINT auto-hébergée : pseudonyme, nom, e-mail, domaine" description = "Plateforme OSINT auto-hébergée : pseudonyme, nom, e-mail, domaine"
requires-python = ">=3.12,<3.14" requires-python = ">=3.12,<3.14"
license = { text = "MIT" } license = { text = "MIT" }
@@ -29,6 +29,10 @@ dependencies = [
"structlog==26.1.0", "structlog==26.1.0",
"prometheus-client==0.26.0", "prometheus-client==0.26.0",
"maigret==0.6.5", "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] [project.optional-dependencies]
+1 -1
View File
@@ -1,3 +1,3 @@
"""Limier — plateforme OSINT auto-hébergée.""" """Limier — plateforme OSINT auto-hébergée."""
__version__ = "1.0.2" __version__ = "1.0.4"
@@ -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 planifié d'auto-contrôle (``jobs.tasks.selfcheck``) de publier une base corrigée
sans redémarrer les workers. sans redémarrer les workers.
Maigret sait se mettre à jour seul depuis GitHub une fois par 24 h. On garde ce Maigret sait se mettre à jour seul depuis GitHub une fois par 24 h, mais
comportement mais on le rend explicite et coupable via ``LIMIER_`` : dans un uniquement quand il est lancé par son CLI. En bibliothèque, ``load_from_path``
conteneur sans accès sortant direct, l'échec de mise à jour doit être un ne déclenche rien : l'application resterait indéfiniment sur la base figée dans
avertissement, pas une erreur fatale. 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 from __future__ import annotations
@@ -52,6 +61,48 @@ def chemin_base() -> str:
return _chemin_base_par_defaut() 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): def charger_base(forcer: bool = False):
"""Retourne l'objet ``MaigretDatabase``, en le rechargeant si nécessaire.""" """Retourne l'objet ``MaigretDatabase``, en le rechargeant si nécessaire."""
from maigret.sites import MaigretDatabase from maigret.sites import MaigretDatabase
+36 -3
View File
@@ -49,6 +49,14 @@ _logger_maigret.setLevel(logging.WARNING)
TAILLE_FILE = 2000 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): class MoteurPseudonyme(Engine):
name = "username" name = "username"
@@ -274,7 +282,7 @@ class MoteurPseudonyme(Engine):
if erreurs_reseau: if erreurs_reseau:
taux = erreurs_reseau / max(1, len(sites)) taux = erreurs_reseau / max(1, len(sites))
if taux > 0.30: if taux > SEUIL_ALERTE_BLOCAGE:
await self.proxies.signaler_echec(proxy) await self.proxies.signaler_echec(proxy)
yield NoticeEvent( yield NoticeEvent(
module=self.name, module=self.name,
@@ -298,10 +306,35 @@ class MoteurPseudonyme(Engine):
) )
def _config_cloudflare(self) -> dict[str, Any] | None: 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: if not url:
return None 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( def _construire_resultat(
self, self,
+16 -3
View File
@@ -28,11 +28,24 @@ from limier.logging import configurer as configurer_journal
async def au_demarrage(ctx: dict) -> None: async def au_demarrage(ctx: dict) -> None:
configurer_journal() configurer_journal()
import asyncio
import structlog import structlog
structlog.get_logger(__name__).info( from limier.engines.username import database
"worker_demarre", concurrence=get_settings().worker_max_jobs
) 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: async def a_l_arret(ctx: dict) -> None:
+4
View File
@@ -223,6 +223,10 @@ async def auto_controle_sites(ctx: dict[str, Any], auto_desactiver: bool = True)
from limier.engines.username import database from limier.engines.username import database
settings = get_settings() 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) db = database.charger_base(forcer=True)
sites = db.ranked_sites_dict(top=settings.username_top_sites_deep, disabled=True) sites = db.ranked_sites_dict(top=settings.username_top_sites_deep, disabled=True)
+3 -2
View File
@@ -7,7 +7,7 @@
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
REGISTRY=registry.tips-of-mine.com REGISTRY=registry.tips-of-mine.com
TAG=1.0.2 TAG=1.0.4
LIMIER_HOST=limier.tips-of-mine.com LIMIER_HOST=limier.tips-of-mine.com
LIMIER_PUBLIC_URL=https://limier.tips-of-mine.com LIMIER_PUBLIC_URL=https://limier.tips-of-mine.com
LIMIER_ENVIRONMENT=prod LIMIER_ENVIRONMENT=prod
@@ -53,7 +53,8 @@ LIMIER_RETENTION_DAYS=30
LIMIER_PROXY_STRATEGY=none LIMIER_PROXY_STRATEGY=none
LIMIER_PROXY_URLS= 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=http://limier-flaresolverr:8191
LIMIER_FLARESOLVERR_URL= LIMIER_FLARESOLVERR_URL=
+4 -4
View File
@@ -56,7 +56,7 @@ x-limier-env: &limier-env
services: services:
limier-api: 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 container_name: limier-api
restart: unless-stopped restart: unless-stopped
command: ["limier-api"] command: ["limier-api"]
@@ -99,7 +99,7 @@ services:
- "homepage.description=Plateforme OSINT" - "homepage.description=Plateforme OSINT"
limier-worker: 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 container_name: limier-worker
restart: unless-stopped restart: unless-stopped
command: ["limier-worker"] command: ["limier-worker"]
@@ -122,7 +122,7 @@ services:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"
limier-web: 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 container_name: limier-web
restart: unless-stopped restart: unless-stopped
volumes: volumes:
@@ -205,7 +205,7 @@ services:
# ``--profile``, pour que l'installation par défaut reste minimale. # ``--profile``, pour que l'installation par défaut reste minimale.
limier-holehe: 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 container_name: limier-holehe
restart: unless-stopped restart: unless-stopped
profiles: ["holehe"] profiles: ["holehe"]
+3 -3
View File
@@ -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 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" \ 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 python /tmp/essai.py domain tips-of-mine.com
``` ```
@@ -24,7 +24,7 @@ Le moteur pseudonyme, en version courte :
```bash ```bash
docker run --rm -v "$PWD/essai_moteur.py:/tmp/essai.py" \ docker run --rm -v "$PWD/essai_moteur.py:/tmp/essai.py" \
-e LIMIER_USERNAME_TOP_SITES=80 \ -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 python /tmp/essai.py username soxoj
``` ```
@@ -33,7 +33,7 @@ Et la recherche par nom, qui montre les variantes déduites :
```bash ```bash
docker run --rm -v "$PWD/essai_moteur.py:/tmp/essai.py" \ docker run --rm -v "$PWD/essai_moteur.py:/tmp/essai.py" \
-e LIMIER_USERNAME_TOP_SITES=60 \ -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" python /tmp/essai.py name "Hubert Cornet"
``` ```
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "limier-frontend", "name": "limier-frontend",
"version": "1.0.2", "version": "1.0.4",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {