Download http_guard.py from GaryLeckey/aureon-public: direct link, hf CLI and curl.
- Browser
- Download file 15.5 kB
-
https://huggingface.co/GaryLeckey/aureon-public/resolve/main/http_guard.py
- Command line
-
hf download hf://GaryLeckey/aureon-public/http_guard.py
-
curl -L -o http_guard.py https://huggingface.co/GaryLeckey/aureon-public/resolve/main/http_guard.py
15.5 kB
| """Streamable HTTP bind rule, bearer check, and per-client limits. | |
| No key is read from disk. AUREON_PUBLIC_API_KEY is an optional runtime | |
| environment variable. When it is unset, the public surface is served only if | |
| AUREON_PUBLIC_OPEN=1. With neither one set, the listener must be 127.0.0.1. | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import hashlib | |
| import hmac | |
| import json | |
| import os | |
| import time | |
| from collections import OrderedDict | |
| from collections.abc import Callable | |
| from dataclasses import dataclass | |
| LOCAL_BIND = "127.0.0.1" | |
| _DEFAULT_RATE_LIMIT = 120 | |
| _DEFAULT_RATE_WINDOW_S = 60.0 | |
| _DEFAULT_MAX_BODY = 1_048_576 | |
| _DEFAULT_TIMEOUT_S = 30.0 | |
| _DEFAULT_TRUSTED_PROXY_HOPS = 1 | |
| _DEFAULT_MAX_CLIENTS = 4096 | |
| # The public worker pool is 4. One live session holds the SSE GET and a POST | |
| # at the same time, so the cap stays at that same low number. | |
| _DEFAULT_MAX_INFLIGHT = 4 | |
| class HttpLimits: | |
| """Limits for one server process. Values come from the environment.""" | |
| api_key: str | |
| max_body_bytes: int | |
| rate_limit: int | |
| rate_window_s: float | |
| timeout_s: float | |
| trusted_proxy_hops: int = _DEFAULT_TRUSTED_PROXY_HOPS | |
| max_clients: int = _DEFAULT_MAX_CLIENTS | |
| max_inflight: int = _DEFAULT_MAX_INFLIGHT | |
| class RateLimiter: | |
| """Fixed window of request starts, keyed by client id. | |
| Expired windows are dropped. The table keeps at most ``max_clients`` | |
| ids and evicts the least recently used id when a new one arrives. | |
| """ | |
| def __init__( | |
| self, | |
| limit: int, | |
| window_s: float, | |
| max_clients: int = _DEFAULT_MAX_CLIENTS, | |
| now: Callable[[], float] | None = None, | |
| ) -> None: | |
| self.limit = limit | |
| self.window_s = window_s | |
| self.max_clients = max_clients | |
| self._now = now or time.monotonic | |
| self._hits: OrderedDict[str, list[float]] = OrderedDict() | |
| def allow(self, client_id: str) -> bool: | |
| now = self._now() | |
| self._evict_expired(now) | |
| if client_id in self._hits: | |
| self._hits.move_to_end(client_id) | |
| elif len(self._hits) >= self.max_clients: | |
| self._hits.popitem(last=False) | |
| hits = [stamp for stamp in self._hits.get(client_id, []) if now - stamp < self.window_s] | |
| if len(hits) >= self.limit: | |
| self._hits[client_id] = hits | |
| self._hits.move_to_end(client_id) | |
| return False | |
| hits.append(now) | |
| self._hits[client_id] = hits | |
| self._hits.move_to_end(client_id) | |
| return True | |
| def _evict_expired(self, now: float) -> None: | |
| expired = [ | |
| client_id | |
| for client_id, hits in self._hits.items() | |
| if not hits or now - hits[-1] >= self.window_s | |
| ] | |
| for client_id in expired: | |
| del self._hits[client_id] | |
| def _positive_int(name: str, default: int) -> int: | |
| raw = os.environ.get(name, "").strip() | |
| if not raw: | |
| return default | |
| try: | |
| value = int(raw) | |
| except ValueError as exc: | |
| raise SystemExit(f"{name} must be a positive integer") from exc | |
| if value <= 0: | |
| raise SystemExit(f"{name} must be a positive integer") | |
| return value | |
| def _positive_float(name: str, default: float) -> float: | |
| raw = os.environ.get(name, "").strip() | |
| if not raw: | |
| return default | |
| try: | |
| value = float(raw) | |
| except ValueError as exc: | |
| raise SystemExit(f"{name} must be a positive number") from exc | |
| if value <= 0: | |
| raise SystemExit(f"{name} must be a positive number") | |
| return value | |
| def load_http_limits() -> HttpLimits: | |
| """Read the optional key and the limit environment variables.""" | |
| return HttpLimits( | |
| api_key=os.environ.get("AUREON_PUBLIC_API_KEY", "").strip(), | |
| max_body_bytes=_positive_int("AUREON_PUBLIC_MAX_BODY_BYTES", _DEFAULT_MAX_BODY), | |
| rate_limit=_positive_int("AUREON_PUBLIC_RATE_LIMIT", _DEFAULT_RATE_LIMIT), | |
| rate_window_s=_positive_float("AUREON_PUBLIC_RATE_WINDOW_S", _DEFAULT_RATE_WINDOW_S), | |
| timeout_s=_positive_float("AUREON_PUBLIC_REQUEST_TIMEOUT_S", _DEFAULT_TIMEOUT_S), | |
| trusted_proxy_hops=_nonnegative_int( | |
| "AUREON_PUBLIC_TRUSTED_PROXY_HOPS", | |
| _DEFAULT_TRUSTED_PROXY_HOPS, | |
| ), | |
| max_clients=_positive_int("AUREON_PUBLIC_RATE_MAX_CLIENTS", _DEFAULT_MAX_CLIENTS), | |
| max_inflight=_positive_int("AUREON_PUBLIC_MAX_INFLIGHT", _DEFAULT_MAX_INFLIGHT), | |
| ) | |
| def _nonnegative_int(name: str, default: int) -> int: | |
| raw = os.environ.get(name, "").strip() | |
| if not raw: | |
| return default | |
| try: | |
| value = int(raw) | |
| except ValueError as exc: | |
| raise SystemExit(f"{name} must be a non-negative integer") from exc | |
| if value < 0: | |
| raise SystemExit(f"{name} must be a non-negative integer") | |
| return value | |
| def public_bind_allowed(host: str) -> bool: | |
| """True when this host may listen. | |
| A bearer key or AUREON_PUBLIC_OPEN=1 allows the requested host. Otherwise | |
| only 127.0.0.1 is allowed. | |
| """ | |
| if os.environ.get("AUREON_PUBLIC_API_KEY", "").strip(): | |
| return True | |
| if os.environ.get("AUREON_PUBLIC_OPEN") == "1": | |
| return True | |
| return host == LOCAL_BIND | |
| def resolve_http_bind(host: str) -> str: | |
| """Return host, or refuse a non-local bind when the surface is locked.""" | |
| if public_bind_allowed(host): | |
| return host | |
| raise SystemExit( | |
| "Streamable HTTP binds to 127.0.0.1 unless AUREON_PUBLIC_API_KEY is set " | |
| "or AUREON_PUBLIC_OPEN=1." | |
| ) | |
| def _header_map(scope: dict) -> dict[str, str]: | |
| found: dict[str, str] = {} | |
| for key, value in scope.get("headers") or []: | |
| found[key.decode("latin1").lower()] = value.decode("latin1") | |
| return found | |
| def _bearer(authorization: str) -> str | None: | |
| parts = authorization.split(" ", 1) | |
| if len(parts) != 2 or parts[0].lower() != "bearer": | |
| return None | |
| token = parts[1].strip() | |
| if not token: | |
| return None | |
| return token | |
| def _key_matches(presented: str, expected: str) -> bool: | |
| presented_bytes = presented.encode("utf-8") | |
| expected_bytes = expected.encode("utf-8") | |
| if len(presented_bytes) != len(expected_bytes): | |
| return False | |
| return hmac.compare_digest(presented_bytes, expected_bytes) | |
| def _socket_peer(scope: dict) -> str: | |
| client = scope.get("client") | |
| if isinstance(client, (list, tuple)) and client and client[0]: | |
| return str(client[0]) | |
| return "unknown" | |
| def _forwarded_hops(value: str) -> list[str]: | |
| return [part.strip() for part in value.split(",") if part.strip()] | |
| def client_address(scope: dict, headers: dict[str, str], trusted_proxy_hops: int) -> str: | |
| """Client address behind ``trusted_proxy_hops`` trusted proxies. | |
| ``AUREON_PUBLIC_TRUSTED_PROXY_HOPS`` defaults to 1. When the header is | |
| present and has at least that many values, the client is | |
| ``hops[-trusted_proxy_hops]``. With the default, that is the last value, | |
| including a header that contains only one address. A fake first hop does | |
| not become its own bucket. Zero trusted hops ignores the header and uses | |
| the socket peer. The real Hugging Face header shape still has to be | |
| confirmed on the private Space. | |
| """ | |
| peer = _socket_peer(scope) | |
| note_forwarded_shape(headers.get("x-forwarded-for", "")) | |
| if trusted_proxy_hops <= 0: | |
| return peer | |
| hops = _forwarded_hops(headers.get("x-forwarded-for", "")) | |
| if len(hops) < trusted_proxy_hops: | |
| return peer | |
| return hops[-trusted_proxy_hops] | |
| _SHAPE_CAP = 32 | |
| _forwarded_shapes: list[dict[str, object]] = [] | |
| def _hop_kind(token: str) -> str: | |
| """Classify one hop. The returned label is not the address.""" | |
| host = token.strip() | |
| if host.startswith("[") and "]" in host: | |
| host = host[1 : host.index("]")] | |
| elif host.count(":") == 1 and host.rsplit(":", 1)[-1].isdigit(): | |
| host = host.rsplit(":", 1)[0] | |
| try: | |
| import ipaddress | |
| parsed = ipaddress.ip_address(host) | |
| except ValueError: | |
| return "other" | |
| return "ipv4" if parsed.version == 4 else "ipv6" | |
| def note_forwarded_shape(value: str) -> None: | |
| """Record hop count and hop kinds when the diagnostic is on. | |
| ``AUREON_PUBLIC_FORWARDED_DIAG=1`` opts in. The record does not store the | |
| header or the addresses. | |
| """ | |
| if os.environ.get("AUREON_PUBLIC_FORWARDED_DIAG") != "1": | |
| return | |
| hops = _forwarded_hops(value) | |
| _forwarded_shapes.append({"hops": len(hops), "kinds": [_hop_kind(hop) for hop in hops]}) | |
| del _forwarded_shapes[:-_SHAPE_CAP] | |
| def forwarded_shapes() -> list[dict[str, object]]: | |
| """Copies of the diagnostic shapes. Empty unless the diagnostic is on.""" | |
| return [dict(item) for item in _forwarded_shapes] | |
| def clear_forwarded_shapes() -> None: | |
| _forwarded_shapes.clear() | |
| def _client_id(scope: dict, headers: dict[str, str], limits: HttpLimits) -> str: | |
| """Bucket by address. The bearer is the id only when it matches the configured key. | |
| Open mode has no configured key, so a presented bearer is never the client id. | |
| """ | |
| address = client_address(scope, headers, limits.trusted_proxy_hops) | |
| if limits.api_key: | |
| presented = _bearer(headers.get("authorization", "")) | |
| if presented is not None and _key_matches(presented, limits.api_key): | |
| digest = hashlib.sha256(presented.encode("utf-8")).hexdigest() | |
| return f"bearer:{digest}" | |
| return f"ip:{address}" | |
| def _content_length(headers: dict[str, str]) -> int | None: | |
| raw = headers.get("content-length", "").strip() | |
| if not raw or not raw.isdigit(): | |
| return None | |
| return int(raw) | |
| async def _reject(send, status: int, error: str, *, retry_after: str | None = None) -> None: | |
| body = json.dumps({"error": error}).encode("utf-8") | |
| header_list = [ | |
| (b"content-type", b"application/json"), | |
| (b"content-length", str(len(body)).encode("ascii")), | |
| ] | |
| if status == 401: | |
| header_list.append((b"www-authenticate", b"Bearer")) | |
| if status == 405: | |
| header_list.append((b"allow", b"POST")) | |
| if retry_after is not None: | |
| header_list.append((b"retry-after", retry_after.encode("ascii"))) | |
| await send({"type": "http.response.start", "status": status, "headers": header_list}) | |
| await send({"type": "http.response.body", "body": body}) | |
| class PublicHttpMiddleware: | |
| """ASGI wrapper for the Streamable HTTP app. Lifespan scopes pass through.""" | |
| def __init__(self, app, limits: HttpLimits, limiter: RateLimiter | None = None) -> None: | |
| self.app = app | |
| self.limits = limits | |
| self.limiter = limiter if limiter is not None else RateLimiter( | |
| limits.rate_limit, | |
| limits.rate_window_s, | |
| max_clients=limits.max_clients, | |
| ) | |
| self._inflight = 0 | |
| self._inflight_lock = asyncio.Lock() | |
| async def __call__(self, scope, receive, send) -> None: | |
| if scope.get("type") != "http": | |
| await self.app(scope, receive, send) | |
| return | |
| path = scope.get("path") or "" | |
| if path != "/mcp" and not path.startswith("/mcp/"): | |
| await self.app(scope, receive, send) | |
| return | |
| headers = _header_map(scope) | |
| client_id = _client_id(scope, headers, self.limits) | |
| if not self.limiter.allow(client_id): | |
| retry_after = str(max(1, int(self.limits.rate_window_s))) | |
| await _reject(send, 429, "rate_limit", retry_after=retry_after) | |
| return | |
| if self.limits.api_key: | |
| presented = _bearer(headers.get("authorization", "")) | |
| if presented is None or not _key_matches(presented, self.limits.api_key): | |
| await _reject(send, 401, "unauthorized") | |
| return | |
| method = scope.get("method", "GET") | |
| if method == "GET": | |
| # Streamable HTTP's long-lived event stream. Stateless JSON mode | |
| # does not use it. Answering here keeps it off the call slots. | |
| await _reject(send, 405, "method_not_allowed") | |
| return | |
| if method == "POST": | |
| declared = _content_length(headers) | |
| if declared is not None and declared > self.limits.max_body_bytes: | |
| await _reject(send, 413, "body_too_large") | |
| return | |
| receive = await _bounded_receive( | |
| receive, | |
| self.limits.max_body_bytes, | |
| send, | |
| self.limits.timeout_s, | |
| ) | |
| if receive is None: | |
| return | |
| if not await self._acquire_inflight(): | |
| await _reject(send, 503, "busy") | |
| return | |
| try: | |
| if method != "POST": | |
| await self.app(scope, receive, send) | |
| return | |
| started = False | |
| async def send_wrapper(message) -> None: | |
| nonlocal started | |
| if message.get("type") == "http.response.start": | |
| started = True | |
| await send(message) | |
| try: | |
| await asyncio.wait_for( | |
| self.app(scope, receive, send_wrapper), | |
| timeout=self.limits.timeout_s, | |
| ) | |
| except TimeoutError: | |
| if started: | |
| raise | |
| await _reject(send, 504, "timeout") | |
| finally: | |
| await self._release_inflight() | |
| async def _acquire_inflight(self) -> bool: | |
| async with self._inflight_lock: | |
| if self._inflight >= self.limits.max_inflight: | |
| return False | |
| self._inflight += 1 | |
| return True | |
| async def _release_inflight(self) -> None: | |
| async with self._inflight_lock: | |
| self._inflight -= 1 | |
| async def _bounded_receive(receive, max_body: int, send, timeout_s: float): | |
| """Buffer a POST body and reject it once the arrived bytes pass the cap. | |
| Chunked requests omit content-length. Every message is counted until | |
| ``more_body`` is false or the client disconnects. Empty chunks do not | |
| stop the count, and the unread remainder is not passed through. | |
| ``timeout_s`` bounds the whole read. An idle or unfinished body returns | |
| HTTP 408 instead of handing the stream to the application. | |
| """ | |
| buffered: list[dict] = [] | |
| total = 0 | |
| deadline = asyncio.get_running_loop().time() + timeout_s | |
| while True: | |
| remaining = deadline - asyncio.get_running_loop().time() | |
| if remaining <= 0: | |
| await _reject(send, 408, "body_timeout") | |
| return None | |
| try: | |
| message = await asyncio.wait_for(receive(), timeout=remaining) | |
| except TimeoutError: | |
| await _reject(send, 408, "body_timeout") | |
| return None | |
| kind = message.get("type") | |
| if kind != "http.request": | |
| buffered.append(message) | |
| if kind == "http.disconnect": | |
| break | |
| continue | |
| chunk = message.get("body") or b"" | |
| total += len(chunk) | |
| if total > max_body: | |
| await _reject(send, 413, "body_too_large") | |
| return None | |
| buffered.append(message) | |
| if not message.get("more_body", False): | |
| break | |
| index = 0 | |
| async def replay(): | |
| nonlocal index | |
| if index < len(buffered): | |
| message = buffered[index] | |
| index += 1 | |
| return message | |
| return await receive() | |
| return replay | |