Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Escape user id following XMPP protocol specifications #28

Open
wants to merge 3 commits into from

3 participants

@vipod

In these commits we applied bunch of fixes to escape user ids, so that, for example, even email addresses will work for user ids. Actually this was our use case: make jarn.xmpp.* bundle work in plone with 'email as login' feature switched on.

We escape user ids both on server and client side. Escaping procedure was implemented according to XMPP protocol specifications.

@ggozad
Owner

Awesome! I am happy to merge as is, would it be possible update changelogs, readme etc, including instructions and migrations if necessary for existing installations? I think since the storage is affected it might be necessary to provide an upgradeStep, if not let me know and I will cut a new release.

@vipod

Okay, we'll get back to you once all above things are reviewed. Another thing I have to mention is that we were not able to make collaborative editing work properly on our installations, so completely disabled it and using only 'messaging' functionality. That's why the changes made in this pull request haven't been tested with collaborative editing. So this is one more thing we should probably check now.

@ggozad
Owner

Thank you! Do not worry about j.x.collaboration. There have been changes in tinymce that completely break it anyway, so it is not an issue with your code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
20 jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js
@@ -82,6 +82,11 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google
};
})();
+ escapeSelector = function(selector) {
+ return selector.replace(/\\/g, "\\\\")
+ .replace(/[@#;&,.+*~':"!^$[\]()=>|\/]/g, "\\$&");
+ };
+
jarnxmpp.UI = {
_: null,
@@ -186,7 +191,8 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google
$(document).bind('jarnxmpp.presence', function (event, jid, status, presence) {
var user_id = Strophe.getNodeFromJid(jid),
barejid = Strophe.getBareJidFromJid(jid),
- existing_user_element = $('#online-users-' + user_id),
+ user_selector = escapeSelector(Strophe.unescapeNode(user_id)),
+ existing_user_element = $('#online-users-' + user_selector),
online_count;
if (barejid === Strophe.getBareJidFromJid(jarnxmpp.connection.jid)) {
return;
@@ -198,7 +204,7 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google
existing_user_element.attr('class', status);
} else {
$.get(portal_url + '/xmpp-userDetails?jid=' + barejid, function (user_details) {
- if ($('#online-users-' + user_id).length > 0) {
+ if ($('#online-users-' + user_selector).length > 0) {
return;
}
user_details = $(user_details);
@@ -240,7 +246,7 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google
$(document).bind('jarnxmpp.message', function (event) {
var user_id = Strophe.getNodeFromJid(event.from),
$text_p = $('<p>').html(event.body),
- $form = $('#online-users li#online-users-' + user_id + ' .replyForm').clone(),
+ $form = $('#online-users li#online-users-' + escapeSelector(Strophe.unescapeNode(user_id)) + ' .replyForm').clone(),
$reply_p = $('<p>').append($form),
text = $('<div>').append($text_p).append($reply_p).remove().html();
$('input[type="submit"]', $form).attr('value', 'Reply');
@@ -282,7 +288,7 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google
return;
}
$('#site-stream-link').addClass('newStreamMessage');
- $('.pubsubNode[data-node*=' + event.node + '], .pubsubNode[data-node=people]').each(function (idx, node) {
+ $('.pubsubNode[data-node*=' + escapeSelector(event.node) + '], .pubsubNode[data-node=people]').each(function (idx, node) {
var $li,
$node = $(node),
isLeaf = $node.attr('data-leaf') === 'True';
@@ -351,7 +357,7 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google
// Follow/unfollow user.
$('a.followingStatus').live('click', function (e) {
var $following_link = $(this),
- node_id = $following_link.attr('data-user'),
+ node_id = Strophe.escapeNode($following_link.attr('data-user')),
fullname = $following_link.attr('data-fullname');
jarnxmpp.PubSub.getSubscriptions(function (following) {
if (following.indexOf(node_id) > -1) {
@@ -588,7 +594,7 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google
.attr('value', node)));
jarnxmpp.Presence.getUserInfo(node, function (info) {
if (info) {
- $('input[value=' + node + ']', $sl).after(info.fullname);
+ $('input[value=' + escapeSelector(node) + ']').after(info.fullname);
} else {
$('input[value=' + node + ']', $sl).parent().remove();
}
@@ -606,7 +612,7 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google
$('#follow-selected').attr('checked', 'checked');
}
$.each(subscribed_nodes, function (idx, node) {
- $('input[value=' + node + ']', $sl)
+ $('input[value=' + escapeSelector(node) + ']', $sl)
.attr('checked', 'checked')
.parent().addClass('subscribed');
});
View
25 jarn/xmpp/core/browser/pubsub.py
@@ -8,6 +8,7 @@
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from jarn.xmpp.core.interfaces import IPubSubStorage
+from jarn.xmpp.core.interfaces import INodeEscaper
class PubSubItem(BrowserView):
@@ -19,8 +20,10 @@ def __init__(self, context, request):
self.mt = getToolByName(self.context, 'portal_membership')
self.host = urlparse(getToolByName(self.context, 'portal_url')()).netloc
self.storage = getUtility(IPubSubStorage)
+ self.escaper = getUtility(INodeEscaper)
def fullname(self, author):
+ author = self.escaper.unescape(author)
member = self.mt.getMemberById(author)
return member.getProperty('fullname', None)
@@ -48,6 +51,8 @@ def __call__(self, item=None, isLeaf=False):
'longitude': self.request.get('geolocation[longitude]')}
if self.request.get('isLeaf') == 'false':
isLeaf = False
+ if item['author']:
+ item['author'] = self.escaper.unescape(item['author'])
self.item = item
self.isLeaf = isLeaf
@@ -80,13 +85,18 @@ class PubSubFeedMixIn(object):
def __init__(self, context):
self.storage = getUtility(IPubSubStorage)
- if self.node in self.storage.leaf_nodes:
+ self.escaper = getUtility(INodeEscaper)
+ self.escape = self.escaper.escape
+ self.unescape = self.escaper.unescape
+
+ if self.escape(self.node) in self.storage.leaf_nodes:
self.nodeType = 'leaf'
else:
self.nodeType = 'collection'
self.mt = getToolByName(self.context, 'portal_membership')
def fullname(self, author):
+ author = self.unescape(author)
member = self.mt.getMemberById(author)
if member:
return member.getProperty('fullname', None)
@@ -102,22 +112,25 @@ def postNode(self):
if self.mt.isAnonymousUser():
return
if self.node is None:
- return self.mt.getAuthenticatedMember().id
+ return self.escape(self.mt.getAuthenticatedMember().id)
user_id = self.mt.getAuthenticatedMember().id
if self.nodeType == 'leaf':
- if user_id in self.storage.publishers[self.node]:
+ if self.escape(user_id) in \
+ self.storage.publishers[self.escape(self.node)]:
return self.node
else:
publisher_nodes = [node
- for node in self.storage.collections[self.node]
- if user_id in self.storage.publishers[node]]
+ for node in self.storage.collections[self.escape(self.node)]
+ if user_id in self.storage.publishers[node]]
if len(publisher_nodes) == 1:
return publisher_nodes[0]
def items(self, node=None, start=0, count=20):
if node is None:
node = self.node
- return self.storage.itemsFromNodes([node], start=start, count=count)
+ return self.storage.itemsFromNodes([self.escape(node)],
+ start=start,
+ count=count)
class PubSubFeed(BrowserView, PubSubFeedMixIn):
View
7 jarn/xmpp/core/browser/userinfo.py
@@ -1,15 +1,20 @@
import json
+from zope.component import getUtility
+
from twisted.words.protocols.jabber.jid import JID
from AccessControl import Unauthorized
from Products.Five.browser import BrowserView
from Products.CMFCore.utils import getToolByName
+from jarn.xmpp.core.interfaces import INodeEscaper
+
class XMPPUserInfo(BrowserView):
def __call__(self, user_id):
+ user_id = getUtility(INodeEscaper).unescape(user_id)
pm = getToolByName(self.context, 'portal_membership')
if pm.isAnonymousUser():
raise Unauthorized
@@ -31,7 +36,7 @@ class XMPPUserDetails(BrowserView):
def __init__(self, context, request):
super(BrowserView, self).__init__(context, request)
self.jid = request.get('jid')
- self.user_id = JID(self.jid).user
+ self.user_id = getUtility(INodeEscaper).unescape(JID(self.jid).user)
self.bare_jid = JID(self.jid).userhost()
self.pm = getToolByName(context, 'portal_membership')
info = self.pm.getMemberInfo(self.user_id)
View
1  jarn/xmpp/core/configure.zcml
@@ -14,6 +14,7 @@
<include package="plone.app.registry" />
<include package=".subscribers" />
<include package=".browser" />
+ <include package=".utils" />
<utility factory=".settings.XMPPUsers" />
<utility factory=".storage.PubSubStorage" />
View
14 jarn/xmpp/core/interfaces.py
@@ -55,3 +55,17 @@ def __init__(self, obj):
class IXMPPLoaderVM(IViewletManager):
"""Viewlet manager for the loader viewlet.
"""
+
+class INodeEscaper(Interface):
+ """ Utility that provides basic escape mechanism for node (XEP-0106)."""
+
+ def escape(self, node):
+ """Replaces all disallowed characters according to the algorithm
+ described in XEP-0106.
+ """
+
+ def unescape(self, node):
+ """Replaces all disallowed characters that were escaped
+ with unescaped ones.
+ """
+
View
2  jarn/xmpp/core/settings.py
@@ -7,6 +7,7 @@
from jarn.xmpp.core.interfaces import IXMPPPasswordStorage
from jarn.xmpp.core.interfaces import IXMPPUsers
+from jarn.xmpp.core.interfaces import INodeEscaper
logger = logging.getLogger('jarn.xmpp.core')
@@ -18,6 +19,7 @@ class XMPPUsers(object):
def getUserJID(self, user_id):
registry = getUtility(IRegistry)
xmpp_domain = registry['jarn.xmpp.xmppDomain']
+ user_id = getUtility(INodeEscaper).escape(user_id)
return JID("%s@%s" % (user_id, xmpp_domain))
def getUserPassword(self, user_id):
View
6 jarn/xmpp/core/subscribers/user_management.py
@@ -36,10 +36,10 @@ def onUserCreation(event):
pass_storage = getUtility(IXMPPPasswordStorage)
principal_pass = pass_storage.set(principal_id)
- storage.leaf_nodes.append(principal_id)
- storage.node_items[principal_id] = []
+ storage.leaf_nodes.append(principal_jid.user)
+ storage.node_items[principal_jid.user] = []
storage.collections['people'].append(principal_id)
- storage.publishers[principal_id] = [principal_id]
+ storage.publishers[principal_jid.user] = [principal_jid.user]
d = setupPrincipal(client, principal_jid, principal_pass, members_jids)
return d
View
36 jarn/xmpp/core/tests/test_node_escaping.py
@@ -0,0 +1,36 @@
+import unittest
+from jarn.xmpp.core.utils.node import NodeEscaper
+
+class NodeEscaperTests(unittest.TestCase):
+
+ jids = [('space cadet@example.com', 'space\\20cadet@example.com'),
+ ('call me "ishmael"@example.com',
+ 'call\\20me\\20\\22ishmael\\22@example.com'),
+ ('at&t guy@example.com', 'at\\26t\\20guy@example.com'),
+ ('d\'artagnan@example.com', 'd\\27artagnan@example.com'),
+ ('/.fanboy@example.com', '\\2f.fanboy@example.com'),
+ ('::foo::@example.com', '\\3a\\3afoo\\3a\\3a@example.com'),
+ ('<foo>@example.com', '\\3cfoo\\3e@example.com'),
+ ('user@host@example.com', 'user\\40host@example.com'),
+ ('c:\\net@example.com', 'c\\3a\\net@example.com'),
+ ('c:\\net@example.com', 'c\\3a\\net@example.com'),
+ ('c:\\cool stuff@example.com', 'c\\3a\\cool\\20stuff@example.com'),
+ ('c:\\5commas@example.com', 'c\\3a\\5c5commas@example.com'),
+ ('example@example.com', 'example@example.com')]
+
+ def setUp(self):
+ self.escaper = NodeEscaper()
+
+ def test_jid_escaping(self):
+ for jid in self.jids:
+ node, host = tuple(jid[0].rsplit('@', 1))
+ self.assertEqual('%s@%s' % (self.escaper.escape(node), host), jid[1])
+
+ def test_jid_unescaping(self):
+ for jid in self.jids:
+ node, host = tuple(jid[1].rsplit('@', 1))
+ self.assertEqual('%s@%s' % (self.escaper.unescape(node), host), jid[0])
+
+
+def test_suite():
+ return unittest.defaultTestLoader.loadTestsFromName(__name__)
View
8 jarn/xmpp/core/utils/configure.zcml
@@ -0,0 +1,8 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:i18n="http://namespaces.zope.org/i18n"
+ i18n_domain="jarn.xmpp.core">
+
+ <utility factory=".node.NodeEscaper" />
+
+</configure>
View
51 jarn/xmpp/core/utils/node.py
@@ -0,0 +1,51 @@
+from zope.interface import implements
+
+from jarn.xmpp.core.interfaces import INodeEscaper
+
+
+class NodeEscaper(object):
+ """Implements basic XEP106 escape mechanism."""
+
+ implements(INodeEscaper)
+
+ XEP0106_mapping = [(' ','20'),
+ ('"','22'),
+ ('&','26'),
+ ('\'','27'),
+ ('/','2f'),
+ (':','3a'),
+ ('<','3c'),
+ ('>','3e'),
+ ('@','40')]
+
+
+ def escape(self, node):
+ """Replaces all characters disallowed by the Nodeprep profile of
+ stringprep using escape mapping.
+ """
+ if not node:
+ return
+
+ node = node.replace('\\5c', '\\5c5c')
+
+ for char, repl in self.XEP0106_mapping:
+ node = node.replace('\\%s' % repl, '\\5c%s' % repl)
+
+ for char, repl in self.XEP0106_mapping:
+ node = node.replace(char, '\\%s' % repl)
+
+ return node
+
+ def unescape(self, node):
+ """Replaces all disallowed characters that were escaped
+ with unescaped ones.
+ """
+
+ if not node:
+ return
+
+ for char, repl in self.XEP0106_mapping:
+ node = node.replace('\\%s' % repl, char)
+
+ return node.replace('\\5c', '\\')
+
Something went wrong with that request. Please try again.