From 8444115262cb3c2fb60aa28934a63ef396ca0cc5 Mon Sep 17 00:00:00 2001 From: Goffi Date: Sat, 24 Sep 2022 16:37:46 +0200 Subject: [PATCH] plugin XEP-0384, xml_tools: avoid `getItems` timeout + fix empty node crash + parsing: - use `max_items` in `getItems` calls for bundles, as otherwise some pubsub service may return full nodes, which may be huge is `max_items=1` is not set on the node, possibly resulting in timeouts. - the plugin was crashing when TWOMEMO devices list node has no items at all. This is not the case anymore. - a naive parsing method has been implemented in `xml_tools` to replace the serialisation/deserialisation method. This should be more efficient and will avoid annoying `ns0:` prefixes in XML logs. --- sat/plugins/plugin_xep_0384.py | 33 ++++++++++---------- sat/tools/xml_tools.py | 55 +++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/sat/plugins/plugin_xep_0384.py b/sat/plugins/plugin_xep_0384.py index f68b4db5..6bcea52b 100644 --- a/sat/plugins/plugin_xep_0384.py +++ b/sat/plugins/plugin_xep_0384.py @@ -419,7 +419,7 @@ async def download_oldmemo_bundle( node = f"eu.siacs.conversations.axolotl.bundles:{device_id}" try: - items, __ = await xep_0060.getItems(client, jid.JID(bare_jid), node) + items, __ = await xep_0060.getItems(client, jid.JID(bare_jid), node, max_items=1) except Exception as e: raise omemo.BundleDownloadFailed( f"Bundle download failed for {bare_jid}: {device_id} under namespace" @@ -432,7 +432,7 @@ async def download_oldmemo_bundle( f" {namespace}: Unexpected number of items retrieved: {len(items)}." ) - element = next(iter(domish_to_etree(cast(domish.Element, items[0]))), None) + element = next(iter(xml_tools.domish_elt_2_et_elt(cast(domish.Element, items[0]))), None) if element is None: raise omemo.BundleDownloadFailed( f"Bundle download failed for {bare_jid}: {device_id} under namespace" @@ -476,7 +476,7 @@ def make_session_manager(sat: SAT, profile: str) -> Type[omemo.SessionManager]: client, client.jid.userhostJID(), node, - etree_to_domish(element), + xml_tools.et_elt_2_domish_elt(element), item_id=str(bundle.device_id), extra={ xep_0060.EXTRA_PUBLISH_OPTIONS: { @@ -516,7 +516,7 @@ def make_session_manager(sat: SAT, profile: str) -> Type[omemo.SessionManager]: client, client.jid.userhostJID(), node, - etree_to_domish(element), + xml_tools.et_elt_2_domish_elt(element), item_id=xep_0060.ID_SINGLETON, extra={ xep_0060.EXTRA_PUBLISH_OPTIONS: { xep_0060.OPT_MAX_ITEMS: 1 }, @@ -563,7 +563,7 @@ def make_session_manager(sat: SAT, profile: str) -> Type[omemo.SessionManager]: ) element = next( - iter(domish_to_etree(cast(domish.Element, items[0]))), + iter(xml_tools.domish_elt_2_et_elt(cast(domish.Element, items[0]))), None ) if element is None: @@ -650,7 +650,7 @@ def make_session_manager(sat: SAT, profile: str) -> Type[omemo.SessionManager]: client, client.jid.userhostJID(), node, - etree_to_domish(element), + xml_tools.et_elt_2_domish_elt(element), item_id=xep_0060.ID_SINGLETON, extra={ xep_0060.EXTRA_PUBLISH_OPTIONS: { @@ -705,13 +705,15 @@ def make_session_manager(sat: SAT, profile: str) -> Type[omemo.SessionManager]: f" {namespace}" ) from e - if len(items) != 1: + if len(items) == 0: + return {} + elif len(items) != 1: raise omemo.DeviceListDownloadFailed( f"Device list download failed for {bare_jid} under namespace" f" {namespace}: Unexpected number of items retrieved: {len(items)}." ) - element = next(iter(domish_to_etree(cast(domish.Element, items[0]))), None) + element = next(iter(xml_tools.domish_elt_2_et_elt(cast(domish.Element, items[0]))), None) if element is None: raise omemo.DeviceListDownloadFailed( f"Device list download failed for {bare_jid} under namespace" @@ -866,10 +868,10 @@ def make_session_manager(sat: SAT, profile: str) -> Type[omemo.SessionManager]: "timestamp": time.time() })) - message_data["xml"].addChild(etree_to_domish(element)) + message_data["xml"].addChild(xml_tools.et_elt_2_domish_elt(element)) try: - await client.send(message_data["xml"]) + await client.a_send(message_data["xml"]) except Exception as e: raise omemo.MessageSendingFailed() from e @@ -1493,7 +1495,6 @@ class OMEMO: encrypted. @return: Whether to continue the message received flow. """ - muc_plaintext_cache_key: Optional[MUCPlaintextCacheKey] = None sender_jid = jid.JID(message_elt["from"]) @@ -1583,7 +1584,7 @@ class OMEMO: if twomemo_encrypted_elt is not None: try: message = twomemo.etree.parse_message( - domish_to_etree(twomemo_encrypted_elt), + xml_tools.domish_elt_2_et_elt(twomemo_encrypted_elt), sender_bare_jid ) except (ValueError, XMLSchemaValidationError): @@ -1597,7 +1598,7 @@ class OMEMO: if oldmemo_encrypted_elt is not None: try: message = await oldmemo.etree.parse_message( - domish_to_etree(oldmemo_encrypted_elt), + xml_tools.domish_elt_2_et_elt(oldmemo_encrypted_elt), sender_bare_jid, client.jid.userhost(), session_manager @@ -1999,11 +2000,11 @@ class OMEMO: if namespace == twomemo.twomemo.NAMESPACE: # Add the encrypted element - stanza.addChild(etree_to_domish(twomemo.etree.serialize_message(message))) + stanza.addChild(xml_tools.et_elt_2_domish_elt(twomemo.etree.serialize_message(message))) if namespace == oldmemo.oldmemo.NAMESPACE: # Add the encrypted element - stanza.addChild(etree_to_domish(oldmemo.etree.serialize_message(message))) + stanza.addChild(xml_tools.et_elt_2_domish_elt(oldmemo.etree.serialize_message(message))) if muc_plaintext_cache_key is not None: self.__muc_plaintext_cache[muc_plaintext_cache_key] = plaintext @@ -2031,7 +2032,7 @@ class OMEMO: log.debug("Ignoring empty device list update.") return - item_elt = domish_to_etree(item) + item_elt = xml_tools.domish_elt_2_et_elt(item) device_list: Dict[int, Optional[str]] = {} namespace: Optional[str] = None diff --git a/sat/tools/xml_tools.py b/sat/tools/xml_tools.py index 50ffd35f..27b79747 100644 --- a/sat/tools/xml_tools.py +++ b/sat/tools/xml_tools.py @@ -18,7 +18,7 @@ import re -from typing import Optional +from typing import Optional, Tuple import html.entities from collections import OrderedDict from xml.dom import minidom, NotFoundErr @@ -30,6 +30,7 @@ from sat.core import exceptions from sat.core.i18n import _ from sat.core.constants import Const as C from sat.core.log import getLogger +import xml.etree.ElementTree as ET log = getLogger(__name__) @@ -1957,3 +1958,55 @@ def pFmtElt(elt, indent=0, defaultUri=""): def ppElt(elt): """Pretty print a domish.Element""" print(pFmtElt(elt)) + + +# ElementTree + +def et_get_namespace_and_name(et_elt: ET.Element) -> Tuple[Optional[str], str]: + """Retrieve element namespace and name from ElementTree element + + @param et_elt: ElementTree element + @return: namespace and name of the element + if not namespace if specified, None is returned + """ + name = et_elt.tag + if not name: + raise ValueError("no name set in ET element") + elif name[0] != "{": + return None, name + end_idx = name.find("}") + if end_idx == -1: + raise ValueError("Invalid ET name") + return name[1:end_idx], name[end_idx+1:] + + +def et_elt_2_domish_elt(et_elt: ET.Element) -> domish.Element: + """Convert ElementTree element to Twisted's domish.Element + + Note: this is a naive implementation, adapted to XMPP, and some content are ignored + (attributes namespaces, tail) + """ + namespace, name = et_get_namespace_and_name(et_elt) + elt = domish.Element((namespace, name), attribs=et_elt.attrib) + if et_elt.text: + elt.addContent(et_elt.text) + for child in et_elt: + elt.addChild(et_elt_2_domish_elt(child)) + return elt + + +def domish_elt_2_et_elt(elt: domish.Element) -> ET.Element: + """Convert Twisted's domish.Element to ElementTree equivalent + + Note: this is a naive implementation, adapter to XMPP, and some text content may be + missing (content put after a tag, i.e. what would go to the "tail" attribute of ET + Element) + """ + tag = f"{{{elt.uri}}}{elt.name}" if elt.uri else elt.name + et_elt = ET.Element(tag, attrib=elt.attributes) + content = str(elt) + if content: + et_elt.text = str(elt) + for child in elt.elements(): + et_elt.append(domish_elt_2_et_elt(child)) + return et_elt