"""In-memory per-IP rate limiting for pre-authentication routes. Same spirit as painel-clientes-web's `app/rate_limit.py`: a single-process in-memory counter, fine because the Hub runs as one container behind a trusted reverse proxy (FORWARDED_ALLOW_IPS), never multiple replicas behind a shared load balancer. A process restart resets the history — accepted, same tradeoff as that panel's SESSION_SECRET. Used for the two public, pre-authentication Agent-distribution routes (GET /dl/agent/{token}, POST /v1/agents/enroll): invalid attempts count against the caller's IP; a legitimate Agent's own successful traffic never gets counted, so it can never push other installations over the limit. """ import time MAX_ATTEMPTS = 10 WINDOW_SECONDS = 5 * 60 _attempts: dict[tuple[str, str], list[float]] = {} def _recent(bucket: str, origin: str, now: float) -> list[float]: cutoff = now - WINDOW_SECONDS return [t for t in _attempts.get((bucket, origin), []) if t > cutoff] def blocked(bucket: str, origin: str, now: float | None = None) -> bool: moment = now if now is not None else time.time() return len(_recent(bucket, origin, moment)) >= MAX_ATTEMPTS def record_attempt(bucket: str, origin: str, now: float | None = None) -> None: moment = now if now is not None else time.time() recent = _recent(bucket, origin, moment) recent.append(moment) _attempts[(bucket, origin)] = recent