From f70165ef6229a92bf8cfa6f16eff980a4a7491fe Mon Sep 17 00:00:00 2001 From: Guillaume Horel Date: Wed, 18 Jan 2012 12:27:37 -0500 Subject: Added setup.py and big reorg of the source tree this should fix tickets #10 and #11 --- README | 2 +- alias | 58 ---------------- alias_server/README | 7 ++ alias_server/__init__.py | 1 + alias_server/alias_plugin.py | 93 ++++++++++++++++++++++++++ alias_server/component.py | 54 +++++++++++++++ alias_server/config.py | 18 +++++ alias_server/object.py | 151 ++++++++++++++++++++++++++++++++++++++++++ alias_server/permission.py | 9 +++ alias_server/scripts/server | 97 +++++++++++++++++++++++++++ alias_server/user.py | 45 +++++++++++++ alias_server/version.py | 5 ++ alias_server/xep_0077.py | 154 +++++++++++++++++++++++++++++++++++++++++++ misc/alias | 58 ++++++++++++++++ misc/config.ini.sample | 9 +++ server/README | 7 -- server/__init__.py | 3 - server/alias_plugin.py | 93 -------------------------- server/component.py | 54 --------------- server/config.ini.sample | 9 --- server/config.py | 18 ----- server/object.py | 151 ------------------------------------------ server/permission.py | 9 --- server/server.py | 97 --------------------------- server/user.py | 45 ------------- server/version.py | 5 -- server/xep_0077.py | 154 ------------------------------------------- setup.py | 9 +++ 28 files changed, 711 insertions(+), 704 deletions(-) delete mode 100644 alias create mode 100644 alias_server/README create mode 100644 alias_server/__init__.py create mode 100644 alias_server/alias_plugin.py create mode 100644 alias_server/component.py create mode 100644 alias_server/config.py create mode 100644 alias_server/object.py create mode 100644 alias_server/permission.py create mode 100644 alias_server/scripts/server create mode 100644 alias_server/user.py create mode 100644 alias_server/version.py create mode 100644 alias_server/xep_0077.py create mode 100644 misc/alias create mode 100644 misc/config.ini.sample delete mode 100644 server/README delete mode 100644 server/__init__.py delete mode 100644 server/alias_plugin.py delete mode 100644 server/component.py delete mode 100644 server/config.ini.sample delete mode 100644 server/config.py delete mode 100644 server/object.py delete mode 100644 server/permission.py delete mode 100644 server/server.py delete mode 100644 server/user.py delete mode 100644 server/version.py delete mode 100644 server/xep_0077.py create mode 100644 setup.py diff --git a/README b/README index 45959fd..a80efb4 100644 --- a/README +++ b/README @@ -1 +1 @@ -See http://alias.fr.nf for more info +See http://dev.alias.im for more info diff --git a/alias b/alias deleted file mode 100644 index 60448dc..0000000 --- a/alias +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/sh - -DAEMON=/usr/bin/alias-server -PIDFILE=/var/run/alias.pid -CONFIG=/etc/conf.d/alias - -if [ ! -x $DAEMON ]; then - echo "ERROR: Can't execute $DAEMON." - exit 1 -fi - -start_service() { - echo -n " * Starting alias server... " - start-stop-daemon -Sq -p $PIDFILE -x $DAEMON -- -c $CONFIG - e=$? - if [ $e -eq 1 ]; then - echo "already running" - return - fi - - if [ $e -eq 255 ]; then - echo "couldn't start :(" - return - fi - - echo "done" -} - -stop_service() { - echo -n " * Stopping alias server... " - start-stop-daemon -Kq -R 10 -p $PIDFILE - e=$? - if [ $e -eq 1 ]; then - echo "not running" - return - fi - - echo "done" -} - -case "$1" in - start) - start_service - ;; - stop) - stop_service - ;; - restart) - stop_service - start_service - ;; - *) - echo "Usage: /etc/init.d/alias {start|stop|restart}" >&2 - exit 1 - ;; -esac - -exit 0 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 diff --git a/misc/alias b/misc/alias new file mode 100644 index 0000000..208401b --- /dev/null +++ b/misc/alias @@ -0,0 +1,58 @@ +#!/bin/sh + +DAEMON=/usr/bin/alias-server +PIDFILE=/var/run/alias.pid +CONFIG=/etc/alias/config.ini + +if [ ! -x $DAEMON ]; then + echo "ERROR: Can't execute $DAEMON." + exit 1 +fi + +start_service() { + echo -n " * Starting alias server... " + start-stop-daemon -Sq -p $PIDFILE -x $DAEMON -- -c $CONFIG + e=$? + if [ $e -eq 1 ]; then + echo "already running" + return + fi + + if [ $e -eq 255 ]; then + echo "couldn't start :(" + return + fi + + echo "done" +} + +stop_service() { + echo -n " * Stopping alias server... " + start-stop-daemon -Kq -R 10 -p $PIDFILE + e=$? + if [ $e -eq 1 ]; then + echo "not running" + return + fi + + echo "done" +} + +case "$1" in + start) + start_service + ;; + stop) + stop_service + ;; + restart) + stop_service + start_service + ;; + *) + echo "Usage: /etc/init.d/alias {start|stop|restart}" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/misc/config.ini.sample b/misc/config.ini.sample new file mode 100644 index 0000000..66dde3a --- /dev/null +++ b/misc/config.ini.sample @@ -0,0 +1,9 @@ +[component] +name = testg.alias.im +secret = Mvdujq06 +host = alias.im +port = 5349 +root = /var/lib/alias/ +background = True +logfile = /var/log/alias.log +pidfile = /var/run/alias.pid diff --git a/server/README b/server/README deleted file mode 100644 index 81ee68b..0000000 --- a/server/README +++ /dev/null @@ -1,7 +0,0 @@ -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/server/__init__.py b/server/__init__.py deleted file mode 100644 index faaaf79..0000000 --- a/server/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- - - diff --git a/server/alias_plugin.py b/server/alias_plugin.py deleted file mode 100644 index 77bcb5d..0000000 --- a/server/alias_plugin.py +++ /dev/null @@ -1,93 +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.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/server/component.py b/server/component.py deleted file mode 100644 index 2ae12d7..0000000 --- a/server/component.py +++ /dev/null @@ -1,54 +0,0 @@ -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/server/config.ini.sample b/server/config.ini.sample deleted file mode 100644 index 66dde3a..0000000 --- a/server/config.ini.sample +++ /dev/null @@ -1,9 +0,0 @@ -[component] -name = testg.alias.im -secret = Mvdujq06 -host = alias.im -port = 5349 -root = /var/lib/alias/ -background = True -logfile = /var/log/alias.log -pidfile = /var/run/alias.pid diff --git a/server/config.py b/server/config.py deleted file mode 100644 index 74befe2..0000000 --- a/server/config.py +++ /dev/null @@ -1,18 +0,0 @@ -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/server/object.py b/server/object.py deleted file mode 100644 index 8dd91a2..0000000 --- a/server/object.py +++ /dev/null @@ -1,151 +0,0 @@ -# -*- 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/server/permission.py b/server/permission.py deleted file mode 100644 index 3ce7323..0000000 --- a/server/permission.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- 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/server/server.py b/server/server.py deleted file mode 100644 index 4556fb8..0000000 --- a/server/server.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/python2 -import logging -from argparse import ArgumentParser -from config import config -import daemon -import daemon.pidfile -from 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/server/user.py b/server/user.py deleted file mode 100644 index f6800f9..0000000 --- a/server/user.py +++ /dev/null @@ -1,45 +0,0 @@ -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/server/version.py b/server/version.py deleted file mode 100644 index 306cebb..0000000 --- a/server/version.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -MAJOR=0 -MINOR=1 -TYPE='dev' -VERSION = str(MAJOR)+'.'+str(MINOR)+TYPE #TODO: add the commit number ? diff --git a/server/xep_0077.py b/server/xep_0077.py deleted file mode 100644 index 0608dc5..0000000 --- a/server/xep_0077.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -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 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..9bc77a9 --- /dev/null +++ b/setup.py @@ -0,0 +1,9 @@ +from distutils.core import setup +setup(name='alias', + version='0.1', + packages=['alias_server'], + scripts=['alias_server/scripts/server'], + data_files=[('/etc/rc.d/',['misc/alias']), + ('/etc/alias/',['misc/config.ini.sample']) + ] + ) -- cgit v1.2.3-70-g09d2