diff options
-rw-r--r-- | synapse/api/urls.py | 1 | ||||
-rwxr-xr-x | synapse/app/homeserver.py | 5 | ||||
-rw-r--r-- | synapse/config/server.py | 1 | ||||
-rw-r--r-- | synapse/handlers/identity.py | 79 | ||||
-rw-r--r-- | synapse/handlers/room_member.py | 51 | ||||
-rw-r--r-- | synapse/rest/identity/__init__.py | 14 | ||||
-rw-r--r-- | synapse/rest/identity/v1/__init__.py | 24 | ||||
-rw-r--r-- | synapse/rest/identity/v1/lookup.py | 70 | ||||
-rw-r--r-- | tests/rest/client/test_identity.py | 2 |
9 files changed, 198 insertions, 49 deletions
diff --git a/synapse/api/urls.py b/synapse/api/urls.py index cb71d80875..e33ea0cf65 100644 --- a/synapse/api/urls.py +++ b/synapse/api/urls.py @@ -33,6 +33,7 @@ CONTENT_REPO_PREFIX = "/_matrix/content" SERVER_KEY_V2_PREFIX = "/_matrix/key/v2" MEDIA_PREFIX = "/_matrix/media/r0" LEGACY_MEDIA_PREFIX = "/_matrix/media/v1" +IDENTITY_PREFIX = "/_matrix/identity/api/v1" class ConsentURIBuilder(object): diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 79be977ea6..06aa582129 100755 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -40,6 +40,7 @@ from synapse import events from synapse.api.urls import ( CONTENT_REPO_PREFIX, FEDERATION_PREFIX, + IDENTITY_PREFIX, LEGACY_MEDIA_PREFIX, MEDIA_PREFIX, SERVER_KEY_V2_PREFIX, @@ -62,6 +63,7 @@ from synapse.python_dependencies import check_requirements from synapse.replication.http import REPLICATION_PREFIX, ReplicationRestResource from synapse.replication.tcp.resource import ReplicationStreamProtocolFactory from synapse.rest import ClientRestResource +from synapse.rest.identity.v1 import IdentityApiV1Resource from synapse.rest.key.v2 import KeyApiV2Resource from synapse.rest.media.v0.content_repository import ContentRepoResource from synapse.rest.well_known import WellKnownResource @@ -227,6 +229,9 @@ class SynapseHomeServer(HomeServer): "'media' resource conflicts with enable_media_repo=False", ) + if name in ["identity"]: + resources[IDENTITY_PREFIX] = IdentityApiV1Resource(self) + if name in ["keys", "federation"]: resources[SERVER_KEY_V2_PREFIX] = KeyApiV2Resource(self) diff --git a/synapse/config/server.py b/synapse/config/server.py index c5e5679d52..ac44b17a09 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -573,6 +573,7 @@ KNOWN_RESOURCES = ( 'replication', 'static', 'webclient', + 'identity', ) diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index 9a50f90c46..d2039e2825 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2017 Vector Creations Ltd -# Copyright 2018 New Vector Ltd +# Copyright 2018, 2019 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. @@ -20,13 +20,18 @@ import logging from canonicaljson import json +from signedjson.key import decode_verify_key_bytes +from signedjson.sign import verify_signed_json +from unpaddedbase64 import decode_base64 from twisted.internet import defer from synapse.api.errors import ( + AuthError, CodeMessageException, Codes, HttpResponseException, + ProxiedRequestError, SynapseError, ) @@ -48,6 +53,7 @@ class IdentityHandler(BaseHandler): hs.config.use_insecure_ssl_client_just_for_testing_do_not_use ) self.rewrite_identity_server_urls = hs.config.rewrite_identity_server_urls + self._enable_lookup = hs.config.enable_3pid_lookup def _should_trust_id_server(self, id_server): if id_server not in self.trusted_id_servers: @@ -328,3 +334,74 @@ class IdentityHandler(BaseHandler): except HttpResponseException as e: logger.info("Proxied requestToken failed: %r", e) raise e.to_synapse_error() + + @defer.inlineCallbacks + def lookup_3pid(self, id_server, medium, address): + """Looks up a 3pid in the passed identity server. + + Args: + id_server (str): The server name (including port, if required) + of the identity server to use. + medium (str): The type of the third party identifier (e.g. "email"). + address (str): The third party identifier (e.g. "foo@example.com"). + + Returns: + Deferred[dict]: The result of the lookup. See + https://matrix.org/docs/spec/identity_service/r0.1.0.html#id15 + for details + """ + if not self._enable_lookup: + raise AuthError( + 403, "Looking up third-party identifiers is denied from this server", + ) + + target = self.rewrite_identity_server_urls.get(id_server, id_server) + + try: + data = yield self.http_client.get_json( + "https://%s/_matrix/identity/api/v1/lookup" % (target,), + { + "medium": medium, + "address": address, + } + ) + + if "mxid" in data: + if "signatures" not in data: + raise AuthError(401, "No signatures on 3pid binding") + yield self._verify_any_signature(data, id_server) + + except HttpResponseException as e: + logger.info("Proxied lookup failed: %r", e) + raise e.to_synapse_error() + except IOError as e: + logger.info("Failed to contact %r: %s", id_server, e) + raise ProxiedRequestError(503, "Failed to contact homeserver") + + defer.returnValue(data) + + @defer.inlineCallbacks + def _verify_any_signature(self, data, server_hostname): + if server_hostname not in data["signatures"]: + raise AuthError(401, "No signature from server %s" % (server_hostname,)) + + for key_name, signature in data["signatures"][server_hostname].items(): + target = self.rewrite_identity_server_urls.get( + server_hostname, server_hostname, + ) + + key_data = yield self.http_client.get_json( + "https://%s/_matrix/identity/api/v1/pubkey/%s" % + (target, key_name,), + ) + if "public_key" not in key_data: + raise AuthError(401, "No public key named %s from %s" % + (key_name, server_hostname,)) + verify_signed_json( + data, + server_hostname, + decode_verify_key_bytes(key_name, decode_base64(key_data["public_key"])) + ) + return + + raise AuthError(401, "No signature from server %s" % (server_hostname,)) diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 254b028fd7..14b56e9876 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -19,16 +19,12 @@ import logging from six.moves import http_client -from signedjson.key import decode_verify_key_bytes -from signedjson.sign import verify_signed_json -from unpaddedbase64 import decode_base64 - from twisted.internet import defer import synapse.server import synapse.types from synapse.api.constants import EventTypes, Membership -from synapse.api.errors import AuthError, Codes, SynapseError +from synapse.api.errors import AuthError, Codes, ProxiedRequestError, SynapseError from synapse.types import RoomID, UserID from synapse.util.async_helpers import Linearizer from synapse.util.distributor import user_joined_room, user_left_room @@ -64,6 +60,7 @@ class RoomMemberHandler(object): self.registration_handler = hs.get_registration_handler() self.profile_handler = hs.get_profile_handler() self.event_creation_handler = hs.get_event_creation_handler() + self.identity_handler = hs.get_handlers().identity_handler self.member_linearizer = Linearizer(name="member") @@ -71,7 +68,6 @@ class RoomMemberHandler(object): self.spam_checker = hs.get_spam_checker() self._server_notices_mxid = self.config.server_notices_mxid self.rewrite_identity_server_urls = self.config.rewrite_identity_server_urls - self._enable_lookup = hs.config.enable_3pid_lookup @abc.abstractmethod def _remote_join(self, requester, remote_room_hosts, room_id, user, content): @@ -824,51 +820,14 @@ class RoomMemberHandler(object): Returns: str: the matrix ID of the 3pid, or None if it is not recognized. """ - if not self._enable_lookup: - raise SynapseError( - 403, "Looking up third-party identifiers is denied from this server", - ) try: - target = self._get_id_server_target(id_server) - data = yield self.simple_http_client.get_json( - "%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, target,), - { - "medium": medium, - "address": address, - } - ) - - if "mxid" in data: - if "signatures" not in data: - raise AuthError(401, "No signatures on 3pid binding") - yield self._verify_any_signature(data, id_server) - defer.returnValue(data["mxid"]) - - except IOError as e: + data = yield self.identity_handler.lookup_3pid(id_server, medium, address) + defer.returnValue(data.get("mxid")) + except ProxiedRequestError as e: logger.warn("Error from identity server lookup: %s" % (e,)) defer.returnValue(None) @defer.inlineCallbacks - def _verify_any_signature(self, data, server_hostname): - if server_hostname not in data["signatures"]: - raise AuthError(401, "No signature from server %s" % (server_hostname,)) - for key_name, signature in data["signatures"][server_hostname].items(): - target = self._get_id_server_target(server_hostname) - key_data = yield self.simple_http_client.get_json( - "%s%s/_matrix/identity/api/v1/pubkey/%s" % - (id_server_scheme, target, key_name,), - ) - if "public_key" not in key_data: - raise AuthError(401, "No public key named %s from %s" % - (key_name, server_hostname,)) - verify_signed_json( - data, - server_hostname, - decode_verify_key_bytes(key_name, decode_base64(key_data["public_key"])) - ) - return - - @defer.inlineCallbacks def _make_and_store_3pid_invite( self, requester, diff --git a/synapse/rest/identity/__init__.py b/synapse/rest/identity/__init__.py new file mode 100644 index 0000000000..1453d04571 --- /dev/null +++ b/synapse/rest/identity/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 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. diff --git a/synapse/rest/identity/v1/__init__.py b/synapse/rest/identity/v1/__init__.py new file mode 100644 index 0000000000..09057ea16b --- /dev/null +++ b/synapse/rest/identity/v1/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 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. + +from twisted.web.resource import Resource + +from .lookup import IdentityLookup + + +class IdentityApiV1Resource(Resource): + def __init__(self, hs): + Resource.__init__(self) + self.putChild(b"lookup", IdentityLookup(hs)) diff --git a/synapse/rest/identity/v1/lookup.py b/synapse/rest/identity/v1/lookup.py new file mode 100644 index 0000000000..c2d18fbb63 --- /dev/null +++ b/synapse/rest/identity/v1/lookup.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 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. + + +import logging + +from twisted.internet import defer +from twisted.web.resource import Resource +from twisted.web.server import NOT_DONE_YET + +from synapse.api.errors import SynapseError +from synapse.handlers.identity import IdentityHandler +from synapse.http.server import respond_with_json, wrap_json_request_handler +from synapse.http.servlet import assert_params_in_dict, parse_string + +logger = logging.getLogger(__name__) + + +class IdentityLookup(Resource): + isLeaf = True + + def __init__(self, hs): + self.config = hs.config + self.auth = hs.get_auth() + self.identity_handler = IdentityHandler(hs) + Resource.__init__(self) + + def render_GET(self, request): + self.async_render_GET(request) + return NOT_DONE_YET + + @wrap_json_request_handler + @defer.inlineCallbacks + def async_render_GET(self, request): + """Proxy a /_matrix/identity/api/v1/lookup request to an identity + server + """ + yield self.auth.get_user_by_req(request, allow_guest=True) + + if not self.config.enable_3pid_lookup: + raise SynapseError( + 403, + "Looking up third-party identifiers is denied from this server" + ) + + # Extract query parameters + query_params = request.args + assert_params_in_dict(query_params, [b"medium", b"address", b"is_server"]) + + # Retrieve address and medium from the request parameters + medium = parse_string(request, "medium") + address = parse_string(request, "address") + is_server = parse_string(request, "is_server") + + # Proxy the request to the identity server + ret = yield self.identity_handler.lookup_3pid(is_server, medium, address) + + respond_with_json(request, 200, ret, send_cors=True) diff --git a/tests/rest/client/test_identity.py b/tests/rest/client/test_identity.py index ca63b2e6ed..7e8b95887f 100644 --- a/tests/rest/client/test_identity.py +++ b/tests/rest/client/test_identity.py @@ -37,8 +37,6 @@ class IdentityTestCase(unittest.HomeserverTestCase): return self.hs def test_3pid_lookup_disabled(self): - self.hs.config.enable_3pid_lookup = False - self.register_user("kermit", "monkey") tok = self.login("kermit", "monkey") |