From 03dceffae29ddf38d60f287ab1f79ea194ef86af Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 11 Mar 2023 16:03:17 +0200 Subject: [PATCH 1/5] Implement MSC2659: application service ping endpoint Signed-off-by: Tulir Asokan --- changelog.d/15249.feature | 1 + synapse/api/errors.py | 5 ++ synapse/appservice/api.py | 13 +++ synapse/config/experimental.py | 3 + synapse/rest/__init__.py | 2 + synapse/rest/client/appservice_ping.py | 106 +++++++++++++++++++++++++ synapse/rest/client/versions.py | 2 + 7 files changed, 132 insertions(+) create mode 100644 changelog.d/15249.feature create mode 100644 synapse/rest/client/appservice_ping.py diff --git a/changelog.d/15249.feature b/changelog.d/15249.feature new file mode 100644 index 000000000000..19bcd7c75305 --- /dev/null +++ b/changelog.d/15249.feature @@ -0,0 +1 @@ +Implement [MSC2659](https://github.com/matrix-org/matrix-spec-proposals/pull/2659): application service ping endpoint. Contributed by Tulir @ Beeper diff --git a/synapse/api/errors.py b/synapse/api/errors.py index e1737de59b51..8c6822f3c6ea 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -108,6 +108,11 @@ class Codes(str, Enum): USER_AWAITING_APPROVAL = "ORG.MATRIX.MSC3866_USER_AWAITING_APPROVAL" + AS_PING_URL_NOT_SET = "FI.MAU.MSC2659_URL_NOT_SET" + AS_PING_BAD_STATUS = "FI.MAU.MSC2659_BAD_STATUS" + AS_PING_CONNECTION_TIMEOUT = "FI.MAU.MSC2659_CONNECTION_TIMEOUT" + AS_PING_CONNECTION_FAILED = "FI.MAU.MSC2659_CONNECTION_FAILED" + # Attempt to send a second annotation with the same event type & annotation key # MSC2677 DUPLICATE_ANNOTATION = "M_DUPLICATE_ANNOTATION" diff --git a/synapse/appservice/api.py b/synapse/appservice/api.py index 1a6f69e7d3f3..1eee7fec886e 100644 --- a/synapse/appservice/api.py +++ b/synapse/appservice/api.py @@ -266,6 +266,19 @@ async def _get() -> Optional[JsonDict]: key = (service.id, protocol) return await self.protocol_meta_cache.wrap(key, _get) + async def ping(self, service: "ApplicationService") -> None: + # The caller should check that url is set + assert service.url is not None, "ping called without URL being set" + + # This is required by the configuration. + assert service.hs_token is not None + + await self.post_json_get_json( + uri=service.url + "/_matrix/app/unstable/fi.mau.msc2659/ping", + post_json={}, + headers={"Authorization": [f"Bearer {service.hs_token}"]}, + ) + async def push_bulk( self, service: "ApplicationService", diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 7e05f78f70a2..99dcd27c7426 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -178,3 +178,6 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None: # MSC3967: Do not require UIA when first uploading cross signing keys self.msc3967_enabled = experimental.get("msc3967_enabled", False) + + # MSC2659: Application service ping endpoint + self.msc2659_enabled = experimental.get("msc2659_enabled", False) diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index 2e19e055d3aa..55b448adfdba 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -20,6 +20,7 @@ account, account_data, account_validity, + appservice_ping, auth, capabilities, devices, @@ -140,6 +141,7 @@ def register_servlets(client_resource: HttpServer, hs: "HomeServer") -> None: if is_main_process: password_policy.register_servlets(hs, client_resource) knock.register_servlets(hs, client_resource) + appservice_ping.register_servlets(hs, client_resource) # moving to /_synapse/admin if is_main_process: diff --git a/synapse/rest/client/appservice_ping.py b/synapse/rest/client/appservice_ping.py new file mode 100644 index 000000000000..5f272df19e40 --- /dev/null +++ b/synapse/rest/client/appservice_ping.py @@ -0,0 +1,106 @@ +# Copyright 2023 Tulir Asokan +# +# 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 +import time +from http import HTTPStatus +from typing import TYPE_CHECKING, Tuple + +from synapse.api.errors import ( + CodeMessageException, + Codes, + HttpResponseException, + SynapseError, +) +from synapse.http import RequestTimedOutError +from synapse.http.server import HttpServer +from synapse.http.servlet import RestServlet, parse_json_object_from_request +from synapse.http.site import SynapseRequest +from synapse.types import JsonDict + +from ._base import client_patterns + +if TYPE_CHECKING: + from synapse.server import HomeServer + +logger = logging.getLogger(__name__) + + +class AppservicePingRestServlet(RestServlet): + PATTERNS = client_patterns( + "/fi.mau.msc2659/appservice/ping", + unstable=True, + releases=(), + ) + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.as_api = hs.get_application_service_api() + self.auth = hs.get_auth() + + async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + + if not requester.app_service: + raise SynapseError( + HTTPStatus.FORBIDDEN, + "Only application services can use the /appservice/ping endpoint", + Codes.FORBIDDEN, + ) + elif not requester.app_service.url: + raise SynapseError( + HTTPStatus.BAD_REQUEST, + "The application service does not have a URL set", + Codes.AS_PING_URL_NOT_SET, + ) + + parse_json_object_from_request(request) + + start = time.monotonic() + try: + await self.as_api.ping(requester.app_service) + except RequestTimedOutError as e: + raise SynapseError( + HTTPStatus.GATEWAY_TIMEOUT, + e.msg, + Codes.AS_PING_CONNECTION_TIMEOUT, + ) + except CodeMessageException as e: + additional_fields = {"status": e.code} + if isinstance(e, HttpResponseException): + try: + additional_fields["body"] = e.response.decode("utf-8") + except UnicodeDecodeError: + pass + raise SynapseError( + HTTPStatus.BAD_GATEWAY, + f"HTTP {e.code} {e.msg}", + Codes.AS_PING_BAD_STATUS, + additional_fields=additional_fields, + ) + except Exception as e: + raise SynapseError( + HTTPStatus.BAD_GATEWAY, + f"{type(e).__name__}: {e}", + Codes.AS_PING_CONNECTION_FAILED, + ) + + duration = time.monotonic() - start + + return HTTPStatus.OK, {"duration": int(duration * 1000)} + + +def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: + if hs.config.experimental.msc2659_enabled: + AppservicePingRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index e19c0946c035..dba0f0891ac6 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -109,6 +109,8 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]: "org.matrix.msc3773": self.config.experimental.msc3773_enabled, # Allows moderators to fetch redacted event content as described in MSC2815 "fi.mau.msc2815": self.config.experimental.msc2815_enabled, + # Adds a ping endpoint for appservices to check HS->AS connection + "fi.mau.msc2659": self.config.experimental.msc2659_enabled, # Adds support for login token requests as per MSC3882 "org.matrix.msc3882": self.config.experimental.msc3882_enabled, # Adds support for remotely enabling/disabling pushers, as per MSC3881 From 1a129787e687afaebcfe45859049fdf7100d57ea Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 11 Mar 2023 16:11:25 +0200 Subject: [PATCH 2/5] Add dot --- changelog.d/15249.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/15249.feature b/changelog.d/15249.feature index 19bcd7c75305..92d48a208723 100644 --- a/changelog.d/15249.feature +++ b/changelog.d/15249.feature @@ -1 +1 @@ -Implement [MSC2659](https://github.com/matrix-org/matrix-spec-proposals/pull/2659): application service ping endpoint. Contributed by Tulir @ Beeper +Implement [MSC2659](https://github.com/matrix-org/matrix-spec-proposals/pull/2659): application service ping endpoint. Contributed by Tulir @ Beeper. From c34483861ffa24fe1d23bfc5177c4b90773074b6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 11 Mar 2023 16:17:46 +0200 Subject: [PATCH 3/5] Fix types --- synapse/rest/client/appservice_ping.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/rest/client/appservice_ping.py b/synapse/rest/client/appservice_ping.py index 5f272df19e40..23276027fa2f 100644 --- a/synapse/rest/client/appservice_ping.py +++ b/synapse/rest/client/appservice_ping.py @@ -15,7 +15,7 @@ import logging import time from http import HTTPStatus -from typing import TYPE_CHECKING, Tuple +from typing import TYPE_CHECKING, Tuple, Dict, Any from synapse.api.errors import ( CodeMessageException, @@ -77,7 +77,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: Codes.AS_PING_CONNECTION_TIMEOUT, ) except CodeMessageException as e: - additional_fields = {"status": e.code} + additional_fields: Dict[str, Any] = {"status": e.code} if isinstance(e, HttpResponseException): try: additional_fields["body"] = e.response.decode("utf-8") From 012a759793815d4a01da2d8c7a903711afe4b363 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 11 Mar 2023 16:26:04 +0200 Subject: [PATCH 4/5] Fix new imports --- synapse/rest/client/appservice_ping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/rest/client/appservice_ping.py b/synapse/rest/client/appservice_ping.py index 23276027fa2f..8f197ef27d16 100644 --- a/synapse/rest/client/appservice_ping.py +++ b/synapse/rest/client/appservice_ping.py @@ -15,7 +15,7 @@ import logging import time from http import HTTPStatus -from typing import TYPE_CHECKING, Tuple, Dict, Any +from typing import TYPE_CHECKING, Any, Dict, Tuple from synapse.api.errors import ( CodeMessageException, From d1af4d33416438357177d1e9b32061523e44b384 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 11 Mar 2023 16:51:43 +0200 Subject: [PATCH 5/5] Add parameters to appservice ping endpoint Signed-off-by: Tulir Asokan --- synapse/appservice/api.py | 4 ++-- synapse/rest/client/appservice_ping.py | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/synapse/appservice/api.py b/synapse/appservice/api.py index 1eee7fec886e..4812fb44961f 100644 --- a/synapse/appservice/api.py +++ b/synapse/appservice/api.py @@ -266,7 +266,7 @@ async def _get() -> Optional[JsonDict]: key = (service.id, protocol) return await self.protocol_meta_cache.wrap(key, _get) - async def ping(self, service: "ApplicationService") -> None: + async def ping(self, service: "ApplicationService", txn_id: Optional[str]) -> None: # The caller should check that url is set assert service.url is not None, "ping called without URL being set" @@ -275,7 +275,7 @@ async def ping(self, service: "ApplicationService") -> None: await self.post_json_get_json( uri=service.url + "/_matrix/app/unstable/fi.mau.msc2659/ping", - post_json={}, + post_json={"transaction_id": txn_id}, headers={"Authorization": [f"Bearer {service.hs_token}"]}, ) diff --git a/synapse/rest/client/appservice_ping.py b/synapse/rest/client/appservice_ping.py index 8f197ef27d16..31466a4ad4cb 100644 --- a/synapse/rest/client/appservice_ping.py +++ b/synapse/rest/client/appservice_ping.py @@ -39,7 +39,7 @@ class AppservicePingRestServlet(RestServlet): PATTERNS = client_patterns( - "/fi.mau.msc2659/appservice/ping", + "/fi.mau.msc2659/appservice/(?P[^/]*)/ping", unstable=True, releases=(), ) @@ -49,7 +49,9 @@ def __init__(self, hs: "HomeServer"): self.as_api = hs.get_application_service_api() self.auth = hs.get_auth() - async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: + async def on_POST( + self, request: SynapseRequest, appservice_id: str + ) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) if not requester.app_service: @@ -58,6 +60,12 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: "Only application services can use the /appservice/ping endpoint", Codes.FORBIDDEN, ) + elif requester.app_service.id != appservice_id: + raise SynapseError( + HTTPStatus.FORBIDDEN, + "Mismatching application service ID in path", + Codes.FORBIDDEN, + ) elif not requester.app_service.url: raise SynapseError( HTTPStatus.BAD_REQUEST, @@ -65,11 +73,12 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: Codes.AS_PING_URL_NOT_SET, ) - parse_json_object_from_request(request) + content = parse_json_object_from_request(request) + txn_id = content.get("transaction_id", None) start = time.monotonic() try: - await self.as_api.ping(requester.app_service) + await self.as_api.ping(requester.app_service, txn_id) except RequestTimedOutError as e: raise SynapseError( HTTPStatus.GATEWAY_TIMEOUT,