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

Commit fa4f637

Browse files
committed
Attempt both the v3 and r0 endpoints.
1 parent c2c5942 commit fa4f637

File tree

4 files changed

+117
-5
lines changed

4 files changed

+117
-5
lines changed

synapse/federation/federation_client.py

+36
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
TYPE_CHECKING,
2222
AbstractSet,
2323
Awaitable,
24+
BinaryIO,
2425
Callable,
2526
Collection,
2627
Container,
@@ -1862,6 +1863,41 @@ def filter_user_id(user_id: str) -> bool:
18621863

18631864
return filtered_statuses, filtered_failures
18641865

1866+
async def download_media(
1867+
self,
1868+
destination: str,
1869+
media_id: str,
1870+
output_stream: BinaryIO,
1871+
max_size: int,
1872+
max_timeout_ms: int,
1873+
) -> Tuple[int, Dict[bytes, List[bytes]]]:
1874+
try:
1875+
return await self.transport_layer.download_media_v3(
1876+
destination,
1877+
media_id,
1878+
output_stream=output_stream,
1879+
max_size=max_size,
1880+
max_timeout_ms=max_timeout_ms,
1881+
)
1882+
except HttpResponseException as e:
1883+
# If an error is received that is due to an unrecognised endpoint,
1884+
# fallback to the r0 endpoint. Otherwise, consider it a legitimate error
1885+
# and raise.
1886+
if not is_unknown_endpoint(e):
1887+
raise
1888+
1889+
logger.debug(
1890+
"Couldn't download media with the v3 API, falling back to the r0 API"
1891+
)
1892+
1893+
return await self.transport_layer.download_media_r0(
1894+
destination,
1895+
media_id,
1896+
output_stream=output_stream,
1897+
max_size=max_size,
1898+
max_timeout_ms=max_timeout_ms,
1899+
)
1900+
18651901

18661902
@attr.s(frozen=True, slots=True, auto_attribs=True)
18671903
class TimestampToEventResponse:

synapse/federation/transport/client.py

+24
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,30 @@ async def download_media_r0(
829829
},
830830
)
831831

832+
async def download_media_v3(
833+
self,
834+
destination: str,
835+
media_id: str,
836+
output_stream: BinaryIO,
837+
max_size: int,
838+
max_timeout_ms: int,
839+
) -> Tuple[int, Dict[bytes, List[bytes]]]:
840+
path = f"/_matrix/media/v3/download/{destination}/{media_id}"
841+
842+
return await self.client.get_file(
843+
destination,
844+
path,
845+
output_stream=output_stream,
846+
max_size=max_size,
847+
args={
848+
# tell the remote server to 404 if it doesn't
849+
# recognise the server_name, to make sure we don't
850+
# end up with a routing loop.
851+
"allow_remote": "false",
852+
"timeout_ms": str(max_timeout_ms),
853+
},
854+
)
855+
832856

833857
def _create_path(federation_prefix: str, path: str, *args: str) -> str:
834858
"""

synapse/media/media_repository.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class MediaRepository:
7777
def __init__(self, hs: "HomeServer"):
7878
self.hs = hs
7979
self.auth = hs.get_auth()
80-
self.client = hs.get_federation_transport_client()
80+
self.client = hs.get_federation_client()
8181
self.clock = hs.get_clock()
8282
self.server_name = hs.hostname
8383
self.store = hs.get_datastores().main
@@ -645,7 +645,7 @@ async def _download_remote_file(
645645

646646
with self.media_storage.store_into_file(file_info) as (f, fname, finish):
647647
try:
648-
length, headers = await self.client.download_media_r0(
648+
length, headers = await self.client.download_media(
649649
server_name,
650650
media_id,
651651
output_stream=f,

tests/media/test_media_storage.py

+55-3
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@
2727

2828
from twisted.internet import defer
2929
from twisted.internet.defer import Deferred
30+
from twisted.python.failure import Failure
3031
from twisted.test.proto_helpers import MemoryReactor
3132
from twisted.web.resource import Resource
3233

33-
from synapse.api.errors import Codes
34+
from synapse.api.errors import Codes, HttpResponseException
3435
from synapse.events import EventBase
3536
from synapse.http.types import QueryParams
3637
from synapse.logging.context import make_deferred_yieldable
@@ -257,10 +258,15 @@ def write_to(
257258
output_stream.write(data)
258259
return response
259260

261+
def write_err(f: Failure) -> Failure:
262+
f.trap(HttpResponseException)
263+
output_stream.write(f.value.response)
264+
return f
265+
260266
d: Deferred[Tuple[bytes, Tuple[int, Dict[bytes, List[bytes]]]]] = Deferred()
261267
self.fetches.append((d, destination, path, args))
262268
# Note that this callback changes the value held by d.
263-
d_after_callback = d.addCallback(write_to)
269+
d_after_callback = d.addCallbacks(write_to, write_err)
264270
return make_deferred_yieldable(d_after_callback)
265271

266272
# Mock out the homeserver's MatrixFederationHttpClient
@@ -316,7 +322,7 @@ def _req(
316322
self.assertEqual(len(self.fetches), 1)
317323
self.assertEqual(self.fetches[0][1], "example.com")
318324
self.assertEqual(
319-
self.fetches[0][2], "/_matrix/media/r0/download/" + self.media_id
325+
self.fetches[0][2], "/_matrix/media/v3/download/" + self.media_id
320326
)
321327
self.assertEqual(
322328
self.fetches[0][3], {"allow_remote": "false", "timeout_ms": "20000"}
@@ -671,6 +677,52 @@ def test_cross_origin_resource_policy_header(self) -> None:
671677
[b"cross-origin"],
672678
)
673679

680+
def test_unknown_v3_endpoint(self) -> None:
681+
"""
682+
If the v3 endpoint fails, try the r0 one.
683+
"""
684+
channel = self.make_request(
685+
"GET",
686+
f"/_matrix/media/v3/download/{self.media_id}",
687+
shorthand=False,
688+
await_result=False,
689+
)
690+
self.pump()
691+
692+
# We've made one fetch, to example.com, using the media URL, and asking
693+
# the other server not to do a remote fetch
694+
self.assertEqual(len(self.fetches), 1)
695+
self.assertEqual(self.fetches[0][1], "example.com")
696+
self.assertEqual(
697+
self.fetches[0][2], "/_matrix/media/v3/download/" + self.media_id
698+
)
699+
700+
# The result which says the endpoint is unknown.
701+
unknown_endpoint = b'{"errcode":"M_UNRECOGNIZED","error":"Unknown request"}'
702+
self.fetches[0][0].errback(
703+
HttpResponseException(404, "NOT FOUND", unknown_endpoint)
704+
)
705+
706+
self.pump()
707+
708+
# There should now be another request to the r0 URL.
709+
self.assertEqual(len(self.fetches), 2)
710+
self.assertEqual(self.fetches[1][1], "example.com")
711+
self.assertEqual(
712+
self.fetches[1][2], f"/_matrix/media/r0/download/{self.media_id}"
713+
)
714+
715+
headers = {
716+
b"Content-Length": [b"%d" % (len(self.test_image.data))],
717+
}
718+
719+
self.fetches[1][0].callback(
720+
(self.test_image.data, (len(self.test_image.data), headers))
721+
)
722+
723+
self.pump()
724+
self.assertEqual(channel.code, 200)
725+
674726

675727
class TestSpamCheckerLegacy:
676728
"""A spam checker module that rejects all media that includes the bytes

0 commit comments

Comments
 (0)