summaryrefslogtreecommitdiff
path: root/src/leap/eip
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/eip')
-rw-r--r--src/leap/eip/checks.py542
-rw-r--r--src/leap/eip/conductor.py272
-rw-r--r--src/leap/eip/config.py398
-rw-r--r--src/leap/eip/constants.py3
-rw-r--r--src/leap/eip/eipconnection.py405
-rw-r--r--src/leap/eip/exceptions.py175
-rw-r--r--src/leap/eip/openvpnconnection.py410
-rw-r--r--src/leap/eip/specs.py136
-rw-r--r--src/leap/eip/tests/__init__.py0
-rw-r--r--src/leap/eip/tests/data.py51
-rw-r--r--src/leap/eip/tests/test_checks.py372
-rw-r--r--src/leap/eip/tests/test_config.py298
-rw-r--r--src/leap/eip/tests/test_eipconnection.py216
-rw-r--r--src/leap/eip/tests/test_openvpnconnection.py161
-rw-r--r--src/leap/eip/udstelnet.py38
-rw-r--r--src/leap/eip/vpnmanager.py262
-rw-r--r--src/leap/eip/vpnwatcher.py169
17 files changed, 3205 insertions, 703 deletions
diff --git a/src/leap/eip/checks.py b/src/leap/eip/checks.py
new file mode 100644
index 00000000..af824c57
--- /dev/null
+++ b/src/leap/eip/checks.py
@@ -0,0 +1,542 @@
+import logging
+import time
+import os
+import sys
+
+import requests
+
+from leap import __branding as BRANDING
+from leap import certs as leapcerts
+from leap.base.auth import srpauth_protected, magick_srpauth
+from leap.base import config as baseconfig
+from leap.base import constants as baseconstants
+from leap.base import providers
+from leap.crypto import certs
+from leap.eip import config as eipconfig
+from leap.eip import constants as eipconstants
+from leap.eip import exceptions as eipexceptions
+from leap.eip import specs as eipspecs
+from leap.util.certs import get_mac_cabundle
+from leap.util.fileutil import mkdir_p
+from leap.util.web import get_https_domain_and_port
+
+logger = logging.getLogger(name=__name__)
+
+"""
+ProviderCertChecker
+-------------------
+Checks on certificates. To be moved to base.
+docs TBD
+
+EIPConfigChecker
+----------
+It is used from the eip conductor (a instance of EIPConnection that is
+managed from the QtApp), running `run_all` method before trying to call
+`connect` or any other of the state-changing methods.
+
+It checks that the needed files are provided or can be discovered over the
+net. Much of these tests are not specific to EIP module, and can be splitted
+into base.tests to be invoked by the base leap init routines.
+However, I'm testing them alltogether for the sake of having the whole unit
+reachable and testable as a whole.
+
+"""
+
+
+def get_branding_ca_cert(domain):
+ # deprecated
+ ca_file = BRANDING.get('provider_ca_file')
+ if ca_file:
+ return leapcerts.where(ca_file)
+
+
+class ProviderCertChecker(object):
+ """
+ Several checks needed for getting
+ client certs and checking tls connection
+ with provider.
+ """
+ def __init__(self, fetcher=requests,
+ domain=None):
+
+ self.fetcher = fetcher
+ self.domain = domain
+ #XXX needs some kind of autoinit
+ #right now we set by hand
+ #by loading and reading provider config
+ self.apidomain = None
+ self.cacert = eipspecs.provider_ca_path(domain)
+
+ def run_all(
+ self, checker=None,
+ skip_download=False, skip_verify=False):
+
+ if not checker:
+ checker = self
+
+ do_verify = not skip_verify
+ logger.debug('do_verify: %s', do_verify)
+ # checker.download_ca_cert()
+
+ # For MVS+
+ # checker.download_ca_signature()
+ # checker.get_ca_signatures()
+ # checker.is_there_trust_path()
+
+ # For MVS
+ checker.is_there_provider_ca()
+
+ checker.is_https_working(verify=do_verify, autocacert=False)
+ checker.check_new_cert_needed(verify=do_verify)
+
+ def download_ca_cert(self, uri=None, verify=True):
+ req = self.fetcher.get(uri, verify=verify)
+ req.raise_for_status()
+
+ # should check domain exists
+ capath = self._get_ca_cert_path(self.domain)
+ with open(capath, 'w') as f:
+ f.write(req.content)
+
+ def check_ca_cert_fingerprint(
+ self, hash_type="SHA256",
+ fingerprint=None):
+ """
+ compares the fingerprint in
+ the ca cert with a string
+ we are passed
+ returns True if they are equal, False if not.
+ @param hash_type: digest function
+ @type hash_type: str
+ @param fingerprint: the fingerprint to compare with.
+ @type fingerprint: str (with : separator)
+ @rtype bool
+ """
+ ca_cert_path = self.ca_cert_path
+ ca_cert_fpr = certs.get_cert_fingerprint(
+ filepath=ca_cert_path)
+ return ca_cert_fpr == fingerprint
+
+ def verify_api_https(self, uri):
+ assert uri.startswith('https://')
+ cacert = self.ca_cert_path
+ verify = cacert or True
+
+ # DEBUG
+ logger.debug('uri -> %s' % uri)
+ logger.debug('cacertpath -> %s' % cacert)
+
+ req = self.fetcher.get(uri, verify=verify)
+ req.raise_for_status()
+ return True
+
+ def download_ca_signature(self):
+ # MVS+
+ raise NotImplementedError
+
+ def get_ca_signatures(self):
+ # MVS+
+ raise NotImplementedError
+
+ def is_there_trust_path(self):
+ # MVS+
+ raise NotImplementedError
+
+ def is_there_provider_ca(self):
+ if not self.cacert:
+ return False
+ cacert_exists = os.path.isfile(self.cacert)
+ if cacert_exists:
+ logger.debug('True')
+ return True
+ logger.debug('False!')
+ return False
+
+ def is_https_working(
+ self, uri=None, verify=True,
+ autocacert=False):
+ if uri is None:
+ uri = self._get_root_uri()
+ # XXX raise InsecureURI or something better
+ try:
+ assert uri.startswith('https')
+ except AssertionError:
+ raise AssertionError(
+ "uri passed should start with https")
+ if autocacert and verify is True and self.cacert is not None:
+ logger.debug('verify cert: %s', self.cacert)
+ verify = self.cacert
+ if sys.platform == "darwin":
+ verify = get_mac_cabundle()
+ logger.debug('checking https connection')
+ logger.debug('uri: %s (verify:%s)', uri, verify)
+
+ try:
+ self.fetcher.get(uri, verify=verify)
+
+ except requests.exceptions.SSLError as exc:
+ raise eipexceptions.HttpsBadCertError
+
+ except requests.exceptions.ConnectionError:
+ logger.error('ConnectionError')
+ raise eipexceptions.HttpsNotSupported
+
+ else:
+ return True
+
+ def check_new_cert_needed(self, skip_download=False, verify=True):
+ # XXX add autocacert
+ if not self.is_cert_valid(do_raise=False):
+ logger.debug('cert needed: true')
+ self.download_new_client_cert(
+ skip_download=skip_download,
+ verify=verify)
+ return True
+ logger.debug('cert needed: false')
+ return False
+
+ def download_new_client_cert(self, uri=None, verify=True,
+ skip_download=False,
+ credentials=None):
+ logger.debug('download new client cert')
+ if skip_download:
+ return True
+ if uri is None:
+ uri = self._get_client_cert_uri()
+ # XXX raise InsecureURI or something better
+ #assert uri.startswith('https')
+
+ if verify is True and self.cacert is not None:
+ verify = self.cacert
+ logger.debug('verify = %s', verify)
+
+ fgetfn = self.fetcher.get
+
+ if credentials:
+ user, passwd = credentials
+ logger.debug('apidomain = %s', self.apidomain)
+
+ @srpauth_protected(user, passwd,
+ server="https://%s" % self.apidomain,
+ verify=verify)
+ def getfn(*args, **kwargs):
+ return fgetfn(*args, **kwargs)
+
+ else:
+ # XXX FIXME fix decorated args
+ @magick_srpauth(verify)
+ def getfn(*args, **kwargs):
+ return fgetfn(*args, **kwargs)
+ try:
+
+ req = getfn(uri, verify=verify)
+ req.raise_for_status()
+
+ except requests.exceptions.SSLError:
+ logger.warning('SSLError while fetching cert. '
+ 'Look below for stack trace.')
+ # XXX raise better exception
+ return self.fail("SSLError")
+ except Exception as exc:
+ return self.fail(exc.message)
+
+ try:
+ logger.debug('validating cert...')
+ pemfile_content = req.content
+ valid = self.is_valid_pemfile(pemfile_content)
+ if not valid:
+ logger.warning('invalid cert')
+ return False
+ cert_path = self._get_client_cert_path()
+ self.write_cert(pemfile_content, to=cert_path)
+ except:
+ logger.warning('Error while validating cert')
+ raise
+ return True
+
+ def is_cert_valid(self, cert_path=None, do_raise=True):
+ exists = lambda: self.is_certificate_exists()
+ valid_pemfile = lambda: self.is_valid_pemfile()
+ not_expired = lambda: self.is_cert_not_expired()
+
+ valid = exists() and valid_pemfile() and not_expired()
+ if not valid:
+ if do_raise:
+ raise Exception('missing valid cert')
+ else:
+ return False
+ return True
+
+ def is_certificate_exists(self, certfile=None):
+ if certfile is None:
+ certfile = self._get_client_cert_path()
+ return os.path.isfile(certfile)
+
+ def is_cert_not_expired(self, certfile=None, now=time.gmtime):
+ if certfile is None:
+ certfile = self._get_client_cert_path()
+ from_, to_ = certs.get_time_boundaries(certfile)
+
+ return from_ < now() < to_
+
+ def is_valid_pemfile(self, cert_s=None):
+ """
+ checks that the passed string
+ is a valid pem certificate
+ @param cert_s: string containing pem content
+ @type cert_s: string
+ @rtype: bool
+ """
+ if cert_s is None:
+ certfile = self._get_client_cert_path()
+ with open(certfile) as cf:
+ cert_s = cf.read()
+ try:
+ valid = certs.can_load_cert_and_pkey(cert_s)
+ except certs.BadCertError:
+ logger.warning("Not valid pemfile")
+ valid = False
+ return valid
+
+ @property
+ def ca_cert_path(self):
+ return self._get_ca_cert_path(self.domain)
+
+ def _get_root_uri(self):
+ return u"https://%s/" % self.domain
+
+ def _get_client_cert_uri(self):
+ return "https://%s/1/cert" % self.apidomain
+
+ def _get_client_cert_path(self):
+ return eipspecs.client_cert_path(domain=self.domain)
+
+ def _get_ca_cert_path(self, domain):
+ # XXX this folder path will be broken for win
+ # and this should be moved to eipspecs.ca_path
+
+ # XXX use baseconfig.get_provider_path(folder=Foo)
+ # !!!
+
+ capath = baseconfig.get_config_file(
+ 'cacert.pem',
+ folder='providers/%s/keys/ca' % domain)
+ folder, fname = os.path.split(capath)
+ if not os.path.isdir(folder):
+ mkdir_p(folder)
+ return capath
+
+ def write_cert(self, pemfile_content, to=None):
+ folder, filename = os.path.split(to)
+ if not os.path.isdir(folder):
+ mkdir_p(folder)
+ with open(to, 'w') as cert_f:
+ cert_f.write(pemfile_content)
+
+ def set_api_domain(self, domain):
+ self.apidomain = domain
+
+
+class EIPConfigChecker(object):
+ """
+ Several checks needed
+ to ensure a EIPConnection
+ can be sucessfully established.
+ use run_all to run all checks.
+ """
+
+ def __init__(self, fetcher=requests, domain=None):
+ # we do not want to accept too many
+ # argument on init.
+ # we want tests
+ # to be explicitely run.
+
+ self.fetcher = fetcher
+
+ # if not domain, get from config
+ self.domain = domain
+ self.apidomain = None
+ self.cacert = eipspecs.provider_ca_path(domain)
+
+ self.defaultprovider = providers.LeapProviderDefinition(domain=domain)
+ self.defaultprovider.load()
+ self.eipconfig = eipconfig.EIPConfig(domain=domain)
+ self.set_api_domain()
+ self.eipserviceconfig = eipconfig.EIPServiceConfig(domain=domain)
+ self.eipserviceconfig.load()
+
+ def run_all(self, checker=None, skip_download=False):
+ """
+ runs all checks in a row.
+ will raise if some error encountered.
+ catching those exceptions is not
+ our responsibility at this moment
+ """
+ if not checker:
+ checker = self
+
+ # let's call all tests
+ # needed for a sane eip session.
+
+ # TODO: get rid of check_default.
+ # check_complete should
+ # be enough. but here to make early tests easier.
+ checker.check_default_eipconfig()
+
+ checker.check_is_there_default_provider()
+ checker.fetch_definition(skip_download=skip_download)
+ checker.fetch_eip_service_config(skip_download=skip_download)
+ checker.check_complete_eip_config()
+ #checker.ping_gateway()
+
+ # public checks
+
+ def check_default_eipconfig(self):
+ """
+ checks if default eipconfig exists,
+ and dumps a default file if not
+ """
+ # XXX ONLY a transient check
+ # because some old function still checks
+ # for eip config at the beginning.
+
+ # it *really* does not make sense to
+ # dump it right now, we can get an in-memory
+ # config object and dump it to disk in a
+ # later moment
+ logger.debug('checking default eip config')
+ if not self._is_there_default_eipconfig():
+ self._dump_default_eipconfig()
+
+ def check_is_there_default_provider(self, config=None):
+ """
+ raises EIPMissingDefaultProvider if no
+ default provider found on eip config.
+ This is catched by ui and runs FirstRunWizard (MVS+)
+ """
+ if config is None:
+ config = self.eipconfig.config
+ logger.debug('checking default provider')
+ provider = config.get('provider', None)
+ if provider is None:
+ raise eipexceptions.EIPMissingDefaultProvider
+ # XXX raise also if malformed ProviderDefinition?
+ return True
+
+ def fetch_definition(self, skip_download=False,
+ force_download=False,
+ config=None, uri=None,
+ domain=None):
+ """
+ fetches a definition file from server
+ """
+ # TODO:
+ # - Implement diff
+ # - overwrite only if different.
+ # (attend to serial field different, for instance)
+
+ logger.debug('fetching definition')
+
+ if skip_download:
+ logger.debug('(fetching def skipped)')
+ return True
+ if config is None:
+ config = self.defaultprovider.config
+ if uri is None:
+ if not domain:
+ domain = config.get('provider', None)
+ uri = self._get_provider_definition_uri(domain=domain)
+
+ if sys.platform == "darwin":
+ verify = get_mac_cabundle()
+ else:
+ verify = True
+
+ self.defaultprovider.load(
+ from_uri=uri,
+ fetcher=self.fetcher,
+ verify=verify)
+ self.defaultprovider.save()
+
+ def fetch_eip_service_config(self, skip_download=False,
+ force_download=False,
+ config=None, uri=None, # domain=None,
+ autocacert=True, verify=True):
+ if skip_download:
+ return True
+ if config is None:
+ self.eipserviceconfig.load()
+ config = self.eipserviceconfig.config
+ if uri is None:
+ #XXX
+ #if not domain:
+ #domain = self.domain or config.get('provider', None)
+ uri = self._get_eip_service_uri(
+ domain=self.apidomain)
+
+ if autocacert and self.cacert is not None:
+ verify = self.cacert
+
+ self.eipserviceconfig.load(
+ from_uri=uri,
+ fetcher=self.fetcher,
+ force_download=force_download,
+ verify=verify)
+ self.eipserviceconfig.save()
+
+ def check_complete_eip_config(self, config=None):
+ # TODO check for gateway
+ if config is None:
+ config = self.eipconfig.config
+ try:
+ assert 'provider' in config
+ assert config['provider'] is not None
+ # XXX assert there is gateway !!
+ except AssertionError:
+ raise eipexceptions.EIPConfigurationError
+
+ # XXX TODO:
+ # We should WRITE eip config if missing or
+ # incomplete at this point
+ #self.eipconfig.save()
+
+ #
+ # private helpers
+ #
+
+ def _is_there_default_eipconfig(self):
+ return self.eipconfig.exists()
+
+ def _dump_default_eipconfig(self):
+ self.eipconfig.save(force=True)
+
+ def _get_provider_definition_uri(self, domain=None, path=None):
+ if domain is None:
+ domain = self.domain or baseconstants.DEFAULT_PROVIDER
+ if path is None:
+ path = baseconstants.DEFINITION_EXPECTED_PATH
+ uri = u"https://%s/%s" % (domain, path)
+ logger.debug('getting provider definition from %s' % uri)
+ return uri
+
+ def _get_eip_service_uri(self, domain=None, path=None):
+ if domain is None:
+ domain = self.domain or baseconstants.DEFAULT_PROVIDER
+ if path is None:
+ path = eipconstants.EIP_SERVICE_EXPECTED_PATH
+ uri = "https://%s/%s" % (domain, path)
+ logger.debug('getting eip service file from %s', uri)
+ return uri
+
+ def set_api_domain(self):
+ """sets api domain from defaultprovider config object"""
+ api = self.defaultprovider.config.get('api_uri', None)
+ # the caller is responsible for having loaded the config
+ # object at this point
+ if api:
+ api_dom = get_https_domain_and_port(api)
+ self.apidomain = "%s:%s" % api_dom
+
+ def get_api_domain(self):
+ """gets api domain"""
+ return self.apidomain
diff --git a/src/leap/eip/conductor.py b/src/leap/eip/conductor.py
deleted file mode 100644
index e3adadc4..00000000
--- a/src/leap/eip/conductor.py
+++ /dev/null
@@ -1,272 +0,0 @@
-"""
-stablishes a vpn connection and monitors its state
-"""
-from __future__ import (division, unicode_literals, print_function)
-#import threading
-from functools import partial
-import logging
-
-from leap.utils.coroutines import spawn_and_watch_process
-from leap.baseapp.config import get_config, get_vpn_stdout_mockup
-from leap.eip.vpnwatcher import EIPConnectionStatus, status_watcher
-from leap.eip.vpnmanager import OpenVPNManager, ConnectionRefusedError
-
-logger = logging.getLogger(name=__name__)
-
-
-# TODO Move exceptions to their own module
-
-
-class ConnectionError(Exception):
- """
- generic connection error
- """
- pass
-
-
-class EIPClientError(Exception):
- """
- base EIPClient exception
- """
- def __str__(self):
- if len(self.args) >= 1:
- return repr(self.args[0])
- else:
- return ConnectionError
-
-
-class UnrecoverableError(EIPClientError):
- """
- we cannot do anything about it, sorry
- """
- pass
-
-
-class OpenVPNConnection(object):
- """
- All related to invocation
- of the openvpn binary
- """
- # Connection Methods
-
- def __init__(self, config_file=None, watcher_cb=None):
- #XXX FIXME
- #change watcher_cb to line_observer
- """
- :param config_file: configuration file to read from
- :param watcher_cb: callback to be \
-called for each line in watched stdout
- :param signal_map: dictionary of signal names and callables \
-to be triggered for each one of them.
- :type config_file: str
- :type watcher_cb: function
- :type signal_map: dict
- """
- # XXX get host/port from config
- self.manager = OpenVPNManager()
-
- self.config_file = config_file
- self.watcher_cb = watcher_cb
- #self.signal_maps = signal_maps
-
- self.subp = None
- self.watcher = None
-
- self.server = None
- self.port = None
- self.proto = None
-
- self.autostart = True
-
- self._get_config()
-
- def _set_command_mockup(self):
- """
- sets command and args for a command mockup
- that just mimics the output from the real thing
- """
- command, args = get_vpn_stdout_mockup()
- self.command, self.args = command, args
-
- def _get_config(self):
- """
- retrieves the config options from defaults or
- home file, or config file passed in command line.
- """
- config = get_config(config_file=self.config_file)
- self.config = config
-
- if config.has_option('openvpn', 'command'):
- commandline = config.get('openvpn', 'command')
- if commandline == "mockup":
- self._set_command_mockup()
- return
- command_split = commandline.split(' ')
- command = command_split[0]
- if len(command_split) > 1:
- args = command_split[1:]
- else:
- args = []
- self.command = command
- #print("debug: command = %s" % command)
- self.args = args
- else:
- self._set_command_mockup()
-
- if config.has_option('openvpn', 'autostart'):
- autostart = config.get('openvpn', 'autostart')
- self.autostart = autostart
-
- def _launch_openvpn(self):
- """
- invocation of openvpn binaries in a subprocess.
- """
- #XXX TODO:
- #deprecate watcher_cb,
- #use _only_ signal_maps instead
-
- if self.watcher_cb is not None:
- linewrite_callback = self.watcher_cb
- else:
- #XXX get logger instead
- linewrite_callback = lambda line: print('watcher: %s' % line)
-
- observers = (linewrite_callback,
- partial(status_watcher, self.status))
- subp, watcher = spawn_and_watch_process(
- self.command,
- self.args,
- observers=observers)
- self.subp = subp
- self.watcher = watcher
-
- conn_result = self.status.CONNECTED
- return conn_result
-
- def _try_connection(self):
- """
- attempts to connect
- """
- if self.subp is not None:
- print('cowardly refusing to launch subprocess again')
- return
- self._launch_openvpn()
-
- def cleanup(self):
- """
- terminates child subprocess
- """
- if self.subp:
- self.subp.terminate()
-
-
-class EIPConductor(OpenVPNConnection):
- """
- Manages the execution of the OpenVPN process, auto starts, monitors the
- network connection, handles configuration, fixes leaky hosts, handles
- errors, etc.
- Preferences will be stored via the Storage API. (TBD)
- Status updates (connected, bandwidth, etc) are signaled to the GUI.
- """
-
- def __init__(self, *args, **kwargs):
- self.settingsfile = kwargs.get('settingsfile', None)
- self.logfile = kwargs.get('logfile', None)
- self.error_queue = []
- self.desired_con_state = None # ???
-
- status_signals = kwargs.pop('status_signals', None)
- self.status = EIPConnectionStatus(callbacks=status_signals)
-
- super(EIPConductor, self).__init__(*args, **kwargs)
-
- def connect(self):
- """
- entry point for connection process
- """
- self.manager.forget_errors()
- self._try_connection()
- # XXX should capture errors?
-
- def disconnect(self):
- """
- disconnects client
- """
- self._disconnect()
- self.status.change_to(self.status.DISCONNECTED)
- pass
-
- def shutdown(self):
- """
- shutdown and quit
- """
- self.desired_con_state = self.status.DISCONNECTED
-
- def connection_state(self):
- """
- returns the current connection state
- """
- return self.status.current
-
- def desired_connection_state(self):
- """
- returns the desired_connection state
- """
- return self.desired_con_state
-
- def poll_connection_state(self):
- """
- """
- try:
- state = self.manager.get_connection_state()
- except ConnectionRefusedError:
- # connection refused. might be not ready yet.
- return
- if not state:
- return
- (ts, status_step,
- ok, ip, remote) = state
- self.status.set_vpn_state(status_step)
- status_step = self.status.get_readable_status()
- return (ts, status_step, ok, ip, remote)
-
- def get_icon_name(self):
- """
- get icon name from status object
- """
- return self.status.get_state_icon()
-
- #
- # private methods
- #
-
- def _disconnect(self):
- """
- private method for disconnecting
- """
- if self.subp is not None:
- self.subp.terminate()
- self.subp = None
- # XXX signal state changes! :)
-
- def _is_alive(self):
- """
- don't know yet
- """
- pass
-
- def _connect(self):
- """
- entry point for connection cascade methods.
- """
- #conn_result = ConState.DISCONNECTED
- try:
- conn_result = self._try_connection()
- except UnrecoverableError as except_msg:
- logger.error("FATAL: %s" % unicode(except_msg))
- conn_result = self.status.UNRECOVERABLE
- except Exception as except_msg:
- self.error_queue.append(except_msg)
- logger.error("Failed Connection: %s" %
- unicode(except_msg))
- return conn_result
diff --git a/src/leap/eip/config.py b/src/leap/eip/config.py
new file mode 100644
index 00000000..917871da
--- /dev/null
+++ b/src/leap/eip/config.py
@@ -0,0 +1,398 @@
+import logging
+import os
+import platform
+import re
+import tempfile
+
+from leap import __branding as BRANDING
+from leap import certs
+from leap.util.misc import null_check
+from leap.util.fileutil import (which, mkdir_p, check_and_fix_urw_only)
+
+from leap.base import config as baseconfig
+from leap.baseapp.permcheck import (is_pkexec_in_system,
+ is_auth_agent_running)
+from leap.eip import exceptions as eip_exceptions
+from leap.eip import specs as eipspecs
+
+logger = logging.getLogger(name=__name__)
+provider_ca_file = BRANDING.get('provider_ca_file', None)
+
+_platform = platform.system()
+
+
+class EIPConfig(baseconfig.JSONLeapConfig):
+ spec = eipspecs.eipconfig_spec
+
+ def _get_slug(self):
+ eipjsonpath = baseconfig.get_config_file(
+ 'eip.json')
+ return eipjsonpath
+
+ def _set_slug(self, *args, **kwargs):
+ raise AttributeError("you cannot set slug")
+
+ slug = property(_get_slug, _set_slug)
+
+
+class EIPServiceConfig(baseconfig.JSONLeapConfig):
+ spec = eipspecs.eipservice_config_spec
+
+ def _get_slug(self):
+ domain = getattr(self, 'domain', None)
+ if domain:
+ path = baseconfig.get_provider_path(domain)
+ else:
+ path = baseconfig.get_default_provider_path()
+ return baseconfig.get_config_file(
+ 'eip-service.json', folder=path)
+
+ def _set_slug(self):
+ raise AttributeError("you cannot set slug")
+
+ slug = property(_get_slug, _set_slug)
+
+
+def get_socket_path():
+ socket_path = os.path.join(
+ tempfile.mkdtemp(prefix="leap-tmp"),
+ 'openvpn.socket')
+ #logger.debug('socket path: %s', socket_path)
+ return socket_path
+
+
+def get_eip_gateway(eipconfig=None, eipserviceconfig=None):
+ """
+ return the first host in eip service config
+ that matches the name defined in the eip.json config
+ file.
+ """
+ # XXX eventually we should move to a more clever
+ # gateway selection. maybe we could return
+ # all gateways that match our cluster.
+
+ null_check(eipconfig, "eipconfig")
+ null_check(eipserviceconfig, "eipserviceconfig")
+ PLACEHOLDER = "testprovider.example.org"
+
+ conf = eipconfig.config
+ eipsconf = eipserviceconfig.config
+
+ primary_gateway = conf.get('primary_gateway', None)
+ if not primary_gateway:
+ return PLACEHOLDER
+
+ gateways = eipsconf.get('gateways', None)
+ if not gateways:
+ logger.error('missing gateways in eip service config')
+ return PLACEHOLDER
+
+ if len(gateways) > 0:
+ for gw in gateways:
+ clustername = gw.get('cluster', None)
+ if not clustername:
+ logger.error('no cluster name')
+ return
+
+ if clustername == primary_gateway:
+ # XXX at some moment, we must
+ # make this a more generic function,
+ # and return ports, protocols...
+ ipaddress = gw.get('ip_address', None)
+ if not ipaddress:
+ logger.error('no ip_address')
+ return
+ return ipaddress
+ logger.error('could not find primary gateway in provider'
+ 'gateway list')
+
+
+def get_cipher_options(eipserviceconfig=None):
+ """
+ gathers optional cipher options from eip-service config.
+ :param eipserviceconfig: EIPServiceConfig instance
+ """
+ null_check(eipserviceconfig, 'eipserviceconfig')
+ eipsconf = eipserviceconfig.get_config()
+
+ ALLOWED_KEYS = ("auth", "cipher", "tls-cipher")
+ CIPHERS_REGEX = re.compile("[A-Z0-9\-]+")
+ opts = []
+ if 'openvpn_configuration' in eipsconf:
+ config = eipserviceconfig.config.get(
+ "openvpn_configuration", {})
+ for key, value in config.items():
+ if key in ALLOWED_KEYS and value is not None:
+ sanitized_val = CIPHERS_REGEX.findall(value)
+ if len(sanitized_val) != 0:
+ _val = sanitized_val[0]
+ opts.append('--%s' % key)
+ opts.append('%s' % _val)
+ return opts
+
+LINUX_UP_DOWN_SCRIPT = "/etc/leap/resolv-update"
+OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so"
+
+
+def has_updown_scripts():
+ """
+ checks the existence of the up/down scripts
+ """
+ # XXX should check permissions too
+ is_file = os.path.isfile(LINUX_UP_DOWN_SCRIPT)
+ if not is_file:
+ logger.warning(
+ "Could not find up/down scripts at %s! "
+ "Risk of DNS Leaks!!!")
+ return is_file
+
+
+def build_ovpn_options(daemon=False, socket_path=None, **kwargs):
+ """
+ build a list of options
+ to be passed in the
+ openvpn invocation
+ @rtype: list
+ @rparam: options
+ """
+ # XXX review which of the
+ # options we don't need.
+
+ # TODO pass also the config file,
+ # since we will need to take some
+ # things from there if present.
+
+ provider = kwargs.pop('provider', None)
+ eipconfig = EIPConfig(domain=provider)
+ eipconfig.load()
+ eipserviceconfig = EIPServiceConfig(domain=provider)
+ eipserviceconfig.load()
+
+ # get user/group name
+ # also from config.
+ user = baseconfig.get_username()
+ group = baseconfig.get_groupname()
+
+ opts = []
+
+ opts.append('--client')
+
+ opts.append('--dev')
+ # XXX same in win?
+ opts.append('tun')
+ opts.append('--persist-tun')
+ opts.append('--persist-key')
+
+ verbosity = kwargs.get('ovpn_verbosity', None)
+ if verbosity and 1 <= verbosity <= 6:
+ opts.append('--verb')
+ opts.append("%s" % verbosity)
+
+ # remote ##############################
+ # (server, port, protocol)
+
+ opts.append('--remote')
+
+ gw = get_eip_gateway(eipconfig=eipconfig,
+ eipserviceconfig=eipserviceconfig)
+ logger.debug('setting eip gateway to %s', gw)
+ opts.append(str(gw))
+
+ # get port/protocol from eipservice too
+ opts.append('1194')
+ #opts.append('80')
+ opts.append('udp')
+
+ opts.append('--tls-client')
+ opts.append('--remote-cert-tls')
+ opts.append('server')
+
+ # get ciphers #######################
+
+ ciphers = get_cipher_options(
+ eipserviceconfig=eipserviceconfig)
+ for cipheropt in ciphers:
+ opts.append(str(cipheropt))
+
+ # set user and group
+ opts.append('--user')
+ opts.append('%s' % user)
+ opts.append('--group')
+ opts.append('%s' % group)
+
+ opts.append('--management-client-user')
+ opts.append('%s' % user)
+ opts.append('--management-signal')
+
+ # set default options for management
+ # interface. unix sockets or telnet interface for win.
+ # XXX take them from the config object.
+
+ if _platform == "Windows":
+ opts.append('--management')
+ opts.append('localhost')
+ # XXX which is a good choice?
+ opts.append('7777')
+
+ if _platform in ("Linux", "Darwin"):
+ opts.append('--management')
+
+ if socket_path is None:
+ socket_path = get_socket_path()
+ opts.append(socket_path)
+ opts.append('unix')
+
+ opts.append('--script-security')
+ opts.append('2')
+
+ if _platform == "Linux":
+ if has_updown_scripts():
+ opts.append("--up")
+ opts.append(LINUX_UP_DOWN_SCRIPT)
+ opts.append("--down")
+ opts.append(LINUX_UP_DOWN_SCRIPT)
+ opts.append("--plugin")
+ opts.append(OPENVPN_DOWN_ROOT)
+ opts.append("'script_type=down %s'" % LINUX_UP_DOWN_SCRIPT)
+
+ # certs
+ client_cert_path = eipspecs.client_cert_path(provider)
+ ca_cert_path = eipspecs.provider_ca_path(provider)
+
+ # XXX FIX paths for MAC
+ opts.append('--cert')
+ opts.append(client_cert_path)
+ opts.append('--key')
+ opts.append(client_cert_path)
+ opts.append('--ca')
+ opts.append(ca_cert_path)
+
+ # we cannot run in daemon mode
+ # with the current subp setting.
+ # see: https://leap.se/code/issues/383
+ #if daemon is True:
+ #opts.append('--daemon')
+
+ logger.debug('vpn options: %s', ' '.join(opts))
+ return opts
+
+
+def build_ovpn_command(debug=False, do_pkexec_check=True, vpnbin=None,
+ socket_path=None, **kwargs):
+ """
+ build a string with the
+ complete openvpn invocation
+
+ @rtype [string, [list of strings]]
+ @rparam: a list containing the command string
+ and a list of options.
+ """
+ command = []
+ use_pkexec = True
+ ovpn = None
+
+ # XXX get use_pkexec from config instead.
+
+ if _platform == "Linux" and use_pkexec and do_pkexec_check:
+
+ # check for both pkexec
+ # AND a suitable authentication
+ # agent running.
+ logger.info('use_pkexec set to True')
+
+ if not is_pkexec_in_system():
+ logger.error('no pkexec in system')
+ raise eip_exceptions.EIPNoPkexecAvailable
+
+ if not is_auth_agent_running():
+ logger.warning(
+ "no polkit auth agent found. "
+ "pkexec will use its own text "
+ "based authentication agent. "
+ "that's probably a bad idea")
+ raise eip_exceptions.EIPNoPolkitAuthAgentAvailable
+
+ command.append('pkexec')
+
+ if vpnbin is None:
+ if _platform == "Darwin":
+ # XXX Should hardcode our installed path
+ # /Applications/LEAPClient.app/Contents/Resources/openvpn.leap
+ openvpn_bin = "openvpn.leap"
+ else:
+ openvpn_bin = "openvpn"
+ #XXX hardcode for darwin
+ ovpn = which(openvpn_bin)
+ else:
+ ovpn = vpnbin
+ if ovpn:
+ vpn_command = ovpn
+ else:
+ vpn_command = "openvpn"
+ command.append(vpn_command)
+ daemon_mode = not debug
+
+ for opt in build_ovpn_options(daemon=daemon_mode, socket_path=socket_path,
+ **kwargs):
+ command.append(opt)
+
+ # XXX check len and raise proper error
+
+ if _platform == "Darwin":
+ OSX_ASADMIN = 'do shell script "%s" with administrator privileges'
+ # XXX fix workaround for Nones
+ _command = [x if x else " " for x in command]
+ # XXX debugging!
+ # XXX get openvpn log path from debug flags
+ _command.append('--log')
+ _command.append('/tmp/leap_openvpn.log')
+ return ["osascript", ["-e", OSX_ASADMIN % ' '.join(_command)]]
+ else:
+ return [command[0], command[1:]]
+
+
+def check_vpn_keys(provider=None):
+ """
+ performs an existance and permission check
+ over the openvpn keys file.
+ Currently we're expecting a single file
+ per provider, containing the CA cert,
+ the provider key, and our client certificate
+ """
+ assert provider is not None
+ provider_ca = eipspecs.provider_ca_path(provider)
+ client_cert = eipspecs.client_cert_path(provider)
+
+ logger.debug('provider ca = %s', provider_ca)
+ logger.debug('client cert = %s', client_cert)
+
+ # if no keys, raise error.
+ # it's catched by the ui and signal user.
+
+ if not os.path.isfile(provider_ca):
+ # not there. let's try to copy.
+ folder, filename = os.path.split(provider_ca)
+ if not os.path.isdir(folder):
+ mkdir_p(folder)
+ if provider_ca_file:
+ cacert = certs.where(provider_ca_file)
+ with open(provider_ca, 'w') as pca:
+ with open(cacert, 'r') as cac:
+ pca.write(cac.read())
+
+ if not os.path.isfile(provider_ca):
+ logger.error('key file %s not found. aborting.',
+ provider_ca)
+ raise eip_exceptions.EIPInitNoKeyFileError
+
+ if not os.path.isfile(client_cert):
+ logger.error('key file %s not found. aborting.',
+ client_cert)
+ raise eip_exceptions.EIPInitNoKeyFileError
+
+ for keyfile in (provider_ca, client_cert):
+ # bad perms? try to fix them
+ try:
+ check_and_fix_urw_only(keyfile)
+ except OSError:
+ raise eip_exceptions.EIPInitBadKeyFilePermError
diff --git a/src/leap/eip/constants.py b/src/leap/eip/constants.py
new file mode 100644
index 00000000..9af5a947
--- /dev/null
+++ b/src/leap/eip/constants.py
@@ -0,0 +1,3 @@
+# not used anymore with the new JSONConfig.slug
+EIP_CONFIG = "eip.json"
+EIP_SERVICE_EXPECTED_PATH = "1/config/eip-service.json"
diff --git a/src/leap/eip/eipconnection.py b/src/leap/eip/eipconnection.py
new file mode 100644
index 00000000..d012c567
--- /dev/null
+++ b/src/leap/eip/eipconnection.py
@@ -0,0 +1,405 @@
+"""
+EIP Connection Class
+"""
+from __future__ import (absolute_import,)
+import logging
+import Queue
+import sys
+import time
+
+from dateutil.parser import parse as dateparse
+
+from leap.eip.checks import ProviderCertChecker
+from leap.eip.checks import EIPConfigChecker
+from leap.eip import config as eipconfig
+from leap.eip import exceptions as eip_exceptions
+from leap.eip.openvpnconnection import OpenVPNConnection
+
+logger = logging.getLogger(name=__name__)
+
+
+class StatusMixIn(object):
+
+ # a bunch of methods related with querying the connection
+ # state/status and displaying useful info.
+ # Needs to get clear on what is what, and
+ # separate functions.
+ # Should separate EIPConnectionStatus (self.status)
+ # from the OpenVPN state/status command and parsing.
+
+ ERR_CONNREFUSED = False
+
+ def connection_state(self):
+ """
+ returns the current connection state
+ """
+ return self.status.current
+
+ def get_icon_name(self):
+ """
+ get icon name from status object
+ """
+ return self.status.get_state_icon()
+
+ def get_leap_status(self):
+ return self.status.get_leap_status()
+
+ def poll_connection_state(self):
+ """
+ """
+ try:
+ state = self.get_connection_state()
+ except eip_exceptions.ConnectionRefusedError:
+ # connection refused. might be not ready yet.
+ if not self.ERR_CONNREFUSED:
+ logger.warning('connection refused')
+ self.ERR_CONNREFUSED = True
+ return
+ if not state:
+ #logger.debug('no state')
+ return
+ (ts, status_step,
+ ok, ip, remote) = state
+ self.status.set_vpn_state(status_step)
+ status_step = self.status.get_readable_status()
+ return (ts, status_step, ok, ip, remote)
+
+ def make_error(self):
+ """
+ capture error and wrap it in an
+ understandable format
+ """
+ # mostly a hack to display errors in the debug UI
+ # w/o breaking the polling.
+ #XXX get helpful error codes
+ self.with_errors = True
+ now = int(time.time())
+ return '%s,LAUNCHER ERROR,ERROR,-,-' % now
+
+ def state(self):
+ """
+ Sends OpenVPN command: state
+ """
+ state = self._send_command("state")
+ if not state:
+ return None
+ if isinstance(state, str):
+ return state
+ if isinstance(state, list):
+ if len(state) == 1:
+ return state[0]
+ else:
+ return state[-1]
+
+ def vpn_status(self):
+ """
+ OpenVPN command: status
+ """
+ status = self._send_command("status")
+ return status
+
+ def vpn_status2(self):
+ """
+ OpenVPN command: last 2 statuses
+ """
+ return self._send_command("status 2")
+
+ #
+ # parse info as the UI expects
+ #
+
+ def get_status_io(self):
+ status = self.vpn_status()
+ if isinstance(status, str):
+ lines = status.split('\n')
+ if isinstance(status, list):
+ lines = status
+ try:
+ (header, when, tun_read, tun_write,
+ tcp_read, tcp_write, auth_read) = tuple(lines)
+ except ValueError:
+ return None
+
+ when_ts = dateparse(when.split(',')[1]).timetuple()
+ sep = ','
+ # XXX clean up this!
+ tun_read = tun_read.split(sep)[1]
+ tun_write = tun_write.split(sep)[1]
+ tcp_read = tcp_read.split(sep)[1]
+ tcp_write = tcp_write.split(sep)[1]
+ auth_read = auth_read.split(sep)[1]
+
+ # XXX this could be a named tuple. prettier.
+ return when_ts, (tun_read, tun_write, tcp_read, tcp_write, auth_read)
+
+ def get_connection_state(self):
+ state = self.state()
+ if state is not None:
+ ts, status_step, ok, ip, remote = state.split(',')
+ ts = time.gmtime(float(ts))
+ # XXX this could be a named tuple. prettier.
+ return ts, status_step, ok, ip, remote
+
+
+class EIPConnection(OpenVPNConnection, StatusMixIn):
+ """
+ Aka conductor.
+ Manages the execution of the OpenVPN process, auto starts, monitors the
+ network connection, handles configuration, fixes leaky hosts, handles
+ errors, etc.
+ Status updates (connected, bandwidth, etc) are signaled to the GUI.
+ """
+
+ # XXX change name to EIPConductor ??
+
+ def __init__(self,
+ provider_cert_checker=ProviderCertChecker,
+ config_checker=EIPConfigChecker,
+ *args, **kwargs):
+ #self.settingsfile = kwargs.get('settingsfile', None)
+ #self.logfile = kwargs.get('logfile', None)
+ self.provider = kwargs.pop('provider', None)
+ self._providercertchecker = provider_cert_checker
+ self._configchecker = config_checker
+
+ self.error_queue = Queue.Queue()
+
+ status_signals = kwargs.pop('status_signals', None)
+ self.status = EIPConnectionStatus(callbacks=status_signals)
+
+ checker_signals = kwargs.pop('checker_signals', None)
+ self.checker_signals = checker_signals
+
+ self.init_checkers()
+
+ host = eipconfig.get_socket_path()
+ kwargs['host'] = host
+
+ super(EIPConnection, self).__init__(*args, **kwargs)
+
+ def connect(self, **kwargs):
+ """
+ entry point for connection process
+ """
+ # in OpenVPNConnection
+ self.try_openvpn_connection()
+
+ def disconnect(self, shutdown=False):
+ """
+ disconnects client
+ """
+ self.terminate_openvpn_connection(shutdown=shutdown)
+ self.status.change_to(self.status.DISCONNECTED)
+
+ def has_errors(self):
+ return True if self.error_queue.qsize() != 0 else False
+
+ def init_checkers(self):
+ """
+ initialize checkers
+ """
+ self.provider_cert_checker = self._providercertchecker(
+ domain=self.provider)
+ self.config_checker = self._configchecker(domain=self.provider)
+
+ def set_provider_domain(self, domain):
+ """
+ sets the provider domain.
+ used from the first run wizard when we launch the run_checks
+ and connect process after having initialized the conductor.
+ """
+ # This looks convoluted, right.
+ # We have to reinstantiate checkers cause we're passing
+ # the domain param that we did not know at the beginning
+ # (only for the firstrunwizard case)
+ self.provider = domain
+ self.init_checkers()
+
+ def run_checks(self, skip_download=False, skip_verify=False):
+ """
+ run all eip checks previous to attempting a connection
+ """
+ logger.debug('running conductor checks')
+
+ def push_err(exc):
+ # keep the original traceback!
+ exc_traceback = sys.exc_info()[2]
+ self.error_queue.put((exc, exc_traceback))
+
+ try:
+ # network (1)
+ if self.checker_signals:
+ for signal in self.checker_signals:
+ signal('checking encryption keys')
+ self.provider_cert_checker.run_all(skip_verify=skip_verify)
+ except Exception as exc:
+ push_err(exc)
+ try:
+ if self.checker_signals:
+ for signal in self.checker_signals:
+ signal('checking provider config')
+ self.config_checker.run_all(skip_download=skip_download)
+ except Exception as exc:
+ push_err(exc)
+ try:
+ self.run_openvpn_checks()
+ except Exception as exc:
+ push_err(exc)
+
+
+class EIPConnectionStatus(object):
+ """
+ Keep track of client (gui) and openvpn
+ states.
+
+ These are the OpenVPN states:
+ CONNECTING -- OpenVPN's initial state.
+ WAIT -- (Client only) Waiting for initial response
+ from server.
+ AUTH -- (Client only) Authenticating with server.
+ GET_CONFIG -- (Client only) Downloading configuration options
+ from server.
+ ASSIGN_IP -- Assigning IP address to virtual network
+ interface.
+ ADD_ROUTES -- Adding routes to system.
+ CONNECTED -- Initialization Sequence Completed.
+ RECONNECTING -- A restart has occurred.
+ EXITING -- A graceful exit is in progress.
+
+ We add some extra states:
+
+ DISCONNECTED -- GUI initial state.
+ UNRECOVERABLE -- An unrecoverable error has been raised
+ while invoking openvpn service.
+ """
+ CONNECTING = 1
+ WAIT = 2
+ AUTH = 3
+ GET_CONFIG = 4
+ ASSIGN_IP = 5
+ ADD_ROUTES = 6
+ CONNECTED = 7
+ RECONNECTING = 8
+ EXITING = 9
+
+ # gui specific states:
+ UNRECOVERABLE = 11
+ DISCONNECTED = 0
+
+ def __init__(self, callbacks=None):
+ """
+ EIPConnectionStatus is initialized with a tuple
+ of signals to be triggered.
+ :param callbacks: a tuple of (callable) observers
+ :type callbacks: tuple
+ """
+ self.current = self.DISCONNECTED
+ self.previous = None
+ # (callbacks to connect to signals in Qt-land)
+ self.callbacks = callbacks
+
+ def get_readable_status(self):
+ # XXX DRY status / labels a little bit.
+ # think we'll want to i18n this.
+ human_status = {
+ 0: 'disconnected',
+ 1: 'connecting',
+ 2: 'waiting',
+ 3: 'authenticating',
+ 4: 'getting config',
+ 5: 'assigning ip',
+ 6: 'adding routes',
+ 7: 'connected',
+ 8: 'reconnecting',
+ 9: 'exiting',
+ 11: 'unrecoverable error',
+ }
+ return human_status[self.current]
+
+ def get_leap_status(self):
+ # XXX improve nomenclature
+ leap_status = {
+ 0: 'disconnected',
+ 1: 'connecting to gateway',
+ 2: 'connecting to gateway',
+ 3: 'authenticating',
+ 4: 'establishing network encryption',
+ 5: 'establishing network encryption',
+ 6: 'establishing network encryption',
+ 7: 'connected',
+ 8: 'reconnecting',
+ 9: 'exiting',
+ 11: 'unrecoverable error',
+ }
+ return leap_status[self.current]
+
+ def get_state_icon(self):
+ """
+ returns the high level icon
+ for each fine-grain openvpn state
+ """
+ connecting = (self.CONNECTING,
+ self.WAIT,
+ self.AUTH,
+ self.GET_CONFIG,
+ self.ASSIGN_IP,
+ self.ADD_ROUTES)
+ connected = (self.CONNECTED,)
+ disconnected = (self.DISCONNECTED,
+ self.UNRECOVERABLE)
+
+ # this can be made smarter,
+ # but it's like it'll change,
+ # so +readability.
+
+ if self.current in connecting:
+ return "connecting"
+ if self.current in connected:
+ return "connected"
+ if self.current in disconnected:
+ return "disconnected"
+
+ def set_vpn_state(self, status):
+ """
+ accepts a state string from the management
+ interface, and sets the internal state.
+ :param status: openvpn STATE (uppercase).
+ :type status: str
+ """
+ if hasattr(self, status):
+ self.change_to(getattr(self, status))
+
+ def set_current(self, to):
+ """
+ setter for the 'current' property
+ :param to: destination state
+ :type to: int
+ """
+ self.current = to
+
+ def change_to(self, to):
+ """
+ :param to: destination state
+ :type to: int
+ """
+ if to == self.current:
+ return
+ changed = False
+ from_ = self.current
+ self.current = to
+
+ # We can add transition restrictions
+ # here to ensure no transitions are
+ # allowed outside the fsm.
+
+ self.set_current(to)
+ changed = True
+
+ #trigger signals (as callbacks)
+ #print('current state: %s' % self.current)
+ if changed:
+ self.previous = from_
+ if self.callbacks:
+ for cb in self.callbacks:
+ if callable(cb):
+ cb(self)
diff --git a/src/leap/eip/exceptions.py b/src/leap/eip/exceptions.py
new file mode 100644
index 00000000..b7d398c3
--- /dev/null
+++ b/src/leap/eip/exceptions.py
@@ -0,0 +1,175 @@
+"""
+Generic error hierarchy
+Leap/EIP exceptions used for exception handling,
+logging, and notifying user of errors
+during leap operation.
+
+Exception hierarchy
+-------------------
+All EIP Errors must inherit from EIPClientError (note: move that to
+a more generic LEAPClientBaseError).
+
+Exception attributes and their meaning/uses
+-------------------------------------------
+
+* critical: if True, will abort execution prematurely,
+ after attempting any cleaning
+ action.
+
+* failfirst: breaks any error_check loop that is examining
+ the error queue.
+
+* message: the message that will be used in the __repr__ of the exception.
+
+* usermessage: the message that will be passed to user in ErrorDialogs
+ in Qt-land.
+
+TODO:
+
+* EIPClientError:
+ Should inherit from LeapException
+
+* gettext / i18n for user messages.
+
+"""
+from leap.base.exceptions import LeapException
+from leap.util.translations import translate
+
+
+# This should inherit from LeapException
+class EIPClientError(Exception):
+ """
+ base EIPClient exception
+ """
+ critical = False
+ failfirst = False
+ warning = False
+
+
+class CriticalError(EIPClientError):
+ """
+ we cannot do anything about it, sorry
+ """
+ critical = True
+ failfirst = True
+
+
+class Warning(EIPClientError):
+ """
+ just that, warnings
+ """
+ warning = True
+
+
+class EIPNoPolkitAuthAgentAvailable(CriticalError):
+ message = "No polkit authentication agent could be found"
+ usermessage = translate(
+ "EIPErrors",
+ "We could not find any authentication "
+ "agent in your system.<br/>"
+ "Make sure you have "
+ "<b>polkit-gnome-authentication-agent-1</b> "
+ "running and try again.")
+
+
+class EIPNoPkexecAvailable(Warning):
+ message = "No pkexec binary found"
+ usermessage = translate(
+ "EIPErrors",
+ "We could not find <b>pkexec</b> in your "
+ "system.<br/> Do you want to try "
+ "<b>setuid workaround</b>? "
+ "(<i>DOES NOTHING YET</i>)")
+ failfirst = True
+
+
+class EIPNoCommandError(EIPClientError):
+ message = "no suitable openvpn command found"
+ usermessage = translate(
+ "EIPErrors",
+ "No suitable openvpn command found. "
+ "<br/>(Might be a permissions problem)")
+
+
+class EIPBadCertError(Warning):
+ # XXX this should be critical and fail close
+ message = "cert verification failed"
+ usermessage = translate(
+ "EIPErrors",
+ "there is a problem with provider certificate")
+
+
+class LeapBadConfigFetchedError(Warning):
+ message = "provider sent a malformed json file"
+ usermessage = translate(
+ "EIPErrors",
+ "an error occurred during configuratio of leap services")
+
+
+class OpenVPNAlreadyRunning(CriticalError):
+ message = "Another OpenVPN Process is already running."
+ usermessage = translate(
+ "EIPErrors",
+ "Another OpenVPN Process has been detected. "
+ "Please close it before starting leap-client")
+
+
+class HttpsNotSupported(LeapException):
+ message = "connection refused while accessing via https"
+ usermessage = translate(
+ "EIPErrors",
+ "Server does not allow secure connections")
+
+
+class HttpsBadCertError(LeapException):
+ message = "verification error on cert"
+ usermessage = translate(
+ "EIPErrors",
+ "Server certificate could not be verified")
+
+#
+# errors still needing some love
+#
+
+
+class EIPInitNoKeyFileError(CriticalError):
+ message = "No vpn keys found in the expected path"
+ usermessage = translate(
+ "EIPErrors",
+ "We could not find your eip certs in the expected path")
+
+
+class EIPInitBadKeyFilePermError(Warning):
+ # I don't know if we should be telling user or not,
+ # we try to fix permissions and should only re-raise
+ # if permission check failed.
+ pass
+
+
+class EIPInitNoProviderError(EIPClientError):
+ pass
+
+
+class EIPInitBadProviderError(EIPClientError):
+ pass
+
+
+class EIPConfigurationError(EIPClientError):
+ pass
+
+#
+# Errors that probably we don't need anymore
+# chase down for them and check.
+#
+
+
+class MissingSocketError(Exception):
+ pass
+
+
+class ConnectionRefusedError(Exception):
+ pass
+
+
+class EIPMissingDefaultProvider(Exception):
+ pass
diff --git a/src/leap/eip/openvpnconnection.py b/src/leap/eip/openvpnconnection.py
new file mode 100644
index 00000000..bee8c010
--- /dev/null
+++ b/src/leap/eip/openvpnconnection.py
@@ -0,0 +1,410 @@
+"""
+OpenVPN Connection
+"""
+from __future__ import (print_function)
+from functools import partial
+import logging
+import os
+import psutil
+import shutil
+import select
+import socket
+from time import sleep
+
+logger = logging.getLogger(name=__name__)
+
+from leap.base.connection import Connection
+from leap.base.constants import OPENVPN_BIN
+from leap.util.coroutines import spawn_and_watch_process
+from leap.util.misc import get_openvpn_pids
+
+from leap.eip.udstelnet import UDSTelnet
+from leap.eip import config as eip_config
+from leap.eip import exceptions as eip_exceptions
+
+
+class OpenVPNManagement(object):
+
+ # TODO explain a little bit how management interface works
+ # and our telnet interface with support for unix sockets.
+
+ """
+ for more information, read openvpn management notes.
+ zcat `dpkg -L openvpn | grep management`
+ """
+
+ def _connect_to_management(self):
+ """
+ Connect to openvpn management interface
+ """
+ if hasattr(self, 'tn'):
+ self._close_management_socket()
+ self.tn = UDSTelnet(self.host, self.port)
+
+ # XXX make password optional
+ # specially for win. we should generate
+ # the pass on the fly when invoking manager
+ # from conductor
+
+ #self.tn.read_until('ENTER PASSWORD:', 2)
+ #self.tn.write(self.password + '\n')
+ #self.tn.read_until('SUCCESS:', 2)
+ if self.tn:
+ self._seek_to_eof()
+ return True
+
+ def _close_management_socket(self, announce=True):
+ """
+ Close connection to openvpn management interface
+ """
+ logger.debug('closing socket')
+ if announce:
+ self.tn.write("quit\n")
+ self.tn.read_all()
+ self.tn.get_socket().close()
+ del self.tn
+
+ def _seek_to_eof(self):
+ """
+ Read as much as available. Position seek pointer to end of stream
+ """
+ try:
+ b = self.tn.read_eager()
+ except EOFError:
+ logger.debug("Could not read from socket. Assuming it died.")
+ return
+ while b:
+ try:
+ b = self.tn.read_eager()
+ except EOFError:
+ logger.debug("Could not read from socket. Assuming it died.")
+
+ def _send_command(self, cmd):
+ """
+ Send a command to openvpn and return response as list
+ """
+ if not self.connected():
+ try:
+ self._connect_to_management()
+ except eip_exceptions.MissingSocketError:
+ #logger.warning('missing management socket')
+ return []
+ try:
+ if hasattr(self, 'tn'):
+ self.tn.write(cmd + "\n")
+ except socket.error:
+ logger.error('socket error')
+ self._close_management_socket(announce=False)
+ return []
+ try:
+ buf = self.tn.read_until(b"END", 2)
+ self._seek_to_eof()
+ blist = buf.split('\r\n')
+ if blist[-1].startswith('END'):
+ del blist[-1]
+ return blist
+ else:
+ return []
+ except socket.error as exc:
+ logger.debug('socket error: %s' % exc.message)
+ except select.error as exc:
+ logger.debug('select error: %s' % exc.message)
+
+ def _send_short_command(self, cmd):
+ """
+ parse output from commands that are
+ delimited by "success" instead
+ """
+ if not self.connected():
+ self.connect()
+ self.tn.write(cmd + "\n")
+ # XXX not working?
+ buf = self.tn.read_until(b"SUCCESS", 2)
+ self._seek_to_eof()
+ blist = buf.split('\r\n')
+ return blist
+
+ #
+ # random maybe useful vpn commands
+ #
+
+ def pid(self):
+ #XXX broken
+ return self._send_short_command("pid")
+
+
+class OpenVPNConnection(Connection, OpenVPNManagement):
+ """
+ All related to invocation
+ of the openvpn binary.
+ It's extended by EIPConnection.
+ """
+
+ # XXX Inheriting from Connection was an early design idea
+ # but currently that's an empty class.
+ # We can get rid of that if we don't use it for sharing
+ # state with other leap modules.
+
+ def __init__(self,
+ watcher_cb=None,
+ debug=False,
+ host=None,
+ port="unix",
+ password=None,
+ *args, **kwargs):
+ """
+ :param watcher_cb: callback to be \
+called for each line in watched stdout
+ :param signal_map: dictionary of signal names and callables \
+to be triggered for each one of them.
+ :type watcher_cb: function
+ :type signal_map: dict
+ """
+ #XXX FIXME
+ #change watcher_cb to line_observer
+ # XXX if not host: raise ImproperlyConfigured
+
+ logger.debug('init openvpn connection')
+ self.debug = debug
+ self.ovpn_verbosity = kwargs.get('ovpn_verbosity', None)
+
+ self.watcher_cb = watcher_cb
+ #self.signal_maps = signal_maps
+
+ self.subp = None
+ self.watcher = None
+
+ self.server = None
+ self.port = None
+ self.proto = None
+
+ self.command = None
+ self.args = None
+
+ # XXX get autostart from config
+ self.autostart = True
+
+ # management interface init
+ self.host = host
+ if isinstance(port, str) and port.isdigit():
+ port = int(port)
+ elif port == "unix":
+ port = "unix"
+ else:
+ port = None
+ self.port = port
+ self.password = password
+
+ def run_openvpn_checks(self):
+ """
+ runs check needed before launching
+ openvpn subprocess. will raise if errors found.
+ """
+ logger.debug('running openvpn checks')
+ # XXX I think that "check_if_running" should be called
+ # from try openvpn connection instead. -- kali.
+ # let's prepare tests for that before changing it...
+ self._check_if_running_instance()
+ self._set_ovpn_command()
+ self._check_vpn_keys()
+
+ def try_openvpn_connection(self):
+ """
+ attempts to connect
+ """
+ # XXX should make public method
+ if self.command is None:
+ raise eip_exceptions.EIPNoCommandError
+ if self.subp is not None:
+ logger.debug('cowardly refusing to launch subprocess again')
+ # XXX this is not returning ???!!
+ # FIXME -- so it's calling it all the same!!
+
+ self._launch_openvpn()
+
+ def connected(self):
+ """
+ Returns True if connected
+ rtype: bool
+ """
+ # XXX make a property
+ return hasattr(self, 'tn')
+
+ def terminate_openvpn_connection(self, shutdown=False):
+ """
+ terminates openvpn child subprocess
+ """
+ if self.subp:
+ try:
+ self._stop_openvpn()
+ except eip_exceptions.ConnectionRefusedError:
+ logger.warning(
+ 'unable to send sigterm signal to openvpn: '
+ 'connection refused.')
+
+ # XXX kali --
+ # XXX review-me
+ # I think this will block if child process
+ # does not return.
+ # Maybe we can .poll() for a given
+ # interval and exit in any case.
+
+ RETCODE = self.subp.wait()
+ if RETCODE:
+ logger.error(
+ 'cannot terminate subprocess! Retcode %s'
+ '(We might have left openvpn running)' % RETCODE)
+
+ if shutdown:
+ self._cleanup_tempfiles()
+
+ def _cleanup_tempfiles(self):
+ """
+ remove all temporal files
+ we might have left behind
+ """
+ # if self.port is 'unix', we have
+ # created a temporal socket path that, under
+ # normal circumstances, we should be able to
+ # delete
+
+ if self.port == "unix":
+ logger.debug('cleaning socket file temp folder')
+
+ tempfolder = os.path.split(self.host)[0]
+ if os.path.isdir(tempfolder):
+ try:
+ shutil.rmtree(tempfolder)
+ except OSError:
+ logger.error('could not delete tmpfolder %s' % tempfolder)
+
+ # checks
+
+ def _check_if_running_instance(self):
+ """
+ check if openvpn is already running
+ """
+ openvpn_pids = get_openvpn_pids()
+ if openvpn_pids:
+ logger.debug('an openvpn instance is already running.')
+ logger.debug('attempting to stop openvpn instance.')
+ if not self._stop_openvpn():
+ raise eip_exceptions.OpenVPNAlreadyRunning
+ return
+ else:
+ logger.debug('no openvpn instance found.')
+
+ def _set_ovpn_command(self):
+ try:
+ command, args = eip_config.build_ovpn_command(
+ provider=self.provider,
+ debug=self.debug,
+ socket_path=self.host,
+ ovpn_verbosity=self.ovpn_verbosity)
+ except eip_exceptions.EIPNoPolkitAuthAgentAvailable:
+ command = args = None
+ raise
+ except eip_exceptions.EIPNoPkexecAvailable:
+ command = args = None
+ raise
+
+ # XXX if not command, signal error.
+ self.command = command
+ self.args = args
+
+ def _check_vpn_keys(self):
+ """
+ checks for correct permissions on vpn keys
+ """
+ try:
+ eip_config.check_vpn_keys(provider=self.provider)
+ except eip_exceptions.EIPInitBadKeyFilePermError:
+ logger.error('Bad VPN Keys permission!')
+ # do nothing now
+ # and raise the rest ...
+
+ # starting and stopping openvpn subprocess
+
+ def _launch_openvpn(self):
+ """
+ invocation of openvpn binaries in a subprocess.
+ """
+ #XXX TODO:
+ #deprecate watcher_cb,
+ #use _only_ signal_maps instead
+
+ #logger.debug('_launch_openvpn called')
+ if self.watcher_cb is not None:
+ linewrite_callback = self.watcher_cb
+ else:
+ #XXX get logger instead
+ linewrite_callback = lambda line: logger.debug(
+ 'watcher: %s' % line)
+
+ # the partial is not
+ # being applied now because we're not observing the process
+ # stdout like we did in the early stages. but I leave it
+ # here since it will be handy for observing patterns in the
+ # thru-the-manager updates (with regex)
+ observers = (linewrite_callback,
+ partial(lambda con_status,
+ line: linewrite_callback, self.status))
+ subp, watcher = spawn_and_watch_process(
+ self.command,
+ self.args,
+ observers=observers)
+ self.subp = subp
+ self.watcher = watcher
+
+ def _stop_openvpn(self):
+ """
+ stop openvpn process
+ by sending SIGTERM to the management
+ interface
+ """
+ # XXX method a bit too long, split
+ logger.debug("atempting to terminate openvpn process...")
+ if self.connected():
+ try:
+ self._send_command("signal SIGTERM\n")
+ sleep(1)
+ if not self.subp: # XXX ???
+ return True
+ except socket.error:
+ logger.warning('management socket died')
+ return
+
+ #shutting openvpn failured
+ #try patching in old openvpn host and trying again
+ # XXX could be more than one!
+ process = self._get_openvpn_process()
+ if process:
+ logger.debug('process: %s' % process.name)
+ cmdline = process.cmdline
+
+ manag_flag = "--management"
+ if isinstance(cmdline, list) and manag_flag in cmdline:
+ _index = cmdline.index(manag_flag)
+ self.host = cmdline[_index + 1]
+ self._send_command("signal SIGTERM\n")
+
+ #make sure the process was terminated
+ process = self._get_openvpn_process()
+ if not process:
+ logger.debug("Existing OpenVPN Process Terminated")
+ return True
+ else:
+ logger.error("Unable to terminate existing OpenVPN Process.")
+ return False
+
+ return True
+
+ def _get_openvpn_process(self):
+ for process in psutil.process_iter():
+ if OPENVPN_BIN in process.name:
+ return process
+ return None
+
+ def get_log(self, lines=1):
+ log = self._send_command("log %s" % lines)
+ return log
diff --git a/src/leap/eip/specs.py b/src/leap/eip/specs.py
new file mode 100644
index 00000000..c41fd29b
--- /dev/null
+++ b/src/leap/eip/specs.py
@@ -0,0 +1,136 @@
+from __future__ import (unicode_literals)
+import os
+
+from leap import __branding
+from leap.base import config as baseconfig
+
+# XXX move provider stuff to base config
+
+PROVIDER_CA_CERT = __branding.get(
+ 'provider_ca_file',
+ 'cacert.pem')
+
+provider_ca_path = lambda domain: str(os.path.join(
+ #baseconfig.get_default_provider_path(),
+ baseconfig.get_provider_path(domain),
+ 'keys', 'ca',
+ 'cacert.pem'
+)) if domain else None
+
+default_provider_ca_path = lambda: str(os.path.join(
+ baseconfig.get_default_provider_path(),
+ 'keys', 'ca',
+ PROVIDER_CA_CERT
+))
+
+PROVIDER_DOMAIN = __branding.get('provider_domain', 'testprovider.example.org')
+
+
+client_cert_path = lambda domain: unicode(os.path.join(
+ baseconfig.get_provider_path(domain),
+ 'keys', 'client',
+ 'openvpn.pem'
+)) if domain else None
+
+default_client_cert_path = lambda: unicode(os.path.join(
+ baseconfig.get_default_provider_path(),
+ 'keys', 'client',
+ 'openvpn.pem'
+))
+
+eipconfig_spec = {
+ 'description': 'sample eipconfig',
+ 'type': 'object',
+ 'properties': {
+ 'provider': {
+ 'type': unicode,
+ 'default': u"%s" % PROVIDER_DOMAIN,
+ 'required': True,
+ },
+ 'transport': {
+ 'type': unicode,
+ 'default': u"openvpn",
+ },
+ 'openvpn_protocol': {
+ 'type': unicode,
+ 'default': u"tcp"
+ },
+ 'openvpn_port': {
+ 'type': int,
+ 'default': 80
+ },
+ 'openvpn_ca_certificate': {
+ 'type': unicode, # path
+ 'default': default_provider_ca_path
+ },
+ 'openvpn_client_certificate': {
+ 'type': unicode, # path
+ 'default': default_client_cert_path
+ },
+ 'connect_on_login': {
+ 'type': bool,
+ 'default': True
+ },
+ 'block_cleartext_traffic': {
+ 'type': bool,
+ 'default': True
+ },
+ 'primary_gateway': {
+ 'type': unicode,
+ 'default': u"location_unknown",
+ #'required': True
+ },
+ 'secondary_gateway': {
+ 'type': unicode,
+ 'default': u"location_unknown2"
+ },
+ 'management_password': {
+ 'type': unicode
+ }
+ }
+}
+
+eipservice_config_spec = {
+ 'description': 'sample eip service config',
+ 'type': 'object',
+ 'properties': {
+ 'serial': {
+ 'type': int,
+ 'required': True,
+ 'default': 1
+ },
+ 'version': {
+ 'type': int,
+ 'required': True,
+ 'default': 1
+ },
+ 'clusters': {
+ 'type': list,
+ 'default': [
+ {"label": {
+ "en": "Location Unknown"},
+ "name": "location_unknown"}]
+ },
+ 'gateways': {
+ 'type': list,
+ 'default': [
+ {"capabilities": {
+ "adblock": True,
+ "filter_dns": True,
+ "ports": ["80", "53", "443", "1194"],
+ "protocols": ["udp", "tcp"],
+ "transport": ["openvpn"],
+ "user_ips": False},
+ "cluster": "location_unknown",
+ "host": "location.example.org",
+ "ip_address": "127.0.0.1"}]
+ },
+ 'openvpn_configuration': {
+ 'type': dict,
+ 'default': {
+ "auth": None,
+ "cipher": None,
+ "tls-cipher": None}
+ }
+ }
+}
diff --git a/src/leap/eip/tests/__init__.py b/src/leap/eip/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/leap/eip/tests/__init__.py
diff --git a/src/leap/eip/tests/data.py b/src/leap/eip/tests/data.py
new file mode 100644
index 00000000..a7fe1853
--- /dev/null
+++ b/src/leap/eip/tests/data.py
@@ -0,0 +1,51 @@
+from __future__ import unicode_literals
+import os
+
+#from leap import __branding
+
+# sample data used in tests
+
+#PROVIDER = __branding.get('provider_domain')
+PROVIDER = "testprovider.example.org"
+
+EIP_SAMPLE_CONFIG = {
+ "provider": "%s" % PROVIDER,
+ "transport": "openvpn",
+ "openvpn_protocol": "tcp",
+ "openvpn_port": 80,
+ "openvpn_ca_certificate": os.path.expanduser(
+ "~/.config/leap/providers/"
+ "%s/"
+ "keys/ca/cacert.pem" % PROVIDER),
+ "openvpn_client_certificate": os.path.expanduser(
+ "~/.config/leap/providers/"
+ "%s/"
+ "keys/client/openvpn.pem" % PROVIDER),
+ "connect_on_login": True,
+ "block_cleartext_traffic": True,
+ "primary_gateway": "location_unknown",
+ "secondary_gateway": "location_unknown2",
+ #"management_password": "oph7Que1othahwiech6J"
+}
+
+EIP_SAMPLE_SERVICE = {
+ "serial": 1,
+ "version": 1,
+ "clusters": [
+ {"label": {
+ "en": "Location Unknown"},
+ "name": "location_unknown"}
+ ],
+ "gateways": [
+ {"capabilities": {
+ "adblock": True,
+ "filter_dns": True,
+ "ports": ["80", "53", "443", "1194"],
+ "protocols": ["udp", "tcp"],
+ "transport": ["openvpn"],
+ "user_ips": False},
+ "cluster": "location_unknown",
+ "host": "location.example.org",
+ "ip_address": "192.0.43.10"}
+ ]
+}
diff --git a/src/leap/eip/tests/test_checks.py b/src/leap/eip/tests/test_checks.py
new file mode 100644
index 00000000..f42a0eeb
--- /dev/null
+++ b/src/leap/eip/tests/test_checks.py
@@ -0,0 +1,372 @@
+from BaseHTTPServer import BaseHTTPRequestHandler
+import copy
+import json
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+import os
+import time
+import urlparse
+
+from mock import (patch, Mock)
+
+import requests
+
+from leap.base import config as baseconfig
+from leap.base import pluggableconfig
+from leap.base.constants import (DEFAULT_PROVIDER_DEFINITION,
+ DEFINITION_EXPECTED_PATH)
+from leap.eip import checks as eipchecks
+from leap.eip import specs as eipspecs
+from leap.eip import exceptions as eipexceptions
+from leap.eip.tests import data as testdata
+from leap.testing.basetest import BaseLeapTest
+from leap.testing.https_server import BaseHTTPSServerTestCase
+from leap.testing.https_server import where as where_cert
+from leap.util.fileutil import mkdir_f
+
+
+class NoLogRequestHandler:
+ def log_message(self, *args):
+ # don't write log msg to stderr
+ pass
+
+ def read(self, n=None):
+ return ''
+
+
+class EIPCheckTest(BaseLeapTest):
+
+ __name__ = "eip_check_tests"
+ provider = "testprovider.example.org"
+ maxDiff = None
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ # test methods are there, and can be called from run_all
+
+ def test_checker_should_implement_check_methods(self):
+ checker = eipchecks.EIPConfigChecker(domain=self.provider)
+
+ self.assertTrue(hasattr(checker, "check_default_eipconfig"),
+ "missing meth")
+ self.assertTrue(hasattr(checker, "check_is_there_default_provider"),
+ "missing meth")
+ self.assertTrue(hasattr(checker, "fetch_definition"), "missing meth")
+ self.assertTrue(hasattr(checker, "fetch_eip_service_config"),
+ "missing meth")
+ self.assertTrue(hasattr(checker, "check_complete_eip_config"),
+ "missing meth")
+
+ def test_checker_should_actually_call_all_tests(self):
+ checker = eipchecks.EIPConfigChecker(domain=self.provider)
+
+ mc = Mock()
+ checker.run_all(checker=mc)
+ self.assertTrue(mc.check_default_eipconfig.called, "not called")
+ self.assertTrue(mc.check_is_there_default_provider.called,
+ "not called")
+ self.assertTrue(mc.fetch_definition.called,
+ "not called")
+ self.assertTrue(mc.fetch_eip_service_config.called,
+ "not called")
+ self.assertTrue(mc.check_complete_eip_config.called,
+ "not called")
+
+ # test individual check methods
+
+ def test_check_default_eipconfig(self):
+ checker = eipchecks.EIPConfigChecker(domain=self.provider)
+ # no eip config (empty home)
+ eipconfig_path = checker.eipconfig.filename
+ self.assertFalse(os.path.isfile(eipconfig_path))
+ checker.check_default_eipconfig()
+ # we've written one, so it should be there.
+ self.assertTrue(os.path.isfile(eipconfig_path))
+ with open(eipconfig_path, 'rb') as fp:
+ deserialized = json.load(fp)
+
+ # force re-evaluation of the paths
+ # small workaround for evaluating home dirs correctly
+ EIP_SAMPLE_CONFIG = copy.copy(testdata.EIP_SAMPLE_CONFIG)
+ EIP_SAMPLE_CONFIG['openvpn_client_certificate'] = \
+ eipspecs.client_cert_path(self.provider)
+ EIP_SAMPLE_CONFIG['openvpn_ca_certificate'] = \
+ eipspecs.provider_ca_path(self.provider)
+ self.assertEqual(deserialized, EIP_SAMPLE_CONFIG)
+
+ # TODO: shold ALSO run validation methods.
+
+ def test_check_is_there_default_provider(self):
+ checker = eipchecks.EIPConfigChecker(domain=self.provider)
+ # we do dump a sample eip config, but lacking a
+ # default provider entry.
+ # This error will be possible catched in a different
+ # place, when JSONConfig does validation of required fields.
+
+ # passing direct config
+ with self.assertRaises(eipexceptions.EIPMissingDefaultProvider):
+ checker.check_is_there_default_provider(config={})
+
+ # ok. now, messing with real files...
+ # blank out default_provider
+ sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG)
+ sampleconfig['provider'] = None
+ eipcfg_path = checker.eipconfig.filename
+ mkdir_f(eipcfg_path)
+ with open(eipcfg_path, 'w') as fp:
+ json.dump(sampleconfig, fp)
+ #with self.assertRaises(eipexceptions.EIPMissingDefaultProvider):
+ # XXX we should catch this as one of our errors, but do not
+ # see how to do it quickly.
+ with self.assertRaises(pluggableconfig.ValidationError):
+ #import ipdb;ipdb.set_trace()
+ checker.eipconfig.load(fromfile=eipcfg_path)
+ checker.check_is_there_default_provider()
+
+ sampleconfig = testdata.EIP_SAMPLE_CONFIG
+ #eipcfg_path = checker._get_default_eipconfig_path()
+ with open(eipcfg_path, 'w') as fp:
+ json.dump(sampleconfig, fp)
+ checker.eipconfig.load()
+ self.assertTrue(checker.check_is_there_default_provider())
+
+ def test_fetch_definition(self):
+ with patch.object(requests, "get") as mocked_get:
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.headers = {
+ 'last-modified': "Wed Dec 12 12:12:12 GMT 2012"}
+ mocked_get.return_value.json = DEFAULT_PROVIDER_DEFINITION
+ checker = eipchecks.EIPConfigChecker(fetcher=requests)
+ sampleconfig = testdata.EIP_SAMPLE_CONFIG
+ checker.fetch_definition(config=sampleconfig)
+
+ fn = os.path.join(baseconfig.get_default_provider_path(),
+ DEFINITION_EXPECTED_PATH)
+ with open(fn, 'r') as fp:
+ deserialized = json.load(fp)
+ self.assertEqual(DEFAULT_PROVIDER_DEFINITION, deserialized)
+
+ # XXX TODO check for ConnectionError, HTTPError, InvalidUrl
+ # (and proper EIPExceptions are raised).
+ # Look at base.test_config.
+
+ def test_fetch_eip_service_config(self):
+ with patch.object(requests, "get") as mocked_get:
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.headers = {
+ 'last-modified': "Wed Dec 12 12:12:12 GMT 2012"}
+ mocked_get.return_value.json = testdata.EIP_SAMPLE_SERVICE
+ checker = eipchecks.EIPConfigChecker(fetcher=requests)
+ sampleconfig = testdata.EIP_SAMPLE_CONFIG
+ checker.fetch_eip_service_config(config=sampleconfig)
+
+ def test_check_complete_eip_config(self):
+ checker = eipchecks.EIPConfigChecker()
+ with self.assertRaises(eipexceptions.EIPConfigurationError):
+ sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG)
+ sampleconfig['provider'] = None
+ checker.check_complete_eip_config(config=sampleconfig)
+ with self.assertRaises(eipexceptions.EIPConfigurationError):
+ sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG)
+ del sampleconfig['provider']
+ checker.check_complete_eip_config(config=sampleconfig)
+
+ # normal case
+ sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG)
+ checker.check_complete_eip_config(config=sampleconfig)
+
+
+class ProviderCertCheckerTest(BaseLeapTest):
+
+ __name__ = "provider_cert_checker_tests"
+ provider = "testprovider.example.org"
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ # test methods are there, and can be called from run_all
+
+ def test_checker_should_implement_check_methods(self):
+ checker = eipchecks.ProviderCertChecker()
+
+ # For MVS+
+ self.assertTrue(hasattr(checker, "download_ca_cert"),
+ "missing meth")
+ self.assertTrue(hasattr(checker, "download_ca_signature"),
+ "missing meth")
+ self.assertTrue(hasattr(checker, "get_ca_signatures"), "missing meth")
+ self.assertTrue(hasattr(checker, "is_there_trust_path"),
+ "missing meth")
+
+ # For MVS
+ self.assertTrue(hasattr(checker, "is_there_provider_ca"),
+ "missing meth")
+ self.assertTrue(hasattr(checker, "is_https_working"), "missing meth")
+ self.assertTrue(hasattr(checker, "check_new_cert_needed"),
+ "missing meth")
+
+ def test_checker_should_actually_call_all_tests(self):
+ checker = eipchecks.ProviderCertChecker()
+
+ mc = Mock()
+ checker.run_all(checker=mc)
+ # XXX MVS+
+ #self.assertTrue(mc.download_ca_cert.called, "not called")
+ #self.assertTrue(mc.download_ca_signature.called, "not called")
+ #self.assertTrue(mc.get_ca_signatures.called, "not called")
+ #self.assertTrue(mc.is_there_trust_path.called, "not called")
+
+ # For MVS
+ self.assertTrue(mc.is_there_provider_ca.called, "not called")
+ self.assertTrue(mc.is_https_working.called,
+ "not called")
+ self.assertTrue(mc.check_new_cert_needed.called,
+ "not called")
+
+ # test individual check methods
+
+ @unittest.skip
+ def test_is_there_provider_ca(self):
+ # XXX commenting out this test.
+ # With the generic client this does not make sense,
+ # we should dump one there.
+ # or test conductor logic.
+ checker = eipchecks.ProviderCertChecker()
+ self.assertTrue(
+ checker.is_there_provider_ca())
+
+
+class ProviderCertCheckerHTTPSTests(BaseHTTPSServerTestCase, BaseLeapTest):
+ provider = "testprovider.example.org"
+
+ class request_handler(NoLogRequestHandler, BaseHTTPRequestHandler):
+ responses = {
+ '/': ['OK', ''],
+ '/client.cert': [
+ # XXX get sample cert
+ '-----BEGIN CERTIFICATE-----',
+ '-----END CERTIFICATE-----'],
+ '/badclient.cert': [
+ 'BADCERT']}
+
+ def do_GET(self):
+ path = urlparse.urlparse(self.path)
+ message = '\n'.join(self.responses.get(
+ path.path, None))
+ self.send_response(200)
+ self.end_headers()
+ self.wfile.write(message)
+
+ def test_is_https_working(self):
+ fetcher = requests
+ uri = "https://%s/" % (self.get_server())
+ # bare requests call. this should just pass (if there is
+ # an https service there).
+ fetcher.get(uri, verify=False)
+ checker = eipchecks.ProviderCertChecker(fetcher=fetcher)
+ self.assertTrue(checker.is_https_working(uri=uri, verify=False))
+
+ # for local debugs, when in doubt
+ #self.assertTrue(checker.is_https_working(uri="https://github.com",
+ #verify=True))
+
+ # for the two checks below, I know they fail because no ca
+ # cert is passed to them, and I know that's the error that
+ # requests return with our implementation.
+ # We're receiving this because our
+ # server is dying prematurely when the handshake is interrupted on the
+ # client side.
+ # Since we have access to the server, we could check that
+ # the error raised has been:
+ # SSL23_READ_BYTES: alert bad certificate
+ with self.assertRaises(requests.exceptions.SSLError) as exc:
+ fetcher.get(uri, verify=True)
+ self.assertTrue(
+ "SSL23_GET_SERVER_HELLO:unknown protocol" in exc.message)
+
+ # XXX FIXME! Uncomment after #638 is done
+ #with self.assertRaises(eipexceptions.EIPBadCertError) as exc:
+ #checker.is_https_working(uri=uri, verify=True)
+ #self.assertTrue(
+ #"cert verification failed" in exc.message)
+
+ # get cacert from testing.https_server
+ cacert = where_cert('cacert.pem')
+ fetcher.get(uri, verify=cacert)
+ self.assertTrue(checker.is_https_working(uri=uri, verify=cacert))
+
+ # same, but get cacert from leap.custom
+ # XXX TODO!
+
+ @unittest.skip
+ def test_download_new_client_cert(self):
+ # FIXME
+ # Magick srp decorator broken right now...
+ # Have to mock the decorator and inject something that
+ # can bypass the authentication
+
+ uri = "https://%s/client.cert" % (self.get_server())
+ cacert = where_cert('cacert.pem')
+ checker = eipchecks.ProviderCertChecker(domain=self.provider)
+ credentials = "testuser", "testpassword"
+ self.assertTrue(checker.download_new_client_cert(
+ credentials=credentials, uri=uri, verify=cacert))
+
+ # now download a malformed cert
+ uri = "https://%s/badclient.cert" % (self.get_server())
+ cacert = where_cert('cacert.pem')
+ checker = eipchecks.ProviderCertChecker()
+ with self.assertRaises(ValueError):
+ self.assertTrue(checker.download_new_client_cert(
+ credentials=credentials, uri=uri, verify=cacert))
+
+ # did we write cert to its path?
+ clientcertfile = eipspecs.client_cert_path()
+ self.assertTrue(os.path.isfile(clientcertfile))
+ certfile = eipspecs.client_cert_path()
+ with open(certfile, 'r') as cf:
+ certcontent = cf.read()
+ self.assertEqual(certcontent,
+ '\n'.join(
+ self.request_handler.responses['/client.cert']))
+ os.remove(clientcertfile)
+
+ def test_is_cert_valid(self):
+ checker = eipchecks.ProviderCertChecker()
+ # TODO: better exception catching
+ # should raise eipexceptions.BadClientCertificate, and give reasons
+ # on msg.
+ with self.assertRaises(Exception) as exc:
+ self.assertFalse(checker.is_cert_valid())
+ exc.message = "missing cert"
+
+ def test_bad_validity_certs(self):
+ checker = eipchecks.ProviderCertChecker()
+ certfile = where_cert('leaptestscert.pem')
+ self.assertFalse(checker.is_cert_not_expired(
+ certfile=certfile,
+ now=lambda: time.mktime((2038, 1, 1, 1, 1, 1, 1, 1, 1))))
+ self.assertFalse(checker.is_cert_not_expired(
+ certfile=certfile,
+ now=lambda: time.mktime((1970, 1, 1, 1, 1, 1, 1, 1, 1))))
+
+ def test_check_new_cert_needed(self):
+ # check: missing cert
+ checker = eipchecks.ProviderCertChecker(domain=self.provider)
+ self.assertTrue(checker.check_new_cert_needed(skip_download=True))
+ # TODO check: malformed cert
+ # TODO check: expired cert
+ # TODO check: pass test server uri instead of skip
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/leap/eip/tests/test_config.py b/src/leap/eip/tests/test_config.py
new file mode 100644
index 00000000..72ab3c8e
--- /dev/null
+++ b/src/leap/eip/tests/test_config.py
@@ -0,0 +1,298 @@
+from collections import OrderedDict
+import json
+import os
+import platform
+import stat
+
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+#from leap.base import constants
+#from leap.eip import config as eip_config
+#from leap import __branding as BRANDING
+from leap.eip import config as eipconfig
+from leap.eip.tests.data import EIP_SAMPLE_CONFIG, EIP_SAMPLE_SERVICE
+from leap.testing.basetest import BaseLeapTest
+from leap.util.fileutil import mkdir_p, mkdir_f
+
+_system = platform.system()
+
+#PROVIDER = BRANDING.get('provider_domain')
+#PROVIDER_SHORTNAME = BRANDING.get('short_name')
+
+
+class EIPConfigTest(BaseLeapTest):
+
+ __name__ = "eip_config_tests"
+ provider = "testprovider.example.org"
+
+ maxDiff = None
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ #
+ # helpers
+ #
+
+ def touch_exec(self):
+ path = os.path.join(
+ self.tempdir, 'bin')
+ mkdir_p(path)
+ tfile = os.path.join(
+ path,
+ 'openvpn')
+ open(tfile, 'wb').close()
+ os.chmod(tfile, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
+
+ def write_sample_eipservice(self, vpnciphers=False, extra_vpnopts=None,
+ gateways=None):
+ conf = eipconfig.EIPServiceConfig()
+ mkdir_f(conf.filename)
+ if gateways:
+ EIP_SAMPLE_SERVICE['gateways'] = gateways
+ if vpnciphers:
+ openvpnconfig = OrderedDict({
+ "auth": "SHA1",
+ "cipher": "AES-128-CBC",
+ "tls-cipher": "DHE-RSA-AES128-SHA"})
+ if extra_vpnopts:
+ for k, v in extra_vpnopts.items():
+ openvpnconfig[k] = v
+ EIP_SAMPLE_SERVICE['openvpn_configuration'] = openvpnconfig
+
+ with open(conf.filename, 'w') as fd:
+ fd.write(json.dumps(EIP_SAMPLE_SERVICE))
+
+ def write_sample_eipconfig(self):
+ conf = eipconfig.EIPConfig()
+ folder, f = os.path.split(conf.filename)
+ if not os.path.isdir(folder):
+ mkdir_p(folder)
+ with open(conf.filename, 'w') as fd:
+ fd.write(json.dumps(EIP_SAMPLE_CONFIG))
+
+ def get_expected_openvpn_args(self, with_openvpn_ciphers=False):
+ """
+ yeah, this is almost as duplicating the
+ code for building the command
+ """
+ args = []
+ eipconf = eipconfig.EIPConfig(domain=self.provider)
+ eipconf.load()
+ eipsconf = eipconfig.EIPServiceConfig(domain=self.provider)
+ eipsconf.load()
+
+ username = self.get_username()
+ groupname = self.get_groupname()
+
+ args.append('--client')
+ args.append('--dev')
+ #does this have to be tap for win??
+ args.append('tun')
+ args.append('--persist-tun')
+ args.append('--persist-key')
+ args.append('--remote')
+
+ args.append('%s' % eipconfig.get_eip_gateway(
+ eipconfig=eipconf,
+ eipserviceconfig=eipsconf))
+ # XXX get port!?
+ args.append('1194')
+ # XXX get proto
+ args.append('udp')
+ args.append('--tls-client')
+ args.append('--remote-cert-tls')
+ args.append('server')
+
+ if with_openvpn_ciphers:
+ CIPHERS = [
+ "--tls-cipher", "DHE-RSA-AES128-SHA",
+ "--cipher", "AES-128-CBC",
+ "--auth", "SHA1"]
+ for opt in CIPHERS:
+ args.append(opt)
+
+ args.append('--user')
+ args.append(username)
+ args.append('--group')
+ args.append(groupname)
+ args.append('--management-client-user')
+ args.append(username)
+ args.append('--management-signal')
+
+ args.append('--management')
+ #XXX hey!
+ #get platform switches here!
+ args.append('/tmp/test.socket')
+ args.append('unix')
+
+ args.append('--script-security')
+ args.append('2')
+
+ if _system == "Linux":
+ UPDOWN_SCRIPT = "/etc/leap/resolv-update"
+ if os.path.isfile(UPDOWN_SCRIPT):
+ args.append('--up')
+ args.append('/etc/leap/resolv-update')
+ args.append('--down')
+ args.append('/etc/leap/resolv-update')
+ args.append('--plugin')
+ args.append('/usr/lib/openvpn/openvpn-down-root.so')
+ args.append("'script_type=down /etc/leap/resolv-update'")
+
+ # certs
+ # XXX get values from specs?
+ args.append('--cert')
+ args.append(os.path.join(
+ self.home,
+ '.config', 'leap', 'providers',
+ '%s' % self.provider,
+ 'keys', 'client',
+ 'openvpn.pem'))
+ args.append('--key')
+ args.append(os.path.join(
+ self.home,
+ '.config', 'leap', 'providers',
+ '%s' % self.provider,
+ 'keys', 'client',
+ 'openvpn.pem'))
+ args.append('--ca')
+ args.append(os.path.join(
+ self.home,
+ '.config', 'leap', 'providers',
+ '%s' % self.provider,
+ 'keys', 'ca',
+ 'cacert.pem'))
+ return args
+
+ # build command string
+ # these tests are going to have to check
+ # many combinations. we should inject some
+ # params in the function call, to disable
+ # some checks.
+
+ def test_get_eip_gateway(self):
+ self.write_sample_eipconfig()
+ eipconf = eipconfig.EIPConfig(domain=self.provider)
+
+ # default eipservice
+ self.write_sample_eipservice()
+ eipsconf = eipconfig.EIPServiceConfig(domain=self.provider)
+
+ gateway = eipconfig.get_eip_gateway(
+ eipconfig=eipconf,
+ eipserviceconfig=eipsconf)
+
+ # in spec is local gateway by default
+ self.assertEqual(gateway, '127.0.0.1')
+
+ # change eipservice
+ # right now we only check that cluster == selected primary gw in
+ # eip.json, and pick first matching ip
+ eipconf._config.config['primary_gateway'] = "foo_provider"
+ newgateways = [{"cluster": "foo_provider",
+ "ip_address": "127.0.0.99"}]
+ self.write_sample_eipservice(gateways=newgateways)
+ eipsconf = eipconfig.EIPServiceConfig(domain=self.provider)
+ # load from disk file
+ eipsconf.load()
+
+ gateway = eipconfig.get_eip_gateway(
+ eipconfig=eipconf,
+ eipserviceconfig=eipsconf)
+ self.assertEqual(gateway, '127.0.0.99')
+
+ # change eipservice, several gateways
+ # right now we only check that cluster == selected primary gw in
+ # eip.json, and pick first matching ip
+ eipconf._config.config['primary_gateway'] = "bar_provider"
+ newgateways = [{"cluster": "foo_provider",
+ "ip_address": "127.0.0.99"},
+ {'cluster': "bar_provider",
+ "ip_address": "127.0.0.88"}]
+ self.write_sample_eipservice(gateways=newgateways)
+ eipsconf = eipconfig.EIPServiceConfig(domain=self.provider)
+ # load from disk file
+ eipsconf.load()
+
+ gateway = eipconfig.get_eip_gateway(
+ eipconfig=eipconf,
+ eipserviceconfig=eipsconf)
+ self.assertEqual(gateway, '127.0.0.88')
+
+ def test_build_ovpn_command_empty_config(self):
+ self.touch_exec()
+ self.write_sample_eipservice()
+ self.write_sample_eipconfig()
+
+ from leap.eip import config as eipconfig
+ from leap.util.fileutil import which
+ path = os.environ['PATH']
+ vpnbin = which('openvpn', path=path)
+ #print 'path =', path
+ #print 'vpnbin = ', vpnbin
+ vpncommand, vpnargs = eipconfig.build_ovpn_command(
+ do_pkexec_check=False, vpnbin=vpnbin,
+ socket_path="/tmp/test.socket",
+ provider=self.provider)
+ self.assertEqual(vpncommand, self.home + '/bin/openvpn')
+ self.assertEqual(vpnargs, self.get_expected_openvpn_args())
+
+ def test_build_ovpn_command_openvpnoptions(self):
+ self.touch_exec()
+
+ from leap.eip import config as eipconfig
+ from leap.util.fileutil import which
+ path = os.environ['PATH']
+ vpnbin = which('openvpn', path=path)
+
+ self.write_sample_eipconfig()
+
+ # regular run, everything normal
+ self.write_sample_eipservice(vpnciphers=True)
+ vpncommand, vpnargs = eipconfig.build_ovpn_command(
+ do_pkexec_check=False, vpnbin=vpnbin,
+ socket_path="/tmp/test.socket",
+ provider=self.provider)
+ self.assertEqual(vpncommand, self.home + '/bin/openvpn')
+ expected = self.get_expected_openvpn_args(
+ with_openvpn_ciphers=True)
+ self.assertEqual(vpnargs, expected)
+
+ # bad options -- illegal options
+ self.write_sample_eipservice(
+ vpnciphers=True,
+ # WE ONLY ALLOW vpn options in auth, cipher, tls-cipher
+ extra_vpnopts={"notallowedconfig": "badvalue"})
+ vpncommand, vpnargs = eipconfig.build_ovpn_command(
+ do_pkexec_check=False, vpnbin=vpnbin,
+ socket_path="/tmp/test.socket",
+ provider=self.provider)
+ self.assertEqual(vpncommand, self.home + '/bin/openvpn')
+ expected = self.get_expected_openvpn_args(
+ with_openvpn_ciphers=True)
+ self.assertEqual(vpnargs, expected)
+
+ # bad options -- illegal chars
+ self.write_sample_eipservice(
+ vpnciphers=True,
+ # WE ONLY ALLOW A-Z09\-
+ extra_vpnopts={"cipher": "AES-128-CBC;FOOTHING"})
+ vpncommand, vpnargs = eipconfig.build_ovpn_command(
+ do_pkexec_check=False, vpnbin=vpnbin,
+ socket_path="/tmp/test.socket",
+ provider=self.provider)
+ self.assertEqual(vpncommand, self.home + '/bin/openvpn')
+ expected = self.get_expected_openvpn_args(
+ with_openvpn_ciphers=True)
+ self.assertEqual(vpnargs, expected)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/leap/eip/tests/test_eipconnection.py b/src/leap/eip/tests/test_eipconnection.py
new file mode 100644
index 00000000..163f8d45
--- /dev/null
+++ b/src/leap/eip/tests/test_eipconnection.py
@@ -0,0 +1,216 @@
+import glob
+import logging
+import platform
+#import os
+import shutil
+
+logging.basicConfig()
+logger = logging.getLogger(name=__name__)
+
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+from mock import Mock, patch # MagicMock
+
+from leap.eip.eipconnection import EIPConnection
+from leap.eip.exceptions import ConnectionRefusedError
+from leap.eip import specs as eipspecs
+from leap.testing.basetest import BaseLeapTest
+
+_system = platform.system()
+
+PROVIDER = "testprovider.example.org"
+
+
+class NotImplementedError(Exception):
+ pass
+
+
+@patch('OpenVPNConnection._get_or_create_config')
+@patch('OpenVPNConnection._set_ovpn_command')
+class MockedEIPConnection(EIPConnection):
+
+ def _set_ovpn_command(self):
+ self.command = "mock_command"
+ self.args = [1, 2, 3]
+
+
+class EIPConductorTest(BaseLeapTest):
+
+ __name__ = "eip_conductor_tests"
+ provider = PROVIDER
+
+ def setUp(self):
+ # XXX there's a conceptual/design
+ # mistake here.
+ # If we're testing just attrs after init,
+ # init shold not be doing so much side effects.
+
+ # for instance:
+ # We have to TOUCH a keys file because
+ # we're triggerig the key checks FROM
+ # the constructor. me not like that,
+ # key checker should better be called explicitelly.
+
+ # XXX change to keys_checker invocation
+ # (see config_checker)
+
+ keyfiles = (eipspecs.provider_ca_path(domain=self.provider),
+ eipspecs.client_cert_path(domain=self.provider))
+ for filepath in keyfiles:
+ self.touch(filepath)
+ self.chmod600(filepath)
+
+ # we init the manager with only
+ # some methods mocked
+ self.manager = Mock(name="openvpnmanager_mock")
+ self.con = MockedEIPConnection()
+ self.con.provider = self.provider
+
+ # XXX watch out. This sometimes is throwing the following error:
+ # NoSuchProcess: process no longer exists (pid=6571)
+ # because of a bad implementation of _check_if_running_instance
+
+ self.con.run_openvpn_checks()
+
+ def tearDown(self):
+ pass
+
+ def doCleanups(self):
+ super(BaseLeapTest, self).doCleanups()
+ self.cleanupSocketDir()
+ del self.con
+
+ def cleanupSocketDir(self):
+ ptt = ('/tmp/leap-tmp*')
+ for tmpdir in glob.glob(ptt):
+ shutil.rmtree(tmpdir)
+
+ #
+ # tests
+ #
+
+ def test_vpnconnection_defaults(self):
+ """
+ default attrs as expected
+ """
+ con = self.con
+ self.assertEqual(con.autostart, True)
+ # XXX moar!
+
+ def test_ovpn_command(self):
+ """
+ set_ovpn_command called
+ """
+ self.assertEqual(self.con.command,
+ "mock_command")
+ self.assertEqual(self.con.args,
+ [1, 2, 3])
+
+ # config checks
+
+ def test_config_checked_called(self):
+ # XXX this single test is taking half of the time
+ # needed to run tests. (roughly 3 secs for this only)
+ # We should modularize and inject Mocks on more places.
+
+ oldcon = self.con
+ del(self.con)
+ config_checker = Mock()
+ self.con = MockedEIPConnection(config_checker=config_checker)
+ self.assertTrue(config_checker.called)
+ self.con.run_checks()
+ self.con.config_checker.run_all.assert_called_with(
+ skip_download=False)
+
+ # XXX test for cert_checker also
+ self.con = oldcon
+
+ # connect/disconnect calls
+
+ def test_disconnect(self):
+ """
+ disconnect method calls private and changes status
+ """
+ self.con._disconnect = Mock(
+ name="_disconnect")
+
+ # first we set status to connected
+ self.con.status.set_current(self.con.status.CONNECTED)
+ self.assertEqual(self.con.status.current,
+ self.con.status.CONNECTED)
+
+ # disconnect
+ self.con.terminate_openvpn_connection = Mock()
+ self.con.disconnect()
+ self.con.terminate_openvpn_connection.assert_called_once_with(
+ shutdown=False)
+ self.con.terminate_openvpn_connection = Mock()
+ self.con.disconnect(shutdown=True)
+ self.con.terminate_openvpn_connection.assert_called_once_with(
+ shutdown=True)
+
+ # new status should be disconnected
+ # XXX this should evolve and check no errors
+ # during disconnection
+ self.assertEqual(self.con.status.current,
+ self.con.status.DISCONNECTED)
+
+ def test_connect(self):
+ """
+ connect calls _launch_openvpn private
+ """
+ self.con._launch_openvpn = Mock()
+ self.con.connect()
+ self.con._launch_openvpn.assert_called_once_with()
+
+ # XXX tests breaking here ...
+
+ def test_good_poll_connection_state(self):
+ """
+ """
+ #@patch --
+ # self.manager.get_connection_state
+
+ #XXX review this set of poll_state tests
+ #they SHOULD NOT NEED TO MOCK ANYTHING IN THE
+ #lower layers!! -- status, vpn_manager..
+ #right now we're testing implementation, not
+ #behavior!!!
+ good_state = ["1345466946", "unknown_state", "ok",
+ "192.168.1.1", "192.168.1.100"]
+ self.con.get_connection_state = Mock(return_value=good_state)
+ self.con.status.set_vpn_state = Mock()
+
+ state = self.con.poll_connection_state()
+ good_state[1] = "disconnected"
+ final_state = tuple(good_state)
+ self.con.status.set_vpn_state.assert_called_with("unknown_state")
+ self.assertEqual(state, final_state)
+
+ # TODO between "good" and "bad" (exception raised) cases,
+ # we can still test for malformed states and see that only good
+ # states do have a change (and from only the expected transition
+ # states).
+
+ def test_bad_poll_connection_state(self):
+ """
+ get connection state raises ConnectionRefusedError
+ state is None
+ """
+ self.con.get_connection_state = Mock(
+ side_effect=ConnectionRefusedError('foo!'))
+ state = self.con.poll_connection_state()
+ self.assertEqual(state, None)
+
+
+ # XXX more things to test:
+ # - called config routines during initz.
+ # - raising proper exceptions with no config
+ # - called proper checks on config / permissions
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/leap/eip/tests/test_openvpnconnection.py b/src/leap/eip/tests/test_openvpnconnection.py
new file mode 100644
index 00000000..95bfb2f0
--- /dev/null
+++ b/src/leap/eip/tests/test_openvpnconnection.py
@@ -0,0 +1,161 @@
+import logging
+import os
+import platform
+import psutil
+import shutil
+#import socket
+
+logging.basicConfig()
+logger = logging.getLogger(name=__name__)
+
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+from mock import Mock, patch # MagicMock
+
+from leap.eip import config as eipconfig
+from leap.eip import openvpnconnection
+from leap.eip import exceptions as eipexceptions
+from leap.eip.udstelnet import UDSTelnet
+from leap.testing.basetest import BaseLeapTest
+
+_system = platform.system()
+
+
+class NotImplementedError(Exception):
+ pass
+
+
+mock_UDSTelnet = Mock(spec=UDSTelnet)
+# XXX cautious!!!
+# this might be fragile right now (counting a global
+# reference of calls I think.
+# investigate this other form instead:
+# http://www.voidspace.org.uk/python/mock/patch.html#start-and-stop
+
+# XXX redo after merge-refactor
+
+
+@patch('openvpnconnection.OpenVPNConnection.connect_to_management')
+class MockedOpenVPNConnection(openvpnconnection.OpenVPNConnection):
+ def __init__(self, *args, **kwargs):
+ self.mock_UDSTelnet = Mock()
+ super(MockedOpenVPNConnection, self).__init__(
+ *args, **kwargs)
+ self.tn = self.mock_UDSTelnet(self.host, self.port)
+
+ def connect_to_management(self):
+ #print 'patched connect'
+ self.tn = mock_UDSTelnet(self.host, port=self.port)
+
+
+class OpenVPNConnectionTest(BaseLeapTest):
+
+ __name__ = "vpnconnection_tests"
+
+ def setUp(self):
+ # XXX this will have to change for win, host=localhost
+ host = eipconfig.get_socket_path()
+ self.host = host
+ self.manager = MockedOpenVPNConnection(host=host)
+
+ def tearDown(self):
+ pass
+
+ def doCleanups(self):
+ super(BaseLeapTest, self).doCleanups()
+ self.cleanupSocketDir()
+
+ def cleanupSocketDir(self):
+ # remove the socket folder.
+ # XXX only if posix. in win, host is localhost, so nothing
+ # has to be done.
+ if self.host:
+ folder, fpath = os.path.split(self.host)
+ try:
+ assert folder.startswith('/tmp/leap-tmp') # safety check
+ shutil.rmtree(folder)
+ except:
+ self.fail("could not remove temp file")
+
+ del self.manager
+
+ #
+ # tests
+ #
+
+ def test_detect_vpn(self):
+ # XXX review, not sure if captured all the logic
+ # while fixing. kali.
+ openvpn_connection = openvpnconnection.OpenVPNConnection()
+
+ with patch.object(psutil, "process_iter") as mocked_psutil:
+ mocked_process = Mock()
+ mocked_process.name = "openvpn"
+ mocked_process.cmdline = ["openvpn", "-foo", "-bar", "-gaaz"]
+ mocked_psutil.return_value = [mocked_process]
+ with self.assertRaises(eipexceptions.OpenVPNAlreadyRunning):
+ openvpn_connection._check_if_running_instance()
+
+ openvpn_connection._check_if_running_instance()
+
+ @unittest.skipIf(_system == "Windows", "lin/mac only")
+ def test_lin_mac_default_init(self):
+ """
+ check default host for management iface
+ """
+ self.assertTrue(self.manager.host.startswith('/tmp/leap-tmp'))
+ self.assertEqual(self.manager.port, 'unix')
+
+ @unittest.skipUnless(_system == "Windows", "win only")
+ def test_win_default_init(self):
+ """
+ check default host for management iface
+ """
+ # XXX should we make the platform specific switch
+ # here or in the vpn command string building?
+ self.assertEqual(self.manager.host, 'localhost')
+ self.assertEqual(self.manager.port, 7777)
+
+ def test_port_types_init(self):
+ oldmanager = self.manager
+ self.manager = MockedOpenVPNConnection(port="42")
+ self.assertEqual(self.manager.port, 42)
+ self.manager = MockedOpenVPNConnection()
+ self.assertEqual(self.manager.port, "unix")
+ self.manager = MockedOpenVPNConnection(port="bad")
+ self.assertEqual(self.manager.port, None)
+ self.manager = oldmanager
+
+ def test_uds_telnet_called_on_connect(self):
+ self.manager.connect_to_management()
+ mock_UDSTelnet.assert_called_with(
+ self.manager.host,
+ port=self.manager.port)
+
+ @unittest.skip
+ def test_connect(self):
+ raise NotImplementedError
+ # XXX calls close
+ # calls UDSTelnet mock.
+
+ # XXX
+ # tests to write:
+ # UDSTelnetTest (for real?)
+ # HAVE A LOOK AT CORE TESTS FOR TELNETLIB.
+ # very illustrative instead...
+
+ # - raise MissingSocket
+ # - raise ConnectionRefusedError
+ # - test send command
+ # - tries connect
+ # - ... tries?
+ # - ... calls _seek_to_eof
+ # - ... read_until --> return value
+ # - ...
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/leap/eip/udstelnet.py b/src/leap/eip/udstelnet.py
new file mode 100644
index 00000000..18e927c2
--- /dev/null
+++ b/src/leap/eip/udstelnet.py
@@ -0,0 +1,38 @@
+import os
+import socket
+import telnetlib
+
+from leap.eip import exceptions as eip_exceptions
+
+
+class UDSTelnet(telnetlib.Telnet):
+ """
+ a telnet-alike class, that can listen
+ on unix domain sockets
+ """
+
+ def open(self, host, port=23, timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
+ """Connect to a host. If port is 'unix', it
+ will open a connection over unix docmain sockets.
+
+ The optional second argument is the port number, which
+ defaults to the standard telnet port (23).
+
+ Don't try to reopen an already connected instance.
+ """
+ self.eof = 0
+ self.host = host
+ self.port = port
+ self.timeout = timeout
+
+ if self.port == "unix":
+ # unix sockets spoken
+ if not os.path.exists(self.host):
+ raise eip_exceptions.MissingSocketError
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ try:
+ self.sock.connect(self.host)
+ except socket.error:
+ raise eip_exceptions.ConnectionRefusedError
+ else:
+ self.sock = socket.create_connection((host, port), timeout)
diff --git a/src/leap/eip/vpnmanager.py b/src/leap/eip/vpnmanager.py
deleted file mode 100644
index 78777cfb..00000000
--- a/src/leap/eip/vpnmanager.py
+++ /dev/null
@@ -1,262 +0,0 @@
-from __future__ import (print_function)
-import logging
-import os
-import socket
-import telnetlib
-import time
-
-logger = logging.getLogger(name=__name__)
-
-TELNET_PORT = 23
-
-
-class MissingSocketError(Exception):
- pass
-
-
-class ConnectionRefusedError(Exception):
- pass
-
-
-class UDSTelnet(telnetlib.Telnet):
-
- def open(self, host, port=0, timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
- """Connect to a host. If port is 'unix', it
- will open a connection over unix docmain sockets.
-
- The optional second argument is the port number, which
- defaults to the standard telnet port (23).
-
- Don't try to reopen an already connected instance.
- """
- self.eof = 0
- if not port:
- port = TELNET_PORT
- self.host = host
- self.port = port
- self.timeout = timeout
-
- if self.port == "unix":
- # unix sockets spoken
- if not os.path.exists(self.host):
- raise MissingSocketError
- self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- try:
- self.sock.connect(self.host)
- except socket.error:
- raise ConnectionRefusedError
- else:
- self.sock = socket.create_connection((host, port), timeout)
-
-
-# this class based in code from cube-routed project
-
-class OpenVPNManager(object):
- """
- Run commands over OpenVPN management interface
- and parses the output.
- """
- # XXX might need a lock to avoid
- # race conditions here...
-
- def __init__(self, host="/tmp/.eip.sock", port="unix", password=None):
- #XXX hardcoded host here. change.
- self.host = host
- if isinstance(port, str) and port.isdigit():
- port = int(port)
- self.port = port
- self.password = password
- self.tn = None
-
- #XXX workaround for signaling
- #the ui that we don't know how to
- #manage a connection error
- self.with_errors = False
-
- def forget_errors(self):
- print('forgetting errors')
- self.with_errors = False
-
- def connect(self):
- """Connect to openvpn management interface"""
- try:
- self.close()
- except:
- #XXX don't like this general
- #catch here.
- pass
- if self.connected():
- return True
- self.tn = UDSTelnet(self.host, self.port)
-
- # XXX make password optional
- # specially for win plat. we should generate
- # the pass on the fly when invoking manager
- # from conductor
-
- #self.tn.read_until('ENTER PASSWORD:', 2)
- #self.tn.write(self.password + '\n')
- #self.tn.read_until('SUCCESS:', 2)
-
- self._seek_to_eof()
- self.forget_errors()
- return True
-
- def _seek_to_eof(self):
- """
- Read as much as available. Position seek pointer to end of stream
- """
- b = self.tn.read_eager()
- while b:
- b = self.tn.read_eager()
-
- def connected(self):
- """
- Returns True if connected
- rtype: bool
- """
- #return bool(getattr(self, 'tn', None))
- try:
- assert self.tn
- return True
- except:
- #XXX get rid of
- #this pokemon exception!!!
- return False
-
- def close(self, announce=True):
- """
- Close connection to openvpn management interface
- """
- if announce:
- self.tn.write("quit\n")
- self.tn.read_all()
- self.tn.get_socket().close()
- del self.tn
-
- def _send_command(self, cmd, tries=0):
- """
- Send a command to openvpn and return response as list
- """
- if tries > 3:
- return []
- if not self.connected():
- try:
- self.connect()
- except MissingSocketError:
- #XXX capture more helpful error
- #messages
- #pass
- return self.make_error()
- try:
- self.tn.write(cmd + "\n")
- except socket.error:
- logger.error('socket error')
- print('socket error!')
- self.close(announce=False)
- self._send_command(cmd, tries=tries + 1)
- return []
- buf = self.tn.read_until(b"END", 2)
- self._seek_to_eof()
- blist = buf.split('\r\n')
- if blist[-1].startswith('END'):
- del blist[-1]
- return blist
- else:
- return []
-
- def _send_short_command(self, cmd):
- """
- parse output from commands that are
- delimited by "success" instead
- """
- if not self.connected():
- self.connect()
- self.tn.write(cmd + "\n")
- # XXX not working?
- buf = self.tn.read_until(b"SUCCESS", 2)
- self._seek_to_eof()
- blist = buf.split('\r\n')
- return blist
-
- #
- # useful vpn commands
- #
-
- def pid(self):
- #XXX broken
- return self._send_short_command("pid")
-
- def make_error(self):
- """
- capture error and wrap it in an
- understandable format
- """
- #XXX get helpful error codes
- self.with_errors = True
- now = int(time.time())
- return '%s,LAUNCHER ERROR,ERROR,-,-' % now
-
- def state(self):
- """
- OpenVPN command: state
- """
- state = self._send_command("state")
- if not state:
- return None
- if isinstance(state, str):
- return state
- if isinstance(state, list):
- if len(state) == 1:
- return state[0]
- else:
- return state[-1]
-
- def status(self):
- """
- OpenVPN command: status
- """
- status = self._send_command("status")
- return status
-
- def status2(self):
- """
- OpenVPN command: last 2 statuses
- """
- return self._send_command("status 2")
-
- #
- # parse info
- #
-
- def get_status_io(self):
- status = self.status()
- if isinstance(status, str):
- lines = status.split('\n')
- if isinstance(status, list):
- lines = status
- try:
- (header, when, tun_read, tun_write,
- tcp_read, tcp_write, auth_read) = tuple(lines)
- except ValueError:
- return None
-
- when_ts = time.strptime(when.split(',')[1], "%a %b %d %H:%M:%S %Y")
- sep = ','
- # XXX cleanup!
- tun_read = tun_read.split(sep)[1]
- tun_write = tun_write.split(sep)[1]
- tcp_read = tcp_read.split(sep)[1]
- tcp_write = tcp_write.split(sep)[1]
- auth_read = auth_read.split(sep)[1]
-
- # XXX this could be a named tuple. prettier.
- return when_ts, (tun_read, tun_write, tcp_read, tcp_write, auth_read)
-
- def get_connection_state(self):
- state = self.state()
- if state is not None:
- ts, status_step, ok, ip, remote = state.split(',')
- ts = time.gmtime(float(ts))
- # XXX this could be a named tuple. prettier.
- return ts, status_step, ok, ip, remote
diff --git a/src/leap/eip/vpnwatcher.py b/src/leap/eip/vpnwatcher.py
deleted file mode 100644
index 09bd5811..00000000
--- a/src/leap/eip/vpnwatcher.py
+++ /dev/null
@@ -1,169 +0,0 @@
-"""generic watcher object that keeps track of connection status"""
-# This should be deprecated in favor of daemon mode + management
-# interface. But we can leave it here for debug purposes.
-
-
-class EIPConnectionStatus(object):
- """
- Keep track of client (gui) and openvpn
- states.
-
- These are the OpenVPN states:
- CONNECTING -- OpenVPN's initial state.
- WAIT -- (Client only) Waiting for initial response
- from server.
- AUTH -- (Client only) Authenticating with server.
- GET_CONFIG -- (Client only) Downloading configuration options
- from server.
- ASSIGN_IP -- Assigning IP address to virtual network
- interface.
- ADD_ROUTES -- Adding routes to system.
- CONNECTED -- Initialization Sequence Completed.
- RECONNECTING -- A restart has occurred.
- EXITING -- A graceful exit is in progress.
-
- We add some extra states:
-
- DISCONNECTED -- GUI initial state.
- UNRECOVERABLE -- An unrecoverable error has been raised
- while invoking openvpn service.
- """
- CONNECTING = 1
- WAIT = 2
- AUTH = 3
- GET_CONFIG = 4
- ASSIGN_IP = 5
- ADD_ROUTES = 6
- CONNECTED = 7
- RECONNECTING = 8
- EXITING = 9
-
- # gui specific states:
- UNRECOVERABLE = 11
- DISCONNECTED = 0
-
- def __init__(self, callbacks=None):
- """
- EIPConnectionStatus is initialized with a tuple
- of signals to be triggered.
- :param callbacks: a tuple of (callable) observers
- :type callbacks: tuple
- """
- # (callbacks to connect to signals in Qt-land)
- self.current = self.DISCONNECTED
- self.previous = None
- self.callbacks = callbacks
-
- def get_readable_status(self):
- # XXX DRY status / labels a little bit.
- # think we'll want to i18n this.
- human_status = {
- 0: 'disconnected',
- 1: 'connecting',
- 2: 'waiting',
- 3: 'authenticating',
- 4: 'getting config',
- 5: 'assigning ip',
- 6: 'adding routes',
- 7: 'connected',
- 8: 'reconnecting',
- 9: 'exiting',
- 11: 'unrecoverable error',
- }
- return human_status[self.current]
-
- def get_state_icon(self):
- """
- returns the high level icon
- for each fine-grain openvpn state
- """
- connecting = (self.CONNECTING,
- self.WAIT,
- self.AUTH,
- self.GET_CONFIG,
- self.ASSIGN_IP,
- self.ADD_ROUTES)
- connected = (self.CONNECTED,)
- disconnected = (self.DISCONNECTED,
- self.UNRECOVERABLE)
-
- # this can be made smarter,
- # but it's like it'll change,
- # so +readability.
-
- if self.current in connecting:
- return "connecting"
- if self.current in connected:
- return "connected"
- if self.current in disconnected:
- return "disconnected"
-
- def set_vpn_state(self, status):
- """
- accepts a state string from the management
- interface, and sets the internal state.
- :param status: openvpn STATE (uppercase).
- :type status: str
- """
- if hasattr(self, status):
- self.change_to(getattr(self, status))
-
- def set_current(self, to):
- """
- setter for the 'current' property
- :param to: destination state
- :type to: int
- """
- self.current = to
-
- def change_to(self, to):
- """
- :param to: destination state
- :type to: int
- """
- if to == self.current:
- return
- changed = False
- from_ = self.current
- self.current = to
-
- # We can add transition restrictions
- # here to ensure no transitions are
- # allowed outside the fsm.
-
- self.set_current(to)
- changed = True
-
- #trigger signals (as callbacks)
- #print('current state: %s' % self.current)
- if changed:
- self.previous = from_
- if self.callbacks:
- for cb in self.callbacks:
- if callable(cb):
- cb(self)
-
-
-def status_watcher(cs, line):
- """
- a wrapper that calls to ConnectionStatus object
- :param cs: a EIPConnectionStatus instance
- :type cs: EIPConnectionStatus object
- :param line: a single line of the watched output
- :type line: str
- """
- #print('status watcher watching')
-
- # from the mullvad code, should watch for
- # things like:
- # "Initialization Sequence Completed"
- # "With Errors"
- # "Tap-Win32"
-
- if "Completed" in line:
- cs.change_to(cs.CONNECTED)
- return
-
- if "Initial packet from" in line:
- cs.change_to(cs.CONNECTING)
- return