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.
This commit is contained in:
Goffi 2022-09-24 16:37:46 +02:00
commit 8444115262
2 changed files with 71 additions and 17 deletions

View file

@ -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

View file

@ -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