File size: 4,529 Bytes
66ee87e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
"""DecisionLab request security: body-size limit and response headers.

A plain ASGI middleware with no web-framework imports, so every rule is testable anywhere.

- No token and no login anywhere (operator ruling 2026-09-28, for the Hugging Face Space): the page and /api/* are
  open to whoever can reach the app. The remaining guards are request limits and response headers.
- /api/* request bodies over max_body bytes get 413, whether the size is declared or streamed.
- The lab's own responses (/, /static/*, /api/*) carry SECURITY_HEADERS. The page may be framed only by
  huggingface.co (a Space is shown inside a frame there), so there is no X-Frame-Options header.
- /gradio/* belongs to the mounted Gradio app, which sets its own headers and handles its own uploads.
"""
from __future__ import annotations

import json
import threading

GRADIO_PREFIX = "/gradio"

SECURITY_HEADERS = {
    "content-security-policy": (
        "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; "
        "font-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'; "
        "frame-ancestors 'self' https://huggingface.co https://*.hf.space"
    ),
    "x-content-type-options": "nosniff",
    "referrer-policy": "no-referrer",
    "cross-origin-resource-policy": "same-origin",
    "permissions-policy": "camera=(), microphone=(), geolocation=(), payment=(), usb=()",
}


class Gate:
    """A non-blocking counter: try_enter() admits up to `size` holders at once, then refuses."""

    def __init__(self, size: int):
        self._sem = threading.BoundedSemaphore(size)

    def try_enter(self) -> bool:
        return self._sem.acquire(blocking=False)

    def leave(self) -> None:
        self._sem.release()


def _header(scope, name: bytes) -> str:
    for k, v in scope.get("headers") or []:
        if k.lower() == name:
            return v.decode("latin-1")
    return ""


class SecurityMiddleware:
    def __init__(self, app, max_body: int):
        self.app, self.max_body = app, max_body

    async def __call__(self, scope, receive, send):
        if scope["type"] != "http":
            return await self.app(scope, receive, send)
        path = scope["path"]
        if path == GRADIO_PREFIX or path.startswith(GRADIO_PREFIX + "/"):
            return await self.app(scope, receive, send)
        is_api = path.startswith("/api/")

        async def send_secured(message):
            if message["type"] == "http.response.start":
                drop = set(SECURITY_HEADERS) | {"cache-control", "x-frame-options"}
                headers = [(k, v) for k, v in message.get("headers", []) if k.decode("latin-1").lower() not in drop]
                headers += [(k.encode(), v.encode()) for k, v in SECURITY_HEADERS.items()]
                # API answers are never stored; the page and its files are revalidated on every load.
                headers.append((b"cache-control", b"no-store" if is_api else b"no-cache"))
                message = dict(message, headers=headers)
            await send(message)

        async def reply(status: int, detail: str):
            body = json.dumps({"detail": detail}).encode()
            await send_secured({"type": "http.response.start", "status": status,
                                "headers": [(b"content-type", b"application/json")]})
            await send_secured({"type": "http.response.body", "body": body})

        if is_api and scope.get("method") in ("POST", "PUT", "PATCH"):
            too_big = f"Request body is over the {self.max_body}-byte limit."
            declared = _header(scope, b"content-length")
            if declared.isdigit() and int(declared) > self.max_body:
                return await reply(413, too_big)
            chunks, size = [], 0
            while True:
                msg = await receive()
                if msg["type"] != "http.request":
                    break
                size += len(msg.get("body", b""))
                if size > self.max_body:
                    return await reply(413, too_big)
                chunks.append(msg.get("body", b""))
                if not msg.get("more_body"):
                    break
            replay = [{"type": "http.request", "body": b"".join(chunks), "more_body": False}]

            async def receive_replay():
                return replay.pop(0) if replay else await receive()

            return await self.app(scope, receive_replay, send_secured)

        return await self.app(scope, receive, send_secured)