Files
KF-OnlineJudge/backend/src/kf_oj/api/body_limits.py
T
Kronecker 3152d59056 feat(problems): add bounded administrator package imports
Use live maintenance sessions, durable capacity reservations and streamed SHA-bound ZIP uploads. An ordinary capped import worker validates stored/deflated v1/v2 packages, persists immutable unpublished candidates and finite audit events, and recovers or releases capacity after interruption and expiry.

Validate actual PG/HTTP/worker/storage fault paths in an isolated capped VM (2 passed, 0 skipped), reuse unchanged actual migration roundtrip evidence, and pass affected backend/architecture checks plus the prior full engineering checks.
2026-10-11 22:19:38 +08:00

76 lines
3.0 KiB
Python

"""Bound request bytes and upload time before schema parsing can allocate large inputs."""
import asyncio
import re
from starlette.responses import JSONResponse
class BodyLimitMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] != "http" or scope["method"] in {"GET", "HEAD", "OPTIONS"}:
await self.app(scope, receive, send)
return
if scope["method"] == "PUT" and re.fullmatch(
r"/api/v1/admin/problem-imports/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/archive",
scope["path"],
):
# Only this exact endpoint can stream. Its live administrator gate runs
# before receive(), then its service enforces actual bytes/chunks/deadline.
await self.app(scope, receive, send)
return
limit = 8192 if scope["path"] == "/api/v1/auth/login" else 1024 * 1024
if scope["path"] == "/api/v1/submissions":
# Max256KiB UTF-8 source, worst-case JSON escaping plus bounded metadata.
limit = 6 * 256 * 1024 + 8192
headers = dict(scope.get("headers", []))
length = headers.get(b"content-length")
if length is not None:
try:
size = int(length)
if size < 0 or size > limit:
raise ValueError
except ValueError:
await JSONResponse({"detail": "request body outside byte policy"}, 413)(
scope, receive, send
)
return
# Buffer a finite body so errors can be returned before response headers are sent.
chunks, size, received_chunks = [], 0, 0
try:
async with asyncio.timeout(10):
while True:
message = await receive()
if message["type"] == "http.disconnect":
return
body = message.get("body", b"")
size += len(body)
received_chunks += 1
if size > limit or received_chunks > 1024:
await JSONResponse({"detail": "request body outside byte policy"}, 413)(
scope, receive, send
)
return
chunks.append(body)
if not message.get("more_body", False):
break
except TimeoutError:
await JSONResponse({"detail": "request body deadline exceeded"}, 408)(
scope, receive, send
)
return
body = b"".join(chunks)
delivered = False
async def bounded_receive():
nonlocal delivered
if not delivered:
delivered = True
return {"type": "http.request", "body": body, "more_body": False}
return await receive()
await self.app(scope, bounded_receive, send)