fix(media): serve .zip archives through /api/media
MEDIA:/path/to/file.zip links always failed to open in chat. .zip was absent from MIME_MAP, so it resolved to application/octet-stream, which _SESSION_MEDIA_TOKEN_TYPES rejects — the browser rendered a download link that the server then refused. Map .zip to application/zip and add an _ARCHIVE_TYPES set to the session MEDIA: token allow-list. Archives are deliberately kept out of _INLINE_PREVIEW_TYPES and _INLINE_IMAGE_TYPES so they are always sent as Content-Disposition: attachment and never rendered inline.
This commit is contained in:
1 parent
06d28c08cb
commit
8c5ee95dab
3 files changed
+100
-1
No files matched your search
@@ -1046,6 +1046,7 @@ MIME_MAP = {
|
||||
".ico": "image/x-icon",
|
||||
".bmp": "image/bmp",
|
||||
".pdf": "application/pdf",
|
||||
".zip": "application/zip",
|
||||
".json": "application/json",
|
||||
".html": "text/html",
|
||||
".htm": "text/html",
|
||||
|
||||
+6
-1
@@ -20775,7 +20775,12 @@ def _handle_media(handler, parsed):
|
||||
"video/mp4", "video/quicktime", "video/webm", "video/ogg",
|
||||
"application/pdf",
|
||||
}
|
||||
_SESSION_MEDIA_TOKEN_TYPES = _INLINE_IMAGE_TYPES | _AUDIO_VIDEO_PDF_TYPES | {"text/html"}
|
||||
# Archives are download-only: never added to the inline-preview sets below,
|
||||
# so they always get Content-Disposition: attachment.
|
||||
_ARCHIVE_TYPES = {"application/zip"}
|
||||
_SESSION_MEDIA_TOKEN_TYPES = (
|
||||
_INLINE_IMAGE_TYPES | _AUDIO_VIDEO_PDF_TYPES | _ARCHIVE_TYPES | {"text/html"}
|
||||
)
|
||||
session_media_allowed = _session_media_token_allows_path(
|
||||
qs.get("session_id", [""])[0],
|
||||
target,
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Tests for zip archive delivery through /api/media.
|
||||
|
||||
Archives previously resolved to application/octet-stream (``.zip`` was absent
|
||||
from ``MIME_MAP``) and were therefore rejected by the session MEDIA: token
|
||||
allow-list, so a ``MEDIA:/path/to/file.zip`` link always failed to open.
|
||||
|
||||
Covers:
|
||||
1. ``.zip`` maps to ``application/zip`` in MIME_MAP
|
||||
2. ``application/zip`` is accepted by the session MEDIA: token allow-list
|
||||
3. ``application/zip`` is NOT an inline-preview type (must download, never render)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import unittest
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).parent.parent
|
||||
ROUTES_PY = (REPO_ROOT / "api" / "routes.py").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
class TestZipMimeMapping(unittest.TestCase):
|
||||
""".zip must resolve to a concrete archive type, not octet-stream."""
|
||||
|
||||
def test_zip_in_mime_map(self):
|
||||
from api.config import MIME_MAP
|
||||
|
||||
self.assertEqual(
|
||||
MIME_MAP.get(".zip"),
|
||||
"application/zip",
|
||||
".zip must map to application/zip so /api/media can serve archives",
|
||||
)
|
||||
|
||||
def test_zip_not_octet_stream(self):
|
||||
from api.config import MIME_MAP
|
||||
|
||||
self.assertNotEqual(
|
||||
MIME_MAP.get(".zip", "application/octet-stream"),
|
||||
"application/octet-stream",
|
||||
"octet-stream is rejected by the MEDIA: token allow-list",
|
||||
)
|
||||
|
||||
|
||||
class TestZipTokenAllowList(unittest.TestCase):
|
||||
"""The session MEDIA: token gate must accept archives."""
|
||||
|
||||
def test_archive_types_declared(self):
|
||||
self.assertIn(
|
||||
'_ARCHIVE_TYPES = {"application/zip"}',
|
||||
ROUTES_PY,
|
||||
"_ARCHIVE_TYPES must declare the servable archive types",
|
||||
)
|
||||
|
||||
def test_archive_types_in_session_token_types(self):
|
||||
match = re.search(
|
||||
r"_SESSION_MEDIA_TOKEN_TYPES\s*=\s*\(?\s*(.+?)\)?\n\s*session_media_allowed",
|
||||
ROUTES_PY,
|
||||
re.S,
|
||||
)
|
||||
self.assertIsNotNone(
|
||||
match, "could not locate the _SESSION_MEDIA_TOKEN_TYPES assignment"
|
||||
)
|
||||
self.assertIn(
|
||||
"_ARCHIVE_TYPES",
|
||||
match.group(1),
|
||||
"_SESSION_MEDIA_TOKEN_TYPES must include _ARCHIVE_TYPES",
|
||||
)
|
||||
|
||||
|
||||
class TestZipIsDownloadOnly(unittest.TestCase):
|
||||
"""Archives must never be offered for inline rendering."""
|
||||
|
||||
def test_archive_not_in_inline_preview_types(self):
|
||||
match = re.search(r"_INLINE_PREVIEW_TYPES\s*=\s*(.+)", ROUTES_PY)
|
||||
self.assertIsNotNone(match, "could not locate _INLINE_PREVIEW_TYPES")
|
||||
self.assertNotIn(
|
||||
"_ARCHIVE_TYPES",
|
||||
match.group(1),
|
||||
"archives must not be inline-previewable; they must download",
|
||||
)
|
||||
|
||||
def test_archive_not_in_inline_image_types(self):
|
||||
match = re.search(r"_INLINE_IMAGE_TYPES\s*=\s*\{(.+?)\}", ROUTES_PY, re.S)
|
||||
self.assertIsNotNone(match, "could not locate _INLINE_IMAGE_TYPES")
|
||||
self.assertNotIn(
|
||||
"zip",
|
||||
match.group(1),
|
||||
"zip must not be treated as an inline image type",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user