118
118
# Maximum allowed timeout_ms for download and thumbnail requests
119
119
MAXIMUM_ALLOWED_MAX_TIMEOUT_MS = 60_000
120
120
121
+ # The ETag header value to use for immutable media. This can be anything.
122
+ _IMMUTABLE_ETAG = "1"
123
+
121
124
122
125
def respond_404 (request : SynapseRequest ) -> None :
123
126
assert request .path is not None
@@ -224,12 +227,7 @@ def _quote(x: str) -> str:
224
227
225
228
request .setHeader (b"Content-Disposition" , disposition .encode ("ascii" ))
226
229
227
- # cache for at least a day.
228
- # XXX: we might want to turn this off for data we don't want to
229
- # recommend caching as it's sensitive or private - or at least
230
- # select private. don't bother setting Expires as all our
231
- # clients are smart enough to be happy with Cache-Control
232
- request .setHeader (b"Cache-Control" , b"public,max-age=86400,s-maxage=86400" )
230
+ _add_cache_headers (request )
233
231
234
232
if file_size is not None :
235
233
request .setHeader (b"Content-Length" , b"%d" % (file_size ,))
@@ -240,6 +238,26 @@ def _quote(x: str) -> str:
240
238
request .setHeader (b"X-Robots-Tag" , "noindex, nofollow, noarchive, noimageindex" )
241
239
242
240
241
+ def _add_cache_headers (request : Request ) -> None :
242
+ """Adds the appropriate cache headers to the response"""
243
+
244
+ # Cache on the client for at least a day.
245
+ #
246
+ # We set this to "public,s-maxage=0,proxy-revalidate" to allow CDNs to cache
247
+ # the media, so long as they "revalidate" the media on every request. By
248
+ # revalidate, we mean send the request to Synapse with a `If-None-Match`
249
+ # header, to which Synapse can either respond with a 304 if the user is
250
+ # authenticated/authorized, or a 401/403 if they're not.
251
+ request .setHeader (
252
+ b"Cache-Control" , b"public,max-age=86400,s-maxage=0,proxy-revalidate"
253
+ )
254
+
255
+ # Set an ETag header to allow requesters to use it in requests to check if
256
+ # the cache is still valid. Since media is immutable (though may be
257
+ # deleted), we just set this to a constant.
258
+ request .setHeader (b"ETag" , _IMMUTABLE_ETAG )
259
+
260
+
243
261
# separators as defined in RFC2616. SP and HT are handled separately.
244
262
# see _can_encode_filename_as_token.
245
263
_FILENAME_SEPARATOR_CHARS = {
@@ -336,13 +354,15 @@ def _quote(x: str) -> str:
336
354
337
355
from synapse .media .media_storage import MultipartFileConsumer
338
356
357
+ _add_cache_headers (request )
358
+
339
359
# note that currently the json_object is just {}, this will change when linked media
340
360
# is implemented
341
361
multipart_consumer = MultipartFileConsumer (
342
362
clock ,
343
363
request ,
344
364
media_type ,
345
- {},
365
+ {}, # Note: if we change this we need to change the returned ETag.
346
366
disposition ,
347
367
media_length ,
348
368
)
@@ -419,6 +439,46 @@ async def respond_with_responder(
419
439
finish_request (request )
420
440
421
441
442
+ def respond_with_304 (request : SynapseRequest ) -> None :
443
+ request .setResponseCode (304 )
444
+
445
+ # could alternatively use request.notifyFinish() and flip a flag when
446
+ # the Deferred fires, but since the flag is RIGHT THERE it seems like
447
+ # a waste.
448
+ if request ._disconnected :
449
+ logger .warning (
450
+ "Not sending response to request %s, already disconnected." , request
451
+ )
452
+ return None
453
+
454
+ _add_cache_headers (request )
455
+
456
+ request .finish ()
457
+
458
+
459
+ def check_for_cached_entry_and_respond (request : SynapseRequest ) -> bool :
460
+ """Check if the request has a conditional header that allows us to return a
461
+ 304 Not Modified response, and if it does, return a 304 response.
462
+
463
+ This handles clients and intermediary proxies caching media.
464
+ This method assumes that the user has already been
465
+ authorised to request the media.
466
+
467
+ Returns True if we have responded."""
468
+
469
+ # We've checked the user has access to the media, so we now check if it
470
+ # is a "conditional request" and we can just return a `304 Not Modified`
471
+ # response. Since media is immutable (though may be deleted), we just
472
+ # check this is the expected constant.
473
+ etag = request .getHeader ("If-None-Match" )
474
+ if etag == _IMMUTABLE_ETAG :
475
+ # Return a `304 Not modified`.
476
+ respond_with_304 (request )
477
+ return True
478
+
479
+ return False
480
+
481
+
422
482
class Responder (ABC ):
423
483
"""Represents a response that can be streamed to the requester.
424
484
0 commit comments