diff options
Diffstat (limited to 'alias_server')
| -rw-r--r-- | alias_server/README | 7 | ||||
| -rw-r--r-- | alias_server/__init__.py | 1 | ||||
| -rw-r--r-- | alias_server/alias_plugin.py | 93 | ||||
| -rw-r--r-- | alias_server/component.py | 54 | ||||
| -rw-r--r-- | alias_server/config.py | 18 | ||||
| -rw-r--r-- | alias_server/object.py | 151 | ||||
| -rw-r--r-- | alias_server/permission.py | 9 | ||||
| -rw-r--r-- | alias_server/scripts/server | 97 | ||||
| -rw-r--r-- | alias_server/user.py | 45 | ||||
| -rw-r--r-- | alias_server/version.py | 5 | ||||
| -rw-r--r-- | alias_server/xep_0077.py | 154 |
11 files changed, 634 insertions, 0 deletions
diff --git a/alias_server/README b/alias_server/README new file mode 100644 index 0000000..81ee68b --- /dev/null +++ b/alias_server/README @@ -0,0 +1,7 @@ +This is Alias server written in python. It depends on SleekXMPP library : + + git clone git://github.com/fritzy/SleekXMPP.git + cd SleekXMPP/ + sudo python setup.py install + +Make sure sleekxmpp is in your pythonpath before starting the server. diff --git a/alias_server/__init__.py b/alias_server/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/alias_server/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/alias_server/alias_plugin.py b/alias_server/alias_plugin.py new file mode 100644 index 0000000..77bcb5d --- /dev/null +++ b/alias_server/alias_plugin.py @@ -0,0 +1,93 @@ +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.base import base_plugin +from sleekxmpp.xmlstream.handler.callback import Callback +from sleekxmpp.xmlstream.matcher.xpath import MatchXPath +from sleekxmpp import Iq + +from object import ObjectReader, ObjectError +from permission import PermissionError +from config import config + +class AliasQuery(ElementBase): + namespace = 'alias:iq:object' + name = 'query' + plugin_attrib = 'alias' + interfaces = set(('node', 'type', 'content', 'permission', 'key')) + sub_interfaces = set(('content', 'permission', 'key')) + + 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_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_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 + + try: + callee = base64.b64decode(iq['to'].user) + except TypeError: + logger.error("callee field not base64 encoded") + + node = iq['alias']['node'] + node = ObjectReader(node, callee) + if iq['alias']['type'] == 'get': + try: + content, key = node.get_content(caller) + except PermissionError: + self.send_permission_error(iq, 'Permission') + else: + iq.reply() + iq['alias']['type'] = 'get' + iq['alias']['node'] = node.hash + iq['alias']['content'] = content + iq['alias']['key'] = key + iq.send() + + if iq['alias']['type'] == 'list': + try: + list = node.get_children_list(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/alias_server/component.py b/alias_server/component.py new file mode 100644 index 0000000..2ae12d7 --- /dev/null +++ b/alias_server/component.py @@ -0,0 +1,54 @@ +import sys +import logging +logger = logging.getLogger(__name__) +from sleekxmpp.componentxmpp import ComponentXMPP +from sleekxmpp.xmlstream.xmlstream import XMLStream + +from user import UserHandler + +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('xep_0077', module="xep_0077") + 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) + self.add_event_handler("changed_subscription", self.presence_subscription) + self.userHandler = UserHandler(root) + + def start(self, event): + for user in self.userHandler.get_user_list(): + self.send_presence(pto = user) + + def disconnect(self, reconnect = False): + for user in self.userHandler.get_user_list(): + self.send_presence(pto = user, ptype = "unavailable") + XMLStream.disconnect(self, reconnect) + logger.info('Component {} disconnected'.format(self.boundjid.bare)) + + def message(self, msg): + msg.reply("Thanks for sending\n{[body]}".format(msg)).send() + + def presence_subscription(self, subscription): + if subscription["type"] == "subscribe": + userJID = subscription["from"].full + if not self.userHandler.registered(userJID): + self.userHandler.register(userJID) + logger.info('registering user {}'.format(userJID)) + subscription.reply().send() + self.send_presence(pto = userJID) + self.send_presence_subscription(pto = userJID, ptype = "subscribe") + if subscription["type"] == "unsubscribe": + userJID = subscription["from"].full + if self.userHandler.registered(userJID): + self.userHandler.unregister(userJID) + logger.info('unregistering user {}'.format(userJID)) + + #def presence_probe(self, event): + # self.send_presence(pto = event["from"].full) + + + diff --git a/alias_server/config.py b/alias_server/config.py new file mode 100644 index 0000000..74befe2 --- /dev/null +++ b/alias_server/config.py @@ -0,0 +1,18 @@ +import ConfigParser + +class AliasConfigParser(ConfigParser.SafeConfigParser): + + def read(self, filename): + ConfigParser.SafeConfigParser.read(self, filename) + self.name = self.get("component", "name") + self.root = self.get("component", "root") + self.host = self.get("component", "host") + self.secret = self.get("component", "secret") + self.port = self.getint("component", "port") + self.background = self.getboolean("component", "background") + if self.has_option("component", "logfile"): + self.logfile = self.get("component", "logfile") + if self.has_option("component", "pidfile"): + self.pidfile = self.get("component", "pidfile") + +config = AliasConfigParser() diff --git a/alias_server/object.py b/alias_server/object.py new file mode 100644 index 0000000..8dd91a2 --- /dev/null +++ b/alias_server/object.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +#import StringIO +import hashlib +#import sys +#import os +import os.path +#import fileinput +import logging +logger = logging.getLogger(__name__) + +from permission import * +from config import config + +class ObjectError(Exception): + pass; + +class Object: + def __init__(self, name, owner, split_name = True): + self.hash = name + self.owner = owner + if split_name: + self.object_path = os.path.join(config.root, owner, name[:2], name[2:]) + else: + self.object_path = os.path.join(config.root, owner, name) + + def exists(self): + return os.path.exists(self.object_path) + +class ObjectReader(Object): + def __init__(self, hash, owner, split_name = True): + Object.__init__(self, hash, owner, split_name) + if not self.exists(): + logger.error("Object {} can't be found for user {}".format(self.hash, + self.owner)) + raise ObjectError + + def get_permission(self, user): + with open(os.path.join(self.object_path, 'permissions'), 'r') as file: + for line in file: + name, perm, key = line.split() + if name == user: + return int(perm) + return None + + def get_key(self, user): + with open(os.path.join(self.object_path, 'permissions'), 'r') as file: + for line in file: + name, perm, key = line.split() + if name == user: + return key + return None + + def get_permission_key(self, user): + with open(os.path.join(self.object_path, 'permissions'), 'r') as file: + for line in file: + name, perm, key = line.split() + if name == user: + return (int(perm), key) + return (None, None) + + def get_children_list(self, user): + perm = self.get_permission(user) + if not perm or (not perm & LIST): + logger.error("User {} doesn't have the list permission for object {}" + .format(user, self.hash)) + raise PermissionError + + file = open(os.path.join(self.object_path, 'childs'), 'r') + result = [] + for line in file: + name = line.rstrip('\n') + try: + child = ObjectReader(name, self.owner) + except ObjectError: + logger.error('Object {} doesn\'t exist'.format(name)) + else: + perm, key = child.get_permission_key(user) + if perm > 0: + result.append((name, perm, key)) + + file.close() + return result + + def get_content(self, user): + """Return object content and the user key to decrypt it.""" + perm, key = self.get_permission_key(user) + if not perm or (not perm & READ) : + logger.error("User {} doesn't have read access to object {}" + .format(user, self.hash)) + raise PermissionError + with open(os.path.join(self.object_path, 'object'), 'r') as file: + content = file.read() + + return content, key + +class ObjectWriter(ObjectReader): + + def __init__(self, hash, owner, split_name = True, key = None): + Object.__init__(self, hash, owner, split_name) + self.files = ('permissions', 'children', 'object') + self.__create_skeleton(key) + + def __create_skeleton(self, key): + #new object + if not self.exists(): + os.makedirs(self.object_path) + for filename in self.files: + file = open(os.path.join(self.object_path, filename), "w") + file.close() + #give all the permissions to the owner + ALLPERM = READ + MODIFY + APPEND + LIST + self.add_user(self.owner, ALLPERM, key) + + def write(self, user, content): + perm = self.get_permission(user) + if not perm or (not perm & MODIFY): + logger.error("User {} doesn't have the modify permission for object {}" + .format(user, self.hash)) + raise PermissionError + with open(os.path.join(self.object_path, 'object'), "w") as file: + file.write('{}'.format(content)) + + def append(self, user, content, parent): + parent_object = ObjectReader(parent, self.owner) + perm = parent_object.get_permission(user) + if not perm or (not perm & APPEND): + logger.error("User {} doesn't have the modify permission for object {}" + .format(user, parent)) + raise PermissionError + with open(os.path.join(self.object_path, 'object'), "w") as file: + for k, v in content: + file.write('{} {}\n'.format(k,v)) + #add the child hash to the parent + with open(os.path.join(parent_object.object_path, 'children'), "a") as file: + file.write('{} {}\n'.format(self.hash)) + + def add_user(self, user, perm, key = None): + with open(os.path.join(self.object_path, 'permissions'), "a") as file: + if key: + file.write('{} {} {}\n'.format(user, perm, key)) + else: + file.write('{} {} None\n'.format(user, perm, key)) + +if __name__ == '__main__': + jid = 'thrasibule@alias.im' + hash = hashlib.sha1(jid).hexdigest() + config.root = '/var/lib/alias' + print ObjectReader(hash, jid).get_content(jid) + + + diff --git a/alias_server/permission.py b/alias_server/permission.py new file mode 100644 index 0000000..3ce7323 --- /dev/null +++ b/alias_server/permission.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +READ = 1 << 0 +MODIFY = 1 << 1 +APPEND = 1 << 2 +LIST = 1 << 3 + +class PermissionError(Exception): + pass
\ No newline at end of file diff --git a/alias_server/scripts/server b/alias_server/scripts/server new file mode 100644 index 0000000..554388f --- /dev/null +++ b/alias_server/scripts/server @@ -0,0 +1,97 @@ +#!/usr/bin/python2 +import logging +from argparse import ArgumentParser +from alias_server.config import config +import daemon +import daemon.pidfile +from alias_server.component import ObjectComponent +import os.path +import sys + +if sys.version_info < (3, 0): + reload(sys) + sys.setdefaultencoding('utf8') +else: + raw_input = input + +if __name__ == '__main__': + commandline = ArgumentParser(description = 'Connect the alias \ + component to a given server') + commandline.add_argument('-p', '--port', + help = 'Port to connect to', + type = int) + commandline.add_argument('-s', '--secret', + help = 'password') + commandline.add_argument('-n', '--name', + help = 'Name the component will have') + commandline.add_argument('-r', '--root', + help = 'Root directory of the user files') + commandline.add_argument('-c', '--config', + help = 'Name of the config file to use') + commandline.add_argument('-d', '--debug', + help = 'Set log level to DEBUG', + action = 'store_const', + const = logging.DEBUG, + default = logging.INFO) + commandline.add_argument('-o', '--host', + help = 'Host to connect to') + commandline.add_argument('-b', '--background', + help = 'run the server in the background', + action = 'store_true') + commandline.add_argument('--logfile', + help = 'location of the log file (default /var/log/${name}.log') + commandline.add_argument('--pidfile', + help = 'location of the pid file (default /var/run/${name}.pid') + args = commandline.parse_args() + + if args.config is None: + config.name = args.name + config.port = args.port + config.secret = args.secret + config.root = args.root + config.host = args.host + config.background = args.background + config.logfile = args.logfile + config.pidfile = args.pidfile + else: + filename = args.config + config.read(filename) + if config.logfile is None: + config.logfile = os.path.join('/var/log/', config.name + '.log') + if config.pidfile is None: + config.pidfile = os.path.join('/var/run/', config.name + '.pid') + + #set up the root logger + logging.getLogger('').setLevel(args.debug) + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + if config.background: + #save logs in a file + fh = logging.FileHandler(config.logfile) + fh.setFormatter(formatter) + logging.getLogger('').addHandler(fh) + else: + #save logs to the console + ch = logging.StreamHandler() + ch.setFormatter(formatter) + logging.getLogger('').addHandler(ch) + + if config.background: + context = daemon.DaemonContext(detach_process = True, + pidfile = daemon.pidfile.TimeoutPIDLockFile(config.pidfile,10), + files_preserve=[fh.stream.fileno()], + working_directory=os.path.abspath(config.root), + stdout=sys.stdout, stderr=sys.stderr) + else: + context = daemon.DaemonContext(detach_process = False, + stdout=sys.stdout, stderr=sys.stderr, + working_directory=os.path.abspath(config.root)) + + with context: + component = ObjectComponent(config.name, config.secret, + config.host, config.port, + config.root) + if component.connect(): + logging.info('Component {} connected'.format(component.boundjid)) + component.process(block=False) + else: + logging.error("Component {} couldn't connect".format(component.boundjid)) diff --git a/alias_server/user.py b/alias_server/user.py new file mode 100644 index 0000000..f6800f9 --- /dev/null +++ b/alias_server/user.py @@ -0,0 +1,45 @@ +import os +import os.path +import shutil +import hashlib +import logging +logger = logging.getLogger(__name__) +from object import * +import base64 + +class User: + + def __init__(self, jid): + self.jid = jid + self.hash = hashlib.sha256(jid).hexdigest() + #self.hash = hashlib.md5(jid).hexdigest() + + def register(self, registration): + ObjectWriter('pubkey', self.jid, split_name = False).write(self.jid, registration['pubkey']) + #everybody can read the pubkey + ObjectWriter('pubkey', self.jid, split_name = False).add_user('*', READ) + ObjectWriter('privkey', self.jid, split_name = False).write(self.jid, registration['privkey']) + ObjectWriter(self.hash, self.jid) + + def get_registration(self): + registration = {} + registration['pubkey'], ignore = ObjectReader('pubkey',self.jid, split_name = False).get_content(self.jid) + registration['privkey'], ignore = ObjectReader('privkey',self.jid, split_name = False).get_content(self.jid) + return registration + + def is_registered(self): + return Object(self.hash, self.jid).exists() + + def unregister(self, jid): + pass + +class UserHandler: + + def __init__(self, root): + self.root = root + + def get_user_list(self): + return os.listdir(self.root) + +if __name__ == '__main__': + print UserHandler('/var/lib/alias').get_user_list()
\ No newline at end of file diff --git a/alias_server/version.py b/alias_server/version.py new file mode 100644 index 0000000..306cebb --- /dev/null +++ b/alias_server/version.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +MAJOR=0 +MINOR=1 +TYPE='dev' +VERSION = str(MAJOR)+'.'+str(MINOR)+TYPE #TODO: add the commit number ? diff --git a/alias_server/xep_0077.py b/alias_server/xep_0077.py new file mode 100644 index 0000000..0608dc5 --- /dev/null +++ b/alias_server/xep_0077.py @@ -0,0 +1,154 @@ +""" +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 User +from config import config +from sleekxmpp.plugins.xep_0004 import Form + +import logging +logger = logging.getLogger(__name__) + +class Registration(ElementBase): + namespace = 'jabber:iq:register' + name = 'query' + plugin_attrib = 'register' + interfaces = set(('registered', 'remove', 'instructions', 'form')) + sub_interfaces = interfaces + subitem = (Form,) + + def get_registered(self): + present = self.xml.find('{%s}registered' % self.namespace) + return present is not None + + def get_remove(self): + present = self.xml.find('{%s}remove' % self.namespace) + return present is not None + + def set_registered(self, registered): + if registered: + self.add_field('registered') + else: + del self['registered'] + + def set_remove(self, remove): + if remove: + self.addField('remove') + else: + del self['remove'] + + def add_field(self, name): + itemXML = ET.Element('{%s}%s' % (self.namespace, name)) + self.xml.append(itemXML) + + def add_form(self): + aliasform = Form(None, self) + aliasform.addField(ftype = "hidden", var = "FORM_TYPE", value = "alias:register") + aliasform.addField(var = "pubkey", ftype = "text-single", label = "Public Key", required = True) + aliasform.addField(var = "privkey", ftype = "text-single", label = "Private Key", required = True) + + def get_form(self): + return Form(self.xml.find('{jabber:x:data}x')).getValues() + + def set_form(self, values): + Form(self.xml.find('{jabber:x:data}x')).setValues(values) + +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 = ("privkey", "pubkey") + self.form_instructions = "Please provide the following information to register\ + an alias account" + + self.xmpp.register_handler( + Callback('In-Band Registration', + MatchXPath('{%s}iq/{jabber:iq:register}query' % self.xmpp.default_ns), + self.__handle_registration)) + register_stanza_plugin(Iq, Registration) + + def post_init(self): + base_plugin.post_init(self) + self.xmpp['xep_0030'].add_feature("jabber:iq:register") + + def __handle_registration(self, iq): + registrant = User(iq['from'].bare) + logger.info('User {} sent registration iq'.format(iq['from'].bare)) + if iq['type'] == 'get': + # Registration form requested + self.send_registration_form(iq, registrant) + elif iq['type'] == 'set': + if iq['register']['remove']: + # Remove an account + registrant.unregister() + self.xmpp.event('unregistered_user', iq) + iq.reply().send() + return + + registration_info = iq['register']['form'] + for field in self.form_fields: + if not registration_info[field]: + # Incomplete Registration + self._send_error(iq, '406', 'modify', 'not-acceptable', + "Please fill in all fields.") + return + + try: + registrant.register(registration_info) + # Successful registration + #self.xmpp.event('registered_user', iq) + iq.reply().setPayload(iq['register'].xml) + iq.send() + except: + return + else: + # Conflicting registration + self._send_error(iq, '409', 'cancel', 'conflict', + "That username is already taken.") + + def setForm(self, *fields): + self.form_fields = fields + + def set_instructions(self, instructions): + self.form_instructions = instructions + + def send_registration_form(self, iq, registrant): + reg = iq['register'] + reg.add_form() + if self.form_instructions: + reg['instructions'] = self.form_instructions + if registrant.is_registered(): + reg['registered'] = True + reg['form'] = registrant.get_registration() + + iq.reply().setPayload(reg.xml) + iq.send() + + def _send_error(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() + +if __name__ == '__main__': + test = Registration() + test.add_form() + print '{}\n'.format(test['form']) + values = {'privkey': 'pomme', 'pubkey': 'poire', 'salt': 'abricot'} + test['form']=values + print test |
