# plugins/http/checker.py """Core HTTP check logic — runs a single monitor check and returns a result dict.""" from __future__ import annotations import asyncio import logging import ssl import time from datetime import datetime, timezone from urllib.parse import urlparse import httpx logger = logging.getLogger(__name__) async def _get_tls_expiry(hostname: str, port: int) -> datetime | None: """Attempt a bare TLS handshake to extract the certificate expiry date.""" ctx = ssl.create_default_context() try: reader, writer = await asyncio.wait_for( asyncio.open_connection(hostname, port, ssl=ctx), timeout=5.0, ) cert = writer.get_extra_info("ssl_object").getpeercert() writer.close() try: await writer.wait_closed() except Exception: pass exp_str = cert.get("notAfter", "") if not exp_str: return None # Format: "Mar 22 12:00:00 2027 GMT" return datetime.strptime(exp_str, "%b %d %H:%M:%S %Y %Z").replace( tzinfo=timezone.utc ) except Exception as exc: logger.debug("TLS check failed for %s:%d — %s", hostname, port, exc) return None async def run_check( url: str, method: str = "GET", expected_status: int = 200, content_match: str = "", headers: dict | None = None, timeout_seconds: int = 10, follow_redirects: bool = True, verify_ssl: bool = True, ) -> dict: """ Perform one HTTP check. Returns a dict with: is_up, status_code, response_ms, content_matched, error_msg, tls_expires_at """ result: dict = { "is_up": False, "status_code": None, "response_ms": None, "content_matched": None, "error_msg": None, "tls_expires_at": None, } parsed = urlparse(url) is_https = parsed.scheme.lower() == "https" t0 = time.monotonic() try: async with httpx.AsyncClient( follow_redirects=follow_redirects, verify=verify_ssl, timeout=timeout_seconds, ) as client: response = await client.request( method, url, headers=headers or {}, ) result["response_ms"] = round((time.monotonic() - t0) * 1000, 1) result["status_code"] = response.status_code status_ok = response.status_code == expected_status if content_match: matched = content_match in response.text result["content_matched"] = matched result["is_up"] = status_ok and matched else: result["is_up"] = status_ok except httpx.TimeoutException: result["response_ms"] = round((time.monotonic() - t0) * 1000, 1) result["error_msg"] = "Timeout" except httpx.ConnectError as exc: result["error_msg"] = f"Connection error: {exc}" except Exception as exc: result["error_msg"] = str(exc)[:512] # TLS expiry — only for HTTPS, and only when the check succeeded or we got a response if is_https and result["status_code"] is not None: port = parsed.port or 443 result["tls_expires_at"] = await _get_tls_expiry(parsed.hostname, port) return result