fix: harden school imports and add optional feedback diagnostics
This commit is contained in:
@@ -29,7 +29,8 @@ from starlette.concurrency import run_in_threadpool
|
||||
MIB = 1024 * 1024
|
||||
FILE_LIMIT = 8 * MIB
|
||||
TOTAL_FILE_LIMIT = 20 * MIB
|
||||
BODY_LIMIT = TOTAL_FILE_LIMIT + 64 * 1024
|
||||
LOG_LIMIT = 256 * 1024
|
||||
BODY_LIMIT = TOTAL_FILE_LIMIT + LOG_LIMIT + 64 * 1024
|
||||
ID_PATTERN = re.compile(r"[A-Za-z0-9_-]{16,128}\Z")
|
||||
ADDRESS_PATTERN = re.compile(r"[A-Za-z0-9._%+-]+@(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,63}\Z")
|
||||
|
||||
@@ -362,10 +363,15 @@ def safe_filename(filename, extension):
|
||||
|
||||
async def parse_submission(request):
|
||||
try:
|
||||
async with request.form(max_files=3, max_fields=5, max_part_size=20000) as form:
|
||||
fields, uploads = {}, []
|
||||
async with request.form(max_files=4, max_fields=5, max_part_size=20000) as form:
|
||||
fields, uploads, diagnostic_upload = {}, [], None
|
||||
for name, value in form.multi_items():
|
||||
if isinstance(value, UploadFile):
|
||||
if name == "diagnostics":
|
||||
if diagnostic_upload is not None:
|
||||
raise ApiError(422, "validation_error", "Only one diagnostic log is allowed.")
|
||||
diagnostic_upload = value
|
||||
continue
|
||||
if name != "attachments":
|
||||
raise ApiError(422, "validation_error", "Unexpected file field.")
|
||||
uploads.append(value)
|
||||
@@ -387,10 +393,12 @@ async def parse_submission(request):
|
||||
if kind == "problem":
|
||||
if not fields.get("message", "").strip() or "school" in fields:
|
||||
raise ApiError(422, "validation_error", "Problem feedback requires message and must omit school.")
|
||||
elif ("message" in fields or uploads or not fields.get("school", "").strip()
|
||||
elif ("message" in fields or uploads or diagnostic_upload or not fields.get("school", "").strip()
|
||||
or not fields.get("contact", "").strip()):
|
||||
raise ApiError(422, "validation_error", "Development requests require school and contact; omit message and attachments.")
|
||||
attachments, total = [], 0
|
||||
if len(uploads) > 3:
|
||||
raise ApiError(413, "upload_too_large", "Maximum three media files.")
|
||||
for upload in uploads:
|
||||
data = await upload.read(FILE_LIMIT + 1)
|
||||
total += len(data)
|
||||
@@ -401,6 +409,17 @@ async def parse_submission(request):
|
||||
if not valid:
|
||||
raise ApiError(415, "unsupported_media", "Attachment MIME type and file signature must match a supported image or video.")
|
||||
attachments.append((safe_filename(upload.filename, extension), mime, data))
|
||||
if diagnostic_upload is not None:
|
||||
data = await diagnostic_upload.read(LOG_LIMIT + 1)
|
||||
if len(data) > LOG_LIMIT:
|
||||
raise ApiError(413, "upload_too_large", "Diagnostic log exceeds 256 KiB.")
|
||||
try:
|
||||
text = data.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
raise ApiError(415, "unsupported_media", "Diagnostic log must be UTF-8 text.") from None
|
||||
if (diagnostic_upload.content_type or '').split(';')[0].strip().lower() != 'text/plain' or '\x00' in text or not text.startswith('TPSchedule diagnostic log\n'):
|
||||
raise ApiError(415, "unsupported_media", "Invalid diagnostic log.")
|
||||
attachments.append(('TPSchedule-diagnostics.txt', 'text/plain', data))
|
||||
return fields, attachments
|
||||
except HTTPException as exc:
|
||||
detail = str(exc.detail).lower()
|
||||
|
||||
Reference in New Issue
Block a user