summaryrefslogtreecommitdiff
path: root/src/leap/mx
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/mx')
-rw-r--r--src/leap/mx/__init__.py4
-rw-r--r--src/leap/mx/util/__init__.py1
-rw-r--r--src/leap/mx/util/config.py172
-rw-r--r--src/leap/mx/util/exceptions.py23
-rw-r--r--src/leap/mx/util/log.py154
-rw-r--r--src/leap/mx/util/net.py126
-rw-r--r--src/leap/mx/util/version.py101
7 files changed, 580 insertions, 1 deletions
diff --git a/src/leap/mx/__init__.py b/src/leap/mx/__init__.py
index 10d8ee7..507efee 100644
--- a/src/leap/mx/__init__.py
+++ b/src/leap/mx/__init__.py
@@ -1 +1,3 @@
-__all__ = ['alias_resolver', 'runner', 'tests']
+#from . import util
+
+__all__ = ['alias_resolver', 'runner', 'tests', 'couchdb']
diff --git a/src/leap/mx/util/__init__.py b/src/leap/mx/util/__init__.py
new file mode 100644
index 0000000..c8448b0
--- /dev/null
+++ b/src/leap/mx/util/__init__.py
@@ -0,0 +1 @@
+__all__ = ['config', 'exceptions', 'log', 'net', 'version, storage']
diff --git a/src/leap/mx/util/config.py b/src/leap/mx/util/config.py
new file mode 100644
index 0000000..5bc5231
--- /dev/null
+++ b/src/leap/mx/util/config.py
@@ -0,0 +1,172 @@
+#! -*- 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
+"""
+
+import os
+import yaml
+
+## xxx only install/import this in *nix
+from xdg import BaseDirectory
+
+from leap.util import log, version, Storage
+from leap.util.exceptions import MissingConfig, UnsupportedOS
+
+
+def _create_config_file(file):
+ """
+ xxx fill me in
+ """
+ with open(file, '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:
+ # Where is the spoolfile of messages to encrypt?:
+ spoolfile: /var/mail/encrypt_me
+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
+
+""")
+ conf.flush()
+ try:
+ assert os.path.isfile(file), "Config file %s not created!" % file
+ except AssertionError, ae:
+ raise SystemExit(ae.message)
+ else:
+ return file
+
+def _get_config_filename(filename=None, use_dot_config_directory=False):
+ """
+ Get the full path and filename of the config file.
+ """
+ platform = version.getClientPlatform()[0]
+ resource = version.name
+
+ ## 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.")
+
+ ## If not given, default to the application's name + '.conf'
+ if not filename:
+ filename = resource + ".conf"
+
+ where = None
+ if not use_dot_config_directory:
+ repo_dir = version.getRepoDir()
+ where = os.path.abspath(repo_dir)
+ ## Use ~/.config/ instead:
+ else:
+ dot_config_dirs = BaseDirectory.xdg_config_dirs
+ for dir in dot_config_dirs:
+ our_dir = os.path.join(dir, resource)
+ if os.path.isdir(our_dir):
+ if filename in os.listdir(our_dir):
+ where = os.path.abspath(our_dir)
+ if not where:
+ where = BaseDirectory.save_config_path(resource)
+
+ conffile = os.path.join(where, filename)
+ try:
+ with open(conffile) as cf: pass
+ except IOError:
+ conffile = _create_config_file(conffile)
+ finally:
+ return conffile
+
+def loadConfig(filename=config_filename):
+ """
+ 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 filename: (optional) If provided, use this filename.
+ """
+ if not filename:
+ filename = _get_config_filename()
+
+ if os.path.isfile(filename):
+ with open(filename, 'a+') as conf:
+ config_contents = '\n'.join(conf.readlines())
+ configuration = yaml.safe_load(config_contents)
+
+ ## These become objects with their keys loaded as attributes:
+ ##
+ ## from leap.util import config
+ ## config.basic.foo = bar
+ ##
+ basic = Storage()
+ try:
+ for k, v in configuration['basic'].items():
+ basic[k] = v
+ except AttributeError:
+ pass
+
+ advanced = Storage()
+ try:
+ for k, v in configuration['advanced'].items():
+ advanced[k] = v
+ except AttributeError:
+ pass
+
+ return basic, advanced
+ 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 config_filename:
+ config_filename = _get_config_filename()
+else:
+ config_filename = _get_config_filename(filename=config_filename)
+
diff --git a/src/leap/mx/util/exceptions.py b/src/leap/mx/util/exceptions.py
new file mode 100644
index 0000000..63b946c
--- /dev/null
+++ b/src/leap/mx/util/exceptions.py
@@ -0,0 +1,23 @@
+#! -*- encoding: utf-8 -*-
+"""
+Custom exceptions for leap_mx.
+
+@authors: Isis Lovecruft, <isis@leap.se> 0x2cdb8b35
+@version: 0.0.1
+@license: see included LICENSE file
+"""
+
+
+class MissingConfig(Exception):
+ """Raised when the config file cannot be found."""
+ def __init__(self, message=None, config_file=None):
+ if message:
+ return
+ else:
+ self.message = "Cannot locate config file"
+ if config_file:
+ self.message += " %s" % config_file
+ self.message += "."
+
+class UnsupportedOS(Exception):
+ """Raised when we're not *nix or *BSD."""
diff --git a/src/leap/mx/util/log.py b/src/leap/mx/util/log.py
new file mode 100644
index 0000000..ef54605
--- /dev/null
+++ b/src/leap/mx/util/log.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python
+# -*- 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.logfile import DailyLogFile
+from twisted.python.failure import Failure
+
+from leap.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(logfile=None, application_name=None):
+ """
+ Start logging to stdout, and optionally to a logfile as well.
+
+ @param logfile: The filename to store logs in, which is placed in
+ /leap_mx/logs/.
+ @param application_name: The name of the running application.
+ """
+ if not application_name:
+ application_name = version.name
+ print "application name: %s" % application_name
+
+ daily_logfile = None
+
+ if not logfile:
+ logfile = config.basic.logfile
+
+ repo_dir = version.getRepoDir()
+ logfile_dir = os.path.join(repo_dir, 'log')
+ logfile_name = logfile
+
+ daily_logfile = DailyLogFile(logfile_name, logfile_dir)
+
+ txlog.startLoggingWithObserver(UnprefixedLogfile(sys.stdout).emit)
+ txlog.addObserver(txlog.FileLogObserver(daily_logfile).emit)
+ txlog.msg("Starting %s on %s (%s UTC)" % (application_name,
+ prettyDateNow(),
+ utcPrettyDateNow()))
+
+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.basic.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..a4104d0
--- /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.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/mx/util/version.py b/src/leap/mx/util/version.py
new file mode 100644
index 0000000..215876c
--- /dev/null
+++ b/src/leap/mx/util/version.py
@@ -0,0 +1,101 @@
+#!/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
+import sys
+
+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'
+
+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 getVersion():
+ """
+ Returns a version object, with attributes authors, git_url, and website.
+ """
+ version.authors = authors
+ version.git_url = git_url
+ version.website = website
+ return version
+
+def getRepoDir():
+ """
+ Get the top-level repository directory.
+ """
+ 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."