diff options
Diffstat (limited to 'src/leap')
-rw-r--r-- | src/leap/__init__.py | 9 | ||||
-rw-r--r-- | src/leap/mx/__init__.py | 0 | ||||
-rw-r--r-- | src/leap/mx/alias_resolver.py | 207 | ||||
-rw-r--r-- | src/leap/util/__init__.py | 0 | ||||
-rw-r--r-- | src/leap/util/net.py | 126 | ||||
-rw-r--r-- | src/leap/util/version.py | 69 |
6 files changed, 411 insertions, 0 deletions
diff --git a/src/leap/__init__.py b/src/leap/__init__.py new file mode 100644 index 0000000..5fefbbf --- /dev/null +++ b/src/leap/__init__.py @@ -0,0 +1,9 @@ +# -*- encoding: utf-8 -*- + +from leap import mx +from leap import util +from leap import tests + +__all__ = ['mx', 'util', 'tests'] +__author__ = util.version.authors +__version__ = util.version.getVersion() diff --git a/src/leap/mx/__init__.py b/src/leap/mx/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/leap/mx/__init__.py diff --git a/src/leap/mx/alias_resolver.py b/src/leap/mx/alias_resolver.py new file mode 100644 index 0000000..3d20ff7 --- /dev/null +++ b/src/leap/mx/alias_resolver.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +''' +alias_resolver.py +================= +Classes for resolving postfix aliases. + +@authors: Isis Agora Lovecruft +@version: 0.0.1-beta +@license: see included LICENSE file +@copyright: copyright 2013 Isis Agora Lovecruft +''' + +import os + +try: + from paisley import client +except ImportError: + print "This software requires paisley. Please see the README file" + print "for instructions on getting required dependencies." + +try: + from twisted.internet import address, defer, reactor + from twisted.mail import maildir, alias + from twisted.protocols import postfix +except ImportError: + print "This software requires paisley. Please see the README file" + print "for instructions on getting required dependencies." + +from leap.mx import net, log ## xxx implement log + + +class ConnectedCouchDB(client.CouchDB): + """ + Connect to a CouchDB instance. + + ## xxx will we need to open CouchDB documents and views? + ## yes, these are in a _design document + ## + + """ + def __init__(self, host, port, dbName=None, + username=None, password=None, *args, **kwargs): + """ + Connect to a CouchDB instance. + + @param host: A hostname string for the CouchDB server. + @param port: The port of the CouchDB server, as an integer. + @param dbName: (optional) The default database to connect to. + @param username: (optional) The username for authorization. + @param password: (optional) The password for authorization. + @returns: A :class:`twisted.internet.defer.Deferred` representing the + the client connection to the CouchDB instance. + """ + super(client.CouchDB, self).__init__(host, port, + dbName=dbName, + username=username, + password=password, + *args, **kwargs) + if dbName: + self.bindToDB(dbName) + else: + databases = self.listDB() + log.msg("Available databases: %s" % databases) + + def queryByEmailOrAlias(self, alias, dbDoc="User", + view="by_email_or_alias"): + """ + Check to see if a particular email or alias exists. + + @param alias: A string representing the email or alias to check. + @param dbDoc: The CouchDB document to open. + @param view: The view of the CouchDB document to use. + """ + assert isinstance(alias, str), "Email or alias queries must be string" + + ## Prepend a forward slash, in case we forgot it: + if not alias.startswith('/'): + alias = '/' + alias + + d = self.openDoc(dbDoc) + d.addCallbacks(self.openView, log.err, (view)) + d.addCallbacks(self.get, log.err, (alias)) + d.addCallbacks(self.parseResult, log.err) + + @d.addCallback + def show_answer(result): + log.msg("Query: %s" % alias) + log.msg("Answer: %s" % alias) + + return d + + def query(self, uri): + """ + Query a CouchDB instance that we are connected to. + """ + try: + self.checkURI(uri) ## xxx write checkURI() + ## xxx we might be able to use self._parseURI() + except SchemeNotSupported, sns: ## xxx where in paisley is this? + log.exception(sns) ## xxx need log.exception() + + d = self.get(uri) + @d.addCallback + def parse_answer(answer): + return answer + + return answer + + @defer.inlineCallbacks + def listUsersAndEmails(self, limit=1000, reverse=False): + """ + List all users and email addresses, up to the given limit. + """ + query = "/users/_design/User/_view/by_email_or_alias/?reduce=false" + answer = yield self.query(query, limit=limit, reverse=reverse) + + if answer: + parsed = yield self.parseResult(answer) + if parsed: + log.msg("%s" % parsed) + else: + log.msg("No answer from database, perhaps there are no users.") + else: + log.msg("Problem querying CouchDB instance...") + log.debug("Host: %s" % host) + log.debug("Port: %d" % port) + +class PostfixAliasResolver(postfix.PostfixTCPMapServer): + """ + Resolve postfix aliases, similarly to using "$ postmap -q <alias>". + + This class starts a simple LineReceiver server which listens for a string + specifying an alias to look up, :param:`key`, and which will be used to + query the local Postfix server. You can test it with: + + $ ./alias_resolver.py & + $ /usr/bin/postmap -q <key> tcp:localhost:4242 + + """ + def __init__(self, *args, **kwargs): + """ + Create a local LineReceiver server which listens for Postfix aliases + to resolve. + """ + super(postfix.PostfixTCPMapServer, self).__init__(*args, **kwargs) + + +class PostfixAliasResolverFactory(postfix.PostfixTCPMapDeferringDictServerFactory): + """ + A Factory for creating PostfixAliasResolver servers, which handles inputs + and outputs, and keeps an in-memory mapping of Postfix aliases in the form + of a dict. + + xxx fill me in + + """ + protocol = PostfixAliasResolver + + def __init__(self, addr='127.0.0.1', port=4242, timeout=120, data=None): + """ + Create a Factory which returns :class:`PostfixAliasResolver` servers. + + @param addr: + (optional) A string giving the IP address of the Postfix server. + Default: '127.0.0.1' + @param port: + (optional) An integer that specifies the port number of the + Postfix server. Default: 4242 + @param timeout: + (optional) An integer specifying the number of seconds to wait + until we should time out. Default: 120 + @param data: + (optional) A dict to use to initialise or update the alias + mapping. + """ + super(postfix.PostfixTCPMapDeferringDictServerFactory, + self).__init__(data=data) + self.timeout = timeout + ## xxx get config value, should be something like verbose = no + self.noisy = False + + try: + assert isinstance(port, int), "Port number must be an integer" + assert isinstance(timeout, int), "Timeout must be an integer" + except AssertionError, ae: + raise SystemExit(ae.message) + + if net.checkIPaddress(addr): + self.addr = address._IPAddress('TCP', addr, int(port)) + else: + log.debug("Using default address for Postfix: 127.0.0.1:%s" % port) + self.addr = address._IPAddress('TCP', '127.0.0.1', int(port)) + + def buildProtocol(self): + """ + Create an instance of the :class:`PostfixAliasResolver` server. + """ + proto = self.protocol() + proto.timeout = self.timeout + proto.factory = self + return proto + + +if __name__ == "__main__": + + print "To test alias_resolver.py, please use /test/test_alias_resolver.py" diff --git a/src/leap/util/__init__.py b/src/leap/util/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/leap/util/__init__.py diff --git a/src/leap/util/net.py b/src/leap/util/net.py new file mode 100644 index 0000000..a4104d0 --- /dev/null +++ b/src/leap/util/net.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +''' +net.py +------- +Utilities for networking. + +@authors: Isis Agora Lovecruft, <isis@leap.se> 0x2cdb8b35 +@license: see included LICENSE file +@copyright: 2013 Isis Agora Lovecruft +''' + +import ipaddr +import sys +import socket + +from random import randint + +from leap.mx.utils import log + + +PLATFORMS = {'LINUX': sys.platform.startswith("linux"), + 'OPENBSD': sys.platform.startswith("openbsd"), + 'FREEBSD': sys.platform.startswith("freebsd"), + 'NETBSD': sys.platform.startswith("netbsd"), + 'DARWIN': sys.platform.startswith("darwin"), + 'SOLARIS': sys.platform.startswith("sunos"), + 'WINDOWS': sys.platform.startswith("win32")} + + +class UnsupportedPlatform(Exception): + """Support for this platform is not currently available.""" + +class IfaceError(Exception): + """Could not find default network interface.""" + +class PermissionsError(SystemExit): + """This test requires admin or root privileges to run. Exiting...""" + + +def checkIPaddress(addr): + """ + Check that a given string is a valid IPv4 or IPv6 address. + + @param addr: Any string defining an IP address, i.e. '1.2.3.4' or '::1'. + @returns: True if :param:`addr` defines a valid IPAddress, else False. + """ + import ipaddr + + try: + check = ipaddr.IPAddress(addr) + except ValueError, ve: + log.warn(ve.message) + return False + else: + return True + +def getClientPlatform(platform_name=None): + for name, test in PLATFORMS.items(): + if not platform_name or platform_name.upper() == name: + if test: + return name, test + +def getPosixIfaces(): + from twisted.internet.test import _posixifaces + log.msg("Attempting to discover network interfaces...") + ifaces = _posixifaces._interfaces() + return ifaces + +def getWindowsIfaces(): + from twisted.internet.test import _win32ifaces + log.msg("Attempting to discover network interfaces...") + ifaces = _win32ifaces._interfaces() + return ifaces + +def getIfaces(platform_name=None): + client, test = getClientPlatform(platform_name) + if client: + if client == ('LINUX' or 'DARWIN') or client[-3:] == 'BSD': + return getPosixIfaces() + elif client == 'WINDOWS': + return getWindowsIfaces() + ## XXX fixme figure out how to get iface for Solaris + else: + raise UnsupportedPlatform + else: + raise UnsupportedPlatform + +def getRandomUnusedPort(addr=None): + free = False + while not free: + port = randint(1024, 65535) + s = socket.socket() + try: + s.bind((addr, port)) + free = True + except: + pass + s.close() + return port + +def getNonLoopbackIfaces(platform_name=None): + try: + ifaces = getIfaces(platform_name) + except UnsupportedPlatform, up: + log.err(up) + + if not ifaces: + log.msg("Unable to discover network interfaces...") + return None + else: + found = [{i[0]: i[2]} for i in ifaces if i[0] != 'lo'] + log.debug("Found non-loopback interfaces: %s" % found) + for iface in ifaces: + try: + interface = checkInterfaces(found) + except IfaceError, ie: + log.err(ie) + return None + else: + return interfaces + + +def getLocalAddress(): + default_iface = getDefaultIface() + return default_iface.ipaddr diff --git a/src/leap/util/version.py b/src/leap/util/version.py new file mode 100644 index 0000000..ecf8a22 --- /dev/null +++ b/src/leap/util/version.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +''' +version.py +---------- +Version information for leap_mx. + +@authors: Isis Agora Lovecruft, <isis@leap.se> 0x2cdb8b35 +@licence: see included LICENSE file +@copyright: 2013 Isis Agora Lovecruft +''' + +import os + +from twisted.python import versions + +name = 'leap_mx' +version = versions.Version(name, 0, 0, 1, None) +authors = [('Isis Agora Lovecruft', '<isis@leap.se>', '0x2cdb8b35'),] +git_url = 'https://github.com/isislovecruft/leap_mx/' +website = 'https://leap.se' + +def getVersion(): + version.authors = authors + version.git_url = git_url + version.website = website + return version + +def getRepoDir(): + here = os.getcwd() + base = here.rsplit(name, 1)[0] + repo = os.path.join(base, name) + return repo + +def __make_text__(extra_text=None): + splitter = "-" * len(version.__str__()) + header = ["\n%s\n" % version.__str__(), "%s\n" % splitter] + footer = ["Website: \t%s\n" % website, "Github: \t%s\n" % git_url, "\n"] + contacts = ["\t%s, %s %s\n" % (a[0], a[1], a[2]) for a in authors] + contacts.insert(0, "Authors: ") + + with_contacts = header + contacts + + if extra_text is not None: + if isinstance(extra_text, iter): + with_contacts.extend((e for e in extra_text)) + elif isinstance(extra_text, str): + with_contacts.append(extra_text) + else: + print "Couldn't add extra text..." + + text = with_contacts + footer + return text + +def __update_version__(): + repo = getRepoDir() + version_file = os.path.join(repo, 'VERSION') + version_text = __make_text__() + + with open(version_file, 'w+') as fh: + fh.writelines((line for line in version_text)) + fh.flush() + fh.truncate() + + +if __name__ == "__main__": + print "Generating new VERSION file..." + __update_version__() + print "Done." |