summaryrefslogtreecommitdiff
path: root/src/leap
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap')
-rw-r--r--src/leap/services/__init__.py0
-rw-r--r--src/leap/services/eip/__init__.py0
-rw-r--r--src/leap/services/eip/eipbootstrapper.py315
-rw-r--r--src/leap/services/eip/eipconfig.py123
-rw-r--r--src/leap/services/eip/eipspec.py63
-rw-r--r--src/leap/services/eip/providerbootstrapper.py520
-rw-r--r--src/leap/services/eip/udstelnet.py61
-rw-r--r--src/leap/services/eip/vpn.py359
-rw-r--r--src/leap/services/eip/vpnlaunchers.py270
9 files changed, 1711 insertions, 0 deletions
diff --git a/src/leap/services/__init__.py b/src/leap/services/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/leap/services/__init__.py
diff --git a/src/leap/services/eip/__init__.py b/src/leap/services/eip/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/leap/services/eip/__init__.py
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 <http://www.gnu.org/licenses/>.
+
+"""
+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 <http://www.gnu.org/licenses/>.
+
+"""
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+"""
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+"""
+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 <http://www.gnu.org/licenses/>.
+
+"""
+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")