Spaces:
Running on Zero
Running on Zero
Download engine/webguard.py from AngeloUNIMI/document_exam_trainer: direct link, hf CLI and curl.
- Browser
- Download file 6.52 kB
-
https://huggingface.co/spaces/AngeloUNIMI/document_exam_trainer/resolve/main/engine/webguard.py
- Command line
-
hf download hf://spaces/AngeloUNIMI/document_exam_trainer/engine/webguard.py
-
curl -L -o webguard.py https://huggingface.co/spaces/AngeloUNIMI/document_exam_trainer/resolve/main/engine/webguard.py
6.52 kB
| """Browser-bound uploads and safe audio playback for the Gradio application. | |
| Gradio's cache is not a private file store by itself. Register paths from real | |
| upload responses, require the uploading browser on input, and permit only owned | |
| recording downloads. Never expose primary/supporting documents or workspaces. | |
| """ | |
| from __future__ import annotations | |
| from http.cookies import SimpleCookie | |
| import hashlib | |
| import json | |
| import os | |
| from pathlib import Path | |
| import re | |
| import secrets | |
| import threading | |
| import time | |
| from urllib.parse import unquote | |
| from starlette.responses import PlainTextResponse | |
| COOKIE_NAME = 'det_browser' | |
| AUDIO_EXTENSIONS = {'.wav', '.mp3', '.m4a', '.ogg', '.oga', '.flac', '.webm', '.opus', '.aac', '.mp4'} | |
| def cookie_owner(headers) -> str: | |
| """Read a capability, not an identity; do not accept a session hash alone.""" | |
| try: | |
| cookie = SimpleCookie() | |
| cookie.load(headers.get('cookie', '')) | |
| value = cookie[COOKIE_NAME].value if COOKIE_NAME in cookie else '' | |
| if not re.fullmatch(r'[0-9a-f]{64}', value): | |
| return '' | |
| # On local password installations, bind upload capabilities to the | |
| # current Gradio login too, so a second login in the same browser does | |
| # not inherit the previous user's cached uploads. | |
| if os.getenv('APP_EDITION') == 'local': | |
| auth = sorted((k, cookie[k].value) for k in cookie if k.startswith('access-token-')) | |
| if auth: | |
| return hashlib.sha256((value + repr(auth)).encode()).hexdigest() | |
| return value | |
| except (TypeError, AttributeError, ValueError): | |
| return '' | |
| class UploadRegistry: | |
| def __init__(self, root: Path, ttl: float = 3600, max_entries: int = 10000): | |
| self.root = root.resolve() | |
| self.ttl = ttl | |
| self.max_entries = max_entries | |
| self._paths: dict[tuple[str, str], float] = {} | |
| self._lock = threading.Lock() | |
| def register(self, browser: str, paths: list[str]) -> None: | |
| now = time.monotonic() | |
| with self._lock: | |
| self._paths = {k: t for k, t in self._paths.items() if now - t <= self.ttl} | |
| for raw in paths: | |
| path = Path(raw).resolve() | |
| if path.is_relative_to(self.root): | |
| self._paths[(browser, str(path))] = now | |
| if len(self._paths) > self.max_entries: | |
| # Bounded metadata even when a public demo is used heavily. | |
| ordered = sorted(self._paths, key=self._paths.get) | |
| for key in ordered[:len(self._paths) - self.max_entries]: | |
| self._paths.pop(key, None) | |
| def owns(self, browser: str, path: Path) -> bool: | |
| if not browser or not path.resolve().is_relative_to(self.root): | |
| return False | |
| with self._lock: | |
| created = self._paths.get((browser, str(path.resolve()))) | |
| return created is not None and time.monotonic() - created <= self.ttl | |
| def revoke(self, browser: str) -> None: | |
| with self._lock: | |
| self._paths = {key: t for key, t in self._paths.items() if key[0] != browser} | |
| UPLOADS = UploadRegistry(Path(os.environ.get('GRADIO_TEMP_DIR', '/tmp/document-exam-trainer-uploads'))) | |
| class PrivateUploads: | |
| def __init__(self, app): | |
| self.app = app | |
| async def __call__(self, scope, receive, send): | |
| if scope['type'] != 'http': | |
| return await self.app(scope, receive, send) | |
| headers = {key.decode('latin1').lower(): value.decode('latin1') for key, value in scope.get('headers', [])} | |
| browser = cookie_owner(headers) | |
| fresh = not browser | |
| if fresh: | |
| browser = secrets.token_hex(32) | |
| path = scope.get('path', '') | |
| method = scope.get('method', 'GET') | |
| if method in ('GET', 'HEAD'): | |
| match = re.search(r'/(?:gradio_api/)?file(?:=|/)(.*)$', path) | |
| if match: | |
| raw_path = unquote(match.group(1)) | |
| candidate = Path(raw_path).resolve() | |
| permitted = (candidate.suffix.lower() in AUDIO_EXTENSIONS and | |
| UPLOADS.owns(browser, candidate) and candidate.is_file()) | |
| if not permitted: | |
| return await PlainTextResponse('This file is not available to this browser.', status_code=403)(scope, receive, send) | |
| if '/gradio_api/stream/' in path: | |
| return await PlainTextResponse('Streaming file downloads are disabled.', status_code=403)(scope, receive, send) | |
| if path.rstrip('/') == '/logout': | |
| UPLOADS.revoke(browser) | |
| is_upload = method == 'POST' and path.rstrip('/').endswith('/gradio_api/upload') | |
| response_status = 0 | |
| response_body = bytearray() | |
| oversized = False | |
| async def send_guarded(message): | |
| nonlocal response_status, oversized | |
| if message['type'] == 'http.response.start': | |
| response_status = message['status'] | |
| if fresh: | |
| # HF embeds the app on another site. CHIPS-capable browsers | |
| # can use this partitioned cookie there; standalone is fallback. | |
| secure = scope.get('scheme') == 'https' or headers.get('x-forwarded-proto', '').split(',')[0].strip() == 'https' | |
| attributes = 'SameSite=None; Secure; Partitioned' if secure else 'SameSite=Lax' | |
| value = f'{COOKIE_NAME}={browser}; Path=/; Max-Age=86400; HttpOnly; {attributes}' | |
| message = {**message, 'headers': [*message.get('headers', []), (b'set-cookie', value.encode('ascii'))]} | |
| elif is_upload and response_status == 200 and message['type'] == 'http.response.body': | |
| if not oversized: | |
| response_body.extend(message.get('body', b'')) | |
| if len(response_body) > 1024 * 1024: | |
| oversized = True | |
| response_body.clear() | |
| if not oversized and not message.get('more_body', False): | |
| try: | |
| paths = json.loads(response_body) | |
| if isinstance(paths, list) and all(isinstance(p, str) for p in paths): | |
| UPLOADS.register(browser, paths) | |
| except (ValueError, TypeError): | |
| pass # Fail closed: unregistered paths cannot reach the engine. | |
| await send(message) | |
| await self.app(scope, receive, send_guarded) | |