summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--synapse/api/urls.py1
-rwxr-xr-xsynapse/app/homeserver.py5
-rw-r--r--synapse/config/server.py1
-rw-r--r--synapse/handlers/identity.py79
-rw-r--r--synapse/handlers/room_member.py51
-rw-r--r--synapse/rest/identity/__init__.py14
-rw-r--r--synapse/rest/identity/v1/__init__.py24
-rw-r--r--synapse/rest/identity/v1/lookup.py70
-rw-r--r--tests/rest/client/test_identity.py2
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")