From ee8fbbdc2f3dbccea3a830b40e9eb0be5b392d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 6 Mar 2013 15:38:05 -0300 Subject: Add EIP service --- src/leap/services/__init__.py | 0 src/leap/services/eip/__init__.py | 0 src/leap/services/eip/eipbootstrapper.py | 315 ++++++++++++++++ src/leap/services/eip/eipconfig.py | 123 ++++++ src/leap/services/eip/eipspec.py | 63 ++++ src/leap/services/eip/providerbootstrapper.py | 520 ++++++++++++++++++++++++++ src/leap/services/eip/udstelnet.py | 61 +++ src/leap/services/eip/vpn.py | 359 ++++++++++++++++++ src/leap/services/eip/vpnlaunchers.py | 270 +++++++++++++ 9 files changed, 1711 insertions(+) create mode 100644 src/leap/services/__init__.py create mode 100644 src/leap/services/eip/__init__.py create mode 100644 src/leap/services/eip/eipbootstrapper.py create mode 100644 src/leap/services/eip/eipconfig.py create mode 100644 src/leap/services/eip/eipspec.py create mode 100644 src/leap/services/eip/providerbootstrapper.py create mode 100644 src/leap/services/eip/udstelnet.py create mode 100644 src/leap/services/eip/vpn.py create mode 100644 src/leap/services/eip/vpnlaunchers.py (limited to 'src/leap/services') diff --git a/src/leap/services/__init__.py b/src/leap/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/services/eip/__init__.py b/src/leap/services/eip/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py new file mode 100644 index 00000000..77d7020a --- /dev/null +++ b/src/leap/services/eip/eipbootstrapper.py @@ -0,0 +1,315 @@ +# -*- coding: utf-8 -*- +# eipbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +EIP bootstrapping +""" + +import requests +import logging +import os +import errno + +from PySide import QtGui, QtCore + +from leap.config.providerconfig import ProviderConfig +from leap.services.eip.eipconfig import EIPConfig + +logger = logging.getLogger(__name__) + + +class EIPBootstrapper(QtCore.QThread): + """ + Sets up EIP for a provider a series of checks and emits signals + after they are passed. + If a check fails, the subsequent checks are not executed + """ + + PASSED_KEY = "passed" + ERROR_KEY = "error" + + IDLE_SLEEP_INTERVAL = 100 + + # All dicts returned are of the form + # {"passed": bool, "error": str} + download_config = QtCore.Signal(dict) + download_client_certificate = QtCore.Signal(dict) + + def __init__(self): + QtCore.QThread.__init__(self) + + self._checks = [] + self._checks_lock = QtCore.QMutex() + + self._should_quit = False + self._should_quit_lock = QtCore.QMutex() + + # **************************************************** # + # Dependency injection helpers, override this for more + # granular testing + self._fetcher = requests + # **************************************************** # + + self._session = self._fetcher.session() + self._provider_config = None + self._eip_config = None + self._download_if_needed = False + + def get_should_quit(self): + """ + Returns wether this thread should quit + + @rtype: bool + @return: True if the thread should terminate itself, Flase otherwise + """ + + QtCore.QMutexLocker(self._should_quit_lock) + return self._should_quit + + def set_should_quit(self): + """ + Sets the should_quit flag to True so that this thread + terminates the first chance it gets + """ + QtCore.QMutexLocker(self._should_quit_lock) + self._should_quit = True + self.wait() + + def start(self): + """ + Starts the thread and resets the should_quit flag + """ + with QtCore.QMutexLocker(self._should_quit_lock): + self._should_quit = False + + QtCore.QThread.start(self) + + def _download_config(self): + """ + Downloads the EIP config for the given provider + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + + assert self._provider_config, "We need a provider configuration!" + + logger.debug("Downloading EIP config for %s" % + (self._provider_config.get_domain(),)) + + download_config_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + self._eip_config = EIPConfig() + + if self._download_if_needed and \ + os.path.exists(os.path.join(self._eip_config.get_path_prefix(), + "leap", + "providers", + self._provider_config.get_domain(), + "eip-service.json")): + download_config_data[self.PASSED_KEY] = True + self.download_config.emit(download_config_data) + return True + + try: + res = self._session.get("%s/%s/%s/%s" % + (self._provider_config.get_api_uri(), + self._provider_config.get_api_version(), + "config", + "eip-service.json"), + verify=self._provider_config + .get_ca_cert_path()) + res.raise_for_status() + + eip_definition = res.content + + self._eip_config.load(data=eip_definition) + self._eip_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "eip-service.json"]) + + download_config_data[self.PASSED_KEY] = True + except Exception as e: + download_config_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting download_config %s" % (download_config_data,)) + self.download_config.emit(download_config_data) + + return download_config_data[self.PASSED_KEY] + + def _download_client_certificates(self): + """ + Downloads the EIP client certificate for the given provider + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + assert self._provider_config, "We need a provider configuration!" + assert self._eip_config, "We need an eip configuration!" + + logger.debug("Downloading EIP client certificate for %s" % + (self._provider_config.get_domain(),)) + + download_cert = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + client_cert_path = self._eip_config.\ + get_client_cert_path(self._provider_config, + about_to_download=True) + + if self._download_if_needed and \ + os.path.exists(client_cert_path): + download_cert[self.PASSED_KEY] = True + self.download_client_certificate.emit(download_cert) + return True + + try: + res = self._session.get("%s/%s/%s/" % + (self._provider_config.get_api_uri(), + self._provider_config.get_api_version(), + "cert"), + verify=self._provider_config + .get_ca_cert_path()) + res.raise_for_status() + + client_cert = res.content + + # TODO: check certificate validity + + try: + os.makedirs(os.path.dirname(client_cert_path)) + except OSError as e: + if e.errno == errno.EEXIST and \ + os.path.isdir(os.path.dirname(client_cert_path)): + pass + else: + raise + + with open(client_cert_path, "w") as f: + f.write(client_cert) + + download_cert[self.PASSED_KEY] = True + except Exception as e: + download_cert[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting download_client_certificates %s" % + (download_cert,)) + self.download_client_certificate.emit(download_cert) + + return download_cert[self.PASSED_KEY] + + def run_eip_setup_checks(self, provider_config, download_if_needed=False): + """ + Starts the checks needed for a new eip setup + + @param provider_config: Provider configuration + @type provider_config: ProviderConfig + """ + assert provider_config, "We need a provider config!" + assert isinstance(provider_config, ProviderConfig), "Expected " + \ + "ProviderConfig type, not %r" % (type(provider_config),) + + self._provider_config = provider_config + self._download_if_needed = download_if_needed + + QtCore.QMutexLocker(self._checks_lock) + self._checks = [ + self._download_config, + self._download_client_certificates + ] + + def run(self): + """ + Main run loop for this thread. Executes the checks. + """ + shouldContinue = False + while True: + if self.get_should_quit(): + logger.debug("Quitting provider bootstrap thread") + return + checkSomething = False + with QtCore.QMutexLocker(self._checks_lock): + if len(self._checks) > 0: + check = self._checks.pop(0) + shouldContinue = check() + checkSomething = True + if not shouldContinue: + logger.debug("Something went wrong with the checks, " + + "clearing...") + self._checks = [] + checkSomething = False + if not checkSomething: + self.usleep(self.IDLE_SLEEP_INTERVAL) + + +if __name__ == "__main__": + import sys + from functools import partial + app = QtGui.QApplication(sys.argv) + + import signal + + def sigint_handler(*args, **kwargs): + logger.debug('SIGINT catched. shutting down...') + bootstrapper_thread = args[0] + bootstrapper_thread.set_should_quit() + QtGui.QApplication.quit() + + def signal_tester(d): + print d + + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + eip_thread = EIPBootstrapper() + + sigint = partial(sigint_handler, eip_thread) + signal.signal(signal.SIGINT, sigint) + + timer = QtCore.QTimer() + timer.start(500) + timer.timeout.connect(lambda: None) + app.connect(app, QtCore.SIGNAL("aboutToQuit()"), + eip_thread.set_should_quit) + w = QtGui.QWidget() + w.resize(100, 100) + w.show() + + eip_thread.start() + + provider_config = ProviderConfig() + if provider_config.load(os.path.join("leap", + "providers", + "bitmask.net", + "provider.json")): + eip_thread.run_eip_setup_checks(provider_config) + + sys.exit(app.exec_()) diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py new file mode 100644 index 00000000..ac06fef1 --- /dev/null +++ b/src/leap/services/eip/eipconfig.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# eipconfig.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Provider configuration +""" +import os +import logging + +from leap.config.baseconfig import BaseConfig +from leap.config.providerconfig import ProviderConfig +from leap.services.eip.eipspec import eipservice_config_spec + +logger = logging.getLogger(__name__) + + +class EIPConfig(BaseConfig): + """ + Provider configuration abstraction class + """ + + def __init__(self): + BaseConfig.__init__(self) + + def _get_spec(self): + """ + Returns the spec object for the specific configuration + """ + return eipservice_config_spec + + def get_clusters(self): + # TODO: create an abstraction for clusters + return self._safe_get_value("clusters") + + def get_gateways(self): + # TODO: create an abstraction for gateways + return self._safe_get_value("gateways") + + def get_openvpn_configuration(self): + return self._safe_get_value("openvpn_configuration") + + def get_serial(self): + return self._safe_get_value("serial") + + def get_version(self): + return self._safe_get_value("version") + + def get_gateway_ip(self, index=0): + gateways = self.get_gateways() + assert len(gateways) > 0, "We don't have any gateway!" + if index > len(gateways): + index = 0 + logger.warning("Provided an unknown gateway index %s, " + + "defaulting to 0") + return gateways[0]["ip_address"] + + def get_client_cert_path(self, + providerconfig=None, + about_to_download=False): + """ + Returns the path to the certificate used by openvpn + """ + + assert providerconfig, "We need a provider" + assert isinstance(providerconfig, ProviderConfig), "The provider " + \ + "needs to be of type ProviderConfig instead of %s" % \ + (type(providerconfig),) + + cert_path = os.path.join(self.get_path_prefix(), + "leap", + "providers", + providerconfig.get_domain(), + "keys", + "client", + "openvpn.pem") + + if not about_to_download: + assert os.path.exists(cert_path), \ + "You need to download the certificate first" + logger.debug("Using OpenVPN cert %s" % (cert_path,)) + + return cert_path + + +if __name__ == "__main__": + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + eipconfig = EIPConfig() + + try: + eipconfig.get_clusters() + except Exception as e: + assert isinstance(e, AssertionError), "Expected an assert" + print "Safe value getting is working" + + if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): + print eipconfig.get_clusters() + print eipconfig.get_gateways() + print eipconfig.get_openvpn_configuration() + print eipconfig.get_serial() + print eipconfig.get_version() diff --git a/src/leap/services/eip/eipspec.py b/src/leap/services/eip/eipspec.py new file mode 100644 index 00000000..d5c73056 --- /dev/null +++ b/src/leap/services/eip/eipspec.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# eipspec.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +eipservice_config_spec = { + 'description': 'sample eip service config', + 'type': 'object', + 'required': [ + 'serial', + 'version' + ], + 'properties': { + 'serial': { + 'type': int, + 'default': 1 + }, + 'version': { + 'type': int, + '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/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py new file mode 100644 index 00000000..babcd47b --- /dev/null +++ b/src/leap/services/eip/providerbootstrapper.py @@ -0,0 +1,520 @@ +# -*- coding: utf-8 -*- +# providerbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Provider bootstrapping +""" + +import requests +import logging +import socket +import os +import errno + +from OpenSSL import crypto +from PySide import QtGui, QtCore + +from leap.config.providerconfig import ProviderConfig + +logger = logging.getLogger(__name__) + + +class ProviderBootstrapper(QtCore.QThread): + """ + Given a provider URL performs a series of checks and emits signals + after they are passed. + If a check fails, the subsequent checks are not executed + """ + + PASSED_KEY = "passed" + ERROR_KEY = "error" + + IDLE_SLEEP_INTERVAL = 100 + + # All dicts returned are of the form + # {"passed": bool, "error": str} + name_resolution = QtCore.Signal(dict) + https_connection = QtCore.Signal(dict) + download_provider_info = QtCore.Signal(dict) + + download_ca_cert = QtCore.Signal(dict) + check_ca_fingerprint = QtCore.Signal(dict) + check_api_certificate = QtCore.Signal(dict) + + def __init__(self): + QtCore.QThread.__init__(self) + + self._checks = [] + self._checks_lock = QtCore.QMutex() + + self._should_quit = False + self._should_quit_lock = QtCore.QMutex() + + # **************************************************** # + # Dependency injection helpers, override this for more + # granular testing + self._fetcher = requests + # **************************************************** # + + self._session = self._fetcher.session() + self._domain = None + self._provider_config = None + self._download_if_needed = False + + def get_should_quit(self): + """ + Returns wether this thread should quit + + @rtype: bool + @return: True if the thread should terminate itself, Flase otherwise + """ + + QtCore.QMutexLocker(self._should_quit_lock) + return self._should_quit + + def set_should_quit(self): + """ + Sets the should_quit flag to True so that this thread + terminates the first chance it gets + """ + QtCore.QMutexLocker(self._should_quit_lock) + self._should_quit = True + self.wait() + + def start(self): + """ + Starts the thread and resets the should_quit flag + """ + with QtCore.QMutexLocker(self._should_quit_lock): + self._should_quit = False + + QtCore.QThread.start(self) + + def _should_proceed_provider(self): + """ + Returns False if provider.json already exists for the given + domain. True otherwise + + @rtype: bool + """ + if not self._download_if_needed: + return True + + # We don't really need a provider config at this stage, just + # the path prefix + return not os.path.exists(os.path.join(ProviderConfig() + .get_path_prefix(), + "leap", + "providers", + self._domain, + "provider.json")) + + def _check_name_resolution(self): + """ + Checks that the name resolution for the provider name works + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + + assert self._domain, "Cannot check DNS without a domain" + + logger.debug("Checking name resolution for %s" % (self._domain)) + + name_resolution_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + # We don't skip this check, since it's basic for the whole + # system to work + try: + socket.gethostbyname(self._domain) + name_resolution_data[self.PASSED_KEY] = True + except socket.gaierror as e: + name_resolution_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting name_resolution %s" % (name_resolution_data,)) + self.name_resolution.emit(name_resolution_data) + + return name_resolution_data[self.PASSED_KEY] + + def _check_https(self): + """ + Checks that https is working and that the provided certificate + checks out + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + + assert self._domain, "Cannot check HTTPS without a domain" + + logger.debug("Checking https for %s" % (self._domain)) + + https_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + # We don't skip this check, since it's basic for the whole + # system to work + + try: + res = self._session.get("https://%s" % (self._domain,)) + res.raise_for_status() + https_data[self.PASSED_KEY] = True + except Exception as e: + https_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting https_connection %s" % (https_data,)) + self.https_connection.emit(https_data) + + return https_data[self.PASSED_KEY] + + def _download_provider_info(self): + """ + Downloads the provider.json defition + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + assert self._domain, "Cannot download provider info without a domain" + + logger.debug("Downloading provider info for %s" % (self._domain)) + + download_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + if not self._should_proceed_provider(): + download_data[self.PASSED_KEY] = True + self.download_provider_info.emit(download_data) + return True + + try: + res = self._session.get("https://%s/%s" % (self._domain, + "provider.json")) + res.raise_for_status() + + provider_definition = res.content + + provider_config = ProviderConfig() + provider_config.load(data=provider_definition) + provider_config.save(["leap", + "providers", + self._domain, + "provider.json"]) + + download_data[self.PASSED_KEY] = True + except Exception as e: + download_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting download_provider_info %s" % (download_data,)) + self.download_provider_info.emit(download_data) + + return download_data[self.PASSED_KEY] + + def run_provider_select_checks(self, domain, download_if_needed=False): + """ + Populates the check queue + + @param domain: domain to check + @type domain: str + @param download_if_needed: if True, makes the checks do not + overwrite already downloaded data + @type download_if_needed: bool + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + assert domain and len(domain) > 0, "We need a domain!" + + self._domain = domain + self._download_if_needed = download_if_needed + + QtCore.QMutexLocker(self._checks_lock) + self._checks = [ + self._check_name_resolution, + self._check_https, + self._download_provider_info + ] + + def _should_proceed_cert(self): + """ + Returns False if the certificate already exists for the given + provider. True otherwise + + @rtype: bool + """ + assert self._provider_config, "We need a provider config!" + + if not self._download_if_needed: + return True + + return not os.path.exists(self._provider_config + .get_ca_cert_path(about_to_download=True)) + + def _download_ca_cert(self): + """ + Downloads the CA cert that is going to be used for the api URL + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + + assert self._provider_config, "Cannot download the ca cert " + \ + "without a provider config!" + + logger.debug("Downloading ca cert for %s at %s" % + (self._domain, self._provider_config.get_ca_cert_uri())) + + download_ca_cert_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + if not self._should_proceed_cert(): + download_ca_cert_data[self.PASSED_KEY] = True + self.download_ca_cert.emit(download_ca_cert_data) + return True + + try: + res = self._session.get(self._provider_config.get_ca_cert_uri()) + res.raise_for_status() + + cert_path = self._provider_config.get_ca_cert_path( + about_to_download=True) + + cert_dir = os.path.dirname(cert_path) + + try: + os.makedirs(cert_dir) + except OSError as e: + if e.errno == errno.EEXIST and os.path.isdir(cert_dir): + pass + else: + raise + + with open(cert_path, "w") as f: + f.write(res.content) + + download_ca_cert_data[self.PASSED_KEY] = True + except Exception as e: + download_ca_cert_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting download_ca_cert %s" % (download_ca_cert_data,)) + self.download_ca_cert.emit(download_ca_cert_data) + + return download_ca_cert_data[self.PASSED_KEY] + + def _check_ca_fingerprint(self): + """ + Checks the CA cert fingerprint against the one provided in the + json definition + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + assert self._provider_config, "Cannot check the ca cert " + \ + "without a provider config!" + + logger.debug("Checking ca fingerprint for %s and cert %s" % + (self._domain, + self._provider_config.get_ca_cert_path())) + + check_ca_fingerprint_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + if not self._should_proceed_cert(): + check_ca_fingerprint_data[self.PASSED_KEY] = True + self.check_ca_fingerprint.emit(check_ca_fingerprint_data) + return True + + try: + parts = self._provider_config.get_ca_cert_fingerprint().split(":") + assert len(parts) == 2, "Wrong fingerprint format" + + method = parts[0].strip() + fingerprint = parts[1].strip() + cert_data = None + with open(self._provider_config.get_ca_cert_path()) as f: + cert_data = f.read() + + assert len(cert_data) > 0, "Could not read certificate data" + + x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert_data) + digest = x509.digest(method).replace(":", "").lower() + + assert digest == fingerprint, \ + "Downloaded certificate has a different fingerprint!" + + check_ca_fingerprint_data[self.PASSED_KEY] = True + except Exception as e: + check_ca_fingerprint_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting check_ca_fingerprint %s" % + (check_ca_fingerprint_data,)) + self.check_ca_fingerprint.emit(check_ca_fingerprint_data) + + return check_ca_fingerprint_data[self.PASSED_KEY] + + def _check_api_certificate(self): + """ + Tries to make an API call with the downloaded cert and checks + if it validates against it + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + assert self._provider_config, "Cannot check the ca cert " + \ + "without a provider config!" + + logger.debug("Checking api certificate for %s and cert %s" % + (self._provider_config.get_api_uri(), + self._provider_config.get_ca_cert_path())) + + check_api_certificate_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + if not self._should_proceed_cert(): + check_api_certificate_data[self.PASSED_KEY] = True + self.check_api_certificate.emit(check_api_certificate_data) + return True + + try: + test_uri = "%s/%s/cert" % (self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + res = self._session.get(test_uri, + verify=self._provider_config + .get_ca_cert_path()) + res.raise_for_status() + check_api_certificate_data[self.PASSED_KEY] = True + except Exception as e: + check_api_certificate_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting check_api_certificate %s" % + (check_api_certificate_data,)) + self.check_api_certificate.emit(check_api_certificate_data) + + return check_api_certificate_data[self.PASSED_KEY] + + def run_provider_setup_checks(self, provider_config, + download_if_needed=False): + """ + Starts the checks needed for a new provider setup + + @param provider_config: Provider configuration + @type provider_config: ProviderConfig + @param download_if_needed: if True, makes the checks do not + overwrite already downloaded data + @type download_if_needed: bool + """ + assert provider_config, "We need a provider config!" + assert isinstance(provider_config, ProviderConfig), "Expected " + \ + "ProviderConfig type, not %r" % (type(provider_config),) + + self._provider_config = provider_config + self._download_if_needed = download_if_needed + + QtCore.QMutexLocker(self._checks_lock) + self._checks = [ + self._download_ca_cert, + self._check_ca_fingerprint, + self._check_api_certificate + ] + + def run(self): + """ + Main run loop for this thread. Executes the checks. + """ + shouldContinue = False + while True: + if self.get_should_quit(): + logger.debug("Quitting provider bootstrap thread") + return + checkSomething = False + with QtCore.QMutexLocker(self._checks_lock): + if len(self._checks) > 0: + check = self._checks.pop(0) + shouldContinue = check() + checkSomething = True + if not shouldContinue: + logger.debug("Something went wrong with the checks, " + "clearing...") + self._checks = [] + checkSomething = False + if not checkSomething: + self.usleep(self.IDLE_SLEEP_INTERVAL) + + +if __name__ == "__main__": + import sys + from functools import partial + app = QtGui.QApplication(sys.argv) + + import signal + + def sigint_handler(*args, **kwargs): + logger.debug('SIGINT catched. shutting down...') + bootstrapper_thread = args[0] + bootstrapper_thread.set_should_quit() + QtGui.QApplication.quit() + + def signal_tester(d): + print d + + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + bootstrapper_thread = ProviderBootstrapper() + + sigint = partial(sigint_handler, bootstrapper_thread) + signal.signal(signal.SIGINT, sigint) + + timer = QtCore.QTimer() + timer.start(500) + timer.timeout.connect(lambda: None) + app.connect(app, QtCore.SIGNAL("aboutToQuit()"), + bootstrapper_thread.set_should_quit) + w = QtGui.QWidget() + w.resize(100, 100) + w.show() + + bootstrapper_thread.start() + bootstrapper_thread.run_provider_select_checks("bitmask.net") + + provider_config = ProviderConfig() + if provider_config.load(os.path.join("leap", + "providers", + "bitmask.net", + "provider.json")): + bootstrapper_thread.run_provider_setup_checks(provider_config) + + sys.exit(app.exec_()) diff --git a/src/leap/services/eip/udstelnet.py b/src/leap/services/eip/udstelnet.py new file mode 100644 index 00000000..a47c24f4 --- /dev/null +++ b/src/leap/services/eip/udstelnet.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# udstelnet.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import socket +import telnetlib + + +class ConnectionRefusedError(Exception): + pass + + +class MissingSocketError(Exception): + pass + + +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 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) diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py new file mode 100644 index 00000000..f117cdbc --- /dev/null +++ b/src/leap/services/eip/vpn.py @@ -0,0 +1,359 @@ +# -*- coding: utf-8 -*- +# vpn.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +VPN launcher and watcher thread +""" +import logging +import sys + +from PySide import QtCore, QtGui +from subprocess import Popen, PIPE +from functools import partial + +from leap.config.providerconfig import ProviderConfig +from leap.services.eip.vpnlaunchers import get_platform_launcher +from leap.services.eip.eipconfig import EIPConfig +from leap.services.eip.udstelnet import UDSTelnet + +logger = logging.getLogger(__name__) +ON_POSIX = 'posix' in sys.builtin_module_names + + +# TODO: abstract the thread that can be asked to quit to another +# generic class that Fetcher and VPN inherit from +class VPN(QtCore.QThread): + """ + VPN launcher and watcher thread. It will emit signals based on + different events caught by the management interface + """ + + state_changed = QtCore.Signal(dict) + status_changed = QtCore.Signal(dict) + + CONNECTION_RETRY_TIME = 1000 + POLL_TIME = 100 + + TS_KEY = "ts" + STATUS_STEP_KEY = "status_step" + OK_KEY = "ok" + IP_KEY = "ip" + REMOTE_KEY = "remote" + + TUNTAP_READ_KEY = "tun_tap_read" + TUNTAP_WRITE_KEY = "tun_tap_write" + TCPUDP_READ_KEY = "tcp_udp_read" + TCPUDP_WRITE_KEY = "tcp_udp_write" + AUTH_READ_KEY = "auth_read" + + def __init__(self): + QtCore.QThread.__init__(self) + + self._should_quit = False + self._should_quit_lock = QtCore.QMutex() + + self._launcher = get_platform_launcher() + self._subp = None + self._started = False + + self._tn = None + self._host = None + self._port = None + + self._last_state = None + self._last_status = None + + def get_should_quit(self): + """ + Returns wether this thread should quit + + @rtype: bool + @return: True if the thread should terminate itself, Flase otherwise + """ + QtCore.QMutexLocker(self._should_quit_lock) + return self._should_quit + + def set_should_quit(self): + """ + Sets the should_quit flag to True so that this thread + terminates the first chance it gets. + Also terminates the VPN process and the connection to it + """ + QtCore.QMutexLocker(self._should_quit_lock) + self._should_quit = True + if self._tn is None or self._subp is None: + return + + try: + self._disconnect() + self._subp.terminate() + except Exception as e: + logger.debug("Could not terminate process, trying command " + + "signal SIGNINT: %r" % (e,)) + self._send_command("signal SIGINT") + self._subp.wait() + self.wait() + self._started = False + + def start(self, eipconfig, providerconfig, socket_host, socket_port): + """ + Launches OpenVPN and starts the thread to watch its output + + @param eipconfig: eip configuration object + @type eipconfig: EIPConfig + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + @param socket_host: either socket path (unix) or socket IP + @type socket_host: str + @param socket_port: either string "unix" if it's a unix + socket, or port otherwise + @type socket_port: str + """ + assert eipconfig, "We need an eip config" + assert isinstance(eipconfig, EIPConfig), "Expected EIPConfig " + \ + "object instead of %s" % (type(eipconfig),) + assert providerconfig, "We need a provider config" + assert isinstance(providerconfig, ProviderConfig), "Expected " + \ + "ProviderConfig object instead of %s" % (type(providerconfig),) + assert not self._started, "Starting process more than once!" + + logger.debug("Starting VPN...") + + with QtCore.QMutexLocker(self._should_quit_lock): + self._should_quit = False + + command = self._launcher.get_vpn_command(eipconfig=eipconfig, + providerconfig=providerconfig, + socket_host=socket_host, + socket_port=socket_port) + try: + self._subp = Popen(command, stdout=PIPE, stderr=PIPE, + bufsize=1, close_fds=ON_POSIX) + + self._host = socket_host + self._port = socket_port + + self._started = True + + QtCore.QThread.start(self) + except Exception as e: + logger.warning("Something went wrong while starting OpenVPN: %r" % + (e,)) + + def _connect(self, socket_host, socket_port): + """ + Connects to the specified socket_host socket_port + @param socket_host: either socket path (unix) or socket IP + @type socket_host: str + @param socket_port: either string "unix" if it's a unix + socket, or port otherwise + @type socket_port: str + """ + try: + self._tn = UDSTelnet(socket_host, socket_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._tn.read_eager() + except Exception as e: + logger.warning("Could not connect to OpenVPN yet: %r" % (e,)) + self._tn = None + + def _disconnect(self): + """ + Disconnects the telnet connection to the openvpn process + """ + logger.debug('Closing socket') + self._tn.write("quit\n") + self._tn.read_all() + self._tn.close() + self._tn = None + + def _send_command(self, command, until=b"END"): + """ + Sends a command to the telnet connection and reads until END + is reached + + @param command: command to send + @type command: str + @param until: byte delimiter string for reading command output + @type until: byte str + @return: response read + @rtype: list + """ + assert self._tn, "We need a tn connection!" + try: + self._tn.write("%s\n" % (command,)) + buf = self._tn.read_until(until, 2) + self._tn.read_eager() + lines = buf.split("\n") + return lines + except Exception as e: + logger.warning("Error sending command %s: %r" % + (command, e)) + return [] + + def _parse_state_and_notify(self, output): + """ + Parses the output of the state command and emits state_changed + signal when the state changes + + @param output: list of lines that the state command printed as + its output + @type output: list + """ + for line in output: + stripped = line.strip() + if stripped == "END": + continue + parts = stripped.split(",") + if len(parts) < 5: + continue + ts, status_step, ok, ip, remote = parts + + state_dict = { + self.TS_KEY: ts, + self.STATUS_STEP_KEY: status_step, + self.OK_KEY: ok, + self.IP_KEY: ip, + self.REMOTE_KEY: remote + } + + if state_dict != self._last_state: + self.state_changed.emit(state_dict) + self._last_state = state_dict + + def _parse_status_and_notify(self, output): + """ + Parses the output of the status command and emits + status_changed signal when the status changes + + @param output: list of lines that the status command printed + as its output + @type output: list + """ + tun_tap_read = "" + tun_tap_write = "" + tcp_udp_read = "" + tcp_udp_write = "" + auth_read = "" + for line in output: + stripped = line.strip() + if stripped.endswith("STATISTICS") or stripped == "END": + continue + parts = stripped.split(",") + if len(parts) < 2: + continue + if parts[0].strip() == "TUN/TAP read bytes": + tun_tap_read = parts[1] + elif parts[0].strip() == "TUN/TAP write bytes": + tun_tap_write = parts[1] + elif parts[0].strip() == "TCP/UDP read bytes": + tcp_udp_read = parts[1] + elif parts[0].strip() == "TCP/UDP write bytes": + tcp_udp_write = parts[1] + elif parts[0].strip() == "Auth read bytes": + auth_read = parts[1] + + status_dict = { + self.TUNTAP_READ_KEY: tun_tap_read, + self.TUNTAP_WRITE_KEY: tun_tap_write, + self.TCPUDP_READ_KEY: tcp_udp_read, + self.TCPUDP_WRITE_KEY: tcp_udp_write, + self.AUTH_READ_KEY: auth_read + } + + if status_dict != self._last_status: + self.status_changed.emit(status_dict) + self._last_status = status_dict + + def run(self): + """ + Main run loop for this thread + """ + while True: + if self.get_should_quit(): + logger.debug("Quitting VPN thread") + return + + if self._tn is None: + self._connect(self._host, self._port) + QtCore.QThread.msleep(self.CONNECTION_RETRY_TIME) + else: + self._parse_state_and_notify(self._send_command("state")) + self._parse_status_and_notify(self._send_command("status")) + QtCore.QThread.msleep(self.POLL_TIME) + + +if __name__ == "__main__": + app = QtGui.QApplication(sys.argv) + + import signal + + def sigint_handler(*args, **kwargs): + logger.debug('SIGINT catched. shutting down...') + vpn_thread = args[0] + vpn_thread.set_should_quit() + QtGui.QApplication.quit() + + def signal_tester(d): + print d + + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + vpn_thread = VPN() + + sigint = partial(sigint_handler, vpn_thread) + signal.signal(signal.SIGINT, sigint) + + eipconfig = EIPConfig() + if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): + provider = ProviderConfig() + if provider.load("leap/providers/bitmask.net/provider.json"): + vpn_thread.start(eipconfig=eipconfig, + providerconfig=provider, + socket_host="/home/chiiph/vpnsock", + socket_port="unix") + + timer = QtCore.QTimer() + timer.start(500) + timer.timeout.connect(lambda: None) + app.connect(app, QtCore.SIGNAL("aboutToQuit()"), + vpn_thread.set_should_quit) + w = QtGui.QWidget() + w.resize(100, 100) + w.show() + + vpn_thread.state_changed.connect(signal_tester) + vpn_thread.status_changed.connect(signal_tester) + + sys.exit(app.exec_()) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py new file mode 100644 index 00000000..68978248 --- /dev/null +++ b/src/leap/services/eip/vpnlaunchers.py @@ -0,0 +1,270 @@ +# -*- coding: utf-8 -*- +# vpnlaunchers.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Platform dependant VPN launchers +""" +import os +import platform +import logging +import commands +import getpass +import grp + +from abc import ABCMeta, abstractmethod + +from leap.config.providerconfig import ProviderConfig +from leap.services.eip.eipconfig import EIPConfig + +logger = logging.getLogger(__name__) + + +class VPNLauncher: + """ + Abstract launcher class + """ + + __metaclass__ = ABCMeta + + # TODO: document parameters + @abstractmethod + def get_vpn_command(self, eipconfig=None, providerconfig=None, + socket_host=None, socket_port=None): + """ + Returns the platform dependant vpn launching command + + @param eipconfig: eip configuration object + @type eipconfig: EIPConfig + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + @param socket_host: either socket path (unix) or socket IP + @type socket_host: str + @param socket_port: either string "unix" if it's a unix + socket, or port otherwise + @type socket_port: str + + @return: A VPN command ready to be launched + @rtype: list + """ + return [] + + +def get_platform_launcher(): + launcher = globals()[platform.system() + "VPNLauncher"] + assert launcher, "Unimplemented platform launcher: %s" % \ + (platform.system(),) + return launcher() + + +# Twisted implementation of which +def which(name, flags=os.X_OK): + """ + Search PATH for executable files with the given name. + + On newer versions of MS-Windows, the PATHEXT environment variable will be + set to the list of file extensions for files considered executable. This + will normally include things like ".EXE". This fuction will also find files + with the given name ending with any of these extensions. + + On MS-Windows the only flag that has any meaning is os.F_OK. Any other + flags will be ignored. + + @type name: C{str} + @param name: The name for which to search. + + @type flags: C{int} + @param flags: Arguments to L{os.access}. + + @rtype: C{list} + @param: A list of the full paths to files found, in the + order in which they were found. + """ + + # TODO: make sure sbin is in path + + result = [] + exts = filter(None, os.environ.get('PATHEXT', '').split(os.pathsep)) + path = os.environ.get('PATH', None) + if path is None: + return [] + for p in os.environ.get('PATH', '').split(os.pathsep): + p = os.path.join(p, name) + if os.access(p, flags): + result.append(p) + for e in exts: + pext = p + e + if os.access(pext, flags): + result.append(pext) + return result + + +def _is_pkexec_in_system(): + pkexec_path = which('pkexec') + if len(pkexec_path) == 0: + return False + return True + + +def _has_updown_scripts(path): + """ + Checks the existence of the up/down scripts + """ + # XXX should check permissions too + is_file = os.path.isfile(path) + if not is_file: + logger.warning("Could not find up/down scripts. " + + "Might produce DNS leaks.") + return is_file + + +def _is_auth_agent_running(): + return bool( + commands.getoutput( + 'ps aux | grep polkit-[g]nome-authentication-agent-1')) + + +class LinuxVPNLauncher(VPNLauncher): + """ + VPN launcher for the Linux platform + """ + + PKEXEC_BIN = 'pkexec' + OPENVPN_BIN = 'openvpn' + UP_DOWN_SCRIPT = "/etc/leap/resolv-update" + OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so" + + def get_vpn_command(self, eipconfig=None, providerconfig=None, + socket_host=None, socket_port="unix"): + """ + Returns the platform dependant vpn launching command + + @param eipconfig: eip configuration object + @type eipconfig: EIPConfig + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + @param socket_host: either socket path (unix) or socket IP + @type socket_host: str + @param socket_port: either string "unix" if it's a unix + socket, or port otherwise + @type socket_port: str + + @return: A VPN command ready to be launched + @rtype: list + """ + assert eipconfig, "We need an eip config" + assert isinstance(eipconfig, EIPConfig), "Expected EIPConfig " + \ + "object instead of %s" % (type(eipconfig),) + assert providerconfig, "We need a provider config" + assert isinstance(providerconfig, ProviderConfig), "Expected " + \ + "ProviderConfig object instead of %s" % (type(providerconfig),) + assert socket_host, "We need a socket host!" + assert socket_port, "We need a socket port!" + + openvpn_possibilities = which(self.OPENVPN_BIN) + assert len(openvpn_possibilities) > 0, "We couldn't find openvpn" + + openvpn = openvpn_possibilities[0] + args = [] + + if _is_pkexec_in_system(): + if _is_auth_agent_running(): + pkexec_possibilities = which(self.PKEXEC_BIN) + assert len(pkexec_possibilities) > 0, "We couldn't find pkexec" + args.append(openvpn) + openvpn = pkexec_possibilities[0] + else: + logger.warning("No polkit auth agent found. pkexec " + + "will use its own auth agent.") + else: + logger.warning("System has no pkexec") + + # TODO: handle verbosity + + gateway_ip = str(eipconfig.get_gateway_ip(0)) + + logger.debug("Using gateway ip %s" % (gateway_ip,)) + + args += [ + '--client', + '--dev', 'tun', + '--persist-tun', + '--persist-key', + '--remote', gateway_ip, '1194', 'udp', + '--tls-client', + '--remote-cert-tls', + 'server' + ] + + openvpn_configuration = eipconfig.get_openvpn_configuration() + for key, value in openvpn_configuration.items(): + args += ['--%s' % (key,), value] + + args += [ + '--user', getpass.getuser(), + '--group', grp.getgrgid(os.getgroups()[-1]).gr_name, + '--management-client-user', getpass.getuser(), + '--management-signal', + '--management', socket_host, socket_port, + '--script-security', '2' + ] + + if _has_updown_scripts(self.UP_DOWN_SCRIPT): + args += [ + '--up', self.UP_DOWN_SCRIPT, + '--down', self.UP_DOWN_SCRIPT, + '--plugin', self.OPENVPN_DOWN_ROOT, + '\'script_type=down %s\'' % self.UP_DOWN_SCRIPT + ] + + args += [ + '--cert', eipconfig.get_client_cert_path(providerconfig), + '--key', eipconfig.get_client_cert_path(providerconfig), + '--ca', providerconfig.get_ca_cert_path() + ] + + logger.debug("Running VPN with command:") + logger.debug("%s %s" % (openvpn, " ".join(args))) + + return [openvpn] + args + + +if __name__ == "__main__": + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + try: + abs_launcher = VPNLauncher() + except Exception as e: + assert isinstance(e, TypeError), "Something went wrong" + print "Abstract Prefixer class is working as expected" + + vpnlauncher = get_platform_launcher() + + eipconfig = EIPConfig() + if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): + provider = ProviderConfig() + if provider.load("leap/providers/bitmask.net/provider.json"): + vpnlauncher.get_vpn_command(eipconfig=eipconfig, + providerconfig=provider, + socket_host="/blah") -- cgit v1.2.3 From 751638b4eb8208e1eaa1beaaed284da6b412bca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 7 Mar 2013 19:05:11 -0300 Subject: Change asserts for a custom leap_assert method Also: - Make SRPAuth and the Bootstrappers be a QObject instead of a QThread so we can use them inside another more generic thread - Add a generic CheckerThread that runs checks or whatever operation as long as it returns a boolean value - Closes the whole application if the wizard is rejected at the first run - Do not fail when the config directory doesn't exist - Set the wizard pixmap logo as LEAP's logo - Improve wizard checks - Make SRPRegister play nice with the CheckerThread --- src/leap/services/eip/eipbootstrapper.py | 101 +++++-------------- src/leap/services/eip/eipconfig.py | 13 ++- src/leap/services/eip/providerbootstrapper.py | 137 ++++++++------------------ src/leap/services/eip/vpn.py | 15 ++- src/leap/services/eip/vpnlaunchers.py | 24 ++--- 5 files changed, 90 insertions(+), 200 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 77d7020a..bd6ab715 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -28,11 +28,13 @@ from PySide import QtGui, QtCore from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig +from leap.util.check import leap_assert, leap_assert_type +from leap.util.checkerthread import CheckerThread logger = logging.getLogger(__name__) -class EIPBootstrapper(QtCore.QThread): +class EIPBootstrapper(QtCore.QObject): """ Sets up EIP for a provider a series of checks and emits signals after they are passed. @@ -50,13 +52,7 @@ class EIPBootstrapper(QtCore.QThread): download_client_certificate = QtCore.Signal(dict) def __init__(self): - QtCore.QThread.__init__(self) - - self._checks = [] - self._checks_lock = QtCore.QMutex() - - self._should_quit = False - self._should_quit_lock = QtCore.QMutex() + QtCore.QObject.__init__(self) # **************************************************** # # Dependency injection helpers, override this for more @@ -69,35 +65,6 @@ class EIPBootstrapper(QtCore.QThread): self._eip_config = None self._download_if_needed = False - def get_should_quit(self): - """ - Returns wether this thread should quit - - @rtype: bool - @return: True if the thread should terminate itself, Flase otherwise - """ - - QtCore.QMutexLocker(self._should_quit_lock) - return self._should_quit - - def set_should_quit(self): - """ - Sets the should_quit flag to True so that this thread - terminates the first chance it gets - """ - QtCore.QMutexLocker(self._should_quit_lock) - self._should_quit = True - self.wait() - - def start(self): - """ - Starts the thread and resets the should_quit flag - """ - with QtCore.QMutexLocker(self._should_quit_lock): - self._should_quit = False - - QtCore.QThread.start(self) - def _download_config(self): """ Downloads the EIP config for the given provider @@ -106,7 +73,8 @@ class EIPBootstrapper(QtCore.QThread): @rtype: bool """ - assert self._provider_config, "We need a provider configuration!" + leap_assert(self._provider_config, + "We need a provider configuration!") logger.debug("Downloading EIP config for %s" % (self._provider_config.get_domain(),)) @@ -162,8 +130,8 @@ class EIPBootstrapper(QtCore.QThread): @return: True if the checks passed, False otherwise @rtype: bool """ - assert self._provider_config, "We need a provider configuration!" - assert self._eip_config, "We need an eip configuration!" + leap_assert(self._provider_config, "We need a provider configuration!") + leap_assert(self._eip_config, "We need an eip configuration!") logger.debug("Downloading EIP client certificate for %s" % (self._provider_config.get_domain(),)) @@ -218,49 +186,25 @@ class EIPBootstrapper(QtCore.QThread): return download_cert[self.PASSED_KEY] - def run_eip_setup_checks(self, provider_config, download_if_needed=False): + def run_eip_setup_checks(self, checker, + provider_config, + download_if_needed=False): """ Starts the checks needed for a new eip setup @param provider_config: Provider configuration @type provider_config: ProviderConfig """ - assert provider_config, "We need a provider config!" - assert isinstance(provider_config, ProviderConfig), "Expected " + \ - "ProviderConfig type, not %r" % (type(provider_config),) + leap_assert(provider_config, "We need a provider config!") + leap_assert_type(provider_config, ProviderConfig) self._provider_config = provider_config self._download_if_needed = download_if_needed - QtCore.QMutexLocker(self._checks_lock) - self._checks = [ + checker.add_checks([ self._download_config, self._download_client_certificates - ] - - def run(self): - """ - Main run loop for this thread. Executes the checks. - """ - shouldContinue = False - while True: - if self.get_should_quit(): - logger.debug("Quitting provider bootstrap thread") - return - checkSomething = False - with QtCore.QMutexLocker(self._checks_lock): - if len(self._checks) > 0: - check = self._checks.pop(0) - shouldContinue = check() - checkSomething = True - if not shouldContinue: - logger.debug("Something went wrong with the checks, " - - "clearing...") - self._checks = [] - checkSomething = False - if not checkSomething: - self.usleep(self.IDLE_SLEEP_INTERVAL) + ]) if __name__ == "__main__": @@ -272,8 +216,8 @@ if __name__ == "__main__": def sigint_handler(*args, **kwargs): logger.debug('SIGINT catched. shutting down...') - bootstrapper_thread = args[0] - bootstrapper_thread.set_should_quit() + checker = args[0] + checker.set_should_quit() QtGui.QApplication.quit() def signal_tester(d): @@ -289,27 +233,28 @@ if __name__ == "__main__": console.setFormatter(formatter) logger.addHandler(console) - eip_thread = EIPBootstrapper() + eip_checks = EIPBootstrapper() + checker = CheckerThread() - sigint = partial(sigint_handler, eip_thread) + sigint = partial(sigint_handler, checker) signal.signal(signal.SIGINT, sigint) timer = QtCore.QTimer() timer.start(500) timer.timeout.connect(lambda: None) app.connect(app, QtCore.SIGNAL("aboutToQuit()"), - eip_thread.set_should_quit) + checker.set_should_quit) w = QtGui.QWidget() w.resize(100, 100) w.show() - eip_thread.start() + checker.start() provider_config = ProviderConfig() if provider_config.load(os.path.join("leap", "providers", "bitmask.net", "provider.json")): - eip_thread.run_eip_setup_checks(provider_config) + eip_checks.run_eip_setup_checks(checker, provider_config) sys.exit(app.exec_()) diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index ac06fef1..eab5bfd4 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -24,6 +24,7 @@ import logging from leap.config.baseconfig import BaseConfig from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipspec import eipservice_config_spec +from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) @@ -61,7 +62,7 @@ class EIPConfig(BaseConfig): def get_gateway_ip(self, index=0): gateways = self.get_gateways() - assert len(gateways) > 0, "We don't have any gateway!" + leap_assert(len(gateways) > 0, "We don't have any gateway!") if index > len(gateways): index = 0 logger.warning("Provided an unknown gateway index %s, " + @@ -75,10 +76,8 @@ class EIPConfig(BaseConfig): Returns the path to the certificate used by openvpn """ - assert providerconfig, "We need a provider" - assert isinstance(providerconfig, ProviderConfig), "The provider " + \ - "needs to be of type ProviderConfig instead of %s" % \ - (type(providerconfig),) + leap_assert(providerconfig, "We need a provider") + leap_assert_type(providerconfig, ProviderConfig) cert_path = os.path.join(self.get_path_prefix(), "leap", @@ -89,8 +88,8 @@ class EIPConfig(BaseConfig): "openvpn.pem") if not about_to_download: - assert os.path.exists(cert_path), \ - "You need to download the certificate first" + leap_assert(os.path.exists(cert_path), + "You need to download the certificate first") logger.debug("Using OpenVPN cert %s" % (cert_path,)) return cert_path diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index babcd47b..ecdc4e07 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -29,11 +29,13 @@ from OpenSSL import crypto from PySide import QtGui, QtCore from leap.config.providerconfig import ProviderConfig +from leap.util.check import leap_assert, leap_assert_type +from leap.util.checkerthread import CheckerThread logger = logging.getLogger(__name__) -class ProviderBootstrapper(QtCore.QThread): +class ProviderBootstrapper(QtCore.QObject): """ Given a provider URL performs a series of checks and emits signals after they are passed. @@ -56,13 +58,7 @@ class ProviderBootstrapper(QtCore.QThread): check_api_certificate = QtCore.Signal(dict) def __init__(self): - QtCore.QThread.__init__(self) - - self._checks = [] - self._checks_lock = QtCore.QMutex() - - self._should_quit = False - self._should_quit_lock = QtCore.QMutex() + QtCore.QObject.__init__(self) # **************************************************** # # Dependency injection helpers, override this for more @@ -75,35 +71,6 @@ class ProviderBootstrapper(QtCore.QThread): self._provider_config = None self._download_if_needed = False - def get_should_quit(self): - """ - Returns wether this thread should quit - - @rtype: bool - @return: True if the thread should terminate itself, Flase otherwise - """ - - QtCore.QMutexLocker(self._should_quit_lock) - return self._should_quit - - def set_should_quit(self): - """ - Sets the should_quit flag to True so that this thread - terminates the first chance it gets - """ - QtCore.QMutexLocker(self._should_quit_lock) - self._should_quit = True - self.wait() - - def start(self): - """ - Starts the thread and resets the should_quit flag - """ - with QtCore.QMutexLocker(self._should_quit_lock): - self._should_quit = False - - QtCore.QThread.start(self) - def _should_proceed_provider(self): """ Returns False if provider.json already exists for the given @@ -131,7 +98,7 @@ class ProviderBootstrapper(QtCore.QThread): @rtype: bool """ - assert self._domain, "Cannot check DNS without a domain" + leap_assert(self._domain, "Cannot check DNS without a domain") logger.debug("Checking name resolution for %s" % (self._domain)) @@ -162,7 +129,7 @@ class ProviderBootstrapper(QtCore.QThread): @rtype: bool """ - assert self._domain, "Cannot check HTTPS without a domain" + leap_assert(self._domain, "Cannot check HTTPS without a domain") logger.debug("Checking https for %s" % (self._domain)) @@ -193,7 +160,8 @@ class ProviderBootstrapper(QtCore.QThread): @return: True if the checks passed, False otherwise @rtype: bool """ - assert self._domain, "Cannot download provider info without a domain" + leap_assert(self._domain, + "Cannot download provider info without a domain") logger.debug("Downloading provider info for %s" % (self._domain)) @@ -230,7 +198,8 @@ class ProviderBootstrapper(QtCore.QThread): return download_data[self.PASSED_KEY] - def run_provider_select_checks(self, domain, download_if_needed=False): + def run_provider_select_checks(self, checker, + domain, download_if_needed=False): """ Populates the check queue @@ -243,17 +212,16 @@ class ProviderBootstrapper(QtCore.QThread): @return: True if the checks passed, False otherwise @rtype: bool """ - assert domain and len(domain) > 0, "We need a domain!" + leap_assert(domain and len(domain) > 0, "We need a domain!") self._domain = domain self._download_if_needed = download_if_needed - QtCore.QMutexLocker(self._checks_lock) - self._checks = [ + checker.add_checks([ self._check_name_resolution, self._check_https, self._download_provider_info - ] + ]) def _should_proceed_cert(self): """ @@ -262,7 +230,7 @@ class ProviderBootstrapper(QtCore.QThread): @rtype: bool """ - assert self._provider_config, "We need a provider config!" + leap_assert(self._provider_config, "We need a provider config!") if not self._download_if_needed: return True @@ -278,8 +246,8 @@ class ProviderBootstrapper(QtCore.QThread): @rtype: bool """ - assert self._provider_config, "Cannot download the ca cert " + \ - "without a provider config!" + leap_assert(self._provider_config, "Cannot download the ca cert " + "without a provider config!") logger.debug("Downloading ca cert for %s at %s" % (self._domain, self._provider_config.get_ca_cert_uri())) @@ -331,8 +299,8 @@ class ProviderBootstrapper(QtCore.QThread): @return: True if the checks passed, False otherwise @rtype: bool """ - assert self._provider_config, "Cannot check the ca cert " + \ - "without a provider config!" + leap_assert(self._provider_config, "Cannot check the ca cert " + "without a provider config!") logger.debug("Checking ca fingerprint for %s and cert %s" % (self._domain, @@ -350,7 +318,7 @@ class ProviderBootstrapper(QtCore.QThread): try: parts = self._provider_config.get_ca_cert_fingerprint().split(":") - assert len(parts) == 2, "Wrong fingerprint format" + leap_assert(len(parts) == 2, "Wrong fingerprint format") method = parts[0].strip() fingerprint = parts[1].strip() @@ -358,13 +326,13 @@ class ProviderBootstrapper(QtCore.QThread): with open(self._provider_config.get_ca_cert_path()) as f: cert_data = f.read() - assert len(cert_data) > 0, "Could not read certificate data" + leap_assert(len(cert_data) > 0, "Could not read certificate data") x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert_data) digest = x509.digest(method).replace(":", "").lower() - assert digest == fingerprint, \ - "Downloaded certificate has a different fingerprint!" + leap_assert(digest == fingerprint, + "Downloaded certificate has a different fingerprint!") check_ca_fingerprint_data[self.PASSED_KEY] = True except Exception as e: @@ -384,8 +352,8 @@ class ProviderBootstrapper(QtCore.QThread): @return: True if the checks passed, False otherwise @rtype: bool """ - assert self._provider_config, "Cannot check the ca cert " + \ - "without a provider config!" + leap_assert(self._provider_config, "Cannot check the ca cert " + "without a provider config!") logger.debug("Checking api certificate for %s and cert %s" % (self._provider_config.get_api_uri(), @@ -418,7 +386,8 @@ class ProviderBootstrapper(QtCore.QThread): return check_api_certificate_data[self.PASSED_KEY] - def run_provider_setup_checks(self, provider_config, + def run_provider_setup_checks(self, checker, + provider_config, download_if_needed=False): """ Starts the checks needed for a new provider setup @@ -429,43 +398,17 @@ class ProviderBootstrapper(QtCore.QThread): overwrite already downloaded data @type download_if_needed: bool """ - assert provider_config, "We need a provider config!" - assert isinstance(provider_config, ProviderConfig), "Expected " + \ - "ProviderConfig type, not %r" % (type(provider_config),) + leap_assert(provider_config, "We need a provider config!") + leap_assert_type(provider_config, ProviderConfig) self._provider_config = provider_config self._download_if_needed = download_if_needed - QtCore.QMutexLocker(self._checks_lock) - self._checks = [ + checker.add_checks([ self._download_ca_cert, self._check_ca_fingerprint, self._check_api_certificate - ] - - def run(self): - """ - Main run loop for this thread. Executes the checks. - """ - shouldContinue = False - while True: - if self.get_should_quit(): - logger.debug("Quitting provider bootstrap thread") - return - checkSomething = False - with QtCore.QMutexLocker(self._checks_lock): - if len(self._checks) > 0: - check = self._checks.pop(0) - shouldContinue = check() - checkSomething = True - if not shouldContinue: - logger.debug("Something went wrong with the checks, " - "clearing...") - self._checks = [] - checkSomething = False - if not checkSomething: - self.usleep(self.IDLE_SLEEP_INTERVAL) - + ]) if __name__ == "__main__": import sys @@ -476,8 +419,8 @@ if __name__ == "__main__": def sigint_handler(*args, **kwargs): logger.debug('SIGINT catched. shutting down...') - bootstrapper_thread = args[0] - bootstrapper_thread.set_should_quit() + bootstrapper_checks = args[0] + bootstrapper_checks.set_should_quit() QtGui.QApplication.quit() def signal_tester(d): @@ -493,28 +436,32 @@ if __name__ == "__main__": console.setFormatter(formatter) logger.addHandler(console) - bootstrapper_thread = ProviderBootstrapper() + bootstrapper_checks = ProviderBootstrapper() + + checker = CheckerThread() + checker.start() - sigint = partial(sigint_handler, bootstrapper_thread) + sigint = partial(sigint_handler, checker) signal.signal(signal.SIGINT, sigint) timer = QtCore.QTimer() timer.start(500) timer.timeout.connect(lambda: None) app.connect(app, QtCore.SIGNAL("aboutToQuit()"), - bootstrapper_thread.set_should_quit) + checker.set_should_quit) w = QtGui.QWidget() w.resize(100, 100) w.show() - bootstrapper_thread.start() - bootstrapper_thread.run_provider_select_checks("bitmask.net") + bootstrapper_checks.run_provider_select_checks(checker, + "bitmask.net") provider_config = ProviderConfig() if provider_config.load(os.path.join("leap", "providers", "bitmask.net", "provider.json")): - bootstrapper_thread.run_provider_setup_checks(provider_config) + bootstrapper_checks.run_provider_setup_checks(checker, + provider_config) sys.exit(app.exec_()) diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index f117cdbc..88692442 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -29,6 +29,7 @@ from leap.config.providerconfig import ProviderConfig from leap.services.eip.vpnlaunchers import get_platform_launcher from leap.services.eip.eipconfig import EIPConfig from leap.services.eip.udstelnet import UDSTelnet +from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) ON_POSIX = 'posix' in sys.builtin_module_names @@ -123,13 +124,11 @@ class VPN(QtCore.QThread): socket, or port otherwise @type socket_port: str """ - assert eipconfig, "We need an eip config" - assert isinstance(eipconfig, EIPConfig), "Expected EIPConfig " + \ - "object instead of %s" % (type(eipconfig),) - assert providerconfig, "We need a provider config" - assert isinstance(providerconfig, ProviderConfig), "Expected " + \ - "ProviderConfig object instead of %s" % (type(providerconfig),) - assert not self._started, "Starting process more than once!" + leap_assert(eipconfig, "We need an eip config") + leap_assert_type(eipconfig, EIPConfig) + leap_assert(providerconfig, "We need a provider config") + leap_assert_type(providerconfig, ProviderConfig) + leap_assert(not self._started, "Starting process more than once!") logger.debug("Starting VPN...") @@ -202,7 +201,7 @@ class VPN(QtCore.QThread): @return: response read @rtype: list """ - assert self._tn, "We need a tn connection!" + leap_assert(self._tn, "We need a tn connection!") try: self._tn.write("%s\n" % (command,)) buf = self._tn.read_until(until, 2) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 68978248..00e9c966 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -29,6 +29,7 @@ from abc import ABCMeta, abstractmethod from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig +from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) @@ -65,8 +66,8 @@ class VPNLauncher: def get_platform_launcher(): launcher = globals()[platform.system() + "VPNLauncher"] - assert launcher, "Unimplemented platform launcher: %s" % \ - (platform.system(),) + leap_assert(launcher, "Unimplemented platform launcher: %s" % + (platform.system(),)) return launcher() @@ -165,17 +166,15 @@ class LinuxVPNLauncher(VPNLauncher): @return: A VPN command ready to be launched @rtype: list """ - assert eipconfig, "We need an eip config" - assert isinstance(eipconfig, EIPConfig), "Expected EIPConfig " + \ - "object instead of %s" % (type(eipconfig),) - assert providerconfig, "We need a provider config" - assert isinstance(providerconfig, ProviderConfig), "Expected " + \ - "ProviderConfig object instead of %s" % (type(providerconfig),) - assert socket_host, "We need a socket host!" - assert socket_port, "We need a socket port!" + leap_assert(eipconfig, "We need an eip config") + leap_assert_type(eipconfig, EIPConfig) + leap_assert(providerconfig, "We need a provider config") + leap_assert_type(providerconfig, ProviderConfig) + leap_assert(socket_host, "We need a socket host!") + leap_assert(socket_port, "We need a socket port!") openvpn_possibilities = which(self.OPENVPN_BIN) - assert len(openvpn_possibilities) > 0, "We couldn't find openvpn" + leap_assert(len(openvpn_possibilities) > 0, "We couldn't find openvpn") openvpn = openvpn_possibilities[0] args = [] @@ -183,7 +182,8 @@ class LinuxVPNLauncher(VPNLauncher): if _is_pkexec_in_system(): if _is_auth_agent_running(): pkexec_possibilities = which(self.PKEXEC_BIN) - assert len(pkexec_possibilities) > 0, "We couldn't find pkexec" + leap_assert(len(pkexec_possibilities) > 0, + "We couldn't find pkexec") args.append(openvpn) openvpn = pkexec_possibilities[0] else: -- cgit v1.2.3 From 01a7faa2033ef3ce85bc5a346eca3601f0f4f7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 8 Mar 2013 11:09:23 -0300 Subject: Migrate VPN process to QProcess Also: - Add a new tray icon for the whole app and a VPN specific one - Add a way to start/stop EIP independently - Improve reaction to the process dying --- src/leap/services/eip/vpn.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index 88692442..71944f50 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -22,7 +22,6 @@ import logging import sys from PySide import QtCore, QtGui -from subprocess import Popen, PIPE from functools import partial from leap.config.providerconfig import ProviderConfig @@ -46,6 +45,8 @@ class VPN(QtCore.QThread): state_changed = QtCore.Signal(dict) status_changed = QtCore.Signal(dict) + process_finished = QtCore.Signal(int) + CONNECTION_RETRY_TIME = 1000 POLL_TIME = 100 @@ -69,7 +70,6 @@ class VPN(QtCore.QThread): self._launcher = get_platform_launcher() self._subp = None - self._started = False self._tn = None self._host = None @@ -100,15 +100,14 @@ class VPN(QtCore.QThread): return try: - self._disconnect() + self._send_command("signal SIGTERM") + self._tn.close() self._subp.terminate() except Exception as e: logger.debug("Could not terminate process, trying command " + "signal SIGNINT: %r" % (e,)) - self._send_command("signal SIGINT") - self._subp.wait() - self.wait() - self._started = False + finally: + self._tn = None def start(self, eipconfig, providerconfig, socket_host, socket_port): """ @@ -128,7 +127,7 @@ class VPN(QtCore.QThread): leap_assert_type(eipconfig, EIPConfig) leap_assert(providerconfig, "We need a provider config") leap_assert_type(providerconfig, ProviderConfig) - leap_assert(not self._started, "Starting process more than once!") + leap_assert(not self.isRunning(), "Starting process more than once!") logger.debug("Starting VPN...") @@ -140,8 +139,12 @@ class VPN(QtCore.QThread): socket_host=socket_host, socket_port=socket_port) try: - self._subp = Popen(command, stdout=PIPE, stderr=PIPE, - bufsize=1, close_fds=ON_POSIX) + self._subp = QtCore.QProcess() + self._subp.finished.connect(self.process_finished) + self._subp.start(command[:1][0], command[1:]) + logger.debug("Waiting for started...") + self._subp.waitForStarted() + logger.debug("Started!") self._host = socket_host self._port = socket_port @@ -296,12 +299,18 @@ class VPN(QtCore.QThread): logger.debug("Quitting VPN thread") return + if self._subp and self._subp.state() != QtCore.QProcess.Running: + QtCore.QThread.msleep(self.CONNECTION_RETRY_TIME) + if self._tn is None: self._connect(self._host, self._port) QtCore.QThread.msleep(self.CONNECTION_RETRY_TIME) else: self._parse_state_and_notify(self._send_command("state")) self._parse_status_and_notify(self._send_command("status")) + output_sofar = self._subp.readAllStandardOutput() + if len(output_sofar) > 0: + logger.debug(output_sofar) QtCore.QThread.msleep(self.POLL_TIME) -- cgit v1.2.3 From 926575bc811e8382100695a3396da7191fb43eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 8 Mar 2013 13:15:38 -0300 Subject: Add translation support Also: - Make OpenVPN use a random port every time - Logout in parallel so the UI doesn't block - Add the WAIT status from OpenVPN to the mainwindow displays - Support non-unix sockets in the LinuxVPNLauncher --- src/leap/services/eip/vpn.py | 5 ++++- src/leap/services/eip/vpnlaunchers.py | 11 +++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index 71944f50..3ec32f6f 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -31,7 +31,6 @@ from leap.services.eip.udstelnet import UDSTelnet from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) -ON_POSIX = 'posix' in sys.builtin_module_names # TODO: abstract the thread that can be asked to quit to another @@ -103,6 +102,7 @@ class VPN(QtCore.QThread): self._send_command("signal SIGTERM") self._tn.close() self._subp.terminate() + self._subp.waitForFinished() except Exception as e: logger.debug("Could not terminate process, trying command " + "signal SIGNINT: %r" % (e,)) @@ -309,6 +309,9 @@ class VPN(QtCore.QThread): self._parse_state_and_notify(self._send_command("state")) self._parse_status_and_notify(self._send_command("status")) output_sofar = self._subp.readAllStandardOutput() + if len(output_sofar) > 0: + logger.debug(output_sofar) + output_sofar = self._subp.readAllStandardError() if len(output_sofar) > 0: logger.debug(output_sofar) QtCore.QThread.msleep(self.POLL_TIME) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 00e9c966..cf817321 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -215,8 +215,15 @@ class LinuxVPNLauncher(VPNLauncher): args += [ '--user', getpass.getuser(), - '--group', grp.getgrgid(os.getgroups()[-1]).gr_name, - '--management-client-user', getpass.getuser(), + '--group', grp.getgrgid(os.getgroups()[-1]).gr_name + ] + + if socket_port == "unix": + args += [ + '--management-client-user', getpass.getuser() + ] + + args += [ '--management-signal', '--management', socket_host, socket_port, '--script-security', '2' -- cgit v1.2.3 From 060984df444dbf407e9078de638ef7486ef7b0a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 11 Mar 2013 16:42:23 -0300 Subject: Notify if openvpn is not found --- src/leap/services/eip/vpnlaunchers.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index cf817321..9f2967aa 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -34,6 +34,10 @@ from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) +class VPNLauncherException(Exception): + pass + + class VPNLauncher: """ Abstract launcher class @@ -153,6 +157,8 @@ class LinuxVPNLauncher(VPNLauncher): """ Returns the platform dependant vpn launching command + Might raise VPNException. + @param eipconfig: eip configuration object @type eipconfig: EIPConfig @param providerconfig: provider specific configuration @@ -174,7 +180,8 @@ class LinuxVPNLauncher(VPNLauncher): leap_assert(socket_port, "We need a socket port!") openvpn_possibilities = which(self.OPENVPN_BIN) - leap_assert(len(openvpn_possibilities) > 0, "We couldn't find openvpn") + if len(openvpn_possibilities) == 0: + raise VPNLauncherException("We couldn't find openvpn") openvpn = openvpn_possibilities[0] args = [] -- cgit v1.2.3 From cf83b3ffe300058a4ed30d255481f0c781b0b942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 09:02:08 -0300 Subject: Extend path to sbin by default --- src/leap/services/eip/vpnlaunchers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 9f2967aa..6f277481 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -76,7 +76,7 @@ def get_platform_launcher(): # Twisted implementation of which -def which(name, flags=os.X_OK): +def which(name, flags=os.X_OK, path_extension="/usr/sbin:/sbin"): """ Search PATH for executable files with the given name. @@ -99,14 +99,14 @@ def which(name, flags=os.X_OK): order in which they were found. """ - # TODO: make sure sbin is in path - result = [] exts = filter(None, os.environ.get('PATHEXT', '').split(os.pathsep)) path = os.environ.get('PATH', None) + path += ":" + path_extension if path is None: return [] - for p in os.environ.get('PATH', '').split(os.pathsep): + parts = path.split(os.pathsep) + for p in parts: p = os.path.join(p, name) if os.access(p, flags): result.append(p) -- cgit v1.2.3 From 2da60cd0f78378fdcb8f6364a798720281b34b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 09:56:05 -0300 Subject: Check and try to fix certificate permissions --- src/leap/services/eip/eipbootstrapper.py | 12 ++++++++++-- src/leap/services/eip/providerbootstrapper.py | 14 ++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index bd6ab715..79ff28d6 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -30,6 +30,7 @@ from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread +from leap.util.files import check_and_fix_urw_only logger = logging.getLogger(__name__) @@ -147,9 +148,14 @@ class EIPBootstrapper(QtCore.QObject): if self._download_if_needed and \ os.path.exists(client_cert_path): - download_cert[self.PASSED_KEY] = True + try: + check_and_fix_urw_only(client_cert_path) + download_cert[self.PASSED_KEY] = True + except Exception as e: + download_cert[self.PASSED_KEY] = False + download_cert[self.ERROR_KEY] = "%s" % (e,) self.download_client_certificate.emit(download_cert) - return True + return download_cert[self.PASSED_KEY] try: res = self._session.get("%s/%s/%s/" % @@ -176,6 +182,8 @@ class EIPBootstrapper(QtCore.QObject): with open(client_cert_path, "w") as f: f.write(client_cert) + check_and_fix_urw_only(client_cert_path) + download_cert[self.PASSED_KEY] = True except Exception as e: download_cert[self.ERROR_KEY] = "%s" % (e,) diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index ecdc4e07..0e9f8563 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -31,6 +31,7 @@ from PySide import QtGui, QtCore from leap.config.providerconfig import ProviderConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread +from leap.util.files import check_and_fix_urw_only logger = logging.getLogger(__name__) @@ -258,9 +259,16 @@ class ProviderBootstrapper(QtCore.QObject): } if not self._should_proceed_cert(): - download_ca_cert_data[self.PASSED_KEY] = True + try: + check_and_fix_urw_only( + self._provider_config + .get_ca_cert_path(about_to_download=True)) + download_ca_cert_data[self.PASSED_KEY] = True + except Exception as e: + download_ca_cert_data[self.PASSED_KEY] = False + download_ca_cert_data[self.ERROR_KEY] = "%s" % (e,) self.download_ca_cert.emit(download_ca_cert_data) - return True + return download_ca_cert_data[self.PASSED_KEY] try: res = self._session.get(self._provider_config.get_ca_cert_uri()) @@ -282,6 +290,8 @@ class ProviderBootstrapper(QtCore.QObject): with open(cert_path, "w") as f: f.write(res.content) + check_and_fix_urw_only(cert_path) + download_ca_cert_data[self.PASSED_KEY] = True except Exception as e: download_ca_cert_data[self.ERROR_KEY] = "%s" % (e,) -- cgit v1.2.3 From a120904b512394346b286bb417adf34fc622e739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 14:26:38 -0300 Subject: Get eip cert with session_id when possible --- src/leap/services/eip/eipbootstrapper.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 79ff28d6..6fbb98b9 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -26,6 +26,7 @@ import errno from PySide import QtGui, QtCore +from leap.crypto.srpauth import SRPAuth from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig from leap.util.check import leap_assert, leap_assert_type @@ -158,12 +159,18 @@ class EIPBootstrapper(QtCore.QObject): return download_cert[self.PASSED_KEY] try: + srp_auth = SRPAuth(self._provider_config) + session_id = srp_auth.get_session_id() + cookies = None + if session_id: + cookies = {"_session_id": session_id} res = self._session.get("%s/%s/%s/" % (self._provider_config.get_api_uri(), self._provider_config.get_api_version(), "cert"), verify=self._provider_config - .get_ca_cert_path()) + .get_ca_cert_path(), + cookies=cookies) res.raise_for_status() client_cert = res.content -- cgit v1.2.3 From 70c402fe170ca4e01159b03739b7cacda7b0dfd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 09:43:13 -0300 Subject: Add mtime check for existing json definitions before download Also, wait for threads to finish when quitting --- src/leap/services/eip/eipbootstrapper.py | 44 +++++++++++--------- src/leap/services/eip/providerbootstrapper.py | 59 ++++++++++++--------------- 2 files changed, 50 insertions(+), 53 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 6fbb98b9..fdf54bbb 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -31,7 +31,7 @@ from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread -from leap.util.files import check_and_fix_urw_only +from leap.util.files import check_and_fix_urw_only, get_mtime logger = logging.getLogger(__name__) @@ -88,33 +88,39 @@ class EIPBootstrapper(QtCore.QObject): self._eip_config = EIPConfig() - if self._download_if_needed and \ - os.path.exists(os.path.join(self._eip_config.get_path_prefix(), - "leap", - "providers", - self._provider_config.get_domain(), - "eip-service.json")): - download_config_data[self.PASSED_KEY] = True - self.download_config.emit(download_config_data) - return True - try: + headers = {} + mtime = get_mtime(os.path.join(self._eip_config + .get_path_prefix(), + "leap", + "providers", + self._provider_config.get_domain(), + "eip-service.json")) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + res = self._session.get("%s/%s/%s/%s" % (self._provider_config.get_api_uri(), self._provider_config.get_api_version(), "config", "eip-service.json"), verify=self._provider_config - .get_ca_cert_path()) + .get_ca_cert_path(), + headers=headers) res.raise_for_status() - eip_definition = res.content - - self._eip_config.load(data=eip_definition) - self._eip_config.save(["leap", - "providers", - self._provider_config.get_domain(), - "eip-service.json"]) + # Not modified + if res.status_code == 304: + logger.debug("EIP definition has not been modified") + else: + eip_definition = res.content + + self._eip_config.load(data=eip_definition) + self._eip_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "eip-service.json"]) download_config_data[self.PASSED_KEY] = True except Exception as e: diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 0e9f8563..f1a917f0 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -31,7 +31,7 @@ from PySide import QtGui, QtCore from leap.config.providerconfig import ProviderConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread -from leap.util.files import check_and_fix_urw_only +from leap.util.files import check_and_fix_urw_only, get_mtime logger = logging.getLogger(__name__) @@ -72,25 +72,6 @@ class ProviderBootstrapper(QtCore.QObject): self._provider_config = None self._download_if_needed = False - def _should_proceed_provider(self): - """ - Returns False if provider.json already exists for the given - domain. True otherwise - - @rtype: bool - """ - if not self._download_if_needed: - return True - - # We don't really need a provider config at this stage, just - # the path prefix - return not os.path.exists(os.path.join(ProviderConfig() - .get_path_prefix(), - "leap", - "providers", - self._domain, - "provider.json")) - def _check_name_resolution(self): """ Checks that the name resolution for the provider name works @@ -171,24 +152,34 @@ class ProviderBootstrapper(QtCore.QObject): self.ERROR_KEY: "" } - if not self._should_proceed_provider(): - download_data[self.PASSED_KEY] = True - self.download_provider_info.emit(download_data) - return True - try: + headers = {} + mtime = get_mtime(os.path.join(ProviderConfig() + .get_path_prefix(), + "leap", + "providers", + self._domain, + "provider.json")) + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + res = self._session.get("https://%s/%s" % (self._domain, - "provider.json")) + "provider.json"), + headers=headers) res.raise_for_status() - provider_definition = res.content - - provider_config = ProviderConfig() - provider_config.load(data=provider_definition) - provider_config.save(["leap", - "providers", - self._domain, - "provider.json"]) + # Not modified + if res.status_code == 304: + logger.debug("Provider definition has not been modified") + else: + provider_definition = res.content + + provider_config = ProviderConfig() + provider_config.load(data=provider_definition) + provider_config.save(["leap", + "providers", + self._domain, + "provider.json"]) download_data[self.PASSED_KEY] = True except Exception as e: -- cgit v1.2.3 From 0ff122cf9fd0a76871093b595910fb7c0d3bfe85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 10:05:58 -0300 Subject: Pass mtime to pluggableconfig's load Also add a request_helpers file to util where all the helper methods for handling requests should go --- src/leap/services/eip/eipbootstrapper.py | 5 +++-- src/leap/services/eip/providerbootstrapper.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index fdf54bbb..84a309cb 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -32,6 +32,7 @@ from leap.services.eip.eipconfig import EIPConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread from leap.util.files import check_and_fix_urw_only, get_mtime +from leap.util.request_helpers import get_content logger = logging.getLogger(__name__) @@ -114,9 +115,9 @@ class EIPBootstrapper(QtCore.QObject): if res.status_code == 304: logger.debug("EIP definition has not been modified") else: - eip_definition = res.content + eip_definition, mtime = get_content(res) - self._eip_config.load(data=eip_definition) + self._eip_config.load(data=eip_definition, mtime=mtime) self._eip_config.save(["leap", "providers", self._provider_config.get_domain(), diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index f1a917f0..4fdd9b8d 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -32,6 +32,7 @@ from leap.config.providerconfig import ProviderConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread from leap.util.files import check_and_fix_urw_only, get_mtime +from leap.util.request_helpers import get_content logger = logging.getLogger(__name__) @@ -172,10 +173,10 @@ class ProviderBootstrapper(QtCore.QObject): if res.status_code == 304: logger.debug("Provider definition has not been modified") else: - provider_definition = res.content + provider_definition, mtime = get_content(res) provider_config = ProviderConfig() - provider_config.load(data=provider_definition) + provider_config.load(data=provider_definition, mtime=mtime) provider_config.save(["leap", "providers", self._domain, -- cgit v1.2.3 From fc434146593060c1fc454776b1feaaa80a700823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 10:11:35 -0300 Subject: Revert jsonschema version to <=0.8 --- src/leap/services/eip/eipspec.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipspec.py b/src/leap/services/eip/eipspec.py index d5c73056..69d4d927 100644 --- a/src/leap/services/eip/eipspec.py +++ b/src/leap/services/eip/eipspec.py @@ -18,18 +18,16 @@ eipservice_config_spec = { 'description': 'sample eip service config', 'type': 'object', - 'required': [ - 'serial', - 'version' - ], 'properties': { 'serial': { 'type': int, - 'default': 1 + 'default': 1, + 'required': True }, 'version': { 'type': int, - 'default': 1 + 'default': 1, + 'required': True }, 'clusters': { 'type': list, -- cgit v1.2.3 From 12d2835c7d1f3c3d11eaa587b2196c104e6859e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 10:21:15 -0300 Subject: Add mkdir_p method to util.files --- src/leap/services/eip/eipbootstrapper.py | 12 ++---------- src/leap/services/eip/providerbootstrapper.py | 10 ++-------- 2 files changed, 4 insertions(+), 18 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 84a309cb..3e4e2063 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -22,7 +22,6 @@ EIP bootstrapping import requests import logging import os -import errno from PySide import QtGui, QtCore @@ -31,7 +30,7 @@ from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread -from leap.util.files import check_and_fix_urw_only, get_mtime +from leap.util.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.util.request_helpers import get_content logger = logging.getLogger(__name__) @@ -184,14 +183,7 @@ class EIPBootstrapper(QtCore.QObject): # TODO: check certificate validity - try: - os.makedirs(os.path.dirname(client_cert_path)) - except OSError as e: - if e.errno == errno.EEXIST and \ - os.path.isdir(os.path.dirname(client_cert_path)): - pass - else: - raise + mkdir_p(os.path.dirname(client_cert_path)) with open(client_cert_path, "w") as f: f.write(client_cert) diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 4fdd9b8d..df56110e 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -31,7 +31,7 @@ from PySide import QtGui, QtCore from leap.config.providerconfig import ProviderConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread -from leap.util.files import check_and_fix_urw_only, get_mtime +from leap.util.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.util.request_helpers import get_content logger = logging.getLogger(__name__) @@ -271,13 +271,7 @@ class ProviderBootstrapper(QtCore.QObject): cert_dir = os.path.dirname(cert_path) - try: - os.makedirs(cert_dir) - except OSError as e: - if e.errno == errno.EEXIST and os.path.isdir(cert_dir): - pass - else: - raise + mkdir_p(cert_dir) with open(cert_path, "w") as f: f.write(res.content) -- cgit v1.2.3 From a12906958e4d117daaf45bd42e7383d2344ea463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 10:38:25 -0300 Subject: Add util.certs and abstract digest there --- src/leap/services/eip/providerbootstrapper.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index df56110e..dc87a1bd 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -23,9 +23,7 @@ import requests import logging import socket import os -import errno -from OpenSSL import crypto from PySide import QtGui, QtCore from leap.config.providerconfig import ProviderConfig @@ -33,6 +31,7 @@ from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread from leap.util.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.util.request_helpers import get_content +from leap.util.certs import get_digest logger = logging.getLogger(__name__) @@ -324,8 +323,7 @@ class ProviderBootstrapper(QtCore.QObject): leap_assert(len(cert_data) > 0, "Could not read certificate data") - x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert_data) - digest = x509.digest(method).replace(":", "").lower() + digest = get_digest(cert_data, method) leap_assert(digest == fingerprint, "Downloaded certificate has a different fingerprint!") -- cgit v1.2.3 From 60bcc7b27aa934a0d62033e7152b87d5af638491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 11:09:38 -0300 Subject: Add valid pemfile check before saving the downloaded client cert --- src/leap/services/eip/eipbootstrapper.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 3e4e2063..ec3dfe7b 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -32,6 +32,7 @@ from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread from leap.util.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.util.request_helpers import get_content +from leap.util.certs import is_valid_pemfile logger = logging.getLogger(__name__) @@ -183,6 +184,10 @@ class EIPBootstrapper(QtCore.QObject): # TODO: check certificate validity + if not is_valid_pemfile(client_cert): + raise Exception(self.tr("The downloaded certificate is not a " + "valid PEM file")) + mkdir_p(os.path.dirname(client_cert_path)) with open(client_cert_path, "w") as f: -- cgit v1.2.3 From 240d6b7762f7cc8f4c6fd229e4538aa9aa2262a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 11:33:42 -0300 Subject: Check validity for downloaded certs and re-download if needed --- src/leap/services/eip/eipbootstrapper.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index ec3dfe7b..c83cb1b5 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -32,7 +32,7 @@ from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread from leap.util.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.util.request_helpers import get_content -from leap.util.certs import is_valid_pemfile +from leap.util.certs import is_valid_pemfile, should_redownload logger = logging.getLogger(__name__) @@ -154,6 +154,10 @@ class EIPBootstrapper(QtCore.QObject): get_client_cert_path(self._provider_config, about_to_download=True) + # For re-download if something is wrong with the cert + self._download_if_needed = self._download_if_needed and \ + not should_redownload(client_cert_path) + if self._download_if_needed and \ os.path.exists(client_cert_path): try: -- cgit v1.2.3 From ebb2d8c1a01e5273d84273fd5dc3bf735be34fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 11:50:54 -0300 Subject: Use expanduser instead of hardcoded string --- src/leap/services/eip/vpn.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index 3ec32f6f..b9988117 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -318,10 +318,11 @@ class VPN(QtCore.QThread): if __name__ == "__main__": - app = QtGui.QApplication(sys.argv) - + import os import signal + app = QtGui.QApplication(sys.argv) + def sigint_handler(*args, **kwargs): logger.debug('SIGINT catched. shutting down...') vpn_thread = args[0] @@ -352,7 +353,7 @@ if __name__ == "__main__": if provider.load("leap/providers/bitmask.net/provider.json"): vpn_thread.start(eipconfig=eipconfig, providerconfig=provider, - socket_host="/home/chiiph/vpnsock", + socket_host=os.path.expanduser("~/vpnsock"), socket_port="unix") timer = QtCore.QTimer() -- cgit v1.2.3 From 4359515dafe572398262ce91bf88d4f122042981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 13:39:00 -0300 Subject: Add vpn already running checks --- src/leap/services/eip/vpn.py | 76 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index b9988117..dd42cd13 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -18,8 +18,10 @@ """ VPN launcher and watcher thread """ + import logging import sys +import psutil from PySide import QtCore, QtGui from functools import partial @@ -61,6 +63,8 @@ class VPN(QtCore.QThread): TCPUDP_WRITE_KEY = "tcp_udp_write" AUTH_READ_KEY = "auth_read" + ALREADY_RUNNING_STEP = "ALREADYRUNNING" + def __init__(self): QtCore.QThread.__init__(self) @@ -134,6 +138,20 @@ class VPN(QtCore.QThread): with QtCore.QMutexLocker(self._should_quit_lock): self._should_quit = False + if not self._stop_if_already_running(): + # We send a fake state + state_dict = { + self.TS_KEY: "", + self.STATUS_STEP_KEY: self.ALREADY_RUNNING_STEP, + self.OK_KEY: "", + self.IP_KEY: "", + self.REMOTE_KEY: "" + } + + self.state_changed.emit(state_dict) + # And just return, don't start the process + return + command = self._launcher.get_vpn_command(eipconfig=eipconfig, providerconfig=providerconfig, socket_host=socket_host, @@ -156,6 +174,64 @@ class VPN(QtCore.QThread): logger.warning("Something went wrong while starting OpenVPN: %r" % (e,)) + def _get_openvpn_process(self): + """ + Looks for openvpn instances running + + @rtype: process + """ + openvpn_process = None + for p in psutil.process_iter(): + try: + # XXX Not exact! + # Will give false positives. + # we should check that cmdline BEGINS + # with openvpn or with our wrapper + # (pkexec / osascript / whatever) + if self._launcher.OPENVPN_BIN in ' '.join(p.cmdline): + openvpn_process = p + break + except psutil.error.AccessDenied: + pass + return openvpn_process + + def _stop_if_already_running(self): + """ + Checks if VPN is already running and tries to stop it + + @return: True if stopped, False otherwise + """ + + process = self._get_openvpn_process() + if process: + logger.debug("OpenVPN is already running, trying to stop it") + cmdline = process.cmdline + + manag_flag = "--management" + if isinstance(cmdline, list) and manag_flag in cmdline: + try: + index = cmdline.index(manag_flag) + host = cmdline[index + 1] + port = cmdline[index + 2] + logger.debug("Trying to connect to %s:%s" + % (host, port)) + self._connect(host, port) + self._send_command("signal SIGTERM") + self._tn.close() + self._tn = None + except Exception as e: + logger.warning("Problem trying to terminate OpenVPN: %r" + % (e,)) + + process = self._get_openvpn_process() + if process is None: + logger.warning("Unabled to terminate OpenVPN") + return True + else: + return False + + return True + def _connect(self, socket_host, socket_port): """ Connects to the specified socket_host socket_port -- cgit v1.2.3 From ef43dff37c6db915757184ad51bc017d45e70c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 14:02:13 -0300 Subject: Notify the user of pkexec/polkit/openvpn checks --- src/leap/services/eip/vpnlaunchers.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 6f277481..9345c0b3 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -38,6 +38,18 @@ class VPNLauncherException(Exception): pass +class OpenVPNNotFoundException(VPNLauncherException): + pass + + +class EIPNoPolkitAuthAgentAvailable(VPNLauncherException): + pass + + +class EIPNoPkexecAvailable(VPNLauncherException): + pass + + class VPNLauncher: """ Abstract launcher class @@ -45,7 +57,6 @@ class VPNLauncher: __metaclass__ = ABCMeta - # TODO: document parameters @abstractmethod def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port=None): @@ -137,9 +148,9 @@ def _has_updown_scripts(path): def _is_auth_agent_running(): - return bool( + return len( commands.getoutput( - 'ps aux | grep polkit-[g]nome-authentication-agent-1')) + 'ps aux | grep polkit-[g]nome-authentication-agent-1')) > 0 class LinuxVPNLauncher(VPNLauncher): @@ -181,7 +192,7 @@ class LinuxVPNLauncher(VPNLauncher): openvpn_possibilities = which(self.OPENVPN_BIN) if len(openvpn_possibilities) == 0: - raise VPNLauncherException("We couldn't find openvpn") + raise OpenVPNNotFoundException() openvpn = openvpn_possibilities[0] args = [] @@ -196,8 +207,10 @@ class LinuxVPNLauncher(VPNLauncher): else: logger.warning("No polkit auth agent found. pkexec " + "will use its own auth agent.") + raise EIPNoPolkitAuthAgentAvailable() else: logger.warning("System has no pkexec") + raise EIPNoPkexecAvailable() # TODO: handle verbosity -- cgit v1.2.3 From fdc1d749a859cefd325e1de712f90eba79d3f678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 14:15:02 -0300 Subject: Display a more related error message for https wizard checks Differentiate between SSLError and other type of errors, so to its clear when the certificate fails and when the connection fails or other kind of problems --- src/leap/services/eip/providerbootstrapper.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index dc87a1bd..82c62f90 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -127,8 +127,14 @@ class ProviderBootstrapper(QtCore.QObject): res = self._session.get("https://%s" % (self._domain,)) res.raise_for_status() https_data[self.PASSED_KEY] = True + except requests.exceptions.SSLError as e: + logger.error("%s" % (e,)) + https_data[self.ERROR_KEY] = self.tr("Provider certificate could " + "not verify") except Exception as e: - https_data[self.ERROR_KEY] = "%s" % (e,) + logger.error("%s" % (e,)) + https_data[self.ERROR_KEY] = self.tr("Provider does not support " + "HTTPS") logger.debug("Emitting https_connection %s" % (https_data,)) self.https_connection.emit(https_data) -- cgit v1.2.3 From 4459619c0d0597447119c67fb7267caae028103f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 14:17:08 -0300 Subject: Document checker parameter in providerbootstrapper --- src/leap/services/eip/providerbootstrapper.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 82c62f90..40ec55c5 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -201,6 +201,8 @@ class ProviderBootstrapper(QtCore.QObject): """ Populates the check queue + @param checker: checker thread to be used to run this check + @type checker: CheckerThread @param domain: domain to check @type domain: str @param download_if_needed: if True, makes the checks do not -- cgit v1.2.3 From 452d1b3a0ab64a6d1e742a74c369336dcdf91897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 15:37:04 -0300 Subject: Make the no up/down log an error instead of a warning --- src/leap/services/eip/vpnlaunchers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 9345c0b3..c646da7d 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -142,8 +142,8 @@ def _has_updown_scripts(path): # XXX should check permissions too is_file = os.path.isfile(path) if not is_file: - logger.warning("Could not find up/down scripts. " + - "Might produce DNS leaks.") + logger.error("Could not find up/down scripts. " + + "Might produce DNS leaks.") return is_file -- cgit v1.2.3 From d0dfad6ac2af360de6421ce74a6831b5b81ad019 Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 14 Mar 2013 07:08:31 +0900 Subject: namespace leap + leap.common split leap is a namespace package from here on. common folder will be deleted and moved to leap_pycommon repository. --- src/leap/services/eip/eipbootstrapper.py | 11 +++--- src/leap/services/eip/eipconfig.py | 2 +- src/leap/services/eip/providerbootstrapper.py | 10 +++--- src/leap/services/eip/vpn.py | 2 +- src/leap/services/eip/vpnlaunchers.py | 51 +++------------------------ 5 files changed, 18 insertions(+), 58 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index c83cb1b5..19b74856 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -19,20 +19,21 @@ EIP bootstrapping """ -import requests import logging import os +import requests + from PySide import QtGui, QtCore -from leap.crypto.srpauth import SRPAuth +from leap.common.check import leap_assert, leap_assert_type +from leap.common.certs import is_valid_pemfile, should_redownload +from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpauth import SRPAuth from leap.services.eip.eipconfig import EIPConfig -from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread -from leap.util.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.util.request_helpers import get_content -from leap.util.certs import is_valid_pemfile, should_redownload logger = logging.getLogger(__name__) diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index eab5bfd4..3f873878 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -21,10 +21,10 @@ Provider configuration import os import logging +from leap.common.check import leap_assert, leap_assert_type from leap.config.baseconfig import BaseConfig from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipspec import eipservice_config_spec -from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 40ec55c5..778d5149 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -18,20 +18,20 @@ """ Provider bootstrapping """ - -import requests import logging import socket import os +import requests + from PySide import QtGui, QtCore +from leap.common.certs import get_digest +from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p +from leap.common.check import leap_assert, leap_assert_type from leap.config.providerconfig import ProviderConfig -from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread -from leap.util.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.util.request_helpers import get_content -from leap.util.certs import get_digest logger = logging.getLogger(__name__) diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index dd42cd13..66b39dd9 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -26,11 +26,11 @@ import psutil from PySide import QtCore, QtGui from functools import partial +from leap.common.check import leap_assert, leap_assert_type from leap.config.providerconfig import ProviderConfig from leap.services.eip.vpnlaunchers import get_platform_launcher from leap.services.eip.eipconfig import EIPConfig from leap.services.eip.udstelnet import UDSTelnet -from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index c646da7d..f9e8e366 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -18,18 +18,19 @@ """ Platform dependant VPN launchers """ -import os -import platform -import logging import commands +import logging import getpass import grp +import os +import platform from abc import ABCMeta, abstractmethod +from leap.common.check import leap_assert, leap_assert_type +from leap.common.files import which from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig -from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) @@ -86,48 +87,6 @@ def get_platform_launcher(): return launcher() -# Twisted implementation of which -def which(name, flags=os.X_OK, path_extension="/usr/sbin:/sbin"): - """ - Search PATH for executable files with the given name. - - On newer versions of MS-Windows, the PATHEXT environment variable will be - set to the list of file extensions for files considered executable. This - will normally include things like ".EXE". This fuction will also find files - with the given name ending with any of these extensions. - - On MS-Windows the only flag that has any meaning is os.F_OK. Any other - flags will be ignored. - - @type name: C{str} - @param name: The name for which to search. - - @type flags: C{int} - @param flags: Arguments to L{os.access}. - - @rtype: C{list} - @param: A list of the full paths to files found, in the - order in which they were found. - """ - - result = [] - exts = filter(None, os.environ.get('PATHEXT', '').split(os.pathsep)) - path = os.environ.get('PATH', None) - path += ":" + path_extension - if path is None: - return [] - parts = path.split(os.pathsep) - for p in parts: - p = os.path.join(p, name) - if os.access(p, flags): - result.append(p) - for e in exts: - pext = p + e - if os.access(pext, flags): - result.append(pext) - return result - - def _is_pkexec_in_system(): pkexec_path = which('pkexec') if len(pkexec_path) == 0: -- cgit v1.2.3 From e33081871affdbca197ea77c461b1379b9039117 Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 20 Mar 2013 19:31:24 +0900 Subject: add darwin prefixer and launcher --- src/leap/services/eip/vpnlaunchers.py | 110 ++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index f9e8e366..44dff4d0 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -188,6 +188,115 @@ class LinuxVPNLauncher(VPNLauncher): 'server' ] + openvpn_configuration = eipconfig.get_openvpn_configuration() + + # FIXME: sanitize this! -- + for key, value in openvpn_configuration.items(): + args += ['--%s' % (key,), value] + + args += [ + '--user', getpass.getuser(), + '--group', grp.getgrgid(os.getgroups()[-1]).gr_name + ] + + if socket_port == "unix": + args += [ + '--management-client-user', getpass.getuser() + ] + + args += [ + '--management-signal', + '--management', socket_host, socket_port, + '--script-security', '2' + ] + + if _has_updown_scripts(self.UP_DOWN_SCRIPT): + args += [ + '--up', self.UP_DOWN_SCRIPT, + '--down', self.UP_DOWN_SCRIPT, + '--plugin', self.OPENVPN_DOWN_ROOT, + '\'script_type=down %s\'' % self.UP_DOWN_SCRIPT + ] + + args += [ + '--cert', eipconfig.get_client_cert_path(providerconfig), + '--key', eipconfig.get_client_cert_path(providerconfig), + '--ca', providerconfig.get_ca_cert_path() + ] + + logger.debug("Running VPN with command:") + logger.debug("%s %s" % (openvpn, " ".join(args))) + + return [openvpn] + args + + +class DarwinVPNLauncher(VPNLauncher): + """ + VPN launcher for the Darwin Platform + """ + + OSASCRIPT_BIN = 'osascript' + OSX_ASADMIN = 'do shell script "%s" with administrator privileges' + OPENVPN_BIN = 'openvpn.leap' + INSTALL_PATH = "/Applications/LEAPClient.app/" + # OPENVPN_BIN = "/%s/Contents/Resources/openvpn.leap" % ( + # self.INSTALL_PATH,) + UP_DOWN_SCRIPT = "/etc/leap/resolv-update" + OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so" + + def get_vpn_command(self, eipconfig=None, providerconfig=None, + socket_host=None, socket_port="unix"): + """ + Returns the platform dependant vpn launching command + + Might raise VPNException. + + @param eipconfig: eip configuration object + @type eipconfig: EIPConfig + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + @param socket_host: either socket path (unix) or socket IP + @type socket_host: str + @param socket_port: either string "unix" if it's a unix + socket, or port otherwise + @type socket_port: str + + @return: A VPN command ready to be launched + @rtype: list + """ + leap_assert(eipconfig, "We need an eip config") + leap_assert_type(eipconfig, EIPConfig) + leap_assert(providerconfig, "We need a provider config") + leap_assert_type(providerconfig, ProviderConfig) + leap_assert(socket_host, "We need a socket host!") + leap_assert(socket_port, "We need a socket port!") + + openvpn_possibilities = which(self.OPENVPN_BIN) + if len(openvpn_possibilities) == 0: + raise OpenVPNNotFoundException() + + openvpn = openvpn_possibilities[0] + args = [] + + # TODO: handle verbosity + + gateway_ip = str(eipconfig.get_gateway_ip(0)) + + logger.debug("Using gateway ip %s" % (gateway_ip,)) + + args += [ + '--client', + '--dev', 'tun', + '--persist-tun', + '--persist-key', + '--remote', gateway_ip, '1194', 'udp', + '--tls-client', + '--remote-cert-tls', + 'server' + ] + + # FIXME: sanitize this! -- + openvpn_configuration = eipconfig.get_openvpn_configuration() for key, value in openvpn_configuration.items(): args += ['--%s' % (key,), value] @@ -225,6 +334,7 @@ class LinuxVPNLauncher(VPNLauncher): logger.debug("Running VPN with command:") logger.debug("%s %s" % (openvpn, " ".join(args))) + # return [self.OSASCRIPT_BIN, ["-e", self.OSX_ASADMIN % ' '.join(args)]] return [openvpn] + args -- cgit v1.2.3 From 85166f4d12fbaafa5ab1cd5fca5ad37a4ba94774 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 20 Mar 2013 23:48:07 +0900 Subject: fix osascript invocation --- src/leap/services/eip/vpn.py | 11 +++++++++++ src/leap/services/eip/vpnlaunchers.py | 36 +++++++++++++++++++++-------------- 2 files changed, 33 insertions(+), 14 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index 66b39dd9..9a60cf73 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -159,6 +159,7 @@ class VPN(QtCore.QThread): try: self._subp = QtCore.QProcess() self._subp.finished.connect(self.process_finished) + self._subp.finished.connect(self._dump_exitinfo) self._subp.start(command[:1][0], command[1:]) logger.debug("Waiting for started...") self._subp.waitForStarted() @@ -174,6 +175,16 @@ class VPN(QtCore.QThread): logger.warning("Something went wrong while starting OpenVPN: %r" % (e,)) + def _dump_exitinfo(self): + """ + SLOT + Prints debug info when quitting the process + + TRIGGER: self._subp.finished + """ + logger.debug("stdout: %s", self._subp.readAllStandardOutput()) + logger.debug("stderr: %s", self._subp.readAllStandardError()) + def _get_openvpn_process(self): """ Looks for openvpn instances running diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 44dff4d0..773b1f92 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -235,14 +235,17 @@ class DarwinVPNLauncher(VPNLauncher): VPN launcher for the Darwin Platform """ - OSASCRIPT_BIN = 'osascript' - OSX_ASADMIN = 'do shell script "%s" with administrator privileges' + OSASCRIPT_BIN = '/usr/bin/osascript' + OSX_ASADMIN = "do shell script \"%s\" with administrator privileges" OPENVPN_BIN = 'openvpn.leap' INSTALL_PATH = "/Applications/LEAPClient.app/" # OPENVPN_BIN = "/%s/Contents/Resources/openvpn.leap" % ( # self.INSTALL_PATH,) - UP_DOWN_SCRIPT = "/etc/leap/resolv-update" - OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so" + UP_SCRIPT = "/%s/client.up.sh" % (INSTALL_PATH,) + DOWN_SCRIPT = "/%s/client.down.sh" % (INSTALL_PATH,) + + # TODO: Add + # OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so" def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="unix"): @@ -276,12 +279,11 @@ class DarwinVPNLauncher(VPNLauncher): raise OpenVPNNotFoundException() openvpn = openvpn_possibilities[0] - args = [] + args = [openvpn] # TODO: handle verbosity gateway_ip = str(eipconfig.get_gateway_ip(0)) - logger.debug("Using gateway ip %s" % (gateway_ip,)) args += [ @@ -317,12 +319,16 @@ class DarwinVPNLauncher(VPNLauncher): '--script-security', '2' ] - if _has_updown_scripts(self.UP_DOWN_SCRIPT): + if _has_updown_scripts(self.UP_SCRIPT): args += [ - '--up', self.UP_DOWN_SCRIPT, - '--down', self.UP_DOWN_SCRIPT, - '--plugin', self.OPENVPN_DOWN_ROOT, - '\'script_type=down %s\'' % self.UP_DOWN_SCRIPT + '--up', self.UP_SCRIPT, + ] + if _has_updown_scripts(self.DOWN_SCRIPT): + args += [ + '--down', self.DOWN_SCRIPT, + # FIXME add down-plugin + # '--plugin', self.OPENVPN_DOWN_ROOT, + # '\'script_type=down %s\'' % self.DOWN_SCRIPT ] args += [ @@ -331,11 +337,13 @@ class DarwinVPNLauncher(VPNLauncher): '--ca', providerconfig.get_ca_cert_path() ] + command = self.OSASCRIPT_BIN + cmd_args = ["-e", self.OSX_ASADMIN % (' '.join(args),)] + logger.debug("Running VPN with command:") - logger.debug("%s %s" % (openvpn, " ".join(args))) + logger.debug("%s %s" % (command, " ".join(cmd_args))) - # return [self.OSASCRIPT_BIN, ["-e", self.OSX_ASADMIN % ' '.join(args)]] - return [openvpn] + args + return [command] + cmd_args if __name__ == "__main__": -- cgit v1.2.3 From 8f54774f6c3f779527718a0158ebd0efc4aab588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 15 Mar 2013 13:30:01 -0300 Subject: Handle configuration and paths in a standalone way Also, abstracts QSettings under LeapSettings and adds a way to define the VPN env in a platform dependant way. --- src/leap/services/eip/vpn.py | 7 +++++++ src/leap/services/eip/vpnlaunchers.py | 38 +++++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index 66b39dd9..4ac7f8a2 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -157,7 +157,14 @@ class VPN(QtCore.QThread): socket_host=socket_host, socket_port=socket_port) try: + env = QtCore.QProcessEnvironment.systemEnvironment() + for key, val in self._launcher.get_vpn_env(providerconfig).items(): + env.insert(key, val) + self._subp = QtCore.QProcess() + + self._subp.setProcessEnvironment(env) + self._subp.finished.connect(self.process_finished) self._subp.start(command[:1][0], command[1:]) logger.debug("Waiting for started...") diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index f9e8e366..c58649b9 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -79,6 +79,20 @@ class VPNLauncher: """ return [] + @abstractmethod + def get_vpn_env(self, providerconfig): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + + @rtype: dict + """ + return {} + def get_platform_launcher(): launcher = globals()[platform.system() + "VPNLauncher"] @@ -125,7 +139,9 @@ class LinuxVPNLauncher(VPNLauncher): def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="unix"): """ - Returns the platform dependant vpn launching command + Returns the platform dependant vpn launching command. It will + look for openvpn in the regular paths and algo in + path_prefix/apps/eip/ (in case standalone is set) Might raise VPNException. @@ -149,7 +165,11 @@ class LinuxVPNLauncher(VPNLauncher): leap_assert(socket_host, "We need a socket host!") leap_assert(socket_port, "We need a socket port!") - openvpn_possibilities = which(self.OPENVPN_BIN) + openvpn_possibilities = which( + self.OPENVPN_BIN, + path_extension=os.path.join(providerconfig.get_path_prefix(), + "..", "apps", "eip")) + if len(openvpn_possibilities) == 0: raise OpenVPNNotFoundException() @@ -227,6 +247,20 @@ class LinuxVPNLauncher(VPNLauncher): return [openvpn] + args + def get_vpn_env(self, providerconfig): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + @rtype: dict + """ + leap_assert(providerconfig, "We need a provider config") + leap_assert_type(providerconfig, ProviderConfig) + + return {"LD_LIBRARY_PATH": os.path.join( + providerconfig.get_path_prefix(), + "..", "lib")} if __name__ == "__main__": logger = logging.getLogger(name='leap') -- cgit v1.2.3 From bdc3e1b840c383f0d9236ad8d4df66633e87baf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 21 Mar 2013 10:38:48 -0300 Subject: Fixes as per review --- src/leap/services/eip/vpnlaunchers.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index c58649b9..e6502813 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -165,10 +165,15 @@ class LinuxVPNLauncher(VPNLauncher): leap_assert(socket_host, "We need a socket host!") leap_assert(socket_port, "We need a socket port!") + kwargs = {} + if ProviderConfig.standalone: + kwargs['path_extension'] = os.path.join( + providerconfig.get_path_prefix(), + "..", "apps", "eip") + openvpn_possibilities = which( self.OPENVPN_BIN, - path_extension=os.path.join(providerconfig.get_path_prefix(), - "..", "apps", "eip")) + **kwargs) if len(openvpn_possibilities) == 0: raise OpenVPNNotFoundException() @@ -253,6 +258,9 @@ class LinuxVPNLauncher(VPNLauncher): This is mainly used for setting LD_LIBRARY_PATH to the correct path when distributing a standalone client + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + @rtype: dict """ leap_assert(providerconfig, "We need a provider config") -- cgit v1.2.3 From 9dace17a4b162c3fbef9909b6f8226903b0ad445 Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 21 Mar 2013 23:30:02 +0900 Subject: fix docstring --- src/leap/services/eip/vpn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index 9a60cf73..55ace23b 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -178,9 +178,9 @@ class VPN(QtCore.QThread): def _dump_exitinfo(self): """ SLOT - Prints debug info when quitting the process - TRIGGER: self._subp.finished + + Prints debug info when quitting the process """ logger.debug("stdout: %s", self._subp.readAllStandardOutput()) logger.debug("stderr: %s", self._subp.readAllStandardError()) -- cgit v1.2.3 From 5488aa1c4f01be567248c2a8ccd0d49d4161877b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 21 Mar 2013 13:39:35 -0300 Subject: Add Windows VPN Launcher --- src/leap/services/eip/vpnlaunchers.py | 107 ++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 78db0176..1b7f11f5 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -272,6 +272,7 @@ class LinuxVPNLauncher(VPNLauncher): providerconfig.get_path_prefix(), "..", "lib")} + class DarwinVPNLauncher(VPNLauncher): """ VPN launcher for the Darwin Platform @@ -388,6 +389,112 @@ class DarwinVPNLauncher(VPNLauncher): return [command] + cmd_args +class WindowsVPNLauncher(VPNLauncher): + """ + VPN launcher for the Windows platform + """ + + OPENVPN_BIN = 'openvpn.exe' + + def get_vpn_command(self, eipconfig=None, providerconfig=None, + socket_host=None, socket_port="9876"): + """ + Returns the platform dependant vpn launching command. It will + look for openvpn in the regular paths and algo in + path_prefix/apps/eip/ (in case standalone is set) + + Might raise VPNException. + + @param eipconfig: eip configuration object + @type eipconfig: EIPConfig + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + @param socket_host: either socket path (unix) or socket IP + @type socket_host: str + @param socket_port: either string "unix" if it's a unix + socket, or port otherwise + @type socket_port: str + + @return: A VPN command ready to be launched + @rtype: list + """ + leap_assert(eipconfig, "We need an eip config") + leap_assert_type(eipconfig, EIPConfig) + leap_assert(providerconfig, "We need a provider config") + leap_assert_type(providerconfig, ProviderConfig) + leap_assert(socket_host, "We need a socket host!") + leap_assert(socket_port, "We need a socket port!") + leap_assert(socket_port != "unix", + "We cannot use unix sockets in windows!") + + openvpn_possibilities = which( + self.OPENVPN_BIN, + path_extension=os.path.join(providerconfig.get_path_prefix(), + "..", "apps", "eip")) + + if len(openvpn_possibilities) == 0: + raise OpenVPNNotFoundException() + + openvpn = openvpn_possibilities[0] + args = [] + + # TODO: handle verbosity + + gateway_ip = str(eipconfig.get_gateway_ip(0)) + + logger.debug("Using gateway ip %s" % (gateway_ip,)) + + args += [ + '--client', + '--dev', 'tun', + '--persist-tun', + '--persist-key', + '--remote', gateway_ip, '1194', 'udp', + '--tls-client', + '--remote-cert-tls', + 'server' + ] + + openvpn_configuration = eipconfig.get_openvpn_configuration() + for key, value in openvpn_configuration.items(): + args += ['--%s' % (key,), value] + + args += [ + '--user', getpass.getuser(), + #'--group', grp.getgrgid(os.getgroups()[-1]).gr_name + ] + + args += [ + '--management-signal', + '--management', socket_host, socket_port, + '--script-security', '2' + ] + + args += [ + '--cert', eipconfig.get_client_cert_path(providerconfig), + '--key', eipconfig.get_client_cert_path(providerconfig), + '--ca', providerconfig.get_ca_cert_path() + ] + + logger.debug("Running VPN with command:") + logger.debug("%s %s" % (openvpn, " ".join(args))) + + return [openvpn] + args + + def get_vpn_env(self, providerconfig): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + + @rtype: dict + """ + return {} + + if __name__ == "__main__": logger = logging.getLogger(name='leap') logger.setLevel(logging.DEBUG) -- cgit v1.2.3 From 01ebc3a165de4f84313b298ab6da78c11f181e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 25 Mar 2013 12:01:03 -0300 Subject: grp doesn't exist in windows --- src/leap/services/eip/vpnlaunchers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 78db0176..9761c225 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -21,9 +21,12 @@ Platform dependant VPN launchers import commands import logging import getpass -import grp import os import platform +try: + import grp +except ImportError: + pass # ignore, probably windows from abc import ABCMeta, abstractmethod -- cgit v1.2.3 From a72a8e0eca6d070b51673129fddb2192066ab1af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 25 Mar 2013 13:07:48 -0300 Subject: Rename the openvpn exe to something more specific --- src/leap/services/eip/vpnlaunchers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 1b7f11f5..b5a6cc94 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -394,7 +394,7 @@ class WindowsVPNLauncher(VPNLauncher): VPN launcher for the Windows platform """ - OPENVPN_BIN = 'openvpn.exe' + OPENVPN_BIN = 'openvpn_leap.exe' def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="9876"): -- cgit v1.2.3 From bd883c86aec15673a56b954cc6046b43e759e562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 25 Mar 2013 13:09:15 -0300 Subject: Mark sanitation point clearly for future reference --- src/leap/services/eip/vpnlaunchers.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index b5a6cc94..5267d918 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -456,6 +456,7 @@ class WindowsVPNLauncher(VPNLauncher): ] openvpn_configuration = eipconfig.get_openvpn_configuration() + # XXX sanitize this for key, value in openvpn_configuration.items(): args += ['--%s' % (key,), value] -- cgit v1.2.3 From 2c3593b803d88b67e8d98f6227a687a6737916ec Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 27 Mar 2013 03:56:57 +0900 Subject: fix osx prefixer and launcher --- src/leap/services/eip/vpnlaunchers.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 57a8092e..37c6256e 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -320,7 +320,15 @@ class DarwinVPNLauncher(VPNLauncher): leap_assert(socket_host, "We need a socket host!") leap_assert(socket_port, "We need a socket port!") - openvpn_possibilities = which(self.OPENVPN_BIN) + kwargs = {} + if ProviderConfig.standalone: + kwargs['path_extension'] = os.path.join( + providerconfig.get_path_prefix(), + "..", "apps", "eip") + + openvpn_possibilities = which( + self.OPENVPN_BIN, + **kwargs) if len(openvpn_possibilities) == 0: raise OpenVPNNotFoundException() @@ -391,6 +399,21 @@ class DarwinVPNLauncher(VPNLauncher): return [command] + cmd_args + def get_vpn_env(self, providerconfig): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + + @rtype: dict + """ + return {"LD_LIBRARY_PATH": os.path.join( + providerconfig.get_path_prefix(), + "..", "lib")} + class WindowsVPNLauncher(VPNLauncher): """ -- cgit v1.2.3 From 2c4cbe8f0e77e0b7cb08fd2dec3cd43bab6ac62e Mon Sep 17 00:00:00 2001 From: kali Date: Mon, 8 Apr 2013 23:54:42 +0900 Subject: fix dyld_library_path for osx --- src/leap/services/eip/vpnlaunchers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 37c6256e..3d36736d 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -410,7 +410,7 @@ class DarwinVPNLauncher(VPNLauncher): @rtype: dict """ - return {"LD_LIBRARY_PATH": os.path.join( + return {"DYLD_LIBRARY_PATH": os.path.join( providerconfig.get_path_prefix(), "..", "lib")} -- cgit v1.2.3 From 3dc9110df56c2919acacb0622915823bfde51d5f Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 10 Apr 2013 00:12:20 +0900 Subject: baseconfig moved to leap.common.config --- src/leap/services/eip/eipconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index 3f873878..4e74687a 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -22,7 +22,7 @@ import os import logging from leap.common.check import leap_assert, leap_assert_type -from leap.config.baseconfig import BaseConfig +from leap.common.config.baseconfig import BaseConfig from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipspec import eipservice_config_spec -- cgit v1.2.3 From 1fbf6db1276c5bca41c4cfbcc90818d9605c1938 Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Fri, 12 Apr 2013 14:07:15 -0300 Subject: Add --danger option to not validate the first hop of certificates This is intended to be used while testing, not in production --- src/leap/services/eip/providerbootstrapper.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 778d5149..f5559143 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -58,7 +58,14 @@ class ProviderBootstrapper(QtCore.QObject): check_ca_fingerprint = QtCore.Signal(dict) check_api_certificate = QtCore.Signal(dict) - def __init__(self): + def __init__(self, bypass_checks=False): + """ + Constructor for provider bootstrapper object + + @param bypass_checks: Set to true if the app should bypass + first round of checks for CA certificates at bootstrap + @type bypass_checks: bool + """ QtCore.QObject.__init__(self) # **************************************************** # @@ -71,6 +78,7 @@ class ProviderBootstrapper(QtCore.QObject): self._domain = None self._provider_config = None self._download_if_needed = False + self._bypass_checks = bypass_checks def _check_name_resolution(self): """ @@ -124,7 +132,8 @@ class ProviderBootstrapper(QtCore.QObject): # system to work try: - res = self._session.get("https://%s" % (self._domain,)) + res = self._session.get("https://%s" % (self._domain,), + verify=not self._bypass_checks) res.raise_for_status() https_data[self.PASSED_KEY] = True except requests.exceptions.SSLError as e: @@ -171,7 +180,8 @@ class ProviderBootstrapper(QtCore.QObject): res = self._session.get("https://%s/%s" % (self._domain, "provider.json"), - headers=headers) + headers=headers, + verify=not self._bypass_checks) res.raise_for_status() # Not modified @@ -270,7 +280,8 @@ class ProviderBootstrapper(QtCore.QObject): return download_ca_cert_data[self.PASSED_KEY] try: - res = self._session.get(self._provider_config.get_ca_cert_uri()) + res = self._session.get(self._provider_config.get_ca_cert_uri(), + verify=not self._bypass_checks) res.raise_for_status() cert_path = self._provider_config.get_ca_cert_path( -- cgit v1.2.3 From 922505a562474b9951bde02f93abbea0a4df5f10 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 16 Apr 2013 22:29:29 +0900 Subject: add locations to spec --- src/leap/services/eip/eipspec.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipspec.py b/src/leap/services/eip/eipspec.py index 69d4d927..7fa782c7 100644 --- a/src/leap/services/eip/eipspec.py +++ b/src/leap/services/eip/eipspec.py @@ -50,6 +50,10 @@ eipservice_config_spec = { "host": "location.example.org", "ip_address": "127.0.0.1"}] }, + 'locations': { + 'type': dict, + 'default': {} + }, 'openvpn_configuration': { 'type': dict, 'default': { -- cgit v1.2.3 From 3d883f79cfe5f8efecd8cbab512eae65101a8c5a Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 1 May 2013 01:48:57 +0900 Subject: add debug to config and cert uris --- src/leap/services/eip/eipbootstrapper.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 19b74856..83f0a0c2 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -102,11 +102,14 @@ class EIPBootstrapper(QtCore.QObject): if self._download_if_needed and mtime: headers['if-modified-since'] = mtime - res = self._session.get("%s/%s/%s/%s" % - (self._provider_config.get_api_uri(), - self._provider_config.get_api_version(), - "config", - "eip-service.json"), + # there is some confusion with this uri, + # it's in 1/config/eip, config/eip and config/1/eip... + config_uri = "%s/%s/config/eip-service.json" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('Downloading eip config from: %s' % config_uri) + + res = self._session.get(config_uri, verify=self._provider_config .get_ca_cert_path(), headers=headers) @@ -176,15 +179,15 @@ class EIPBootstrapper(QtCore.QObject): cookies = None if session_id: cookies = {"_session_id": session_id} - res = self._session.get("%s/%s/%s/" % - (self._provider_config.get_api_uri(), - self._provider_config.get_api_version(), - "cert"), + cert_uri = "%s/%s/cert" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('getting cert from uri: %s' % cert_uri) + res = self._session.get(cert_uri, verify=self._provider_config .get_ca_cert_path(), cookies=cookies) res.raise_for_status() - client_cert = res.content # TODO: check certificate validity -- cgit v1.2.3 From 21b57bfd059ff32201c3403bd5ecc00d4b7d3aed Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 1 May 2013 04:11:26 +0900 Subject: whitelist openvpn cipher parameters --- src/leap/services/eip/eipconfig.py | 27 +++- src/leap/services/eip/tests/__init__.py | 0 src/leap/services/eip/tests/test_eipconfig.py | 174 ++++++++++++++++++++++++++ 3 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 src/leap/services/eip/tests/__init__.py create mode 100644 src/leap/services/eip/tests/test_eipconfig.py (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index 4e74687a..baf26bca 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -18,8 +18,9 @@ """ Provider configuration """ -import os import logging +import os +import re from leap.common.check import leap_assert, leap_assert_type from leap.common.config.baseconfig import BaseConfig @@ -33,6 +34,8 @@ class EIPConfig(BaseConfig): """ Provider configuration abstraction class """ + OPENVPN_ALLOWED_KEYS = ("auth", "cipher", "tls-cipher") + OPENVPN_CIPHERS_REGEX = re.compile("[A-Z0-9\-]+") def __init__(self): BaseConfig.__init__(self) @@ -52,7 +55,24 @@ class EIPConfig(BaseConfig): return self._safe_get_value("gateways") def get_openvpn_configuration(self): - return self._safe_get_value("openvpn_configuration") + """ + Returns a dictionary containing the openvpn configuration + parameters. + + These are sanitized with alphanumeric whitelist. + + @returns: openvpn configuration dict + @rtype: C{dict} + """ + ovpncfg = self._safe_get_value("openvpn_configuration") + config = {} + for key, value in ovpncfg.items(): + if key in self.OPENVPN_ALLOWED_KEYS and value is not None: + sanitized_val = self.OPENVPN_CIPHERS_REGEX.findall(value) + if len(sanitized_val) != 0: + _val = sanitized_val[0] + config[str(key)] = str(_val) + return config def get_serial(self): return self._safe_get_value("serial") @@ -61,6 +81,9 @@ class EIPConfig(BaseConfig): return self._safe_get_value("version") def get_gateway_ip(self, index=0): + """ + Returns the ip of the gateway + """ gateways = self.get_gateways() leap_assert(len(gateways) > 0, "We don't have any gateway!") if index > len(gateways): diff --git a/src/leap/services/eip/tests/__init__.py b/src/leap/services/eip/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/services/eip/tests/test_eipconfig.py b/src/leap/services/eip/tests/test_eipconfig.py new file mode 100644 index 00000000..1675472f --- /dev/null +++ b/src/leap/services/eip/tests/test_eipconfig.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# test_eipconfig.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +tests for eipconfig +""" +import copy +import json +import os +import unittest + +from leap.common.testing.basetest import BaseLeapTest +from leap.services.eip.eipconfig import EIPConfig + + +sample_config = { + "gateways": [ + { + "capabilities": { + "adblock": False, + "filter_dns": True, + "limited": True, + "ports": [ + "1194", + "443", + "53", + "80" + ], + "protocols": [ + "tcp", + "udp"], + "transport": [ + "openvpn"], + "user_ips": False}, + "host": "host.dev.example.org", + "ip_address": "11.22.33.44", + "location": "cyberspace" + }], + "locations": { + "ankara": { + "country_code": "XX", + "hemisphere": "S", + "name": "Antarctica", + "timezone": "+2" + } + }, + "openvpn_configuration": { + "auth": "SHA1", + "cipher": "AES-128-CBC", + "tls-cipher": "DHE-RSA-AES128-SHA" + }, + "serial": 1, + "version": 1 +} + + +class EIPConfigTest(BaseLeapTest): + + __name__ = "eip_config_tests" + #provider = "testprovider.example.org" + + maxDiff = None + + def setUp(self): + pass + + def tearDown(self): + pass + + # + # helpers + # + + def write_config(self, data): + self.configfile = os.path.join( + self.tempdir, "eipconfig.json") + conf = open(self.configfile, "w") + conf.write(json.dumps(data)) + conf.close() + + def test_load_valid_config(self): + """ + load a sample config + """ + self.write_config(sample_config) + config = EIPConfig() + self.assertRaises( + AssertionError, + config.get_clusters) + self.assertTrue(config.load(self.configfile)) + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + self.assertEqual( + config.get_gateway_ip(), + "11.22.33.44") + self.assertEqual(config.get_version(), 1) + self.assertEqual(config.get_serial(), 1) + self.assertEqual(config.get_gateways(), + sample_config["gateways"]) + self.assertEqual( + config.get_clusters(), None) + + def test_openvpnoptions(self): + """ + check the sanitization of openvpn options + """ + # extra parameters + data = copy.deepcopy(sample_config) + data['openvpn_configuration']["extra_param"] = "FOO" + self.write_config(data) + config = EIPConfig() + config.load(self.configfile) + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + + # non allowed chars + data = copy.deepcopy(sample_config) + data['openvpn_configuration']["auth"] = "SHA1;" + self.write_config(data) + config = EIPConfig() + config.load(self.configfile) + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + + # non allowed chars + data = copy.deepcopy(sample_config) + data['openvpn_configuration']["auth"] = "SHA1>`&|" + self.write_config(data) + config = EIPConfig() + config.load(self.configfile) + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + + # lowercase + data = copy.deepcopy(sample_config) + data['openvpn_configuration']["auth"] = "shaSHA1" + self.write_config(data) + config = EIPConfig() + config.load(self.configfile) + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + + # all characters invalid -> null value + data = copy.deepcopy(sample_config) + data['openvpn_configuration']["auth"] = "sha&*!@#;" + self.write_config(data) + config = EIPConfig() + config.load(self.configfile) + self.assertEqual( + config.get_openvpn_configuration(), + {'cipher': 'AES-128-CBC', + 'tls-cipher': 'DHE-RSA-AES128-SHA'}) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From 544717da3e95a553fa2af8555df6b4e06d9e5af2 Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 1 May 2013 04:41:11 +0900 Subject: sanitize ip address --- src/leap/services/eip/eipconfig.py | 5 ++++- src/leap/services/eip/tests/test_eipconfig.py | 22 ++++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index baf26bca..e6b93647 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -36,6 +36,7 @@ class EIPConfig(BaseConfig): """ OPENVPN_ALLOWED_KEYS = ("auth", "cipher", "tls-cipher") OPENVPN_CIPHERS_REGEX = re.compile("[A-Z0-9\-]+") + IP_REGEX = re.compile("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$") def __init__(self): BaseConfig.__init__(self) @@ -90,7 +91,9 @@ class EIPConfig(BaseConfig): index = 0 logger.warning("Provided an unknown gateway index %s, " + "defaulting to 0") - return gateways[0]["ip_address"] + ip_addr = gateways[0]["ip_address"] + if self.IP_REGEX.search(ip_addr): + return ip_addr def get_client_cert_path(self, providerconfig=None, diff --git a/src/leap/services/eip/tests/test_eipconfig.py b/src/leap/services/eip/tests/test_eipconfig.py index 1675472f..ce04c2fc 100644 --- a/src/leap/services/eip/tests/test_eipconfig.py +++ b/src/leap/services/eip/tests/test_eipconfig.py @@ -114,9 +114,9 @@ class EIPConfigTest(BaseLeapTest): self.assertEqual( config.get_clusters(), None) - def test_openvpnoptions(self): + def test_sanitize_config(self): """ - check the sanitization of openvpn options + check the sanitization of options """ # extra parameters data = copy.deepcopy(sample_config) @@ -169,6 +169,24 @@ class EIPConfigTest(BaseLeapTest): {'cipher': 'AES-128-CBC', 'tls-cipher': 'DHE-RSA-AES128-SHA'}) + # bad_ip + data = copy.deepcopy(sample_config) + data['gateways'][0]["ip_address"] = "11.22.33.44;" + self.write_config(data) + config = EIPConfig() + config.load(self.configfile) + self.assertEqual( + config.get_gateway_ip(), + None) + + data = copy.deepcopy(sample_config) + data['gateways'][0]["ip_address"] = "11.22.33.44`" + self.write_config(data) + config = EIPConfig() + config.load(self.configfile) + self.assertEqual( + config.get_gateway_ip(), + None) if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From 2dae2703fb8c2ae7e721ce83020c0dd10ff9ca33 Mon Sep 17 00:00:00 2001 From: kali Date: Fri, 3 May 2013 02:59:22 +0900 Subject: updated documentation * documentation reviewed after rewrite, ready for 0.2.1 * updated docstrings format to fit sphinx autodoc --- src/leap/services/eip/eipbootstrapper.py | 12 +-- src/leap/services/eip/providerbootstrapper.py | 59 +++++++------ src/leap/services/eip/udstelnet.py | 1 - src/leap/services/eip/vpn.py | 52 ++++++------ src/leap/services/eip/vpnlaunchers.py | 114 ++++++++++++++------------ 5 files changed, 121 insertions(+), 117 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 83f0a0c2..a881f235 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -73,8 +73,8 @@ class EIPBootstrapper(QtCore.QObject): """ Downloads the EIP config for the given provider - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._provider_config, @@ -140,8 +140,8 @@ class EIPBootstrapper(QtCore.QObject): """ Downloads the EIP client certificate for the given provider - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._provider_config, "We need a provider configuration!") leap_assert(self._eip_config, "We need an eip configuration!") @@ -219,8 +219,8 @@ class EIPBootstrapper(QtCore.QObject): """ Starts the checks needed for a new eip setup - @param provider_config: Provider configuration - @type provider_config: ProviderConfig + :param provider_config: Provider configuration + :type provider_config: ProviderConfig """ leap_assert(provider_config, "We need a provider config!") leap_assert_type(provider_config, ProviderConfig) diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index f5559143..734d3867 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -62,9 +62,9 @@ class ProviderBootstrapper(QtCore.QObject): """ Constructor for provider bootstrapper object - @param bypass_checks: Set to true if the app should bypass + :param bypass_checks: Set to true if the app should bypass first round of checks for CA certificates at bootstrap - @type bypass_checks: bool + :type bypass_checks: bool """ QtCore.QObject.__init__(self) @@ -84,8 +84,8 @@ class ProviderBootstrapper(QtCore.QObject): """ Checks that the name resolution for the provider name works - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._domain, "Cannot check DNS without a domain") @@ -115,8 +115,8 @@ class ProviderBootstrapper(QtCore.QObject): Checks that https is working and that the provided certificate checks out - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._domain, "Cannot check HTTPS without a domain") @@ -154,8 +154,8 @@ class ProviderBootstrapper(QtCore.QObject): """ Downloads the provider.json defition - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._domain, "Cannot download provider info without a domain") @@ -211,16 +211,15 @@ class ProviderBootstrapper(QtCore.QObject): """ Populates the check queue - @param checker: checker thread to be used to run this check - @type checker: CheckerThread - @param domain: domain to check - @type domain: str - @param download_if_needed: if True, makes the checks do not - overwrite already downloaded data - @type download_if_needed: bool + :param checker: checker thread to be used to run this check + :type checker: CheckerThread + :param domain: domain to check + :type domain: str + :param download_if_needed: if True, makes the checks do not overwrite already downloaded data + :type download_if_needed: bool - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(domain and len(domain) > 0, "We need a domain!") @@ -238,7 +237,7 @@ class ProviderBootstrapper(QtCore.QObject): Returns False if the certificate already exists for the given provider. True otherwise - @rtype: bool + :rtype: bool """ leap_assert(self._provider_config, "We need a provider config!") @@ -252,8 +251,8 @@ class ProviderBootstrapper(QtCore.QObject): """ Downloads the CA cert that is going to be used for the api URL - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._provider_config, "Cannot download the ca cert " @@ -310,8 +309,8 @@ class ProviderBootstrapper(QtCore.QObject): Checks the CA cert fingerprint against the one provided in the json definition - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._provider_config, "Cannot check the ca cert " "without a provider config!") @@ -362,8 +361,8 @@ class ProviderBootstrapper(QtCore.QObject): Tries to make an API call with the downloaded cert and checks if it validates against it - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._provider_config, "Cannot check the ca cert " "without a provider config!") @@ -403,13 +402,13 @@ class ProviderBootstrapper(QtCore.QObject): provider_config, download_if_needed=False): """ - Starts the checks needed for a new provider setup + Starts the checks needed for a new provider setup. - @param provider_config: Provider configuration - @type provider_config: ProviderConfig - @param download_if_needed: if True, makes the checks do not - overwrite already downloaded data - @type download_if_needed: bool + :param provider_config: Provider configuration + :type provider_config: ProviderConfig + + :param download_if_needed: if True, makes the checks do not overwrite already downloaded data. + :type download_if_needed: bool """ leap_assert(provider_config, "We need a provider config!") leap_assert_type(provider_config, ProviderConfig) diff --git a/src/leap/services/eip/udstelnet.py b/src/leap/services/eip/udstelnet.py index a47c24f4..e6c82350 100644 --- a/src/leap/services/eip/udstelnet.py +++ b/src/leap/services/eip/udstelnet.py @@ -40,7 +40,6 @@ class UDSTelnet(telnetlib.Telnet): 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 diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index 9d838609..af1febe6 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -85,8 +85,8 @@ class VPN(QtCore.QThread): """ Returns wether this thread should quit - @rtype: bool - @return: True if the thread should terminate itself, Flase otherwise + :rtype: bool + :return: True if the thread should terminate itself, Flase otherwise """ QtCore.QMutexLocker(self._should_quit_lock) return self._should_quit @@ -117,15 +117,15 @@ class VPN(QtCore.QThread): """ Launches OpenVPN and starts the thread to watch its output - @param eipconfig: eip configuration object - @type eipconfig: EIPConfig - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig - @param socket_host: either socket path (unix) or socket IP - @type socket_host: str - @param socket_port: either string "unix" if it's a unix + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + :param socket_port: either string "unix" if it's a unix socket, or port otherwise - @type socket_port: str + :type socket_port: str """ leap_assert(eipconfig, "We need an eip config") leap_assert_type(eipconfig, EIPConfig) @@ -196,7 +196,7 @@ class VPN(QtCore.QThread): """ Looks for openvpn instances running - @rtype: process + :rtype: process """ openvpn_process = None for p in psutil.process_iter(): @@ -217,7 +217,7 @@ class VPN(QtCore.QThread): """ Checks if VPN is already running and tries to stop it - @return: True if stopped, False otherwise + :return: True if stopped, False otherwise """ process = self._get_openvpn_process() @@ -253,11 +253,11 @@ class VPN(QtCore.QThread): def _connect(self, socket_host, socket_port): """ Connects to the specified socket_host socket_port - @param socket_host: either socket path (unix) or socket IP - @type socket_host: str - @param socket_port: either string "unix" if it's a unix + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + :param socket_port: either string "unix" if it's a unix socket, or port otherwise - @type socket_port: str + :type socket_port: str """ try: self._tn = UDSTelnet(socket_host, socket_port) @@ -291,12 +291,12 @@ class VPN(QtCore.QThread): Sends a command to the telnet connection and reads until END is reached - @param command: command to send - @type command: str - @param until: byte delimiter string for reading command output - @type until: byte str - @return: response read - @rtype: list + :param command: command to send + :type command: str + :param until: byte delimiter string for reading command output + :type until: byte str + :return: response read + :rtype: list """ leap_assert(self._tn, "We need a tn connection!") try: @@ -315,9 +315,9 @@ class VPN(QtCore.QThread): Parses the output of the state command and emits state_changed signal when the state changes - @param output: list of lines that the state command printed as + :param output: list of lines that the state command printed as its output - @type output: list + :type output: list """ for line in output: stripped = line.strip() @@ -345,9 +345,9 @@ class VPN(QtCore.QThread): Parses the output of the status command and emits status_changed signal when the status changes - @param output: list of lines that the status command printed + :param output: list of lines that the status command printed as its output - @type output: list + :type output: list """ tun_tap_read = "" tun_tap_write = "" diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 3d36736d..540bc45e 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -67,18 +67,18 @@ class VPNLauncher: """ Returns the platform dependant vpn launching command - @param eipconfig: eip configuration object - @type eipconfig: EIPConfig - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig - @param socket_host: either socket path (unix) or socket IP - @type socket_host: str - @param socket_port: either string "unix" if it's a unix + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + :param socket_port: either string "unix" if it's a unix socket, or port otherwise - @type socket_port: str + :type socket_port: str - @return: A VPN command ready to be launched - @rtype: list + :return: A VPN command ready to be launched + :rtype: list """ return [] @@ -89,10 +89,10 @@ class VPNLauncher: This is mainly used for setting LD_LIBRARY_PATH to the correct path when distributing a standalone client - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig - @rtype: dict + :rtype: dict """ return {} @@ -148,18 +148,21 @@ class LinuxVPNLauncher(VPNLauncher): Might raise VPNException. - @param eipconfig: eip configuration object - @type eipconfig: EIPConfig - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig - @param socket_host: either socket path (unix) or socket IP - @type socket_host: str - @param socket_port: either string "unix" if it's a unix - socket, or port otherwise - @type socket_port: str + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + + :param socket_port: either string "unix" if it's a unix + socket, or port otherwise + :type socket_port: str - @return: A VPN command ready to be launched - @rtype: list + :return: A VPN command ready to be launched + :rtype: list """ leap_assert(eipconfig, "We need an eip config") leap_assert_type(eipconfig, EIPConfig) @@ -263,10 +266,10 @@ class LinuxVPNLauncher(VPNLauncher): This is mainly used for setting LD_LIBRARY_PATH to the correct path when distributing a standalone client - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig - @rtype: dict + :rtype: dict """ leap_assert(providerconfig, "We need a provider config") leap_assert_type(providerconfig, ProviderConfig) @@ -300,18 +303,21 @@ class DarwinVPNLauncher(VPNLauncher): Might raise VPNException. - @param eipconfig: eip configuration object - @type eipconfig: EIPConfig - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig - @param socket_host: either socket path (unix) or socket IP - @type socket_host: str - @param socket_port: either string "unix" if it's a unix - socket, or port otherwise - @type socket_port: str + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + + :param socket_port: either string "unix" if it's a unix + socket, or port otherwise + :type socket_port: str - @return: A VPN command ready to be launched - @rtype: list + :return: A VPN command ready to be launched + :rtype: list """ leap_assert(eipconfig, "We need an eip config") leap_assert_type(eipconfig, EIPConfig) @@ -405,10 +411,10 @@ class DarwinVPNLauncher(VPNLauncher): This is mainly used for setting LD_LIBRARY_PATH to the correct path when distributing a standalone client - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig - @rtype: dict + :rtype: dict """ return {"DYLD_LIBRARY_PATH": os.path.join( providerconfig.get_path_prefix(), @@ -431,18 +437,18 @@ class WindowsVPNLauncher(VPNLauncher): Might raise VPNException. - @param eipconfig: eip configuration object - @type eipconfig: EIPConfig - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig - @param socket_host: either socket path (unix) or socket IP - @type socket_host: str - @param socket_port: either string "unix" if it's a unix + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + :param socket_port: either string "unix" if it's a unix socket, or port otherwise - @type socket_port: str + :type socket_port: str - @return: A VPN command ready to be launched - @rtype: list + :return: A VPN command ready to be launched + :rtype: list """ leap_assert(eipconfig, "We need an eip config") leap_assert_type(eipconfig, EIPConfig) @@ -514,10 +520,10 @@ class WindowsVPNLauncher(VPNLauncher): This is mainly used for setting LD_LIBRARY_PATH to the correct path when distributing a standalone client - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig - @rtype: dict + :rtype: dict """ return {} -- cgit v1.2.3 From 7126aad25c3dd45bfe026ba1ad383bf5476ffb15 Mon Sep 17 00:00:00 2001 From: kali Date: Tue, 7 May 2013 22:51:08 +0900 Subject: use ipaddr to get ipv6 support --- src/leap/services/eip/eipconfig.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index e6b93647..0a7d2b23 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -22,6 +22,8 @@ import logging import os import re +import ipaddr + from leap.common.check import leap_assert, leap_assert_type from leap.common.config.baseconfig import BaseConfig from leap.config.providerconfig import ProviderConfig @@ -36,7 +38,6 @@ class EIPConfig(BaseConfig): """ OPENVPN_ALLOWED_KEYS = ("auth", "cipher", "tls-cipher") OPENVPN_CIPHERS_REGEX = re.compile("[A-Z0-9\-]+") - IP_REGEX = re.compile("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$") def __init__(self): BaseConfig.__init__(self) @@ -91,9 +92,14 @@ class EIPConfig(BaseConfig): index = 0 logger.warning("Provided an unknown gateway index %s, " + "defaulting to 0") - ip_addr = gateways[0]["ip_address"] - if self.IP_REGEX.search(ip_addr): - return ip_addr + ip_addr_str = gateways[0]["ip_address"] + + try: + ipaddr.IPAddress(ip_addr_str) + return ip_addr_str + except ValueError: + logger.error("Invalid ip address in config: %s" % (ip_addr_str,)) + return None def get_client_cert_path(self, providerconfig=None, -- cgit v1.2.3 From 1cb931e83522746da668f9a8bb5943aca1882086 Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 16 May 2013 04:26:00 +0900 Subject: use qtreactor so twisted is driven by qt main loop aboutToQuit signal is not raised anymore with the qt4reactor. So we are calling all cleanup callbacks from the quit function. --- src/leap/services/tx.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/leap/services/tx.py (limited to 'src/leap/services') diff --git a/src/leap/services/tx.py b/src/leap/services/tx.py new file mode 100644 index 00000000..ef08fcc6 --- /dev/null +++ b/src/leap/services/tx.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# twisted.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Twisted services launched by the client +""" +import logging + +from twisted.application.service import Application +from twisted.internet.task import LoopingCall + +logger = logging.getLogger(__name__) + + +def task(): + """ + stub periodic task, mainly for tests. + DELETE-ME when there's real meat here :) + """ + from datetime import datetime + logger.debug("hi there %s", datetime.now()) + + +def leap_services(): + """ + Check which twisted services are enabled and + register them. + """ + logger.debug('starting leap services') + application = Application("LEAP Client Local Services") + #lc = LoopingCall(task) + #lc.start(5) + return application -- cgit v1.2.3 From b0abf507bb8eb570328172b659ab072bc4b08634 Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Wed, 15 May 2013 16:16:32 -0300 Subject: Integrate soledad and keymanager in the client --- src/leap/services/soledad/__init__.py | 0 src/leap/services/soledad/soledadbootstrapper.py | 279 +++++++++++++++++++++++ src/leap/services/soledad/soledadconfig.py | 48 ++++ src/leap/services/soledad/soledadspec.py | 57 +++++ 4 files changed, 384 insertions(+) create mode 100644 src/leap/services/soledad/__init__.py create mode 100644 src/leap/services/soledad/soledadbootstrapper.py create mode 100644 src/leap/services/soledad/soledadconfig.py create mode 100644 src/leap/services/soledad/soledadspec.py (limited to 'src/leap/services') diff --git a/src/leap/services/soledad/__init__.py b/src/leap/services/soledad/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/services/soledad/soledadbootstrapper.py b/src/leap/services/soledad/soledadbootstrapper.py new file mode 100644 index 00000000..51c53a6e --- /dev/null +++ b/src/leap/services/soledad/soledadbootstrapper.py @@ -0,0 +1,279 @@ +# -*- coding: utf-8 -*- +# soledadbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Soledad bootstrapping +""" + +import logging +import os + +import requests + +from PySide import QtCore +from mock import Mock + +from leap.common.check import leap_assert, leap_assert_type +from leap.common.files import get_mtime +from leap.common.keymanager import KeyManager, openpgp +from leap.common.keymanager.errors import KeyNotFound +from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpauth import SRPAuth +from leap.services.soledad.soledadconfig import SoledadConfig +from leap.util.request_helpers import get_content +from leap.soledad import Soledad + +logger = logging.getLogger(__name__) + + +class SoledadBootstrapper(QtCore.QObject): + """ + Soledad init procedure + """ + + PASSED_KEY = "passed" + ERROR_KEY = "error" + + PUBKEY_KEY = "user[public_key]" + + IDLE_SLEEP_INTERVAL = 100 + + # All dicts returned are of the form + # {"passed": bool, "error": str} + download_config = QtCore.Signal(dict) + gen_key = QtCore.Signal(dict) + + def __init__(self): + QtCore.QObject.__init__(self) + + # **************************************************** # + # Dependency injection helpers, override this for more + # granular testing + self._fetcher = requests + # **************************************************** # + + self._session = self._fetcher.session() + self._provider_config = None + self._soledad_config = None + self._keymanager = None + self._download_if_needed = False + self._user = "" + self._password = "" + + def _load_and_sync_soledad(self, srp_auth): + """ + Once everthing is in the right place, we instantiate and sync + Soledad + + :param srp_auth: SRPAuth object used + :type srp_auth: SRPAuth + """ + uuid = srp_auth.get_uid() + + prefix = os.path.join(self._soledad_config.get_path_prefix(), + "leap", "soledad") + secrets_path = "%s/%s.secret" % (prefix, uuid) + local_db_path = "%s/%s.db" % (prefix, uuid) + + # TODO: use the proper URL + server_url = 'https://mole.dev.bitmask.net:2424/user-%s' % (uuid,) + # server_url = self._soledad_config.get_hosts(...) + + cert_file = self._provider_config.get_ca_cert_path() + + self._soledad = Soledad(uuid, + self._password.encode("utf-8"), + secrets_path, + local_db_path, + server_url, + cert_file, + srp_auth.get_token()) + self._soledad.sync() + + def _download_config(self): + """ + Downloads the Soledad config for the given provider + + :return: True if everything went as expected, False otherwise + :rtype: bool + """ + + leap_assert(self._provider_config, + "We need a provider configuration!") + + logger.debug("Downloading Soledad config for %s" % + (self._provider_config.get_domain(),)) + + download_config_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + self._soledad_config = SoledadConfig() + + try: + headers = {} + mtime = get_mtime(os.path.join(self._soledad_config + .get_path_prefix(), + "leap", + "providers", + self._provider_config.get_domain(), + "soledad-service.json")) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + # there is some confusion with this uri, + config_uri = "%s/%s/config/soledad-service.json" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('Downloading soledad config from: %s' % config_uri) + + srp_auth = SRPAuth(self._provider_config) + session_id = srp_auth.get_session_id() + cookies = None + if session_id: + cookies = {"_session_id": session_id} + + res = self._session.get(config_uri, + verify=self._provider_config + .get_ca_cert_path(), + headers=headers, + cookies=cookies) + res.raise_for_status() + + # Not modified + if res.status_code == 304: + logger.debug("Soledad definition has not been modified") + else: + soledad_definition, mtime = get_content(res) + + self._soledad_config.load(data=soledad_definition, mtime=mtime) + self._soledad_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "soledad-service.json"]) + + self._load_and_sync_soledad(srp_auth) + + download_config_data[self.PASSED_KEY] = True + except Exception as e: + download_config_data[self.PASSED_KEY] = False + download_config_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting download_config %s" % (download_config_data,)) + self.download_config.emit(download_config_data) + + return download_config_data[self.PASSED_KEY] + + def _gen_key(self): + """ + Generates the key pair if needed, uploads it to the webapp and + nickserver + + :return: True if everything is done successfully, False + otherwise + :rtype: bool + """ + leap_assert(self._provider_config, + "We need a provider configuration!") + + # XXX Sanitize this + address = "%s@%s" % (self._user, self._provider_config.get_domain()) + + logger.debug("Retrieving key for %s" % (address,)) + + genkey_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + try: + srp_auth = SRPAuth(self._provider_config) + self._keymanager = KeyManager( + address, + "https://nickserver", # TODO: nickserver url, none for now + self._soledad, + token=srp_auth.get_token()) + self._keymanager._fetcher.put = Mock() + try: + self._keymanager.get_key(address, openpgp.OpenPGPKey, + private=True, fetch_remote=False) + except KeyNotFound: + logger.debug("Key not found. Generating key for %s" % (address,)) + self._keymanager.gen_key(openpgp.OpenPGPKey) + + logger.debug("Key generated successfully.") + + cookies = None + session_id = srp_auth.get_session_id() + if session_id: + cookies = {"_session_id": session_id} + + key_uri = "%s/%s/users/%s.json" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version(), + srp_auth.get_uid()) + + logger.debug("Uploading public key to %s" % (key_uri,)) + + pubkey = self._keymanager.get_key(address, openpgp.OpenPGPKey, + private=False, fetch_remote=False) + key_data = { + self.PUBKEY_KEY: pubkey.key_data, + } + + # TODO: check if uploaded before uploading it + key_result = self._session.put(key_uri, + data=key_data, + verify=self._provider_config + .get_ca_cert_path(), + cookies=cookies) + key_result.raise_for_status() + genkey_data[self.PASSED_KEY] = True + except Exception as e: + genkey_data[self.PASSED_KEY] = False + genkey_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting gen_key %s" % (genkey_data,)) + self.gen_key.emit(genkey_data) + + return genkey_data[self.PASSED_KEY] + + def run_soledad_setup_checks(self, + checker, + provider_config, + user, + password, + download_if_needed=False): + """ + Starts the checks needed for a new soledad setup + + :param provider_config: Provider configuration + :type provider_config: ProviderConfig + """ + leap_assert_type(provider_config, ProviderConfig) + + self._provider_config = provider_config + self._download_if_needed = download_if_needed + self._user = user + self._password = password + + checker.add_checks([ + self._download_config, + self._gen_key + ]) diff --git a/src/leap/services/soledad/soledadconfig.py b/src/leap/services/soledad/soledadconfig.py new file mode 100644 index 00000000..836265f3 --- /dev/null +++ b/src/leap/services/soledad/soledadconfig.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# soledadconfig.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Soledad configuration +""" +import logging + +from leap.common.config.baseconfig import BaseConfig +from leap.services.soledad.soledadspec import soledad_config_spec + +logger = logging.getLogger(__name__) + + +class SoledadConfig(BaseConfig): + """ + Soledad configuration abstraction class + """ + + def __init__(self): + BaseConfig.__init__(self) + + def _get_spec(self): + """ + Returns the spec object for the specific configuration + """ + return soledad_config_spec + + def get_hosts(self): + return self._safe_get_value("hosts") + + def get_locations(self): + return self._safe_get_value("locations") + diff --git a/src/leap/services/soledad/soledadspec.py b/src/leap/services/soledad/soledadspec.py new file mode 100644 index 00000000..d5a437cc --- /dev/null +++ b/src/leap/services/soledad/soledadspec.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# soledadspec.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +soledad_config_spec = { + 'description': 'sample soledad service config', + 'type': 'object', + 'properties': { + 'serial': { + 'type': int, + 'default': 1, + 'required': True + }, + 'version': { + 'type': int, + 'default': 1, + 'required': True + }, + 'hosts': { + 'type': dict, + 'default': { + "python": { + "hostname": "someprovider", + "ip_address": "1.1.1.1", + "location": "loc", + "port": 1111 + }, + }, + }, + 'locations': { + 'type': dict, + 'default': { + "locations": { + "ankara": { + "country_code": "TR", + "hemisphere": "N", + "name": "loc", + "timezone": "+0" + } + } + } + } + } +} -- cgit v1.2.3 From c2a0900868eecda3958185bf355a15b046d4e4af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Sat, 18 May 2013 12:38:29 -0300 Subject: Integrate SMTP --- src/leap/services/mail/__init__.py | 0 src/leap/services/mail/smtpbootstrapper.py | 169 +++++++++++++++++++++++ src/leap/services/mail/smtpconfig.py | 48 +++++++ src/leap/services/mail/smtpspec.py | 51 +++++++ src/leap/services/soledad/soledadbootstrapper.py | 8 +- 5 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 src/leap/services/mail/__init__.py create mode 100644 src/leap/services/mail/smtpbootstrapper.py create mode 100644 src/leap/services/mail/smtpconfig.py create mode 100644 src/leap/services/mail/smtpspec.py (limited to 'src/leap/services') diff --git a/src/leap/services/mail/__init__.py b/src/leap/services/mail/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/services/mail/smtpbootstrapper.py b/src/leap/services/mail/smtpbootstrapper.py new file mode 100644 index 00000000..7e0f10de --- /dev/null +++ b/src/leap/services/mail/smtpbootstrapper.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# smtpbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +SMTP bootstrapping +""" + +import logging +import os + +import requests + +from PySide import QtCore + +from leap.common.check import leap_assert, leap_assert_type +from leap.common.files import get_mtime +from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpauth import SRPAuth +from leap.util.request_helpers import get_content + +logger = logging.getLogger(__name__) + + +class SMTPBootstrapper(QtCore.QObject): + """ + SMTP init procedure + """ + + PASSED_KEY = "passed" + ERROR_KEY = "error" + + IDLE_SLEEP_INTERVAL = 100 + + # All dicts returned are of the form + # {"passed": bool, "error": str} + download_config = QtCore.Signal(dict) + + def __init__(self): + QtCore.QObject.__init__(self) + + # **************************************************** # + # Dependency injection helpers, override this for more + # granular testing + self._fetcher = requests + # **************************************************** # + + self._session = self._fetcher.session() + self._provider_config = None + self._smtp_config = None + self._download_if_needed = False + + def _download_config(self): + """ + Downloads the SMTP config for the given provider + + :return: True if everything went as expected, False otherwise + :rtype: bool + """ + + leap_assert(self._provider_config, + "We need a provider configuration!") + + logger.debug("Downloading SMTP config for %s" % + (self._provider_config.get_domain(),)) + + download_config_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + try: + headers = {} + mtime = get_mtime(os.path.join(self._smtp_config + .get_path_prefix(), + "leap", + "providers", + self._provider_config.get_domain(), + "smtp-service.json")) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + # there is some confusion with this uri, + config_uri = "%s/%s/config/smtp-service.json" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('Downloading SMTP config from: %s' % config_uri) + + srp_auth = SRPAuth(self._provider_config) + session_id = srp_auth.get_session_id() + cookies = None + if session_id: + cookies = {"_session_id": session_id} + + res = self._session.get(config_uri, + verify=self._provider_config + .get_ca_cert_path(), + headers=headers, + cookies=cookies) + res.raise_for_status() + + # Not modified + if res.status_code == 304: + logger.debug("SMTP definition has not been modified") + self._smtp_config.load(os.path.join("leap", + "providers", + self._provider_config.get_domain(), + "smtp-service.json")) + else: + smtp_definition, mtime = get_content(res) + + self._smtp_config.load(data=smtp_definition, mtime=mtime) + self._smtp_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "smtp-service.json"]) + + download_config_data[self.PASSED_KEY] = True + except Exception as e: + download_config_data[self.PASSED_KEY] = False + download_config_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting download_config %s" % (download_config_data,)) + self.download_config.emit(download_config_data) + + return download_config_data[self.PASSED_KEY] + + def run_smtp_setup_checks(self, + checker, + provider_config, + smtp_config, + download_if_needed=False): + """ + Starts the checks needed for a new smtp setup + + :param checker: Object that executes actions in a different + thread + :type checker: leap.util.checkerthread.CheckerThread + :param provider_config: Provider configuration + :type provider_config: ProviderConfig + :param smtp_config: SMTP configuration to populate + :type smtp_config: SMTPConfig + :param download_if_needed: True if it should check for mtime + for the file + :type download_if_needed: bool + """ + leap_assert_type(provider_config, ProviderConfig) + + self._provider_config = provider_config + self._smtp_config = smtp_config + self._download_if_needed = download_if_needed + + checker.add_checks([ + self._download_config + ]) diff --git a/src/leap/services/mail/smtpconfig.py b/src/leap/services/mail/smtpconfig.py new file mode 100644 index 00000000..e7e2895a --- /dev/null +++ b/src/leap/services/mail/smtpconfig.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# smtpconfig.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +SMTP configuration +""" +import logging + +from leap.common.config.baseconfig import BaseConfig +from leap.services.mail.smtpspec import smtp_config_spec + +logger = logging.getLogger(__name__) + + +class SMTPConfig(BaseConfig): + """ + SMTP configuration abstraction class + """ + + def __init__(self): + BaseConfig.__init__(self) + + def _get_spec(self): + """ + Returns the spec object for the specific configuration + """ + return smtp_config_spec + + def get_hosts(self): + return self._safe_get_value("hosts") + + def get_locations(self): + return self._safe_get_value("locations") + diff --git a/src/leap/services/mail/smtpspec.py b/src/leap/services/mail/smtpspec.py new file mode 100644 index 00000000..b455b196 --- /dev/null +++ b/src/leap/services/mail/smtpspec.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# smtpspec.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +smtp_config_spec = { + 'description': 'sample smtp service config', + 'type': 'object', + 'properties': { + 'serial': { + 'type': int, + 'default': 1, + 'required': True + }, + 'version': { + 'type': int, + 'default': 1, + 'required': True + }, + 'hosts': { + 'type': dict, + 'default': { + "walrus": { + "hostname": "someprovider", + "ip_address": "1.1.1.1", + "port": 1111 + }, + }, + }, + 'locations': { + 'type': dict, + 'default': { + "locations": { + + } + } + } + } +} diff --git a/src/leap/services/soledad/soledadbootstrapper.py b/src/leap/services/soledad/soledadbootstrapper.py index 51c53a6e..bae933de 100644 --- a/src/leap/services/soledad/soledadbootstrapper.py +++ b/src/leap/services/soledad/soledadbootstrapper.py @@ -47,6 +47,8 @@ class SoledadBootstrapper(QtCore.QObject): PASSED_KEY = "passed" ERROR_KEY = "error" + SOLEDAD_KEY = "soledad" + KEYMANAGER_KEY = "keymanager" PUBKEY_KEY = "user[public_key]" @@ -199,7 +201,9 @@ class SoledadBootstrapper(QtCore.QObject): genkey_data = { self.PASSED_KEY: False, - self.ERROR_KEY: "" + self.ERROR_KEY: "", + self.SOLEDAD_KEY: None, + self.KEYMANAGER_KEY: None } try: @@ -245,6 +249,8 @@ class SoledadBootstrapper(QtCore.QObject): cookies=cookies) key_result.raise_for_status() genkey_data[self.PASSED_KEY] = True + genkey_data[self.SOLEDAD_KEY] = self._soledad + genkey_data[self.KEYMANAGER_KEY] = self._keymanager except Exception as e: genkey_data[self.PASSED_KEY] = False genkey_data[self.ERROR_KEY] = "%s" % (e,) -- cgit v1.2.3 From 372a9aad068e16c0ed957fac5c081ce409dbf7e7 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Tue, 21 May 2013 10:20:43 -0300 Subject: Add support for kde polkit, closes #2630 Close issue #2630 Add changes file for #2630 Add documentation to '_is_auth_agent_running' method. --- src/leap/services/eip/vpnlaunchers.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 540bc45e..addad959 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -124,9 +124,16 @@ def _has_updown_scripts(path): def _is_auth_agent_running(): - return len( - commands.getoutput( - 'ps aux | grep polkit-[g]nome-authentication-agent-1')) > 0 + """ + Checks if a polkit daemon is running. + + :return: True if it's running, False if it's not. + :rtype: boolean + """ + polkit_gnome = 'ps aux | grep polkit-[g]nome-authentication-agent-1' + polkit_kde = 'ps aux | grep polkit-[k]de-authentication-agent-1' + return (len(commands.getoutput(polkit_gnome) > 0) or + len(commands.getoutput(polkit_kde)) > 0) class LinuxVPNLauncher(VPNLauncher): -- cgit v1.2.3 From 116f559198e0a9c26e9291209e81190bb27ef5cf Mon Sep 17 00:00:00 2001 From: drebs Date: Tue, 28 May 2013 20:51:58 -0300 Subject: Move KeyManager submodule from leap.common to here. --- src/leap/services/soledad/soledadbootstrapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/soledad/soledadbootstrapper.py b/src/leap/services/soledad/soledadbootstrapper.py index bae933de..2869e098 100644 --- a/src/leap/services/soledad/soledadbootstrapper.py +++ b/src/leap/services/soledad/soledadbootstrapper.py @@ -29,8 +29,8 @@ from mock import Mock from leap.common.check import leap_assert, leap_assert_type from leap.common.files import get_mtime -from leap.common.keymanager import KeyManager, openpgp -from leap.common.keymanager.errors import KeyNotFound +from leap.keymanager import KeyManager, openpgp +from leap.keymanager.errors import KeyNotFound from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth from leap.services.soledad.soledadconfig import SoledadConfig -- cgit v1.2.3 From 884d0e0f4dbba34b6f6f5afe6e27390a7606a7fa Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 29 May 2013 04:02:43 +0900 Subject: make tests pass & fix pep8 --- src/leap/services/eip/eipspec.py | 4 ++-- src/leap/services/eip/providerbootstrapper.py | 10 +++++++--- src/leap/services/eip/tests/test_eipconfig.py | 25 +++++++++++++----------- src/leap/services/mail/smtpbootstrapper.py | 8 ++++---- src/leap/services/mail/smtpconfig.py | 1 - src/leap/services/soledad/soledadbootstrapper.py | 8 +++++--- src/leap/services/soledad/soledadconfig.py | 1 - 7 files changed, 32 insertions(+), 25 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipspec.py b/src/leap/services/eip/eipspec.py index 7fa782c7..94ba674f 100644 --- a/src/leap/services/eip/eipspec.py +++ b/src/leap/services/eip/eipspec.py @@ -22,12 +22,12 @@ eipservice_config_spec = { 'serial': { 'type': int, 'default': 1, - 'required': True + 'required': ["True"] }, 'version': { 'type': int, 'default': 1, - 'required': True + 'required': ["True"] }, 'clusters': { 'type': list, diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 734d3867..289d212b 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -209,13 +209,16 @@ class ProviderBootstrapper(QtCore.QObject): def run_provider_select_checks(self, checker, domain, download_if_needed=False): """ - Populates the check queue + Populates the check queue. :param checker: checker thread to be used to run this check :type checker: CheckerThread + :param domain: domain to check :type domain: str - :param download_if_needed: if True, makes the checks do not overwrite already downloaded data + + :param download_if_needed: if True, makes the checks do not + overwrite already downloaded data :type download_if_needed: bool :return: True if the checks passed, False otherwise @@ -407,7 +410,8 @@ class ProviderBootstrapper(QtCore.QObject): :param provider_config: Provider configuration :type provider_config: ProviderConfig - :param download_if_needed: if True, makes the checks do not overwrite already downloaded data. + :param download_if_needed: if True, makes the checks do not + overwrite already downloaded data. :type download_if_needed: bool """ leap_assert(provider_config, "We need a provider config!") diff --git a/src/leap/services/eip/tests/test_eipconfig.py b/src/leap/services/eip/tests/test_eipconfig.py index ce04c2fc..0bd19d5e 100644 --- a/src/leap/services/eip/tests/test_eipconfig.py +++ b/src/leap/services/eip/tests/test_eipconfig.py @@ -97,10 +97,12 @@ class EIPConfigTest(BaseLeapTest): """ self.write_config(sample_config) config = EIPConfig() - self.assertRaises( - AssertionError, - config.get_clusters) - self.assertTrue(config.load(self.configfile)) + #self.assertRaises( + #AssertionError, + #config.get_clusters) + + self.assertTrue(config.load( + self.configfile, relative=False)) self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) @@ -123,7 +125,8 @@ class EIPConfigTest(BaseLeapTest): data['openvpn_configuration']["extra_param"] = "FOO" self.write_config(data) config = EIPConfig() - config.load(self.configfile) + config.load( + self.configfile, relative=False) self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) @@ -133,7 +136,7 @@ class EIPConfigTest(BaseLeapTest): data['openvpn_configuration']["auth"] = "SHA1;" self.write_config(data) config = EIPConfig() - config.load(self.configfile) + config.load(self.configfile, relative=False) self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) @@ -143,7 +146,7 @@ class EIPConfigTest(BaseLeapTest): data['openvpn_configuration']["auth"] = "SHA1>`&|" self.write_config(data) config = EIPConfig() - config.load(self.configfile) + config.load(self.configfile, relative=False) self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) @@ -153,7 +156,7 @@ class EIPConfigTest(BaseLeapTest): data['openvpn_configuration']["auth"] = "shaSHA1" self.write_config(data) config = EIPConfig() - config.load(self.configfile) + config.load(self.configfile, relative=False) self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) @@ -163,7 +166,7 @@ class EIPConfigTest(BaseLeapTest): data['openvpn_configuration']["auth"] = "sha&*!@#;" self.write_config(data) config = EIPConfig() - config.load(self.configfile) + config.load(self.configfile, relative=False) self.assertEqual( config.get_openvpn_configuration(), {'cipher': 'AES-128-CBC', @@ -174,7 +177,7 @@ class EIPConfigTest(BaseLeapTest): data['gateways'][0]["ip_address"] = "11.22.33.44;" self.write_config(data) config = EIPConfig() - config.load(self.configfile) + config.load(self.configfile, relative=False) self.assertEqual( config.get_gateway_ip(), None) @@ -183,7 +186,7 @@ class EIPConfigTest(BaseLeapTest): data['gateways'][0]["ip_address"] = "11.22.33.44`" self.write_config(data) config = EIPConfig() - config.load(self.configfile) + config.load(self.configfile, relative=False) self.assertEqual( config.get_gateway_ip(), None) diff --git a/src/leap/services/mail/smtpbootstrapper.py b/src/leap/services/mail/smtpbootstrapper.py index 7e0f10de..6e0a0a47 100644 --- a/src/leap/services/mail/smtpbootstrapper.py +++ b/src/leap/services/mail/smtpbootstrapper.py @@ -116,10 +116,10 @@ class SMTPBootstrapper(QtCore.QObject): # Not modified if res.status_code == 304: logger.debug("SMTP definition has not been modified") - self._smtp_config.load(os.path.join("leap", - "providers", - self._provider_config.get_domain(), - "smtp-service.json")) + self._smtp_config.load(os.path.join( + "leap", "providers", + self._provider_config.get_domain(), + "smtp-service.json")) else: smtp_definition, mtime = get_content(res) diff --git a/src/leap/services/mail/smtpconfig.py b/src/leap/services/mail/smtpconfig.py index e7e2895a..30371005 100644 --- a/src/leap/services/mail/smtpconfig.py +++ b/src/leap/services/mail/smtpconfig.py @@ -45,4 +45,3 @@ class SMTPConfig(BaseConfig): def get_locations(self): return self._safe_get_value("locations") - diff --git a/src/leap/services/soledad/soledadbootstrapper.py b/src/leap/services/soledad/soledadbootstrapper.py index bae933de..db019a87 100644 --- a/src/leap/services/soledad/soledadbootstrapper.py +++ b/src/leap/services/soledad/soledadbootstrapper.py @@ -218,7 +218,8 @@ class SoledadBootstrapper(QtCore.QObject): self._keymanager.get_key(address, openpgp.OpenPGPKey, private=True, fetch_remote=False) except KeyNotFound: - logger.debug("Key not found. Generating key for %s" % (address,)) + logger.debug( + "Key not found. Generating key for %s" % (address,)) self._keymanager.gen_key(openpgp.OpenPGPKey) logger.debug("Key generated successfully.") @@ -235,8 +236,9 @@ class SoledadBootstrapper(QtCore.QObject): logger.debug("Uploading public key to %s" % (key_uri,)) - pubkey = self._keymanager.get_key(address, openpgp.OpenPGPKey, - private=False, fetch_remote=False) + pubkey = self._keymanager.get_key( + address, openpgp.OpenPGPKey, + private=False, fetch_remote=False) key_data = { self.PUBKEY_KEY: pubkey.key_data, } diff --git a/src/leap/services/soledad/soledadconfig.py b/src/leap/services/soledad/soledadconfig.py index 836265f3..80a82d11 100644 --- a/src/leap/services/soledad/soledadconfig.py +++ b/src/leap/services/soledad/soledadconfig.py @@ -45,4 +45,3 @@ class SoledadConfig(BaseConfig): def get_locations(self): return self._safe_get_value("locations") - -- cgit v1.2.3 From 4c726c1531abfe288604eaa4c1d347e85bed81eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 3 Jun 2013 15:02:41 -0300 Subject: Use Twisted's deferToThread and Deferreds to handle parallel tasks This removes CheckerThread --- src/leap/services/abstractbootstrapper.py | 155 +++++++++ src/leap/services/eip/eipbootstrapper.py | 264 +++++----------- src/leap/services/eip/providerbootstrapper.py | 384 ++++++----------------- src/leap/services/eip/vpnlaunchers.py | 2 +- src/leap/services/mail/smtpbootstrapper.py | 142 ++++----- src/leap/services/soledad/soledadbootstrapper.py | 233 +++++--------- 6 files changed, 466 insertions(+), 714 deletions(-) create mode 100644 src/leap/services/abstractbootstrapper.py (limited to 'src/leap/services') diff --git a/src/leap/services/abstractbootstrapper.py b/src/leap/services/abstractbootstrapper.py new file mode 100644 index 00000000..bce03e6b --- /dev/null +++ b/src/leap/services/abstractbootstrapper.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# abstractbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Abstract bootstrapper implementation +""" +import logging + +import requests + +from PySide import QtCore +from twisted.internet import threads +from leap.common.check import leap_assert, leap_assert_type + +logger = logging.getLogger(__name__) + + +class AbstractBootstrapper(QtCore.QObject): + """ + Abstract Bootstrapper that implements the needed deferred callbacks + """ + + PASSED_KEY = "passed" + ERROR_KEY = "error" + + def __init__(self, bypass_checks=False): + """ + Constructor for the abstract bootstrapper + + :param bypass_checks: Set to true if the app should bypass + first round of checks for CA + certificates at bootstrap + :type bypass_checks: bool + """ + QtCore.QObject.__init__(self) + + leap_assert(self._gui_errback.im_func == \ + AbstractBootstrapper._gui_errback.im_func, + "Cannot redefine _gui_errback") + leap_assert(self._errback.im_func == \ + AbstractBootstrapper._errback.im_func, + "Cannot redefine _errback") + leap_assert(self._gui_notify.im_func == \ + AbstractBootstrapper._gui_notify.im_func, + "Cannot redefine _gui_notify") + + # **************************************************** # + # Dependency injection helpers, override this for more + # granular testing + self._fetcher = requests + # **************************************************** # + + self._session = self._fetcher.session() + self._bypass_checks = bypass_checks + self._signal_to_emit = None + self._err_msg = None + + def _gui_errback(self, failure): + """ + Errback used to notify the GUI of a problem, it should be used + as the last errback of the whole chain. + + Traps all exceptions if a signal is defined, otherwise it just + lets it continue. + + NOTE: This method is final, it should not be redefined. + + :param failure: failure object that Twisted generates + :type failure: twisted.python.failure.Failure + """ + if self._signal_to_emit: + err_msg = self._err_msg \ + if self._err_msg is not None \ + else str(failure.value) + self._signal_to_emit.emit({ + self.PASSED_KEY: False, + self.ERROR_KEY: err_msg + }) + failure.trap(Exception) + + def _errback(self, failure, signal=None): + """ + Regular errback used for the middle of the chain. If it's + executed, the first one will set the signal to emit as + failure. + + NOTE: This method is final, it should not be redefined. + + :param failure: failure object that Twisted generates + :type failure: twisted.python.failure.Failure + :param signal: Signal to emit if it fails here first + :type signal: QtCore.SignalInstance + + :returns: failure object that Twisted generates + :rtype: twisted.python.failure.Failure + """ + if self._signal_to_emit is None: + self._signal_to_emit = signal + return failure + + def _gui_notify(self, _, signal=None): + """ + Callback used to notify the GUI of a success. Will emit signal + if specified + + NOTE: This method is final, it should not be redefined. + + :param _: IGNORED. Returned from the previous callback + :type _: IGNORED + :param signal: Signal to emit if it fails here first + :type signal: QtCore.SignalInstance + """ + if signal: + logger.debug("Emitting %s" % (signal,)) + signal.emit({self.PASSED_KEY: True, self.ERROR_KEY: ""}) + + def addCallbackChain(self, callbacks): + """ + Creates a callback/errback chain on another thread using + deferToThread and adds the _gui_errback to the end to notify + the GUI on an error. + + :param callbacks: List of tuples of callbacks and the signal + associated to that callback + :type callbacks: list(tuple(func, func)) + """ + leap_assert_type(callbacks, list) + + self._signal_to_emit = None + self._err_msg = None + + d = None + for cb, sig in callbacks: + if d is None: + d = threads.deferToThread(cb) + else: + d.addCallback(cb) + d.addErrback(self._errback, signal=sig) + d.addCallback(self._gui_notify, signal=sig) + d.addErrback(self._gui_errback) + diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index a881f235..7216bb80 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -22,9 +22,7 @@ EIP bootstrapping import logging import os -import requests - -from PySide import QtGui, QtCore +from PySide import QtCore from leap.common.check import leap_assert, leap_assert_type from leap.common.certs import is_valid_pemfile, should_redownload @@ -32,49 +30,34 @@ from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth from leap.services.eip.eipconfig import EIPConfig -from leap.util.checkerthread import CheckerThread from leap.util.request_helpers import get_content +from leap.services.abstractbootstrapper import AbstractBootstrapper logger = logging.getLogger(__name__) -class EIPBootstrapper(QtCore.QObject): +class EIPBootstrapper(AbstractBootstrapper): """ Sets up EIP for a provider a series of checks and emits signals after they are passed. If a check fails, the subsequent checks are not executed """ - PASSED_KEY = "passed" - ERROR_KEY = "error" - - IDLE_SLEEP_INTERVAL = 100 - # All dicts returned are of the form # {"passed": bool, "error": str} download_config = QtCore.Signal(dict) download_client_certificate = QtCore.Signal(dict) def __init__(self): - QtCore.QObject.__init__(self) + AbstractBootstrapper.__init__(self) - # **************************************************** # - # Dependency injection helpers, override this for more - # granular testing - self._fetcher = requests - # **************************************************** # - - self._session = self._fetcher.session() self._provider_config = None self._eip_config = None self._download_if_needed = False - def _download_config(self): + def _download_config(self, *args): """ Downloads the EIP config for the given provider - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(self._provider_config, @@ -83,65 +66,47 @@ class EIPBootstrapper(QtCore.QObject): logger.debug("Downloading EIP config for %s" % (self._provider_config.get_domain(),)) - download_config_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - self._eip_config = EIPConfig() - try: - headers = {} - mtime = get_mtime(os.path.join(self._eip_config - .get_path_prefix(), - "leap", - "providers", - self._provider_config.get_domain(), - "eip-service.json")) - - if self._download_if_needed and mtime: - headers['if-modified-since'] = mtime - - # there is some confusion with this uri, - # it's in 1/config/eip, config/eip and config/1/eip... - config_uri = "%s/%s/config/eip-service.json" % ( - self._provider_config.get_api_uri(), - self._provider_config.get_api_version()) - logger.debug('Downloading eip config from: %s' % config_uri) - - res = self._session.get(config_uri, - verify=self._provider_config - .get_ca_cert_path(), - headers=headers) - res.raise_for_status() - - # Not modified - if res.status_code == 304: - logger.debug("EIP definition has not been modified") - else: - eip_definition, mtime = get_content(res) - - self._eip_config.load(data=eip_definition, mtime=mtime) - self._eip_config.save(["leap", + headers = {} + mtime = get_mtime(os.path.join(self._eip_config + .get_path_prefix(), + "leap", "providers", self._provider_config.get_domain(), - "eip-service.json"]) - - download_config_data[self.PASSED_KEY] = True - except Exception as e: - download_config_data[self.ERROR_KEY] = "%s" % (e,) - - logger.debug("Emitting download_config %s" % (download_config_data,)) - self.download_config.emit(download_config_data) - - return download_config_data[self.PASSED_KEY] - - def _download_client_certificates(self): + "eip-service.json")) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + # there is some confusion with this uri, + # it's in 1/config/eip, config/eip and config/1/eip... + config_uri = "%s/%s/config/eip-service.json" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('Downloading eip config from: %s' % config_uri) + + res = self._session.get(config_uri, + verify=self._provider_config + .get_ca_cert_path(), + headers=headers) + res.raise_for_status() + + # Not modified + if res.status_code == 304: + logger.debug("EIP definition has not been modified") + else: + eip_definition, mtime = get_content(res) + + self._eip_config.load(data=eip_definition, mtime=mtime) + self._eip_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "eip-service.json"]) + + def _download_client_certificates(self, *args): """ Downloads the EIP client certificate for the given provider - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(self._provider_config, "We need a provider configuration!") leap_assert(self._eip_config, "We need an eip configuration!") @@ -149,11 +114,6 @@ class EIPBootstrapper(QtCore.QObject): logger.debug("Downloading EIP client certificate for %s" % (self._provider_config.get_domain(),)) - download_cert = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - client_cert_path = self._eip_config.\ get_client_cert_path(self._provider_config, about_to_download=True) @@ -164,56 +124,39 @@ class EIPBootstrapper(QtCore.QObject): if self._download_if_needed and \ os.path.exists(client_cert_path): - try: - check_and_fix_urw_only(client_cert_path) - download_cert[self.PASSED_KEY] = True - except Exception as e: - download_cert[self.PASSED_KEY] = False - download_cert[self.ERROR_KEY] = "%s" % (e,) - self.download_client_certificate.emit(download_cert) - return download_cert[self.PASSED_KEY] - - try: - srp_auth = SRPAuth(self._provider_config) - session_id = srp_auth.get_session_id() - cookies = None - if session_id: - cookies = {"_session_id": session_id} - cert_uri = "%s/%s/cert" % ( - self._provider_config.get_api_uri(), - self._provider_config.get_api_version()) - logger.debug('getting cert from uri: %s' % cert_uri) - res = self._session.get(cert_uri, - verify=self._provider_config - .get_ca_cert_path(), - cookies=cookies) - res.raise_for_status() - client_cert = res.content - - # TODO: check certificate validity - - if not is_valid_pemfile(client_cert): - raise Exception(self.tr("The downloaded certificate is not a " - "valid PEM file")) - - mkdir_p(os.path.dirname(client_cert_path)) - - with open(client_cert_path, "w") as f: - f.write(client_cert) - check_and_fix_urw_only(client_cert_path) - - download_cert[self.PASSED_KEY] = True - except Exception as e: - download_cert[self.ERROR_KEY] = "%s" % (e,) - - logger.debug("Emitting download_client_certificates %s" % - (download_cert,)) - self.download_client_certificate.emit(download_cert) - - return download_cert[self.PASSED_KEY] - - def run_eip_setup_checks(self, checker, + return + + srp_auth = SRPAuth(self._provider_config) + session_id = srp_auth.get_session_id() + cookies = None + if session_id: + cookies = {"_session_id": session_id} + cert_uri = "%s/%s/cert" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('getting cert from uri: %s' % cert_uri) + res = self._session.get(cert_uri, + verify=self._provider_config + .get_ca_cert_path(), + cookies=cookies) + res.raise_for_status() + client_cert = res.content + + # TODO: check certificate validity + + if not is_valid_pemfile(client_cert): + raise Exception(self.tr("The downloaded certificate is not a " + "valid PEM file")) + + mkdir_p(os.path.dirname(client_cert_path)) + + with open(client_cert_path, "w") as f: + f.write(client_cert) + + check_and_fix_urw_only(client_cert_path) + + def run_eip_setup_checks(self, provider_config, download_if_needed=False): """ @@ -228,60 +171,9 @@ class EIPBootstrapper(QtCore.QObject): self._provider_config = provider_config self._download_if_needed = download_if_needed - checker.add_checks([ - self._download_config, - self._download_client_certificates - ]) - - -if __name__ == "__main__": - import sys - from functools import partial - app = QtGui.QApplication(sys.argv) - - import signal - - def sigint_handler(*args, **kwargs): - logger.debug('SIGINT catched. shutting down...') - checker = args[0] - checker.set_should_quit() - QtGui.QApplication.quit() - - def signal_tester(d): - print d - - logger = logging.getLogger(name='leap') - logger.setLevel(logging.DEBUG) - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - eip_checks = EIPBootstrapper() - checker = CheckerThread() - - sigint = partial(sigint_handler, checker) - signal.signal(signal.SIGINT, sigint) - - timer = QtCore.QTimer() - timer.start(500) - timer.timeout.connect(lambda: None) - app.connect(app, QtCore.SIGNAL("aboutToQuit()"), - checker.set_should_quit) - w = QtGui.QWidget() - w.resize(100, 100) - w.show() - - checker.start() - - provider_config = ProviderConfig() - if provider_config.load(os.path.join("leap", - "providers", - "bitmask.net", - "provider.json")): - eip_checks.run_eip_setup_checks(checker, provider_config) - - sys.exit(app.exec_()) + cb_chain = [ + (self._download_config, self.download_config), + (self._download_client_certificates, self.download_client_certificate) + ] + + self.addCallbackChain(cb_chain) diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 289d212b..1339e086 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -24,30 +24,25 @@ import os import requests -from PySide import QtGui, QtCore +from PySide import QtCore from leap.common.certs import get_digest from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.common.check import leap_assert, leap_assert_type from leap.config.providerconfig import ProviderConfig -from leap.util.checkerthread import CheckerThread from leap.util.request_helpers import get_content +from leap.services.abstractbootstrapper import AbstractBootstrapper logger = logging.getLogger(__name__) -class ProviderBootstrapper(QtCore.QObject): +class ProviderBootstrapper(AbstractBootstrapper): """ Given a provider URL performs a series of checks and emits signals after they are passed. If a check fails, the subsequent checks are not executed """ - PASSED_KEY = "passed" - ERROR_KEY = "error" - - IDLE_SLEEP_INTERVAL = 100 - # All dicts returned are of the form # {"passed": bool, "error": str} name_resolution = QtCore.Signal(dict) @@ -66,68 +61,34 @@ class ProviderBootstrapper(QtCore.QObject): first round of checks for CA certificates at bootstrap :type bypass_checks: bool """ - QtCore.QObject.__init__(self) + AbstractBootstrapper.__init__(self, bypass_checks) - # **************************************************** # - # Dependency injection helpers, override this for more - # granular testing - self._fetcher = requests - # **************************************************** # - - self._session = self._fetcher.session() self._domain = None self._provider_config = None self._download_if_needed = False - self._bypass_checks = bypass_checks def _check_name_resolution(self): """ Checks that the name resolution for the provider name works - - :return: True if the checks passed, False otherwise - :rtype: bool """ - leap_assert(self._domain, "Cannot check DNS without a domain") logger.debug("Checking name resolution for %s" % (self._domain)) - name_resolution_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - # We don't skip this check, since it's basic for the whole # system to work - try: - socket.gethostbyname(self._domain) - name_resolution_data[self.PASSED_KEY] = True - except socket.gaierror as e: - name_resolution_data[self.ERROR_KEY] = "%s" % (e,) + socket.gethostbyname(self._domain) - logger.debug("Emitting name_resolution %s" % (name_resolution_data,)) - self.name_resolution.emit(name_resolution_data) - - return name_resolution_data[self.PASSED_KEY] - - def _check_https(self): + def _check_https(self, *args): """ Checks that https is working and that the provided certificate checks out - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(self._domain, "Cannot check HTTPS without a domain") logger.debug("Checking https for %s" % (self._domain)) - https_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - # We don't skip this check, since it's basic for the whole # system to work @@ -135,105 +96,75 @@ class ProviderBootstrapper(QtCore.QObject): res = self._session.get("https://%s" % (self._domain,), verify=not self._bypass_checks) res.raise_for_status() - https_data[self.PASSED_KEY] = True - except requests.exceptions.SSLError as e: - logger.error("%s" % (e,)) - https_data[self.ERROR_KEY] = self.tr("Provider certificate could " - "not verify") - except Exception as e: - logger.error("%s" % (e,)) - https_data[self.ERROR_KEY] = self.tr("Provider does not support " - "HTTPS") - - logger.debug("Emitting https_connection %s" % (https_data,)) - self.https_connection.emit(https_data) - - return https_data[self.PASSED_KEY] - - def _download_provider_info(self): + except requests.exceptions.SSLError: + self._err_msg = self.tr("Provider certificate could " + "not be verified") + raise + except Exception: + self._err_msg = self.tr("Provider does not support HTTPS") + raise + + def _download_provider_info(self, *args): """ Downloads the provider.json defition - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(self._domain, "Cannot download provider info without a domain") logger.debug("Downloading provider info for %s" % (self._domain)) - download_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - - try: - headers = {} - mtime = get_mtime(os.path.join(ProviderConfig() - .get_path_prefix(), - "leap", - "providers", - self._domain, - "provider.json")) - if self._download_if_needed and mtime: - headers['if-modified-since'] = mtime - - res = self._session.get("https://%s/%s" % (self._domain, - "provider.json"), - headers=headers, - verify=not self._bypass_checks) - res.raise_for_status() - - # Not modified - if res.status_code == 304: - logger.debug("Provider definition has not been modified") - else: - provider_definition, mtime = get_content(res) - - provider_config = ProviderConfig() - provider_config.load(data=provider_definition, mtime=mtime) - provider_config.save(["leap", - "providers", - self._domain, - "provider.json"]) - - download_data[self.PASSED_KEY] = True - except Exception as e: - download_data[self.ERROR_KEY] = "%s" % (e,) - - logger.debug("Emitting download_provider_info %s" % (download_data,)) - self.download_provider_info.emit(download_data) - - return download_data[self.PASSED_KEY] - - def run_provider_select_checks(self, checker, - domain, download_if_needed=False): + headers = {} + mtime = get_mtime(os.path.join(ProviderConfig() + .get_path_prefix(), + "leap", + "providers", + self._domain, + "provider.json")) + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + res = self._session.get("https://%s/%s" % (self._domain, + "provider.json"), + headers=headers, + verify=not self._bypass_checks) + res.raise_for_status() + + # Not modified + if res.status_code == 304: + logger.debug("Provider definition has not been modified") + else: + provider_definition, mtime = get_content(res) + + provider_config = ProviderConfig() + provider_config.load(data=provider_definition, mtime=mtime) + provider_config.save(["leap", + "providers", + self._domain, + "provider.json"]) + + def run_provider_select_checks(self, domain, download_if_needed=False): """ Populates the check queue. - :param checker: checker thread to be used to run this check - :type checker: CheckerThread - :param domain: domain to check :type domain: str :param download_if_needed: if True, makes the checks do not overwrite already downloaded data :type download_if_needed: bool - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(domain and len(domain) > 0, "We need a domain!") self._domain = domain self._download_if_needed = download_if_needed - checker.add_checks([ - self._check_name_resolution, - self._check_https, - self._download_provider_info - ]) + cb_chain = [ + (self._check_name_resolution, self.name_resolution), + (self._check_https, self.https_connection), + (self._download_provider_info, self.download_provider_info) + ] + + self.addCallbackChain(cb_chain) def _should_proceed_cert(self): """ @@ -250,12 +181,9 @@ class ProviderBootstrapper(QtCore.QObject): return not os.path.exists(self._provider_config .get_ca_cert_path(about_to_download=True)) - def _download_ca_cert(self): + def _download_ca_cert(self, *args): """ Downloads the CA cert that is going to be used for the api URL - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(self._provider_config, "Cannot download the ca cert " @@ -264,56 +192,28 @@ class ProviderBootstrapper(QtCore.QObject): logger.debug("Downloading ca cert for %s at %s" % (self._domain, self._provider_config.get_ca_cert_uri())) - download_ca_cert_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - if not self._should_proceed_cert(): - try: - check_and_fix_urw_only( - self._provider_config - .get_ca_cert_path(about_to_download=True)) - download_ca_cert_data[self.PASSED_KEY] = True - except Exception as e: - download_ca_cert_data[self.PASSED_KEY] = False - download_ca_cert_data[self.ERROR_KEY] = "%s" % (e,) - self.download_ca_cert.emit(download_ca_cert_data) - return download_ca_cert_data[self.PASSED_KEY] - - try: - res = self._session.get(self._provider_config.get_ca_cert_uri(), - verify=not self._bypass_checks) - res.raise_for_status() - - cert_path = self._provider_config.get_ca_cert_path( - about_to_download=True) - - cert_dir = os.path.dirname(cert_path) + check_and_fix_urw_only( + self._provider_config + .get_ca_cert_path(about_to_download=True)) - mkdir_p(cert_dir) + res = self._session.get(self._provider_config.get_ca_cert_uri(), + verify=not self._bypass_checks) + res.raise_for_status() - with open(cert_path, "w") as f: - f.write(res.content) + cert_path = self._provider_config.get_ca_cert_path( + about_to_download=True) + cert_dir = os.path.dirname(cert_path) + mkdir_p(cert_dir) + with open(cert_path, "w") as f: + f.write(res.content) - check_and_fix_urw_only(cert_path) + check_and_fix_urw_only(cert_path) - download_ca_cert_data[self.PASSED_KEY] = True - except Exception as e: - download_ca_cert_data[self.ERROR_KEY] = "%s" % (e,) - - logger.debug("Emitting download_ca_cert %s" % (download_ca_cert_data,)) - self.download_ca_cert.emit(download_ca_cert_data) - - return download_ca_cert_data[self.PASSED_KEY] - - def _check_ca_fingerprint(self): + def _check_ca_fingerprint(self, *args): """ Checks the CA cert fingerprint against the one provided in the json definition - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(self._provider_config, "Cannot check the ca cert " "without a provider config!") @@ -322,50 +222,27 @@ class ProviderBootstrapper(QtCore.QObject): (self._domain, self._provider_config.get_ca_cert_path())) - check_ca_fingerprint_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - if not self._should_proceed_cert(): - check_ca_fingerprint_data[self.PASSED_KEY] = True - self.check_ca_fingerprint.emit(check_ca_fingerprint_data) - return True + return - try: - parts = self._provider_config.get_ca_cert_fingerprint().split(":") - leap_assert(len(parts) == 2, "Wrong fingerprint format") - - method = parts[0].strip() - fingerprint = parts[1].strip() - cert_data = None - with open(self._provider_config.get_ca_cert_path()) as f: - cert_data = f.read() - - leap_assert(len(cert_data) > 0, "Could not read certificate data") - - digest = get_digest(cert_data, method) + parts = self._provider_config.get_ca_cert_fingerprint().split(":") + leap_assert(len(parts) == 2, "Wrong fingerprint format") - leap_assert(digest == fingerprint, - "Downloaded certificate has a different fingerprint!") + method = parts[0].strip() + fingerprint = parts[1].strip() + cert_data = None + with open(self._provider_config.get_ca_cert_path()) as f: + cert_data = f.read() - check_ca_fingerprint_data[self.PASSED_KEY] = True - except Exception as e: - check_ca_fingerprint_data[self.ERROR_KEY] = "%s" % (e,) + leap_assert(len(cert_data) > 0, "Could not read certificate data") + digest = get_digest(cert_data, method) + leap_assert(digest == fingerprint, + "Downloaded certificate has a different fingerprint!") - logger.debug("Emitting check_ca_fingerprint %s" % - (check_ca_fingerprint_data,)) - self.check_ca_fingerprint.emit(check_ca_fingerprint_data) - - return check_ca_fingerprint_data[self.PASSED_KEY] - - def _check_api_certificate(self): + def _check_api_certificate(self, *args): """ Tries to make an API call with the downloaded cert and checks if it validates against it - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(self._provider_config, "Cannot check the ca cert " "without a provider config!") @@ -374,34 +251,17 @@ class ProviderBootstrapper(QtCore.QObject): (self._provider_config.get_api_uri(), self._provider_config.get_ca_cert_path())) - check_api_certificate_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - if not self._should_proceed_cert(): - check_api_certificate_data[self.PASSED_KEY] = True - self.check_api_certificate.emit(check_api_certificate_data) - return True + return - try: - test_uri = "%s/%s/cert" % (self._provider_config.get_api_uri(), - self._provider_config.get_api_version()) - res = self._session.get(test_uri, - verify=self._provider_config - .get_ca_cert_path()) - res.raise_for_status() - check_api_certificate_data[self.PASSED_KEY] = True - except Exception as e: - check_api_certificate_data[self.ERROR_KEY] = "%s" % (e,) + test_uri = "%s/%s/cert" % (self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + res = self._session.get(test_uri, + verify=self._provider_config + .get_ca_cert_path()) + res.raise_for_status() - logger.debug("Emitting check_api_certificate %s" % - (check_api_certificate_data,)) - self.check_api_certificate.emit(check_api_certificate_data) - - return check_api_certificate_data[self.PASSED_KEY] - - def run_provider_setup_checks(self, checker, + def run_provider_setup_checks(self, provider_config, download_if_needed=False): """ @@ -420,64 +280,10 @@ class ProviderBootstrapper(QtCore.QObject): self._provider_config = provider_config self._download_if_needed = download_if_needed - checker.add_checks([ - self._download_ca_cert, - self._check_ca_fingerprint, - self._check_api_certificate - ]) - -if __name__ == "__main__": - import sys - from functools import partial - app = QtGui.QApplication(sys.argv) - - import signal - - def sigint_handler(*args, **kwargs): - logger.debug('SIGINT catched. shutting down...') - bootstrapper_checks = args[0] - bootstrapper_checks.set_should_quit() - QtGui.QApplication.quit() - - def signal_tester(d): - print d - - logger = logging.getLogger(name='leap') - logger.setLevel(logging.DEBUG) - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - bootstrapper_checks = ProviderBootstrapper() - - checker = CheckerThread() - checker.start() - - sigint = partial(sigint_handler, checker) - signal.signal(signal.SIGINT, sigint) - - timer = QtCore.QTimer() - timer.start(500) - timer.timeout.connect(lambda: None) - app.connect(app, QtCore.SIGNAL("aboutToQuit()"), - checker.set_should_quit) - w = QtGui.QWidget() - w.resize(100, 100) - w.show() - - bootstrapper_checks.run_provider_select_checks(checker, - "bitmask.net") - - provider_config = ProviderConfig() - if provider_config.load(os.path.join("leap", - "providers", - "bitmask.net", - "provider.json")): - bootstrapper_checks.run_provider_setup_checks(checker, - provider_config) - - sys.exit(app.exec_()) + cb_chain = [ + (self._download_ca_cert, self.download_ca_cert), + (self._check_ca_fingerprint, self.check_ca_fingerprint), + (self._check_api_certificate, self.check_api_certificate) + ] + + self.addCallbackChain(cb_chain) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index addad959..0691e121 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -132,7 +132,7 @@ def _is_auth_agent_running(): """ polkit_gnome = 'ps aux | grep polkit-[g]nome-authentication-agent-1' polkit_kde = 'ps aux | grep polkit-[k]de-authentication-agent-1' - return (len(commands.getoutput(polkit_gnome) > 0) or + return (len(commands.getoutput(polkit_gnome)) > 0 or len(commands.getoutput(polkit_kde)) > 0) diff --git a/src/leap/services/mail/smtpbootstrapper.py b/src/leap/services/mail/smtpbootstrapper.py index 6e0a0a47..64bf3153 100644 --- a/src/leap/services/mail/smtpbootstrapper.py +++ b/src/leap/services/mail/smtpbootstrapper.py @@ -22,8 +22,6 @@ SMTP bootstrapping import logging import os -import requests - from PySide import QtCore from leap.common.check import leap_assert, leap_assert_type @@ -31,44 +29,30 @@ from leap.common.files import get_mtime from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth from leap.util.request_helpers import get_content +from leap.services.abstractbootstrapper import AbstractBootstrapper logger = logging.getLogger(__name__) -class SMTPBootstrapper(QtCore.QObject): +class SMTPBootstrapper(AbstractBootstrapper): """ SMTP init procedure """ - PASSED_KEY = "passed" - ERROR_KEY = "error" - - IDLE_SLEEP_INTERVAL = 100 - # All dicts returned are of the form # {"passed": bool, "error": str} download_config = QtCore.Signal(dict) def __init__(self): - QtCore.QObject.__init__(self) + AbstractBootstrapper.__init__(self) - # **************************************************** # - # Dependency injection helpers, override this for more - # granular testing - self._fetcher = requests - # **************************************************** # - - self._session = self._fetcher.session() self._provider_config = None self._smtp_config = None self._download_if_needed = False - def _download_config(self): + def _download_config(self, *args): """ Downloads the SMTP config for the given provider - - :return: True if everything went as expected, False otherwise - :rtype: bool """ leap_assert(self._provider_config, @@ -77,79 +61,59 @@ class SMTPBootstrapper(QtCore.QObject): logger.debug("Downloading SMTP config for %s" % (self._provider_config.get_domain(),)) - download_config_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - - try: - headers = {} - mtime = get_mtime(os.path.join(self._smtp_config - .get_path_prefix(), - "leap", - "providers", - self._provider_config.get_domain(), - "smtp-service.json")) - - if self._download_if_needed and mtime: - headers['if-modified-since'] = mtime - - # there is some confusion with this uri, - config_uri = "%s/%s/config/smtp-service.json" % ( - self._provider_config.get_api_uri(), - self._provider_config.get_api_version()) - logger.debug('Downloading SMTP config from: %s' % config_uri) - - srp_auth = SRPAuth(self._provider_config) - session_id = srp_auth.get_session_id() - cookies = None - if session_id: - cookies = {"_session_id": session_id} - - res = self._session.get(config_uri, - verify=self._provider_config - .get_ca_cert_path(), - headers=headers, - cookies=cookies) - res.raise_for_status() - - # Not modified - if res.status_code == 304: - logger.debug("SMTP definition has not been modified") - self._smtp_config.load(os.path.join( - "leap", "providers", - self._provider_config.get_domain(), - "smtp-service.json")) - else: - smtp_definition, mtime = get_content(res) - - self._smtp_config.load(data=smtp_definition, mtime=mtime) - self._smtp_config.save(["leap", - "providers", - self._provider_config.get_domain(), - "smtp-service.json"]) - - download_config_data[self.PASSED_KEY] = True - except Exception as e: - download_config_data[self.PASSED_KEY] = False - download_config_data[self.ERROR_KEY] = "%s" % (e,) - - logger.debug("Emitting download_config %s" % (download_config_data,)) - self.download_config.emit(download_config_data) - - return download_config_data[self.PASSED_KEY] + headers = {} + mtime = get_mtime(os.path.join(self._smtp_config + .get_path_prefix(), + "leap", + "providers", + self._provider_config.get_domain(), + "smtp-service.json")) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + # there is some confusion with this uri, + config_uri = "%s/%s/config/smtp-service.json" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('Downloading SMTP config from: %s' % config_uri) + + srp_auth = SRPAuth(self._provider_config) + session_id = srp_auth.get_session_id() + cookies = None + if session_id: + cookies = {"_session_id": session_id} + + res = self._session.get(config_uri, + verify=self._provider_config + .get_ca_cert_path(), + headers=headers, + cookies=cookies) + res.raise_for_status() + + # Not modified + if res.status_code == 304: + logger.debug("SMTP definition has not been modified") + self._smtp_config.load(os.path.join("leap", + "providers", + self._provider_config.get_domain(), + "smtp-service.json")) + else: + smtp_definition, mtime = get_content(res) + + self._smtp_config.load(data=smtp_definition, mtime=mtime) + self._smtp_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "smtp-service.json"]) def run_smtp_setup_checks(self, - checker, provider_config, smtp_config, download_if_needed=False): """ Starts the checks needed for a new smtp setup - :param checker: Object that executes actions in a different - thread - :type checker: leap.util.checkerthread.CheckerThread :param provider_config: Provider configuration :type provider_config: ProviderConfig :param smtp_config: SMTP configuration to populate @@ -164,6 +128,8 @@ class SMTPBootstrapper(QtCore.QObject): self._smtp_config = smtp_config self._download_if_needed = download_if_needed - checker.add_checks([ - self._download_config - ]) + cb_chain = [ + (self._download_config, self.download_config), + ] + + self.addCallbackChain(cb_chain) diff --git a/src/leap/services/soledad/soledadbootstrapper.py b/src/leap/services/soledad/soledadbootstrapper.py index eea9b0d5..2635a7e6 100644 --- a/src/leap/services/soledad/soledadbootstrapper.py +++ b/src/leap/services/soledad/soledadbootstrapper.py @@ -22,10 +22,7 @@ Soledad bootstrapping import logging import os -import requests - from PySide import QtCore -from mock import Mock from leap.common.check import leap_assert, leap_assert_type from leap.common.files import get_mtime @@ -36,39 +33,29 @@ from leap.crypto.srpauth import SRPAuth from leap.services.soledad.soledadconfig import SoledadConfig from leap.util.request_helpers import get_content from leap.soledad import Soledad +from leap.services.abstractbootstrapper import AbstractBootstrapper logger = logging.getLogger(__name__) -class SoledadBootstrapper(QtCore.QObject): +class SoledadBootstrapper(AbstractBootstrapper): """ Soledad init procedure """ - PASSED_KEY = "passed" - ERROR_KEY = "error" SOLEDAD_KEY = "soledad" KEYMANAGER_KEY = "keymanager" PUBKEY_KEY = "user[public_key]" - IDLE_SLEEP_INTERVAL = 100 - # All dicts returned are of the form # {"passed": bool, "error": str} download_config = QtCore.Signal(dict) gen_key = QtCore.Signal(dict) def __init__(self): - QtCore.QObject.__init__(self) - - # **************************************************** # - # Dependency injection helpers, override this for more - # granular testing - self._fetcher = requests - # **************************************************** # + AbstractBootstrapper.__init__(self) - self._session = self._fetcher.session() self._provider_config = None self._soledad_config = None self._keymanager = None @@ -76,6 +63,14 @@ class SoledadBootstrapper(QtCore.QObject): self._user = "" self._password = "" + @property + def keymanager(self): + return self._keymanager + + @property + def soledad(self): + return self._soledad + def _load_and_sync_soledad(self, srp_auth): """ Once everthing is in the right place, we instantiate and sync @@ -92,7 +87,8 @@ class SoledadBootstrapper(QtCore.QObject): local_db_path = "%s/%s.db" % (prefix, uuid) # TODO: use the proper URL - server_url = 'https://mole.dev.bitmask.net:2424/user-%s' % (uuid,) + #server_url = 'https://mole.dev.bitmask.net:2424/user-%s' % (uuid,) + server_url = 'https://gadwall.dev.bitmask.net:1111/user-%s' % (uuid,) # server_url = self._soledad_config.get_hosts(...) cert_file = self._provider_config.get_ca_cert_path() @@ -109,9 +105,6 @@ class SoledadBootstrapper(QtCore.QObject): def _download_config(self): """ Downloads the Soledad config for the given provider - - :return: True if everything went as expected, False otherwise - :rtype: bool """ leap_assert(self._provider_config, @@ -120,150 +113,84 @@ class SoledadBootstrapper(QtCore.QObject): logger.debug("Downloading Soledad config for %s" % (self._provider_config.get_domain(),)) - download_config_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - self._soledad_config = SoledadConfig() - try: - headers = {} - mtime = get_mtime(os.path.join(self._soledad_config - .get_path_prefix(), - "leap", - "providers", - self._provider_config.get_domain(), - "soledad-service.json")) - - if self._download_if_needed and mtime: - headers['if-modified-since'] = mtime - - # there is some confusion with this uri, - config_uri = "%s/%s/config/soledad-service.json" % ( - self._provider_config.get_api_uri(), - self._provider_config.get_api_version()) - logger.debug('Downloading soledad config from: %s' % config_uri) - - srp_auth = SRPAuth(self._provider_config) - session_id = srp_auth.get_session_id() - cookies = None - if session_id: - cookies = {"_session_id": session_id} - - res = self._session.get(config_uri, - verify=self._provider_config - .get_ca_cert_path(), - headers=headers, - cookies=cookies) - res.raise_for_status() - - # Not modified - if res.status_code == 304: - logger.debug("Soledad definition has not been modified") - else: - soledad_definition, mtime = get_content(res) - - self._soledad_config.load(data=soledad_definition, mtime=mtime) - self._soledad_config.save(["leap", - "providers", - self._provider_config.get_domain(), - "soledad-service.json"]) - - self._load_and_sync_soledad(srp_auth) - - download_config_data[self.PASSED_KEY] = True - except Exception as e: - download_config_data[self.PASSED_KEY] = False - download_config_data[self.ERROR_KEY] = "%s" % (e,) - - logger.debug("Emitting download_config %s" % (download_config_data,)) - self.download_config.emit(download_config_data) - - return download_config_data[self.PASSED_KEY] + headers = {} + mtime = get_mtime(os.path.join(self._soledad_config + .get_path_prefix(), + "leap", + "providers", + self._provider_config.get_domain(), + "soledad-service.json")) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + # there is some confusion with this uri, + config_uri = "%s/%s/config/soledad-service.json" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('Downloading soledad config from: %s' % config_uri) + + srp_auth = SRPAuth(self._provider_config) + session_id = srp_auth.get_session_id() + cookies = None + if session_id: + cookies = {"_session_id": session_id} + + res = self._session.get(config_uri, + verify=self._provider_config + .get_ca_cert_path(), + headers=headers, + cookies=cookies) + res.raise_for_status() + + # Not modified + if res.status_code == 304: + logger.debug("Soledad definition has not been modified") + else: + soledad_definition, mtime = get_content(res) + + self._soledad_config.load(data=soledad_definition, mtime=mtime) + self._soledad_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "soledad-service.json"]) + + self._load_and_sync_soledad(srp_auth) def _gen_key(self): """ Generates the key pair if needed, uploads it to the webapp and nickserver - - :return: True if everything is done successfully, False - otherwise - :rtype: bool """ leap_assert(self._provider_config, "We need a provider configuration!") - # XXX Sanitize this address = "%s@%s" % (self._user, self._provider_config.get_domain()) logger.debug("Retrieving key for %s" % (address,)) - genkey_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "", - self.SOLEDAD_KEY: None, - self.KEYMANAGER_KEY: None - } - + srp_auth = SRPAuth(self._provider_config) + self._keymanager = KeyManager( + address, + "https://%s:6425" % (self._provider_config.get_domain()), + self._soledad, + #token=srp_auth.get_token(), # TODO: enable token usage + session_id=srp_auth.get_session_id(), + ca_cert_path=self._provider_config.get_ca_cert_path(), + api_uri=self._provider_config.get_api_uri(), + api_version=self._provider_config.get_api_version(), + uid=srp_auth.get_uid()) try: - srp_auth = SRPAuth(self._provider_config) - self._keymanager = KeyManager( - address, - "https://nickserver", # TODO: nickserver url, none for now - self._soledad, - token=srp_auth.get_token()) - self._keymanager._fetcher.put = Mock() - try: - self._keymanager.get_key(address, openpgp.OpenPGPKey, - private=True, fetch_remote=False) - except KeyNotFound: - logger.debug( - "Key not found. Generating key for %s" % (address,)) - self._keymanager.gen_key(openpgp.OpenPGPKey) - - logger.debug("Key generated successfully.") - - cookies = None - session_id = srp_auth.get_session_id() - if session_id: - cookies = {"_session_id": session_id} - - key_uri = "%s/%s/users/%s.json" % ( - self._provider_config.get_api_uri(), - self._provider_config.get_api_version(), - srp_auth.get_uid()) - - logger.debug("Uploading public key to %s" % (key_uri,)) - - pubkey = self._keymanager.get_key( - address, openpgp.OpenPGPKey, - private=False, fetch_remote=False) - key_data = { - self.PUBKEY_KEY: pubkey.key_data, - } - - # TODO: check if uploaded before uploading it - key_result = self._session.put(key_uri, - data=key_data, - verify=self._provider_config - .get_ca_cert_path(), - cookies=cookies) - key_result.raise_for_status() - genkey_data[self.PASSED_KEY] = True - genkey_data[self.SOLEDAD_KEY] = self._soledad - genkey_data[self.KEYMANAGER_KEY] = self._keymanager - except Exception as e: - genkey_data[self.PASSED_KEY] = False - genkey_data[self.ERROR_KEY] = "%s" % (e,) - - logger.debug("Emitting gen_key %s" % (genkey_data,)) - self.gen_key.emit(genkey_data) - - return genkey_data[self.PASSED_KEY] + self._keymanager.get_key(address, openpgp.OpenPGPKey, + private=True, fetch_remote=False) + except KeyNotFound: + logger.debug("Key not found. Generating key for %s" % (address,)) + self._keymanager.gen_key(openpgp.OpenPGPKey) + logger.debug("Key generated successfully.") def run_soledad_setup_checks(self, - checker, provider_config, user, password, @@ -273,6 +200,10 @@ class SoledadBootstrapper(QtCore.QObject): :param provider_config: Provider configuration :type provider_config: ProviderConfig + :param user: User's login + :type user: str + :param password: User's password + :type password: str """ leap_assert_type(provider_config, ProviderConfig) @@ -281,7 +212,9 @@ class SoledadBootstrapper(QtCore.QObject): self._user = user self._password = password - checker.add_checks([ - self._download_config, - self._gen_key - ]) + cb_chain = [ + (self._download_config, self.download_config), + (self._gen_key, self.gen_key) + ] + + self.addCallbackChain(cb_chain) -- cgit v1.2.3 From 655cec1fec89eb30fc17bdc0a5f527e5a91ba5b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 4 Jun 2013 12:56:17 -0300 Subject: Remove CheckerThread from SRPAuth Also, some pep8 fixes --- src/leap/services/abstractbootstrapper.py | 1 - src/leap/services/eip/eipbootstrapper.py | 3 ++- src/leap/services/mail/smtpbootstrapper.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/abstractbootstrapper.py b/src/leap/services/abstractbootstrapper.py index bce03e6b..7bebdc15 100644 --- a/src/leap/services/abstractbootstrapper.py +++ b/src/leap/services/abstractbootstrapper.py @@ -152,4 +152,3 @@ class AbstractBootstrapper(QtCore.QObject): d.addErrback(self._errback, signal=sig) d.addCallback(self._gui_notify, signal=sig) d.addErrback(self._gui_errback) - diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 7216bb80..51c3dab4 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -173,7 +173,8 @@ class EIPBootstrapper(AbstractBootstrapper): cb_chain = [ (self._download_config, self.download_config), - (self._download_client_certificates, self.download_client_certificate) + (self._download_client_certificates, \ + self.download_client_certificate) ] self.addCallbackChain(cb_chain) diff --git a/src/leap/services/mail/smtpbootstrapper.py b/src/leap/services/mail/smtpbootstrapper.py index 64bf3153..ea480c6d 100644 --- a/src/leap/services/mail/smtpbootstrapper.py +++ b/src/leap/services/mail/smtpbootstrapper.py @@ -96,7 +96,8 @@ class SMTPBootstrapper(AbstractBootstrapper): logger.debug("SMTP definition has not been modified") self._smtp_config.load(os.path.join("leap", "providers", - self._provider_config.get_domain(), + self._provider_config.\ + get_domain(), "smtp-service.json")) else: smtp_definition, mtime = get_content(res) -- cgit v1.2.3 From 364d31999dbc488b5f99d81a0480c67ef248a515 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Mon, 10 Jun 2013 15:39:20 -0300 Subject: Check provider api version for compatibility --- src/leap/services/eip/providerbootstrapper.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 1339e086..e099eee7 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -32,10 +32,19 @@ from leap.common.check import leap_assert, leap_assert_type from leap.config.providerconfig import ProviderConfig from leap.util.request_helpers import get_content from leap.services.abstractbootstrapper import AbstractBootstrapper +from leap.provider.supportedapis import SupportedAPIs + logger = logging.getLogger(__name__) +class UnsupportedProviderAPI(Exception): + """ + Raised when attempting to use a provider with an incompatible API. + """ + pass + + class ProviderBootstrapper(AbstractBootstrapper): """ Given a provider URL performs a series of checks and emits signals @@ -142,6 +151,18 @@ class ProviderBootstrapper(AbstractBootstrapper): self._domain, "provider.json"]) + api_version = provider_config.get_api_version() + if SupportedAPIs.supports(api_version): + logger.debug("Provider definition has been modified") + else: + api_supported = ', '.join(self._supported_api_versions) + error = ('Unsupported provider API version. ' + 'Supported versions are: {}. ' + 'Found: {}.').format(api_supported, api_version) + + logger.error(error) + raise UnsupportedProviderAPI(error) + def run_provider_select_checks(self, domain, download_if_needed=False): """ Populates the check queue. -- cgit v1.2.3 From dbb873016042b213dd9cd84a59aec0c0a2383691 Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 5 Jun 2013 05:18:39 +0900 Subject: use twisted processProtocol instead of QProcess to drive openvpn --- src/leap/services/eip/vpn.py | 465 ------------------------ src/leap/services/eip/vpnlaunchers.py | 1 + src/leap/services/eip/vpnprocess.py | 592 +++++++++++++++++++++++++++++++ src/leap/services/mail/smtpspec.py | 4 +- src/leap/services/soledad/soledadspec.py | 4 +- 5 files changed, 597 insertions(+), 469 deletions(-) delete mode 100644 src/leap/services/eip/vpn.py create mode 100644 src/leap/services/eip/vpnprocess.py (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py deleted file mode 100644 index af1febe6..00000000 --- a/src/leap/services/eip/vpn.py +++ /dev/null @@ -1,465 +0,0 @@ -# -*- coding: utf-8 -*- -# vpn.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -VPN launcher and watcher thread -""" - -import logging -import sys -import psutil - -from PySide import QtCore, QtGui -from functools import partial - -from leap.common.check import leap_assert, leap_assert_type -from leap.config.providerconfig import ProviderConfig -from leap.services.eip.vpnlaunchers import get_platform_launcher -from leap.services.eip.eipconfig import EIPConfig -from leap.services.eip.udstelnet import UDSTelnet - -logger = logging.getLogger(__name__) - - -# TODO: abstract the thread that can be asked to quit to another -# generic class that Fetcher and VPN inherit from -class VPN(QtCore.QThread): - """ - VPN launcher and watcher thread. It will emit signals based on - different events caught by the management interface - """ - - state_changed = QtCore.Signal(dict) - status_changed = QtCore.Signal(dict) - - process_finished = QtCore.Signal(int) - - CONNECTION_RETRY_TIME = 1000 - POLL_TIME = 100 - - TS_KEY = "ts" - STATUS_STEP_KEY = "status_step" - OK_KEY = "ok" - IP_KEY = "ip" - REMOTE_KEY = "remote" - - TUNTAP_READ_KEY = "tun_tap_read" - TUNTAP_WRITE_KEY = "tun_tap_write" - TCPUDP_READ_KEY = "tcp_udp_read" - TCPUDP_WRITE_KEY = "tcp_udp_write" - AUTH_READ_KEY = "auth_read" - - ALREADY_RUNNING_STEP = "ALREADYRUNNING" - - def __init__(self): - QtCore.QThread.__init__(self) - - self._should_quit = False - self._should_quit_lock = QtCore.QMutex() - - self._launcher = get_platform_launcher() - self._subp = None - - self._tn = None - self._host = None - self._port = None - - self._last_state = None - self._last_status = None - - def get_should_quit(self): - """ - Returns wether this thread should quit - - :rtype: bool - :return: True if the thread should terminate itself, Flase otherwise - """ - QtCore.QMutexLocker(self._should_quit_lock) - return self._should_quit - - def set_should_quit(self): - """ - Sets the should_quit flag to True so that this thread - terminates the first chance it gets. - Also terminates the VPN process and the connection to it - """ - QtCore.QMutexLocker(self._should_quit_lock) - self._should_quit = True - if self._tn is None or self._subp is None: - return - - try: - self._send_command("signal SIGTERM") - self._tn.close() - self._subp.terminate() - self._subp.waitForFinished() - except Exception as e: - logger.debug("Could not terminate process, trying command " + - "signal SIGNINT: %r" % (e,)) - finally: - self._tn = None - - def start(self, eipconfig, providerconfig, socket_host, socket_port): - """ - Launches OpenVPN and starts the thread to watch its output - - :param eipconfig: eip configuration object - :type eipconfig: EIPConfig - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - :param socket_host: either socket path (unix) or socket IP - :type socket_host: str - :param socket_port: either string "unix" if it's a unix - socket, or port otherwise - :type socket_port: str - """ - leap_assert(eipconfig, "We need an eip config") - leap_assert_type(eipconfig, EIPConfig) - leap_assert(providerconfig, "We need a provider config") - leap_assert_type(providerconfig, ProviderConfig) - leap_assert(not self.isRunning(), "Starting process more than once!") - - logger.debug("Starting VPN...") - - with QtCore.QMutexLocker(self._should_quit_lock): - self._should_quit = False - - if not self._stop_if_already_running(): - # We send a fake state - state_dict = { - self.TS_KEY: "", - self.STATUS_STEP_KEY: self.ALREADY_RUNNING_STEP, - self.OK_KEY: "", - self.IP_KEY: "", - self.REMOTE_KEY: "" - } - - self.state_changed.emit(state_dict) - # And just return, don't start the process - return - - command = self._launcher.get_vpn_command(eipconfig=eipconfig, - providerconfig=providerconfig, - socket_host=socket_host, - socket_port=socket_port) - try: - env = QtCore.QProcessEnvironment.systemEnvironment() - for key, val in self._launcher.get_vpn_env(providerconfig).items(): - env.insert(key, val) - - self._subp = QtCore.QProcess() - - self._subp.setProcessEnvironment(env) - - self._subp.finished.connect(self.process_finished) - self._subp.finished.connect(self._dump_exitinfo) - self._subp.start(command[:1][0], command[1:]) - logger.debug("Waiting for started...") - self._subp.waitForStarted() - logger.debug("Started!") - - self._host = socket_host - self._port = socket_port - - self._started = True - - QtCore.QThread.start(self) - except Exception as e: - logger.warning("Something went wrong while starting OpenVPN: %r" % - (e,)) - - def _dump_exitinfo(self): - """ - SLOT - TRIGGER: self._subp.finished - - Prints debug info when quitting the process - """ - logger.debug("stdout: %s", self._subp.readAllStandardOutput()) - logger.debug("stderr: %s", self._subp.readAllStandardError()) - - def _get_openvpn_process(self): - """ - Looks for openvpn instances running - - :rtype: process - """ - openvpn_process = None - for p in psutil.process_iter(): - try: - # XXX Not exact! - # Will give false positives. - # we should check that cmdline BEGINS - # with openvpn or with our wrapper - # (pkexec / osascript / whatever) - if self._launcher.OPENVPN_BIN in ' '.join(p.cmdline): - openvpn_process = p - break - except psutil.error.AccessDenied: - pass - return openvpn_process - - def _stop_if_already_running(self): - """ - Checks if VPN is already running and tries to stop it - - :return: True if stopped, False otherwise - """ - - process = self._get_openvpn_process() - if process: - logger.debug("OpenVPN is already running, trying to stop it") - cmdline = process.cmdline - - manag_flag = "--management" - if isinstance(cmdline, list) and manag_flag in cmdline: - try: - index = cmdline.index(manag_flag) - host = cmdline[index + 1] - port = cmdline[index + 2] - logger.debug("Trying to connect to %s:%s" - % (host, port)) - self._connect(host, port) - self._send_command("signal SIGTERM") - self._tn.close() - self._tn = None - except Exception as e: - logger.warning("Problem trying to terminate OpenVPN: %r" - % (e,)) - - process = self._get_openvpn_process() - if process is None: - logger.warning("Unabled to terminate OpenVPN") - return True - else: - return False - - return True - - def _connect(self, socket_host, socket_port): - """ - Connects to the specified socket_host socket_port - :param socket_host: either socket path (unix) or socket IP - :type socket_host: str - :param socket_port: either string "unix" if it's a unix - socket, or port otherwise - :type socket_port: str - """ - try: - self._tn = UDSTelnet(socket_host, socket_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._tn.read_eager() - except Exception as e: - logger.warning("Could not connect to OpenVPN yet: %r" % (e,)) - self._tn = None - - def _disconnect(self): - """ - Disconnects the telnet connection to the openvpn process - """ - logger.debug('Closing socket') - self._tn.write("quit\n") - self._tn.read_all() - self._tn.close() - self._tn = None - - def _send_command(self, command, until=b"END"): - """ - Sends a command to the telnet connection and reads until END - is reached - - :param command: command to send - :type command: str - :param until: byte delimiter string for reading command output - :type until: byte str - :return: response read - :rtype: list - """ - leap_assert(self._tn, "We need a tn connection!") - try: - self._tn.write("%s\n" % (command,)) - buf = self._tn.read_until(until, 2) - self._tn.read_eager() - lines = buf.split("\n") - return lines - except Exception as e: - logger.warning("Error sending command %s: %r" % - (command, e)) - return [] - - def _parse_state_and_notify(self, output): - """ - Parses the output of the state command and emits state_changed - signal when the state changes - - :param output: list of lines that the state command printed as - its output - :type output: list - """ - for line in output: - stripped = line.strip() - if stripped == "END": - continue - parts = stripped.split(",") - if len(parts) < 5: - continue - ts, status_step, ok, ip, remote = parts - - state_dict = { - self.TS_KEY: ts, - self.STATUS_STEP_KEY: status_step, - self.OK_KEY: ok, - self.IP_KEY: ip, - self.REMOTE_KEY: remote - } - - if state_dict != self._last_state: - self.state_changed.emit(state_dict) - self._last_state = state_dict - - def _parse_status_and_notify(self, output): - """ - Parses the output of the status command and emits - status_changed signal when the status changes - - :param output: list of lines that the status command printed - as its output - :type output: list - """ - tun_tap_read = "" - tun_tap_write = "" - tcp_udp_read = "" - tcp_udp_write = "" - auth_read = "" - for line in output: - stripped = line.strip() - if stripped.endswith("STATISTICS") or stripped == "END": - continue - parts = stripped.split(",") - if len(parts) < 2: - continue - if parts[0].strip() == "TUN/TAP read bytes": - tun_tap_read = parts[1] - elif parts[0].strip() == "TUN/TAP write bytes": - tun_tap_write = parts[1] - elif parts[0].strip() == "TCP/UDP read bytes": - tcp_udp_read = parts[1] - elif parts[0].strip() == "TCP/UDP write bytes": - tcp_udp_write = parts[1] - elif parts[0].strip() == "Auth read bytes": - auth_read = parts[1] - - status_dict = { - self.TUNTAP_READ_KEY: tun_tap_read, - self.TUNTAP_WRITE_KEY: tun_tap_write, - self.TCPUDP_READ_KEY: tcp_udp_read, - self.TCPUDP_WRITE_KEY: tcp_udp_write, - self.AUTH_READ_KEY: auth_read - } - - if status_dict != self._last_status: - self.status_changed.emit(status_dict) - self._last_status = status_dict - - def run(self): - """ - Main run loop for this thread - """ - while True: - if self.get_should_quit(): - logger.debug("Quitting VPN thread") - return - - if self._subp and self._subp.state() != QtCore.QProcess.Running: - QtCore.QThread.msleep(self.CONNECTION_RETRY_TIME) - - if self._tn is None: - self._connect(self._host, self._port) - QtCore.QThread.msleep(self.CONNECTION_RETRY_TIME) - else: - self._parse_state_and_notify(self._send_command("state")) - self._parse_status_and_notify(self._send_command("status")) - output_sofar = self._subp.readAllStandardOutput() - if len(output_sofar) > 0: - logger.debug(output_sofar) - output_sofar = self._subp.readAllStandardError() - if len(output_sofar) > 0: - logger.debug(output_sofar) - QtCore.QThread.msleep(self.POLL_TIME) - - -if __name__ == "__main__": - import os - import signal - - app = QtGui.QApplication(sys.argv) - - def sigint_handler(*args, **kwargs): - logger.debug('SIGINT catched. shutting down...') - vpn_thread = args[0] - vpn_thread.set_should_quit() - QtGui.QApplication.quit() - - def signal_tester(d): - print d - - logger = logging.getLogger(name='leap') - logger.setLevel(logging.DEBUG) - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - vpn_thread = VPN() - - sigint = partial(sigint_handler, vpn_thread) - signal.signal(signal.SIGINT, sigint) - - eipconfig = EIPConfig() - if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): - provider = ProviderConfig() - if provider.load("leap/providers/bitmask.net/provider.json"): - vpn_thread.start(eipconfig=eipconfig, - providerconfig=provider, - socket_host=os.path.expanduser("~/vpnsock"), - socket_port="unix") - - timer = QtCore.QTimer() - timer.start(500) - timer.timeout.connect(lambda: None) - app.connect(app, QtCore.SIGNAL("aboutToQuit()"), - vpn_thread.set_should_quit) - w = QtGui.QWidget() - w.resize(100, 100) - w.show() - - vpn_thread.state_changed.connect(signal_tester) - vpn_thread.status_changed.connect(signal_tester) - - sys.exit(app.exec_()) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 0691e121..952d3618 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -132,6 +132,7 @@ def _is_auth_agent_running(): """ polkit_gnome = 'ps aux | grep polkit-[g]nome-authentication-agent-1' polkit_kde = 'ps aux | grep polkit-[k]de-authentication-agent-1' + return (len(commands.getoutput(polkit_gnome)) > 0 or len(commands.getoutput(polkit_kde)) > 0) diff --git a/src/leap/services/eip/vpnprocess.py b/src/leap/services/eip/vpnprocess.py new file mode 100644 index 00000000..eae8aadd --- /dev/null +++ b/src/leap/services/eip/vpnprocess.py @@ -0,0 +1,592 @@ +# -*- coding: utf-8 -*- +# vpnprocess.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +VPN Manager, spawned in a custom processProtocol. +""" +import logging +import os +import psutil + +from PySide import QtCore + +from leap.common.check import leap_assert, leap_assert_type +from leap.config.providerconfig import ProviderConfig +from leap.services.eip.vpnlaunchers import get_platform_launcher +from leap.services.eip.eipconfig import EIPConfig +from leap.services.eip.udstelnet import UDSTelnet + +logger = logging.getLogger(__name__) +vpnlog = logging.getLogger('leap.openvpn') + +from twisted.internet import protocol +from twisted.internet import defer +from twisted.internet.task import LoopingCall +from twisted.internet import error as internet_error + + +class VPNSignals(QtCore.QObject): + """ + These are the signals that we use to let the UI know + about the events we are polling. + They are instantiated in the VPN object and passed along + till the VPNProcess. + """ + state_changed = QtCore.Signal(dict) + status_changed = QtCore.Signal(dict) + process_finished = QtCore.Signal(int) + + def __init__(self): + QtCore.QObject.__init__(self) + + +class VPN(object): + """ + This is the high-level object that the GUI is dealing with. + It exposes the start and terminate methods. + + On start, it spawns a VPNProcess instance that will use a vpnlauncher + suited for the running platform and connect to the management interface + opened by the openvpn process, executing commands over that interface on + demand. + """ + def __init__(self): + """ + Instantiate empty attributes and get a copy + of a QObject containing the QSignals that we will pass along + to the VPNManager. + """ + from twisted.internet import reactor + self._vpnproc = None + self._pollers = [] + self._reactor = reactor + self._qtsigs = VPNSignals() + + @property + def qtsigs(self): + return self._qtsigs + + def start(self, *args, **kwargs): + """ + Starts the openvpn subprocess. + + :param args: args to be passed to the VPNProcess + :type args: tuple + + :param kwargs: kwargs to be passed to the VPNProcess + :type kwargs: dict + """ + kwargs['qtsigs'] = self.qtsigs + + # start the main vpn subprocess + vpnproc = VPNProcess(*args, **kwargs) + + cmd = vpnproc.getCommand() + env = os.environ + for key, val in vpnproc.vpn_env.items(): + env[key] = val + + self._reactor.spawnProcess(vpnproc, cmd[0], cmd, env) + self._vpnproc = vpnproc + + # add pollers for status and state + # XXX this could be extended to a collection of + # generic watchers + + poll_list = [LoopingCall(vpnproc.pollStatus), + LoopingCall(vpnproc.pollState)] + self._pollers.extend(poll_list) + self._start_pollers() + + def terminate(self): + """ + Stops the openvpn subprocess. + """ + self._stop_pollers() + # XXX we should leave a KILL as a last resort. + # First we should try to send a SIGTERM + if self._vpnproc: + self._vpnproc.killProcess() + + def _start_pollers(self): + """ + Iterate through the registered observers + and start the looping call for them. + """ + for poller in self._pollers: + poller.start(VPNManager.POLL_TIME) + + def _stop_pollers(self): + """ + Iterate through the registered observers + and stop the looping calls if they are running. + """ + for poller in self._pollers: + if poller.running: + poller.stop() + self._pollers = [] + + +class VPNManager(object): + """ + This is a mixin that we use in the VPNProcess class. + Here we get together all methods related with the openvpn management + interface. + + A copy of a QObject containing signals as attributes is passed along + upon initialization, and we use that object to emit signals to qt-land. + """ + + # Timers, in secs + POLL_TIME = 0.5 + CONNECTION_RETRY_TIME = 1 + + TS_KEY = "ts" + STATUS_STEP_KEY = "status_step" + OK_KEY = "ok" + IP_KEY = "ip" + REMOTE_KEY = "remote" + + TUNTAP_READ_KEY = "tun_tap_read" + TUNTAP_WRITE_KEY = "tun_tap_write" + TCPUDP_READ_KEY = "tcp_udp_read" + TCPUDP_WRITE_KEY = "tcp_udp_write" + AUTH_READ_KEY = "auth_read" + + def __init__(self, qtsigs=None): + """ + Initializes the VPNManager. + + :param qtsigs: a QObject containing the Qt signals used by the UI + to give feedback about state changes. + :type qtsigs: QObject + """ + from twisted.internet import reactor + self._reactor = reactor + self._tn = None + self._qtsigs = qtsigs + + @property + def qtsigs(self): + return self._qtsigs + + def _disconnect(self): + """ + Disconnects the telnet connection to the openvpn process. + """ + logger.debug('Closing socket') + self._tn.write("quit\n") + self._tn.read_all() + self._tn.close() + self._tn = None + + def _send_command(self, command, until=b"END"): + """ + Sends a command to the telnet connection and reads until END + is reached. + + :param command: command to send + :type command: str + + :param until: byte delimiter string for reading command output + :type until: byte str + + :return: response read + :rtype: list + """ + leap_assert(self._tn, "We need a tn connection!") + try: + self._tn.write("%s\n" % (command,)) + buf = self._tn.read_until(until, 2) + self._tn.read_eager() + lines = buf.split("\n") + return lines + + # XXX should move this to a errBack! + except Exception as e: + logger.warning("Error sending command %s: %r" % + (command, e)) + return [] + + def _connect(self, socket_host, socket_port): + """ + Connects to the specified socket_host socket_port. + + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + + :param socket_port: either string "unix" if it's a unix + socket, or port otherwise + :type socket_port: str + """ + try: + self._tn = UDSTelnet(socket_host, socket_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._tn.read_eager() + + # XXX move this to the Errback + except Exception as e: + logger.warning("Could not connect to OpenVPN yet: %r" % (e,)) + self._tn = None + + def _connectCb(self, *args): + """ + Callback for connection. + + :param args: not used + """ + if self._tn: + logger.info('connected to management') + + def _connectErr(self, failure): + """ + Errorback for connection. + + :param failure: Failure + """ + logger.warning(failure) + + def connect(self, host, port): + """ + Connect to a management interface. + + :param host: the host of the management interface + :type host: str + + :param port: the port of the management interface + :type port: str + + :returns: a deferred + """ + self.connectd = defer.maybeDeferred(self._connect, host, port) + self.connectd.addCallbacks(self._connectCb, self._connectErr) + return self.connectd + + def is_connected(self): + """ + Returns the status of the management interface. + + :returns: True if connected, False otherwise + :rtype: bool + """ + return True if self._tn else False + + def try_to_connect(self, retry=0): + """ + Attempts to connect to a management interface, and retries + after CONNECTION_RETRY_TIME if not successful. + + :param retry: number of the retry + :type retry: int + """ + # TODO decide about putting a max_lim to retries and signaling + # an error. + if not self.is_connected(): + self.connect(self._socket_host, self._socket_port) + self._reactor.callLater( + self.CONNECTION_RETRY_TIME, self.try_to_connect, retry + 1) + + def _parse_state_and_notify(self, output): + """ + Parses the output of the state command and emits state_changed + signal when the state changes. + + :param output: list of lines that the state command printed as + its output + :type output: list + """ + for line in output: + stripped = line.strip() + if stripped == "END": + continue + parts = stripped.split(",") + if len(parts) < 5: + continue + ts, status_step, ok, ip, remote = parts + + state_dict = { + self.TS_KEY: ts, + self.STATUS_STEP_KEY: status_step, + self.OK_KEY: ok, + self.IP_KEY: ip, + self.REMOTE_KEY: remote + } + + if state_dict != self._last_state: + self.qtsigs.state_changed.emit(state_dict) + self._last_state = state_dict + + def _parse_status_and_notify(self, output): + """ + Parses the output of the status command and emits + status_changed signal when the status changes. + + :param output: list of lines that the status command printed + as its output + :type output: list + """ + tun_tap_read = "" + tun_tap_write = "" + tcp_udp_read = "" + tcp_udp_write = "" + auth_read = "" + for line in output: + stripped = line.strip() + if stripped.endswith("STATISTICS") or stripped == "END": + continue + parts = stripped.split(",") + if len(parts) < 2: + continue + if parts[0].strip() == "TUN/TAP read bytes": + tun_tap_read = parts[1] + elif parts[0].strip() == "TUN/TAP write bytes": + tun_tap_write = parts[1] + elif parts[0].strip() == "TCP/UDP read bytes": + tcp_udp_read = parts[1] + elif parts[0].strip() == "TCP/UDP write bytes": + tcp_udp_write = parts[1] + elif parts[0].strip() == "Auth read bytes": + auth_read = parts[1] + + status_dict = { + self.TUNTAP_READ_KEY: tun_tap_read, + self.TUNTAP_WRITE_KEY: tun_tap_write, + self.TCPUDP_READ_KEY: tcp_udp_read, + self.TCPUDP_WRITE_KEY: tcp_udp_write, + self.AUTH_READ_KEY: auth_read + } + + if status_dict != self._last_status: + self.qtsigs.status_changed.emit(status_dict) + self._last_status = status_dict + + def get_state(self): + """ + Notifies the gui of the output of the state command over + the openvpn management interface. + """ + if self.is_connected(): + return self._parse_state_and_notify(self._send_command("state")) + + def get_status(self): + """ + Notifies the gui of the output of the status command over + the openvpn management interface. + """ + if self.is_connected(): + return self._parse_status_and_notify(self._send_command("status")) + + @property + def vpn_env(self): + """ + Return a dict containing the vpn environment to be used. + """ + return self._launcher.get_vpn_env(self._providerconfig) + + # XXX old methods, not adapted to twisted process yet + + def _get_openvpn_process(self): + """ + Looks for openvpn instances running. + + :rtype: process + """ + openvpn_process = None + for p in psutil.process_iter(): + try: + # XXX Not exact! + # Will give false positives. + # we should check that cmdline BEGINS + # with openvpn or with our wrapper + # (pkexec / osascript / whatever) + if self._launcher.OPENVPN_BIN in ' '.join(p.cmdline): + openvpn_process = p + break + except psutil.error.AccessDenied: + pass + return openvpn_process + + def _stop_if_already_running(self): + """ + Checks if VPN is already running and tries to stop it. + + :return: True if stopped, False otherwise + """ + + process = self._get_openvpn_process() + if process: + logger.debug("OpenVPN is already running, trying to stop it") + cmdline = process.cmdline + + manag_flag = "--management" + if isinstance(cmdline, list) and manag_flag in cmdline: + try: + index = cmdline.index(manag_flag) + host = cmdline[index + 1] + port = cmdline[index + 2] + logger.debug("Trying to connect to %s:%s" + % (host, port)) + self._connect(host, port) + self._send_command("signal SIGTERM") + self._tn.close() + self._tn = None + #self._disconnect() + except Exception as e: + logger.warning("Problem trying to terminate OpenVPN: %r" + % (e,)) + + process = self._get_openvpn_process() + if process is None: + logger.warning("Unabled to terminate OpenVPN") + return True + else: + return False + return True + + +class VPNProcess(protocol.ProcessProtocol, VPNManager): + """ + A ProcessProtocol class that can be used to spawn a process that will + launch openvpn and connect to its management interface to control it + programmatically. + """ + + def __init__(self, eipconfig, providerconfig, socket_host, socket_port, + qtsigs): + """ + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + + :param socket_port: either string "unix" if it's a unix + socket, or port otherwise + :type socket_port: str + + :param qtsigs: a QObject containing the Qt signals used to notify the + UI. + :type qtsigs: QObject + """ + VPNManager.__init__(self, qtsigs=qtsigs) + leap_assert_type(eipconfig, EIPConfig) + leap_assert_type(providerconfig, ProviderConfig) + leap_assert_type(qtsigs, QtCore.QObject) + + #leap_assert(not self.isRunning(), "Starting process more than once!") + + self._eipconfig = eipconfig + self._providerconfig = providerconfig + self._socket_host = socket_host + self._socket_port = socket_port + + self._launcher = get_platform_launcher() + + self._last_state = None + self._last_status = None + + # processProtocol methods + + def connectionMade(self): + """ + Called when the connection is made. + + .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa + """ + self.try_to_connect() + + def outReceived(self, data): + """ + Called when new data is available on stdout. + + :param data: the data read on stdout + + .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa + """ + # truncate the newline + # should send this to the logging window + vpnlog.info(data[:-1]) + + def processExited(self, reason): + """ + Called when the child process exits. + + .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa + """ + exit_code = reason.value.exitCode + if isinstance(exit_code, int): + logger.debug("processExited, status %d" % (exit_code,)) + + def processEnded(self, reason): + """ + Called when the child process exits and all file descriptors associated + with it have been closed. + + .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa + """ + exit_code = reason.value.exitCode + if isinstance(exit_code, int): + logger.debug("processEnded, status %d" % (exit_code,)) + + # polling + + def pollStatus(self): + """ + Polls connection status. + """ + self.get_status() + + def pollState(self): + """ + Polls connection state. + """ + self.get_state() + + # launcher + + def getCommand(self): + """ + Gets the vpn command from the aproppriate launcher. + """ + cmd = self._launcher.get_vpn_command( + eipconfig=self._eipconfig, + providerconfig=self._providerconfig, + socket_host=self._socket_host, + socket_port=self._socket_port) + return map(str, cmd) + + # shutdown + + def killProcess(self): + """ + Sends the KILL signal to the running process. + """ + try: + self.transport.signalProcess('KILL') + except internet_error.ProcessExitedAlready: + logger.debug('Process Exited Already') diff --git a/src/leap/services/mail/smtpspec.py b/src/leap/services/mail/smtpspec.py index b455b196..270dfb76 100644 --- a/src/leap/services/mail/smtpspec.py +++ b/src/leap/services/mail/smtpspec.py @@ -22,12 +22,12 @@ smtp_config_spec = { 'serial': { 'type': int, 'default': 1, - 'required': True + 'required': ["True"] }, 'version': { 'type': int, 'default': 1, - 'required': True + 'required': ["True"] }, 'hosts': { 'type': dict, diff --git a/src/leap/services/soledad/soledadspec.py b/src/leap/services/soledad/soledadspec.py index d5a437cc..8233d6a0 100644 --- a/src/leap/services/soledad/soledadspec.py +++ b/src/leap/services/soledad/soledadspec.py @@ -22,12 +22,12 @@ soledad_config_spec = { 'serial': { 'type': int, 'default': 1, - 'required': True + 'required': ["True"] }, 'version': { 'type': int, 'default': 1, - 'required': True + 'required': ["True"] }, 'hosts': { 'type': dict, -- cgit v1.2.3 From ba27c14ba84c6869c187bdd09138bfae4424445d Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 13 Jun 2013 01:19:49 +0900 Subject: copy missing updown scripts if missing --- src/leap/services/eip/vpnlaunchers.py | 96 ++++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 23 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 952d3618..d15aed82 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -29,6 +29,7 @@ except ImportError: pass # ignore, probably windows from abc import ABCMeta, abstractmethod +from functools import partial from leap.common.check import leap_assert, leap_assert_type from leap.common.files import which @@ -105,22 +106,37 @@ def get_platform_launcher(): def _is_pkexec_in_system(): + """ + Checks the existence of the pkexec binary in system. + """ pkexec_path = which('pkexec') if len(pkexec_path) == 0: return False return True -def _has_updown_scripts(path): +def _has_updown_scripts(path, warn=True): """ - Checks the existence of the up/down scripts + Checks the existence of the up/down scripts. + + :param path: the path to be checked + :type path: str + + :param warn: whether we should log the absence + :type warn: bool + + :rtype: bool """ - # XXX should check permissions too is_file = os.path.isfile(path) - if not is_file: - logger.error("Could not find up/down scripts. " + - "Might produce DNS leaks.") - return is_file + if warn and not is_file: + logger.error("Could not find up/down script %s. " + "Might produce DNS leaks." % (path,)) + + is_exe = os.access(path, os.X_OK) + if warn and not is_exe: + logger.error("Up/down script %s is not executable. " + "Might produce DNS leaks." % (path,)) + return is_file and is_exe def _is_auth_agent_running(): @@ -229,7 +245,6 @@ class LinuxVPNLauncher(VPNLauncher): openvpn_configuration = eipconfig.get_openvpn_configuration() - # FIXME: sanitize this! -- for key, value in openvpn_configuration.items(): args += ['--%s' % (key,), value] @@ -294,15 +309,40 @@ class DarwinVPNLauncher(VPNLauncher): OSASCRIPT_BIN = '/usr/bin/osascript' OSX_ASADMIN = "do shell script \"%s\" with administrator privileges" - OPENVPN_BIN = 'openvpn.leap' - INSTALL_PATH = "/Applications/LEAPClient.app/" + + INSTALL_PATH = "/Applications/LEAP\ Client.app" # OPENVPN_BIN = "/%s/Contents/Resources/openvpn.leap" % ( # self.INSTALL_PATH,) - UP_SCRIPT = "/%s/client.up.sh" % (INSTALL_PATH,) - DOWN_SCRIPT = "/%s/client.down.sh" % (INSTALL_PATH,) + OPENVPN_BIN = 'openvpn.leap' + OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,) + + UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,) + DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,) + OPENVPN_DOWN_PLUGIN = '%s/openvpn-down-root.so' % (OPENVPN_PATH,) + + UPDOWN_FILES = (UP_SCRIPT, DOWN_SCRIPT, OPENVPN_DOWN_PLUGIN) - # TODO: Add - # OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so" + @classmethod + def missing_updown_scripts(kls): + """ + Returns what updown scripts are missing. + :rtype: list + """ + file_exist = partial(_has_updown_scripts, warn=False) + zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) + missing = filter(lambda (path, exists): exists is False, zipped) + return [path for path, exists in missing] + + @classmethod + def cmd_for_missing_scripts(kls, frompath): + """ + Returns a command that can copy the missing scripts. + :rtype: str + """ + to = kls.OPENVPN_PATH + cmd = "#!/bin/sh\nmkdir -p %s\ncp \"%s/\"* %s" % (to, frompath, to) + #return kls.OSX_ASADMIN % cmd + return cmd def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="unix"): @@ -365,20 +405,19 @@ class DarwinVPNLauncher(VPNLauncher): 'server' ] - # FIXME: sanitize this! -- - openvpn_configuration = eipconfig.get_openvpn_configuration() for key, value in openvpn_configuration.items(): args += ['--%s' % (key,), value] + user = getpass.getuser() args += [ - '--user', getpass.getuser(), + '--user', user, '--group', grp.getgrgid(os.getgroups()[-1]).gr_name ] if socket_port == "unix": args += [ - '--management-client-user', getpass.getuser() + '--management-client-user', user ] args += [ @@ -391,13 +430,21 @@ class DarwinVPNLauncher(VPNLauncher): args += [ '--up', self.UP_SCRIPT, ] + if _has_updown_scripts(self.DOWN_SCRIPT): args += [ - '--down', self.DOWN_SCRIPT, - # FIXME add down-plugin - # '--plugin', self.OPENVPN_DOWN_ROOT, - # '\'script_type=down %s\'' % self.DOWN_SCRIPT - ] + '--down', self.DOWN_SCRIPT] + + # should have the down script too + if _has_updown_scripts(self.OPENVPN_DOWN_PLUGIN): + args += [ + '--plugin', self.OPENVPN_DOWN_PLUGIN, + '\'%s\'' % self.DOWN_SCRIPT + ] + + # we set user to be passed to the up/down scripts + args += [ + '--setenv', "LEAPUSER", "%s" % (user,)] args += [ '--cert', eipconfig.get_client_cert_path(providerconfig), @@ -405,6 +452,9 @@ class DarwinVPNLauncher(VPNLauncher): '--ca', providerconfig.get_ca_cert_path() ] + # We are using osascript until we can write a proper wrapper + # for privilege escalation. + command = self.OSASCRIPT_BIN cmd_args = ["-e", self.OSX_ASADMIN % (' '.join(args),)] -- cgit v1.2.3 From cd11784b8fdf0cb45783e8d6a8e9b5288f34820d Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 13 Jun 2013 22:48:29 +0900 Subject: pep8 --- src/leap/services/abstractbootstrapper.py | 18 +++++++++--------- src/leap/services/eip/eipbootstrapper.py | 4 ++-- src/leap/services/eip/vpnlaunchers.py | 2 +- src/leap/services/mail/smtpbootstrapper.py | 9 ++++----- 4 files changed, 16 insertions(+), 17 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/abstractbootstrapper.py b/src/leap/services/abstractbootstrapper.py index 7bebdc15..2cbd56bc 100644 --- a/src/leap/services/abstractbootstrapper.py +++ b/src/leap/services/abstractbootstrapper.py @@ -48,14 +48,14 @@ class AbstractBootstrapper(QtCore.QObject): """ QtCore.QObject.__init__(self) - leap_assert(self._gui_errback.im_func == \ - AbstractBootstrapper._gui_errback.im_func, + leap_assert(self._gui_errback.im_func == + AbstractBootstrapper._gui_errback.im_func, "Cannot redefine _gui_errback") - leap_assert(self._errback.im_func == \ - AbstractBootstrapper._errback.im_func, + leap_assert(self._errback.im_func == + AbstractBootstrapper._errback.im_func, "Cannot redefine _errback") - leap_assert(self._gui_notify.im_func == \ - AbstractBootstrapper._gui_notify.im_func, + leap_assert(self._gui_notify.im_func == + AbstractBootstrapper._gui_notify.im_func, "Cannot redefine _gui_notify") # **************************************************** # @@ -87,9 +87,9 @@ class AbstractBootstrapper(QtCore.QObject): if self._err_msg is not None \ else str(failure.value) self._signal_to_emit.emit({ - self.PASSED_KEY: False, - self.ERROR_KEY: err_msg - }) + self.PASSED_KEY: False, + self.ERROR_KEY: err_msg + }) failure.trap(Exception) def _errback(self, failure, signal=None): diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 51c3dab4..4da8f90f 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -173,8 +173,8 @@ class EIPBootstrapper(AbstractBootstrapper): cb_chain = [ (self._download_config, self.download_config), - (self._download_client_certificates, \ - self.download_client_certificate) + (self._download_client_certificates, + self.download_client_certificate) ] self.addCallbackChain(cb_chain) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index d15aed82..6c2ff006 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -319,7 +319,7 @@ class DarwinVPNLauncher(VPNLauncher): UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,) DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,) OPENVPN_DOWN_PLUGIN = '%s/openvpn-down-root.so' % (OPENVPN_PATH,) - + UPDOWN_FILES = (UP_SCRIPT, DOWN_SCRIPT, OPENVPN_DOWN_PLUGIN) @classmethod diff --git a/src/leap/services/mail/smtpbootstrapper.py b/src/leap/services/mail/smtpbootstrapper.py index ea480c6d..e8af5349 100644 --- a/src/leap/services/mail/smtpbootstrapper.py +++ b/src/leap/services/mail/smtpbootstrapper.py @@ -94,11 +94,10 @@ class SMTPBootstrapper(AbstractBootstrapper): # Not modified if res.status_code == 304: logger.debug("SMTP definition has not been modified") - self._smtp_config.load(os.path.join("leap", - "providers", - self._provider_config.\ - get_domain(), - "smtp-service.json")) + self._smtp_config.load(os.path.join( + "leap", "providers", + self._provider_config.get_domain(), + "smtp-service.json")) else: smtp_definition, mtime = get_content(res) -- cgit v1.2.3 From 336c21f8f5691f30cdf43c025695c5476be7fcec Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Thu, 13 Jun 2013 18:04:38 -0300 Subject: Autoselect VPN gateway based on timezone. --- src/leap/services/eip/eipconfig.py | 91 +++++++++++++++++++++++++++++++++-- src/leap/services/eip/vpnlaunchers.py | 12 +++-- 2 files changed, 96 insertions(+), 7 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index 0a7d2b23..f7d03963 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -21,6 +21,8 @@ Provider configuration import logging import os import re +import datetime +import time import ipaddr @@ -32,6 +34,79 @@ from leap.services.eip.eipspec import eipservice_config_spec logger = logging.getLogger(__name__) +class VPNGatewaySelector(object): + """ + VPN Gateway selector. + """ + + def __init__(self, eipconfig): + ''' + Constructor for VPNGatewaySelector. + + :param eipconfig: a valid EIP Configuration. + :type eipconfig: EIPConfig + ''' + leap_assert_type(eipconfig, EIPConfig) + self._local_offset = 0 # defaults to GMT + self._local_timezone = None + self._set_local_offset() + self._eipconfig = eipconfig + + def _get_best_gateway(self): + """ + Returns index of the closest gateway, using timezones offsets. + + :rtype: int + """ + best_gateway = (-1, 99) # gateway, distance + locations = self._eipconfig.get_locations() + gateways = self._eipconfig.get_gateways() + for idx, gateway in enumerate(gateways): + gateway_offset = int(locations[gateway['location']]['timezone']) + gateway_distance = self._get_timezone_distance(gateway_offset) + if gateway_distance < best_gateway[1]: + best_gateway = (idx, gateway_distance) + + return best_gateway[0] + + def get_best_gateway_ip(self): + """ + Returns the ip of the best possible gateway. + + :rtype: An IPv4Address or IPv6Address object. + """ + best_gateway = self._get_best_gateway() + gateway_ip = self._eipconfig.get_gateway_ip(best_gateway) + + return gateway_ip + + def _get_timezone_distance(self, offset): + ''' + Returns the distance between the local timezone and + the one with offset 'offset'. + + :param offset: the distance of a timezone to GMT. + :type offset: int + :returns: distance between local offset and param offset. + :rtype: int + ''' + delta1 = datetime.timedelta(hours=offset) + delta2 = self._local_offset + diff = abs(delta1 - delta2) + hours = diff.seconds / (60 * 60) + return hours + + def _set_local_offset(self): + ''' + Sets the distance between GMT and the local timezone. + ''' + local_offset = time.timezone + if time.daylight: + local_offset = time.altzone + + self._local_offset = datetime.timedelta(seconds=-local_offset) + + class EIPConfig(BaseConfig): """ Provider configuration abstraction class @@ -56,6 +131,14 @@ class EIPConfig(BaseConfig): # TODO: create an abstraction for gateways return self._safe_get_value("gateways") + def get_locations(self): + ''' + Returns a list of locations + + :rtype: dict + ''' + return self._safe_get_value("locations") + def get_openvpn_configuration(self): """ Returns a dictionary containing the openvpn configuration @@ -63,8 +146,8 @@ class EIPConfig(BaseConfig): These are sanitized with alphanumeric whitelist. - @returns: openvpn configuration dict - @rtype: C{dict} + :returns: openvpn configuration dict + :rtype: C{dict} """ ovpncfg = self._safe_get_value("openvpn_configuration") config = {} @@ -84,7 +167,9 @@ class EIPConfig(BaseConfig): def get_gateway_ip(self, index=0): """ - Returns the ip of the gateway + Returns the ip of the gateway. + + :rtype: An IPv4Address or IPv6Address object. """ gateways = self.get_gateways() leap_assert(len(gateways) > 0, "We don't have any gateway!") diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 6c2ff006..fa2989bc 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -34,7 +34,7 @@ from functools import partial from leap.common.check import leap_assert, leap_assert_type from leap.common.files import which from leap.config.providerconfig import ProviderConfig -from leap.services.eip.eipconfig import EIPConfig +from leap.services.eip.eipconfig import EIPConfig, VPNGatewaySelector logger = logging.getLogger(__name__) @@ -228,7 +228,8 @@ class LinuxVPNLauncher(VPNLauncher): # TODO: handle verbosity - gateway_ip = str(eipconfig.get_gateway_ip(0)) + gateway_selector = VPNGatewaySelector(eipconfig) + gateway_ip = gateway_selector.get_best_gateway_ip() logger.debug("Using gateway ip %s" % (gateway_ip,)) @@ -391,7 +392,9 @@ class DarwinVPNLauncher(VPNLauncher): # TODO: handle verbosity - gateway_ip = str(eipconfig.get_gateway_ip(0)) + gateway_selector = VPNGatewaySelector(eipconfig) + gateway_ip = gateway_selector.get_best_gateway_ip() + logger.debug("Using gateway ip %s" % (gateway_ip,)) args += [ @@ -530,7 +533,8 @@ class WindowsVPNLauncher(VPNLauncher): # TODO: handle verbosity - gateway_ip = str(eipconfig.get_gateway_ip(0)) + gateway_selector = VPNGatewaySelector(eipconfig) + gateway_ip = gateway_selector.get_best_gateway_ip() logger.debug("Using gateway ip %s" % (gateway_ip,)) -- cgit v1.2.3 From 8bee5f4e9a1bb0f7069fe41ab37dfec000487d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 14 Jun 2013 12:45:08 -0300 Subject: Actually deferToThread all the things we expect to do in parallel --- src/leap/services/abstractbootstrapper.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/abstractbootstrapper.py b/src/leap/services/abstractbootstrapper.py index 2cbd56bc..f0937197 100644 --- a/src/leap/services/abstractbootstrapper.py +++ b/src/leap/services/abstractbootstrapper.py @@ -22,6 +22,8 @@ import logging import requests +from functools import partial + from PySide import QtCore from twisted.internet import threads from leap.common.check import leap_assert, leap_assert_type @@ -128,6 +130,9 @@ class AbstractBootstrapper(QtCore.QObject): logger.debug("Emitting %s" % (signal,)) signal.emit({self.PASSED_KEY: True, self.ERROR_KEY: ""}) + def _callback_threader(self, cb, res, *args, **kwargs): + return threads.deferToThread(cb, res, *args, **kwargs) + def addCallbackChain(self, callbacks): """ Creates a callback/errback chain on another thread using @@ -148,7 +153,7 @@ class AbstractBootstrapper(QtCore.QObject): if d is None: d = threads.deferToThread(cb) else: - d.addCallback(cb) + d.addCallback(partial(self._callback_threader, cb)) d.addErrback(self._errback, signal=sig) d.addCallback(self._gui_notify, signal=sig) d.addErrback(self._gui_errback) -- cgit v1.2.3 From fdda95c92d22b4ede61bfb7587320ddb36da5cd7 Mon Sep 17 00:00:00 2001 From: kali Date: Sat, 15 Jun 2013 01:17:35 +0900 Subject: working openvpn termination: SIGTERM, then SIGKILL. Closes: #2753 --- src/leap/services/eip/vpnprocess.py | 141 +++++++++++++++++++++++++++++------- 1 file changed, 113 insertions(+), 28 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnprocess.py b/src/leap/services/eip/vpnprocess.py index eae8aadd..162dc7f0 100644 --- a/src/leap/services/eip/vpnprocess.py +++ b/src/leap/services/eip/vpnprocess.py @@ -20,6 +20,8 @@ VPN Manager, spawned in a custom processProtocol. import logging import os import psutil +import socket +import time from PySide import QtCore @@ -63,6 +65,9 @@ class VPN(object): opened by the openvpn process, executing commands over that interface on demand. """ + TERMINATE_MAXTRIES = 10 + TERMINATE_WAIT = 1 # secs + def __init__(self): """ Instantiate empty attributes and get a copy @@ -94,6 +99,10 @@ class VPN(object): # start the main vpn subprocess vpnproc = VPNProcess(*args, **kwargs) + # XXX Should stop if already running ------- + if vpnproc.get_openvpn_process(): + logger.warning("Another vpnprocess is running!") + cmd = vpnproc.getCommand() env = os.environ for key, val in vpnproc.vpn_env.items(): @@ -103,7 +112,7 @@ class VPN(object): self._vpnproc = vpnproc # add pollers for status and state - # XXX this could be extended to a collection of + # this could be extended to a collection of # generic watchers poll_list = [LoopingCall(vpnproc.pollStatus), @@ -111,15 +120,50 @@ class VPN(object): self._pollers.extend(poll_list) self._start_pollers() + def _kill_if_left_alive(self, tries=0): + """ + Check if the process is still alive, and sends a + SIGKILL after a timeout period. + + :param tries: counter of tries, used in recursion + :type tries: int + """ + from twisted.internet import reactor + while tries < self.TERMINATE_MAXTRIES: + if self._vpnproc.transport.pid is None: + logger.debug("Process has been happily terminated.") + return + else: + logger.debug("Process did not die, waiting...") + tries += 1 + reactor.callLater(self.TERMINATE_WAIT, + self._kill_if_left_alive, tries) + + # after running out of patience, we try a killProcess + logger.debug("Process did not died. Sending a SIGKILL.") + self._vpnproc.killProcess() + def terminate(self): """ Stops the openvpn subprocess. + + Attempts to send a SIGTERM first, and after a timeout + it sends a SIGKILL. """ + from twisted.internet import reactor self._stop_pollers() - # XXX we should leave a KILL as a last resort. - # First we should try to send a SIGTERM + + # First we try to be polite and send a SIGTERM... if self._vpnproc: - self._vpnproc.killProcess() + self._sentterm = True + self._vpnproc.terminate_openvpn() + + # ...but we also trigger a countdown to be unpolite + # if strictly needed. + reactor.callLater( + self.TERMINATE_WAIT, self._kill_if_left_alive) + + # TODO: should also cleanup tempfiles!!! def _start_pollers(self): """ @@ -148,6 +192,10 @@ class VPNManager(object): A copy of a QObject containing signals as attributes is passed along upon initialization, and we use that object to emit signals to qt-land. + + For more info about management methods:: + + zcat `dpkg -L openvpn | grep management` """ # Timers, in secs @@ -183,15 +231,15 @@ class VPNManager(object): def qtsigs(self): return self._qtsigs - def _disconnect(self): + def _seek_to_eof(self): """ - Disconnects the telnet connection to the openvpn process. + Read as much as available. Position seek pointer to end of stream """ - logger.debug('Closing socket') - self._tn.write("quit\n") - self._tn.read_all() - self._tn.close() - self._tn = None + try: + self._tn.read_eager() + except EOFError: + logger.debug("Could not read from socket. Assuming it died.") + return def _send_command(self, command, until=b"END"): """ @@ -208,12 +256,24 @@ class VPNManager(object): :rtype: list """ leap_assert(self._tn, "We need a tn connection!") + try: self._tn.write("%s\n" % (command,)) buf = self._tn.read_until(until, 2) - self._tn.read_eager() - lines = buf.split("\n") - return lines + self._seek_to_eof() + blist = buf.split('\r\n') + if blist[-1].startswith(until): + del blist[-1] + return blist + else: + return [] + + except socket.error: + # XXX should get a counter and repeat only + # after mod X times. + logger.warning('socket error') + self._close_management_socket(announce=False) + return [] # XXX should move this to a errBack! except Exception as e: @@ -221,9 +281,21 @@ class VPNManager(object): (command, e)) return [] - def _connect(self, socket_host, socket_port): + 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 _connect_management(self, socket_host, socket_port): """ - Connects to the specified socket_host socket_port. + Connects to the management interface on the specified + socket_host socket_port. :param socket_host: either socket path (unix) or socket IP :type socket_host: str @@ -232,6 +304,9 @@ class VPNManager(object): socket, or port otherwise :type socket_port: str """ + if self.is_connected(): + self._close_management_socket() + try: self._tn = UDSTelnet(socket_host, socket_port) @@ -268,7 +343,7 @@ class VPNManager(object): """ logger.warning(failure) - def connect(self, host, port): + def connect_to_management(self, host, port): """ Connect to a management interface. @@ -280,7 +355,8 @@ class VPNManager(object): :returns: a deferred """ - self.connectd = defer.maybeDeferred(self._connect, host, port) + self.connectd = defer.maybeDeferred( + self._connect_management, host, port) self.connectd.addCallbacks(self._connectCb, self._connectErr) return self.connectd @@ -293,7 +369,7 @@ class VPNManager(object): """ return True if self._tn else False - def try_to_connect(self, retry=0): + def try_to_connect_to_management(self, retry=0): """ Attempts to connect to a management interface, and retries after CONNECTION_RETRY_TIME if not successful. @@ -304,9 +380,10 @@ class VPNManager(object): # TODO decide about putting a max_lim to retries and signaling # an error. if not self.is_connected(): - self.connect(self._socket_host, self._socket_port) + self.connect_to_management(self._socket_host, self._socket_port) self._reactor.callLater( - self.CONNECTION_RETRY_TIME, self.try_to_connect, retry + 1) + self.CONNECTION_RETRY_TIME, + self.try_to_connect_to_management, retry + 1) def _parse_state_and_notify(self, output): """ @@ -405,9 +482,17 @@ class VPNManager(object): """ return self._launcher.get_vpn_env(self._providerconfig) + def terminate_openvpn(self): + """ + Attempts to terminate openvpn by sending a SIGTERM. + """ + if self.is_connected(): + self._send_command("signal SIGTERM") + + # --------------------------------------------------- # XXX old methods, not adapted to twisted process yet - def _get_openvpn_process(self): + def get_openvpn_process(self): """ Looks for openvpn instances running. @@ -421,7 +506,7 @@ class VPNManager(object): # we should check that cmdline BEGINS # with openvpn or with our wrapper # (pkexec / osascript / whatever) - if self._launcher.OPENVPN_BIN in ' '.join(p.cmdline): + if "openvpn" in ' '.join(p.cmdline): openvpn_process = p break except psutil.error.AccessDenied: @@ -434,10 +519,10 @@ class VPNManager(object): :return: True if stopped, False otherwise """ - + # TODO cleanup this process = self._get_openvpn_process() if process: - logger.debug("OpenVPN is already running, trying to stop it") + logger.debug("OpenVPN is already running, trying to stop it...") cmdline = process.cmdline manag_flag = "--management" @@ -448,11 +533,11 @@ class VPNManager(object): port = cmdline[index + 2] logger.debug("Trying to connect to %s:%s" % (host, port)) - self._connect(host, port) + self._connect_to_management(host, port) self._send_command("signal SIGTERM") self._tn.close() self._tn = None - #self._disconnect() + #self._disconnect_management() except Exception as e: logger.warning("Problem trying to terminate OpenVPN: %r" % (e,)) @@ -518,7 +603,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa """ - self.try_to_connect() + self.try_to_connect_to_management() def outReceived(self, data): """ -- cgit v1.2.3 From 0f43f3db34dd0262e298dd4431b1e971c77dfbdd Mon Sep 17 00:00:00 2001 From: kali Date: Mon, 17 Jun 2013 05:37:04 +0900 Subject: Remove temporal folder during shutdown Closes: #2891 This is the temporal folder that is used under osx and linux for the unix socket to connect to the management interface. --- src/leap/services/eip/vpnprocess.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnprocess.py b/src/leap/services/eip/vpnprocess.py index 162dc7f0..f3443533 100644 --- a/src/leap/services/eip/vpnprocess.py +++ b/src/leap/services/eip/vpnprocess.py @@ -20,8 +20,8 @@ VPN Manager, spawned in a custom processProtocol. import logging import os import psutil +import shutil import socket -import time from PySide import QtCore @@ -143,7 +143,7 @@ class VPN(object): logger.debug("Process did not died. Sending a SIGKILL.") self._vpnproc.killProcess() - def terminate(self): + def terminate(self, shutdown=False): """ Stops the openvpn subprocess. @@ -156,15 +156,13 @@ class VPN(object): # First we try to be polite and send a SIGTERM... if self._vpnproc: self._sentterm = True - self._vpnproc.terminate_openvpn() + self._vpnproc.terminate_openvpn(shutdown=shutdown) # ...but we also trigger a countdown to be unpolite # if strictly needed. reactor.callLater( self.TERMINATE_WAIT, self._kill_if_left_alive) - # TODO: should also cleanup tempfiles!!! - def _start_pollers(self): """ Iterate through the registered observers @@ -482,12 +480,30 @@ class VPNManager(object): """ return self._launcher.get_vpn_env(self._providerconfig) - def terminate_openvpn(self): + def terminate_openvpn(self, shutdown=False): """ Attempts to terminate openvpn by sending a SIGTERM. """ if self.is_connected(): self._send_command("signal SIGTERM") + if shutdown: + self._cleanup_tempfiles() + + def _cleanup_tempfiles(self): + """ + Remove all temporal files we might have left behind. + + Iif self.port is 'unix', we have created a temporal socket path that, + under normal circumstances, we should be able to delete. + """ + if self._socket_port == "unix": + logger.debug('cleaning socket file temp folder') + tempfolder = os.path.split(self._socket_host)[0] # XXX use `first` + if os.path.isdir(tempfolder): + try: + shutil.rmtree(tempfolder) + except OSError: + logger.error('could not delete tmpfolder %s' % tempfolder) # --------------------------------------------------- # XXX old methods, not adapted to twisted process yet -- cgit v1.2.3 From 6c309232c6b669a3f715913f5e172dc6a5e24078 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Tue, 18 Jun 2013 14:39:33 -0300 Subject: Improve timezone gateway selector. Closes #2894 It allows to use multiple gateways in openvpn for redundancy. --- src/leap/services/eip/eipconfig.py | 33 ++++++++++++++++----------------- src/leap/services/eip/vpnlaunchers.py | 24 +++++++++++++++--------- 2 files changed, 31 insertions(+), 26 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index f7d03963..a85fe64a 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -52,33 +52,32 @@ class VPNGatewaySelector(object): self._set_local_offset() self._eipconfig = eipconfig - def _get_best_gateway(self): + def get_gateways(self): """ - Returns index of the closest gateway, using timezones offsets. + Returns the 4 best gateways, sorted by timezone proximity. - :rtype: int + :rtype: list of IPv4Address or IPv6Address object. """ - best_gateway = (-1, 99) # gateway, distance + gateways_timezones = [] locations = self._eipconfig.get_locations() gateways = self._eipconfig.get_gateways() + for idx, gateway in enumerate(gateways): - gateway_offset = int(locations[gateway['location']]['timezone']) - gateway_distance = self._get_timezone_distance(gateway_offset) - if gateway_distance < best_gateway[1]: - best_gateway = (idx, gateway_distance) + gateway_location = gateway.get('location') + gateway_distance = 99 # if hasn't location -> should go last - return best_gateway[0] + if gateway_location is not None: + gw_offset = int(locations[gateway['location']]['timezone']) + gateway_distance = self._get_timezone_distance(gw_offset) - def get_best_gateway_ip(self): - """ - Returns the ip of the best possible gateway. + ip = self._eipconfig.get_gateway_ip(idx) + gateways_timezones.append((ip, gateway_distance)) - :rtype: An IPv4Address or IPv6Address object. - """ - best_gateway = self._get_best_gateway() - gateway_ip = self._eipconfig.get_gateway_ip(best_gateway) + gateways_timezones = sorted(gateways_timezones, + key=lambda gw: gw[1])[:4] - return gateway_ip + gateways = [ip for ip, dist in gateways_timezones] + return gateways def _get_timezone_distance(self, offset): ''' diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index fa2989bc..fdc863c6 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -229,16 +229,18 @@ class LinuxVPNLauncher(VPNLauncher): # TODO: handle verbosity gateway_selector = VPNGatewaySelector(eipconfig) - gateway_ip = gateway_selector.get_best_gateway_ip() + gateways = gateway_selector.get_gateways() - logger.debug("Using gateway ip %s" % (gateway_ip,)) + logger.debug("Using gateways ips: {}".format(', '.join(gateways))) + + for gw in gateways: + args += ['--remote', gw, '1194', 'udp'] args += [ '--client', '--dev', 'tun', '--persist-tun', '--persist-key', - '--remote', gateway_ip, '1194', 'udp', '--tls-client', '--remote-cert-tls', 'server' @@ -393,16 +395,18 @@ class DarwinVPNLauncher(VPNLauncher): # TODO: handle verbosity gateway_selector = VPNGatewaySelector(eipconfig) - gateway_ip = gateway_selector.get_best_gateway_ip() + gateways = gateway_selector.get_gateways() + + logger.debug("Using gateways ips: {}".format(', '.join(gateways))) - logger.debug("Using gateway ip %s" % (gateway_ip,)) + for gw in gateways: + args += ['--remote', gw, '1194', 'udp'] args += [ '--client', '--dev', 'tun', '--persist-tun', '--persist-key', - '--remote', gateway_ip, '1194', 'udp', '--tls-client', '--remote-cert-tls', 'server' @@ -534,16 +538,18 @@ class WindowsVPNLauncher(VPNLauncher): # TODO: handle verbosity gateway_selector = VPNGatewaySelector(eipconfig) - gateway_ip = gateway_selector.get_best_gateway_ip() + gateways = gateway_selector.get_gateways() + + logger.debug("Using gateways ips: {}".format(', '.join(gateways))) - logger.debug("Using gateway ip %s" % (gateway_ip,)) + for gw in gateways: + args += ['--remote', gw, '1194', 'udp'] args += [ '--client', '--dev', 'tun', '--persist-tun', '--persist-key', - '--remote', gateway_ip, '1194', 'udp', '--tls-client', '--remote-cert-tls', 'server' -- cgit v1.2.3 From b36fe9cf87bc1917abc0667756f01e6d4609cc4c Mon Sep 17 00:00:00 2001 From: kali Date: Mon, 17 Jun 2013 04:46:06 +0900 Subject: install missing files during linux initialization Closes: #2247, #2761 --- src/leap/services/eip/vpnlaunchers.py | 164 +++++++++++++++++++++++++--------- 1 file changed, 122 insertions(+), 42 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index fa2989bc..436072d2 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -35,6 +35,7 @@ from leap.common.check import leap_assert, leap_assert_type from leap.common.files import which from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig, VPNGatewaySelector +from leap.util import first logger = logging.getLogger(__name__) @@ -59,9 +60,11 @@ class VPNLauncher: """ Abstract launcher class """ - __metaclass__ = ABCMeta + UPDOWN_FILES = None + OTHER_FILES = None + @abstractmethod def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port=None): @@ -97,6 +100,35 @@ class VPNLauncher: """ return {} + @classmethod + def missing_updown_scripts(kls): + """ + Returns what updown scripts are missing. + :rtype: list + """ + leap_assert(kls.UPDOWN_FILES is not None, + "Need to define UPDOWN_FILES for this particular " + "auncher before calling this method") + file_exist = partial(_has_updown_scripts, warn=False) + zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) + missing = filter(lambda (path, exists): exists is False, zipped) + return [path for path, exists in missing] + + @classmethod + def missing_other_files(kls): + """ + Returns what other important files are missing during startup. + Same as missing_updown_scripts but does not check for exec bit. + :rtype: list + """ + leap_assert(kls.UPDOWN_FILES is not None, + "Need to define OTHER_FILES for this particular " + "auncher before calling this method") + file_exist = partial(_has_other_files, warn=False) + zipped = zip(kls.OTHER_FILES, map(file_exist, kls.OTHER_FILES)) + missing = filter(lambda (path, exists): exists is False, zipped) + return [path for path, exists in missing] + def get_platform_launcher(): launcher = globals()[platform.system() + "VPNLauncher"] @@ -117,7 +149,8 @@ def _is_pkexec_in_system(): def _has_updown_scripts(path, warn=True): """ - Checks the existence of the up/down scripts. + Checks the existence of the up/down scripts and its + exec bit if applicable. :param path: the path to be checked :type path: str @@ -132,6 +165,7 @@ def _has_updown_scripts(path, warn=True): logger.error("Could not find up/down script %s. " "Might produce DNS leaks." % (path,)) + # XXX check if applies in win is_exe = os.access(path, os.X_OK) if warn and not is_exe: logger.error("Up/down script %s is not executable. " @@ -139,6 +173,25 @@ def _has_updown_scripts(path, warn=True): return is_file and is_exe +def _has_other_files(path, warn=True): + """ + Checks the existence of other important files. + + :param path: the path to be checked + :type path: str + + :param warn: whether we should log the absence + :type warn: bool + + :rtype: bool + """ + is_file = os.path.isfile(path) + if warn and not is_file: + logger.warning("Could not find file during checks: %s. " % ( + path,)) + return is_file + + def _is_auth_agent_running(): """ Checks if a polkit daemon is running. @@ -160,8 +213,59 @@ class LinuxVPNLauncher(VPNLauncher): PKEXEC_BIN = 'pkexec' OPENVPN_BIN = 'openvpn' - UP_DOWN_SCRIPT = "/etc/leap/resolv-update" - OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so" + SYSTEM_CONFIG = "/etc/leap" + UP_DOWN_FILE = "resolv-update" + UP_DOWN_PATH = "%s/%s" % (SYSTEM_CONFIG, UP_DOWN_FILE) + + # We assume this is there by our openvpn dependency, and + # we will put it there on the bundle too. + # TODO adapt to the bundle path. + OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-plugin-down-root.so" + + POLKIT_BASE = "/usr/share/polkit-1/actions" + POLKIT_FILE = "net.openvpn.gui.leap.policy" + POLKIT_PATH = "%s/%s" % (POLKIT_BASE, POLKIT_FILE) + + UPDOWN_FILES = (UP_DOWN_PATH,) + OTHER_FILES = (POLKIT_PATH,) + + @classmethod + def cmd_for_missing_scripts(kls, frompath): + """ + Returns a command that can copy the missing scripts. + :rtype: str + """ + to = kls.SYSTEM_CONFIG + cmd = "#!/bin/sh\nset -e\nmkdir -p %s\ncp %s/%s %s\ncp %s/%s %s" % ( + to, + frompath, kls.UP_DOWN_FILE, to, + frompath, kls.POLKIT_FILE, kls.POLKIT_PATH) + return cmd + + @classmethod + def maybe_pkexec(kls): + """ + Checks whether pkexec is available in the system, and + returns the path if found. + + Might raise EIPNoPkexecAvailable or EIPNoPolkitAuthAgentAvailable + + :returns: a list of the paths where pkexec is to be found + :rtype: list + """ + if _is_pkexec_in_system(): + if _is_auth_agent_running(): + pkexec_possibilities = which(kls.PKEXEC_BIN) + leap_assert(len(pkexec_possibilities) > 0, + "We couldn't find pkexec") + return pkexec_possibilities + else: + logger.warning("No polkit auth agent found. pkexec " + + "will use its own auth agent.") + raise EIPNoPolkitAuthAgentAvailable() + else: + logger.warning("System has no pkexec") + raise EIPNoPkexecAvailable() def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="unix"): @@ -201,30 +305,18 @@ class LinuxVPNLauncher(VPNLauncher): providerconfig.get_path_prefix(), "..", "apps", "eip") - openvpn_possibilities = which( - self.OPENVPN_BIN, - **kwargs) + openvpn_possibilities = which(self.OPENVPN_BIN, **kwargs) if len(openvpn_possibilities) == 0: raise OpenVPNNotFoundException() - openvpn = openvpn_possibilities[0] + openvpn = first(openvpn_possibilities) args = [] - if _is_pkexec_in_system(): - if _is_auth_agent_running(): - pkexec_possibilities = which(self.PKEXEC_BIN) - leap_assert(len(pkexec_possibilities) > 0, - "We couldn't find pkexec") - args.append(openvpn) - openvpn = pkexec_possibilities[0] - else: - logger.warning("No polkit auth agent found. pkexec " + - "will use its own auth agent.") - raise EIPNoPolkitAuthAgentAvailable() - else: - logger.warning("System has no pkexec") - raise EIPNoPkexecAvailable() + pkexec = self.maybe_pkexec() + if pkexec: + args.append(openvpn) + openvpn = first(pkexec) # TODO: handle verbosity @@ -265,12 +357,12 @@ class LinuxVPNLauncher(VPNLauncher): '--script-security', '2' ] - if _has_updown_scripts(self.UP_DOWN_SCRIPT): + if _has_updown_scripts(self.UP_DOWN_PATH): args += [ - '--up', self.UP_DOWN_SCRIPT, - '--down', self.UP_DOWN_SCRIPT, + '--up', self.UP_DOWN_PATH, + '--down', self.UP_DOWN_PATH, '--plugin', self.OPENVPN_DOWN_ROOT, - '\'script_type=down %s\'' % self.UP_DOWN_SCRIPT + '\'script_type=down %s\'' % self.UP_DOWN_PATH ] args += [ @@ -323,17 +415,6 @@ class DarwinVPNLauncher(VPNLauncher): UPDOWN_FILES = (UP_SCRIPT, DOWN_SCRIPT, OPENVPN_DOWN_PLUGIN) - @classmethod - def missing_updown_scripts(kls): - """ - Returns what updown scripts are missing. - :rtype: list - """ - file_exist = partial(_has_updown_scripts, warn=False) - zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) - missing = filter(lambda (path, exists): exists is False, zipped) - return [path for path, exists in missing] - @classmethod def cmd_for_missing_scripts(kls, frompath): """ @@ -387,7 +468,7 @@ class DarwinVPNLauncher(VPNLauncher): if len(openvpn_possibilities) == 0: raise OpenVPNNotFoundException() - openvpn = openvpn_possibilities[0] + openvpn = first(openvpn_possibilities) args = [openvpn] # TODO: handle verbosity @@ -489,6 +570,8 @@ class WindowsVPNLauncher(VPNLauncher): OPENVPN_BIN = 'openvpn_leap.exe' + # XXX UPDOWN_FILES ... we do not have updown files defined yet! + def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="9876"): """ @@ -528,7 +611,7 @@ class WindowsVPNLauncher(VPNLauncher): if len(openvpn_possibilities) == 0: raise OpenVPNNotFoundException() - openvpn = openvpn_possibilities[0] + openvpn = first(openvpn_possibilities) args = [] # TODO: handle verbosity @@ -550,7 +633,6 @@ class WindowsVPNLauncher(VPNLauncher): ] openvpn_configuration = eipconfig.get_openvpn_configuration() - # XXX sanitize this for key, value in openvpn_configuration.items(): args += ['--%s' % (key,), value] @@ -558,13 +640,11 @@ class WindowsVPNLauncher(VPNLauncher): '--user', getpass.getuser(), #'--group', grp.getgrgid(os.getgroups()[-1]).gr_name ] - args += [ '--management-signal', '--management', socket_host, socket_port, '--script-security', '2' ] - args += [ '--cert', eipconfig.get_client_cert_path(providerconfig), '--key', eipconfig.get_client_cert_path(providerconfig), -- cgit v1.2.3 From d43a6905bc255946c8ecb02dfdfed5d159a86b26 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 20 Jun 2013 02:59:44 +0900 Subject: fix zero length field format in 2.6 --- src/leap/services/eip/vpnlaunchers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index af77c146..ef73ed94 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -478,7 +478,8 @@ class DarwinVPNLauncher(VPNLauncher): gateway_selector = VPNGatewaySelector(eipconfig) gateways = gateway_selector.get_gateways() - logger.debug("Using gateways ips: {}".format(', '.join(gateways))) + logger.debug("Using gateways ips: {gw}".format( + gw=', '.join(gateways))) for gw in gateways: args += ['--remote', gw, '1194', 'udp'] -- cgit v1.2.3 From cdf63528b09b702400e0aac0c6f39e79a7e29773 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 20 Jun 2013 03:02:09 +0900 Subject: nullify _tn instead of deleting --- src/leap/services/eip/vpnprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnprocess.py b/src/leap/services/eip/vpnprocess.py index f3443533..4b558160 100644 --- a/src/leap/services/eip/vpnprocess.py +++ b/src/leap/services/eip/vpnprocess.py @@ -288,7 +288,7 @@ class VPNManager(object): self._tn.write("quit\n") self._tn.read_all() self._tn.get_socket().close() - del self._tn + self._tn = None def _connect_management(self, socket_host, socket_port): """ -- cgit v1.2.3 From c640e979a202021111a2b14e9ec089d78118fbb6 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 19 Jun 2013 16:44:04 -0300 Subject: Add psutil.error import to solve OSX problem. Closes #2936 --- src/leap/services/eip/vpnprocess.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnprocess.py b/src/leap/services/eip/vpnprocess.py index f3443533..f1d06943 100644 --- a/src/leap/services/eip/vpnprocess.py +++ b/src/leap/services/eip/vpnprocess.py @@ -20,6 +20,7 @@ VPN Manager, spawned in a custom processProtocol. import logging import os import psutil +import psutil.error import shutil import socket -- cgit v1.2.3 From 8b690a0a84dcb19259e863e05d71423d1272b435 Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 20 Jun 2013 05:57:38 +0900 Subject: look for down-root plugin in bundle and system --- src/leap/services/eip/vpnlaunchers.py | 42 +++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index ef73ed94..01ec42cf 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -220,7 +220,11 @@ class LinuxVPNLauncher(VPNLauncher): # We assume this is there by our openvpn dependency, and # we will put it there on the bundle too. # TODO adapt to the bundle path. - OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-plugin-down-root.so" + OPENVPN_DOWN_ROOT_BASE = "/usr/lib/openvpn/" + OPENVPN_DOWN_ROOT_FILE = "openvpn-plugin-down-root.so" + OPENVPN_DOWN_ROOT_PATH = "%s/%s" % ( + OPENVPN_DOWN_ROOT_BASE, + OPENVPN_DOWN_ROOT_FILE) POLKIT_BASE = "/usr/share/polkit-1/actions" POLKIT_FILE = "net.openvpn.gui.leap.policy" @@ -267,6 +271,30 @@ class LinuxVPNLauncher(VPNLauncher): logger.warning("System has no pkexec") raise EIPNoPkexecAvailable() + @classmethod + def maybe_down_plugin(kls): + """ + Returns the path of the openvpn down-root-plugin, searching first + in the relative path for the standalone bundle, and then in the system + path where the debian package puts it. + + :returns: the path where the plugin was found, or None + :rtype: str or None + """ + cwd = os.getcwd() + rel_path_in_bundle = os.path.join( + 'apps', 'eip', 'files', kls.OPENVPN_DOWN_ROOT_FILE) + abs_path_in_bundle = os.path.join(cwd, rel_path_in_bundle) + if os.path.isfile(abs_path_in_bundle): + return abs_path_in_bundle + abs_path_in_system = kls.OPENVPN_DOWN_ROOT_FILE + if os.path.isfile(abs_path_in_system): + return abs_path_in_system + + logger.warning("We could not find the down-root-plugin, so no updown " + "scripts will be run. DNS leaks are likely!") + return None + def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="unix"): """ @@ -348,7 +376,7 @@ class LinuxVPNLauncher(VPNLauncher): '--group', grp.getgrgid(os.getgroups()[-1]).gr_name ] - if socket_port == "unix": + if socket_port == "unix": # that's always the case for linux args += [ '--management-client-user', getpass.getuser() ] @@ -359,11 +387,17 @@ class LinuxVPNLauncher(VPNLauncher): '--script-security', '2' ] - if _has_updown_scripts(self.UP_DOWN_PATH): + plugin_path = self.maybe_down_plugin() + # If we do not have the down plugin neither in the bundle + # nor in the system, we do not do updown scripts. The alternative + # is leaving the user without the ability to restore dns and routes + # to its original state. + + if plugin_path and _has_updown_scripts(self.UP_DOWN_PATH): args += [ '--up', self.UP_DOWN_PATH, '--down', self.UP_DOWN_PATH, - '--plugin', self.OPENVPN_DOWN_ROOT, + '--plugin', plugin_path, '\'script_type=down %s\'' % self.UP_DOWN_PATH ] -- cgit v1.2.3 From d288330c6294a51cec6fd1dcc35b1b6f4f36932d Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 20 Jun 2013 08:38:23 +0900 Subject: tries to launch polkit authentication agent Closes: #2884 --- src/leap/services/eip/vpnlaunchers.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index ef73ed94..813527bd 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -23,6 +23,7 @@ import logging import getpass import os import platform +import subprocess try: import grp except ImportError: @@ -199,11 +200,25 @@ def _is_auth_agent_running(): :return: True if it's running, False if it's not. :rtype: boolean """ - polkit_gnome = 'ps aux | grep polkit-[g]nome-authentication-agent-1' - polkit_kde = 'ps aux | grep polkit-[k]de-authentication-agent-1' + ps = 'ps aux | grep polkit-%s-authentication-agent-1' + opts = (ps % case for case in ['[g]nome', '[k]de']) + is_running = map(lambda l: commands.getoutput(l), opts) + return any(is_running) - return (len(commands.getoutput(polkit_gnome)) > 0 or - len(commands.getoutput(polkit_kde)) > 0) + +def _try_to_launch_agent(): + """ + Tries to launch a polkit daemon. + """ + opts = [ + "/usr/lib/policykit-1-gnome/polkit-gnome-authentication-agent-1&", + # XXX add kde thing here + ] + for cmd in opts: + try: + subprocess.Popen([cmd], shell=True) + except: + pass class LinuxVPNLauncher(VPNLauncher): @@ -254,6 +269,8 @@ class LinuxVPNLauncher(VPNLauncher): :rtype: list """ if _is_pkexec_in_system(): + if not _is_auth_agent_running(): + _try_to_launch_agent() if _is_auth_agent_running(): pkexec_possibilities = which(kls.PKEXEC_BIN) leap_assert(len(pkexec_possibilities) > 0, -- cgit v1.2.3 From 4a422c92ae83453807074afca6c2f038823e296f Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 20 Jun 2013 10:26:46 +0900 Subject: hide mx until we integrate it Closes: #2938 --- src/leap/services/__init__.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) (limited to 'src/leap/services') diff --git a/src/leap/services/__init__.py b/src/leap/services/__init__.py index e69de29b..70a5dcf2 100644 --- a/src/leap/services/__init__.py +++ b/src/leap/services/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# __init__.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Services module. +""" +NOT_YET_DEPLOYED = ["mx"] # for 0.2.2 release + + +def get_available(services): + """ + Returns a list of the available services. + + :param services: a list containing the services to be filtered. + :type services: list of str + + :returns: a list of the available services + :rtype: list of str + """ + return filter(lambda s: s not in NOT_YET_DEPLOYED, services) -- cgit v1.2.3 From b265380ebedb1603933251a6e8fd0e7c850eba5a Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Fri, 21 Jun 2013 14:17:09 -0300 Subject: Use an alternative method to check for file permission --- src/leap/services/eip/vpnlaunchers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 762b536d..c5b21eac 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -24,6 +24,7 @@ import getpass import os import platform import subprocess +import stat try: import grp except ImportError: @@ -167,7 +168,7 @@ def _has_updown_scripts(path, warn=True): "Might produce DNS leaks." % (path,)) # XXX check if applies in win - is_exe = os.access(path, os.X_OK) + is_exe = (stat.S_IXUSR & os.stat(path)[stat.ST_MODE] != 0) if warn and not is_exe: logger.error("Up/down script %s is not executable. " "Might produce DNS leaks." % (path,)) -- cgit v1.2.3 From 1f9acbe3366d08c280b9076274f612efabde3870 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 21 Jun 2013 17:35:46 -0300 Subject: Bugfix: return the correct gateway. After this fix we always returned the first gateway, no matter what the user asked for. --- src/leap/services/eip/eipconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index a85fe64a..ff98bf55 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -176,7 +176,7 @@ class EIPConfig(BaseConfig): index = 0 logger.warning("Provided an unknown gateway index %s, " + "defaulting to 0") - ip_addr_str = gateways[0]["ip_address"] + ip_addr_str = gateways[index]["ip_address"] try: ipaddr.IPAddress(ip_addr_str) -- cgit v1.2.3 From 4c54df049b3ef23b29c1e4e2c42201012843c8a1 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 21 Jun 2013 17:38:03 -0300 Subject: Allow to create the class using a specific offset. This is useful for testing purposes, so we can be consistent with the distance calculation. --- src/leap/services/eip/eipconfig.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index ff98bf55..e79314ce 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -39,17 +39,21 @@ class VPNGatewaySelector(object): VPN Gateway selector. """ - def __init__(self, eipconfig): + def __init__(self, eipconfig, tz_offset=None): ''' Constructor for VPNGatewaySelector. :param eipconfig: a valid EIP Configuration. :type eipconfig: EIPConfig + :param tz_offset: use this offset as a local distance to GMT. + :type tz_offset: datetime.timedelta ''' leap_assert_type(eipconfig, EIPConfig) - self._local_offset = 0 # defaults to GMT - self._local_timezone = None - self._set_local_offset() + + self._local_offset = tz_offset + if tz_offset is None: + self._local_offset = self._get_local_offset() + self._eipconfig = eipconfig def get_gateways(self): @@ -95,15 +99,17 @@ class VPNGatewaySelector(object): hours = diff.seconds / (60 * 60) return hours - def _set_local_offset(self): + def _get_local_offset(self): ''' - Sets the distance between GMT and the local timezone. + Returns the distance between GMT and the local timezone. + + :rtype: datetime.timedelta ''' local_offset = time.timezone if time.daylight: local_offset = time.altzone - self._local_offset = datetime.timedelta(seconds=-local_offset) + return datetime.timedelta(seconds=-local_offset) class EIPConfig(BaseConfig): @@ -233,6 +239,7 @@ if __name__ == "__main__": if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): print eipconfig.get_clusters() print eipconfig.get_gateways() + print eipconfig.get_locations() print eipconfig.get_openvpn_configuration() print eipconfig.get_serial() print eipconfig.get_version() -- cgit v1.2.3 From 63bc283c90a80ddc030e24fcc38c533b1e7c551d Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 24 Jun 2013 02:21:34 +0900 Subject: let errors go in their own panel --- src/leap/services/eip/vpnlaunchers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index c5b21eac..939f51d7 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -212,7 +212,7 @@ def _try_to_launch_agent(): Tries to launch a polkit daemon. """ opts = [ - "/usr/lib/policykit-1-gnome/polkit-gnome-authentication-agent-1&", + "/usr/lib/policykit-1-gnome/polkit-gnome-authentication-agent-1", # XXX add kde thing here ] for cmd in opts: -- cgit v1.2.3 From 19c90e02a65d1d6fe30e6915cd2a9440878aef83 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 24 Jun 2013 02:57:26 +0900 Subject: Improve OpenVPN Process Control * Disable toggle button until we get "connected" status. #2956 * Catch abnormal termination and update UI accordingly. #2890 --- src/leap/services/eip/vpnprocess.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnprocess.py b/src/leap/services/eip/vpnprocess.py index d9fa85a6..0ec56ae7 100644 --- a/src/leap/services/eip/vpnprocess.py +++ b/src/leap/services/eip/vpnprocess.py @@ -142,6 +142,14 @@ class VPN(object): # after running out of patience, we try a killProcess logger.debug("Process did not died. Sending a SIGKILL.") + self.killit() + + def killit(self): + """ + Sends a kill signal to the process. + """ + self._stop_pollers() + self._vpnproc.aborted = True self._vpnproc.killProcess() def terminate(self, shutdown=False): @@ -225,11 +233,20 @@ class VPNManager(object): self._reactor = reactor self._tn = None self._qtsigs = qtsigs + self._aborted = False @property def qtsigs(self): return self._qtsigs + @property + def aborted(self): + return self._aborted + + @aborted.setter + def aborted(self, value): + self._aborted = value + def _seek_to_eof(self): """ Read as much as available. Position seek pointer to end of stream @@ -378,7 +395,7 @@ class VPNManager(object): """ # TODO decide about putting a max_lim to retries and signaling # an error. - if not self.is_connected(): + if not self.aborted and not self.is_connected(): self.connect_to_management(self._socket_host, self._socket_port) self._reactor.callLater( self.CONNECTION_RETRY_TIME, @@ -611,6 +628,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): self._last_state = None self._last_status = None + self._alive = False # processProtocol methods @@ -620,6 +638,8 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa """ + self._alive = True + self.aborted = False self.try_to_connect_to_management() def outReceived(self, data): @@ -643,6 +663,8 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): exit_code = reason.value.exitCode if isinstance(exit_code, int): logger.debug("processExited, status %d" % (exit_code,)) + self.qtsigs.process_finished.emit(exit_code) + self._alive = False def processEnded(self, reason): """ @@ -661,13 +683,15 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): """ Polls connection status. """ - self.get_status() + if self._alive: + self.get_status() def pollState(self): """ Polls connection state. """ - self.get_state() + if self._alive: + self.get_state() # launcher -- cgit v1.2.3 From f9257a1bf085736592a6a8daca4d4e11dfcc1748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 24 Jun 2013 11:33:38 -0300 Subject: Catch OSError when checking for permissions --- src/leap/services/eip/vpnlaunchers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index c5b21eac..7449acd0 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -168,7 +168,11 @@ def _has_updown_scripts(path, warn=True): "Might produce DNS leaks." % (path,)) # XXX check if applies in win - is_exe = (stat.S_IXUSR & os.stat(path)[stat.ST_MODE] != 0) + is_exe = False + try: + is_exe = (stat.S_IXUSR & os.stat(path)[stat.ST_MODE] != 0) + except OSError as e: + logger.warn("%s" % (e,)) if warn and not is_exe: logger.error("Up/down script %s is not executable. " "Might produce DNS leaks." % (path,)) -- cgit v1.2.3 From edda5a3c4762c7eeb3bdeda19ddfa0c72d98f387 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Mon, 24 Jun 2013 12:21:18 -0300 Subject: Bugfix: timezone calculation. Also use int notation instead of datetime.timedelta. --- src/leap/services/eip/eipconfig.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index e79314ce..97eb3dfb 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -21,7 +21,6 @@ Provider configuration import logging import os import re -import datetime import time import ipaddr @@ -46,7 +45,7 @@ class VPNGatewaySelector(object): :param eipconfig: a valid EIP Configuration. :type eipconfig: EIPConfig :param tz_offset: use this offset as a local distance to GMT. - :type tz_offset: datetime.timedelta + :type tz_offset: int ''' leap_assert_type(eipconfig, EIPConfig) @@ -93,23 +92,29 @@ class VPNGatewaySelector(object): :returns: distance between local offset and param offset. :rtype: int ''' - delta1 = datetime.timedelta(hours=offset) - delta2 = self._local_offset - diff = abs(delta1 - delta2) - hours = diff.seconds / (60 * 60) - return hours + timezones = range(-11, 13) + tz1 = offset + tz2 = self._local_offset + distance = abs(timezones.index(tz1) - timezones.index(tz2)) + if distance > 12: + if tz1 < 0: + distance = timezones.index(tz1) + timezones[::-1].index(tz2) + else: + distance = timezones[::-1].index(tz1) + timezones.index(tz2) + + return distance def _get_local_offset(self): ''' Returns the distance between GMT and the local timezone. - :rtype: datetime.timedelta + :rtype: int ''' local_offset = time.timezone if time.daylight: local_offset = time.altzone - return datetime.timedelta(seconds=-local_offset) + return local_offset / 3600 class EIPConfig(BaseConfig): -- cgit v1.2.3 From eeb9396cc3320e43e2e2f8ff62228aa53585fdc3 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 21 Jun 2013 17:40:20 -0300 Subject: Add test for vpngatewayselector class. --- .../services/eip/tests/test_vpngatewayselector.py | 86 ++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/leap/services/eip/tests/test_vpngatewayselector.py (limited to 'src/leap/services') diff --git a/src/leap/services/eip/tests/test_vpngatewayselector.py b/src/leap/services/eip/tests/test_vpngatewayselector.py new file mode 100644 index 00000000..250e6e00 --- /dev/null +++ b/src/leap/services/eip/tests/test_vpngatewayselector.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# test_vpngatewayselector.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +tests for vpngatewayselector +""" + +import unittest + +from leap.services.eip.eipconfig import EIPConfig, VPNGatewaySelector +from leap.common.testing.basetest import BaseLeapTest +from mock import Mock + + +sample_gateways = [ + {u'host': u'gateway1.com', + u'ip_address': u'1.2.3.4', + u'location': u'location1'}, + {u'host': u'gateway2.com', + u'ip_address': u'2.3.4.5', + u'location': u'location2'}, + {u'host': u'gateway3.com', + u'ip_address': u'3.4.5.6', + u'location': u'location3'} +] + +sample_locations = { + u'location1': {u'timezone': u'2'}, + u'location2': {u'timezone': u'-7'}, + u'location3': {u'timezone': u'-4'} +} + + +class VPNGatewaySelectorTest(BaseLeapTest): + """ + VPNGatewaySelector's tests. + """ + def setUp(self): + self.eipconfig = EIPConfig() + self.eipconfig.get_gateways = Mock(return_value=sample_gateways) + self.eipconfig.get_locations = Mock(return_value=sample_locations) + + def tearDown(self): + pass + + def test_correct_order_gmt(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, 0) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [u'1.2.3.4', u'3.4.5.6', u'2.3.4.5']) + + def test_correct_order_gmt_minus_3(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, -3) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [u'3.4.5.6', u'2.3.4.5', u'1.2.3.4']) + + def test_correct_order_gmt_minus_7(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, -7) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [u'2.3.4.5', u'3.4.5.6', u'1.2.3.4']) + + def test_correct_order_gmt_plus_5(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, 5) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [u'1.2.3.4', u'3.4.5.6', u'2.3.4.5']) + + def test_correct_order_gmt_plus_10(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, 10) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [u'2.3.4.5', u'1.2.3.4', u'3.4.5.6']) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From 6873b5b5ef4d1e4af44fe41a52f402d3d9a16d5c Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 20 Jun 2013 07:52:37 +0900 Subject: use cocoasudo instead of osascript Closes:#2925 --- src/leap/services/eip/vpnlaunchers.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 56df0b1c..570a7893 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -458,12 +458,12 @@ class DarwinVPNLauncher(VPNLauncher): VPN launcher for the Darwin Platform """ - OSASCRIPT_BIN = '/usr/bin/osascript' - OSX_ASADMIN = "do shell script \"%s\" with administrator privileges" + COCOASUDO = "cocoasudo" + # XXX need magic translate for this string + SUDO_MSG = ("LEAP needs administrative privileges to run " + "Encrypted Internet.") INSTALL_PATH = "/Applications/LEAP\ Client.app" - # OPENVPN_BIN = "/%s/Contents/Resources/openvpn.leap" % ( - # self.INSTALL_PATH,) OPENVPN_BIN = 'openvpn.leap' OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,) @@ -481,9 +481,25 @@ class DarwinVPNLauncher(VPNLauncher): """ to = kls.OPENVPN_PATH cmd = "#!/bin/sh\nmkdir -p %s\ncp \"%s/\"* %s" % (to, frompath, to) - #return kls.OSX_ASADMIN % cmd return cmd + def get_cocoasudo_cmd(self): + """ + Returns a string with the cocoasudo command needed to run openvpn + as admin with a nice password prompt. The actual command needs to be + appended. + + :rtype: (str, list) + """ + iconpath = os.path.abspath(os.path.join( + os.getcwd(), + "../../../Resources/leap-client.tiff")) + has_icon = os.path.isfile(iconpath) + args = ["--icon=%s" % iconpath] if has_icon else [] + args.append("--prompt=%s" % (self.SUDO_MSG,)) + + return self.COCOASUDO, args + def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="unix"): """ @@ -597,11 +613,8 @@ class DarwinVPNLauncher(VPNLauncher): '--ca', providerconfig.get_ca_cert_path() ] - # We are using osascript until we can write a proper wrapper - # for privilege escalation. - - command = self.OSASCRIPT_BIN - cmd_args = ["-e", self.OSX_ASADMIN % (' '.join(args),)] + command, cargs = self.get_cocoasudo_cmd() + cmd_args = cargs + args logger.debug("Running VPN with command:") logger.debug("%s %s" % (command, " ".join(cmd_args))) -- cgit v1.2.3 From 38809bd51fe72a351de6eaf951df6495e6aa7541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 24 Jun 2013 20:35:27 -0300 Subject: Return the deferred so that we can use it for other things --- src/leap/services/abstractbootstrapper.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src/leap/services') diff --git a/src/leap/services/abstractbootstrapper.py b/src/leap/services/abstractbootstrapper.py index f0937197..633d818d 100644 --- a/src/leap/services/abstractbootstrapper.py +++ b/src/leap/services/abstractbootstrapper.py @@ -157,3 +157,4 @@ class AbstractBootstrapper(QtCore.QObject): d.addErrback(self._errback, signal=sig) d.addCallback(self._gui_notify, signal=sig) d.addErrback(self._gui_errback) + return d -- cgit v1.2.3 From 900543f380ec6e002023be4d5210e0d93c74ee70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 24 Jun 2013 20:44:10 -0300 Subject: Add tests for AbstractBootstrapper --- src/leap/services/tests/__init__.py | 0 .../services/tests/test_abstractbootstrapper.py | 196 +++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 src/leap/services/tests/__init__.py create mode 100644 src/leap/services/tests/test_abstractbootstrapper.py (limited to 'src/leap/services') diff --git a/src/leap/services/tests/__init__.py b/src/leap/services/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/services/tests/test_abstractbootstrapper.py b/src/leap/services/tests/test_abstractbootstrapper.py new file mode 100644 index 00000000..a9ee220f --- /dev/null +++ b/src/leap/services/tests/test_abstractbootstrapper.py @@ -0,0 +1,196 @@ +## -*- coding: utf-8 -*- +# test_abstrctbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +Tests for the Abstract Boostrapper functionality +""" + +import mock + +from PySide import QtCore + +from nose.twistedtools import deferred + +from leap.services.abstractbootstrapper import AbstractBootstrapper +from leap.util.pyside_tests_helper import UsesQApplication, BasicPySlotCase + + +class TesterBootstrapper(AbstractBootstrapper): + test_signal1 = QtCore.Signal(dict) + test_signal2 = QtCore.Signal(dict) + test_signal3 = QtCore.Signal(dict) + + ERROR_MSG = "This is a test error msg" + + def _check_that_passes(self, *args): + pass + + def _second_check_that_passes(self, *args): + pass + + def _check_that_fails(self, *args): + raise Exception(self.ERROR_MSG) + + def run_checks_pass(self): + cb_chain = [ + (self._check_that_passes, self.test_signal1), + (self._second_check_that_passes, self.test_signal2), + ] + return self.addCallbackChain(cb_chain) + + def run_second_checks_pass(self): + cb_chain = [ + (self._check_that_passes, None), + ] + return self.addCallbackChain(cb_chain) + + def run_checks_fail(self): + cb_chain = [ + (self._check_that_passes, self.test_signal1), + (self._check_that_fails, self.test_signal2) + ] + return self.addCallbackChain(cb_chain) + + def run_second_checks_fail(self): + cb_chain = [ + (self._check_that_passes, self.test_signal1), + (self._check_that_fails, self.test_signal2), + (self._second_check_that_passes, self.test_signal1) + ] + return self.addCallbackChain(cb_chain) + + def run_third_checks_fail(self): + cb_chain = [ + (self._check_that_passes, self.test_signal1), + (self._check_that_fails, None) + ] + return self.addCallbackChain(cb_chain) + + +class AbstractBootstrapperTest(UsesQApplication, BasicPySlotCase): + def setUp(self): + UsesQApplication.setUp(self) + BasicPySlotCase.setUp(self) + + self.tbt = TesterBootstrapper() + self.called1 = self.called2 = 0 + + @deferred() + def test_all_checks_executed_once(self): + self.tbt._check_that_passes = mock.MagicMock() + self.tbt._second_check_that_passes = mock.MagicMock() + + d = self.tbt.run_checks_pass() + + def check(*args): + self.tbt._check_that_passes.assert_called_once_with() + self.tbt._second_check_that_passes.\ + assert_called_once_with(None) + + d.addCallback(check) + return d + + ####################################################################### + # Dummy callbacks that test the arguments expected from a certain + # signal and only allow being called once + + def cb1(self, *args): + if tuple(self.args1) == args: + self.called1 += 1 + else: + raise ValueError('Invalid arguments for callback') + + def cb2(self, *args): + if tuple(self.args2) == args: + self.called2 += 1 + else: + raise ValueError('Invalid arguments for callback') + + # + ####################################################################### + + def _check_cb12_once(self, *args): + self.assertEquals(self.called1, 1) + self.assertEquals(self.called2, 1) + + @deferred() + def test_emits_correct(self): + self.tbt.test_signal1.connect(self.cb1) + self.tbt.test_signal2.connect(self.cb2) + d = self.tbt.run_checks_pass() + + self.args1 = [{ + AbstractBootstrapper.PASSED_KEY: True, + AbstractBootstrapper.ERROR_KEY: "" + }] + + self.args2 = self.args1 + + d.addCallback(self._check_cb12_once) + return d + + @deferred() + def test_emits_failed(self): + self.tbt.test_signal1.connect(self.cb1) + self.tbt.test_signal2.connect(self.cb2) + d = self.tbt.run_checks_fail() + + self.args1 = [{ + AbstractBootstrapper.PASSED_KEY: True, + AbstractBootstrapper.ERROR_KEY: "" + }] + + self.args2 = [{ + AbstractBootstrapper.PASSED_KEY: False, + AbstractBootstrapper.ERROR_KEY: + TesterBootstrapper.ERROR_MSG + }] + + d.addCallback(self._check_cb12_once) + return d + + @deferred() + def test_emits_failed_and_stops(self): + self.tbt.test_signal1.connect(self.cb1) + self.tbt.test_signal2.connect(self.cb2) + self.tbt.test_signal3.connect(self.cb1) + d = self.tbt.run_second_checks_fail() + + self.args1 = [{ + AbstractBootstrapper.PASSED_KEY: True, + AbstractBootstrapper.ERROR_KEY: "" + }] + + self.args2 = [{ + AbstractBootstrapper.PASSED_KEY: False, + AbstractBootstrapper.ERROR_KEY: + TesterBootstrapper.ERROR_MSG + }] + + d.addCallback(self._check_cb12_once) + return d + + @deferred() + def test_failed_without_signal(self): + d = self.tbt.run_third_checks_fail() + return d + + @deferred() + def test_sucess_without_signal(self): + d = self.tbt.run_second_checks_pass() + return d -- cgit v1.2.3 From 0fe9f43baf0d9da887d595384f100146f27f2393 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 26 Jun 2013 16:29:39 -0300 Subject: Improve VPNGatewaySelector tests coverage. Add +13 and +14 timezones support. --- src/leap/services/eip/eipconfig.py | 12 ++++- .../services/eip/tests/test_vpngatewayselector.py | 63 ++++++++++++++++++---- 2 files changed, 65 insertions(+), 10 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index 97eb3dfb..9e3a9b29 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -37,6 +37,8 @@ class VPNGatewaySelector(object): """ VPN Gateway selector. """ + # http://www.timeanddate.com/time/map/ + equivalent_timezones = {13: -11, 14: -10} def __init__(self, eipconfig, tz_offset=None): ''' @@ -51,7 +53,12 @@ class VPNGatewaySelector(object): self._local_offset = tz_offset if tz_offset is None: - self._local_offset = self._get_local_offset() + tz_offset = self._get_local_offset() + + if tz_offset in self.equivalent_timezones: + tz_offset = self.equivalent_timezones[tz_offset] + + self._local_offset = tz_offset self._eipconfig = eipconfig @@ -71,6 +78,9 @@ class VPNGatewaySelector(object): if gateway_location is not None: gw_offset = int(locations[gateway['location']]['timezone']) + if gw_offset in self.equivalent_timezones: + gw_offset = self.equivalent_timezones[gw_offset] + gateway_distance = self._get_timezone_distance(gw_offset) ip = self._eipconfig.get_gateway_ip(idx) diff --git a/src/leap/services/eip/tests/test_vpngatewayselector.py b/src/leap/services/eip/tests/test_vpngatewayselector.py index 250e6e00..c90681d7 100644 --- a/src/leap/services/eip/tests/test_vpngatewayselector.py +++ b/src/leap/services/eip/tests/test_vpngatewayselector.py @@ -34,15 +34,31 @@ sample_gateways = [ u'location': u'location2'}, {u'host': u'gateway3.com', u'ip_address': u'3.4.5.6', - u'location': u'location3'} + u'location': u'location3'}, + {u'host': u'gateway4.com', + u'ip_address': u'4.5.6.7', + u'location': u'location4'} +] + +sample_gateways_no_location = [ + {u'host': u'gateway1.com', + u'ip_address': u'1.2.3.4'}, + {u'host': u'gateway2.com', + u'ip_address': u'2.3.4.5'}, + {u'host': u'gateway3.com', + u'ip_address': u'3.4.5.6'} ] sample_locations = { u'location1': {u'timezone': u'2'}, u'location2': {u'timezone': u'-7'}, - u'location3': {u'timezone': u'-4'} + u'location3': {u'timezone': u'-4'}, + u'location4': {u'timezone': u'+13'} } +# 0 is not used, only for indexing from 1 in tests +ips = (0, u'1.2.3.4', u'2.3.4.5', u'3.4.5.6', u'4.5.6.7') + class VPNGatewaySelectorTest(BaseLeapTest): """ @@ -56,30 +72,59 @@ class VPNGatewaySelectorTest(BaseLeapTest): def tearDown(self): pass + def test_get_no_gateways(self): + gateway_selector = VPNGatewaySelector(self.eipconfig) + self.eipconfig.get_gateways = Mock(return_value=[]) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, []) + + def test_get_gateway_with_no_locations(self): + gateway_selector = VPNGatewaySelector(self.eipconfig) + self.eipconfig.get_gateways = Mock( + return_value=sample_gateways_no_location) + self.eipconfig.get_locations = Mock(return_value=[]) + gateways = gateway_selector.get_gateways() + gateways_default_order = [ + sample_gateways[0]['ip_address'], + sample_gateways[1]['ip_address'], + sample_gateways[2]['ip_address'] + ] + self.assertEqual(gateways, gateways_default_order) + def test_correct_order_gmt(self): gateway_selector = VPNGatewaySelector(self.eipconfig, 0) gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [u'1.2.3.4', u'3.4.5.6', u'2.3.4.5']) + self.assertEqual(gateways, [ips[1], ips[3], ips[2], ips[4]]) def test_correct_order_gmt_minus_3(self): gateway_selector = VPNGatewaySelector(self.eipconfig, -3) gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [u'3.4.5.6', u'2.3.4.5', u'1.2.3.4']) + self.assertEqual(gateways, [ips[3], ips[2], ips[1], ips[4]]) def test_correct_order_gmt_minus_7(self): gateway_selector = VPNGatewaySelector(self.eipconfig, -7) gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [u'2.3.4.5', u'3.4.5.6', u'1.2.3.4']) + self.assertEqual(gateways, [ips[2], ips[3], ips[4], ips[1]]) def test_correct_order_gmt_plus_5(self): gateway_selector = VPNGatewaySelector(self.eipconfig, 5) gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [u'1.2.3.4', u'3.4.5.6', u'2.3.4.5']) + self.assertEqual(gateways, [ips[1], ips[4], ips[3], ips[2]]) + + def test_correct_order_gmt_plus_12(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, 12) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [ips[4], ips[2], ips[3], ips[1]]) + + def test_correct_order_gmt_minus_11(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, -11) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [ips[4], ips[2], ips[3], ips[1]]) - def test_correct_order_gmt_plus_10(self): - gateway_selector = VPNGatewaySelector(self.eipconfig, 10) + def test_correct_order_gmt_plus_14(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, 14) gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [u'2.3.4.5', u'1.2.3.4', u'3.4.5.6']) + self.assertEqual(gateways, [ips[4], ips[2], ips[3], ips[1]]) if __name__ == "__main__": -- cgit v1.2.3 From d88f1e79fa5b6e3ec3ee7691e1c45680e85c6f12 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Tue, 25 Jun 2013 16:21:58 -0300 Subject: Refactor & add tests for eipconfig --- src/leap/services/eip/tests/test_eipconfig.py | 280 ++++++++++++++++++-------- 1 file changed, 199 insertions(+), 81 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/tests/test_eipconfig.py b/src/leap/services/eip/tests/test_eipconfig.py index 0bd19d5e..8b746b78 100644 --- a/src/leap/services/eip/tests/test_eipconfig.py +++ b/src/leap/services/eip/tests/test_eipconfig.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ -tests for eipconfig +Tests for eipconfig """ import copy import json @@ -24,6 +24,9 @@ import unittest from leap.common.testing.basetest import BaseLeapTest from leap.services.eip.eipconfig import EIPConfig +from leap.config.providerconfig import ProviderConfig + +from mock import Mock sample_config = { @@ -34,27 +37,50 @@ sample_config = { "filter_dns": True, "limited": True, "ports": [ - "1194", - "443", - "53", - "80" - ], - "protocols": [ - "tcp", - "udp"], - "transport": [ - "openvpn"], - "user_ips": False}, - "host": "host.dev.example.org", - "ip_address": "11.22.33.44", - "location": "cyberspace" - }], + "1194", + "443", + "53", + "80"], + "protocols": [ + "tcp", + "udp"], + "transport": ["openvpn"], + "user_ips": False}, + "host": "host.dev.example.org", + "ip_address": "11.22.33.44", + "location": "cyberspace" + }, { + "capabilities": { + "adblock": False, + "filter_dns": True, + "limited": True, + "ports": [ + "1194", + "443", + "53", + "80"], + "protocols": [ + "tcp", + "udp"], + "transport": ["openvpn"], + "user_ips": False}, + "host": "host2.dev.example.org", + "ip_address": "22.33.44.55", + "location": "cyberspace" + } + ], "locations": { "ankara": { - "country_code": "XX", - "hemisphere": "S", - "name": "Antarctica", - "timezone": "+2" + "country_code": "XX", + "hemisphere": "S", + "name": "Antarctica", + "timezone": "+2" + }, + "cyberspace": { + "country_code": "XX", + "hemisphere": "X", + "name": "outer space", + "timezone": "" } }, "openvpn_configuration": { @@ -70,126 +96,218 @@ sample_config = { class EIPConfigTest(BaseLeapTest): __name__ = "eip_config_tests" - #provider = "testprovider.example.org" maxDiff = None def setUp(self): - pass + self._old_ospath_exists = os.path.exists def tearDown(self): - pass + os.path.exists = self._old_ospath_exists - # - # helpers - # + def _write_config(self, data): + """ + Helper to write some data to a temp config file. - def write_config(self, data): - self.configfile = os.path.join( - self.tempdir, "eipconfig.json") + :param data: data to be used to save in the config file. + :data type: dict (valid json) + """ + self.configfile = os.path.join(self.tempdir, "eipconfig.json") conf = open(self.configfile, "w") conf.write(json.dumps(data)) conf.close() - def test_load_valid_config(self): + def _get_eipconfig(self, fromfile=True, data=sample_config): """ - load a sample config + Helper that returns an EIPConfig object using the data parameter + or a sample data. + + :param fromfile: sets if we should use a file or a string + :fromfile type: bool + :param data: sets the data to be used to load in the EIPConfig object + :data type: dict (valid json) + :rtype: EIPConfig """ - self.write_config(sample_config) config = EIPConfig() - #self.assertRaises( - #AssertionError, - #config.get_clusters) - self.assertTrue(config.load( - self.configfile, relative=False)) + loaded = False + if fromfile: + self._write_config(data) + loaded = config.load(self.configfile, relative=False) + else: + json_string = json.dumps(data) + loaded = config.load(data=json_string) + + if not loaded: + return None + + return config + + def test_loads_from_file(self): + config = self._get_eipconfig() + self.assertIsNotNone(config) + + def test_loads_from_data(self): + config = self._get_eipconfig(fromfile=False) + self.assertIsNotNone(config) + + def test_load_valid_config_from_file(self): + config = self._get_eipconfig() + self.assertIsNotNone(config) + self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) + + sample_ip = sample_config["gateways"][0]["ip_address"] self.assertEqual( config.get_gateway_ip(), - "11.22.33.44") - self.assertEqual(config.get_version(), 1) - self.assertEqual(config.get_serial(), 1) - self.assertEqual(config.get_gateways(), - sample_config["gateways"]) + sample_ip) + self.assertEqual(config.get_version(), sample_config["version"]) + self.assertEqual(config.get_serial(), sample_config["serial"]) + self.assertEqual(config.get_gateways(), sample_config["gateways"]) + self.assertEqual(config.get_locations(), sample_config["locations"]) + self.assertEqual(config.get_clusters(), None) + + def test_load_valid_config_from_data(self): + config = self._get_eipconfig(fromfile=False) + self.assertIsNotNone(config) + self.assertEqual( - config.get_clusters(), None) + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) - def test_sanitize_config(self): - """ - check the sanitization of options - """ - # extra parameters + sample_ip = sample_config["gateways"][0]["ip_address"] + self.assertEqual( + config.get_gateway_ip(), + sample_ip) + + self.assertEqual(config.get_version(), sample_config["version"]) + self.assertEqual(config.get_serial(), sample_config["serial"]) + self.assertEqual(config.get_gateways(), sample_config["gateways"]) + self.assertEqual(config.get_locations(), sample_config["locations"]) + self.assertEqual(config.get_clusters(), None) + + def test_sanitize_extra_parameters(self): data = copy.deepcopy(sample_config) data['openvpn_configuration']["extra_param"] = "FOO" - self.write_config(data) - config = EIPConfig() - config.load( - self.configfile, relative=False) + config = self._get_eipconfig(data=data) + self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) - # non allowed chars + def test_sanitize_non_allowed_chars(self): data = copy.deepcopy(sample_config) data['openvpn_configuration']["auth"] = "SHA1;" - self.write_config(data) - config = EIPConfig() - config.load(self.configfile, relative=False) + config = self._get_eipconfig(data=data) + self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) - # non allowed chars data = copy.deepcopy(sample_config) data['openvpn_configuration']["auth"] = "SHA1>`&|" - self.write_config(data) - config = EIPConfig() - config.load(self.configfile, relative=False) + config = self._get_eipconfig(data=data) + self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) - # lowercase + def test_sanitize_lowercase(self): data = copy.deepcopy(sample_config) data['openvpn_configuration']["auth"] = "shaSHA1" - self.write_config(data) - config = EIPConfig() - config.load(self.configfile, relative=False) + config = self._get_eipconfig(data=data) + self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) - # all characters invalid -> null value + def test_all_characters_invalid(self): data = copy.deepcopy(sample_config) data['openvpn_configuration']["auth"] = "sha&*!@#;" - self.write_config(data) - config = EIPConfig() - config.load(self.configfile, relative=False) + config = self._get_eipconfig(data=data) + self.assertEqual( config.get_openvpn_configuration(), {'cipher': 'AES-128-CBC', 'tls-cipher': 'DHE-RSA-AES128-SHA'}) - # bad_ip + def test_sanitize_bad_ip(self): data = copy.deepcopy(sample_config) data['gateways'][0]["ip_address"] = "11.22.33.44;" - self.write_config(data) - config = EIPConfig() - config.load(self.configfile, relative=False) - self.assertEqual( - config.get_gateway_ip(), - None) + config = self._get_eipconfig(data=data) + + self.assertEqual(config.get_gateway_ip(), None) data = copy.deepcopy(sample_config) data['gateways'][0]["ip_address"] = "11.22.33.44`" - self.write_config(data) - config = EIPConfig() - config.load(self.configfile, relative=False) - self.assertEqual( - config.get_gateway_ip(), - None) + config = self._get_eipconfig(data=data) + + self.assertEqual(config.get_gateway_ip(), None) + + def test_default_gateway_on_unknown_index(self): + config = self._get_eipconfig() + sample_ip = sample_config["gateways"][0]["ip_address"] + self.assertEqual(config.get_gateway_ip(999), sample_ip) + + def test_get_gateway_by_index(self): + config = self._get_eipconfig() + sample_ip_0 = sample_config["gateways"][0]["ip_address"] + sample_ip_1 = sample_config["gateways"][1]["ip_address"] + self.assertEqual(config.get_gateway_ip(0), sample_ip_0) + self.assertEqual(config.get_gateway_ip(1), sample_ip_1) + + def test_get_client_cert_path_as_expected(self): + config = self._get_eipconfig() + config.get_path_prefix = Mock(return_value='test') + + provider_config = ProviderConfig() + + # mock 'get_domain' so we don't need to load a config + provider_domain = 'test.provider.com' + provider_config.get_domain = Mock(return_value=provider_domain) + + expected_path = os.path.join('test', 'leap', 'providers', + provider_domain, 'keys', 'client', + 'openvpn.pem') + + # mock 'os.path.exists' so we don't get an error for unexisting file + os.path.exists = Mock(return_value=True) + cert_path = config.get_client_cert_path(provider_config) + + self.assertEqual(cert_path, expected_path) + + def test_get_client_cert_path_about_to_download(self): + config = self._get_eipconfig() + config.get_path_prefix = Mock(return_value='test') + + provider_config = ProviderConfig() + + # mock 'get_domain' so we don't need to load a config + provider_domain = 'test.provider.com' + provider_config.get_domain = Mock(return_value=provider_domain) + + expected_path = os.path.join('test', 'leap', 'providers', + provider_domain, 'keys', 'client', + 'openvpn.pem') + + cert_path = config.get_client_cert_path( + provider_config, about_to_download=True) + + self.assertEqual(cert_path, expected_path) + + def test_get_client_cert_path_fails(self): + config = self._get_eipconfig() + provider_config = ProviderConfig() + + # mock 'get_domain' so we don't need to load a config + provider_domain = 'test.provider.com' + provider_config.get_domain = Mock(return_value=provider_domain) + + with self.assertRaises(AssertionError): + config.get_client_cert_path(provider_config) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From b1429c296ee852d941a0c88e976631d03140ddee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 27 Jun 2013 09:59:57 -0300 Subject: Fix minor bugs and return defer in the run_* methods --- src/leap/services/eip/providerbootstrapper.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index e099eee7..754d0643 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -155,7 +155,7 @@ class ProviderBootstrapper(AbstractBootstrapper): if SupportedAPIs.supports(api_version): logger.debug("Provider definition has been modified") else: - api_supported = ', '.join(self._supported_api_versions) + api_supported = ', '.join(SupportedAPIs.SUPPORTED_APIS) error = ('Unsupported provider API version. ' 'Supported versions are: {}. ' 'Found: {}.').format(api_supported, api_version) @@ -185,7 +185,7 @@ class ProviderBootstrapper(AbstractBootstrapper): (self._download_provider_info, self.download_provider_info) ] - self.addCallbackChain(cb_chain) + return self.addCallbackChain(cb_chain) def _should_proceed_cert(self): """ @@ -217,6 +217,7 @@ class ProviderBootstrapper(AbstractBootstrapper): check_and_fix_urw_only( self._provider_config .get_ca_cert_path(about_to_download=True)) + return res = self._session.get(self._provider_config.get_ca_cert_uri(), verify=not self._bypass_checks) @@ -307,4 +308,4 @@ class ProviderBootstrapper(AbstractBootstrapper): (self._check_api_certificate, self.check_api_certificate) ] - self.addCallbackChain(cb_chain) + return self.addCallbackChain(cb_chain) -- cgit v1.2.3 From c66b1f7287660d349f189b7b316144e4cd72bd72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 27 Jun 2013 10:00:27 -0300 Subject: Add ProviderBootstrapper tests --- .../eip/tests/test_providerbootstrapper.py | 504 +++++++++++++++++++++ src/leap/services/eip/tests/wrongcert.pem | 33 ++ 2 files changed, 537 insertions(+) create mode 100644 src/leap/services/eip/tests/test_providerbootstrapper.py create mode 100644 src/leap/services/eip/tests/wrongcert.pem (limited to 'src/leap/services') diff --git a/src/leap/services/eip/tests/test_providerbootstrapper.py b/src/leap/services/eip/tests/test_providerbootstrapper.py new file mode 100644 index 00000000..cd740793 --- /dev/null +++ b/src/leap/services/eip/tests/test_providerbootstrapper.py @@ -0,0 +1,504 @@ +# -*- coding: utf-8 -*- +# test_providerbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +Tests for the Provider Boostrapper checks + +These will be whitebox tests since we want to make sure the private +implementation is checking what we expect. +""" + +import os +import mock +import socket +import stat +import tempfile +import time +import requests +try: + import unittest2 as unittest +except ImportError: + import unittest + +from nose.twistedtools import deferred, reactor +from twisted.internet import threads +from requests.models import Response + +from leap.common.testing.https_server import where +from leap.common.testing.basetest import BaseLeapTest +from leap.services.eip.providerbootstrapper import ProviderBootstrapper +from leap.services.eip.providerbootstrapper import UnsupportedProviderAPI +from leap.provider.supportedapis import SupportedAPIs +from leap.config.providerconfig import ProviderConfig +from leap.crypto.tests import fake_provider +from leap.common.files import mkdir_p + + +class ProviderBootstrapperTest(BaseLeapTest): + def setUp(self): + self.pb = ProviderBootstrapper() + + def tearDown(self): + pass + + def test_name_resolution_check(self): + # Something highly likely to success + self.pb._domain = "google.com" + self.pb._check_name_resolution() + # Something highly likely to fail + self.pb._domain = "uquhqweuihowquie.abc.def" + with self.assertRaises(socket.gaierror): + self.pb._check_name_resolution() + + @deferred() + def test_run_provider_select_checks(self): + self.pb._check_name_resolution = mock.MagicMock() + self.pb._check_https = mock.MagicMock() + self.pb._download_provider_info = mock.MagicMock() + + d = self.pb.run_provider_select_checks("somedomain") + + def check(*args): + self.pb._check_name_resolution.assert_called_once_with() + self.pb._check_https.assert_called_once_with(None) + self.pb._download_provider_info.assert_called_once_with(None) + d.addCallback(check) + return d + + @deferred() + def test_run_provider_setup_checks(self): + self.pb._download_ca_cert = mock.MagicMock() + self.pb._check_ca_fingerprint = mock.MagicMock() + self.pb._check_api_certificate = mock.MagicMock() + + d = self.pb.run_provider_setup_checks(ProviderConfig()) + + def check(*args): + self.pb._download_ca_cert.assert_called_once_with() + self.pb._check_ca_fingerprint.assert_called_once_with(None) + self.pb._check_api_certificate.assert_called_once_with(None) + d.addCallback(check) + return d + + def test_should_proceed_cert(self): + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=where("cacert.pem")) + + self.pb._download_if_needed = False + self.assertTrue(self.pb._should_proceed_cert()) + + self.pb._download_if_needed = True + self.assertFalse(self.pb._should_proceed_cert()) + + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=where("somefilethatdoesntexist.pem")) + self.assertTrue(self.pb._should_proceed_cert()) + + def _check_download_ca_cert(self, should_proceed): + """ + Helper to check different paths easily for the download ca + cert check + + :param should_proceed: sets the _should_proceed_cert in the + provider bootstrapper being tested + :type should_proceed: bool + + :returns: The contents of the certificate, the expected + content depending on should_proceed, and the mode of + the file to be checked by the caller + :rtype: tuple of str, str, int + """ + old_content = "NOT THE NEW CERT" + new_content = "NEW CERT" + new_cert_path = os.path.join(tempfile.mkdtemp(), + "mynewcert.pem") + + with open(new_cert_path, "w") as c: + c.write(old_content) + + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=new_cert_path) + self.pb._domain = "somedomain" + + self.pb._should_proceed_cert = mock.MagicMock( + return_value=should_proceed) + + read = None + content_to_check = None + mode = None + + with mock.patch('requests.models.Response.content', + new_callable=mock.PropertyMock) as \ + content: + content.return_value = new_content + response_obj = Response() + response_obj.raise_for_status = mock.MagicMock() + + self.pb._session.get = mock.MagicMock(return_value=response_obj) + self.pb._download_ca_cert() + with open(new_cert_path, "r") as nc: + read = nc.read() + if should_proceed: + content_to_check = new_content + else: + content_to_check = old_content + mode = stat.S_IMODE(os.stat(new_cert_path).st_mode) + + os.unlink(new_cert_path) + return read, content_to_check, mode + + def test_download_ca_cert_no_saving(self): + read, expected_read, mode = self._check_download_ca_cert(False) + self.assertEqual(read, expected_read) + self.assertEqual(mode, int("600", 8)) + + def test_download_ca_cert_saving(self): + read, expected_read, mode = self._check_download_ca_cert(True) + self.assertEqual(read, expected_read) + self.assertEqual(mode, int("600", 8)) + + def test_check_ca_fingerprint_skips(self): + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( + return_value="") + self.pb._domain = "somedomain" + + self.pb._should_proceed_cert = mock.MagicMock(return_value=False) + + self.pb._check_ca_fingerprint() + self.assertFalse(self.pb._provider_config. + get_ca_cert_fingerprint.called) + + def test_check_ca_cert_fingerprint_raises_bad_format(self): + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( + return_value="wrongfprformat!!") + self.pb._domain = "somedomain" + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + with self.assertRaises(AssertionError): + self.pb._check_ca_fingerprint() + + # This two hashes different in the last byte, but that's good enough + # for the tests + KNOWN_BAD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034efe" \ + "7dd1b910062ca323eb4da5c7f" + KNOWN_GOOD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034ef" \ + "e7dd1b910062ca323eb4da5c7e" + KNOWN_GOOD_CERT = """ +-----BEGIN CERTIFICATE----- +MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt +YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v +Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw +FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV +BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai +dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB +7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84 +CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+ +znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4 +MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4 +lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0 +bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl +DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB +lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy +YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw +XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE +MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w +DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl +cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY +k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj +RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG +htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX +EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J +aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l +mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK +G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co +Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d +69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e +yV8e +-----END CERTIFICATE----- +""" + + def _prepare_provider_config_with(self, cert_path, cert_hash): + """ + Mocks the provider config to give the cert_path and cert_hash + specified + + :param cert_path: path for the certificate + :type cert_path: str + :param cert_hash: hash for the certificate as it would appear + in the provider config json + :type cert_hash: str + """ + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( + return_value=cert_hash) + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=cert_path) + self.pb._domain = "somedomain" + + def test_check_ca_fingerprint_checksout(self): + cert_path = os.path.join(tempfile.mkdtemp(), + "mynewcert.pem") + + with open(cert_path, "w") as c: + c.write(self.KNOWN_GOOD_CERT) + + self._prepare_provider_config_with(cert_path, self.KNOWN_GOOD_HASH) + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + self.pb._check_ca_fingerprint() + + os.unlink(cert_path) + + def test_check_ca_fingerprint_fails(self): + cert_path = os.path.join(tempfile.mkdtemp(), + "mynewcert.pem") + + with open(cert_path, "w") as c: + c.write(self.KNOWN_GOOD_CERT) + + self._prepare_provider_config_with(cert_path, self.KNOWN_BAD_HASH) + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + with self.assertRaises(AssertionError): + self.pb._check_ca_fingerprint() + + os.unlink(cert_path) + + +############################################################################### +# Tests with a fake provider # +############################################################################### + +class ProviderBootstrapperActiveTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + factory = fake_provider.get_provider_factory() + http = reactor.listenTCP(8002, factory) + https = reactor.listenSSL( + 0, factory, + fake_provider.OpenSSLServerContextFactory()) + get_port = lambda p: p.getHost().port + cls.http_port = get_port(http) + cls.https_port = get_port(https) + + def setUp(self): + self.pb = ProviderBootstrapper() + + # At certain points we are going to be replacing these methods + # directly in ProviderConfig to be able to catch calls from + # new ProviderConfig objects inside the methods tested. We + # need to save the old implementation and restore it in + # tearDown so we are sure everything is as expected for each + # test. If we do it inside each specific test, a failure in + # the test will leave the implementation with the mock. + self.old_gpp = ProviderConfig.get_path_prefix + self.old_load = ProviderConfig.load + self.old_save = ProviderConfig.save + self.old_api_version = ProviderConfig.get_api_version + + def tearDown(self): + ProviderConfig.get_path_prefix = self.old_gpp + ProviderConfig.load = self.old_load + ProviderConfig.save = self.old_save + ProviderConfig.get_api_version = self.old_api_version + + def test_check_https_succeeds(self): + # XXX: Need a proper CA signed cert to test this + pass + + @deferred() + def test_check_https_fails(self): + self.pb._domain = "localhost:%s" % (self.https_port,) + + def check(*args): + with self.assertRaises(requests.exceptions.SSLError): + self.pb._check_https() + return threads.deferToThread(check) + + @deferred() + def test_second_check_https_fails(self): + self.pb._domain = "localhost:1234" + + def check(*args): + with self.assertRaises(Exception): + self.pb._check_https() + return threads.deferToThread(check) + + @deferred() + def test_check_https_succeeds_if_danger(self): + self.pb._domain = "localhost:%s" % (self.https_port,) + self.pb._bypass_checks = True + + def check(*args): + self.pb._check_https() + + return threads.deferToThread(check) + + def _setup_provider_config_with(self, api, path_prefix): + """ + Sets up the ProviderConfig with mocks for the path prefix, the + api returned and load/save methods. + It modifies ProviderConfig directly instead of an object + because the object used is created in the method itself and we + cannot control that. + + :param api: API to return + :type api: str + :param path_prefix: path prefix to be used when calculating + paths + :type path_prefix: str + """ + ProviderConfig.get_path_prefix = mock.MagicMock( + return_value=path_prefix) + ProviderConfig.get_api_version = mock.MagicMock( + return_value=api) + ProviderConfig.load = mock.MagicMock() + ProviderConfig.save = mock.MagicMock() + + def _setup_providerbootstrapper(self, ifneeded): + """ + Sets the provider bootstrapper's domain to + localhost:https_port, sets it to bypass https checks and sets + the download if needed based on the ifneeded value. + + :param ifneeded: Value for _download_if_needed + :type ifneeded: bool + """ + self.pb._domain = "localhost:%s" % (self.https_port,) + self.pb._bypass_checks = True + self.pb._download_if_needed = ifneeded + + def _produce_dummy_provider_json(self): + """ + Creates a dummy provider json on disk in order to test + behaviour around it (download if newer online, etc) + + :returns: the provider.json path used + :rtype: str + """ + provider_dir = os.path.join(ProviderConfig() + .get_path_prefix(), + "leap", + "providers", + self.pb._domain) + mkdir_p(provider_dir) + provider_path = os.path.join(provider_dir, + "provider.json") + + with open(provider_path, "w") as p: + p.write("A") + return provider_path + + def test_download_provider_info_not_modified(self): + self._setup_provider_config_with("1", tempfile.mkdtemp()) + self._setup_providerbootstrapper(True) + provider_path = self._produce_dummy_provider_json() + + # set mtime to something really new + os.utime(provider_path, (-1, time.time())) + + self.pb._download_provider_info() + # we check that it doesn't do anything with the provider + # config, because it's new enough + self.assertFalse(ProviderConfig.load.called) + self.assertFalse(ProviderConfig.save.called) + + def test_download_provider_info_modified(self): + self._setup_provider_config_with("1", tempfile.mkdtemp()) + self._setup_providerbootstrapper(True) + provider_path = self._produce_dummy_provider_json() + + # set mtime to something really old + os.utime(provider_path, (-1, 100)) + + self.pb._download_provider_info() + self.assertTrue(ProviderConfig.load.called) + self.assertTrue(ProviderConfig.save.called) + + def test_download_provider_info_unsupported_api_raises(self): + self._setup_provider_config_with("9999999", tempfile.mkdtemp()) + self._setup_providerbootstrapper(False) + self._produce_dummy_provider_json() + + with self.assertRaises(UnsupportedProviderAPI): + self.pb._download_provider_info() + + def test_download_provider_info_unsupported_api(self): + self._setup_provider_config_with(SupportedAPIs.SUPPORTED_APIS[0], + tempfile.mkdtemp()) + self._setup_providerbootstrapper(False) + self._produce_dummy_provider_json() + + self.pb._download_provider_info() + + def test_check_api_certificate_skips(self): + self.pb._provider_config = ProviderConfig() + self.pb._provider_config.get_api_uri = mock.MagicMock( + return_value="api.uri") + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value="/cert/path") + self.pb._session.get = mock.MagicMock(return_value=Response()) + + self.pb._should_proceed_cert = mock.MagicMock(return_value=False) + self.pb._check_api_certificate() + self.assertFalse(self.pb._session.get.called) + + @deferred() + def test_check_api_certificate_fails(self): + self.pb._provider_config = ProviderConfig() + self.pb._provider_config.get_api_uri = mock.MagicMock( + return_value="https://localhost:%s" % (self.https_port,)) + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=os.path.join( + os.path.split(__file__)[0], + "wrongcert.pem")) + self.pb._provider_config.get_api_version = mock.MagicMock( + return_value="1") + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + def check(*args): + with self.assertRaises(requests.exceptions.SSLError): + self.pb._check_api_certificate() + d = threads.deferToThread(check) + return d + + @deferred() + def test_check_api_certificate_succeeds(self): + self.pb._provider_config = ProviderConfig() + self.pb._provider_config.get_api_uri = mock.MagicMock( + return_value="https://localhost:%s" % (self.https_port,)) + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=where('cacert.pem')) + self.pb._provider_config.get_api_version = mock.MagicMock( + return_value="1") + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + def check(*args): + self.pb._check_api_certificate() + d = threads.deferToThread(check) + return d diff --git a/src/leap/services/eip/tests/wrongcert.pem b/src/leap/services/eip/tests/wrongcert.pem new file mode 100644 index 00000000..e6cff38a --- /dev/null +++ b/src/leap/services/eip/tests/wrongcert.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAIWZus5EIXNtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTMwNjI1MTc0NjExWhcNMTgwNjI1MTc0NjExWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEA2ObM7ESjyuxFZYD/Y68qOPQgjgggW+cdXfBpU2p4n7clsrUeMhWdW40Y +77Phzor9VOeqs3ZpHuyLzsYVp/kFDm8tKyo2ah5fJwzL0VCSLYaZkUQQ7GNUmTCk +furaxl8cQx/fg395V7/EngsS9B3/y5iHbctbA4MnH3jaotO5EGeo6hw7/eyCotQ9 +KbBV9GJMcY94FsXBCmUB+XypKklWTLhSaS6Cu4Fo8YLW6WmcnsyEOGS2F7WVf5at +7CBWFQZHaSgIBLmc818/mDYCnYmCVMFn/6Ndx7V2NTlz+HctWrQn0dmIOnCUeCwS +wXq9PnBR1rSx/WxwyF/WpyjOFkcIo7vm72kS70pfrYsXcZD4BQqkXYj3FyKnPt3O +ibLKtCxL8/83wOtErPcYpG6LgFkgAAlHQ9MkUi5dbmjCJtpqQmlZeK1RALdDPiB3 +K1KZimrGsmcE624dJxUIOJJpuwJDy21F8kh5ZAsAtE1prWETrQYNElNFjQxM83rS +ZR1Ql2MPSB4usEZT57+KvpEzlOnAT3elgCg21XrjSFGi14hCEao4g2OEZH5GAwm5 +frf6UlSRZ/g3tLTfI8Hv1prw15W2qO+7q7SBAplTODCRk+Yb0YoA2mMM/QXBUcXs +vKEDLSSxzNIBi3T62l39RB/ml+gPKo87ZMDivex1ZhrcJc3Yu3sCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUPjE+4pun+8FreIdpoR8v6N7xKtUwdQYDVR0jBG4wbIAUPjE+ +4pun+8FreIdpoR8v6N7xKtWhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCF +mbrORCFzbTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCpvCPdtvXJ +muTj379TZuCJs7/l0FhA7AHa1WAlHjsXHaA7N0+3ZWAbdtXDsowal6S+ldgU/kfV +Lq7NrRq+amJWC7SYj6cvVwhrSwSvu01fe/TWuOzHrRv1uTfJ/VXLonVufMDd9opo +bhqYxMaxLdIx6t/MYmZH4Wpiq0yfZuv//M8i7BBl/qvaWbLhg0yVAKRwjFvf59h6 +6tRFCLddELOIhLDQtk8zMbioPEbfAlKdwwP8kYGtDGj6/9/YTd/oTKRdgHuwyup3 +m0L20Y6LddC+tb0WpK5EyrNbCbEqj1L4/U7r6f/FKNA3bx6nfdXbscaMfYonKAKg +1cRrRg45sErmCz0QyTnWzXyvbjR4oQRzyW3kJ1JZudZ+AwOi00J5FYa3NiLuxl1u +gIGKWSrASQWhEdpa1nlCgX7PhdaQgYjEMpQvA0GCA0OF5JDu8en1yZqsOt1hCLIN +lkz/5jKPqrclY5hV99bE3hgCHRmIPNHCZG3wbZv2yJKxJX1YLMmQwAmSh2N7YwGG +yXRvCxQs5ChPHyRairuf/5MZCZnSVb45ppTVuNUijsbflKRUgfj/XvfqQ22f+C9N +Om2dmNvAiS2TOIfuP47CF2OUa5q4plUwmr+nyXQGM0SIoHNCj+MBdFfb3oxxAtI+ +SLhbnzQv5e84Doqz3YF0XW8jyR7q8GFLNA== +-----END CERTIFICATE----- -- cgit v1.2.3 From 14ddf421eb7ef2c39d4a375b4203cf5692402839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 27 Jun 2013 17:51:36 -0300 Subject: Change leap.common.certs.* calls to be dependent on certs This gives us the possibility of mocking up the methods inside it. Also, return the deferred from the run_* method --- src/leap/services/eip/eipbootstrapper.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 4da8f90f..b2af0aea 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -25,7 +25,7 @@ import os from PySide import QtCore from leap.common.check import leap_assert, leap_assert_type -from leap.common.certs import is_valid_pemfile, should_redownload +from leap.common import certs from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth @@ -120,7 +120,7 @@ class EIPBootstrapper(AbstractBootstrapper): # For re-download if something is wrong with the cert self._download_if_needed = self._download_if_needed and \ - not should_redownload(client_cert_path) + not certs.should_redownload(client_cert_path) if self._download_if_needed and \ os.path.exists(client_cert_path): @@ -143,9 +143,7 @@ class EIPBootstrapper(AbstractBootstrapper): res.raise_for_status() client_cert = res.content - # TODO: check certificate validity - - if not is_valid_pemfile(client_cert): + if not certs.is_valid_pemfile(client_cert): raise Exception(self.tr("The downloaded certificate is not a " "valid PEM file")) @@ -177,4 +175,4 @@ class EIPBootstrapper(AbstractBootstrapper): self.download_client_certificate) ] - self.addCallbackChain(cb_chain) + return self.addCallbackChain(cb_chain) -- cgit v1.2.3 From 6fcf0faa8b23bbdc1b9dd462d24cbb30d0c79652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 27 Jun 2013 17:52:28 -0300 Subject: Add EIPBootstrapper tests --- .../services/eip/tests/test_eipbootstrapper.py | 347 +++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 src/leap/services/eip/tests/test_eipbootstrapper.py (limited to 'src/leap/services') diff --git a/src/leap/services/eip/tests/test_eipbootstrapper.py b/src/leap/services/eip/tests/test_eipbootstrapper.py new file mode 100644 index 00000000..f2331eca --- /dev/null +++ b/src/leap/services/eip/tests/test_eipbootstrapper.py @@ -0,0 +1,347 @@ +# -*- coding: utf-8 -*- +# test_eipbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +Tests for the EIP Boostrapper checks + +These will be whitebox tests since we want to make sure the private +implementation is checking what we expect. +""" + +import os +import mock +import tempfile +import time +try: + import unittest2 as unittest +except ImportError: + import unittest + +from nose.twistedtools import deferred, reactor +from twisted.internet import threads +from requests.models import Response + +from leap.common.testing.basetest import BaseLeapTest +from leap.services.eip.eipbootstrapper import EIPBootstrapper +from leap.services.eip.eipconfig import EIPConfig +from leap.config.providerconfig import ProviderConfig +from leap.crypto.tests import fake_provider +from leap.common.files import mkdir_p +from leap.crypto.srpauth import SRPAuth + + +class EIPBootstrapperActiveTest(BaseLeapTest): + @classmethod + def setUpClass(cls): + BaseLeapTest.setUpClass() + factory = fake_provider.get_provider_factory() + http = reactor.listenTCP(0, factory) + https = reactor.listenSSL( + 0, factory, + fake_provider.OpenSSLServerContextFactory()) + get_port = lambda p: p.getHost().port + cls.http_port = get_port(http) + cls.https_port = get_port(https) + + def setUp(self): + self.eb = EIPBootstrapper() + self.old_pp = EIPConfig.get_path_prefix + self.old_save = EIPConfig.save + self.old_load = EIPConfig.load + self.old_si = SRPAuth.get_session_id + + def tearDown(self): + EIPConfig.get_path_prefix = self.old_pp + EIPConfig.save = self.old_save + EIPConfig.load = self.old_load + SRPAuth.get_session_id = self.old_si + + def _download_config_test_template(self, ifneeded, new): + """ + All download config tests have the same structure, so this is + a parametrized test for that. + + :param ifneeded: sets _download_if_needed + :type ifneeded: bool + :param new: if True uses time.time() as mtime for the mocked + eip-service file, otherwise it uses 100 (a really + old mtime) + :type new: float or int (will be coersed) + """ + pc = ProviderConfig() + pc.get_domain = mock.MagicMock( + return_value="localhost:%s" % (self.https_port)) + self.eb._provider_config = pc + + pc.get_api_uri = mock.MagicMock( + return_value="https://%s" % (pc.get_domain())) + pc.get_api_version = mock.MagicMock(return_value="1") + + # This is to ignore https checking, since it's not the point + # of this test + pc.get_ca_cert_path = mock.MagicMock(return_value=False) + + path_prefix = tempfile.mkdtemp() + EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix) + EIPConfig.save = mock.MagicMock() + EIPConfig.load = mock.MagicMock() + + self.eb._download_if_needed = ifneeded + + provider_dir = os.path.join(EIPConfig.get_path_prefix(), + "leap", + "providers", + pc.get_domain()) + mkdir_p(provider_dir) + eip_config_path = os.path.join(provider_dir, + "eip-service.json") + + with open(eip_config_path, "w") as ec: + ec.write("A") + + # set mtime to something really new + if new: + os.utime(eip_config_path, (-1, time.time())) + else: + os.utime(eip_config_path, (-1, 100)) + + @deferred() + def test_download_config_not_modified(self): + self._download_config_test_template(True, True) + + d = threads.deferToThread(self.eb._download_config) + + def check(*args): + self.assertFalse(self.eb._eip_config.save.called) + d.addCallback(check) + return d + + @deferred() + def test_download_config_modified(self): + self._download_config_test_template(True, False) + + d = threads.deferToThread(self.eb._download_config) + + def check(*args): + self.assertTrue(self.eb._eip_config.save.called) + d.addCallback(check) + return d + + @deferred() + def test_download_config_ignores_mtime(self): + self._download_config_test_template(False, True) + + d = threads.deferToThread(self.eb._download_config) + + def check(*args): + self.eb._eip_config.save.assert_called_once_with( + ["leap", + "providers", + self.eb._provider_config.get_domain(), + "eip-service.json"]) + d.addCallback(check) + return d + + def _download_certificate_test_template(self, ifneeded, createcert): + """ + All download client certificate tests have the same structure, + so this is a parametrized test for that. + + :param ifneeded: sets _download_if_needed + :type ifneeded: bool + :param createcert: if True it creates a dummy file to play the + part of a downloaded certificate + :type createcert: bool + + :returns: the temp eip cert path and the dummy cert contents + :rtype: tuple of str, str + """ + pc = ProviderConfig() + ec = EIPConfig() + self.eb._provider_config = pc + self.eb._eip_config = ec + + pc.get_domain = mock.MagicMock( + return_value="localhost:%s" % (self.https_port)) + pc.get_api_uri = mock.MagicMock( + return_value="https://%s" % (pc.get_domain())) + pc.get_api_version = mock.MagicMock(return_value="1") + pc.get_ca_cert_path = mock.MagicMock(return_value=False) + + path_prefix = tempfile.mkdtemp() + EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix) + EIPConfig.save = mock.MagicMock() + EIPConfig.load = mock.MagicMock() + + self.eb._download_if_needed = ifneeded + + provider_dir = os.path.join(EIPConfig.get_path_prefix(), + "leap", + "providers", + "somedomain") + mkdir_p(provider_dir) + eip_cert_path = os.path.join(provider_dir, + "cert") + + ec.get_client_cert_path = mock.MagicMock( + return_value=eip_cert_path) + + cert_content = "A" + if createcert: + with open(eip_cert_path, "w") as ec: + ec.write(cert_content) + + return eip_cert_path, cert_content + + def test_download_client_certificate_not_modified(self): + cert_path, old_cert_content = self._download_certificate_test_template( + True, True) + + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=False): + self.eb._download_client_certificates() + with open(cert_path, "r") as c: + self.assertEqual(c.read(), old_cert_content) + + @deferred() + def test_download_client_certificate_old_cert(self): + cert_path, old_cert_content = self._download_certificate_test_template( + True, True) + + def wrapper(*args): + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=True): + with mock.patch('leap.common.certs.is_valid_pemfile', + new_callable=mock.MagicMock, + return_value=True): + self.eb._download_client_certificates() + + def check(*args): + with open(cert_path, "r") as c: + self.assertNotEqual(c.read(), old_cert_content) + d = threads.deferToThread(wrapper) + d.addCallback(check) + + return d + + @deferred() + def test_download_client_certificate_no_cert(self): + cert_path, _ = self._download_certificate_test_template( + True, False) + + def wrapper(*args): + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=False): + with mock.patch('leap.common.certs.is_valid_pemfile', + new_callable=mock.MagicMock, + return_value=True): + self.eb._download_client_certificates() + + def check(*args): + self.assertTrue(os.path.exists(cert_path)) + d = threads.deferToThread(wrapper) + d.addCallback(check) + + return d + + @deferred() + def test_download_client_certificate_force_not_valid(self): + cert_path, old_cert_content = self._download_certificate_test_template( + True, True) + + def wrapper(*args): + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=True): + with mock.patch('leap.common.certs.is_valid_pemfile', + new_callable=mock.MagicMock, + return_value=True): + self.eb._download_client_certificates() + + def check(*args): + with open(cert_path, "r") as c: + self.assertNotEqual(c.read(), old_cert_content) + d = threads.deferToThread(wrapper) + d.addCallback(check) + + return d + + @deferred() + def test_download_client_certificate_invalid_download(self): + cert_path, _ = self._download_certificate_test_template( + False, False) + + def wrapper(*args): + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=True): + with mock.patch('leap.common.certs.is_valid_pemfile', + new_callable=mock.MagicMock, + return_value=False): + with self.assertRaises(Exception): + self.eb._download_client_certificates() + d = threads.deferToThread(wrapper) + + return d + + @deferred() + def test_download_client_certificate_uses_session_id(self): + _, _ = self._download_certificate_test_template( + False, False) + + SRPAuth.get_session_id = mock.MagicMock(return_value="1") + + def check_cookie(*args, **kwargs): + cookies = kwargs.get("cookies", None) + self.assertEqual(cookies, {'_session_id': '1'}) + return Response() + + def wrapper(*args): + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=False): + with mock.patch('leap.common.certs.is_valid_pemfile', + new_callable=mock.MagicMock, + return_value=True): + with mock.patch('requests.sessions.Session.get', + new_callable=mock.MagicMock, + side_effect=check_cookie): + with mock.patch('requests.models.Response.content', + new_callable=mock.PropertyMock, + return_value="A"): + self.eb._download_client_certificates() + + d = threads.deferToThread(wrapper) + + return d + + @deferred() + def test_run_eip_setup_checks(self): + self.eb._download_config = mock.MagicMock() + self.eb._download_client_certificates = mock.MagicMock() + + d = self.eb.run_eip_setup_checks(ProviderConfig()) + + def check(*args): + self.eb._download_config.assert_called_once_with() + self.eb._download_client_certificates.assert_called_once_with(None) + d.addCallback(check) + return d -- cgit v1.2.3 From 8ff98b63c077af25fb58dc73750e7765c35ce2f0 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 28 Jun 2013 10:59:56 -0300 Subject: Bugfix: Update available/supported implementation. Closes bug #3032. --- src/leap/services/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/leap/services') diff --git a/src/leap/services/__init__.py b/src/leap/services/__init__.py index 70a5dcf2..fc4aa416 100644 --- a/src/leap/services/__init__.py +++ b/src/leap/services/__init__.py @@ -17,10 +17,10 @@ """ Services module. """ -NOT_YET_DEPLOYED = ["mx"] # for 0.2.2 release +DEPLOYED = ["openvpn"] # for 0.2.2 release -def get_available(services): +def get_supported(services): """ Returns a list of the available services. @@ -30,4 +30,4 @@ def get_available(services): :returns: a list of the available services :rtype: list of str """ - return filter(lambda s: s not in NOT_YET_DEPLOYED, services) + return filter(lambda s: s in DEPLOYED, services) -- cgit v1.2.3