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

Commit d98b560

Browse files
committed
Follow redirects when fetching from /download.
1 parent fa4f637 commit d98b560

File tree

4 files changed

+65
-21
lines changed

4 files changed

+65
-21
lines changed

changelog.d/16701.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Follow redirects when downloading media over federation (per [MSC3860](https://github.com/matrix-org/matrix-spec-proposals/pull/3860)).

synapse/federation/transport/client.py

+4
Original file line numberDiff line numberDiff line change
@@ -850,7 +850,11 @@ async def download_media_v3(
850850
# end up with a routing loop.
851851
"allow_remote": "false",
852852
"timeout_ms": str(max_timeout_ms),
853+
# Matix 1.7 allows for this to redirect to another URL, this should
854+
# just be ignored for an old homeserver, so always provide it.
855+
"allow_redirect": "true",
853856
},
857+
follow_redirects=True,
854858
)
855859

856860

synapse/http/matrixfederationclient.py

+57-20
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,18 @@ class MatrixFederationRequest:
153153
"""Query arguments.
154154
"""
155155

156-
txn_id: Optional[str] = None
157-
"""Unique ID for this request (for logging)
156+
txn_id: str = attr.ib(init=False)
157+
"""Unique ID for this request (for logging), this is autogenerated.
158158
"""
159159

160-
uri: bytes = attr.ib(init=False)
161-
"""The URI of this request
160+
uri: bytes = b""
161+
"""The URI of this request, usually generated from the above information.
162+
"""
163+
164+
_generate_uri: bool = True
165+
"""True to automatically generate the uri field based on the above information.
166+
167+
Set to False if manually configuring the URI.
162168
"""
163169

164170
def __attrs_post_init__(self) -> None:
@@ -168,22 +174,23 @@ def __attrs_post_init__(self) -> None:
168174

169175
object.__setattr__(self, "txn_id", txn_id)
170176

171-
destination_bytes = self.destination.encode("ascii")
172-
path_bytes = self.path.encode("ascii")
173-
query_bytes = encode_query_args(self.query)
174-
175-
# The object is frozen so we can pre-compute this.
176-
uri = urllib.parse.urlunparse(
177-
(
178-
b"matrix-federation",
179-
destination_bytes,
180-
path_bytes,
181-
None,
182-
query_bytes,
183-
b"",
177+
if self._generate_uri:
178+
destination_bytes = self.destination.encode("ascii")
179+
path_bytes = self.path.encode("ascii")
180+
query_bytes = encode_query_args(self.query)
181+
182+
# The object is frozen so we can pre-compute this.
183+
uri = urllib.parse.urlunparse(
184+
(
185+
b"matrix-federation",
186+
destination_bytes,
187+
path_bytes,
188+
None,
189+
query_bytes,
190+
b"",
191+
)
184192
)
185-
)
186-
object.__setattr__(self, "uri", uri)
193+
object.__setattr__(self, "uri", uri)
187194

188195
def get_json(self) -> Optional[JsonDict]:
189196
if self.json_callback:
@@ -513,6 +520,7 @@ async def _send_request(
513520
ignore_backoff: bool = False,
514521
backoff_on_404: bool = False,
515522
backoff_on_all_error_codes: bool = False,
523+
follow_redirects: bool = False,
516524
) -> IResponse:
517525
"""
518526
Sends a request to the given server.
@@ -555,6 +563,9 @@ async def _send_request(
555563
backoff_on_404: Back off if we get a 404
556564
backoff_on_all_error_codes: Back off if we get any error response
557565
566+
follow_redirects: True to follow the Location header of 307/308 redirect
567+
responses. This does not recurse.
568+
558569
Returns:
559570
Resolves with the HTTP response object on success.
560571
@@ -714,6 +725,26 @@ async def _send_request(
714725
response.code,
715726
response_phrase,
716727
)
728+
elif (
729+
response.code in (307, 308)
730+
and follow_redirects
731+
and response.headers.hasHeader("Location")
732+
):
733+
# The Location header *might* be relative so resolve it.
734+
location = response.headers.getRawHeaders(b"Location")[0]
735+
new_uri = urllib.parse.urljoin(request.uri, location)
736+
737+
return await self._send_request(
738+
attr.evolve(request, uri=new_uri, generate_uri=False),
739+
retry_on_dns_fail,
740+
timeout,
741+
long_retries,
742+
ignore_backoff,
743+
backoff_on_404,
744+
backoff_on_all_error_codes,
745+
# Do not continue following redirects.
746+
follow_redirects=False,
747+
)
717748
else:
718749
logger.info(
719750
"{%s} [%s] Got response headers: %d %s",
@@ -1383,6 +1414,7 @@ async def get_file(
13831414
retry_on_dns_fail: bool = True,
13841415
max_size: Optional[int] = None,
13851416
ignore_backoff: bool = False,
1417+
follow_redirects: bool = False,
13861418
) -> Tuple[int, Dict[bytes, List[bytes]]]:
13871419
"""GETs a file from a given homeserver
13881420
Args:
@@ -1392,6 +1424,8 @@ async def get_file(
13921424
args: Optional dictionary used to create the query string.
13931425
ignore_backoff: true to ignore the historical backoff data
13941426
and try the request anyway.
1427+
follow_redirects: True to follow the Location header of 307/308 redirect
1428+
responses. This does not recurse.
13951429
13961430
Returns:
13971431
Resolves with an (int,dict) tuple of
@@ -1412,7 +1446,10 @@ async def get_file(
14121446
)
14131447

14141448
response = await self._send_request(
1415-
request, retry_on_dns_fail=retry_on_dns_fail, ignore_backoff=ignore_backoff
1449+
request,
1450+
retry_on_dns_fail=retry_on_dns_fail,
1451+
ignore_backoff=ignore_backoff,
1452+
follow_redirects=follow_redirects,
14161453
)
14171454

14181455
headers = dict(response.headers.getAllRawHeaders())

tests/media/test_media_storage.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ def get_file(
248248
retry_on_dns_fail: bool = True,
249249
max_size: Optional[int] = None,
250250
ignore_backoff: bool = False,
251+
follow_redirects: bool = False,
251252
) -> "Deferred[Tuple[int, Dict[bytes, List[bytes]]]]":
252253
"""A mock for MatrixFederationHttpClient.get_file."""
253254

@@ -325,7 +326,8 @@ def _req(
325326
self.fetches[0][2], "/_matrix/media/v3/download/" + self.media_id
326327
)
327328
self.assertEqual(
328-
self.fetches[0][3], {"allow_remote": "false", "timeout_ms": "20000"}
329+
self.fetches[0][3],
330+
{"allow_remote": "false", "timeout_ms": "20000", "allow_redirect": "true"},
329331
)
330332

331333
headers = {

0 commit comments

Comments
 (0)