1
0

Configuration options for media redirect

This commit is contained in:
Quentin Gliech
2025-08-12 15:58:40 +02:00
parent 585e4d3ed6
commit ce12db5378
4 changed files with 134 additions and 6 deletions

View File

@@ -2182,6 +2182,39 @@ media_upload_limits:
max_size: 500M
```
---
### `media_redirect`
*(object)* When enabled, Synapse will use HTTP redirect responses to serve media instead of directly serving the media from the media store. This can help with caching, but requires /_synapse/media/* to be routed to the media worker.
This setting has the following sub-options:
* `enabled` (boolean): Enables the media redirect feature. If enabled, you must specify a `media_redirect.secret` or `media_redirect.secret_path`. Defaults to `false`.
* `secret` (string): Secret used to sign media redirect URLs. This must be set if `media_redirect.enabled` is set. Defaults to `null`.
* `secret_path` (string): An alternative to `media_redirect.secret` that specifies a file containing the secret. Defaults to `null`.
* `ttl` (duration): How long the redirect URLs should be valid for. Defaults to `"10m"`.
Default configuration:
```yaml
media_redirect:
enabled: false
```
Example configurations:
```yaml
media_redirect:
enabled: true
secret: aiCh9gu4Zahvueveisooquu7chaiw9Ee
```
```yaml
media_redirect:
enabled: true
secret_path: /path/to/secrets/file
```
---
### `max_image_pixels`
*(byte size)* Maximum number of pixels that will be thumbnailed. Defaults to `"32M"`.

View File

@@ -2433,6 +2433,44 @@ properties:
max_size: 100M
- time_period: 1w
max_size: 500M
media_redirect:
type: object
description: >-
When enabled, Synapse will use HTTP redirect responses to serve media
instead of directly serving the media from the media store. This can help
with caching, but requires /_synapse/media/* to be routed to the media
worker.
properties:
enabled:
type: boolean
description: >-
Enables the media redirect feature. If enabled, you must specify a
`media_redirect.secret` or `media_redirect.secret_path`.
default: false
secret:
type: string
description: >-
Secret used to sign media redirect URLs. This must be set if
`media_redirect.enabled` is set.
default: null
secret_path:
type: string
description: >-
An alternative to `media_redirect.secret` that specifies a file
containing the secret.
default: null
ttl:
$ref: "#/$defs/duration"
description: >-
How long the redirect URLs should be valid for.
default: 10m
default:
enabled: false
examples:
- enabled: true
secret: aiCh9gu4Zahvueveisooquu7chaiw9Ee
- enabled: true
secret_path: /path/to/secrets/file
max_image_pixels:
$ref: "#/$defs/bytes"
description: Maximum number of pixels that will be thumbnailed.

View File

@@ -21,7 +21,7 @@
import logging
import os
from typing import Any, Dict, List, Tuple
from typing import Any, Dict, List, Optional, Tuple
import attr
@@ -30,7 +30,7 @@ from synapse.types import JsonDict
from synapse.util.check_dependencies import check_requirements
from synapse.util.module_loader import load_module
from ._base import Config, ConfigError
from ._base import Config, ConfigError, read_file
logger = logging.getLogger(__name__)
@@ -130,7 +130,9 @@ class MediaUploadLimit:
class ContentRepositoryConfig(Config):
section = "media"
def read_config(self, config: JsonDict, **kwargs: Any) -> None:
def read_config(
self, config: JsonDict, allow_secrets_in_config: bool, **kwargs: Any
) -> None:
# Only enable the media repo if either the media repo is enabled or the
# current worker app is the media repo.
if (
@@ -290,6 +292,55 @@ class ContentRepositoryConfig(Config):
self.enable_authenticated_media = config.get("enable_authenticated_media", True)
redirect_config = config.get("media_redirect", {})
if redirect_config is None:
redirect_config = {}
if not isinstance(redirect_config, dict):
raise ConfigError(
"`media_redirect` must be a dictionary",
("media_redirect",),
)
# Whether we should use a redirect to /_synapse/media/* when serving
# media for better caching. This requires this endpoint to be routed to
# the media worker.
self.use_redirect = redirect_config.get("enabled", False)
redirect_secret = redirect_config.get("secret")
if redirect_secret and not allow_secrets_in_config:
raise ConfigError(
"Config options that expect an in-line secret as value are disabled",
("media_redirect", "secret"),
)
if redirect_secret is not None and not isinstance(redirect_secret, str):
raise ConfigError(
"`media_redirect.secret` must be a string.",
("media_redirect", "secret"),
)
redirect_secret_path = redirect_config.get("secret_path")
if redirect_secret_path:
if redirect_secret:
raise ConfigError(
"You have configured both `media_redirect.secret` and `media_redirect.secret_path`.\n"
"These are mutually incompatible.",
("media_redirect", "secret_path"),
)
redirect_secret = read_file(
redirect_secret_path, ("media_redirect", "secret_path")
).strip()
self.redirect_secret: Optional[bytes] = (
redirect_secret.encode("utf-8") if redirect_secret else None
)
if self.use_redirect and self.redirect_secret is None:
raise ConfigError(
"You have configured `media_redirect.enabled` but not set `media_redirect.secret` or `media_redirect.secret_path`."
)
self.redirect_ttl_ms = self.parse_duration(redirect_config.get("ttl", "10m"))
self.media_upload_limits: List[MediaUploadLimit] = []
for limit_config in config.get("media_upload_limits", []):
time_period_ms = self.parse_duration(limit_config["time_period"])

View File

@@ -126,9 +126,7 @@ class MediaRepository:
cfg=hs.config.ratelimiting.remote_media_downloads,
)
self._media_request_signature_secret = (
b"supersecret" # TODO: make this configurable
)
self._media_request_signature_secret = hs.config.media.redirect_secret
# List of StorageProviders where we should search for media and
# potentially upload to.
@@ -1562,6 +1560,10 @@ class MediaRepository:
This currently uses a HMAC-SHA256 signature encoded as hex, but this could
be swapped to an asymmetric signature.
"""
assert self._media_request_signature_secret is not None, (
"media request signature secret not set"
)
# XXX: alternatively, we could do multiple rounds of HMAC with the
# different segments, like AWS SigV4 does
bytes_payload = "|".join(payload).encode("utf-8")
@@ -1581,6 +1583,10 @@ class MediaRepository:
Returns True if the signature is valid, False otherwise.
"""
# In case there is no secret, we can't verify the signature
if self._media_request_signature_secret is None:
return False
bytes_payload = "|".join(payload).encode("utf-8")
decoded_signature = bytes.fromhex(signature)
computed_signature = hmac.digest(