summaryrefslogtreecommitdiff
path: root/src/leap/mx/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/mx/util')
-rw-r--r--src/leap/mx/util/__init__.py12
-rw-r--r--src/leap/mx/util/config.py221
-rw-r--r--src/leap/mx/util/log.py143
-rw-r--r--src/leap/mx/util/net.py126
-rw-r--r--src/leap/mx/util/storage.py42
-rw-r--r--src/leap/mx/util/version.py95
6 files changed, 639 insertions, 0 deletions
diff --git a/src/leap/mx/util/__init__.py b/src/leap/mx/util/__init__.py
new file mode 100644
index 0000000..c4a93b8
--- /dev/null
+++ b/src/leap/mx/util/__init__.py
@@ -0,0 +1,12 @@
+#-*- encoding: utf-8 -*-
+"""
+leap/mx/util/__init__.py
+------------------------
+Module intialization file for leap.mx.util.
+"""
+
+import version
+version = version.Version()
+
+__all__ = ['config', 'log', 'net', 'storage', 'version']
+
diff --git a/src/leap/mx/util/config.py b/src/leap/mx/util/config.py
new file mode 100644
index 0000000..f655ca9
--- /dev/null
+++ b/src/leap/mx/util/config.py
@@ -0,0 +1,221 @@
+#! -*- encoding: utf-8 -*-
+"""
+Config file utilities.
+
+This module has an :attr:`config_filename`, which can be used to set the
+filename outside of function calls:
+
+ >>> from leap.mx.util import config
+ >>> config.config_filename = "blahblah.yaml"
+
+If not set anywhere, it will default to using the top level repository
+directory, i.e. "/.../leap_mx/leap_mx.conf", and will create that file with
+the default settings if it does not exist.
+
+The config file can be loaded/created with :func:`config.loadConfig`:
+
+ >>> config.loadConfig()
+
+Once the config file is loaded, this module presents a highly object-oriented
+interface, so that sections taken from the config file become attribute of
+this module, and the name of their respective settings become attributes of
+the section names. Like this:
+
+ >>> print config.basic.postfix_port
+ 465
+
+@authors: Isis Lovecruft, <isis@leap.se> 0x2cdb8b35
+@version: 0.0.1
+@license: see included LICENSE file
+"""
+
+from os import path as ospath
+
+import sys
+import yaml
+
+from leap.mx.util import version, storage
+from leap.mx.exceptions import MissingConfig, UnsupportedOS
+
+
+filename = None
+config_version = None
+basic = storage.Storage()
+couch = storage.Storage()
+advanced = storage.Storage()
+
+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")}
+
+def getClientPlatform(platform_name=None):
+ """
+ Determine the client's operating system platform. Optionally, if
+ :param:`platform_name` is given, check that this is indeed the platform
+ we're operating on.
+
+ @param platform_name: A string, upper-, lower-, or mixed case, of one
+ of the keys in the :attr:`leap.util.version.PLATFORMS`
+ dictionary. E.g. 'Linux' or 'OPENBSD', etc.
+ @returns: A string specifying the platform name, and the boolean test
+ used to determine it.
+ """
+ for name, test in PLATFORMS.items():
+ if not platform_name or platform_name.upper() == name:
+ if test:
+ return name, test
+
+def _create_config_file(conffile):
+ """
+ Create the config file if it doesn't exist.
+
+ @param conffile: The full path to the config file to write to.
+ """
+ with open(conffile, 'w+') as conf:
+ conf.write("""
+#
+# mx.conf
+# =======
+# Configurable options for the leap_mx encrypting mail exchange.
+#
+# This file follows YAML markup format: http://yaml.org/spec/1.2/spec.html
+# Keep in mind that indentation matters.
+#
+
+basic:
+ # Whether or not to log to file:
+ enable_logfile: True
+ # The name of the logfile:
+ logfile: mx.log
+ # Where is the spoolfile of messages to encrypt?:
+ spoolfile: /var/mail/encrypt_me
+couch:
+ # The couch username for authentication to a CouchDB instance:
+ user: admin
+ # The couch username's password:
+ passwd: passwd
+ # The CouchDB hostname or IP address to connect to:
+ host: couchdb.example.com
+ # The CouchDB port to connect to:
+ port: 7001
+advanced:
+ # Which port on localhost should postfix send check_recipient queries to?:
+ check_recipient_access_port: 1347
+ # Which port on localhost should postfix ask for UUIDs?:
+ virtual_alias_map_port: 1348
+ # Enable debugging output in the logger:
+ debug: True
+ # Print enough things really fast to make you look super 1337:
+ noisy: False
+config_version: 0.0.2
+
+""")
+ conf.flush()
+ assert ospath.isfile(conffile), "Config file %s not created!" % conffile
+
+def _get_config_location(config_filename=None,
+ use_dot_config_directory=False):
+ """
+ Get the full path and filename of the config file.
+ """
+ platform = getClientPlatform()[0]
+
+ ## If not given, default to the application's name + '.conf'
+ if not config_filename:
+ if not filename:
+ config_filename = "mx.conf"
+ else:
+ config_filename = filename
+
+ ## Oh hell, it could be said only to beguile:
+ ## That windoze users are capable of editing a .conf file.
+ ## Also, what maddened wingnut would be so fool
+ ## To run a mail exchange on a windoze nodule?
+ ## I'm ignoring these loons for now. And pardon if I seem jaded,
+ ## But srsly, this and that solaris sh*t should be deprecated.
+ if not platform.endswith('LINUX') and not platform.endswith('BSD'):
+ raise UnsupportedOS("Sorry, your operating system isn't supported.")
+
+ where = None
+ if use_dot_config_directory:
+ ## xxx only install/import this in *nix
+ from xdg import BaseDirectory
+
+ dot_config_dirs = BaseDirectory.xdg_config_dirs
+ for dir in dot_config_dirs:
+ our_dir = ospath.join(dir, package_name)
+ if ospath.isdir(our_dir):
+ if config_filename in os.listdir(our_dir):
+ where = ospath.abspath(our_dir)
+ ## Use repo dir instead:
+ if not where:
+ where = version.getRepoDir()
+
+ conffile = ospath.join(where, config_filename)
+ try:
+ with open(conffile) as cf: pass
+ except IOError:
+ _create_config_file(conffile)
+ finally:
+ return conffile
+
+def loadConfig(file=None):
+ """
+ Some of this is taken from OONI config code for now, and so this should be
+ refacotored, along with the leap_client config code, so that we have
+ similarly structured config files. It is perhaps desirable to also use
+ soledad as a backend for remote setup and maintainance, and thus this code
+ will need to hook into u1db (and potentially "pysqlcipher").
+
+ Excuse the yaml for now, I just wanted something that works.
+
+ @param file: (optional) If provided, use this filename.
+ """
+ if not file:
+ file = _get_config_location()
+
+ if ospath.isfile(file):
+ with open(file, 'a+') as conf:
+ config_contents = '\n'.join(conf.readlines())
+ cfg = yaml.safe_load(config_contents)
+
+ ## These become objects with their keys loaded as attributes:
+ ##
+ ## from leap.util import config
+ ## config.basic.foo = bar
+ ##
+ try:
+ for k, v in cfg['basic'].items():
+ basic[k] = v
+ except (AttributeError, KeyError): pass
+
+ try:
+ for k, v in cfg['advanced'].items():
+ advanced[k] = v
+ except (AttributeError, KeyError): pass
+
+ try:
+ for k, v in cfg['couch'].items():
+ couch[k] = v
+ except (AttributeError, KeyError): pass
+
+ if 'config_version' in cfg:
+ config_version = cfg['config_version']
+ else:
+ config_version = 'unknown'
+
+ return basic, couch, advanced, config_version
+ else:
+ raise MissingConfig("Could not load config file.")
+
+
+## This is the name of the config file to use:
+## If not set, it defaults to 'leap_mx/leap_mx.conf'
+if not filename:
+ filename = _get_config_location()
+else:
+ filename = _get_config_location(config_filename=filename)
diff --git a/src/leap/mx/util/log.py b/src/leap/mx/util/log.py
new file mode 100644
index 0000000..f31684d
--- /dev/null
+++ b/src/leap/mx/util/log.py
@@ -0,0 +1,143 @@
+# -*- encoding: utf-8 -*-
+'''
+log.py
+------
+Logging for leap_mx.
+
+@authors: Isis Agora Lovecruft, <isis@leap.se> 0x2cdb8b35
+@licence: see included LICENSE file
+@copyright: 2013 Isis Agora Lovecruft
+'''
+
+from datetime import datetime
+from functools import wraps
+
+import logging
+import os
+import sys
+import time
+import traceback
+
+from twisted.python import log as txlog
+from twisted.python import util as txutil
+from twisted.python import logfile as txlogfile
+from twisted.python.failure import Failure
+
+from leap.mx.util import version, config
+
+
+class InvalidTimestampFormat(Exception):
+ pass
+
+class UnprefixedLogfile(txlog.FileLogObserver):
+ """Logfile with plain messages, without timestamp prefixes."""
+ def emit(self, eventDict):
+ text = txlog.textFromEventDict(eventDict)
+ if text is None:
+ return
+
+ txutil.untilConcludes(self.write, "%s\n" % text)
+ txutil.untilConcludes(self.flush)
+
+
+def utcDateNow():
+ """The current date for UTC time."""
+ return datetime.utcnow()
+
+def utcTimeNow():
+ """Seconds since epoch in UTC time, as type float."""
+ return time.mktime(time.gmtime())
+
+def dateToTime(date):
+ """Convert datetime to seconds since epoch."""
+ return time.mktime(date.timetuple())
+
+def prettyDateNow():
+ """Pretty string for the local time."""
+ return datetime.now().ctime()
+
+def utcPrettyDateNow():
+ """Pretty string for UTC."""
+ return datetime.utcnow().ctime()
+
+def timeToPrettyDate(time_val):
+ """Convert seconds since epoch to date."""
+ return time.ctime(time_val)
+
+def start(logfilename=None, logfiledir=None):
+ """
+ Start logging to stdout, and optionally to a logfile as well.
+
+ @param logfile: The full path of the filename to store logs in.
+ """
+ txlog.startLoggingWithObserver(UnprefixedLogfile(sys.stdout).emit)
+
+ if logfilename and logfiledir:
+ if not os.path.isdir(logfiledir):
+ os.makedirs(logfiledir)
+ daily_logfile = txlogfile.DailyLogFile(logfilename, logfiledir)
+ txlog.addObserver(txlog.FileLogObserver(daily_logfile).emit)
+
+ txlog.msg("Starting %s, version %s, on %s UTC" % (version.getPackageName(),
+ version.getVersion(),
+ utcPrettyDateNow()))
+ txlog.msg("Authors: %s" % version.getAuthors())
+
+def msg(msg, *arg, **kwarg):
+ """Log a message at the INFO level."""
+ print "[*] %s" % msg
+
+def debug(msg, *arg, **kwarg):
+ """Log a message at the DEBUG level."""
+ if config.advanced.debug:
+ print "[d] %s" % msg
+
+def warn(msg, *arg, **kwarg):
+ """Log a message at the WARN level."""
+ if config.basic.show_warnings:
+ txlog.logging.captureWarnings('true')
+ print "[#] %s" % msg
+
+def err(msg, *arg, **kwarg):
+ """Log a message at the ERROR level."""
+ print "[!] %s" % msg
+
+def fail(*failure):
+ """Log a message at the CRITICAL level."""
+ logging.critical(failure)
+ ## xxx should we take steps to exit here?
+
+def exception(error):
+ """
+ Catch an exception and print only the error message, then continue normal
+ program execution.
+
+ @param error: Can be error messages printed to stdout and to the
+ logfile, or can be a twisted.python.failure.Failure instance.
+ """
+ if isinstance(error, Failure):
+ error.printTraceback()
+ else:
+ exc_type, exc_value, exc_traceback = sys.exc_info()
+ traceback.print_exception(exc_type, exc_value, exc_traceback)
+
+def catch(func):
+ """
+ Quick wrapper to add around test methods for debugging purposes,
+ catches the given Exception. Use like so:
+
+ >>> @log.catch
+ def foo(bar):
+ if bar == 'baz':
+ raise Exception("catch me no matter what I am")
+ >>> foo("baz")
+ [!] catch me no matter what I am
+
+ """
+ @wraps(func)
+ def _catch(*args, **kwargs):
+ try:
+ func(*args, **kwargs)
+ except Exception, exc:
+ exception(exc)
+ return _catch
diff --git a/src/leap/mx/util/net.py b/src/leap/mx/util/net.py
new file mode 100644
index 0000000..64dbc90
--- /dev/null
+++ b/src/leap/mx/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.util 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/mx/util/storage.py b/src/leap/mx/util/storage.py
new file mode 100644
index 0000000..c4c797a
--- /dev/null
+++ b/src/leap/mx/util/storage.py
@@ -0,0 +1,42 @@
+
+class Storage(dict):
+ """
+ A Storage object is like a dictionary except `obj.foo` can be used
+ in addition to `obj['foo']`.
+
+ >>> o = Storage(a=1)
+ >>> o.a
+ 1
+ >>> o['a']
+ 1
+ >>> o.a = 2
+ >>> o['a']
+ 2
+ >>> del o.a
+ >>> o.a
+ None
+ """
+ def __getattr__(self, key):
+ try:
+ return self[key]
+ except KeyError, k:
+ return None
+
+ def __setattr__(self, key, value):
+ self[key] = value
+
+ def __delattr__(self, key):
+ try:
+ del self[key]
+ except KeyError, k:
+ raise AttributeError, k
+
+ def __repr__(self):
+ return '<Storage ' + dict.__repr__(self) + '>'
+
+ def __getstate__(self):
+ return dict(self)
+
+ def __setstate__(self, value):
+ for (k, v) in value.items():
+ self[k] = v
diff --git a/src/leap/mx/util/version.py b/src/leap/mx/util/version.py
new file mode 100644
index 0000000..c32166f
--- /dev/null
+++ b/src/leap/mx/util/version.py
@@ -0,0 +1,95 @@
+#!/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
+'''
+
+from os import getcwd
+from os import path as ospath
+
+import sys
+
+
+class Version(object):
+ def __init__(self):
+ self.name = 'leap_mx'
+ self.version = '0.0.2'
+ self.pipfile = ospath.join(self.getRepoDir(),
+ 'pkg/mx-requirements.pip')
+ self.authors = [
+ ('Isis Agora Lovecruft', '<isis@leap.se>', '0x2cdb8b35'),
+ ]
+ self.git_url = 'https://github.com/isislovecruft/leap_mx/'
+ self.website = 'https://leap.se'
+
+ def getPackageName(self):
+ """Returns the application name."""
+ return self.name
+
+ def getPipfile(self):
+ """Returns the full path of the pip requirements.txt file."""
+ return self.pipfile
+
+ def getVersion(self):
+ """Returns a version the application name and version number."""
+ return self.version
+
+ def getAuthors(self):
+ credits = str()
+ for author in self.authors:
+ credits += " ".join(author)
+ return credits
+
+ def getRepoDir(self):
+ """Get the top-level repository directory."""
+ here = getcwd()
+ base = here.rsplit(self.name, 1)[0]
+ repo = ospath.join(base, self.name)
+ return repo
+
+ def __make_text__(self, extra_text=None):
+ splitr = "-" * len(self.version.__str__())
+ header = ["\n%s\n" % self.version.__str__(),
+ "%s\n" % splitr]
+ footer = ["Website: \t%s\n" % self.website,
+ "Github: \t%s\n" % self.git_url,
+ "\n"]
+ contacts = ["\t%s, %s %s\n"
+ % (a[0], a[1], a[2]) for a in self.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__(self):
+ repo = self.getRepoDir()
+ self.version_file = ospath.join(repo, 'VERSION')
+ version_text = self.__make_text__()
+
+ with open(self.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..."
+ vrsn = Version()
+ vrsn.__update_version__()
+ print "Done."