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

Implement MSC2659: application service ping endpoint #15249

Merged
merged 5 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/15249.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement [MSC2659](https://github.com/matrix-org/matrix-spec-proposals/pull/2659): application service ping endpoint. Contributed by Tulir @ Beeper.
5 changes: 5 additions & 0 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 13 additions & 0 deletions synapse/appservice/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", 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"

# 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={"transaction_id": txn_id},
headers={"Authorization": [f"Bearer {service.hs_token}"]},
)

async def push_bulk(
self,
service: "ApplicationService",
Expand Down
3 changes: 3 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 2 additions & 0 deletions synapse/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
account,
account_data,
account_validity,
appservice_ping,
auth,
capabilities,
devices,
Expand Down Expand Up @@ -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:
Expand Down
115 changes: 115 additions & 0 deletions synapse/rest/client/appservice_ping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# 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, Any, Dict, 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/(?P<appservice_id>[^/]*)/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, appservice_id: str
) -> 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 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,
"The application service does not have a URL set",
Codes.AS_PING_URL_NOT_SET,
)

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, txn_id)
except RequestTimedOutError as e:
raise SynapseError(
HTTPStatus.GATEWAY_TIMEOUT,
e.msg,
Codes.AS_PING_CONNECTION_TIMEOUT,
)
except CodeMessageException as e:
additional_fields: Dict[str, Any] = {"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)}
MatMaul marked this conversation as resolved.
Show resolved Hide resolved


def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
if hs.config.experimental.msc2659_enabled:
AppservicePingRestServlet(hs).register(http_server)
2 changes: 2 additions & 0 deletions synapse/rest/client/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down