"""API hardening kit (rate limiting, compression, headers, breaker, readiness). ``init_hardening(app, config)`` is the single composition root: call it once, as the LAST statement in ``create_app``, after blueprints register (``setup_rate_limiting`` walks ``app.view_functions``, which is only populated once every blueprint is registered). """ from urllib.parse import urlparse from flask import Flask from limits import parse_many from core.config import Config from core.hardening.body_limit import setup_body_limit from core.hardening.client_ip import install_proxyfix from core.hardening.compression import setup_compression from core.hardening.edge_probe import install_edge_probe from core.hardening.errors import register_hardening_error_handlers from core.hardening.rate_limit import RateLimitMode, setup_rate_limiting from core.hardening.security_headers import setup_security_headers def validate_config(config: type[Config]) -> None: """Fail fast at boot on a hardening config value that would misbehave silently at runtime. Each of these would otherwise surface as a confusing failure well after boot (or, for a schemeless storage URI, possibly not until the first request hits the limiter): better to raise here, before any hardening middleware is installed. """ try: RateLimitMode(config.RATELIMIT_MODE) except ValueError: raise ValueError( f'RATELIMIT_MODE must be one of {[m.value for m in RateLimitMode]}, ' f'got {config.RATELIMIT_MODE!r}' ) from None try: parse_many(config.RATELIMIT_DEFAULT) except ValueError as e: raise ValueError( f'RATELIMIT_DEFAULT is not a valid limit string: {config.RATELIMIT_DEFAULT!r} ({e})' ) from None if not urlparse(config.RATELIMIT_STORAGE_URI).scheme: raise ValueError( f'RATELIMIT_STORAGE_URI must include a scheme (e.g. "memory://", "redis://..."), ' f'got {config.RATELIMIT_STORAGE_URI!r}' ) def init_hardening(app: Flask, config: type[Config]) -> None: """Install the full API hardening kit on ``app``. Order: ProxyFix first, so ``request.remote_addr`` is the real client IP before anything downstream keys on it; the leak-free error handlers next, so they are registered before any hardening middleware can raise the exceptions they catch; body limit, security headers, and compression (order-independent among themselves); rate limiting LAST, since it walks ``app.view_functions`` to resolve ``@rate_category`` markers. """ validate_config(config) install_proxyfix(app, config) install_edge_probe(app, config) register_hardening_error_handlers(app) setup_body_limit(app, config) setup_security_headers(app, config) setup_compression(app, config) setup_rate_limiting(app, config)