aboutsummaryrefslogtreecommitdiffstats
path: root/alias_server
diff options
context:
space:
mode:
authorGuillaume Horel <guillaume.horel@gmail.com>2012-01-18 12:27:37 -0500
committerGuillaume Horel <guillaume.horel@gmail.com>2012-01-18 12:27:37 -0500
commitf70165ef6229a92bf8cfa6f16eff980a4a7491fe (patch)
treee2954759f71c0b4c482334ba9b278a4224e97197 /alias_server
parent0ce9f4fb84693f34113630c59610b47a082ae71a (diff)
downloadalias-f70165ef6229a92bf8cfa6f16eff980a4a7491fe.tar.gz
Added setup.py and big reorg of the source tree
this should fix tickets #10 and #11
Diffstat (limited to 'alias_server')
-rw-r--r--alias_server/README7
-rw-r--r--alias_server/__init__.py1
-rw-r--r--alias_server/alias_plugin.py93
-rw-r--r--alias_server/component.py54
-rw-r--r--alias_server/config.py18
-rw-r--r--alias_server/object.py151
-rw-r--r--alias_server/permission.py9
-rw-r--r--alias_server/scripts/server97
-rw-r--r--alias_server/user.py45
-rw-r--r--alias_server/version.py5
-rw-r--r--alias_server/xep_0077.py154
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