1.0.4 — contournement Cloudflare reellement applique
This commit is contained in:
1 parent
530c61c814
commit
eb38aa9c11
12 files changed
+155
-23
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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=
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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,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": {
|
||||||
|
|||||||
Reference in new issue
Block a user