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

Commit d22c1c8

Browse files
authored
Respond correctly to unknown methods on known endpoints (#14605)
Respond with a 405 error if a request is received on a known endpoint, but to an unknown method, per MSC3743.
1 parent 8a6e043 commit d22c1c8

File tree

8 files changed

+89
-51
lines changed

8 files changed

+89
-51
lines changed

changelog.d/14605.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Return spec-compliant JSON errors when unknown endpoints are requested.

docs/admin_api/media_admin_api.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,14 @@ The following fields are returned in the JSON response body:
235235

236236
Request:
237237

238+
```
239+
POST /_synapse/admin/v1/media/delete?before_ts=<before_ts>
240+
241+
{}
242+
```
243+
244+
*Deprecated in Synapse v1.78.0:* This API is available at the deprecated endpoint:
245+
238246
```
239247
POST /_synapse/admin/v1/media/<server_name>/delete?before_ts=<before_ts>
240248
@@ -243,7 +251,7 @@ POST /_synapse/admin/v1/media/<server_name>/delete?before_ts=<before_ts>
243251

244252
URL Parameters
245253

246-
* `server_name`: string - The name of your local server (e.g `matrix.org`).
254+
* `server_name`: string - The name of your local server (e.g `matrix.org`). *Deprecated in Synapse v1.78.0.*
247255
* `before_ts`: string representing a positive integer - Unix timestamp in milliseconds.
248256
Files that were last used before this timestamp will be deleted. It is the timestamp of
249257
last access, not the timestamp when the file was created.

docs/upgrade.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ process, for example:
8888
dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb
8989
```
9090
91+
# Upgrading to v1.78.0
92+
93+
## Deprecate the `/_synapse/admin/v1/media/<server_name>/delete` admin API
94+
95+
Synapse 1.78.0 replaces the `/_synapse/admin/v1/media/<server_name>/delete`
96+
admin API with an identical endpoint at `/_synapse/admin/v1/media/delete`. Please
97+
update your tooling to use the new endpoint. The deprecated version will be removed
98+
in a future release.
99+
91100
# Upgrading to v1.76.0
92101
93102
## Faster joins are enabled by default
@@ -137,6 +146,7 @@ and then do `pip install matrix-synapse[user-search]` for a PyPI install.
137146
Docker images and Debian packages need nothing specific as they already
138147
include or specify ICU as an explicit dependency.
139148
149+
140150
# Upgrading to v1.73.0
141151
142152
## Legacy Prometheus metric names have now been removed

synapse/http/server.py

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
Iterable,
3131
Iterator,
3232
List,
33-
NoReturn,
3433
Optional,
3534
Pattern,
3635
Tuple,
@@ -340,7 +339,8 @@ async def _async_render(self, request: SynapseRequest) -> Optional[Tuple[int, An
340339

341340
return callback_return
342341

343-
return _unrecognised_request_handler(request)
342+
# A request with an unknown method (for a known endpoint) was received.
343+
raise UnrecognizedRequestError(code=405)
344344

345345
@abc.abstractmethod
346346
def _send_response(
@@ -396,7 +396,6 @@ def _send_error_response(
396396

397397
@attr.s(slots=True, frozen=True, auto_attribs=True)
398398
class _PathEntry:
399-
pattern: Pattern
400399
callback: ServletCallback
401400
servlet_classname: str
402401

@@ -425,13 +424,14 @@ def __init__(
425424
):
426425
super().__init__(canonical_json, extract_context)
427426
self.clock = hs.get_clock()
428-
self.path_regexs: Dict[bytes, List[_PathEntry]] = {}
427+
# Map of path regex -> method -> callback.
428+
self._routes: Dict[Pattern[str], Dict[bytes, _PathEntry]] = {}
429429
self.hs = hs
430430

431431
def register_paths(
432432
self,
433433
method: str,
434-
path_patterns: Iterable[Pattern],
434+
path_patterns: Iterable[Pattern[str]],
435435
callback: ServletCallback,
436436
servlet_classname: str,
437437
) -> None:
@@ -455,8 +455,8 @@ def register_paths(
455455

456456
for path_pattern in path_patterns:
457457
logger.debug("Registering for %s %s", method, path_pattern.pattern)
458-
self.path_regexs.setdefault(method_bytes, []).append(
459-
_PathEntry(path_pattern, callback, servlet_classname)
458+
self._routes.setdefault(path_pattern, {})[method_bytes] = _PathEntry(
459+
callback, servlet_classname
460460
)
461461

462462
def _get_handler_for_request(
@@ -478,14 +478,17 @@ def _get_handler_for_request(
478478

479479
# Loop through all the registered callbacks to check if the method
480480
# and path regex match
481-
for path_entry in self.path_regexs.get(request_method, []):
482-
m = path_entry.pattern.match(request_path)
481+
for path_pattern, methods in self._routes.items():
482+
m = path_pattern.match(request_path)
483483
if m:
484-
# We found a match!
484+
# We found a matching path!
485+
path_entry = methods.get(request_method)
486+
if not path_entry:
487+
raise UnrecognizedRequestError(code=405)
485488
return path_entry.callback, path_entry.servlet_classname, m.groupdict()
486489

487-
# Huh. No one wanted to handle that? Fiiiiiine. Send 400.
488-
return _unrecognised_request_handler, "unrecognised_request_handler", {}
490+
# Huh. No one wanted to handle that? Fiiiiiine.
491+
raise UnrecognizedRequestError(code=404)
489492

490493
async def _async_render(self, request: SynapseRequest) -> Tuple[int, Any]:
491494
callback, servlet_classname, group_dict = self._get_handler_for_request(request)
@@ -567,19 +570,6 @@ def render_GET(self, request: Request) -> bytes:
567570
return super().render_GET(request)
568571

569572

570-
def _unrecognised_request_handler(request: Request) -> NoReturn:
571-
"""Request handler for unrecognised requests
572-
573-
This is a request handler suitable for return from
574-
_get_handler_for_request. It actually just raises an
575-
UnrecognizedRequestError.
576-
577-
Args:
578-
request: Unused, but passed in to match the signature of ServletCallback.
579-
"""
580-
raise UnrecognizedRequestError(code=404)
581-
582-
583573
class UnrecognizedRequestResource(resource.Resource):
584574
"""
585575
Similar to twisted.web.resource.NoResource, but returns a JSON 404 with an

synapse/rest/admin/media.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import logging
1717
from http import HTTPStatus
18-
from typing import TYPE_CHECKING, Tuple
18+
from typing import TYPE_CHECKING, Optional, Tuple
1919

2020
from synapse.api.constants import Direction
2121
from synapse.api.errors import Codes, NotFoundError, SynapseError
@@ -285,7 +285,12 @@ class DeleteMediaByDateSize(RestServlet):
285285
timestamp and size.
286286
"""
287287

288-
PATTERNS = admin_patterns("/media/(?P<server_name>[^/]*)/delete$")
288+
PATTERNS = [
289+
*admin_patterns("/media/delete$"),
290+
# This URL kept around for legacy reasons, it is undesirable since it
291+
# overlaps with the DeleteMediaByID servlet.
292+
*admin_patterns("/media/(?P<server_name>[^/]*)/delete$"),
293+
]
289294

290295
def __init__(self, hs: "HomeServer"):
291296
self.store = hs.get_datastores().main
@@ -294,7 +299,7 @@ def __init__(self, hs: "HomeServer"):
294299
self.media_repository = hs.get_media_repository()
295300

296301
async def on_POST(
297-
self, request: SynapseRequest, server_name: str
302+
self, request: SynapseRequest, server_name: Optional[str] = None
298303
) -> Tuple[int, JsonDict]:
299304
await assert_requester_is_admin(self.auth, request)
300305

@@ -322,7 +327,8 @@ async def on_POST(
322327
errcode=Codes.INVALID_PARAM,
323328
)
324329

325-
if self.server_name != server_name:
330+
# This check is useless, we keep it for the legacy endpoint only.
331+
if server_name is not None and self.server_name != server_name:
326332
raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only delete local media")
327333

328334
logging.info(
@@ -489,6 +495,8 @@ def register_servlets_for_media_repo(hs: "HomeServer", http_server: HttpServer)
489495
ProtectMediaByID(hs).register(http_server)
490496
UnprotectMediaByID(hs).register(http_server)
491497
ListMediaInRoom(hs).register(http_server)
492-
DeleteMediaByID(hs).register(http_server)
498+
# XXX DeleteMediaByDateSize must be registered before DeleteMediaByID as
499+
# their URL routes overlap.
493500
DeleteMediaByDateSize(hs).register(http_server)
501+
DeleteMediaByID(hs).register(http_server)
494502
UserMediaRestServlet(hs).register(http_server)

synapse/rest/client/room_keys.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,32 @@ def __init__(self, hs: "HomeServer"):
259259
self.auth = hs.get_auth()
260260
self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler()
261261

262+
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
263+
"""
264+
Retrieve the version information about the most current backup version (if any)
265+
266+
It takes out an exclusive lock on this user's room_key backups, to ensure
267+
clients only upload to the current backup.
268+
269+
Returns 404 if the given version does not exist.
270+
271+
GET /room_keys/version HTTP/1.1
272+
{
273+
"version": "12345",
274+
"algorithm": "m.megolm_backup.v1",
275+
"auth_data": "dGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgZW5jcnlwdGVkIGpzb24K"
276+
}
277+
"""
278+
requester = await self.auth.get_user_by_req(request, allow_guest=False)
279+
user_id = requester.user.to_string()
280+
281+
try:
282+
info = await self.e2e_room_keys_handler.get_version_info(user_id)
283+
except SynapseError as e:
284+
if e.code == 404:
285+
raise SynapseError(404, "No backup found", Codes.NOT_FOUND)
286+
return 200, info
287+
262288
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
263289
"""
264290
Create a new backup version for this user's room_keys with the given
@@ -301,20 +327,19 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
301327

302328

303329
class RoomKeysVersionServlet(RestServlet):
304-
PATTERNS = client_patterns("/room_keys/version(/(?P<version>[^/]+))?$")
330+
PATTERNS = client_patterns("/room_keys/version/(?P<version>[^/]+)$")
305331

306332
def __init__(self, hs: "HomeServer"):
307333
super().__init__()
308334
self.auth = hs.get_auth()
309335
self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler()
310336

311337
async def on_GET(
312-
self, request: SynapseRequest, version: Optional[str]
338+
self, request: SynapseRequest, version: str
313339
) -> Tuple[int, JsonDict]:
314340
"""
315341
Retrieve the version information about a given version of the user's
316-
room_keys backup. If the version part is missing, returns info about the
317-
most current backup version (if any)
342+
room_keys backup.
318343
319344
It takes out an exclusive lock on this user's room_key backups, to ensure
320345
clients only upload to the current backup.
@@ -339,28 +364,24 @@ async def on_GET(
339364
return 200, info
340365

341366
async def on_DELETE(
342-
self, request: SynapseRequest, version: Optional[str]
367+
self, request: SynapseRequest, version: str
343368
) -> Tuple[int, JsonDict]:
344369
"""
345370
Delete the information about a given version of the user's
346-
room_keys backup. If the version part is missing, deletes the most
347-
current backup version (if any). Doesn't delete the actual room data.
371+
room_keys backup. Doesn't delete the actual room data.
348372
349373
DELETE /room_keys/version/12345 HTTP/1.1
350374
HTTP/1.1 200 OK
351375
{}
352376
"""
353-
if version is None:
354-
raise SynapseError(400, "No version specified to delete", Codes.NOT_FOUND)
355-
356377
requester = await self.auth.get_user_by_req(request, allow_guest=False)
357378
user_id = requester.user.to_string()
358379

359380
await self.e2e_room_keys_handler.delete_version(user_id, version)
360381
return 200, {}
361382

362383
async def on_PUT(
363-
self, request: SynapseRequest, version: Optional[str]
384+
self, request: SynapseRequest, version: str
364385
) -> Tuple[int, JsonDict]:
365386
"""
366387
Update the information about a given version of the user's room_keys backup.
@@ -386,11 +407,6 @@ async def on_PUT(
386407
user_id = requester.user.to_string()
387408
info = parse_json_object_from_request(request)
388409

389-
if version is None:
390-
raise SynapseError(
391-
400, "No version specified to update", Codes.MISSING_PARAM
392-
)
393-
394410
await self.e2e_room_keys_handler.update_version(user_id, version, info)
395411
return 200, {}
396412

synapse/rest/client/tags.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ class TagListServlet(RestServlet):
3434
GET /user/{user_id}/rooms/{room_id}/tags HTTP/1.1
3535
"""
3636

37-
PATTERNS = client_patterns("/user/(?P<user_id>[^/]*)/rooms/(?P<room_id>[^/]*)/tags")
37+
PATTERNS = client_patterns(
38+
"/user/(?P<user_id>[^/]*)/rooms/(?P<room_id>[^/]*)/tags$"
39+
)
3840

3941
def __init__(self, hs: "HomeServer"):
4042
super().__init__()

tests/rest/admin/test_media.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,8 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
213213
self.admin_user_tok = self.login("admin", "pass")
214214

215215
self.filepaths = MediaFilePaths(hs.config.media.media_store_path)
216-
self.url = "/_synapse/admin/v1/media/%s/delete" % self.server_name
216+
self.url = "/_synapse/admin/v1/media/delete"
217+
self.legacy_url = "/_synapse/admin/v1/media/%s/delete" % self.server_name
217218

218219
# Move clock up to somewhat realistic time
219220
self.reactor.advance(1000000000)
@@ -332,11 +333,13 @@ def test_invalid_parameter(self) -> None:
332333
channel.json_body["error"],
333334
)
334335

335-
def test_delete_media_never_accessed(self) -> None:
336+
@parameterized.expand([(True,), (False,)])
337+
def test_delete_media_never_accessed(self, use_legacy_url: bool) -> None:
336338
"""
337339
Tests that media deleted if it is older than `before_ts` and never accessed
338340
`last_access_ts` is `NULL` and `created_ts` < `before_ts`
339341
"""
342+
url = self.legacy_url if use_legacy_url else self.url
340343

341344
# upload and do not access
342345
server_and_media_id = self._create_media()
@@ -351,7 +354,7 @@ def test_delete_media_never_accessed(self) -> None:
351354
now_ms = self.clock.time_msec()
352355
channel = self.make_request(
353356
"POST",
354-
self.url + "?before_ts=" + str(now_ms),
357+
url + "?before_ts=" + str(now_ms),
355358
access_token=self.admin_user_tok,
356359
)
357360
self.assertEqual(200, channel.code, msg=channel.json_body)

0 commit comments

Comments
 (0)