diff --git a/synapse/media/thumbnailer.py b/synapse/media/thumbnailer.py
index ef6aa8ccf5..5d9afda322 100644
--- a/synapse/media/thumbnailer.py
+++ b/synapse/media/thumbnailer.py
@@ -34,6 +34,7 @@ from synapse.logging.opentracing import trace
from synapse.media._base import (
FileInfo,
ThumbnailInfo,
+ check_for_cached_entry_and_respond,
respond_404,
respond_with_file,
respond_with_multipart_responder,
@@ -67,6 +68,11 @@ class ThumbnailError(Exception):
class Thumbnailer:
FORMATS = {"image/jpeg": "JPEG", "image/png": "PNG"}
+ # Which image formats we allow Pillow to open.
+ # This should intentionally be kept restrictive, because the decoder of any
+ # format in this list becomes part of our trusted computing base.
+ PILLOW_FORMATS = ("jpeg", "png", "webp", "gif")
+
@staticmethod
def set_limits(max_image_pixels: int) -> None:
Image.MAX_IMAGE_PIXELS = max_image_pixels
@@ -76,7 +82,7 @@ class Thumbnailer:
self._closed = False
try:
- self.image = Image.open(input_path)
+ self.image = Image.open(input_path, formats=self.PILLOW_FORMATS)
except OSError as e:
# If an error occurs opening the image, a thumbnail won't be able to
# be generated.
@@ -206,7 +212,7 @@ class Thumbnailer:
def _encode_image(self, output_image: Image.Image, output_type: str) -> BytesIO:
output_bytes_io = BytesIO()
fmt = self.FORMATS[output_type]
- if fmt == "JPEG":
+ if fmt == "JPEG" or fmt == "PNG" and output_image.mode == "CMYK":
output_image = output_image.convert("RGB")
output_image.save(output_bytes_io, fmt, quality=80)
return output_bytes_io
@@ -259,6 +265,7 @@ class ThumbnailProvider:
media_storage: MediaStorage,
):
self.hs = hs
+ self.reactor = hs.get_reactor()
self.media_repo = media_repo
self.media_storage = media_storage
self.store = hs.get_datastores().main
@@ -288,6 +295,11 @@ class ThumbnailProvider:
if media_info.authenticated:
raise NotFoundError()
+ # Once we've checked auth we can return early if the media is cached on
+ # the client
+ if check_for_cached_entry_and_respond(request):
+ return
+
thumbnail_infos = await self.store.get_local_media_thumbnails(media_id)
await self._select_and_respond_with_thumbnail(
request,
@@ -328,6 +340,11 @@ class ThumbnailProvider:
if media_info.authenticated:
raise NotFoundError()
+ # Once we've checked auth we can return early if the media is cached on
+ # the client
+ if check_for_cached_entry_and_respond(request):
+ return
+
thumbnail_infos = await self.store.get_local_media_thumbnails(media_id)
for info in thumbnail_infos:
t_w = info.width == desired_width
@@ -347,7 +364,12 @@ class ThumbnailProvider:
if responder:
if for_federation:
await respond_with_multipart_responder(
- self.hs.get_clock(), request, responder, media_info
+ self.hs.get_clock(),
+ request,
+ responder,
+ info.type,
+ info.length,
+ None,
)
return
else:
@@ -359,7 +381,7 @@ class ThumbnailProvider:
logger.debug("We don't have a thumbnail of that size. Generating")
# Okay, so we generate one.
- file_path = await self.media_repo.generate_local_exact_thumbnail(
+ thumbnail_result = await self.media_repo.generate_local_exact_thumbnail(
media_id,
desired_width,
desired_height,
@@ -368,16 +390,21 @@ class ThumbnailProvider:
url_cache=bool(media_info.url_cache),
)
- if file_path:
+ if thumbnail_result:
+ file_path, file_info = thumbnail_result
+ assert file_info.thumbnail is not None
+
if for_federation:
await respond_with_multipart_responder(
self.hs.get_clock(),
request,
- FileResponder(open(file_path, "rb")),
- media_info,
+ FileResponder(self.hs, open(file_path, "rb")),
+ file_info.thumbnail.type,
+ file_info.thumbnail.length,
+ None,
)
else:
- await respond_with_file(request, desired_type, file_path)
+ await respond_with_file(self.hs, request, desired_type, file_path)
else:
logger.warning("Failed to generate thumbnail")
raise SynapseError(400, "Failed to generate thumbnail.")
@@ -415,6 +442,10 @@ class ThumbnailProvider:
respond_404(request)
return
+ # Check if the media is cached on the client, if so return 304.
+ if check_for_cached_entry_and_respond(request):
+ return
+
thumbnail_infos = await self.store.get_remote_media_thumbnails(
server_name, media_id
)
@@ -455,7 +486,7 @@ class ThumbnailProvider:
)
if file_path:
- await respond_with_file(request, desired_type, file_path)
+ await respond_with_file(self.hs, request, desired_type, file_path)
else:
logger.warning("Failed to generate thumbnail")
raise SynapseError(400, "Failed to generate thumbnail.")
@@ -494,6 +525,10 @@ class ThumbnailProvider:
if media_info.authenticated:
raise NotFoundError()
+ # Check if the media is cached on the client, if so return 304.
+ if check_for_cached_entry_and_respond(request):
+ return
+
thumbnail_infos = await self.store.get_remote_media_thumbnails(
server_name, media_id
)
@@ -579,7 +614,12 @@ class ThumbnailProvider:
if for_federation:
assert media_info is not None
await respond_with_multipart_responder(
- self.hs.get_clock(), request, responder, media_info
+ self.hs.get_clock(),
+ request,
+ responder,
+ file_info.thumbnail.type,
+ file_info.thumbnail.length,
+ None,
)
return
else:
@@ -633,7 +673,12 @@ class ThumbnailProvider:
if for_federation:
assert media_info is not None
await respond_with_multipart_responder(
- self.hs.get_clock(), request, responder, media_info
+ self.hs.get_clock(),
+ request,
+ responder,
+ file_info.thumbnail.type,
+ file_info.thumbnail.length,
+ None,
)
else:
await respond_with_responder(
|