changeset 460:607616f9ef5b

backend: new `server_jid` option: Server domain must be known to validate requests, this can be done explicitely by using the `server_jid` option. If this option is not set, the server domain is found: - by using the `from` name of the initial delegation advertising message - or it fallbacks to using the part after initial `.` (`pubsub.example.org` would give `example.org`)
author Goffi <goffi@goffi.org>
date Fri, 15 Oct 2021 09:32:07 +0200
parents cebcb7f56889
children c9238fca1fb3
files CHANGELOG sat_pubsub/backend.py sat_pubsub/delegation.py sat_pubsub/privilege.py twisted/plugins/pubsub.py
diffstat 5 files changed, 57 insertions(+), 40 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGELOG	Fri Oct 15 09:32:04 2021 +0200
+++ b/CHANGELOG	Fri Oct 15 09:32:07 2021 +0200
@@ -6,6 +6,7 @@
     - Full-Text Search, with node setting to specify language
     - XEP-0346 (Form Discovery and Publishing) implementation (replacing the non standard node schema)
     - environment variables can now be used to set options
+    - server jid can now be specified with "server_jid" parameter, otherwise it's determined automatically
     - service name can now be specified with "service_name" parameter
     - namespace delegation update to v0.5 ("urn:xmpp:delegation:2" is now used)
     - bug fixes
--- a/sat_pubsub/backend.py	Fri Oct 15 09:32:04 2021 +0200
+++ b/sat_pubsub/backend.py	Fri Oct 15 09:32:07 2021 +0200
@@ -224,6 +224,11 @@
         self.storage = storage
         self.config = config
         self.admins = config['admins_jids_list']
+        self.jid = config["jid"]
+        if config["server_jid"] is None:
+            self.server_jid = jid.JID(str(self.jid).split(".", 1)[1])
+        else:
+            self.server_jid = jid.JID(config["server_jid"])
         d = self.storage.getFTSLanguages()
         d.addCallbacks(self._getFTSLanguagesCb, self._getFTSLanguagesEb)
 
--- a/sat_pubsub/delegation.py	Fri Oct 15 09:32:04 2021 +0200
+++ b/sat_pubsub/delegation.py	Fri Oct 15 09:32:07 2021 +0200
@@ -55,6 +55,7 @@
 
     def __init__(self):
         super(DelegationsHandler, self).__init__()
+        self.backend = None
 
     def _service_hack(self):
         """Patch the request classes of services to track delegated stanzas"""
@@ -102,6 +103,7 @@
         DelegationsHandler._service_hacked = True
 
     def connectionInitialized(self):
+        self.backend = self.parent.parent.getServiceNamed('backend')
         if not self._service_hacked:
             self._service_hack()
         self.xmlstream.addObserver(DELEGATION_ADV_XPATH, self.onAdvertise)
@@ -150,6 +152,20 @@
 
     def onAdvertise(self, message):
         """Manage the <message/> advertising delegations"""
+        if self.backend.config["server_jid"] is None:
+            # if server_jid is not specified in config, we use the advertising message
+            # to get it (and replace the one found from this component jid)
+            self.backend.server_jid = self.backend.config["server_jid"] = jid.JID(
+                message["from"]
+            )
+        else:
+            if jid.JID(message["from"]) != self.backend.server_jid:
+                log.err(
+                    f"Delagation advertising message received from {message['from']}, "
+                    f"while expected server jid is {self.backend.server_jid}, this may "
+                    "be a security threat, please check your configuration and network."
+                )
+                raise error.StanzaError("not-allowed")
         delegation_elt = next(message.elements(DELEGATION_NS, 'delegation'))
         delegated = {}
         for delegated_elt in delegation_elt.elements(DELEGATION_NS):
@@ -182,15 +198,9 @@
 
         @param iq(domish.Element): full delegation stanza
         """
-
-        # FIXME: we use a hack supposing that our delegation come from hostname
-        #        and we are a component named [name].hostname
-        #        but we need to manage properly allowed servers
-        # TODO: do proper origin security check
-        _, allowed = iq['to'].split('.', 1)
-        if jid.JID(iq['from']) != jid.JID(allowed):
-            log.msg(("SECURITY WARNING: forwarded stanza doesn't come from our server: {}"
-                     .format(iq.toXml())).encode('utf-8'))
+        if jid.JID(iq['from']) != self.backend.server_jid:
+            log.err("SECURITY WARNING: forwarded stanza doesn't come from our server: "
+                    f"{iq.toXml()}")
             raise error.StanzaError('not-allowed')
 
         try:
@@ -224,7 +234,6 @@
         """
         if not nodeIdentifier.startswith(DELEGATION_NS):
             return []
-
         try:
             _, namespace = nodeIdentifier.split(DELEGATION_MAIN_SEP, 1)
         except ValueError:
--- a/sat_pubsub/privilege.py	Fri Oct 15 09:32:04 2021 +0200
+++ b/sat_pubsub/privilege.py	Fri Oct 15 09:32:07 2021 +0200
@@ -1,7 +1,6 @@
 #!/usr/bin/env python3
-#-*- coding: utf-8 -*-
 #
-# Copyright (c) 2015 Jérôme Poisson
+# Copyright (c) 2015-2021 Jérôme Poisson
 
 
 # This program is free software: you can redistribute it and/or modify
@@ -17,7 +16,8 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-# ---
+"This module implements XEP-0356 (Privileged Entity) to manage rosters, messages and "
+"presences"
 
 # This module implements XEP-0356 (Privileged Entity) to manage rosters, messages and presences
 
@@ -58,21 +58,17 @@
 
     def __init__(self, service_jid):
         super(PrivilegesHandler, self).__init__()
+        self.backend = None
         self._permissions = {PERM_ROSTER: 'none',
                              PERM_MESSAGE: 'none',
                              PERM_PRESENCE: 'none'}
         self._pubsub_service = None
-        self._backend = None
-        # FIXME: we use a hack supposing that our privilege come from hostname
-        #        and we are a component named [name].hostname
-        #        but we need to manage properly server
-        # TODO: do proper server handling
-        self.server_jid = jid.JID(service_jid.host.split('.', 1)[1])
         self.caps_map = {}  # key: bare jid, value: dict of resources with caps hash
-        self.hash_map = {}  # key: (hash,version), value: dict with DiscoInfo instance (infos) and nodes to notify (notify)
+        # key: (hash,version), value: dict with DiscoInfo instance (infos) and nodes to
+        # notify (notify)
+        self.hash_map = {}
         self.roster_cache = {}  # key: jid, value: dict with "timestamp" and "roster"
         self.presence_map = {}  # inverted roster: key: jid, value: set of entities who has this jid in roster (with presence of "from" or "both")
-        self.server = None
 
     @property
     def permissions(self):
@@ -83,9 +79,9 @@
             if IPubSubService.providedBy(handler):
                 self._pubsub_service = handler
                 break
-        self._backend = self.parent.parent.getServiceNamed('backend')
+        self.backend = self.parent.parent.getServiceNamed('backend')
         self.xmlstream.addObserver(PRIV_ENT_ADV_XPATH, self.onAdvertise)
-        self.xmlstream.addObserver('/presence', self.onPresence)
+        self.xmlstream.addObserver('/presence', self._onPresence)
 
     def onAdvertise(self, message):
         """Managage the <message/> advertising privileges
@@ -175,7 +171,7 @@
 
         main_message = domish.Element((None, "message"))
         if to_jid is None:
-            to_jid = self.server_jid
+            to_jid = self.backend.server_jid
         main_message['to'] = to_jid.full()
         privilege_elt = main_message.addElement((PRIV_ENT_NS, 'privilege'))
         forwarded_elt = privilege_elt.addElement((FORWARDED_NS, 'forwarded'))
@@ -238,18 +234,15 @@
 
     ## presence ##
 
-    @defer.inlineCallbacks
-    def onPresence(self, presence_elt):
-        if self.server is None:
-            # FIXME: we use a hack supposing that our delegation come from hostname
-            #        and we are a component named [name].hostname
-            #        but we need to manage properly allowed servers
-            # TODO: do proper origin security check
-            _, self.server = presence_elt['to'].split('.', 1)
+    def _onPresence(self, presence_elt: domish.Element) -> None:
+        defer.ensureDeferred(self.onPresence(presence_elt))
+
+    async def onPresence(self, presence_elt: domish.Element) -> None:
         from_jid = jid.JID(presence_elt['from'])
         from_jid_bare = from_jid.userhostJID()
-        if from_jid.host == self.server and from_jid_bare not in self.roster_cache:
-            roster = yield self.getRoster(from_jid_bare)
+        if ((jid.JID(from_jid.host) == self.backend.server_jid
+             and from_jid_bare not in self.roster_cache)):
+            roster = await self.getRoster(from_jid_bare)
             timestamp = time.time()
             self.roster_cache[from_jid_bare] = {'timestamp': timestamp,
                                                 'roster': roster,
--- a/twisted/plugins/pubsub.py	Fri Oct 15 09:32:04 2021 +0200
+++ b/twisted/plugins/pubsub.py	Fri Oct 15 09:32:07 2021 +0200
@@ -64,8 +64,6 @@
 from sat_pubsub import const
 
 
-
-
 def coerceListType(value):
     return next(csv.reader(
         [value], delimiter=",", quotechar='"', skipinitialspace=True
@@ -79,10 +77,23 @@
     return values
 
 
+def coerceJidDomainType(value):
+    try:
+        jid_ = JID(value)
+    except Exception as e:
+        raise ValueError(f"JID set in configuration ({value!r}) is invalid: {e}")
+    if jid_.resource or jid_.user:
+        raise ValueError(
+            f"JID in configuration ({jid_!r}) must have no local part and no resource"
+        )
+    return jid_
+
 
 OPT_PARAMETERS_BOTH = [
-    ['jid', None, None, 'JID this component will be available at'],
+    ['jid', None, None, 'JID this component will be available at', coerceJidDomainType],
     ['xmpp_pwd', None, None, 'XMPP server component password'],
+    ['server_jid', None, None, 'jid of the server this component is plugged to',
+     coerceJidDomainType],
     ['rhost', None, '127.0.0.1', 'XMPP server host'],
     ['rport', None, '5347', 'XMPP server port'],
     ['backend', None, 'pgsql', 'Choice of storage backend'],
@@ -148,7 +159,7 @@
     ]
 
     def __init__(self):
-        """Read SàT Pubsub configuration file in order to overwrite the hard-coded default values.
+        """Read Libervia Pubsub configuration file in order to overwrite the hard-coded default values.
 
         Priority for the usage of the values is (from lowest to highest):
             - hard-coded default values
@@ -200,8 +211,6 @@
         if self['backend'] == 'memory':
             raise NotImplementedError('memory backend is not available at the moment')
 
-        self['jid'] = JID(self['jid']) if self['jid'] else None
-
 
 @implementer(IServiceMaker, IPlugin)
 class SatPubsubMaker(object):