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):
resolve_and_validate(url)→ lista de IPs pinneadas, rechazo SSRF temprano.- SSL check (si HTTPS):
_check_ssl_cert()iterando cada IP pinneada. - Stream HTTP request:
try_each_ip()con callback que ejecuta la petición a IP pineada. - Validar respuesta: status code, tiempo, content-length.
- Manejo de redirects:
follow_redirectsforzado 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
| Guardia | Nivel | Implementación |
|---|---|---|
| SSRF | Temprano (pre-connection) | resolve_and_validate() rechaza rangos internos |
| DNS-rebinding | Connection-time | try_each_ip() — cliente NUNCA re-resuelve |
| Redirect SSRF | Response-time | follow_redirects=False — Location no se sigue |
| DoS (response size) | Read-time | MAX_RESPONSE_BYTES (hard cap) |
Integración con net_guard
| Función | Uso |
|---|---|
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/healthpodría usarcheck()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