From b38e784e1aeb0b3f3db4a8a33799c4bd94a5d10a Mon Sep 17 00:00:00 2001 From: Guillaume Horel Date: Wed, 27 Apr 2011 01:04:03 -0400 Subject: Preliminary work for in-band registration. --- server/alias_plugin.py | 101 +++++++++++++++++++++++++++++++ server/object.py | 5 +- server/plugin.py | 101 ------------------------------- server/server.py | 4 +- server/user.py | 29 ++++++--- server/xep_0077.py | 158 +++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 286 insertions(+), 112 deletions(-) create mode 100644 server/alias_plugin.py delete mode 100644 server/plugin.py create mode 100644 server/xep_0077.py diff --git a/server/alias_plugin.py b/server/alias_plugin.py new file mode 100644 index 0000000..2001b1b --- /dev/null +++ b/server/alias_plugin.py @@ -0,0 +1,101 @@ +import logging +logger = logging.getLogger(__name__) +import base64 +import hashlib +from xml.etree import cElementTree as ET + +from sleekxmpp.xmlstream.stanzabase import ElementBase, register_stanza_plugin +from sleekxmpp.plugins import base +from sleekxmpp.xmlstream.handler.callback import Callback +from sleekxmpp.xmlstream.matcher.xpath import MatchXPath +from sleekxmpp.stanza.iq import Iq + +from object import ObjectReader, ObjectError +from permission import PermissionError + +class AliasQuery(ElementBase): + namespace = 'alias:query' + name = 'query' + plugin_attrib = 'alias' + interfaces = set(('node', 'type', 'content', 'permission', 'key')) + sub_interfaces = set(('content', 'permission', 'key', 'salt')) + + def addItem(self, node, key, permission = None): + item = AliasItem(None, self) + item['node'] = node + item['key'] = key + if permission is not None: + item['permission'] = str(permission) + +class AliasItem(ElementBase): + namespace = 'alias:query' + name = 'item' + plugin_attrib = 'item' + interfaces = set(('node', 'permission', 'key')) + +class AliasPlugin(base.base_plugin): + + def plugin_init(self): + self.description = 'Plugin to handle alias queries' + register_stanza_plugin(Iq, AliasQuery) + query_parser = MatchXPath('{{{}}}iq/{{{}}}query'.format(self.xmpp.default_ns, + AliasQuery.namespace)) + self.xmpp.register_handler(Callback('Alias queries', query_parser, + self.handle_alias_query)) + + def post_init(self): + base.base_plugin.post_init(self) + self.xmpp.plugin['xep_0030'].add_feature("alias:query") + + def send_permission_error(self, iq, message): + node = iq['alias']['node'] + iq.reply() + iq['alias']['type'] = 'error' + iq['alias']['node'] = node + iq['alias']['permission'] = message + iq.send() + + def handle_alias_query(self, iq): + caller = iq['from'].bare + + if iq['alias']['type'] == 'keys': + key, salt = ObjectReader(caller).get_private_key(); + iq.reply() + iq['alias']['type'] = 'keys' + iq['alias']['key'] = key + iq['alias']['salt'] = salt + iq.send() + + try: + callee = base64.b64decode(iq['to'].user) + except TypeError: + logger.error("callee field not base64 encoded") + + node = iq['alias']['node'] + node = ObjectReader(callee, node) + + if iq['alias']['type'] == 'items': + try: + childs = node.get_child_list(caller) + except PermissionError: + self.send_permission_error(iq, 'Permission') + else: + iq.reply() + iq['alias']['type'] = 'items' + iq['alias']['node'] = node.hash + for name, perm, key in childs: + iq['alias'].addItem(name, key, perm) + iq.send() + + if iq['alias']['type'] == 'content': + try: + content, key = node.get_content(caller) + except PermissionError: + self.send_permission_error(iq, 'Permission') + else: + iq.reply() + iq['alias']['type'] = 'content' + iq['alias']['node'] = node.hash + iq['alias']['content'] = content + iq['alias']['key'] = key + iq.send() \ No newline at end of file diff --git a/server/object.py b/server/object.py index 6a5e45c..7a2e46f 100644 --- a/server/object.py +++ b/server/object.py @@ -8,6 +8,7 @@ import zlib import datetime import fileinput import logging +import pickle logger = logging.getLogger(__name__) from permission import * @@ -93,11 +94,11 @@ class ObjectReader(Object): file.close() return content, key - def get_private_key(self): + def get_registration_data(self): pass class ObjectWriter(Object): - def __init__(self, owner, hash = None): + def __init__(self, owner, hash = None, content): Object.__init__(self, owner, hash) if not os.path.exists(self.owner_path): logger.error("User {} is not registered".format(self.owner)) diff --git a/server/plugin.py b/server/plugin.py deleted file mode 100644 index 2001b1b..0000000 --- a/server/plugin.py +++ /dev/null @@ -1,101 +0,0 @@ -import logging -logger = logging.getLogger(__name__) -import base64 -import hashlib -from xml.etree import cElementTree as ET - -from sleekxmpp.xmlstream.stanzabase import ElementBase, register_stanza_plugin -from sleekxmpp.plugins import base -from sleekxmpp.xmlstream.handler.callback import Callback -from sleekxmpp.xmlstream.matcher.xpath import MatchXPath -from sleekxmpp.stanza.iq import Iq - -from object import ObjectReader, ObjectError -from permission import PermissionError - -class AliasQuery(ElementBase): - namespace = 'alias:query' - name = 'query' - plugin_attrib = 'alias' - interfaces = set(('node', 'type', 'content', 'permission', 'key')) - sub_interfaces = set(('content', 'permission', 'key', 'salt')) - - def addItem(self, node, key, permission = None): - item = AliasItem(None, self) - item['node'] = node - item['key'] = key - if permission is not None: - item['permission'] = str(permission) - -class AliasItem(ElementBase): - namespace = 'alias:query' - name = 'item' - plugin_attrib = 'item' - interfaces = set(('node', 'permission', 'key')) - -class AliasPlugin(base.base_plugin): - - def plugin_init(self): - self.description = 'Plugin to handle alias queries' - register_stanza_plugin(Iq, AliasQuery) - query_parser = MatchXPath('{{{}}}iq/{{{}}}query'.format(self.xmpp.default_ns, - AliasQuery.namespace)) - self.xmpp.register_handler(Callback('Alias queries', query_parser, - self.handle_alias_query)) - - def post_init(self): - base.base_plugin.post_init(self) - self.xmpp.plugin['xep_0030'].add_feature("alias:query") - - def send_permission_error(self, iq, message): - node = iq['alias']['node'] - iq.reply() - iq['alias']['type'] = 'error' - iq['alias']['node'] = node - iq['alias']['permission'] = message - iq.send() - - def handle_alias_query(self, iq): - caller = iq['from'].bare - - if iq['alias']['type'] == 'keys': - key, salt = ObjectReader(caller).get_private_key(); - iq.reply() - iq['alias']['type'] = 'keys' - iq['alias']['key'] = key - iq['alias']['salt'] = salt - iq.send() - - try: - callee = base64.b64decode(iq['to'].user) - except TypeError: - logger.error("callee field not base64 encoded") - - node = iq['alias']['node'] - node = ObjectReader(callee, node) - - if iq['alias']['type'] == 'items': - try: - childs = node.get_child_list(caller) - except PermissionError: - self.send_permission_error(iq, 'Permission') - else: - iq.reply() - iq['alias']['type'] = 'items' - iq['alias']['node'] = node.hash - for name, perm, key in childs: - iq['alias'].addItem(name, key, perm) - iq.send() - - if iq['alias']['type'] == 'content': - try: - content, key = node.get_content(caller) - except PermissionError: - self.send_permission_error(iq, 'Permission') - else: - iq.reply() - iq['alias']['type'] = 'content' - iq['alias']['node'] = node.hash - iq['alias']['content'] = content - iq['alias']['key'] = key - iq.send() \ No newline at end of file diff --git a/server/server.py b/server/server.py index 0404ca6..3bd953e 100644 --- a/server/server.py +++ b/server/server.py @@ -10,7 +10,9 @@ class ObjectComponent(ComponentXMPP): def __init__(self, jid, secret, server, port, root): ComponentXMPP.__init__(self, jid, secret, server, port) self.register_plugin('xep_0030') - self.register_plugin("AliasPlugin", module = "plugin", pconfig = {'root': root}) + self.register_plugin('xep_0077', pconfig = {'root': root}) + self.plugin['xep_0077'].setForm('pubkey','privkey','salt') + self.register_plugin("AliasPlugin", module = "alias_plugin", pconfig = {'root': root}) self.add_event_handler("session_start", self.start) self.add_event_handler("presence_probe", self.presence_probe) self.add_event_handler("message", self.message) diff --git a/server/user.py b/server/user.py index 2491164..3fbf55c 100644 --- a/server/user.py +++ b/server/user.py @@ -11,22 +11,35 @@ class UserHandler: def __init__(self, root): self.root = root - def register(self, name): - userDir = self.root + '/' + name + def register(self, jid, registration): + userDir = self.root + '/' + jid if os.path.exists(userDir): - logger.error("User {} path already exists".format(name)) + logger.error("User {} path already exists".format(jid)) os.mkdir(userDir) - ObjectWriter(name) + ObjectWriter(jid) + return True - def registered(self, name): - return os.path.exists(self.root + '/' + name) + def registered(self, jid): + return os.path.exists(self.root + '/' + jid) - def unregister(self, name): - shutil.rmtree(self.root + '/' + name) + def unregister(self, jid): + shutil.rmtree(self.root + '/' + jid) def get_user_list(self): return os.listdir(self.root) +def register(self, jid, registration): + username = registration['username'] + + def filter_usernames(user): + return user != jid and self.users[user]['username'] == username + + conflicts = filter(filter_usernames, self.users.keys()) + if conflicts: + return False + + self.users[jid] = registration + return True class User: def __init__(self, name): diff --git a/server/xep_0077.py b/server/xep_0077.py new file mode 100644 index 0000000..b07105b --- /dev/null +++ b/server/xep_0077.py @@ -0,0 +1,158 @@ +""" +Creating a SleekXMPP Plugin + +This is a minimal implementation of XEP-0077 to serve +as a tutorial for creating SleekXMPP plugins. +""" + +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.xmlstream.handler.callback import Callback +from sleekxmpp.xmlstream.matcher.xpath import MatchXPath +from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin +from sleekxmpp import Iq +from user import UserHandler +from config import config + +class Registration(ElementBase): + namespace = 'jabber:iq:register' + name = 'query' + plugin_attrib = 'register' + interfaces = set(('pubkey', 'privkey', 'salt' + 'registered', 'remove', 'instructions')) + sub_interfaces = interfaces + + def getRegistered(self): + present = self.xml.find('{%s}registered' % self.namespace) + return present is not None + + def getRemove(self): + present = self.xml.find('{%s}remove' % self.namespace) + return present is not None + + def setRegistered(self, registered): + if registered: + self.addField('registered') + else: + del self['registered'] + + def setRemove(self, remove): + if remove: + self.addField('remove') + else: + del self['remove'] + + def addField(self, name): + itemXML = ET.Element('{%s}%s' % (self.namespace, name)) + self.xml.append(itemXML) + + +class UserStore(object): + def __init__(self): + self.users = {} + + def __getitem__(self, jid): + return self.users.get(jid, None) + + def register(self, jid, registration): + username = registration['username'] + + def filter_usernames(user): + return user != jid and self.users[user]['username'] == username + + conflicts = filter(filter_usernames, self.users.keys()) + if conflicts: + return False + + self.users[jid] = registration + return True + + def unregister(self, jid): + del self.users[jid] + +class xep_0077(base_plugin): + """ + XEP-0077 In-Band Registration + """ + + def plugin_init(self): + self.description = "In-Band Registration" + self.xep = "0077" + self.form_fields = () + self.form_instructions = "" + self.backend = UserHandler(config.root) + + self.xmpp.registerHandler( + Callback('In-Band Registration', + MatchXPath('{%s}iq/{jabber:iq:register}query' % self.xmpp.default_ns), + self.__handleRegistration)) + register_stanza_plugin(Iq, Registration) + + def post_init(self): + base_plugin.post_init(self) + self.xmpp['xep_0030'].add_feature("jabber:iq:register") + + def __handleRegistration(self, iq): + if iq['type'] == 'get': + # Registration form requested + self.sendRegistrationForm(iq, self.backend) + elif iq['type'] == 'set': + if iq['register']['remove']: + # Remove an account + self.backend.unregister(iq['from'].bare) + #self.xmpp.event('unregistered_user', iq) + iq.reply().send() + return + + for field in self.form_fields: + if not iq['register'][field]: + # Incomplete Registration + self._sendError(iq, '406', 'modify', 'not-acceptable', + "Please fill in all fields.") + return + + if self.backend.register(iq['from'].bare, iq['register']): + # Successful registration + #self.xmpp.event('registered_user', iq) + iq.reply().setPayload(iq['register'].xml) + iq.send() + else: + # Conflicting registration + self._sendError(iq, '409', 'cancel', 'conflict', + "That username is already taken.") + + def setForm(self, *fields): + self.form_fields = fields + + def setInstructions(self, instructions): + self.form_instructions = instructions + + def sendRegistrationForm(self, iq, userHandler): + reg = iq['register'] + if user + userData = {} + else: + reg['registered'] = True + + if self.form_instructions: + reg['instructions'] = self.form_instructions + + for field in self.form_fields: + data = userData.get(field, '') + if data: + # Add field with existing data + reg[field] = data + else: + # Add a blank field + reg.addField(field) + + iq.reply().setPayload(reg.xml) + iq.send() + + def _sendError(self, iq, code, error_type, name, text=''): + iq.reply().setPayload(iq['register'].xml) + iq.error() + iq['error']['code'] = code + iq['error']['type'] = error_type + iq['error']['condition'] = name + iq['error']['text'] = text + iq.send() \ No newline at end of file -- cgit v1.2.3-70-g09d2