fix(xmpp): corriger le timeout XMPP — TLS mode + timeouts de connexion/cleanup (#25) #26

Merged
AntoineVe merged 1 commits from fix/xmpp-tls-timeout-25 into main 2026-09-11 17:25:23 +02:00
Owner

Contexte

Résolution du bug de production #25 — le canal XMPP ne parvenait pas à établir sa session : timeout 30 s puis erreur, nécessitant un Ctrl-C pour terminer le processus. Le serveur XMPP ne voyait aucune connexion depuis l'identité configurée.

Cause racine

use_tls=True (défaut) était interprété comme direct TLS (enable_direct_tls=True, enable_starttls=False), mais le port par défaut 5222 est conventionnellement le port STARTTLS. Le client envoyait un ClientHello TLS sur un port attendant un stream XMPP en clair → le serveur ne reconnaissait jamais l'identité → timeout.

Causes secondaires : événement connection_failed de Slixmpp non géré, pas de timeout sur connect_future, pas de timeout sur disconnect_future dans le finally.

Corrections

Configuration (settings.py)

  • tls_mode: Literal["direct", "starttls", "disabled"] (défaut "starttls") remplace le booléen use_tls. Compatible avec le port 5222 par défaut.
  • use_tls conservé comme alias déprécié avec DeprecationWarning (True → direct, False → starttls).
  • Nouveaux champs : connect_timeout (15 s), cleanup_timeout (10 s) — distincts du timeout de session (30 s).
  • La restriction « TLS désactivé uniquement en loopback » s'applique désormais à tls_mode="disabled" uniquement.

Canal XMPP (channels/xmpp.py)

  • match self.settings.tls_mode pour configurer enable_direct_tls / enable_starttls.
  • Événement connection_failed de Slixmpp géré → échec rapide au lieu d'attendre le timeout de session.
  • asyncio.wait_for sur connect_future (anti-blocage DNS/TCP/TLS).
  • asyncio.wait_for sur disconnect_future dans le finally (anti-blocage cleanup).
  • Attente conjointe connect_future + session_future via asyncio.wait(FIRST_COMPLETED) pour détecter connection_failed avant l'expiration du connect_timeout.
  • Annulation des tâches pending sur tous les chemins de retour (_cancel_pending() idempotent).
  • asyncio.CancelledError attrapé → retour False (contrat « never raises »).
  • ClientXMPP construit dans le try avec guard client is not None dans le finally.
  • Tous les redact_exception() passent extra_secrets=_secret_values(self.settings).

Tests

  • FakeClientXMPP enrichi avec modes connect (ok/pending/connection_failed) et disconnect (ok/pending).
  • 25 nouveaux tests : timeout connexion, connection_failed, cleanup bloqué, CancelledError, mapping tls_mode, fuite de secrets, backward compat use_tls, validation connect_timeout/cleanup_timeout.
  • Couverture pronote_sync/channels/xmpp.py : 96 %.

Documentation

  • .env.example : XMPP_TLS_MODE=starttls, XMPP_CONNECT_TIMEOUT=15, XMPP_CLEANUP_TIMEOUT=10.
  • GUIDE_DEV_PYTHON.md et README.LLM.md mis à jour.

Validation

Contrôle Résultat
ruff check Pass
ruff format --check Pass
mypy (strict) 107 fichiers, 0 erreur
bandit Aucun finding
pytest 735 passed
Couverture xmpp.py 95.96 %
detect-secrets Pass

Migration

La variable XMPP_USE_TLS reste supportée mais dépréciée. Pour migrer :

  • XMPP_USE_TLS=trueXMPP_TLS_MODE=direct (ou simplement supprimer la ligne pour utiliser le défaut starttls)
  • XMPP_USE_TLS=falseXMPP_TLS_MODE=starttls (défaut, peut être supprimé)

Closes #25

## Contexte Résolution du bug de production #25 — le canal XMPP ne parvenait pas à établir sa session : timeout 30 s puis erreur, nécessitant un Ctrl-C pour terminer le processus. Le serveur XMPP ne voyait aucune connexion depuis l'identité configurée. ## Cause racine `use_tls=True` (défaut) était interprété comme **direct TLS** (`enable_direct_tls=True`, `enable_starttls=False`), mais le port par défaut 5222 est conventionnellement le port STARTTLS. Le client envoyait un ClientHello TLS sur un port attendant un stream XMPP en clair → le serveur ne reconnaissait jamais l'identité → timeout. Causes secondaires : événement `connection_failed` de Slixmpp non géré, pas de timeout sur `connect_future`, pas de timeout sur `disconnect_future` dans le `finally`. ## Corrections ### Configuration (`settings.py`) - **`tls_mode: Literal["direct", "starttls", "disabled"]`** (défaut `"starttls"`) remplace le booléen `use_tls`. Compatible avec le port 5222 par défaut. - `use_tls` conservé comme **alias déprécié** avec `DeprecationWarning` (`True → direct`, `False → starttls`). - Nouveaux champs : `connect_timeout` (15 s), `cleanup_timeout` (10 s) — distincts du `timeout` de session (30 s). - La restriction « TLS désactivé uniquement en loopback » s'applique désormais à `tls_mode="disabled"` uniquement. ### Canal XMPP (`channels/xmpp.py`) - `match self.settings.tls_mode` pour configurer `enable_direct_tls` / `enable_starttls`. - Événement `connection_failed` de Slixmpp géré → échec rapide au lieu d'attendre le timeout de session. - `asyncio.wait_for` sur `connect_future` (anti-blocage DNS/TCP/TLS). - `asyncio.wait_for` sur `disconnect_future` dans le `finally` (anti-blocage cleanup). - Attente conjointe `connect_future` + `session_future` via `asyncio.wait(FIRST_COMPLETED)` pour détecter `connection_failed` avant l'expiration du `connect_timeout`. - Annulation des tâches pending sur tous les chemins de retour (`_cancel_pending()` idempotent). - `asyncio.CancelledError` attrapé → retour `False` (contrat « never raises »). - `ClientXMPP` construit dans le `try` avec guard `client is not None` dans le `finally`. - Tous les `redact_exception()` passent `extra_secrets=_secret_values(self.settings)`. ### Tests - `FakeClientXMPP` enrichi avec modes `connect` (`ok`/`pending`/`connection_failed`) et `disconnect` (`ok`/`pending`). - 25 nouveaux tests : timeout connexion, `connection_failed`, cleanup bloqué, `CancelledError`, mapping `tls_mode`, fuite de secrets, backward compat `use_tls`, validation `connect_timeout`/`cleanup_timeout`. - **Couverture `pronote_sync/channels/xmpp.py` : 96 %**. ### Documentation - `.env.example` : `XMPP_TLS_MODE=starttls`, `XMPP_CONNECT_TIMEOUT=15`, `XMPP_CLEANUP_TIMEOUT=10`. - `GUIDE_DEV_PYTHON.md` et `README.LLM.md` mis à jour. ## Validation | Contrôle | Résultat | |----------|----------| | `ruff check` | ✅ Pass | | `ruff format --check` | ✅ Pass | | `mypy` (strict) | ✅ 107 fichiers, 0 erreur | | `bandit` | ✅ Aucun finding | | `pytest` | ✅ 735 passed | | Couverture `xmpp.py` | ✅ 95.96 % | | `detect-secrets` | ✅ Pass | ## Migration La variable `XMPP_USE_TLS` reste supportée mais dépréciée. Pour migrer : - `XMPP_USE_TLS=true` → `XMPP_TLS_MODE=direct` (ou simplement supprimer la ligne pour utiliser le défaut `starttls`) - `XMPP_USE_TLS=false` → `XMPP_TLS_MODE=starttls` (défaut, peut être supprimé) Closes #25
AntoineVe added 1 commit 2026-09-11 17:02:10 +02:00
Cause racine (#25) : use_tls=True (défaut) activait le direct TLS sur le
port 5222 (conventionnellement STARTTLS). Le client envoyait un ClientHello
TLS sur un port attendant un stream XMPP en clair, le serveur ne voyait
jamais l'identité configurée, et la session expirait après 30 s.

Corrections :
- Remplacer use_tls (bool) par tls_mode: Literal[direct|starttls|disabled]
  (défaut starttls, compatible port 5222). use_tls conservé comme alias
  déprécié avec DeprecationWarning.
- Ajouter connect_timeout (15 s) et cleanup_timeout (10 s) distincts du
  timeout de session (30 s).
- Gérer l'événement connection_failed de Slixmpp pour échouer rapidement
  au lieu d'attendre le timeout de session.
- Borner await connect_future et await disconnect_future par leurs
  timeouts respectifs (anti-blocage).
- Attendre connect_future et session_future conjointement
  (asyncio.wait, FIRST_COMPLETED) pour détecter connection_failed avant
  l'expiration du connect_timeout.
- Annuler les tâches pending sur tous les chemins de retour, y compris
  CancelledError et Exception.
- Redact tous les redact_exception avec extra_secrets=_secret_values().
- Construire ClientXMPP dans le try (contrat « never raises »).
- Enrichir FakeClientXMPP avec modes connect/disconnect configurables.
- 25 nouveaux tests (timeout connexion, connection_failed, cleanup
  bloqué, CancelledError, TLS mismatch, fuite secrets). Couverture 96 %.
- Mettre à jour .env.example, GUIDE_DEV_PYTHON.md, README.LLM.md.

Co-authored-by: OpenCode <opencode@antoineve.me>
AntoineVe merged commit f194ed985d into main 2026-09-11 17:25:23 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: AntoineVe/college-infos#26