CreaRack-SL

Servicio http_service

Archivo: monitoring/services/http_service.py | Versión: Etapa 3 (cb4fe5e+) | Status: Activo

Propósito

Ejecuta pruebas HTTP sobre un objetivo (MonitoringTarget) y reporta el estado (UP/DOWN), tiempo de respuesta, validez del certificado SSL, códigos de estado y location headers. Usado por el CNS (Correlation aNalysis System) para detectar anomalías de disponibilidad y certificados.

Seguridad (Etapa 3): Integrado con net_guard.resolve_and_validate() para eliminar DNS-rebinding / TOCTOU — el cliente nunca re-resuelve después de la validación inicial.

API Pública

Clase HttpProbe

class HttpProbe:
    def __init__(self, timeout=5, follow_redirects=False, verify_ssl=True):
        self.timeout = timeout
        self.follow_redirects = False  # Forzado OFF (Etapa 3): un redirect escapa el pin
        self.verify_ssl = verify_ssl

Métodos principales:

check(url, method="GET", expected_codes=None, headers=None, check_ssl=True) -> HttpResult

Realiza una petición HTTP síncrona a url, devolviendo un HttpResult.

Parámetros:

  • url: URL destino (p.ej. “https://api.example.com:8443/health”).
  • method: “GET”, “POST”, “HEAD”, etc.
  • expected_codes: Códigos a considerar “UP” (p.ej. [200, 201]). Default: [200, 201, 202, 204, 301, 302, 304].
  • headers: Headers HTTP adicionales (p.ej. {"User-Agent": "..."}, {"Authorization": "Bearer ..."}.
  • check_ssl: Verificar certificado SSL (default True).

Flujo (Etapa 3):

  1. resolve_and_validate(url) → lista de IPs pinneadas, rechazo SSRF temprano.
  2. SSL check (si HTTPS): _check_ssl_cert() iterando cada IP pinneada.
  3. Stream HTTP request: try_each_ip() con callback que ejecuta la petición a IP pineada.
  4. Validar respuesta: status code, tiempo, content-length.
  5. Manejo de redirects: follow_redirects forzado OFF; Location header se reporta pero no se sigue.

Devuelve: HttpResult

@dataclass
class HttpResult:
    status: Literal["up", "down", "unknown"]
    status_code: int | None
    response_time_ms: float | None
    ssl_valid: bool | None
    ssl_expires_days: int | None
    error: str | None
    redirect_url: str | None

check_async(url, method="GET", ...) -> HttpResult (async)

Variante asíncrona usando httpx.AsyncClient, ídem check() pero awaitable.

Clase interna HttpResult

@dataclass
class HttpResult:
    status: Literal["up", "down", "unknown"]
    status_code: int | None
    response_time_ms: float | None
    ssl_valid: bool | None
    ssl_expires_days: int | None
    error: str | None
    redirect_url: str | None

Flujo de ejecución (sync)

check(url="https://api.example.com/health")
  ↓
resolve_and_validate(url) → ResolvedDestination(ips=[203.0.113.1, 203.0.113.2], ...)
  ↓
[SSRF blocked] → return HttpResult(status="down", error="Blocked destination: ...")
  ↓
if check_ssl:
    _check_ssl_cert(hostname="api.example.com", ips=[203.0.113.1, 203.0.113.2])
      ↓
      [try_each_ip] iterate IPs, socket.create_connection((ip, 443)) + TLS
      ↓
      return (is_valid, days_until_expiry)
  ↓
try_each_ip(dest, _attempt) → ejecuta _attempt(ip) en cada IP:
    def _attempt(ip):
        with client.stream(
            method="GET",
            url=pinned_url(dest, ip),  # p.ej. "https://203.0.113.1/health"
            headers={**headers, "Host": "api.example.com", **pin_extensions},
            extensions={"sni_hostname": "api.example.com"}
        ) as response:
            [read body up to MAX_RESPONSE_BYTES]
            return (status_code, location, content_length)
  ↓
return HttpResult(status="up", status_code=200, response_time_ms=42.5, ...)

Crítico:

  • pinned_url(dest, ip) → URL con IP literal (p.ej. https://203.0.113.1/...).
  • Host header + SNI hostname → siempre el original (api.example.com).
  • follow_redirects=False (forzado) → el cliente nunca sigue Location, que sería un SSRF.

Flujo async (igual, async/await)

await check_async(...) → usa httpx.AsyncClient, try_each_ip_async() en lugar de try_each_ip()

Métodos internos

_determine_status(status_code, response_time, expected_codes) -> str

Lógica: si status_code in expected_codes y response_time < timeout, devuelve “up”; else “down”.

_check_ssl_cert(hostname, port, ips=None) -> (is_valid, days_until_expiry)

Valida el certificado SSL conectando a IPs pinneadas (ahora iterable en lugar de hostname).

Etapa 3 (cb4fe5e):

  • Parámetro nuevo: ips: list | None — lista de IPs pinneadas.
  • Itera ips, intenta cada una.
  • ssl.SSLError (certificado malo) → devuelve (False, None) (terminal, no reintenta).
  • Connection exception (timeout, refused) → intenta siguiente IP, si todas fallan devuelve (None, None).

Guardias de seguridad

GuardiaNivelImplementación
SSRFTemprano (pre-connection)resolve_and_validate() rechaza rangos internos
DNS-rebindingConnection-timetry_each_ip() — cliente NUNCA re-resuelve
Redirect SSRFResponse-timefollow_redirects=False — Location no se sigue
DoS (response size)Read-timeMAX_RESPONSE_BYTES (hard cap)

Integración con net_guard

FunciónUso
resolve_and_validate(url)Resolver + validar una sola vez
pinned_url(dest, ip)Reconstruir URL con IP pinneada
try_each_ip(dest, _attempt)Retry entre IPs
try_each_ip_async(dest, _attempt)Retry async entre IPs
httpx_pin_kwargs(dest)Headers + extensions para httpx

Usos principales

  • Monitoring probe: Task periodic que ejecuta check() sobre cada MonitoringTarget HTTP.
  • Health check endpoint: Endpoint /api/health podría usar check() en otro host.
  • ITSM webhook validation: Antes de guardar un webhook URL, verificar check(..., expected_codes=[200, 201, ...]).

Véase también

  • [[feature—monitoring—cierre-deudas-sa3]] — feature de cierre de deudas (M15)
  • [[entity—monitoring—service—net-guard]] — módulo centralizado de pinning
  • [[entity—monitoring—service—notification-service]] — otro cliente de net_guard
  • [[entity—monitoring—model—monitoring-target]] — objetivo de prueba
  • [[entity—monitoring—model—monitoring-alert]] — alerta generada por probe