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

This commit was merged in pull request #26.
This commit is contained in:
2026-09-11 17:25:21 +02:00
parent d124f78b55
commit f194ed985d
9 changed files with 1070 additions and 105 deletions

View File

@@ -8,6 +8,7 @@ depuis les variables d'environnement (préfixées par groupe) et le fichier
from __future__ import annotations
import warnings
from datetime import date
from typing import Literal
from urllib.parse import urlparse
@@ -18,6 +19,7 @@ from pydantic import (
ValidationInfo,
field_serializer,
field_validator,
model_validator,
)
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -158,10 +160,16 @@ class XmppSettings(BaseSettings):
Contraintes de champs : ``port`` est borné entre 1 et 65535 et ``timeout``
doit être strictement positif.
Politique TLS : la désactivation de TLS (``use_tls`` à ``False``) n'est
autorisée que sur un hôte de boucle locale (``localhost``, ``127.0.0.1``,
``::1``). Dans tout autre cas, une erreur de validation est levée,
indépendamment de l'état du champ ``enabled``.
Politique TLS : le mode ``tls_mode`` détermine la négociation TLS
(``direct``, ``starttls`` ou ``disabled``). Le mode ``disabled`` n'est
autorisé que sur un hôte de boucle locale (``localhost``, ``127.0.0.1``,
``::1``) ; ``starttls`` et ``direct`` sont permis pour tous les hôtes.
Compatibilité : le champ historique ``use_tls`` (booléen) est un alias
obsolète ; ``use_tls=True`` mappe vers ``tls_mode="direct"`` et
``use_tls=False`` vers ``tls_mode="starttls"``, avec un
:pyexc:`DeprecationWarning`. La valeur brute fournie reste lisible via
``use_tls`` (``None`` si non fournie).
"""
model_config = SettingsConfigDict(
@@ -178,33 +186,61 @@ class XmppSettings(BaseSettings):
port: int = Field(default=5222, ge=1, le=65535)
to: str | None = None
resource: str = "pronote-sync"
use_tls: bool = True
timeout: int = Field(default=30, gt=0)
tls_mode: Literal["direct", "starttls", "disabled"] = "starttls"
use_tls: bool | None = Field(
default=None,
deprecated="Utiliser tls_mode à la place (XMPP_USE_TLS obsolète).",
)
timeout: float = Field(default=30, gt=0)
connect_timeout: float = Field(default=15, gt=0)
cleanup_timeout: float = Field(default=10, gt=0)
@field_validator("use_tls")
@model_validator(mode="before")
@classmethod
def _validate_tls_policy(cls, v: bool, info: ValidationInfo) -> bool:
"""Refuse la désactivation de TLS hors des hôtes de boucle locale.
def _migrate_use_tls(cls, data: object) -> object:
"""Mappe l'alias obsolète ``use_tls`` vers le mode canonique ``tls_mode``.
La règle s'applique quel que soit l'état du champ ``enabled``. Le
message d'erreur ne contient aucune valeur sensible (``jid``,
``use_tls=True`` devient ``tls_mode="direct"`` et ``use_tls=False``
devient ``tls_mode="starttls"`` ; un :pyexc:`DeprecationWarning` est
émis à chaque usage explicite de l'alias. ``tls_mode`` fourni
explicitement prend le pas sur l'alias.
:param data: Données d'entrée du modèle (dict ou autre).
:return: Données d'entrée avec ``tls_mode`` dérivé de ``use_tls``.
:rtype: object
"""
if not isinstance(data, dict) or "use_tls" not in data:
return data
warnings.warn(
"XMPP_USE_TLS est obsolète : utiliser XMPP_TLS_MODE "
"('direct', 'starttls' ou 'disabled').",
DeprecationWarning,
stacklevel=2,
)
if data.get("tls_mode") is None:
data["tls_mode"] = "direct" if data["use_tls"] else "starttls"
return data
@model_validator(mode="after")
def _validate_tls_policy(self) -> XmppSettings:
"""Refuse le mode ``disabled`` hors des hôtes de boucle locale.
La règle s'applique quel que soit l'état du champ ``enabled``. Les
modes ``starttls`` et ``direct`` sont autorisés pour tous les hôtes.
Le message d'erreur ne contient aucune valeur sensible (``jid``,
``password``, ``to``).
:param v: Valeur du champ ``use_tls`` à valider.
:param info: Contexte de validation (accès aux autres champs).
:return: La valeur validée inchangée.
:rtype: bool
:raises ValueError: Si ``use_tls`` est ``False`` et que ``host``
:return: L'instance validée inchangée.
:rtype: XmppSettings
:raises ValueError: Si ``tls_mode`` est ``disabled`` et que ``host``
n'est pas un hôte de boucle locale.
"""
if v is False:
host = info.data.get("host", "")
if host not in _XMPP_LOOPBACK_HOSTS:
raise ValueError(
"TLS désactivé n'est autorisé que sur les hôtes de loopback "
"(localhost, 127.0.0.1, ::1)."
) from None
return v
if self.tls_mode == "disabled" and self.host not in _XMPP_LOOPBACK_HOSTS:
raise ValueError(
"TLS désactivé n'est autorisé que sur les hôtes de loopback "
"(localhost, 127.0.0.1, ::1)."
) from None
return self
class AISettings(BaseSettings):