diff options
author | reivilibre <38398653+reivilibre@users.noreply.github.com> | 2021-08-17 12:57:58 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-17 11:57:58 +0000 |
commit | 642a42eddece60afbbd5e5a6659fa9b939238b4a (patch) | |
tree | fde46b103f636a302d6a47aadf368e19e4fc43da /synapse/rest/client/v1 | |
parent | Always list fallback key types in /sync (#10623) (diff) | |
download | synapse-642a42eddece60afbbd5e5a6659fa9b939238b4a.tar.xz |
Flatten the synapse.rest.client package (#10600)
Diffstat (limited to 'synapse/rest/client/v1')
-rw-r--r-- | synapse/rest/client/v1/__init__.py | 13 | ||||
-rw-r--r-- | synapse/rest/client/v1/directory.py | 185 | ||||
-rw-r--r-- | synapse/rest/client/v1/events.py | 94 | ||||
-rw-r--r-- | synapse/rest/client/v1/initial_sync.py | 47 | ||||
-rw-r--r-- | synapse/rest/client/v1/login.py | 600 | ||||
-rw-r--r-- | synapse/rest/client/v1/logout.py | 72 | ||||
-rw-r--r-- | synapse/rest/client/v1/presence.py | 95 | ||||
-rw-r--r-- | synapse/rest/client/v1/profile.py | 155 | ||||
-rw-r--r-- | synapse/rest/client/v1/push_rule.py | 354 | ||||
-rw-r--r-- | synapse/rest/client/v1/pusher.py | 171 | ||||
-rw-r--r-- | synapse/rest/client/v1/room.py | 1152 | ||||
-rw-r--r-- | synapse/rest/client/v1/voip.py | 73 |
12 files changed, 0 insertions, 3011 deletions
diff --git a/synapse/rest/client/v1/__init__.py b/synapse/rest/client/v1/__init__.py deleted file mode 100644 index 5e83dba2ed..0000000000 --- a/synapse/rest/client/v1/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# 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. diff --git a/synapse/rest/client/v1/directory.py b/synapse/rest/client/v1/directory.py deleted file mode 100644 index ae92a3df8e..0000000000 --- a/synapse/rest/client/v1/directory.py +++ /dev/null @@ -1,185 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# 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 - -from synapse.api.errors import ( - AuthError, - Codes, - InvalidClientCredentialsError, - NotFoundError, - SynapseError, -) -from synapse.http.servlet import RestServlet, parse_json_object_from_request -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.types import RoomAlias - -logger = logging.getLogger(__name__) - - -def register_servlets(hs, http_server): - ClientDirectoryServer(hs).register(http_server) - ClientDirectoryListServer(hs).register(http_server) - ClientAppserviceDirectoryListServer(hs).register(http_server) - - -class ClientDirectoryServer(RestServlet): - PATTERNS = client_patterns("/directory/room/(?P<room_alias>[^/]*)$", v1=True) - - def __init__(self, hs): - super().__init__() - self.store = hs.get_datastore() - self.directory_handler = hs.get_directory_handler() - self.auth = hs.get_auth() - - async def on_GET(self, request, room_alias): - room_alias = RoomAlias.from_string(room_alias) - - res = await self.directory_handler.get_association(room_alias) - - return 200, res - - async def on_PUT(self, request, room_alias): - room_alias = RoomAlias.from_string(room_alias) - - content = parse_json_object_from_request(request) - if "room_id" not in content: - raise SynapseError( - 400, 'Missing params: ["room_id"]', errcode=Codes.BAD_JSON - ) - - logger.debug("Got content: %s", content) - logger.debug("Got room name: %s", room_alias.to_string()) - - room_id = content["room_id"] - servers = content["servers"] if "servers" in content else None - - logger.debug("Got room_id: %s", room_id) - logger.debug("Got servers: %s", servers) - - # TODO(erikj): Check types. - - room = await self.store.get_room(room_id) - if room is None: - raise SynapseError(400, "Room does not exist") - - requester = await self.auth.get_user_by_req(request) - - await self.directory_handler.create_association( - requester, room_alias, room_id, servers - ) - - return 200, {} - - async def on_DELETE(self, request, room_alias): - try: - service = self.auth.get_appservice_by_req(request) - room_alias = RoomAlias.from_string(room_alias) - await self.directory_handler.delete_appservice_association( - service, room_alias - ) - logger.info( - "Application service at %s deleted alias %s", - service.url, - room_alias.to_string(), - ) - return 200, {} - except InvalidClientCredentialsError: - # fallback to default user behaviour if they aren't an AS - pass - - requester = await self.auth.get_user_by_req(request) - user = requester.user - - room_alias = RoomAlias.from_string(room_alias) - - await self.directory_handler.delete_association(requester, room_alias) - - logger.info( - "User %s deleted alias %s", user.to_string(), room_alias.to_string() - ) - - return 200, {} - - -class ClientDirectoryListServer(RestServlet): - PATTERNS = client_patterns("/directory/list/room/(?P<room_id>[^/]*)$", v1=True) - - def __init__(self, hs): - super().__init__() - self.store = hs.get_datastore() - self.directory_handler = hs.get_directory_handler() - self.auth = hs.get_auth() - - async def on_GET(self, request, room_id): - room = await self.store.get_room(room_id) - if room is None: - raise NotFoundError("Unknown room") - - return 200, {"visibility": "public" if room["is_public"] else "private"} - - async def on_PUT(self, request, room_id): - requester = await self.auth.get_user_by_req(request) - - content = parse_json_object_from_request(request) - visibility = content.get("visibility", "public") - - await self.directory_handler.edit_published_room_list( - requester, room_id, visibility - ) - - return 200, {} - - async def on_DELETE(self, request, room_id): - requester = await self.auth.get_user_by_req(request) - - await self.directory_handler.edit_published_room_list( - requester, room_id, "private" - ) - - return 200, {} - - -class ClientAppserviceDirectoryListServer(RestServlet): - PATTERNS = client_patterns( - "/directory/list/appservice/(?P<network_id>[^/]*)/(?P<room_id>[^/]*)$", v1=True - ) - - def __init__(self, hs): - super().__init__() - self.store = hs.get_datastore() - self.directory_handler = hs.get_directory_handler() - self.auth = hs.get_auth() - - def on_PUT(self, request, network_id, room_id): - content = parse_json_object_from_request(request) - visibility = content.get("visibility", "public") - return self._edit(request, network_id, room_id, visibility) - - def on_DELETE(self, request, network_id, room_id): - return self._edit(request, network_id, room_id, "private") - - async def _edit(self, request, network_id, room_id, visibility): - requester = await self.auth.get_user_by_req(request) - if not requester.app_service: - raise AuthError( - 403, "Only appservices can edit the appservice published room list" - ) - - await self.directory_handler.edit_published_appservice_room_list( - requester.app_service.id, network_id, room_id, visibility - ) - - return 200, {} diff --git a/synapse/rest/client/v1/events.py b/synapse/rest/client/v1/events.py deleted file mode 100644 index ee7454996e..0000000000 --- a/synapse/rest/client/v1/events.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# 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. - -"""This module contains REST servlets to do with event streaming, /events.""" -import logging - -from synapse.api.errors import SynapseError -from synapse.http.servlet import RestServlet -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.streams.config import PaginationConfig - -logger = logging.getLogger(__name__) - - -class EventStreamRestServlet(RestServlet): - PATTERNS = client_patterns("/events$", v1=True) - - DEFAULT_LONGPOLL_TIME_MS = 30000 - - def __init__(self, hs): - super().__init__() - self.event_stream_handler = hs.get_event_stream_handler() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - is_guest = requester.is_guest - room_id = None - if is_guest: - if b"room_id" not in request.args: - raise SynapseError(400, "Guest users must specify room_id param") - if b"room_id" in request.args: - room_id = request.args[b"room_id"][0].decode("ascii") - - pagin_config = await PaginationConfig.from_request(self.store, request) - timeout = EventStreamRestServlet.DEFAULT_LONGPOLL_TIME_MS - if b"timeout" in request.args: - try: - timeout = int(request.args[b"timeout"][0]) - except ValueError: - raise SynapseError(400, "timeout must be in milliseconds.") - - as_client_event = b"raw" not in request.args - - chunk = await self.event_stream_handler.get_stream( - requester.user.to_string(), - pagin_config, - timeout=timeout, - as_client_event=as_client_event, - affect_presence=(not is_guest), - room_id=room_id, - is_guest=is_guest, - ) - - return 200, chunk - - -class EventRestServlet(RestServlet): - PATTERNS = client_patterns("/events/(?P<event_id>[^/]*)$", v1=True) - - def __init__(self, hs): - super().__init__() - self.clock = hs.get_clock() - self.event_handler = hs.get_event_handler() - self.auth = hs.get_auth() - self._event_serializer = hs.get_event_client_serializer() - - async def on_GET(self, request, event_id): - requester = await self.auth.get_user_by_req(request) - event = await self.event_handler.get_event(requester.user, None, event_id) - - time_now = self.clock.time_msec() - if event: - event = await self._event_serializer.serialize_event(event, time_now) - return 200, event - else: - return 404, "Event not found." - - -def register_servlets(hs, http_server): - EventStreamRestServlet(hs).register(http_server) - EventRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/initial_sync.py b/synapse/rest/client/v1/initial_sync.py deleted file mode 100644 index bef1edc838..0000000000 --- a/synapse/rest/client/v1/initial_sync.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# 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. - - -from synapse.http.servlet import RestServlet, parse_boolean -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.streams.config import PaginationConfig - - -# TODO: Needs unit testing -class InitialSyncRestServlet(RestServlet): - PATTERNS = client_patterns("/initialSync$", v1=True) - - def __init__(self, hs): - super().__init__() - self.initial_sync_handler = hs.get_initial_sync_handler() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req(request) - as_client_event = b"raw" not in request.args - pagination_config = await PaginationConfig.from_request(self.store, request) - include_archived = parse_boolean(request, "archived", default=False) - content = await self.initial_sync_handler.snapshot_all_rooms( - user_id=requester.user.to_string(), - pagin_config=pagination_config, - as_client_event=as_client_event, - include_archived=include_archived, - ) - - return 200, content - - -def register_servlets(hs, http_server): - InitialSyncRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py deleted file mode 100644 index 11567bf32c..0000000000 --- a/synapse/rest/client/v1/login.py +++ /dev/null @@ -1,600 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# 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 re -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional - -from typing_extensions import TypedDict - -from synapse.api.errors import Codes, LoginError, SynapseError -from synapse.api.ratelimiting import Ratelimiter -from synapse.api.urls import CLIENT_API_PREFIX -from synapse.appservice import ApplicationService -from synapse.handlers.sso import SsoIdentityProvider -from synapse.http import get_request_uri -from synapse.http.server import HttpServer, finish_request -from synapse.http.servlet import ( - RestServlet, - assert_params_in_dict, - parse_boolean, - parse_bytes_from_args, - parse_json_object_from_request, - parse_string, -) -from synapse.http.site import SynapseRequest -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.rest.well_known import WellKnownBuilder -from synapse.types import JsonDict, UserID - -if TYPE_CHECKING: - from synapse.server import HomeServer - -logger = logging.getLogger(__name__) - - -class LoginResponse(TypedDict, total=False): - user_id: str - access_token: str - home_server: str - expires_in_ms: Optional[int] - refresh_token: Optional[str] - device_id: str - well_known: Optional[Dict[str, Any]] - - -class LoginRestServlet(RestServlet): - PATTERNS = client_patterns("/login$", v1=True) - CAS_TYPE = "m.login.cas" - SSO_TYPE = "m.login.sso" - TOKEN_TYPE = "m.login.token" - JWT_TYPE = "org.matrix.login.jwt" - JWT_TYPE_DEPRECATED = "m.login.jwt" - APPSERVICE_TYPE = "uk.half-shot.msc2778.login.application_service" - REFRESH_TOKEN_PARAM = "org.matrix.msc2918.refresh_token" - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.hs = hs - - # JWT configuration variables. - self.jwt_enabled = hs.config.jwt_enabled - self.jwt_secret = hs.config.jwt_secret - self.jwt_algorithm = hs.config.jwt_algorithm - self.jwt_issuer = hs.config.jwt_issuer - self.jwt_audiences = hs.config.jwt_audiences - - # SSO configuration. - self.saml2_enabled = hs.config.saml2_enabled - self.cas_enabled = hs.config.cas_enabled - self.oidc_enabled = hs.config.oidc_enabled - self._msc2858_enabled = hs.config.experimental.msc2858_enabled - self._msc2918_enabled = hs.config.access_token_lifetime is not None - - self.auth = hs.get_auth() - - self.clock = hs.get_clock() - - self.auth_handler = self.hs.get_auth_handler() - self.registration_handler = hs.get_registration_handler() - self._sso_handler = hs.get_sso_handler() - - self._well_known_builder = WellKnownBuilder(hs) - self._address_ratelimiter = Ratelimiter( - store=hs.get_datastore(), - clock=hs.get_clock(), - rate_hz=self.hs.config.rc_login_address.per_second, - burst_count=self.hs.config.rc_login_address.burst_count, - ) - self._account_ratelimiter = Ratelimiter( - store=hs.get_datastore(), - clock=hs.get_clock(), - rate_hz=self.hs.config.rc_login_account.per_second, - burst_count=self.hs.config.rc_login_account.burst_count, - ) - - def on_GET(self, request: SynapseRequest): - flows = [] - if self.jwt_enabled: - flows.append({"type": LoginRestServlet.JWT_TYPE}) - flows.append({"type": LoginRestServlet.JWT_TYPE_DEPRECATED}) - - if self.cas_enabled: - # we advertise CAS for backwards compat, though MSC1721 renamed it - # to SSO. - flows.append({"type": LoginRestServlet.CAS_TYPE}) - - if self.cas_enabled or self.saml2_enabled or self.oidc_enabled: - sso_flow: JsonDict = { - "type": LoginRestServlet.SSO_TYPE, - "identity_providers": [ - _get_auth_flow_dict_for_idp( - idp, - ) - for idp in self._sso_handler.get_identity_providers().values() - ], - } - - if self._msc2858_enabled: - # backwards-compatibility support for clients which don't - # support the stable API yet - sso_flow["org.matrix.msc2858.identity_providers"] = [ - _get_auth_flow_dict_for_idp(idp, use_unstable_brands=True) - for idp in self._sso_handler.get_identity_providers().values() - ] - - flows.append(sso_flow) - - # While it's valid for us to advertise this login type generally, - # synapse currently only gives out these tokens as part of the - # SSO login flow. - # Generally we don't want to advertise login flows that clients - # don't know how to implement, since they (currently) will always - # fall back to the fallback API if they don't understand one of the - # login flow types returned. - flows.append({"type": LoginRestServlet.TOKEN_TYPE}) - - flows.extend({"type": t} for t in self.auth_handler.get_supported_login_types()) - - flows.append({"type": LoginRestServlet.APPSERVICE_TYPE}) - - return 200, {"flows": flows} - - async def on_POST(self, request: SynapseRequest): - login_submission = parse_json_object_from_request(request) - - if self._msc2918_enabled: - # Check if this login should also issue a refresh token, as per - # MSC2918 - should_issue_refresh_token = parse_boolean( - request, name=LoginRestServlet.REFRESH_TOKEN_PARAM, default=False - ) - else: - should_issue_refresh_token = False - - try: - if login_submission["type"] == LoginRestServlet.APPSERVICE_TYPE: - appservice = self.auth.get_appservice_by_req(request) - - if appservice.is_rate_limited(): - await self._address_ratelimiter.ratelimit( - None, request.getClientIP() - ) - - result = await self._do_appservice_login( - login_submission, - appservice, - should_issue_refresh_token=should_issue_refresh_token, - ) - elif self.jwt_enabled and ( - login_submission["type"] == LoginRestServlet.JWT_TYPE - or login_submission["type"] == LoginRestServlet.JWT_TYPE_DEPRECATED - ): - await self._address_ratelimiter.ratelimit(None, request.getClientIP()) - result = await self._do_jwt_login( - login_submission, - should_issue_refresh_token=should_issue_refresh_token, - ) - elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE: - await self._address_ratelimiter.ratelimit(None, request.getClientIP()) - result = await self._do_token_login( - login_submission, - should_issue_refresh_token=should_issue_refresh_token, - ) - else: - await self._address_ratelimiter.ratelimit(None, request.getClientIP()) - result = await self._do_other_login( - login_submission, - should_issue_refresh_token=should_issue_refresh_token, - ) - except KeyError: - raise SynapseError(400, "Missing JSON keys.") - - well_known_data = self._well_known_builder.get_well_known() - if well_known_data: - result["well_known"] = well_known_data - return 200, result - - async def _do_appservice_login( - self, - login_submission: JsonDict, - appservice: ApplicationService, - should_issue_refresh_token: bool = False, - ): - identifier = login_submission.get("identifier") - logger.info("Got appservice login request with identifier: %r", identifier) - - if not isinstance(identifier, dict): - raise SynapseError( - 400, "Invalid identifier in login submission", Codes.INVALID_PARAM - ) - - # this login flow only supports identifiers of type "m.id.user". - if identifier.get("type") != "m.id.user": - raise SynapseError( - 400, "Unknown login identifier type", Codes.INVALID_PARAM - ) - - user = identifier.get("user") - if not isinstance(user, str): - raise SynapseError(400, "Invalid user in identifier", Codes.INVALID_PARAM) - - if user.startswith("@"): - qualified_user_id = user - else: - qualified_user_id = UserID(user, self.hs.hostname).to_string() - - if not appservice.is_interested_in_user(qualified_user_id): - raise LoginError(403, "Invalid access_token", errcode=Codes.FORBIDDEN) - - return await self._complete_login( - qualified_user_id, - login_submission, - ratelimit=appservice.is_rate_limited(), - should_issue_refresh_token=should_issue_refresh_token, - ) - - async def _do_other_login( - self, login_submission: JsonDict, should_issue_refresh_token: bool = False - ) -> LoginResponse: - """Handle non-token/saml/jwt logins - - Args: - login_submission: - should_issue_refresh_token: True if this login should issue - a refresh token alongside the access token. - - Returns: - HTTP response - """ - # Log the request we got, but only certain fields to minimise the chance of - # logging someone's password (even if they accidentally put it in the wrong - # field) - logger.info( - "Got login request with identifier: %r, medium: %r, address: %r, user: %r", - login_submission.get("identifier"), - login_submission.get("medium"), - login_submission.get("address"), - login_submission.get("user"), - ) - canonical_user_id, callback = await self.auth_handler.validate_login( - login_submission, ratelimit=True - ) - result = await self._complete_login( - canonical_user_id, - login_submission, - callback, - should_issue_refresh_token=should_issue_refresh_token, - ) - return result - - async def _complete_login( - self, - user_id: str, - login_submission: JsonDict, - callback: Optional[Callable[[LoginResponse], Awaitable[None]]] = None, - create_non_existent_users: bool = False, - ratelimit: bool = True, - auth_provider_id: Optional[str] = None, - should_issue_refresh_token: bool = False, - ) -> LoginResponse: - """Called when we've successfully authed the user and now need to - actually login them in (e.g. create devices). This gets called on - all successful logins. - - Applies the ratelimiting for successful login attempts against an - account. - - Args: - user_id: ID of the user to register. - login_submission: Dictionary of login information. - callback: Callback function to run after login. - create_non_existent_users: Whether to create the user if they don't - exist. Defaults to False. - ratelimit: Whether to ratelimit the login request. - auth_provider_id: The SSO IdP the user used, if any (just used for the - prometheus metrics). - should_issue_refresh_token: True if this login should issue - a refresh token alongside the access token. - - Returns: - result: Dictionary of account information after successful login. - """ - - # Before we actually log them in we check if they've already logged in - # too often. This happens here rather than before as we don't - # necessarily know the user before now. - if ratelimit: - await self._account_ratelimiter.ratelimit(None, user_id.lower()) - - if create_non_existent_users: - canonical_uid = await self.auth_handler.check_user_exists(user_id) - if not canonical_uid: - canonical_uid = await self.registration_handler.register_user( - localpart=UserID.from_string(user_id).localpart - ) - user_id = canonical_uid - - device_id = login_submission.get("device_id") - initial_display_name = login_submission.get("initial_device_display_name") - ( - device_id, - access_token, - valid_until_ms, - refresh_token, - ) = await self.registration_handler.register_device( - user_id, - device_id, - initial_display_name, - auth_provider_id=auth_provider_id, - should_issue_refresh_token=should_issue_refresh_token, - ) - - result = LoginResponse( - user_id=user_id, - access_token=access_token, - home_server=self.hs.hostname, - device_id=device_id, - ) - - if valid_until_ms is not None: - expires_in_ms = valid_until_ms - self.clock.time_msec() - result["expires_in_ms"] = expires_in_ms - - if refresh_token is not None: - result["refresh_token"] = refresh_token - - if callback is not None: - await callback(result) - - return result - - async def _do_token_login( - self, login_submission: JsonDict, should_issue_refresh_token: bool = False - ) -> LoginResponse: - """ - Handle the final stage of SSO login. - - Args: - login_submission: The JSON request body. - should_issue_refresh_token: True if this login should issue - a refresh token alongside the access token. - - Returns: - The body of the JSON response. - """ - token = login_submission["token"] - auth_handler = self.auth_handler - res = await auth_handler.validate_short_term_login_token(token) - - return await self._complete_login( - res.user_id, - login_submission, - self.auth_handler._sso_login_callback, - auth_provider_id=res.auth_provider_id, - should_issue_refresh_token=should_issue_refresh_token, - ) - - async def _do_jwt_login( - self, login_submission: JsonDict, should_issue_refresh_token: bool = False - ) -> LoginResponse: - token = login_submission.get("token", None) - if token is None: - raise LoginError( - 403, "Token field for JWT is missing", errcode=Codes.FORBIDDEN - ) - - import jwt - - try: - payload = jwt.decode( - token, - self.jwt_secret, - algorithms=[self.jwt_algorithm], - issuer=self.jwt_issuer, - audience=self.jwt_audiences, - ) - except jwt.PyJWTError as e: - # A JWT error occurred, return some info back to the client. - raise LoginError( - 403, - "JWT validation failed: %s" % (str(e),), - errcode=Codes.FORBIDDEN, - ) - - user = payload.get("sub", None) - if user is None: - raise LoginError(403, "Invalid JWT", errcode=Codes.FORBIDDEN) - - user_id = UserID(user, self.hs.hostname).to_string() - result = await self._complete_login( - user_id, - login_submission, - create_non_existent_users=True, - should_issue_refresh_token=should_issue_refresh_token, - ) - return result - - -def _get_auth_flow_dict_for_idp( - idp: SsoIdentityProvider, use_unstable_brands: bool = False -) -> JsonDict: - """Return an entry for the login flow dict - - Returns an entry suitable for inclusion in "identity_providers" in the - response to GET /_matrix/client/r0/login - - Args: - idp: the identity provider to describe - use_unstable_brands: whether we should use brand identifiers suitable - for the unstable API - """ - e: JsonDict = {"id": idp.idp_id, "name": idp.idp_name} - if idp.idp_icon: - e["icon"] = idp.idp_icon - if idp.idp_brand: - e["brand"] = idp.idp_brand - # use the stable brand identifier if the unstable identifier isn't defined. - if use_unstable_brands and idp.unstable_idp_brand: - e["brand"] = idp.unstable_idp_brand - return e - - -class RefreshTokenServlet(RestServlet): - PATTERNS = client_patterns( - "/org.matrix.msc2918.refresh_token/refresh$", releases=(), unstable=True - ) - - def __init__(self, hs: "HomeServer"): - self._auth_handler = hs.get_auth_handler() - self._clock = hs.get_clock() - self.access_token_lifetime = hs.config.access_token_lifetime - - async def on_POST( - self, - request: SynapseRequest, - ): - refresh_submission = parse_json_object_from_request(request) - - assert_params_in_dict(refresh_submission, ["refresh_token"]) - token = refresh_submission["refresh_token"] - if not isinstance(token, str): - raise SynapseError(400, "Invalid param: refresh_token", Codes.INVALID_PARAM) - - valid_until_ms = self._clock.time_msec() + self.access_token_lifetime - access_token, refresh_token = await self._auth_handler.refresh_token( - token, valid_until_ms - ) - expires_in_ms = valid_until_ms - self._clock.time_msec() - return ( - 200, - { - "access_token": access_token, - "refresh_token": refresh_token, - "expires_in_ms": expires_in_ms, - }, - ) - - -class SsoRedirectServlet(RestServlet): - PATTERNS = list(client_patterns("/login/(cas|sso)/redirect$", v1=True)) + [ - re.compile( - "^" - + CLIENT_API_PREFIX - + "/r0/login/sso/redirect/(?P<idp_id>[A-Za-z0-9_.~-]+)$" - ) - ] - - def __init__(self, hs: "HomeServer"): - # make sure that the relevant handlers are instantiated, so that they - # register themselves with the main SSOHandler. - if hs.config.cas_enabled: - hs.get_cas_handler() - if hs.config.saml2_enabled: - hs.get_saml_handler() - if hs.config.oidc_enabled: - hs.get_oidc_handler() - self._sso_handler = hs.get_sso_handler() - self._msc2858_enabled = hs.config.experimental.msc2858_enabled - self._public_baseurl = hs.config.public_baseurl - - def register(self, http_server: HttpServer) -> None: - super().register(http_server) - if self._msc2858_enabled: - # expose additional endpoint for MSC2858 support: backwards-compat support - # for clients which don't yet support the stable endpoints. - http_server.register_paths( - "GET", - client_patterns( - "/org.matrix.msc2858/login/sso/redirect/(?P<idp_id>[A-Za-z0-9_.~-]+)$", - releases=(), - unstable=True, - ), - self.on_GET, - self.__class__.__name__, - ) - - async def on_GET( - self, request: SynapseRequest, idp_id: Optional[str] = None - ) -> None: - if not self._public_baseurl: - raise SynapseError(400, "SSO requires a valid public_baseurl") - - # if this isn't the expected hostname, redirect to the right one, so that we - # get our cookies back. - requested_uri = get_request_uri(request) - baseurl_bytes = self._public_baseurl.encode("utf-8") - if not requested_uri.startswith(baseurl_bytes): - # swap out the incorrect base URL for the right one. - # - # The idea here is to redirect from - # https://foo.bar/whatever/_matrix/... - # to - # https://public.baseurl/_matrix/... - # - i = requested_uri.index(b"/_matrix") - new_uri = baseurl_bytes[:-1] + requested_uri[i:] - logger.info( - "Requested URI %s is not canonical: redirecting to %s", - requested_uri.decode("utf-8", errors="replace"), - new_uri.decode("utf-8", errors="replace"), - ) - request.redirect(new_uri) - finish_request(request) - return - - args: Dict[bytes, List[bytes]] = request.args # type: ignore - client_redirect_url = parse_bytes_from_args(args, "redirectUrl", required=True) - sso_url = await self._sso_handler.handle_redirect_request( - request, - client_redirect_url, - idp_id, - ) - logger.info("Redirecting to %s", sso_url) - request.redirect(sso_url) - finish_request(request) - - -class CasTicketServlet(RestServlet): - PATTERNS = client_patterns("/login/cas/ticket", v1=True) - - def __init__(self, hs): - super().__init__() - self._cas_handler = hs.get_cas_handler() - - async def on_GET(self, request: SynapseRequest) -> None: - client_redirect_url = parse_string(request, "redirectUrl") - ticket = parse_string(request, "ticket", required=True) - - # Maybe get a session ID (if this ticket is from user interactive - # authentication). - session = parse_string(request, "session") - - # Either client_redirect_url or session must be provided. - if not client_redirect_url and not session: - message = "Missing string query parameter redirectUrl or session" - raise SynapseError(400, message, errcode=Codes.MISSING_PARAM) - - await self._cas_handler.handle_ticket( - request, ticket, client_redirect_url, session - ) - - -def register_servlets(hs, http_server): - LoginRestServlet(hs).register(http_server) - if hs.config.access_token_lifetime is not None: - RefreshTokenServlet(hs).register(http_server) - SsoRedirectServlet(hs).register(http_server) - if hs.config.cas_enabled: - CasTicketServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/logout.py b/synapse/rest/client/v1/logout.py deleted file mode 100644 index 5aa7908d73..0000000000 --- a/synapse/rest/client/v1/logout.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright 2016 OpenMarket Ltd -# -# 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 - -from synapse.http.servlet import RestServlet -from synapse.rest.client.v2_alpha._base import client_patterns - -logger = logging.getLogger(__name__) - - -class LogoutRestServlet(RestServlet): - PATTERNS = client_patterns("/logout$", v1=True) - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self._auth_handler = hs.get_auth_handler() - self._device_handler = hs.get_device_handler() - - async def on_POST(self, request): - requester = await self.auth.get_user_by_req(request, allow_expired=True) - - if requester.device_id is None: - # The access token wasn't associated with a device. - # Just delete the access token - access_token = self.auth.get_access_token_from_request(request) - await self._auth_handler.delete_access_token(access_token) - else: - await self._device_handler.delete_device( - requester.user.to_string(), requester.device_id - ) - - return 200, {} - - -class LogoutAllRestServlet(RestServlet): - PATTERNS = client_patterns("/logout/all$", v1=True) - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self._auth_handler = hs.get_auth_handler() - self._device_handler = hs.get_device_handler() - - async def on_POST(self, request): - requester = await self.auth.get_user_by_req(request, allow_expired=True) - user_id = requester.user.to_string() - - # first delete all of the user's devices - await self._device_handler.delete_all_devices_for_user(user_id) - - # .. and then delete any access tokens which weren't associated with - # devices. - await self._auth_handler.delete_access_tokens_for_user(user_id) - return 200, {} - - -def register_servlets(hs, http_server): - LogoutRestServlet(hs).register(http_server) - LogoutAllRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/presence.py b/synapse/rest/client/v1/presence.py deleted file mode 100644 index 2b24fe5aa6..0000000000 --- a/synapse/rest/client/v1/presence.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# 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. - -""" This module contains REST servlets to do with presence: /presence/<paths> -""" -import logging - -from synapse.api.errors import AuthError, SynapseError -from synapse.handlers.presence import format_user_presence_state -from synapse.http.servlet import RestServlet, parse_json_object_from_request -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.types import UserID - -logger = logging.getLogger(__name__) - - -class PresenceStatusRestServlet(RestServlet): - PATTERNS = client_patterns("/presence/(?P<user_id>[^/]*)/status", v1=True) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.presence_handler = hs.get_presence_handler() - self.clock = hs.get_clock() - self.auth = hs.get_auth() - - self._use_presence = hs.config.server.use_presence - - async def on_GET(self, request, user_id): - requester = await self.auth.get_user_by_req(request) - user = UserID.from_string(user_id) - - if not self._use_presence: - return 200, {"presence": "offline"} - - if requester.user != user: - allowed = await self.presence_handler.is_visible( - observed_user=user, observer_user=requester.user - ) - - if not allowed: - raise AuthError(403, "You are not allowed to see their presence.") - - state = await self.presence_handler.get_state(target_user=user) - state = format_user_presence_state( - state, self.clock.time_msec(), include_user_id=False - ) - - return 200, state - - async def on_PUT(self, request, user_id): - requester = await self.auth.get_user_by_req(request) - user = UserID.from_string(user_id) - - if requester.user != user: - raise AuthError(403, "Can only set your own presence state") - - state = {} - - content = parse_json_object_from_request(request) - - try: - state["presence"] = content.pop("presence") - - if "status_msg" in content: - state["status_msg"] = content.pop("status_msg") - if not isinstance(state["status_msg"], str): - raise SynapseError(400, "status_msg must be a string.") - - if content: - raise KeyError() - except SynapseError as e: - raise e - except Exception: - raise SynapseError(400, "Unable to parse state") - - if self._use_presence: - await self.presence_handler.set_state(user, state) - - return 200, {} - - -def register_servlets(hs, http_server): - PresenceStatusRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/profile.py b/synapse/rest/client/v1/profile.py deleted file mode 100644 index f42f4b3567..0000000000 --- a/synapse/rest/client/v1/profile.py +++ /dev/null @@ -1,155 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# 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. - -""" This module contains REST servlets to do with profile: /profile/<paths> """ - -from synapse.api.errors import Codes, SynapseError -from synapse.http.servlet import RestServlet, parse_json_object_from_request -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.types import UserID - - -class ProfileDisplaynameRestServlet(RestServlet): - PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)/displayname", v1=True) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.profile_handler = hs.get_profile_handler() - self.auth = hs.get_auth() - - async def on_GET(self, request, user_id): - requester_user = None - - if self.hs.config.require_auth_for_profile_requests: - requester = await self.auth.get_user_by_req(request) - requester_user = requester.user - - user = UserID.from_string(user_id) - - await self.profile_handler.check_profile_query_allowed(user, requester_user) - - displayname = await self.profile_handler.get_displayname(user) - - ret = {} - if displayname is not None: - ret["displayname"] = displayname - - return 200, ret - - async def on_PUT(self, request, user_id): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - user = UserID.from_string(user_id) - is_admin = await self.auth.is_server_admin(requester.user) - - content = parse_json_object_from_request(request) - - try: - new_name = content["displayname"] - except Exception: - raise SynapseError( - code=400, - msg="Unable to parse name", - errcode=Codes.BAD_JSON, - ) - - await self.profile_handler.set_displayname(user, requester, new_name, is_admin) - - return 200, {} - - -class ProfileAvatarURLRestServlet(RestServlet): - PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)/avatar_url", v1=True) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.profile_handler = hs.get_profile_handler() - self.auth = hs.get_auth() - - async def on_GET(self, request, user_id): - requester_user = None - - if self.hs.config.require_auth_for_profile_requests: - requester = await self.auth.get_user_by_req(request) - requester_user = requester.user - - user = UserID.from_string(user_id) - - await self.profile_handler.check_profile_query_allowed(user, requester_user) - - avatar_url = await self.profile_handler.get_avatar_url(user) - - ret = {} - if avatar_url is not None: - ret["avatar_url"] = avatar_url - - return 200, ret - - async def on_PUT(self, request, user_id): - requester = await self.auth.get_user_by_req(request) - user = UserID.from_string(user_id) - is_admin = await self.auth.is_server_admin(requester.user) - - content = parse_json_object_from_request(request) - try: - new_avatar_url = content["avatar_url"] - except KeyError: - raise SynapseError( - 400, "Missing key 'avatar_url'", errcode=Codes.MISSING_PARAM - ) - - await self.profile_handler.set_avatar_url( - user, requester, new_avatar_url, is_admin - ) - - return 200, {} - - -class ProfileRestServlet(RestServlet): - PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)", v1=True) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.profile_handler = hs.get_profile_handler() - self.auth = hs.get_auth() - - async def on_GET(self, request, user_id): - requester_user = None - - if self.hs.config.require_auth_for_profile_requests: - requester = await self.auth.get_user_by_req(request) - requester_user = requester.user - - user = UserID.from_string(user_id) - - await self.profile_handler.check_profile_query_allowed(user, requester_user) - - displayname = await self.profile_handler.get_displayname(user) - avatar_url = await self.profile_handler.get_avatar_url(user) - - ret = {} - if displayname is not None: - ret["displayname"] = displayname - if avatar_url is not None: - ret["avatar_url"] = avatar_url - - return 200, ret - - -def register_servlets(hs, http_server): - ProfileDisplaynameRestServlet(hs).register(http_server) - ProfileAvatarURLRestServlet(hs).register(http_server) - ProfileRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/push_rule.py b/synapse/rest/client/v1/push_rule.py deleted file mode 100644 index be29a0b39e..0000000000 --- a/synapse/rest/client/v1/push_rule.py +++ /dev/null @@ -1,354 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# 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. - -from synapse.api.errors import ( - NotFoundError, - StoreError, - SynapseError, - UnrecognizedRequestError, -) -from synapse.http.servlet import ( - RestServlet, - parse_json_value_from_request, - parse_string, -) -from synapse.push.baserules import BASE_RULE_IDS, NEW_RULE_IDS -from synapse.push.clientformat import format_push_rules_for_user -from synapse.push.rulekinds import PRIORITY_CLASS_MAP -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.storage.push_rule import InconsistentRuleException, RuleNotFoundException - - -class PushRuleRestServlet(RestServlet): - PATTERNS = client_patterns("/(?P<path>pushrules/.*)$", v1=True) - SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR = ( - "Unrecognised request: You probably wanted a trailing slash" - ) - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - self.notifier = hs.get_notifier() - self._is_worker = hs.config.worker_app is not None - - self._users_new_default_push_rules = hs.config.users_new_default_push_rules - - async def on_PUT(self, request, path): - if self._is_worker: - raise Exception("Cannot handle PUT /push_rules on worker") - - spec = _rule_spec_from_path(path.split("/")) - try: - priority_class = _priority_class_from_spec(spec) - except InvalidRuleException as e: - raise SynapseError(400, str(e)) - - requester = await self.auth.get_user_by_req(request) - - if "/" in spec["rule_id"] or "\\" in spec["rule_id"]: - raise SynapseError(400, "rule_id may not contain slashes") - - content = parse_json_value_from_request(request) - - user_id = requester.user.to_string() - - if "attr" in spec: - await self.set_rule_attr(user_id, spec, content) - self.notify_user(user_id) - return 200, {} - - if spec["rule_id"].startswith("."): - # Rule ids starting with '.' are reserved for server default rules. - raise SynapseError(400, "cannot add new rule_ids that start with '.'") - - try: - (conditions, actions) = _rule_tuple_from_request_object( - spec["template"], spec["rule_id"], content - ) - except InvalidRuleException as e: - raise SynapseError(400, str(e)) - - before = parse_string(request, "before") - if before: - before = _namespaced_rule_id(spec, before) - - after = parse_string(request, "after") - if after: - after = _namespaced_rule_id(spec, after) - - try: - await self.store.add_push_rule( - user_id=user_id, - rule_id=_namespaced_rule_id_from_spec(spec), - priority_class=priority_class, - conditions=conditions, - actions=actions, - before=before, - after=after, - ) - self.notify_user(user_id) - except InconsistentRuleException as e: - raise SynapseError(400, str(e)) - except RuleNotFoundException as e: - raise SynapseError(400, str(e)) - - return 200, {} - - async def on_DELETE(self, request, path): - if self._is_worker: - raise Exception("Cannot handle DELETE /push_rules on worker") - - spec = _rule_spec_from_path(path.split("/")) - - requester = await self.auth.get_user_by_req(request) - user_id = requester.user.to_string() - - namespaced_rule_id = _namespaced_rule_id_from_spec(spec) - - try: - await self.store.delete_push_rule(user_id, namespaced_rule_id) - self.notify_user(user_id) - return 200, {} - except StoreError as e: - if e.code == 404: - raise NotFoundError() - else: - raise - - async def on_GET(self, request, path): - requester = await self.auth.get_user_by_req(request) - user_id = requester.user.to_string() - - # we build up the full structure and then decide which bits of it - # to send which means doing unnecessary work sometimes but is - # is probably not going to make a whole lot of difference - rules = await self.store.get_push_rules_for_user(user_id) - - rules = format_push_rules_for_user(requester.user, rules) - - path = path.split("/")[1:] - - if path == []: - # we're a reference impl: pedantry is our job. - raise UnrecognizedRequestError( - PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR - ) - - if path[0] == "": - return 200, rules - elif path[0] == "global": - result = _filter_ruleset_with_path(rules["global"], path[1:]) - return 200, result - else: - raise UnrecognizedRequestError() - - def notify_user(self, user_id): - stream_id = self.store.get_max_push_rules_stream_id() - self.notifier.on_new_event("push_rules_key", stream_id, users=[user_id]) - - async def set_rule_attr(self, user_id, spec, val): - if spec["attr"] not in ("enabled", "actions"): - # for the sake of potential future expansion, shouldn't report - # 404 in the case of an unknown request so check it corresponds to - # a known attribute first. - raise UnrecognizedRequestError() - - namespaced_rule_id = _namespaced_rule_id_from_spec(spec) - rule_id = spec["rule_id"] - is_default_rule = rule_id.startswith(".") - if is_default_rule: - if namespaced_rule_id not in BASE_RULE_IDS: - raise NotFoundError("Unknown rule %s" % (namespaced_rule_id,)) - if spec["attr"] == "enabled": - if isinstance(val, dict) and "enabled" in val: - val = val["enabled"] - if not isinstance(val, bool): - # Legacy fallback - # This should *actually* take a dict, but many clients pass - # bools directly, so let's not break them. - raise SynapseError(400, "Value for 'enabled' must be boolean") - return await self.store.set_push_rule_enabled( - user_id, namespaced_rule_id, val, is_default_rule - ) - elif spec["attr"] == "actions": - actions = val.get("actions") - _check_actions(actions) - namespaced_rule_id = _namespaced_rule_id_from_spec(spec) - rule_id = spec["rule_id"] - is_default_rule = rule_id.startswith(".") - if is_default_rule: - if user_id in self._users_new_default_push_rules: - rule_ids = NEW_RULE_IDS - else: - rule_ids = BASE_RULE_IDS - - if namespaced_rule_id not in rule_ids: - raise SynapseError(404, "Unknown rule %r" % (namespaced_rule_id,)) - return await self.store.set_push_rule_actions( - user_id, namespaced_rule_id, actions, is_default_rule - ) - else: - raise UnrecognizedRequestError() - - -def _rule_spec_from_path(path): - """Turn a sequence of path components into a rule spec - - Args: - path (sequence[unicode]): the URL path components. - - Returns: - dict: rule spec dict, containing scope/template/rule_id entries, - and possibly attr. - - Raises: - UnrecognizedRequestError if the path components cannot be parsed. - """ - if len(path) < 2: - raise UnrecognizedRequestError() - if path[0] != "pushrules": - raise UnrecognizedRequestError() - - scope = path[1] - path = path[2:] - if scope != "global": - raise UnrecognizedRequestError() - - if len(path) == 0: - raise UnrecognizedRequestError() - - template = path[0] - path = path[1:] - - if len(path) == 0 or len(path[0]) == 0: - raise UnrecognizedRequestError() - - rule_id = path[0] - - spec = {"scope": scope, "template": template, "rule_id": rule_id} - - path = path[1:] - - if len(path) > 0 and len(path[0]) > 0: - spec["attr"] = path[0] - - return spec - - -def _rule_tuple_from_request_object(rule_template, rule_id, req_obj): - if rule_template in ["override", "underride"]: - if "conditions" not in req_obj: - raise InvalidRuleException("Missing 'conditions'") - conditions = req_obj["conditions"] - for c in conditions: - if "kind" not in c: - raise InvalidRuleException("Condition without 'kind'") - elif rule_template == "room": - conditions = [{"kind": "event_match", "key": "room_id", "pattern": rule_id}] - elif rule_template == "sender": - conditions = [{"kind": "event_match", "key": "user_id", "pattern": rule_id}] - elif rule_template == "content": - if "pattern" not in req_obj: - raise InvalidRuleException("Content rule missing 'pattern'") - pat = req_obj["pattern"] - - conditions = [{"kind": "event_match", "key": "content.body", "pattern": pat}] - else: - raise InvalidRuleException("Unknown rule template: %s" % (rule_template,)) - - if "actions" not in req_obj: - raise InvalidRuleException("No actions found") - actions = req_obj["actions"] - - _check_actions(actions) - - return conditions, actions - - -def _check_actions(actions): - if not isinstance(actions, list): - raise InvalidRuleException("No actions found") - - for a in actions: - if a in ["notify", "dont_notify", "coalesce"]: - pass - elif isinstance(a, dict) and "set_tweak" in a: - pass - else: - raise InvalidRuleException("Unrecognised action") - - -def _filter_ruleset_with_path(ruleset, path): - if path == []: - raise UnrecognizedRequestError( - PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR - ) - - if path[0] == "": - return ruleset - template_kind = path[0] - if template_kind not in ruleset: - raise UnrecognizedRequestError() - path = path[1:] - if path == []: - raise UnrecognizedRequestError( - PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR - ) - if path[0] == "": - return ruleset[template_kind] - rule_id = path[0] - - the_rule = None - for r in ruleset[template_kind]: - if r["rule_id"] == rule_id: - the_rule = r - if the_rule is None: - raise NotFoundError - - path = path[1:] - if len(path) == 0: - return the_rule - - attr = path[0] - if attr in the_rule: - # Make sure we return a JSON object as the attribute may be a - # JSON value. - return {attr: the_rule[attr]} - else: - raise UnrecognizedRequestError() - - -def _priority_class_from_spec(spec): - if spec["template"] not in PRIORITY_CLASS_MAP.keys(): - raise InvalidRuleException("Unknown template: %s" % (spec["template"])) - pc = PRIORITY_CLASS_MAP[spec["template"]] - - return pc - - -def _namespaced_rule_id_from_spec(spec): - return _namespaced_rule_id(spec, spec["rule_id"]) - - -def _namespaced_rule_id(spec, rule_id): - return "global/%s/%s" % (spec["template"], rule_id) - - -class InvalidRuleException(Exception): - pass - - -def register_servlets(hs, http_server): - PushRuleRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/pusher.py b/synapse/rest/client/v1/pusher.py deleted file mode 100644 index 18102eca6c..0000000000 --- a/synapse/rest/client/v1/pusher.py +++ /dev/null @@ -1,171 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# 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 - -from synapse.api.errors import Codes, StoreError, SynapseError -from synapse.http.server import respond_with_html_bytes -from synapse.http.servlet import ( - RestServlet, - assert_params_in_dict, - parse_json_object_from_request, - parse_string, -) -from synapse.push import PusherConfigException -from synapse.rest.client.v2_alpha._base import client_patterns - -logger = logging.getLogger(__name__) - - -class PushersRestServlet(RestServlet): - PATTERNS = client_patterns("/pushers$", v1=True) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req(request) - user = requester.user - - pushers = await self.hs.get_datastore().get_pushers_by_user_id(user.to_string()) - - filtered_pushers = [p.as_dict() for p in pushers] - - return 200, {"pushers": filtered_pushers} - - -class PushersSetRestServlet(RestServlet): - PATTERNS = client_patterns("/pushers/set$", v1=True) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.notifier = hs.get_notifier() - self.pusher_pool = self.hs.get_pusherpool() - - async def on_POST(self, request): - requester = await self.auth.get_user_by_req(request) - user = requester.user - - content = parse_json_object_from_request(request) - - if ( - "pushkey" in content - and "app_id" in content - and "kind" in content - and content["kind"] is None - ): - await self.pusher_pool.remove_pusher( - content["app_id"], content["pushkey"], user_id=user.to_string() - ) - return 200, {} - - assert_params_in_dict( - content, - [ - "kind", - "app_id", - "app_display_name", - "device_display_name", - "pushkey", - "lang", - "data", - ], - ) - - logger.debug("set pushkey %s to kind %s", content["pushkey"], content["kind"]) - logger.debug("Got pushers request with body: %r", content) - - append = False - if "append" in content: - append = content["append"] - - if not append: - await self.pusher_pool.remove_pushers_by_app_id_and_pushkey_not_user( - app_id=content["app_id"], - pushkey=content["pushkey"], - not_user_id=user.to_string(), - ) - - try: - await self.pusher_pool.add_pusher( - user_id=user.to_string(), - access_token=requester.access_token_id, - kind=content["kind"], - app_id=content["app_id"], - app_display_name=content["app_display_name"], - device_display_name=content["device_display_name"], - pushkey=content["pushkey"], - lang=content["lang"], - data=content["data"], - profile_tag=content.get("profile_tag", ""), - ) - except PusherConfigException as pce: - raise SynapseError( - 400, "Config Error: " + str(pce), errcode=Codes.MISSING_PARAM - ) - - self.notifier.on_new_replication_data() - - return 200, {} - - -class PushersRemoveRestServlet(RestServlet): - """ - To allow pusher to be delete by clicking a link (ie. GET request) - """ - - PATTERNS = client_patterns("/pushers/remove$", v1=True) - SUCCESS_HTML = b"<html><body>You have been unsubscribed</body><html>" - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.notifier = hs.get_notifier() - self.auth = hs.get_auth() - self.pusher_pool = self.hs.get_pusherpool() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req(request, rights="delete_pusher") - user = requester.user - - app_id = parse_string(request, "app_id", required=True) - pushkey = parse_string(request, "pushkey", required=True) - - try: - await self.pusher_pool.remove_pusher( - app_id=app_id, pushkey=pushkey, user_id=user.to_string() - ) - except StoreError as se: - if se.code != 404: - # This is fine: they're already unsubscribed - raise - - self.notifier.on_new_replication_data() - - respond_with_html_bytes( - request, - 200, - PushersRemoveRestServlet.SUCCESS_HTML, - ) - return None - - -def register_servlets(hs, http_server): - PushersRestServlet(hs).register(http_server) - PushersSetRestServlet(hs).register(http_server) - PushersRemoveRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py deleted file mode 100644 index ba7250ad8e..0000000000 --- a/synapse/rest/client/v1/room.py +++ /dev/null @@ -1,1152 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# Copyright 2018 New Vector Ltd -# -# 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. - -""" This module contains REST servlets to do with rooms: /rooms/<paths> """ -import logging -import re -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple -from urllib import parse as urlparse - -from synapse.api.constants import EventTypes, Membership -from synapse.api.errors import ( - AuthError, - Codes, - InvalidClientCredentialsError, - MissingClientTokenError, - ShadowBanError, - SynapseError, -) -from synapse.api.filtering import Filter -from synapse.events.utils import format_event_for_client_v2 -from synapse.http.servlet import ( - ResolveRoomIdMixin, - RestServlet, - assert_params_in_dict, - parse_boolean, - parse_integer, - parse_json_object_from_request, - parse_string, - parse_strings_from_args, -) -from synapse.http.site import SynapseRequest -from synapse.logging.opentracing import set_tag -from synapse.rest.client.transactions import HttpTransactionCache -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.storage.state import StateFilter -from synapse.streams.config import PaginationConfig -from synapse.types import JsonDict, StreamToken, ThirdPartyInstanceID, UserID -from synapse.util import json_decoder -from synapse.util.stringutils import parse_and_validate_server_name, random_string - -if TYPE_CHECKING: - from synapse.server import HomeServer - -logger = logging.getLogger(__name__) - - -class TransactionRestServlet(RestServlet): - def __init__(self, hs): - super().__init__() - self.txns = HttpTransactionCache(hs) - - -class RoomCreateRestServlet(TransactionRestServlet): - # No PATTERN; we have custom dispatch rules here - - def __init__(self, hs): - super().__init__(hs) - self._room_creation_handler = hs.get_room_creation_handler() - self.auth = hs.get_auth() - - def register(self, http_server): - PATTERNS = "/createRoom" - register_txn_path(self, PATTERNS, http_server) - - def on_PUT(self, request, txn_id): - set_tag("txn_id", txn_id) - return self.txns.fetch_or_execute_request(request, self.on_POST, request) - - async def on_POST(self, request): - requester = await self.auth.get_user_by_req(request) - - info, _ = await self._room_creation_handler.create_room( - requester, self.get_room_config(request) - ) - - return 200, info - - def get_room_config(self, request): - user_supplied_config = parse_json_object_from_request(request) - return user_supplied_config - - -# TODO: Needs unit testing for generic events -class RoomStateEventRestServlet(TransactionRestServlet): - def __init__(self, hs): - super().__init__(hs) - self.event_creation_handler = hs.get_event_creation_handler() - self.room_member_handler = hs.get_room_member_handler() - self.message_handler = hs.get_message_handler() - self.auth = hs.get_auth() - - def register(self, http_server): - # /room/$roomid/state/$eventtype - no_state_key = "/rooms/(?P<room_id>[^/]*)/state/(?P<event_type>[^/]*)$" - - # /room/$roomid/state/$eventtype/$statekey - state_key = ( - "/rooms/(?P<room_id>[^/]*)/state/" - "(?P<event_type>[^/]*)/(?P<state_key>[^/]*)$" - ) - - http_server.register_paths( - "GET", - client_patterns(state_key, v1=True), - self.on_GET, - self.__class__.__name__, - ) - http_server.register_paths( - "PUT", - client_patterns(state_key, v1=True), - self.on_PUT, - self.__class__.__name__, - ) - http_server.register_paths( - "GET", - client_patterns(no_state_key, v1=True), - self.on_GET_no_state_key, - self.__class__.__name__, - ) - http_server.register_paths( - "PUT", - client_patterns(no_state_key, v1=True), - self.on_PUT_no_state_key, - self.__class__.__name__, - ) - - def on_GET_no_state_key(self, request, room_id, event_type): - return self.on_GET(request, room_id, event_type, "") - - def on_PUT_no_state_key(self, request, room_id, event_type): - return self.on_PUT(request, room_id, event_type, "") - - async def on_GET(self, request, room_id, event_type, state_key): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - format = parse_string( - request, "format", default="content", allowed_values=["content", "event"] - ) - - msg_handler = self.message_handler - data = await msg_handler.get_room_data( - user_id=requester.user.to_string(), - room_id=room_id, - event_type=event_type, - state_key=state_key, - ) - - if not data: - raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) - - if format == "event": - event = format_event_for_client_v2(data.get_dict()) - return 200, event - elif format == "content": - return 200, data.get_dict()["content"] - - async def on_PUT(self, request, room_id, event_type, state_key, txn_id=None): - requester = await self.auth.get_user_by_req(request) - - if txn_id: - set_tag("txn_id", txn_id) - - content = parse_json_object_from_request(request) - - event_dict = { - "type": event_type, - "content": content, - "room_id": room_id, - "sender": requester.user.to_string(), - } - - if state_key is not None: - event_dict["state_key"] = state_key - - try: - if event_type == EventTypes.Member: - membership = content.get("membership", None) - event_id, _ = await self.room_member_handler.update_membership( - requester, - target=UserID.from_string(state_key), - room_id=room_id, - action=membership, - content=content, - ) - else: - ( - event, - _, - ) = await self.event_creation_handler.create_and_send_nonmember_event( - requester, event_dict, txn_id=txn_id - ) - event_id = event.event_id - except ShadowBanError: - event_id = "$" + random_string(43) - - set_tag("event_id", event_id) - ret = {"event_id": event_id} - return 200, ret - - -# TODO: Needs unit testing for generic events + feedback -class RoomSendEventRestServlet(TransactionRestServlet): - def __init__(self, hs): - super().__init__(hs) - self.event_creation_handler = hs.get_event_creation_handler() - self.auth = hs.get_auth() - - def register(self, http_server): - # /rooms/$roomid/send/$event_type[/$txn_id] - PATTERNS = "/rooms/(?P<room_id>[^/]*)/send/(?P<event_type>[^/]*)" - register_txn_path(self, PATTERNS, http_server, with_get=True) - - async def on_POST(self, request, room_id, event_type, txn_id=None): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - content = parse_json_object_from_request(request) - - event_dict = { - "type": event_type, - "content": content, - "room_id": room_id, - "sender": requester.user.to_string(), - } - - if b"ts" in request.args and requester.app_service: - event_dict["origin_server_ts"] = parse_integer(request, "ts", 0) - - try: - ( - event, - _, - ) = await self.event_creation_handler.create_and_send_nonmember_event( - requester, event_dict, txn_id=txn_id - ) - event_id = event.event_id - except ShadowBanError: - event_id = "$" + random_string(43) - - set_tag("event_id", event_id) - return 200, {"event_id": event_id} - - def on_GET(self, request, room_id, event_type, txn_id): - return 200, "Not implemented" - - def on_PUT(self, request, room_id, event_type, txn_id): - set_tag("txn_id", txn_id) - - return self.txns.fetch_or_execute_request( - request, self.on_POST, request, room_id, event_type, txn_id - ) - - -# TODO: Needs unit testing for room ID + alias joins -class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet): - def __init__(self, hs): - super().__init__(hs) - super(ResolveRoomIdMixin, self).__init__(hs) # ensure the Mixin is set up - self.auth = hs.get_auth() - - def register(self, http_server): - # /join/$room_identifier[/$txn_id] - PATTERNS = "/join/(?P<room_identifier>[^/]*)" - register_txn_path(self, PATTERNS, http_server) - - async def on_POST( - self, - request: SynapseRequest, - room_identifier: str, - txn_id: Optional[str] = None, - ): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - - try: - content = parse_json_object_from_request(request) - except Exception: - # Turns out we used to ignore the body entirely, and some clients - # cheekily send invalid bodies. - content = {} - - # twisted.web.server.Request.args is incorrectly defined as Optional[Any] - args: Dict[bytes, List[bytes]] = request.args # type: ignore - remote_room_hosts = parse_strings_from_args(args, "server_name", required=False) - room_id, remote_room_hosts = await self.resolve_room_id( - room_identifier, - remote_room_hosts, - ) - - await self.room_member_handler.update_membership( - requester=requester, - target=requester.user, - room_id=room_id, - action="join", - txn_id=txn_id, - remote_room_hosts=remote_room_hosts, - content=content, - third_party_signed=content.get("third_party_signed", None), - ) - - return 200, {"room_id": room_id} - - def on_PUT(self, request, room_identifier, txn_id): - set_tag("txn_id", txn_id) - - return self.txns.fetch_or_execute_request( - request, self.on_POST, request, room_identifier, txn_id - ) - - -# TODO: Needs unit testing -class PublicRoomListRestServlet(TransactionRestServlet): - PATTERNS = client_patterns("/publicRooms$", v1=True) - - def __init__(self, hs): - super().__init__(hs) - self.hs = hs - self.auth = hs.get_auth() - - async def on_GET(self, request): - server = parse_string(request, "server") - - try: - await self.auth.get_user_by_req(request, allow_guest=True) - except InvalidClientCredentialsError as e: - # Option to allow servers to require auth when accessing - # /publicRooms via CS API. This is especially helpful in private - # federations. - if not self.hs.config.allow_public_rooms_without_auth: - raise - - # We allow people to not be authed if they're just looking at our - # room list, but require auth when we proxy the request. - # In both cases we call the auth function, as that has the side - # effect of logging who issued this request if an access token was - # provided. - if server: - raise e - - limit: Optional[int] = parse_integer(request, "limit", 0) - since_token = parse_string(request, "since") - - if limit == 0: - # zero is a special value which corresponds to no limit. - limit = None - - handler = self.hs.get_room_list_handler() - if server and server != self.hs.config.server_name: - # Ensure the server is valid. - try: - parse_and_validate_server_name(server) - except ValueError: - raise SynapseError( - 400, - "Invalid server name: %s" % (server,), - Codes.INVALID_PARAM, - ) - - data = await handler.get_remote_public_room_list( - server, limit=limit, since_token=since_token - ) - else: - data = await handler.get_local_public_room_list( - limit=limit, since_token=since_token - ) - - return 200, data - - async def on_POST(self, request): - await self.auth.get_user_by_req(request, allow_guest=True) - - server = parse_string(request, "server") - content = parse_json_object_from_request(request) - - limit: Optional[int] = int(content.get("limit", 100)) - since_token = content.get("since", None) - search_filter = content.get("filter", None) - - include_all_networks = content.get("include_all_networks", False) - third_party_instance_id = content.get("third_party_instance_id", None) - - if include_all_networks: - network_tuple = None - if third_party_instance_id is not None: - raise SynapseError( - 400, "Can't use include_all_networks with an explicit network" - ) - elif third_party_instance_id is None: - network_tuple = ThirdPartyInstanceID(None, None) - else: - network_tuple = ThirdPartyInstanceID.from_string(third_party_instance_id) - - if limit == 0: - # zero is a special value which corresponds to no limit. - limit = None - - handler = self.hs.get_room_list_handler() - if server and server != self.hs.config.server_name: - # Ensure the server is valid. - try: - parse_and_validate_server_name(server) - except ValueError: - raise SynapseError( - 400, - "Invalid server name: %s" % (server,), - Codes.INVALID_PARAM, - ) - - data = await handler.get_remote_public_room_list( - server, - limit=limit, - since_token=since_token, - search_filter=search_filter, - include_all_networks=include_all_networks, - third_party_instance_id=third_party_instance_id, - ) - - else: - data = await handler.get_local_public_room_list( - limit=limit, - since_token=since_token, - search_filter=search_filter, - network_tuple=network_tuple, - ) - - return 200, data - - -# TODO: Needs unit testing -class RoomMemberListRestServlet(RestServlet): - PATTERNS = client_patterns("/rooms/(?P<room_id>[^/]*)/members$", v1=True) - - def __init__(self, hs): - super().__init__() - self.message_handler = hs.get_message_handler() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - - async def on_GET(self, request, room_id): - # TODO support Pagination stream API (limit/tokens) - requester = await self.auth.get_user_by_req(request, allow_guest=True) - handler = self.message_handler - - # request the state as of a given event, as identified by a stream token, - # for consistency with /messages etc. - # useful for getting the membership in retrospect as of a given /sync - # response. - at_token_string = parse_string(request, "at") - if at_token_string is None: - at_token = None - else: - at_token = await StreamToken.from_string(self.store, at_token_string) - - # let you filter down on particular memberships. - # XXX: this may not be the best shape for this API - we could pass in a filter - # instead, except filters aren't currently aware of memberships. - # See https://github.com/matrix-org/matrix-doc/issues/1337 for more details. - membership = parse_string(request, "membership") - not_membership = parse_string(request, "not_membership") - - events = await handler.get_state_events( - room_id=room_id, - user_id=requester.user.to_string(), - at_token=at_token, - state_filter=StateFilter.from_types([(EventTypes.Member, None)]), - ) - - chunk = [] - - for event in events: - if (membership and event["content"].get("membership") != membership) or ( - not_membership and event["content"].get("membership") == not_membership - ): - continue - chunk.append(event) - - return 200, {"chunk": chunk} - - -# deprecated in favour of /members?membership=join? -# except it does custom AS logic and has a simpler return format -class JoinedRoomMemberListRestServlet(RestServlet): - PATTERNS = client_patterns("/rooms/(?P<room_id>[^/]*)/joined_members$", v1=True) - - def __init__(self, hs): - super().__init__() - self.message_handler = hs.get_message_handler() - self.auth = hs.get_auth() - - async def on_GET(self, request, room_id): - requester = await self.auth.get_user_by_req(request) - - users_with_profile = await self.message_handler.get_joined_members( - requester, room_id - ) - - return 200, {"joined": users_with_profile} - - -# TODO: Needs better unit testing -class RoomMessageListRestServlet(RestServlet): - PATTERNS = client_patterns("/rooms/(?P<room_id>[^/]*)/messages$", v1=True) - - def __init__(self, hs): - super().__init__() - self.pagination_handler = hs.get_pagination_handler() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - - async def on_GET(self, request, room_id): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - pagination_config = await PaginationConfig.from_request( - self.store, request, default_limit=10 - ) - as_client_event = b"raw" not in request.args - filter_str = parse_string(request, "filter", encoding="utf-8") - if filter_str: - filter_json = urlparse.unquote(filter_str) - event_filter: Optional[Filter] = Filter(json_decoder.decode(filter_json)) - if ( - event_filter - and event_filter.filter_json.get("event_format", "client") - == "federation" - ): - as_client_event = False - else: - event_filter = None - - msgs = await self.pagination_handler.get_messages( - room_id=room_id, - requester=requester, - pagin_config=pagination_config, - as_client_event=as_client_event, - event_filter=event_filter, - ) - - return 200, msgs - - -# TODO: Needs unit testing -class RoomStateRestServlet(RestServlet): - PATTERNS = client_patterns("/rooms/(?P<room_id>[^/]*)/state$", v1=True) - - def __init__(self, hs): - super().__init__() - self.message_handler = hs.get_message_handler() - self.auth = hs.get_auth() - - async def on_GET(self, request, room_id): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - # Get all the current state for this room - events = await self.message_handler.get_state_events( - room_id=room_id, - user_id=requester.user.to_string(), - is_guest=requester.is_guest, - ) - return 200, events - - -# TODO: Needs unit testing -class RoomInitialSyncRestServlet(RestServlet): - PATTERNS = client_patterns("/rooms/(?P<room_id>[^/]*)/initialSync$", v1=True) - - def __init__(self, hs): - super().__init__() - self.initial_sync_handler = hs.get_initial_sync_handler() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - - async def on_GET(self, request, room_id): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - pagination_config = await PaginationConfig.from_request(self.store, request) - content = await self.initial_sync_handler.room_initial_sync( - room_id=room_id, requester=requester, pagin_config=pagination_config - ) - return 200, content - - -class RoomEventServlet(RestServlet): - PATTERNS = client_patterns( - "/rooms/(?P<room_id>[^/]*)/event/(?P<event_id>[^/]*)$", v1=True - ) - - def __init__(self, hs): - super().__init__() - self.clock = hs.get_clock() - self.event_handler = hs.get_event_handler() - self._event_serializer = hs.get_event_client_serializer() - self.auth = hs.get_auth() - - async def on_GET(self, request, room_id, event_id): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - try: - event = await self.event_handler.get_event( - requester.user, room_id, event_id - ) - except AuthError: - # This endpoint is supposed to return a 404 when the requester does - # not have permission to access the event - # https://matrix.org/docs/spec/client_server/r0.5.0#get-matrix-client-r0-rooms-roomid-event-eventid - raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) - - time_now = self.clock.time_msec() - if event: - event = await self._event_serializer.serialize_event(event, time_now) - return 200, event - - return SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) - - -class RoomEventContextServlet(RestServlet): - PATTERNS = client_patterns( - "/rooms/(?P<room_id>[^/]*)/context/(?P<event_id>[^/]*)$", v1=True - ) - - def __init__(self, hs): - super().__init__() - self.clock = hs.get_clock() - self.room_context_handler = hs.get_room_context_handler() - self._event_serializer = hs.get_event_client_serializer() - self.auth = hs.get_auth() - - async def on_GET(self, request, room_id, event_id): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - - limit = parse_integer(request, "limit", default=10) - - # picking the API shape for symmetry with /messages - filter_str = parse_string(request, "filter", encoding="utf-8") - if filter_str: - filter_json = urlparse.unquote(filter_str) - event_filter: Optional[Filter] = Filter(json_decoder.decode(filter_json)) - else: - event_filter = None - - results = await self.room_context_handler.get_event_context( - requester, room_id, event_id, limit, event_filter - ) - - if not results: - raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) - - time_now = self.clock.time_msec() - results["events_before"] = await self._event_serializer.serialize_events( - results["events_before"], time_now - ) - results["event"] = await self._event_serializer.serialize_event( - results["event"], time_now - ) - results["events_after"] = await self._event_serializer.serialize_events( - results["events_after"], time_now - ) - results["state"] = await self._event_serializer.serialize_events( - results["state"], - time_now, - # No need to bundle aggregations for state events - bundle_aggregations=False, - ) - - return 200, results - - -class RoomForgetRestServlet(TransactionRestServlet): - def __init__(self, hs): - super().__init__(hs) - self.room_member_handler = hs.get_room_member_handler() - self.auth = hs.get_auth() - - def register(self, http_server): - PATTERNS = "/rooms/(?P<room_id>[^/]*)/forget" - register_txn_path(self, PATTERNS, http_server) - - async def on_POST(self, request, room_id, txn_id=None): - requester = await self.auth.get_user_by_req(request, allow_guest=False) - - await self.room_member_handler.forget(user=requester.user, room_id=room_id) - - return 200, {} - - def on_PUT(self, request, room_id, txn_id): - set_tag("txn_id", txn_id) - - return self.txns.fetch_or_execute_request( - request, self.on_POST, request, room_id, txn_id - ) - - -# TODO: Needs unit testing -class RoomMembershipRestServlet(TransactionRestServlet): - def __init__(self, hs): - super().__init__(hs) - self.room_member_handler = hs.get_room_member_handler() - self.auth = hs.get_auth() - - def register(self, http_server): - # /rooms/$roomid/[invite|join|leave] - PATTERNS = ( - "/rooms/(?P<room_id>[^/]*)/" - "(?P<membership_action>join|invite|leave|ban|unban|kick)" - ) - register_txn_path(self, PATTERNS, http_server) - - async def on_POST(self, request, room_id, membership_action, txn_id=None): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - - if requester.is_guest and membership_action not in { - Membership.JOIN, - Membership.LEAVE, - }: - raise AuthError(403, "Guest access not allowed") - - try: - content = parse_json_object_from_request(request) - except Exception: - # Turns out we used to ignore the body entirely, and some clients - # cheekily send invalid bodies. - content = {} - - if membership_action == "invite" and self._has_3pid_invite_keys(content): - try: - await self.room_member_handler.do_3pid_invite( - room_id, - requester.user, - content["medium"], - content["address"], - content["id_server"], - requester, - txn_id, - content.get("id_access_token"), - ) - except ShadowBanError: - # Pretend the request succeeded. - pass - return 200, {} - - target = requester.user - if membership_action in ["invite", "ban", "unban", "kick"]: - assert_params_in_dict(content, ["user_id"]) - target = UserID.from_string(content["user_id"]) - - event_content = None - if "reason" in content: - event_content = {"reason": content["reason"]} - - try: - await self.room_member_handler.update_membership( - requester=requester, - target=target, - room_id=room_id, - action=membership_action, - txn_id=txn_id, - third_party_signed=content.get("third_party_signed", None), - content=event_content, - ) - except ShadowBanError: - # Pretend the request succeeded. - pass - - return_value = {} - - if membership_action == "join": - return_value["room_id"] = room_id - - return 200, return_value - - def _has_3pid_invite_keys(self, content): - for key in {"id_server", "medium", "address"}: - if key not in content: - return False - return True - - def on_PUT(self, request, room_id, membership_action, txn_id): - set_tag("txn_id", txn_id) - - return self.txns.fetch_or_execute_request( - request, self.on_POST, request, room_id, membership_action, txn_id - ) - - -class RoomRedactEventRestServlet(TransactionRestServlet): - def __init__(self, hs): - super().__init__(hs) - self.event_creation_handler = hs.get_event_creation_handler() - self.auth = hs.get_auth() - - def register(self, http_server): - PATTERNS = "/rooms/(?P<room_id>[^/]*)/redact/(?P<event_id>[^/]*)" - register_txn_path(self, PATTERNS, http_server) - - async def on_POST(self, request, room_id, event_id, txn_id=None): - requester = await self.auth.get_user_by_req(request) - content = parse_json_object_from_request(request) - - try: - ( - event, - _, - ) = await self.event_creation_handler.create_and_send_nonmember_event( - requester, - { - "type": EventTypes.Redaction, - "content": content, - "room_id": room_id, - "sender": requester.user.to_string(), - "redacts": event_id, - }, - txn_id=txn_id, - ) - event_id = event.event_id - except ShadowBanError: - event_id = "$" + random_string(43) - - set_tag("event_id", event_id) - return 200, {"event_id": event_id} - - def on_PUT(self, request, room_id, event_id, txn_id): - set_tag("txn_id", txn_id) - - return self.txns.fetch_or_execute_request( - request, self.on_POST, request, room_id, event_id, txn_id - ) - - -class RoomTypingRestServlet(RestServlet): - PATTERNS = client_patterns( - "/rooms/(?P<room_id>[^/]*)/typing/(?P<user_id>[^/]*)$", v1=True - ) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.hs = hs - self.presence_handler = hs.get_presence_handler() - self.auth = hs.get_auth() - - # If we're not on the typing writer instance we should scream if we get - # requests. - self._is_typing_writer = ( - hs.config.worker.writers.typing == hs.get_instance_name() - ) - - async def on_PUT(self, request, room_id, user_id): - requester = await self.auth.get_user_by_req(request) - - if not self._is_typing_writer: - raise Exception("Got /typing request on instance that is not typing writer") - - room_id = urlparse.unquote(room_id) - target_user = UserID.from_string(urlparse.unquote(user_id)) - - content = parse_json_object_from_request(request) - - await self.presence_handler.bump_presence_active_time(requester.user) - - # Limit timeout to stop people from setting silly typing timeouts. - timeout = min(content.get("timeout", 30000), 120000) - - # Defer getting the typing handler since it will raise on workers. - typing_handler = self.hs.get_typing_writer_handler() - - try: - if content["typing"]: - await typing_handler.started_typing( - target_user=target_user, - requester=requester, - room_id=room_id, - timeout=timeout, - ) - else: - await typing_handler.stopped_typing( - target_user=target_user, requester=requester, room_id=room_id - ) - except ShadowBanError: - # Pretend this worked without error. - pass - - return 200, {} - - -class RoomAliasListServlet(RestServlet): - PATTERNS = [ - re.compile( - r"^/_matrix/client/unstable/org\.matrix\.msc2432" - r"/rooms/(?P<room_id>[^/]*)/aliases" - ), - ] + list(client_patterns("/rooms/(?P<room_id>[^/]*)/aliases$", unstable=False)) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.directory_handler = hs.get_directory_handler() - - async def on_GET(self, request, room_id): - requester = await self.auth.get_user_by_req(request) - - alias_list = await self.directory_handler.get_aliases_for_room( - requester, room_id - ) - - return 200, {"aliases": alias_list} - - -class SearchRestServlet(RestServlet): - PATTERNS = client_patterns("/search$", v1=True) - - def __init__(self, hs): - super().__init__() - self.search_handler = hs.get_search_handler() - self.auth = hs.get_auth() - - async def on_POST(self, request): - requester = await self.auth.get_user_by_req(request) - - content = parse_json_object_from_request(request) - - batch = parse_string(request, "next_batch") - results = await self.search_handler.search(requester.user, content, batch) - - return 200, results - - -class JoinedRoomsRestServlet(RestServlet): - PATTERNS = client_patterns("/joined_rooms$", v1=True) - - def __init__(self, hs): - super().__init__() - self.store = hs.get_datastore() - self.auth = hs.get_auth() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - - room_ids = await self.store.get_rooms_for_user(requester.user.to_string()) - return 200, {"joined_rooms": list(room_ids)} - - -def register_txn_path(servlet, regex_string, http_server, with_get=False): - """Registers a transaction-based path. - - This registers two paths: - PUT regex_string/$txnid - POST regex_string - - Args: - regex_string (str): The regex string to register. Must NOT have a - trailing $ as this string will be appended to. - http_server : The http_server to register paths with. - with_get: True to also register respective GET paths for the PUTs. - """ - http_server.register_paths( - "POST", - client_patterns(regex_string + "$", v1=True), - servlet.on_POST, - servlet.__class__.__name__, - ) - http_server.register_paths( - "PUT", - client_patterns(regex_string + "/(?P<txn_id>[^/]*)$", v1=True), - servlet.on_PUT, - servlet.__class__.__name__, - ) - if with_get: - http_server.register_paths( - "GET", - client_patterns(regex_string + "/(?P<txn_id>[^/]*)$", v1=True), - servlet.on_GET, - servlet.__class__.__name__, - ) - - -class RoomSpaceSummaryRestServlet(RestServlet): - PATTERNS = ( - re.compile( - "^/_matrix/client/unstable/org.matrix.msc2946" - "/rooms/(?P<room_id>[^/]*)/spaces$" - ), - ) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self._auth = hs.get_auth() - self._room_summary_handler = hs.get_room_summary_handler() - - async def on_GET( - self, request: SynapseRequest, room_id: str - ) -> Tuple[int, JsonDict]: - requester = await self._auth.get_user_by_req(request, allow_guest=True) - - max_rooms_per_space = parse_integer(request, "max_rooms_per_space") - if max_rooms_per_space is not None and max_rooms_per_space < 0: - raise SynapseError( - 400, - "Value for 'max_rooms_per_space' must be a non-negative integer", - Codes.BAD_JSON, - ) - - return 200, await self._room_summary_handler.get_space_summary( - requester.user.to_string(), - room_id, - suggested_only=parse_boolean(request, "suggested_only", default=False), - max_rooms_per_space=max_rooms_per_space, - ) - - # TODO When switching to the stable endpoint, remove the POST handler. - async def on_POST( - self, request: SynapseRequest, room_id: str - ) -> Tuple[int, JsonDict]: - requester = await self._auth.get_user_by_req(request, allow_guest=True) - content = parse_json_object_from_request(request) - - suggested_only = content.get("suggested_only", False) - if not isinstance(suggested_only, bool): - raise SynapseError( - 400, "'suggested_only' must be a boolean", Codes.BAD_JSON - ) - - max_rooms_per_space = content.get("max_rooms_per_space") - if max_rooms_per_space is not None: - if not isinstance(max_rooms_per_space, int): - raise SynapseError( - 400, "'max_rooms_per_space' must be an integer", Codes.BAD_JSON - ) - if max_rooms_per_space < 0: - raise SynapseError( - 400, - "Value for 'max_rooms_per_space' must be a non-negative integer", - Codes.BAD_JSON, - ) - - return 200, await self._room_summary_handler.get_space_summary( - requester.user.to_string(), - room_id, - suggested_only=suggested_only, - max_rooms_per_space=max_rooms_per_space, - ) - - -class RoomHierarchyRestServlet(RestServlet): - PATTERNS = ( - re.compile( - "^/_matrix/client/unstable/org.matrix.msc2946" - "/rooms/(?P<room_id>[^/]*)/hierarchy$" - ), - ) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self._auth = hs.get_auth() - self._room_summary_handler = hs.get_room_summary_handler() - - async def on_GET( - self, request: SynapseRequest, room_id: str - ) -> Tuple[int, JsonDict]: - requester = await self._auth.get_user_by_req(request, allow_guest=True) - - max_depth = parse_integer(request, "max_depth") - if max_depth is not None and max_depth < 0: - raise SynapseError( - 400, "'max_depth' must be a non-negative integer", Codes.BAD_JSON - ) - - limit = parse_integer(request, "limit") - if limit is not None and limit <= 0: - raise SynapseError( - 400, "'limit' must be a positive integer", Codes.BAD_JSON - ) - - return 200, await self._room_summary_handler.get_room_hierarchy( - requester.user.to_string(), - room_id, - suggested_only=parse_boolean(request, "suggested_only", default=False), - max_depth=max_depth, - limit=limit, - from_token=parse_string(request, "from"), - ) - - -class RoomSummaryRestServlet(ResolveRoomIdMixin, RestServlet): - PATTERNS = ( - re.compile( - "^/_matrix/client/unstable/im.nheko.summary" - "/rooms/(?P<room_identifier>[^/]*)/summary$" - ), - ) - - def __init__(self, hs: "HomeServer"): - super().__init__(hs) - self._auth = hs.get_auth() - self._room_summary_handler = hs.get_room_summary_handler() - - async def on_GET( - self, request: SynapseRequest, room_identifier: str - ) -> Tuple[int, JsonDict]: - try: - requester = await self._auth.get_user_by_req(request, allow_guest=True) - requester_user_id: Optional[str] = requester.user.to_string() - except MissingClientTokenError: - # auth is optional - requester_user_id = None - - # twisted.web.server.Request.args is incorrectly defined as Optional[Any] - args: Dict[bytes, List[bytes]] = request.args # type: ignore - remote_room_hosts = parse_strings_from_args(args, "via", required=False) - room_id, remote_room_hosts = await self.resolve_room_id( - room_identifier, - remote_room_hosts, - ) - - return 200, await self._room_summary_handler.get_room_summary( - requester_user_id, - room_id, - remote_room_hosts, - ) - - -def register_servlets(hs: "HomeServer", http_server, is_worker=False): - RoomStateEventRestServlet(hs).register(http_server) - RoomMemberListRestServlet(hs).register(http_server) - JoinedRoomMemberListRestServlet(hs).register(http_server) - RoomMessageListRestServlet(hs).register(http_server) - JoinRoomAliasServlet(hs).register(http_server) - RoomMembershipRestServlet(hs).register(http_server) - RoomSendEventRestServlet(hs).register(http_server) - PublicRoomListRestServlet(hs).register(http_server) - RoomStateRestServlet(hs).register(http_server) - RoomRedactEventRestServlet(hs).register(http_server) - RoomTypingRestServlet(hs).register(http_server) - RoomEventContextServlet(hs).register(http_server) - RoomSpaceSummaryRestServlet(hs).register(http_server) - RoomHierarchyRestServlet(hs).register(http_server) - if hs.config.experimental.msc3266_enabled: - RoomSummaryRestServlet(hs).register(http_server) - RoomEventServlet(hs).register(http_server) - JoinedRoomsRestServlet(hs).register(http_server) - RoomAliasListServlet(hs).register(http_server) - SearchRestServlet(hs).register(http_server) - - # Some servlets only get registered for the main process. - if not is_worker: - RoomCreateRestServlet(hs).register(http_server) - RoomForgetRestServlet(hs).register(http_server) - - -def register_deprecated_servlets(hs, http_server): - RoomInitialSyncRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/voip.py b/synapse/rest/client/v1/voip.py deleted file mode 100644 index c780ffded5..0000000000 --- a/synapse/rest/client/v1/voip.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# 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 base64 -import hashlib -import hmac - -from synapse.http.servlet import RestServlet -from synapse.rest.client.v2_alpha._base import client_patterns - - -class VoipRestServlet(RestServlet): - PATTERNS = client_patterns("/voip/turnServer$", v1=True) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req( - request, self.hs.config.turn_allow_guests - ) - - turnUris = self.hs.config.turn_uris - turnSecret = self.hs.config.turn_shared_secret - turnUsername = self.hs.config.turn_username - turnPassword = self.hs.config.turn_password - userLifetime = self.hs.config.turn_user_lifetime - - if turnUris and turnSecret and userLifetime: - expiry = (self.hs.get_clock().time_msec() + userLifetime) / 1000 - username = "%d:%s" % (expiry, requester.user.to_string()) - - mac = hmac.new( - turnSecret.encode(), msg=username.encode(), digestmod=hashlib.sha1 - ) - # We need to use standard padded base64 encoding here - # encode_base64 because we need to add the standard padding to get the - # same result as the TURN server. - password = base64.b64encode(mac.digest()).decode("ascii") - - elif turnUris and turnUsername and turnPassword and userLifetime: - username = turnUsername - password = turnPassword - - else: - return 200, {} - - return ( - 200, - { - "username": username, - "password": password, - "ttl": userLifetime / 1000, - "uris": turnUris, - }, - ) - - -def register_servlets(hs, http_server): - VoipRestServlet(hs).register(http_server) |