Welcome to mirror list, hosted at ThFree Co, Russian Federation.

dev.gajim.org/gajim/python-nbxmpp.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilipp Hörist <philipp@hoerist.com>2023-04-18 00:25:09 +0300
committerPhilipp Hörist <philipp@hoerist.com>2023-04-20 23:42:59 +0300
commit63889e9543c56b9e0172dc6be816e9b75d52fa49 (patch)
treedb611993369283a6e27f854cf91a4937ac3d0a9e
parentf9220af8238c623c7a1cfb28776276c4ef84d70f (diff)
feat: Add method to generate XMPP IRIs
-rw-r--r--nbxmpp/protocol.py46
-rw-r--r--nbxmpp/xmppiri.py57
-rw-r--r--test/unit/test_jid_parsing.py23
3 files changed, 126 insertions, 0 deletions
diff --git a/nbxmpp/protocol.py b/nbxmpp/protocol.py
index 7ff4595..ecf233a 100644
--- a/nbxmpp/protocol.py
+++ b/nbxmpp/protocol.py
@@ -38,6 +38,12 @@ from dataclasses import asdict
from gi.repository import GLib
import idna
+from nbxmpp.xmppiri import escape_ifragment
+from nbxmpp.xmppiri import escape_inode
+from nbxmpp.xmppiri import escape_ires
+from nbxmpp.xmppiri import escape_ivalue
+from nbxmpp.xmppiri import validate_ikey
+from nbxmpp.xmppiri import validate_querytype
from nbxmpp.simplexml import Node
from nbxmpp.namespaces import Namespace
from nbxmpp.stringprep import nodeprep
@@ -315,6 +321,7 @@ ERR_REDIRECT = 'urn:ietf:params:xml:ns:xmpp-stanzas redirect'
STREAM_UNSUPPORTED_STANZA_TYPE = 'urn:ietf:params:xml:ns:xmpp-streams unsupported-stanza-type'
ERR_FORBIDDEN = 'urn:ietf:params:xml:ns:xmpp-stanzas forbidden'
+
def isResultNode(node):
"""
Return true if the node is a positive reply
@@ -768,6 +775,45 @@ class JID:
return f'{localpart}@{self.domain}{domain_encoded}'
return f'{localpart}@{self.domain}/{self.resource}{domain_encoded}'
+ def to_iri(self,
+ query: Optional[str | tuple[str, list[tuple[str, str]]]] = None,
+ fragment: Optional[str] = None
+ ) -> str:
+
+ if self.localpart:
+ inode = escape_inode(self.localpart)
+ ipathxmpp = f'{inode}@{self.domain}'
+ else:
+ ipathxmpp = f'{self.domain}'
+
+ if self.resource is not None:
+ ires = escape_ires(self.resource)
+ ipathxmpp = f'{ipathxmpp}/{ires}'
+
+ iri = f'xmpp:{ipathxmpp}'
+
+ if query is not None:
+ if isinstance(query, str):
+ querytype = query
+ queryparams = None
+ else:
+ querytype, queryparams = query
+
+ iquerytype = validate_querytype(querytype)
+ iri += f'?{iquerytype}'
+
+ if queryparams is not None:
+ for ikey, ivalue in queryparams:
+ ivalue = escape_ivalue(ivalue)
+ ikey = validate_ikey(ikey)
+ iri += f';{ikey}={ivalue}'
+
+ if fragment is not None:
+ ifragment = escape_ifragment(fragment)
+ iri += f'#{ifragment}'
+
+ return iri
+
def copy(self) -> JID:
deprecation_warning('copy() is not needed, JID is immutable')
return self
diff --git a/nbxmpp/xmppiri.py b/nbxmpp/xmppiri.py
new file mode 100644
index 0000000..18af188
--- /dev/null
+++ b/nbxmpp/xmppiri.py
@@ -0,0 +1,57 @@
+
+import re
+from gi.repository import GLib
+
+
+# https://www.rfc-editor.org/rfc/rfc3987
+
+ucschar = r'\xA0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF'\
+ r'\U00010000-\U0001FFFD\U00020000-\U0002FFFD\U00030000-\U0003FFFD'\
+ r'\U00040000-\U0004FFFD\U00050000-\U0005FFFD\U00060000-\U0006FFFD'\
+ r'\U00070000-\U0007FFFD\U00080000-\U0008FFFD\U00090000-\U0009FFFD'\
+ r'\U000A0000-\U000AFFFD\U000B0000-\U000BFFFD\U000C0000-\U000CFFFD'\
+ r'\U000D0000-\U000DFFFD\U000E1000-\U000EFFFD'
+unreserved = r'A-Za-z0-9\-._~'
+iunreserved = fr'{unreserved}{ucschar}'
+subdelims = r"!$&'()*+,;="
+
+# https://www.rfc-editor.org/rfc/rfc5122.html#section-2.2
+nodeallow = r"!$()*+,;="
+resallow = r"!$&'()*+,:;="
+
+# ifragment without iunreserved and pct-encoded
+reserved_chars_allowed_in_ifragment = subdelims + ":@" + "/?"
+
+rx_ikey = f'[{iunreserved}]*'
+rx_iquerytype = f'[{iunreserved}]*'
+
+
+def validate_ikey(ikey: str) -> str:
+ res = re.fullmatch(rx_ikey, ikey)
+ if res is None:
+ raise ValueError('Not allowed characters in key')
+ return ikey
+
+
+def validate_querytype(querytype: str) -> str:
+ res = re.fullmatch(rx_iquerytype, querytype)
+ if res is None:
+ raise ValueError('Not allowed characters in querytype')
+ return querytype
+
+
+def escape_ifragment(ifragment: str) -> str:
+ return GLib.Uri.escape_string(
+ ifragment, reserved_chars_allowed_in_ifragment, True)
+
+
+def escape_ivalue(ivalue: str) -> str:
+ return GLib.Uri.escape_string(ivalue, None, True)
+
+
+def escape_inode(inode: str) -> str:
+ return GLib.Uri.escape_string(inode, nodeallow, True)
+
+
+def escape_ires(ires: str) -> str:
+ return GLib.Uri.escape_string(ires, resallow, True)
diff --git a/test/unit/test_jid_parsing.py b/test/unit/test_jid_parsing.py
index a91c667..ae59ab4 100644
--- a/test/unit/test_jid_parsing.py
+++ b/test/unit/test_jid_parsing.py
@@ -189,3 +189,26 @@ class JIDParsing(unittest.TestCase):
# from_user_input does only support bare jids
with self.assertRaises(Exception):
JID.from_user_input(user_input)
+
+ def test_jid_to_iri(self):
+ tests = [
+ ('nasty!#$%()*+,-.;=?[\\]^_`{|}~node@example.com', 'xmpp:nasty!%23$%25()*+,-.;=%3F%5B%5C%5D%5E_%60%7B%7C%7D~node@example.com'),
+ ('node@example.com/repulsive !#"$%&\'()*+,-./:;<=>?@[\\]^_`{|}~resource', 'xmpp:node@example.com/repulsive%20!%23%22$%25&\'()*+,-.%2F:;%3C=%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~resource'),
+ ]
+
+ for jid, iri in tests:
+ jid = JID.from_string(jid)
+ self.assertEqual(jid.to_iri(), iri)
+
+ jid = JID.from_string('example-node@example.com')
+ iri = jid.to_iri(('message', [('subject', 'Hello World')]), 'frag')
+ self.assertEqual(iri, 'xmpp:example-node@example.com?message;subject=Hello%20World#frag')
+
+ iri = jid.to_iri('message')
+ self.assertEqual(iri, 'xmpp:example-node@example.com?message')
+
+ iri = jid.to_iri(fragment='onlyfragment')
+ self.assertEqual(iri, 'xmpp:example-node@example.com#onlyfragment')
+
+ jid = JID.from_user_input('call me "ishmael"@example.com')
+ self.assertEqual(jid.to_iri(), 'xmpp:call%5C20me%5C20%5C22ishmael%5C22@example.com')