Mercurial > libervia-pubsub
view sat_pubsub/bookmark_compat.py @ 491:4e8e8788bc86
Bookmark compatibility layer:
The new `bookmark_compat` module add a compatibility layer between XEP-0048 (with
XEP-0049 private XML storage) and XEP-0402, i.e. it implements the
`urn:xmpp:bookmarks:1#compat` feature.
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 21 Nov 2024 11:03:51 +0100 |
parents | |
children | 468b7cd6c344 |
line wrap: on
line source
#!/usr/bin/env python3 # # Copyright (c) 2015-2024 Jérôme Poisson # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # 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 a compatibility layer for XEP-0048." from twisted.internet import defer from twisted.python import log from twisted.words.protocols.jabber import error as jabber_error, jid, xmlstream from twisted.words.xish import domish from wokkel import disco, iwokkel, pubsub from wokkel.iwokkel import IPubSubService from zope.interface import implementer from sat_pubsub import const from . import error NS_IQ_PRIVATE = "jabber:iq:private" NS_STORAGE_BOOKMARKS = "storage:bookmarks" NS_BOOKMARKS2 = "urn:xmpp:bookmarks:1" NS_BOOKMARKS_COMPAT = f"{NS_BOOKMARKS2}#compat" IQ_PRIVATE_GET = f'/iq[@type="get"]/query[@xmlns="{NS_IQ_PRIVATE}"]' IQ_PRIVATE_SET = f'/iq[@type="set"]/query[@xmlns="{NS_IQ_PRIVATE}"]' @implementer(iwokkel.IDisco) class BookmarkCompatHandler(disco.DiscoClientProtocol): def __init__(self, service_jid): super().__init__() self.backend = None def connectionInitialized(self): for handler in self.parent.handlers: if IPubSubService.providedBy(handler): self._pubsub_service = handler break self.backend = self.parent.parent.getServiceNamed("backend") self.xmlstream.addObserver(IQ_PRIVATE_GET, self._on_get) self.xmlstream.addObserver(IQ_PRIVATE_SET, self._on_set) def _on_get(self, iq_elt: domish.Element) -> None: """Handle incoming legacy IQ GET requests for bookmarks. This method processes legacy XEP-0048 IQ GET requests. It does some checks and then proceeds to return the bookmarks. @param iq_elt: The incoming IQ element. """ if not iq_elt.delegated: return assert self.xmlstream is not None iq_elt.handled = True from_jid = jid.JID(iq_elt["from"]) to_jid = jid.JID(iq_elt["to"]) if from_jid.userhostJID() != to_jid: msg = ( f"{from_jid.userhost()} is not allowed to access private storage of " f"{to_jid.userhost()}!" ) log.msg(f"Hack attempt? {msg}") error_elt = jabber_error.StanzaError("forbidden", text=msg).toResponse(iq_elt) self.xmlstream.send(error_elt) return query_elt = iq_elt.query assert query_elt is not None storage_elt = query_elt.firstChildElement() if ( storage_elt is None or storage_elt.name != "storage" or storage_elt.uri != NS_STORAGE_BOOKMARKS ): error_elt = jabber_error.StanzaError( "not-allowed", text=( f'"{NS_STORAGE_BOOKMARKS}" is the only private XML storage allowed ' "on this server" ), ).toResponse(iq_elt) self.xmlstream.send(error_elt) return defer.ensureDeferred(self.return_bookmarks(iq_elt, to_jid)) async def return_bookmarks(self, iq_elt: domish.Element, requestor: jid.JID) -> None: """Send IQ result for bookmark request on private XML. Retrieve bookmark from Bookmarks2 PEP node, convert them to private XML XEP-0048 format, and send the IQ result. @param iq_elt: The incoming IQ element. @param requestor: The JID of the entity requesting the bookmarks. """ assert self.backend is not None assert self.xmlstream is not None items, __ = await self.backend.getItems( NS_BOOKMARKS2, requestor, requestor, ext_data={"pep": True} ) iq_result_elt = xmlstream.toResponse(iq_elt, "result") query_elt = iq_result_elt.addElement((NS_IQ_PRIVATE, "query")) storage_elt = query_elt.addElement((NS_STORAGE_BOOKMARKS, "storage")) # For simply add all bookmarks to get XEP-0048 private XML elements. for item_elt in items: conference_elt = next(item_elt.elements(NS_BOOKMARKS2, "conference"), None) if conference_elt is not None: # The namespace is not the same for XEP-0048 conference_elt.uri = NS_STORAGE_BOOKMARKS for elt in conference_elt.children: elt.uri = NS_STORAGE_BOOKMARKS conference_elt["jid"] = item_elt["id"] storage_elt.addChild(conference_elt) else: log.msg( "Warning: Unexpectedly missing conference element: " f"{item_elt.toXml()}" ) self.xmlstream.send(iq_result_elt) def _on_set(self, iq_elt: domish.Element) -> None: if not iq_elt.delegated: return assert self.xmlstream is not None iq_elt.handled = True from_jid = jid.JID(iq_elt["from"]) to_jid = jid.JID(iq_elt["to"]) if from_jid.userhostJID() != to_jid: msg = ( f"{from_jid.userhost()} is not allowed to access private storage of " f"{to_jid.userhost()}!" ) log.msg(f"Hack attempt? {msg}") error_elt = jabber_error.StanzaError("forbidden", text=msg).toResponse(iq_elt) self.xmlstream.send(error_elt) return query_elt = iq_elt.query assert query_elt is not None storage_elt = query_elt.firstChildElement() if ( storage_elt is None or storage_elt.name != "storage" or storage_elt.uri != NS_STORAGE_BOOKMARKS ): error_elt = jabber_error.StanzaError( "not-allowed", text=( f'"{NS_STORAGE_BOOKMARKS}" is the only private XML storage allowed ' "on this server" ), ).toResponse(iq_elt) self.xmlstream.send(error_elt) return defer.ensureDeferred(self.on_set(iq_elt, from_jid)) async def publish_bookmarks( self, iq_elt: domish.Element, items: list[domish.Element], requestor: jid.JID ) -> None: """Publish bookmarks on Bookmarks2 PEP node""" assert self.backend is not None assert self.xmlstream is not None try: await self.backend.publish( NS_BOOKMARKS2, items, requestor, options={const.OPT_ACCESS_MODEL: const.VAL_AMODEL_WHITELIST}, pep=True, recipient=requestor, ) except error.NodeNotFound: # The node doesn't exist, we create it await self.backend.createNode( NS_BOOKMARKS2, requestor, options={const.OPT_ACCESS_MODEL: const.VAL_AMODEL_WHITELIST}, pep=True, recipient=requestor, ) await self.publish_bookmarks(iq_elt, items, requestor) except Exception as e: log.err(f"Error while publishing converted bookmarks: {e}") error_elt = jabber_error.StanzaError( "internal-server-error", text=(f"Something went wrong while publishing the bookmark items: {e}"), ).toResponse(iq_elt) self.xmlstream.send(error_elt) raise e async def on_set(self, iq_elt: domish.Element, requestor: jid.JID) -> None: """Handle IQ set request for bookmarks on private XML. This method processes an IQ set request to update bookmarks stored in private XML. It extracts conference elements from the request, transforms them into XEP-0402 format, and publishes them to XEP-0402 PEP node. @param iq_elt: The incoming IQ element containing the set request. @param requestor: The JID of the entity making the request. """ # TODO: We should check if items already exist in Bookmarks 2 and avoid updating # them if there is no change. However, considering that XEP-0048 is deprecated and # active implementations without XEP-0402 are rare, it might not be worth the # trouble. assert self.backend is not None assert self.xmlstream is not None query_elt = iq_elt.query assert query_elt is not None # <storage> presence has been checked in ``_on_set``, we know that we have it # here. storage_elt = next(query_elt.elements(NS_STORAGE_BOOKMARKS, "storage")) items = [] for conference_elt in storage_elt.elements(NS_STORAGE_BOOKMARKS, "conference"): item_elt = domish.Element((pubsub.NS_PUBSUB, "item")) try: item_elt["id"] = conference_elt["jid"] except AttributeError: log.msg( "Warning: ignoring <conference> with missing jid: " f"conference_elt.toXml()" ) continue new_conference_elt = item_elt.addElement((NS_BOOKMARKS2, "conference")) for attr, value in conference_elt.attributes.items(): if attr == "jid": continue new_conference_elt[attr] = value for child in conference_elt.children: new_child = domish.Element((NS_BOOKMARKS2, child.name)) new_child.addContent(str(child)) new_conference_elt.addChild(new_child) items.append(item_elt) await self.publish_bookmarks(iq_elt, items, requestor) iq_result_elt = xmlstream.toResponse(iq_elt, "result") self.xmlstream.send(iq_result_elt) def getDiscoInfo(self, requestor, service, nodeIdentifier=""): return [ disco.DiscoFeature(NS_IQ_PRIVATE), disco.DiscoFeature(NS_BOOKMARKS_COMPAT), ] def getDiscoItems(self, requestor, service, nodeIdentifier=""): return []