Browse Source

Merge branch 'babolivier/info_mainline' into bbz/info_mainline-20200626

Ben Banfield-Zanin 3 years ago
parent
commit
dec3309f2c

+ 6 - 0
sydent/http/httpserver.py

@@ -56,6 +56,9 @@ class ClientApiHttpServer:
         lookup = self.sydent.servlets.lookup
         bulk_lookup = self.sydent.servlets.bulk_lookup
 
+        info = self.sydent.servlets.info
+        internalInfo = self.sydent.servlets.internalInfo
+
         hash_details = self.sydent.servlets.hash_details
         lookup_v2 = self.sydent.servlets.lookup_v2
 
@@ -83,6 +86,9 @@ class ClientApiHttpServer:
         v1.putChild(b'lookup', lookup)
         v1.putChild(b'bulk_lookup', bulk_lookup)
 
+        v1.putChild(b'info', info)
+        v1.putChild(b'internal-info', internalInfo)
+
         v1.putChild(b'pubkey', pubkey)
         pubkey.putChild(b'isvalid', self.sydent.servlets.pubkeyIsValid)
         pubkey.putChild(b'ed25519:0', pk_ed25519)

+ 79 - 0
sydent/http/info.py

@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+
+# 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.
+
+import logging
+import re
+import copy
+import yaml
+
+logger = logging.getLogger(__name__)
+
+class Info(object):
+    """Returns information from info.yaml, which contains user-specific metadata."""
+
+    def __init__(self, syd, info_file_path):
+        """
+        :param syd: The global sydent object
+        :type syd: Sydent
+
+        :param info_file_path: Filepath of the info.yaml file that defines which homeservers
+            users belong to
+        :type info_file_path: str
+        """
+        self.sydent = syd
+
+        try:
+            file = open(info_file_path)
+            self.config = yaml.safe_load(file)
+            file.close()
+
+            # medium:
+            #   email:
+            #     entries:
+            #       matthew@matrix.org: { hs: 'matrix.org', shadow_hs: 'shadow-matrix.org' }
+            #     patterns:
+            #       - .*@matrix.org: { hs: 'matrix.org', shadow_hs: 'shadow-matrix.org' }
+
+        except Exception as e:
+            logger.error(e)
+
+    def match_user_id(self, medium, address):
+        """Return information for a given medium/address combination.
+
+        :param medium: The medium of the address.
+        :type medium: str
+        :param address: The address of the 3PID.
+        :type address: str
+        :returns a dict containing information regarding the user, or an
+            empty dict if no match was found.
+        :rtype: Dict
+        """
+        result = {}
+
+        # Find an entry in the info file matching this user's ID
+        if address in self.config['medium']['email']['entries']:
+            result = self.config['medium']['email']['entries'][address]
+        else:
+            for pattern_group in self.config['medium']['email']['patterns']:
+                for pattern in pattern_group:
+                    if (re.match("^" + pattern + "$", address)):
+                        result = pattern_group[pattern]
+                        break
+                if result:
+                    break
+
+        # Copy before changing elements
+        return copy.deepcopy(result)

+ 84 - 0
sydent/http/servlets/infoservlet.py

@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+
+# 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.
+
+from twisted.web.resource import Resource
+from netaddr import IPAddress
+
+import logging
+import json
+
+from sydent.db.threepid_associations import GlobalAssociationStore
+from sydent.http.servlets import get_args, jsonwrap, send_cors
+
+logger = logging.getLogger(__name__)
+
+
+class InfoServlet(Resource):
+    """Maps a threepid to the responsible HS domain. For use by clients.
+
+    :param syd: A sydent instance.
+    :type syd: Sydent
+    :param info: An instance of Info.
+    :type info: Sydent.http.Info
+    """
+
+    isLeaf = True
+
+    def __init__(self, syd, info):
+        self.sydent = syd
+        self.info = info
+
+    @jsonwrap
+    def render_GET(self, request):
+        """Clients who are "whitelisted" should receive both hs and shadow_hs in
+        their response JSON. Clients that are not whitelisted should only
+        receive hs, and it's contents should be that of shadow_hs in the
+        config file.
+
+        Returns: { hs: ..., [shadow_hs: ...]}
+        """
+
+        send_cors(request)
+        args = get_args(request, ('medium', 'address'))
+
+        medium = args['medium']
+        address = args['address']
+
+        # Find an entry in the info file matching this user's ID
+        result = self.info.match_user_id(medium, address)
+
+        # Check if there's a MXID associated with this address, if so use its domain as
+        # the value for "hs" (so that the user can still access their account through
+        # Tchap) and move the value "hs" should have had otherwise to "new_hs" if it's a
+        # different one.
+        store = GlobalAssociationStore(self.sydent)
+        mxid = store.getMxid(medium, address)
+        if mxid:
+            current_hs = mxid.split(':', 1)[1]
+            if current_hs != result['hs']:
+                result['new_hs'] = result['hs']
+                result['hs'] = current_hs
+
+        # Non-internal. Remove 'requires_invite' if found
+        result.pop('requires_invite', None)
+
+        return result
+
+    @jsonwrap
+    def render_OPTIONS(self, request):
+        send_cors(request)
+        request.setResponseCode(200)
+        return {}

+ 70 - 0
sydent/http/servlets/internalinfoservlet.py

@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+
+# 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.
+
+from twisted.web.resource import Resource
+
+import logging
+import json
+
+from sydent.db.invite_tokens import JoinTokenStore
+from sydent.http.servlets import get_args, jsonwrap, send_cors
+
+
+logger = logging.getLogger(__name__)
+
+
+class InternalInfoServlet(Resource):
+    """Maps a threepid to the responsible HS domain, and gives invitation status.
+    For use by homeserver instances.
+
+    :param syd: A sydent instance.
+    :type syd: Sydent
+    :param info: An instance of Info.
+    :type info: Sydent.http.Info
+    """
+    isLeaf = True
+
+    def __init__(self, syd, info):
+        self.sydent = syd
+        self.info = info
+
+    @jsonwrap
+    def render_GET(self, request):
+        """
+        Returns: { hs: ..., [shadow_hs: ...], invited: true/false, requires_invite: true/false }
+        """
+
+        send_cors(request)
+        args = get_args(request, ('medium', 'address'))
+
+        medium = args['medium']
+        address = args['address']
+
+        # Find an entry in the info file matching this user's ID
+        result = self.info.match_user_id(medium, address)
+
+        joinTokenStore = JoinTokenStore(self.sydent)
+        pendingJoinTokens = joinTokenStore.getTokens(medium, address)
+
+        # Report whether this user has been invited to a room
+        result['invited'] = True if pendingJoinTokens else False
+        return result
+
+    @jsonwrap
+    def render_OPTIONS(self, request):
+        send_cors(request)
+        request.setResponseCode(200)
+        return {}

+ 8 - 0
sydent/sydent.py

@@ -59,11 +59,14 @@ from sydent.http.servlets.threepidunbindservlet import ThreePidUnbindServlet
 from sydent.http.servlets.replication import ReplicationPushServlet
 from sydent.http.servlets.getvalidated3pidservlet import GetValidated3pidServlet
 from sydent.http.servlets.store_invite_servlet import StoreInviteServlet
+from sydent.http.servlets.infoservlet import InfoServlet
+from sydent.http.servlets.internalinfoservlet import InternalInfoServlet
 from sydent.http.servlets.v1_servlet import V1Servlet
 from sydent.http.servlets.accountservlet import AccountServlet
 from sydent.http.servlets.registerservlet import RegisterServlet
 from sydent.http.servlets.logoutservlet import LogoutServlet
 from sydent.http.servlets.v2_servlet import V2Servlet
+from sydent.http.info import Info
 
 from sydent.db.valsession import ThreePidValSessionStore
 from sydent.db.hashing_metadata import HashingMetadataStore
@@ -94,6 +97,7 @@ CONFIG_DEFAULTS = {
         # Whether clients and homeservers can register an association using v1 endpoints.
         'enable_v1_associations': 'true',
         'delete_tokens_on_bind': 'true',
+        'info_path': 'info.yaml',
     },
     'db': {
         'db.file': os.environ.get('SYDENT_DB_PATH', 'sydent.db'),
@@ -230,6 +234,10 @@ class Sydent:
         self.servlets.registerServlet = RegisterServlet(self)
         self.servlets.logoutServlet = LogoutServlet(self)
 
+        info = Info(self, self.cfg.get("general", "info_path"))
+        self.servlets.info = InfoServlet(self, info)
+        self.servlets.internalInfo = InternalInfoServlet(self, info)
+
         self.threepidBinder = ThreepidBinder(self)
 
         self.sslComponents = SslComponents(self)