Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

MSC2246 async uploads #12484

Closed
1 change: 1 addition & 0 deletions changelog.d/12484.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Experimental support for asynchronous uploads as defined by [MSC2246](https://github.com/matrix-org/matrix-spec-proposals/pull/2246). Contributed by @sumnerevans at Beeper.
9 changes: 9 additions & 0 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,10 @@ log_config: "CONFDIR/SERVERNAME.log.config"
#rc_third_party_invite:
# per_second: 0.2
# burst_count: 10
#
#rc_media_create:
# per_second: 10
# burst_count: 50

# Ratelimiting settings for incoming federation
#
Expand Down Expand Up @@ -1034,6 +1038,11 @@ media_store_path: "DATADIR/media_store"
#
#max_image_pixels: 32M

# How long to wait before expiring created media IDs when MSC2246 support is
# enabled.
#
#unused_expiration_time: 1m

# Whether to generate new thumbnails on the fly to precisely match
# the resolution requested by the client. If true then whenever
# a new resolution is requested by the client the server will
Expand Down
4 changes: 1 addition & 3 deletions synapse/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@
FEDERATION_UNSTABLE_PREFIX = FEDERATION_PREFIX + "/unstable"
STATIC_PREFIX = "/_matrix/static"
SERVER_KEY_V2_PREFIX = "/_matrix/key/v2"
MEDIA_R0_PREFIX = "/_matrix/media/r0"
MEDIA_V3_PREFIX = "/_matrix/media/v3"
LEGACY_MEDIA_PREFIX = "/_matrix/media/v1"
MEDIA_PREFIX = "/_matrix/media"


class ConsentURIBuilder:
Expand Down
8 changes: 2 additions & 6 deletions synapse/app/generic_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@
from synapse.api.urls import (
CLIENT_API_PREFIX,
FEDERATION_PREFIX,
LEGACY_MEDIA_PREFIX,
MEDIA_R0_PREFIX,
MEDIA_V3_PREFIX,
MEDIA_PREFIX,
SERVER_KEY_V2_PREFIX,
)
from synapse.app import _base
Expand Down Expand Up @@ -338,9 +336,7 @@ def _listen_http(self, listener_config: ListenerConfig) -> None:

resources.update(
{
MEDIA_R0_PREFIX: media_repo,
MEDIA_V3_PREFIX: media_repo,
LEGACY_MEDIA_PREFIX: media_repo,
MEDIA_PREFIX: media_repo,
"/_synapse/admin": admin_resource,
}
)
Expand Down
12 changes: 2 additions & 10 deletions synapse/app/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@
from synapse.api.urls import (
CLIENT_API_PREFIX,
FEDERATION_PREFIX,
LEGACY_MEDIA_PREFIX,
MEDIA_R0_PREFIX,
MEDIA_V3_PREFIX,
MEDIA_PREFIX,
SERVER_KEY_V2_PREFIX,
STATIC_PREFIX,
)
Expand Down Expand Up @@ -245,13 +243,7 @@ def _configure_named_resource(
if name in ["media", "federation", "client"]:
if self.config.server.enable_media_repo:
media_repo = self.get_media_repository_resource()
resources.update(
{
MEDIA_R0_PREFIX: media_repo,
MEDIA_V3_PREFIX: media_repo,
LEGACY_MEDIA_PREFIX: media_repo,
}
)
resources[MEDIA_PREFIX] = media_repo
elif name == "media":
raise ConfigError(
"'media' resource conflicts with enable_media_repo=False"
Expand Down
3 changes: 3 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,6 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:

# MSC3772: A push rule for mutual relations.
self.msc3772_enabled: bool = experimental.get("msc3772_enabled", False)

# MSC2246 (async media uploads)
self.msc2246_enabled: bool = experimental.get("msc2246_enabled", False)
10 changes: 10 additions & 0 deletions synapse/config/ratelimiting.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
},
)

# Ratelimit create media requests:
self.rc_media_create = RateLimitConfig(
config.get("rc_media_create", {}),
defaults={"per_second": 10, "burst_count": 50},
)

def generate_config_section(self, **kwargs: Any) -> str:
return """\
## Ratelimiting ##
Expand Down Expand Up @@ -234,6 +240,10 @@ def generate_config_section(self, **kwargs: Any) -> str:
#rc_third_party_invite:
# per_second: 0.2
# burst_count: 10
#
#rc_media_create:
# per_second: 10
# burst_count: 50

# Ratelimiting settings for incoming federation
#
Expand Down
9 changes: 9 additions & 0 deletions synapse/config/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
self.max_image_pixels = self.parse_size(config.get("max_image_pixels", "32M"))
self.max_spider_size = self.parse_size(config.get("max_spider_size", "10M"))

self.unused_expiration_time = self.parse_duration(
config.get("unused_expiration_time", "1m")
)

self.media_store_path = self.ensure_directory(
config.get("media_store_path", "media_store")
)
Expand Down Expand Up @@ -292,6 +296,11 @@ def generate_config_section(self, data_dir_path: str, **kwargs: Any) -> str:
#
#max_image_pixels: 32M

# How long to wait before expiring created media IDs when MSC2246 support is
# enabled.
#
#unused_expiration_time: 1m

# Whether to generate new thumbnails on the fly to precisely match
# the resolution requested by the client. If true then whenever
# a new resolution is requested by the client the server will
Expand Down
2 changes: 2 additions & 0 deletions synapse/rest/media/v1/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
"text/xml",
]

DEFAULT_MSC2246_DELAY = 20_000


def parse_media_id(request: Request) -> Tuple[str, str, Optional[str]]:
"""Parses the server name, media ID and optional file name from the request URI
Expand Down
84 changes: 84 additions & 0 deletions synapse/rest/media/v1/create_resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Copyright 2014-2016 OpenMarket Ltd
# Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a new file so should be copy righted to 2022 to whatever legal entity you're contributing as

#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
from typing import TYPE_CHECKING

from synapse.api.errors import LimitExceededError
from synapse.api.ratelimiting import Ratelimiter
from synapse.http.server import DirectServeJsonResource, respond_with_json
from synapse.http.site import SynapseRequest

if TYPE_CHECKING:
from synapse.rest.media.v1.media_repository import MediaRepository
from synapse.server import HomeServer


logger = logging.getLogger(__name__)


class CreateResource(DirectServeJsonResource):
isLeaf = True

def __init__(self, hs: "HomeServer", media_repo: "MediaRepository"):
super().__init__()

self.media_repo = media_repo
self.clock = hs.get_clock()
self.auth = hs.get_auth()

# A rate limiter for creating new media IDs.
self._create_media_rate_limiter = Ratelimiter(
store=hs.get_datastores().main,
clock=self.clock,
rate_hz=hs.config.ratelimiting.rc_media_create.per_second,
burst_count=hs.config.ratelimiting.rc_media_create.burst_count,
)

async def _async_render_OPTIONS(self, request: SynapseRequest) -> None:
respond_with_json(request, 200, {}, send_cors=True)

async def _async_render_POST(self, request: SynapseRequest) -> None:
requester = await self.auth.get_user_by_req(request)

# If the create media requests for the user are over the limit, drop
# them.
allowed, time_allowed = await self._create_media_rate_limiter.can_do_action(
requester
)
if not allowed:
time_now_s = self.clock.time()
raise LimitExceededError(
retry_after_ms=int(1000 * (time_allowed - time_now_s))
)

content_uri, unused_expires_at = await self.media_repo.create_media_id(
requester.user
)

logger.info(
"Created Media URI %r that if unused will expire at %d",
content_uri,
unused_expires_at,
)
respond_with_json(
request,
200,
{
"content_uri": content_uri,
"unused_expires_at": unused_expires_at,
},
send_cors=True,
)
20 changes: 12 additions & 8 deletions synapse/rest/media/v1/download_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
from typing import TYPE_CHECKING

from synapse.http.server import DirectServeJsonResource, set_cors_headers
from synapse.http.servlet import parse_boolean
from synapse.http.servlet import parse_boolean, parse_integer
from synapse.http.site import SynapseRequest

from ._base import parse_media_id, respond_404
from ._base import DEFAULT_MSC2246_DELAY, parse_media_id, respond_404

if TYPE_CHECKING:
from synapse.rest.media.v1.media_repository import MediaRepository
Expand All @@ -35,6 +35,7 @@ def __init__(self, hs: "HomeServer", media_repo: "MediaRepository"):
super().__init__()
self.media_repo = media_repo
self.server_name = hs.hostname
self.enable_msc2246 = hs.config.experimental.msc2246_enabled

async def _async_render_GET(self, request: SynapseRequest) -> None:
set_cors_headers(request)
Expand All @@ -50,13 +51,14 @@ async def _async_render_GET(self, request: SynapseRequest) -> None:
)
# Limited non-standard form of CSP for IE11
request.setHeader(b"X-Content-Security-Policy", b"sandbox;")
request.setHeader(
b"Referrer-Policy",
b"no-referrer",
)
request.setHeader(b"Referrer-Policy", b"no-referrer")
server_name, media_id, name = parse_media_id(request)
max_stall_ms = parse_integer(
request, "fi.mau.msc2246.max_stall_ms", default=DEFAULT_MSC2246_DELAY
)

if server_name == self.server_name:
await self.media_repo.get_local_media(request, media_id, name)
await self.media_repo.get_local_media(request, media_id, name, max_stall_ms)
else:
allow_remote = parse_boolean(request, "allow_remote", default=True)
if not allow_remote:
Expand All @@ -68,4 +70,6 @@ async def _async_render_GET(self, request: SynapseRequest) -> None:
respond_404(request)
return

await self.media_repo.get_remote_media(request, server_name, media_id, name)
await self.media_repo.get_remote_media(
request, server_name, media_id, name, max_stall_ms
)