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 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