diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md index a0dd661c70..ef73284e42 100644 --- a/docs/usage/configuration/config_documentation.md +++ b/docs/usage/configuration/config_documentation.md @@ -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"`. diff --git a/schema/synapse-config.schema.yaml b/schema/synapse-config.schema.yaml index 865c85fdbe..374cf3d0f7 100644 --- a/schema/synapse-config.schema.yaml +++ b/schema/synapse-config.schema.yaml @@ -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. diff --git a/synapse/config/repository.py b/synapse/config/repository.py index efdc505659..8b23fbdfda 100644 --- a/synapse/config/repository.py +++ b/synapse/config/repository.py @@ -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"]) diff --git a/synapse/media/media_repository.py b/synapse/media/media_repository.py index 8ac6c0c75a..e50c303c85 100644 --- a/synapse/media/media_repository.py +++ b/synapse/media/media_repository.py @@ -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(