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.
This commit is contained in:
parent
8a6e043488
commit
d22c1c862c
|
@ -0,0 +1 @@
|
||||||
|
Return spec-compliant JSON errors when unknown endpoints are requested.
|
|
@ -235,6 +235,14 @@ The following fields are returned in the JSON response body:
|
||||||
|
|
||||||
Request:
|
Request:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /_synapse/admin/v1/media/delete?before_ts=<before_ts>
|
||||||
|
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
|
||||||
|
*Deprecated in Synapse v1.78.0:* This API is available at the deprecated endpoint:
|
||||||
|
|
||||||
```
|
```
|
||||||
POST /_synapse/admin/v1/media/<server_name>/delete?before_ts=<before_ts>
|
POST /_synapse/admin/v1/media/<server_name>/delete?before_ts=<before_ts>
|
||||||
|
|
||||||
|
@ -243,7 +251,7 @@ POST /_synapse/admin/v1/media/<server_name>/delete?before_ts=<before_ts>
|
||||||
|
|
||||||
URL Parameters
|
URL Parameters
|
||||||
|
|
||||||
* `server_name`: string - The name of your local server (e.g `matrix.org`).
|
* `server_name`: string - The name of your local server (e.g `matrix.org`). *Deprecated in Synapse v1.78.0.*
|
||||||
* `before_ts`: string representing a positive integer - Unix timestamp in milliseconds.
|
* `before_ts`: string representing a positive integer - Unix timestamp in milliseconds.
|
||||||
Files that were last used before this timestamp will be deleted. It is the timestamp of
|
Files that were last used before this timestamp will be deleted. It is the timestamp of
|
||||||
last access, not the timestamp when the file was created.
|
last access, not the timestamp when the file was created.
|
||||||
|
|
|
@ -88,6 +88,15 @@ process, for example:
|
||||||
dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb
|
dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Upgrading to v1.78.0
|
||||||
|
|
||||||
|
## Deprecate the `/_synapse/admin/v1/media/<server_name>/delete` admin API
|
||||||
|
|
||||||
|
Synapse 1.78.0 replaces the `/_synapse/admin/v1/media/<server_name>/delete`
|
||||||
|
admin API with an identical endpoint at `/_synapse/admin/v1/media/delete`. Please
|
||||||
|
update your tooling to use the new endpoint. The deprecated version will be removed
|
||||||
|
in a future release.
|
||||||
|
|
||||||
# Upgrading to v1.76.0
|
# Upgrading to v1.76.0
|
||||||
|
|
||||||
## Faster joins are enabled by default
|
## Faster joins are enabled by default
|
||||||
|
@ -137,6 +146,7 @@ and then do `pip install matrix-synapse[user-search]` for a PyPI install.
|
||||||
Docker images and Debian packages need nothing specific as they already
|
Docker images and Debian packages need nothing specific as they already
|
||||||
include or specify ICU as an explicit dependency.
|
include or specify ICU as an explicit dependency.
|
||||||
|
|
||||||
|
|
||||||
# Upgrading to v1.73.0
|
# Upgrading to v1.73.0
|
||||||
|
|
||||||
## Legacy Prometheus metric names have now been removed
|
## Legacy Prometheus metric names have now been removed
|
||||||
|
|
|
@ -30,7 +30,6 @@ from typing import (
|
||||||
Iterable,
|
Iterable,
|
||||||
Iterator,
|
Iterator,
|
||||||
List,
|
List,
|
||||||
NoReturn,
|
|
||||||
Optional,
|
Optional,
|
||||||
Pattern,
|
Pattern,
|
||||||
Tuple,
|
Tuple,
|
||||||
|
@ -340,7 +339,8 @@ class _AsyncResource(resource.Resource, metaclass=abc.ABCMeta):
|
||||||
|
|
||||||
return callback_return
|
return callback_return
|
||||||
|
|
||||||
return _unrecognised_request_handler(request)
|
# A request with an unknown method (for a known endpoint) was received.
|
||||||
|
raise UnrecognizedRequestError(code=405)
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def _send_response(
|
def _send_response(
|
||||||
|
@ -396,7 +396,6 @@ class DirectServeJsonResource(_AsyncResource):
|
||||||
|
|
||||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||||
class _PathEntry:
|
class _PathEntry:
|
||||||
pattern: Pattern
|
|
||||||
callback: ServletCallback
|
callback: ServletCallback
|
||||||
servlet_classname: str
|
servlet_classname: str
|
||||||
|
|
||||||
|
@ -425,13 +424,14 @@ class JsonResource(DirectServeJsonResource):
|
||||||
):
|
):
|
||||||
super().__init__(canonical_json, extract_context)
|
super().__init__(canonical_json, extract_context)
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
self.path_regexs: Dict[bytes, List[_PathEntry]] = {}
|
# Map of path regex -> method -> callback.
|
||||||
|
self._routes: Dict[Pattern[str], Dict[bytes, _PathEntry]] = {}
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
|
|
||||||
def register_paths(
|
def register_paths(
|
||||||
self,
|
self,
|
||||||
method: str,
|
method: str,
|
||||||
path_patterns: Iterable[Pattern],
|
path_patterns: Iterable[Pattern[str]],
|
||||||
callback: ServletCallback,
|
callback: ServletCallback,
|
||||||
servlet_classname: str,
|
servlet_classname: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -455,8 +455,8 @@ class JsonResource(DirectServeJsonResource):
|
||||||
|
|
||||||
for path_pattern in path_patterns:
|
for path_pattern in path_patterns:
|
||||||
logger.debug("Registering for %s %s", method, path_pattern.pattern)
|
logger.debug("Registering for %s %s", method, path_pattern.pattern)
|
||||||
self.path_regexs.setdefault(method_bytes, []).append(
|
self._routes.setdefault(path_pattern, {})[method_bytes] = _PathEntry(
|
||||||
_PathEntry(path_pattern, callback, servlet_classname)
|
callback, servlet_classname
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_handler_for_request(
|
def _get_handler_for_request(
|
||||||
|
@ -478,14 +478,17 @@ class JsonResource(DirectServeJsonResource):
|
||||||
|
|
||||||
# Loop through all the registered callbacks to check if the method
|
# Loop through all the registered callbacks to check if the method
|
||||||
# and path regex match
|
# and path regex match
|
||||||
for path_entry in self.path_regexs.get(request_method, []):
|
for path_pattern, methods in self._routes.items():
|
||||||
m = path_entry.pattern.match(request_path)
|
m = path_pattern.match(request_path)
|
||||||
if m:
|
if m:
|
||||||
# We found a match!
|
# We found a matching path!
|
||||||
|
path_entry = methods.get(request_method)
|
||||||
|
if not path_entry:
|
||||||
|
raise UnrecognizedRequestError(code=405)
|
||||||
return path_entry.callback, path_entry.servlet_classname, m.groupdict()
|
return path_entry.callback, path_entry.servlet_classname, m.groupdict()
|
||||||
|
|
||||||
# Huh. No one wanted to handle that? Fiiiiiine. Send 400.
|
# Huh. No one wanted to handle that? Fiiiiiine.
|
||||||
return _unrecognised_request_handler, "unrecognised_request_handler", {}
|
raise UnrecognizedRequestError(code=404)
|
||||||
|
|
||||||
async def _async_render(self, request: SynapseRequest) -> Tuple[int, Any]:
|
async def _async_render(self, request: SynapseRequest) -> Tuple[int, Any]:
|
||||||
callback, servlet_classname, group_dict = self._get_handler_for_request(request)
|
callback, servlet_classname, group_dict = self._get_handler_for_request(request)
|
||||||
|
@ -567,19 +570,6 @@ class StaticResource(File):
|
||||||
return super().render_GET(request)
|
return super().render_GET(request)
|
||||||
|
|
||||||
|
|
||||||
def _unrecognised_request_handler(request: Request) -> NoReturn:
|
|
||||||
"""Request handler for unrecognised requests
|
|
||||||
|
|
||||||
This is a request handler suitable for return from
|
|
||||||
_get_handler_for_request. It actually just raises an
|
|
||||||
UnrecognizedRequestError.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request: Unused, but passed in to match the signature of ServletCallback.
|
|
||||||
"""
|
|
||||||
raise UnrecognizedRequestError(code=404)
|
|
||||||
|
|
||||||
|
|
||||||
class UnrecognizedRequestResource(resource.Resource):
|
class UnrecognizedRequestResource(resource.Resource):
|
||||||
"""
|
"""
|
||||||
Similar to twisted.web.resource.NoResource, but returns a JSON 404 with an
|
Similar to twisted.web.resource.NoResource, but returns a JSON 404 with an
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import TYPE_CHECKING, Tuple
|
from typing import TYPE_CHECKING, Optional, Tuple
|
||||||
|
|
||||||
from synapse.api.constants import Direction
|
from synapse.api.constants import Direction
|
||||||
from synapse.api.errors import Codes, NotFoundError, SynapseError
|
from synapse.api.errors import Codes, NotFoundError, SynapseError
|
||||||
|
@ -285,7 +285,12 @@ class DeleteMediaByDateSize(RestServlet):
|
||||||
timestamp and size.
|
timestamp and size.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PATTERNS = admin_patterns("/media/(?P<server_name>[^/]*)/delete$")
|
PATTERNS = [
|
||||||
|
*admin_patterns("/media/delete$"),
|
||||||
|
# This URL kept around for legacy reasons, it is undesirable since it
|
||||||
|
# overlaps with the DeleteMediaByID servlet.
|
||||||
|
*admin_patterns("/media/(?P<server_name>[^/]*)/delete$"),
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
self.store = hs.get_datastores().main
|
self.store = hs.get_datastores().main
|
||||||
|
@ -294,7 +299,7 @@ class DeleteMediaByDateSize(RestServlet):
|
||||||
self.media_repository = hs.get_media_repository()
|
self.media_repository = hs.get_media_repository()
|
||||||
|
|
||||||
async def on_POST(
|
async def on_POST(
|
||||||
self, request: SynapseRequest, server_name: str
|
self, request: SynapseRequest, server_name: Optional[str] = None
|
||||||
) -> Tuple[int, JsonDict]:
|
) -> Tuple[int, JsonDict]:
|
||||||
await assert_requester_is_admin(self.auth, request)
|
await assert_requester_is_admin(self.auth, request)
|
||||||
|
|
||||||
|
@ -322,7 +327,8 @@ class DeleteMediaByDateSize(RestServlet):
|
||||||
errcode=Codes.INVALID_PARAM,
|
errcode=Codes.INVALID_PARAM,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.server_name != server_name:
|
# This check is useless, we keep it for the legacy endpoint only.
|
||||||
|
if server_name is not None and self.server_name != server_name:
|
||||||
raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only delete local media")
|
raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only delete local media")
|
||||||
|
|
||||||
logging.info(
|
logging.info(
|
||||||
|
@ -489,6 +495,8 @@ def register_servlets_for_media_repo(hs: "HomeServer", http_server: HttpServer)
|
||||||
ProtectMediaByID(hs).register(http_server)
|
ProtectMediaByID(hs).register(http_server)
|
||||||
UnprotectMediaByID(hs).register(http_server)
|
UnprotectMediaByID(hs).register(http_server)
|
||||||
ListMediaInRoom(hs).register(http_server)
|
ListMediaInRoom(hs).register(http_server)
|
||||||
DeleteMediaByID(hs).register(http_server)
|
# XXX DeleteMediaByDateSize must be registered before DeleteMediaByID as
|
||||||
|
# their URL routes overlap.
|
||||||
DeleteMediaByDateSize(hs).register(http_server)
|
DeleteMediaByDateSize(hs).register(http_server)
|
||||||
|
DeleteMediaByID(hs).register(http_server)
|
||||||
UserMediaRestServlet(hs).register(http_server)
|
UserMediaRestServlet(hs).register(http_server)
|
||||||
|
|
|
@ -259,6 +259,32 @@ class RoomKeysNewVersionServlet(RestServlet):
|
||||||
self.auth = hs.get_auth()
|
self.auth = hs.get_auth()
|
||||||
self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler()
|
self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler()
|
||||||
|
|
||||||
|
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||||
|
"""
|
||||||
|
Retrieve the version information about the most current backup version (if any)
|
||||||
|
|
||||||
|
It takes out an exclusive lock on this user's room_key backups, to ensure
|
||||||
|
clients only upload to the current backup.
|
||||||
|
|
||||||
|
Returns 404 if the given version does not exist.
|
||||||
|
|
||||||
|
GET /room_keys/version HTTP/1.1
|
||||||
|
{
|
||||||
|
"version": "12345",
|
||||||
|
"algorithm": "m.megolm_backup.v1",
|
||||||
|
"auth_data": "dGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgZW5jcnlwdGVkIGpzb24K"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
requester = await self.auth.get_user_by_req(request, allow_guest=False)
|
||||||
|
user_id = requester.user.to_string()
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = await self.e2e_room_keys_handler.get_version_info(user_id)
|
||||||
|
except SynapseError as e:
|
||||||
|
if e.code == 404:
|
||||||
|
raise SynapseError(404, "No backup found", Codes.NOT_FOUND)
|
||||||
|
return 200, info
|
||||||
|
|
||||||
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||||
"""
|
"""
|
||||||
Create a new backup version for this user's room_keys with the given
|
Create a new backup version for this user's room_keys with the given
|
||||||
|
@ -301,7 +327,7 @@ class RoomKeysNewVersionServlet(RestServlet):
|
||||||
|
|
||||||
|
|
||||||
class RoomKeysVersionServlet(RestServlet):
|
class RoomKeysVersionServlet(RestServlet):
|
||||||
PATTERNS = client_patterns("/room_keys/version(/(?P<version>[^/]+))?$")
|
PATTERNS = client_patterns("/room_keys/version/(?P<version>[^/]+)$")
|
||||||
|
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -309,12 +335,11 @@ class RoomKeysVersionServlet(RestServlet):
|
||||||
self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler()
|
self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler()
|
||||||
|
|
||||||
async def on_GET(
|
async def on_GET(
|
||||||
self, request: SynapseRequest, version: Optional[str]
|
self, request: SynapseRequest, version: str
|
||||||
) -> Tuple[int, JsonDict]:
|
) -> Tuple[int, JsonDict]:
|
||||||
"""
|
"""
|
||||||
Retrieve the version information about a given version of the user's
|
Retrieve the version information about a given version of the user's
|
||||||
room_keys backup. If the version part is missing, returns info about the
|
room_keys backup.
|
||||||
most current backup version (if any)
|
|
||||||
|
|
||||||
It takes out an exclusive lock on this user's room_key backups, to ensure
|
It takes out an exclusive lock on this user's room_key backups, to ensure
|
||||||
clients only upload to the current backup.
|
clients only upload to the current backup.
|
||||||
|
@ -339,20 +364,16 @@ class RoomKeysVersionServlet(RestServlet):
|
||||||
return 200, info
|
return 200, info
|
||||||
|
|
||||||
async def on_DELETE(
|
async def on_DELETE(
|
||||||
self, request: SynapseRequest, version: Optional[str]
|
self, request: SynapseRequest, version: str
|
||||||
) -> Tuple[int, JsonDict]:
|
) -> Tuple[int, JsonDict]:
|
||||||
"""
|
"""
|
||||||
Delete the information about a given version of the user's
|
Delete the information about a given version of the user's
|
||||||
room_keys backup. If the version part is missing, deletes the most
|
room_keys backup. Doesn't delete the actual room data.
|
||||||
current backup version (if any). Doesn't delete the actual room data.
|
|
||||||
|
|
||||||
DELETE /room_keys/version/12345 HTTP/1.1
|
DELETE /room_keys/version/12345 HTTP/1.1
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
{}
|
{}
|
||||||
"""
|
"""
|
||||||
if version is None:
|
|
||||||
raise SynapseError(400, "No version specified to delete", Codes.NOT_FOUND)
|
|
||||||
|
|
||||||
requester = await self.auth.get_user_by_req(request, allow_guest=False)
|
requester = await self.auth.get_user_by_req(request, allow_guest=False)
|
||||||
user_id = requester.user.to_string()
|
user_id = requester.user.to_string()
|
||||||
|
|
||||||
|
@ -360,7 +381,7 @@ class RoomKeysVersionServlet(RestServlet):
|
||||||
return 200, {}
|
return 200, {}
|
||||||
|
|
||||||
async def on_PUT(
|
async def on_PUT(
|
||||||
self, request: SynapseRequest, version: Optional[str]
|
self, request: SynapseRequest, version: str
|
||||||
) -> Tuple[int, JsonDict]:
|
) -> Tuple[int, JsonDict]:
|
||||||
"""
|
"""
|
||||||
Update the information about a given version of the user's room_keys backup.
|
Update the information about a given version of the user's room_keys backup.
|
||||||
|
@ -386,11 +407,6 @@ class RoomKeysVersionServlet(RestServlet):
|
||||||
user_id = requester.user.to_string()
|
user_id = requester.user.to_string()
|
||||||
info = parse_json_object_from_request(request)
|
info = parse_json_object_from_request(request)
|
||||||
|
|
||||||
if version is None:
|
|
||||||
raise SynapseError(
|
|
||||||
400, "No version specified to update", Codes.MISSING_PARAM
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.e2e_room_keys_handler.update_version(user_id, version, info)
|
await self.e2e_room_keys_handler.update_version(user_id, version, info)
|
||||||
return 200, {}
|
return 200, {}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,9 @@ class TagListServlet(RestServlet):
|
||||||
GET /user/{user_id}/rooms/{room_id}/tags HTTP/1.1
|
GET /user/{user_id}/rooms/{room_id}/tags HTTP/1.1
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PATTERNS = client_patterns("/user/(?P<user_id>[^/]*)/rooms/(?P<room_id>[^/]*)/tags")
|
PATTERNS = client_patterns(
|
||||||
|
"/user/(?P<user_id>[^/]*)/rooms/(?P<room_id>[^/]*)/tags$"
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
|
@ -213,7 +213,8 @@ class DeleteMediaByDateSizeTestCase(unittest.HomeserverTestCase):
|
||||||
self.admin_user_tok = self.login("admin", "pass")
|
self.admin_user_tok = self.login("admin", "pass")
|
||||||
|
|
||||||
self.filepaths = MediaFilePaths(hs.config.media.media_store_path)
|
self.filepaths = MediaFilePaths(hs.config.media.media_store_path)
|
||||||
self.url = "/_synapse/admin/v1/media/%s/delete" % self.server_name
|
self.url = "/_synapse/admin/v1/media/delete"
|
||||||
|
self.legacy_url = "/_synapse/admin/v1/media/%s/delete" % self.server_name
|
||||||
|
|
||||||
# Move clock up to somewhat realistic time
|
# Move clock up to somewhat realistic time
|
||||||
self.reactor.advance(1000000000)
|
self.reactor.advance(1000000000)
|
||||||
|
@ -332,11 +333,13 @@ class DeleteMediaByDateSizeTestCase(unittest.HomeserverTestCase):
|
||||||
channel.json_body["error"],
|
channel.json_body["error"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_delete_media_never_accessed(self) -> None:
|
@parameterized.expand([(True,), (False,)])
|
||||||
|
def test_delete_media_never_accessed(self, use_legacy_url: bool) -> None:
|
||||||
"""
|
"""
|
||||||
Tests that media deleted if it is older than `before_ts` and never accessed
|
Tests that media deleted if it is older than `before_ts` and never accessed
|
||||||
`last_access_ts` is `NULL` and `created_ts` < `before_ts`
|
`last_access_ts` is `NULL` and `created_ts` < `before_ts`
|
||||||
"""
|
"""
|
||||||
|
url = self.legacy_url if use_legacy_url else self.url
|
||||||
|
|
||||||
# upload and do not access
|
# upload and do not access
|
||||||
server_and_media_id = self._create_media()
|
server_and_media_id = self._create_media()
|
||||||
|
@ -351,7 +354,7 @@ class DeleteMediaByDateSizeTestCase(unittest.HomeserverTestCase):
|
||||||
now_ms = self.clock.time_msec()
|
now_ms = self.clock.time_msec()
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"POST",
|
"POST",
|
||||||
self.url + "?before_ts=" + str(now_ms),
|
url + "?before_ts=" + str(now_ms),
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||||
|
|
Loading…
Reference in New Issue