# HG changeset patch # User Goffi # Date 1634283127 -7200 # Node ID 607616f9ef5b2aa6d7be9426d7c8562283907502 # Parent cebcb7f56889f900829176bbe3bdf258d073fee6 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`) diff -r cebcb7f56889 -r 607616f9ef5b CHANGELOG --- 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 diff -r cebcb7f56889 -r 607616f9ef5b sat_pubsub/backend.py --- 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) diff -r cebcb7f56889 -r 607616f9ef5b sat_pubsub/delegation.py --- 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 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: diff -r cebcb7f56889 -r 607616f9ef5b sat_pubsub/privilege.py --- 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 . -# --- +"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 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, diff -r cebcb7f56889 -r 607616f9ef5b twisted/plugins/pubsub.py --- 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):