summaryrefslogtreecommitdiff
path: root/src/leap/bitmask/services
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/bitmask/services')
-rw-r--r--src/leap/bitmask/services/__init__.py12
-rw-r--r--src/leap/bitmask/services/connections.py1
-rw-r--r--src/leap/bitmask/services/eip/__init__.py27
-rw-r--r--src/leap/bitmask/services/eip/connection.py1
-rw-r--r--src/leap/bitmask/services/eip/darwinvpnlauncher.py190
-rw-r--r--src/leap/bitmask/services/eip/eipbootstrapper.py1
-rw-r--r--src/leap/bitmask/services/eip/eipconfig.py40
-rw-r--r--src/leap/bitmask/services/eip/linuxvpnlauncher.py232
-rw-r--r--src/leap/bitmask/services/eip/providerbootstrapper.py340
-rw-r--r--src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py17
-rw-r--r--src/leap/bitmask/services/eip/tests/test_eipconfig.py12
-rw-r--r--src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py560
-rw-r--r--src/leap/bitmask/services/eip/vpnlauncher.py290
-rw-r--r--src/leap/bitmask/services/eip/vpnlaunchers.py963
-rw-r--r--src/leap/bitmask/services/eip/vpnprocess.py4
-rw-r--r--src/leap/bitmask/services/eip/windowsvpnlauncher.py69
-rw-r--r--src/leap/bitmask/services/soledad/soledadbootstrapper.py366
17 files changed, 1113 insertions, 2012 deletions
diff --git a/src/leap/bitmask/services/__init__.py b/src/leap/bitmask/services/__init__.py
index afce72f6..f9456159 100644
--- a/src/leap/bitmask/services/__init__.py
+++ b/src/leap/bitmask/services/__init__.py
@@ -27,7 +27,7 @@ from leap.bitmask.crypto.srpauth import SRPAuth
from leap.bitmask.util.constants import REQUEST_TIMEOUT
from leap.bitmask.util.privilege_policies import is_missing_policy_permissions
from leap.bitmask.util.request_helpers import get_content
-from leap.bitmask.util import get_path_prefix
+from leap.bitmask import util
from leap.common.check import leap_assert
from leap.common.config.baseconfig import BaseConfig
@@ -105,10 +105,11 @@ def download_service_config(provider_config, service_config,
service_name = service_config.name
service_json = "{0}-service.json".format(service_name)
headers = {}
- mtime = get_mtime(os.path.join(get_path_prefix(),
+ mtime = get_mtime(os.path.join(util.get_path_prefix(),
"leap", "providers",
provider_config.get_domain(),
service_json))
+
if download_if_needed and mtime:
headers['if-modified-since'] = mtime
@@ -125,10 +126,15 @@ def download_service_config(provider_config, service_config,
# XXX make and use @with_srp_auth decorator
srp_auth = SRPAuth(provider_config)
session_id = srp_auth.get_session_id()
+ token = srp_auth.get_token()
cookies = None
- if session_id:
+ if session_id is not None:
cookies = {"_session_id": session_id}
+ # API v2 will only support token auth, but in v1 we can send both
+ if token is not None:
+ headers["Authorization"] = 'Token token="{0}"'.format(token)
+
res = session.get(config_uri,
verify=provider_config.get_ca_cert_path(),
headers=headers,
diff --git a/src/leap/bitmask/services/connections.py b/src/leap/bitmask/services/connections.py
index f3ab9e8e..8aeb4e0c 100644
--- a/src/leap/bitmask/services/connections.py
+++ b/src/leap/bitmask/services/connections.py
@@ -103,6 +103,7 @@ class AbstractLEAPConnection(object):
# Bypass stages
connection_died_signal = None
+ connection_aborted_signal = None
class Disconnected(State):
"""Disconnected state"""
diff --git a/src/leap/bitmask/services/eip/__init__.py b/src/leap/bitmask/services/eip/__init__.py
index dd010027..6030cac3 100644
--- a/src/leap/bitmask/services/eip/__init__.py
+++ b/src/leap/bitmask/services/eip/__init__.py
@@ -20,7 +20,11 @@ leap.bitmask.services.eip module initialization
import os
import tempfile
-from leap.bitmask.platform_init import IS_WIN
+from leap.bitmask.services.eip.darwinvpnlauncher import DarwinVPNLauncher
+from leap.bitmask.services.eip.linuxvpnlauncher import LinuxVPNLauncher
+from leap.bitmask.services.eip.windowsvpnlauncher import WindowsVPNLauncher
+from leap.bitmask.platform_init import IS_LINUX, IS_MAC, IS_WIN
+from leap.common.check import leap_assert
def get_openvpn_management():
@@ -40,3 +44,24 @@ def get_openvpn_management():
port = "unix"
return host, port
+
+
+def get_vpn_launcher():
+ """
+ Return the VPN launcher for the current platform.
+ """
+ if not (IS_LINUX or IS_MAC or IS_WIN):
+ error_msg = "VPN Launcher not implemented for this platform."
+ raise NotImplementedError(error_msg)
+
+ launcher = None
+ if IS_LINUX:
+ launcher = LinuxVPNLauncher
+ elif IS_MAC:
+ launcher = DarwinVPNLauncher
+ elif IS_WIN:
+ launcher = WindowsVPNLauncher
+
+ leap_assert(launcher is not None)
+
+ return launcher()
diff --git a/src/leap/bitmask/services/eip/connection.py b/src/leap/bitmask/services/eip/connection.py
index 5f05ba07..08b29070 100644
--- a/src/leap/bitmask/services/eip/connection.py
+++ b/src/leap/bitmask/services/eip/connection.py
@@ -40,6 +40,7 @@ class EIPConnectionSignals(QtCore.QObject):
disconnected_signal = QtCore.Signal()
connection_died_signal = QtCore.Signal()
+ connection_aborted_signal = QtCore.Signal()
class EIPConnection(AbstractLEAPConnection):
diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py
new file mode 100644
index 00000000..f3b6bfc8
--- /dev/null
+++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+# darwinvpnlauncher.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/>.
+"""
+Darwin VPN launcher implementation.
+"""
+import commands
+import getpass
+import logging
+import os
+
+from leap.bitmask.services.eip.vpnlauncher import VPNLauncher
+from leap.bitmask.services.eip.vpnlauncher import VPNLauncherException
+from leap.bitmask.util import get_path_prefix
+
+logger = logging.getLogger(__name__)
+
+
+class EIPNoTunKextLoaded(VPNLauncherException):
+ pass
+
+
+class DarwinVPNLauncher(VPNLauncher):
+ """
+ VPN launcher for the Darwin Platform
+ """
+ COCOASUDO = "cocoasudo"
+ # XXX need the good old magic translate for these strings
+ # (look for magic in 0.2.0 release)
+ SUDO_MSG = ("Bitmask needs administrative privileges to run "
+ "Encrypted Internet.")
+ INSTALL_MSG = ("\"Bitmask needs administrative privileges to install "
+ "missing scripts and fix permissions.\"")
+
+ INSTALL_PATH = os.path.realpath(os.getcwd() + "/../../")
+ INSTALL_PATH_ESCAPED = os.path.realpath(os.getcwd() + "/../../")
+ OPENVPN_BIN = 'openvpn.leap'
+ OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,)
+ OPENVPN_PATH_ESCAPED = "%s/Contents/Resources/openvpn" % (
+ INSTALL_PATH_ESCAPED,)
+
+ UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,)
+ DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,)
+ OPENVPN_DOWN_PLUGIN = '%s/openvpn-down-root.so' % (OPENVPN_PATH,)
+
+ UPDOWN_FILES = (UP_SCRIPT, DOWN_SCRIPT, OPENVPN_DOWN_PLUGIN)
+ OTHER_FILES = []
+
+ @classmethod
+ def cmd_for_missing_scripts(kls, frompath):
+ """
+ Returns a command that can copy the missing scripts.
+ :rtype: str
+ """
+ to = kls.OPENVPN_PATH_ESCAPED
+
+ cmd = "#!/bin/sh\n"
+ cmd += "mkdir -p {0}\n".format(to)
+ cmd += "cp '{0}'/* {1}\n".format(frompath, to)
+ cmd += "chmod 744 {0}/*".format(to)
+
+ return cmd
+
+ @classmethod
+ def is_kext_loaded(kls):
+ """
+ Checks if the needed kext is loaded before launching openvpn.
+
+ :returns: True if kext is loaded, False otherwise.
+ :rtype: bool
+ """
+ return bool(commands.getoutput('kextstat | grep "leap.tun"'))
+
+ @classmethod
+ def _get_icon_path(kls):
+ """
+ Returns the absolute path to the app icon.
+
+ :rtype: str
+ """
+ resources_path = os.path.abspath(
+ os.path.join(os.getcwd(), "../../Contents/Resources"))
+
+ return os.path.join(resources_path, "leap-client.tiff")
+
+ @classmethod
+ def get_cocoasudo_ovpn_cmd(kls):
+ """
+ Returns a string with the cocoasudo command needed to run openvpn
+ as admin with a nice password prompt. The actual command needs to be
+ appended.
+
+ :rtype: (str, list)
+ """
+ # TODO add translation support for this
+ sudo_msg = ("Bitmask needs administrative privileges to run "
+ "Encrypted Internet.")
+ iconpath = kls._get_icon_path()
+ has_icon = os.path.isfile(iconpath)
+ args = ["--icon=%s" % iconpath] if has_icon else []
+ args.append("--prompt=%s" % (sudo_msg,))
+
+ return kls.COCOASUDO, args
+
+ @classmethod
+ def get_cocoasudo_installmissing_cmd(kls):
+ """
+ Returns a string with the cocoasudo command needed to install missing
+ files as admin with a nice password prompt. The actual command needs to
+ be appended.
+
+ :rtype: (str, list)
+ """
+ # TODO add translation support for this
+ install_msg = ('"Bitmask needs administrative privileges to install '
+ 'missing scripts and fix permissions."')
+ iconpath = kls._get_icon_path()
+ has_icon = os.path.isfile(iconpath)
+ args = ["--icon=%s" % iconpath] if has_icon else []
+ args.append("--prompt=%s" % (install_msg,))
+
+ return kls.COCOASUDO, args
+
+ @classmethod
+ def get_vpn_command(kls, eipconfig, providerconfig, socket_host,
+ socket_port="unix", openvpn_verb=1):
+ """
+ Returns the OSX implementation for the vpn launching command.
+
+ Might raise:
+ EIPNoTunKextLoaded,
+ OpenVPNNotFoundException,
+ VPNLauncherException.
+
+ :param eipconfig: eip configuration object
+ :type eipconfig: EIPConfig
+ :param providerconfig: provider specific configuration
+ :type providerconfig: ProviderConfig
+ :param socket_host: either socket path (unix) or socket IP
+ :type socket_host: str
+ :param socket_port: either string "unix" if it's a unix socket,
+ or port otherwise
+ :type socket_port: str
+ :param openvpn_verb: the openvpn verbosity wanted
+ :type openvpn_verb: int
+
+ :return: A VPN command ready to be launched.
+ :rtype: list
+ """
+ if not kls.is_kext_loaded():
+ raise EIPNoTunKextLoaded
+
+ # we use `super` in order to send the class to use
+ command = super(DarwinVPNLauncher, kls).get_vpn_command(
+ eipconfig, providerconfig, socket_host, socket_port, openvpn_verb)
+
+ cocoa, cargs = kls.get_cocoasudo_ovpn_cmd()
+ cargs.extend(command)
+ command = cargs
+ command.insert(0, cocoa)
+
+ command.extend(['--setenv', "LEAPUSER", getpass.getuser()])
+
+ return command
+
+ @classmethod
+ def get_vpn_env(kls):
+ """
+ Returns a dictionary with the custom env for the platform.
+ This is mainly used for setting LD_LIBRARY_PATH to the correct
+ path when distributing a standalone client
+
+ :rtype: dict
+ """
+ return {
+ "DYLD_LIBRARY_PATH": os.path.join(get_path_prefix(), "..", "lib")
+ }
diff --git a/src/leap/bitmask/services/eip/eipbootstrapper.py b/src/leap/bitmask/services/eip/eipbootstrapper.py
index 885c4420..5a238a1c 100644
--- a/src/leap/bitmask/services/eip/eipbootstrapper.py
+++ b/src/leap/bitmask/services/eip/eipbootstrapper.py
@@ -28,7 +28,6 @@ from leap.bitmask.services import download_service_config
from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper
from leap.bitmask.services.eip.eipconfig import EIPConfig
from leap.common import certs as leap_certs
-from leap.bitmask.util import get_path_prefix
from leap.common.check import leap_assert, leap_assert_type
from leap.common.files import check_and_fix_urw_only
diff --git a/src/leap/bitmask/services/eip/eipconfig.py b/src/leap/bitmask/services/eip/eipconfig.py
index 7d8995b4..16ed4cc0 100644
--- a/src/leap/bitmask/services/eip/eipconfig.py
+++ b/src/leap/bitmask/services/eip/eipconfig.py
@@ -33,6 +33,45 @@ from leap.common.check import leap_assert, leap_assert_type
logger = logging.getLogger(__name__)
+def get_eipconfig_path(domain):
+ """
+ Returns relative path for EIP config.
+
+ :param domain: the domain to which this eipconfig belongs to.
+ :type domain: str
+ :returns: the path
+ :rtype: str
+ """
+ leap_assert(domain is not None, "get_eipconfig_path: We need a domain")
+ return os.path.join("leap", "providers", domain, "eip-service.json")
+
+
+def load_eipconfig_if_needed(provider_config, eip_config, domain):
+ """
+ Utility function to prime a eip_config object from a loaded
+ provider_config and the chosen provider domain.
+
+ :param provider_config: a loaded instance of ProviderConfig
+ :type provider_config: ProviderConfig
+
+ :param eip_config: the eipconfig object to be primed.
+ :type eip_config: EIPConfig
+
+ :param domain: the chosen provider domain
+ :type domain: str
+
+ :returns: Whether the eip_config object has been succesfully loaded
+ :rtype: bool
+ """
+ loaded = eip_config.loaded()
+ if not loaded:
+ eip_config_path = get_eipconfig_path(domain)
+ api_version = provider_config.get_api_version()
+ eip_config.set_api_version(api_version)
+ loaded = eip_config.load(eip_config_path)
+ return loaded
+
+
class VPNGatewaySelector(object):
"""
VPN Gateway selector.
@@ -59,7 +98,6 @@ class VPNGatewaySelector(object):
tz_offset = self.equivalent_timezones[tz_offset]
self._local_offset = tz_offset
-
self._eipconfig = eipconfig
def get_gateways_list(self):
diff --git a/src/leap/bitmask/services/eip/linuxvpnlauncher.py b/src/leap/bitmask/services/eip/linuxvpnlauncher.py
new file mode 100644
index 00000000..c2c28627
--- /dev/null
+++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py
@@ -0,0 +1,232 @@
+# -*- coding: utf-8 -*-
+# linuxvpnlauncher.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/>.
+"""
+Linux VPN launcher implementation.
+"""
+import commands
+import logging
+import os
+import subprocess
+import time
+
+from leap.bitmask.config import flags
+from leap.bitmask.util import privilege_policies
+from leap.bitmask.util.privilege_policies import LinuxPolicyChecker
+from leap.common.files import which
+from leap.bitmask.services.eip.vpnlauncher import VPNLauncher
+from leap.bitmask.services.eip.vpnlauncher import VPNLauncherException
+from leap.bitmask.util import get_path_prefix
+from leap.common.check import leap_assert
+from leap.bitmask.util import first
+
+logger = logging.getLogger(__name__)
+
+
+class EIPNoPolkitAuthAgentAvailable(VPNLauncherException):
+ pass
+
+
+class EIPNoPkexecAvailable(VPNLauncherException):
+ pass
+
+
+def _is_pkexec_in_system():
+ """
+ Checks the existence of the pkexec binary in system.
+ """
+ pkexec_path = which('pkexec')
+ if len(pkexec_path) == 0:
+ return False
+ return True
+
+
+def _is_auth_agent_running():
+ """
+ Checks if a polkit daemon is running.
+
+ :return: True if it's running, False if it's not.
+ :rtype: boolean
+ """
+ ps = 'ps aux | grep polkit-%s-authentication-agent-1'
+ opts = (ps % case for case in ['[g]nome', '[k]de'])
+ is_running = map(lambda l: commands.getoutput(l), opts)
+ return any(is_running)
+
+
+def _try_to_launch_agent():
+ """
+ Tries to launch a polkit daemon.
+ """
+ env = None
+ if flags.STANDALONE is True:
+ env = {"PYTHONPATH": os.path.abspath('../../../../lib/')}
+ try:
+ # We need to quote the command because subprocess call
+ # will do "sh -c 'foo'", so if we do not quoute it we'll end
+ # up with a invocation to the python interpreter. And that
+ # is bad.
+ subprocess.call(["python -m leap.bitmask.util.polkit_agent"],
+ shell=True, env=env)
+ except Exception as exc:
+ logger.exception(exc)
+
+
+class LinuxVPNLauncher(VPNLauncher):
+ PKEXEC_BIN = 'pkexec'
+ OPENVPN_BIN = 'openvpn'
+ OPENVPN_BIN_PATH = os.path.join(
+ get_path_prefix(), "..", "apps", "eip", OPENVPN_BIN)
+
+ SYSTEM_CONFIG = "/etc/leap"
+ UP_DOWN_FILE = "resolv-update"
+ UP_DOWN_PATH = "%s/%s" % (SYSTEM_CONFIG, UP_DOWN_FILE)
+
+ # We assume this is there by our openvpn dependency, and
+ # we will put it there on the bundle too.
+ # TODO adapt to the bundle path.
+ OPENVPN_DOWN_ROOT_BASE = "/usr/lib/openvpn/"
+ OPENVPN_DOWN_ROOT_FILE = "openvpn-plugin-down-root.so"
+ OPENVPN_DOWN_ROOT_PATH = "%s/%s" % (
+ OPENVPN_DOWN_ROOT_BASE,
+ OPENVPN_DOWN_ROOT_FILE)
+
+ UP_SCRIPT = DOWN_SCRIPT = UP_DOWN_PATH
+ UPDOWN_FILES = (UP_DOWN_PATH,)
+ POLKIT_PATH = LinuxPolicyChecker.get_polkit_path()
+ OTHER_FILES = (POLKIT_PATH, )
+
+ @classmethod
+ def maybe_pkexec(kls):
+ """
+ Checks whether pkexec is available in the system, and
+ returns the path if found.
+
+ Might raise:
+ EIPNoPkexecAvailable,
+ EIPNoPolkitAuthAgentAvailable.
+
+ :returns: a list of the paths where pkexec is to be found
+ :rtype: list
+ """
+ if _is_pkexec_in_system():
+ if not _is_auth_agent_running():
+ _try_to_launch_agent()
+ time.sleep(0.5)
+ if _is_auth_agent_running():
+ pkexec_possibilities = which(kls.PKEXEC_BIN)
+ leap_assert(len(pkexec_possibilities) > 0,
+ "We couldn't find pkexec")
+ return pkexec_possibilities
+ else:
+ logger.warning("No polkit auth agent found. pkexec " +
+ "will use its own auth agent.")
+ raise EIPNoPolkitAuthAgentAvailable()
+ else:
+ logger.warning("System has no pkexec")
+ raise EIPNoPkexecAvailable()
+
+ @classmethod
+ def missing_other_files(kls):
+ """
+ 'Extend' the VPNLauncher's missing_other_files to check if the polkit
+ files is outdated. If the polkit file that is in OTHER_FILES exists but
+ is not up to date, it is added to the missing list.
+
+ :returns: a list of missing files
+ :rtype: list of str
+ """
+ # we use `super` in order to send the class to use
+ missing = super(LinuxVPNLauncher, kls).missing_other_files()
+ polkit_file = LinuxPolicyChecker.get_polkit_path()
+ if polkit_file not in missing:
+ if privilege_policies.is_policy_outdated(kls.OPENVPN_BIN_PATH):
+ missing.append(polkit_file)
+
+ return missing
+
+ @classmethod
+ def get_vpn_command(kls, eipconfig, providerconfig, socket_host,
+ socket_port="unix", openvpn_verb=1):
+ """
+ Returns the Linux implementation for the vpn launching command.
+
+ Might raise:
+ EIPNoPkexecAvailable,
+ EIPNoPolkitAuthAgentAvailable,
+ OpenVPNNotFoundException,
+ VPNLauncherException.
+
+ :param eipconfig: eip configuration object
+ :type eipconfig: EIPConfig
+ :param providerconfig: provider specific configuration
+ :type providerconfig: ProviderConfig
+ :param socket_host: either socket path (unix) or socket IP
+ :type socket_host: str
+ :param socket_port: either string "unix" if it's a unix socket,
+ or port otherwise
+ :type socket_port: str
+ :param openvpn_verb: the openvpn verbosity wanted
+ :type openvpn_verb: int
+
+ :return: A VPN command ready to be launched.
+ :rtype: list
+ """
+ # we use `super` in order to send the class to use
+ command = super(LinuxVPNLauncher, kls).get_vpn_command(
+ eipconfig, providerconfig, socket_host, socket_port, openvpn_verb)
+
+ pkexec = kls.maybe_pkexec()
+ if pkexec:
+ command.insert(0, first(pkexec))
+
+ return command
+
+ @classmethod
+ def cmd_for_missing_scripts(kls, frompath, pol_file):
+ """
+ Returns a sh script that can copy the missing files.
+
+ :param frompath: The path where the up/down scripts live
+ :type frompath: str
+ :param pol_file: The path where the dynamically generated
+ policy file lives
+ :type pol_file: str
+
+ :rtype: str
+ """
+ to = kls.SYSTEM_CONFIG
+
+ cmd = '#!/bin/sh\n'
+ cmd += 'mkdir -p "%s"\n' % (to, )
+ cmd += 'cp "%s/%s" "%s"\n' % (frompath, kls.UP_DOWN_FILE, to)
+ cmd += 'cp "%s" "%s"\n' % (pol_file, kls.POLKIT_PATH)
+ cmd += 'chmod 644 "%s"\n' % (kls.POLKIT_PATH, )
+
+ return cmd
+
+ @classmethod
+ def get_vpn_env(kls):
+ """
+ Returns a dictionary with the custom env for the platform.
+ This is mainly used for setting LD_LIBRARY_PATH to the correct
+ path when distributing a standalone client
+
+ :rtype: dict
+ """
+ return {
+ "LD_LIBRARY_PATH": os.path.join(get_path_prefix(), "..", "lib")
+ }
diff --git a/src/leap/bitmask/services/eip/providerbootstrapper.py b/src/leap/bitmask/services/eip/providerbootstrapper.py
deleted file mode 100644
index 3b7c9899..00000000
--- a/src/leap/bitmask/services/eip/providerbootstrapper.py
+++ /dev/null
@@ -1,340 +0,0 @@
-# -*- 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 logging
-import socket
-import os
-
-import requests
-
-from PySide import QtCore
-
-from leap.bitmask.config.providerconfig import ProviderConfig, MissingCACert
-from leap.bitmask.util.request_helpers import get_content
-from leap.bitmask.util import get_path_prefix
-from leap.bitmask.util.constants import REQUEST_TIMEOUT
-from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper
-from leap.bitmask.provider.supportedapis import SupportedAPIs
-from leap.common.certs import get_digest
-from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p
-from leap.common.check import leap_assert, leap_assert_type, leap_check
-
-
-logger = logging.getLogger(__name__)
-
-
-class UnsupportedProviderAPI(Exception):
- """
- Raised when attempting to use a provider with an incompatible API.
- """
- pass
-
-
-class WrongFingerprint(Exception):
- """
- Raised when a fingerprint comparison does not match.
- """
- pass
-
-
-class ProviderBootstrapper(AbstractBootstrapper):
- """
- Given a provider URL performs a series of checks and emits signals
- after they are passed.
- If a check fails, the subsequent checks are not executed
- """
-
- # 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, bypass_checks=False):
- """
- Constructor for provider bootstrapper object
-
- :param bypass_checks: Set to true if the app should bypass
- first round of checks for CA certificates at bootstrap
- :type bypass_checks: bool
- """
- AbstractBootstrapper.__init__(self, bypass_checks)
-
- self._domain = None
- self._provider_config = None
- self._download_if_needed = False
-
- def _check_name_resolution(self):
- """
- Checks that the name resolution for the provider name works
- """
- leap_assert(self._domain, "Cannot check DNS without a domain")
-
- logger.debug("Checking name resolution for %s" % (self._domain))
-
- # We don't skip this check, since it's basic for the whole
- # system to work
- socket.gethostbyname(self._domain)
-
- def _check_https(self, *args):
- """
- Checks that https is working and that the provided certificate
- checks out
- """
-
- leap_assert(self._domain, "Cannot check HTTPS without a domain")
-
- logger.debug("Checking https for %s" % (self._domain))
-
- # 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,),
- verify=not self._bypass_checks,
- timeout=REQUEST_TIMEOUT)
- res.raise_for_status()
- except requests.exceptions.SSLError:
- self._err_msg = self.tr("Provider certificate could "
- "not be verified")
- raise
- except Exception:
- self._err_msg = self.tr("Provider does not support HTTPS")
- raise
-
- def _download_provider_info(self, *args):
- """
- Downloads the provider.json defition
- """
- leap_assert(self._domain,
- "Cannot download provider info without a domain")
-
- logger.debug("Downloading provider info for %s" % (self._domain))
-
- headers = {}
-
- provider_json = os.path.join(get_path_prefix(), "leap", "providers",
- self._domain, "provider.json")
- mtime = get_mtime(provider_json)
-
- if self._download_if_needed and mtime:
- headers['if-modified-since'] = mtime
-
- uri = "https://%s/%s" % (self._domain, "provider.json")
- verify = not self._bypass_checks
-
- if mtime: # the provider.json exists
- provider_config = ProviderConfig()
- provider_config.load(provider_json)
- try:
- verify = provider_config.get_ca_cert_path()
- uri = provider_config.get_api_uri() + '/provider.json'
- except MissingCACert:
- # get_ca_cert_path fails if the certificate does not exists.
- pass
-
- logger.debug("Requesting for provider.json... "
- "uri: {0}, verify: {1}, headers: {2}".format(
- uri, verify, headers))
- res = self._session.get(uri, verify=verify,
- headers=headers, timeout=REQUEST_TIMEOUT)
- res.raise_for_status()
- logger.debug("Request status code: {0}".format(res.status_code))
-
- # Not modified
- if res.status_code == 304:
- logger.debug("Provider definition has not been modified")
- else:
- provider_definition, mtime = get_content(res)
-
- provider_config = ProviderConfig()
- provider_config.load(data=provider_definition, mtime=mtime)
- provider_config.save(["leap",
- "providers",
- self._domain,
- "provider.json"])
-
- api_version = provider_config.get_api_version()
- if SupportedAPIs.supports(api_version):
- logger.debug("Provider definition has been modified")
- else:
- api_supported = ', '.join(SupportedAPIs.SUPPORTED_APIS)
- error = ('Unsupported provider API version. '
- 'Supported versions are: {}. '
- 'Found: {}.').format(api_supported, api_version)
-
- logger.error(error)
- raise UnsupportedProviderAPI(error)
-
- def run_provider_select_checks(self, domain, download_if_needed=False):
- """
- Populates the check queue.
-
- :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
- """
- leap_assert(domain and len(domain) > 0, "We need a domain!")
-
- self._domain = ProviderConfig.sanitize_path_component(domain)
- self._download_if_needed = download_if_needed
-
- cb_chain = [
- (self._check_name_resolution, self.name_resolution),
- (self._check_https, self.https_connection),
- (self._download_provider_info, self.download_provider_info)
- ]
-
- return self.addCallbackChain(cb_chain)
-
- def _should_proceed_cert(self):
- """
- Returns False if the certificate already exists for the given
- provider. True otherwise
-
- :rtype: bool
- """
- leap_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, *args):
- """
- Downloads the CA cert that is going to be used for the api URL
- """
-
- leap_assert(self._provider_config, "Cannot download the ca cert "
- "without a provider config!")
-
- logger.debug("Downloading ca cert for %s at %s" %
- (self._domain, self._provider_config.get_ca_cert_uri()))
-
- if not self._should_proceed_cert():
- check_and_fix_urw_only(
- self._provider_config
- .get_ca_cert_path(about_to_download=True))
- return
-
- res = self._session.get(self._provider_config.get_ca_cert_uri(),
- verify=not self._bypass_checks,
- timeout=REQUEST_TIMEOUT)
- res.raise_for_status()
-
- cert_path = self._provider_config.get_ca_cert_path(
- about_to_download=True)
- cert_dir = os.path.dirname(cert_path)
- mkdir_p(cert_dir)
- with open(cert_path, "w") as f:
- f.write(res.content)
-
- check_and_fix_urw_only(cert_path)
-
- def _check_ca_fingerprint(self, *args):
- """
- Checks the CA cert fingerprint against the one provided in the
- json definition
- """
- leap_assert(self._provider_config, "Cannot check the ca cert "
- "without a provider config!")
-
- logger.debug("Checking ca fingerprint for %s and cert %s" %
- (self._domain,
- self._provider_config.get_ca_cert_path()))
-
- if not self._should_proceed_cert():
- return
-
- parts = self._provider_config.get_ca_cert_fingerprint().split(":")
-
- error_msg = "Wrong fingerprint format"
- leap_check(len(parts) == 2, error_msg, WrongFingerprint)
-
- method = parts[0].strip()
- fingerprint = parts[1].strip()
- cert_data = None
- with open(self._provider_config.get_ca_cert_path()) as f:
- cert_data = f.read()
-
- leap_assert(len(cert_data) > 0, "Could not read certificate data")
- digest = get_digest(cert_data, method)
-
- error_msg = "Downloaded certificate has a different fingerprint!"
- leap_check(digest == fingerprint, error_msg, WrongFingerprint)
-
- def _check_api_certificate(self, *args):
- """
- Tries to make an API call with the downloaded cert and checks
- if it validates against it
- """
- leap_assert(self._provider_config, "Cannot check the ca cert "
- "without a provider config!")
-
- logger.debug("Checking api certificate for %s and cert %s" %
- (self._provider_config.get_api_uri(),
- self._provider_config.get_ca_cert_path()))
-
- if not self._should_proceed_cert():
- return
-
- 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(),
- timeout=REQUEST_TIMEOUT)
- res.raise_for_status()
-
- 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
- """
- leap_assert(provider_config, "We need a provider config!")
- leap_assert_type(provider_config, ProviderConfig)
-
- self._provider_config = provider_config
- self._download_if_needed = download_if_needed
-
- cb_chain = [
- (self._download_ca_cert, self.download_ca_cert),
- (self._check_ca_fingerprint, self.check_ca_fingerprint),
- (self._check_api_certificate, self.check_api_certificate)
- ]
-
- return self.addCallbackChain(cb_chain)
diff --git a/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py b/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py
index d0d78eed..6640a860 100644
--- a/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py
+++ b/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py
@@ -41,6 +41,7 @@ from leap.bitmask.services.eip.eipconfig import EIPConfig
from leap.bitmask.config.providerconfig import ProviderConfig
from leap.bitmask.crypto.tests import fake_provider
from leap.bitmask.crypto.srpauth import SRPAuth
+from leap.bitmask import util
from leap.common.testing.basetest import BaseLeapTest
from leap.common.files import mkdir_p
@@ -60,13 +61,13 @@ class EIPBootstrapperActiveTest(BaseLeapTest):
def setUp(self):
self.eb = EIPBootstrapper()
- self.old_pp = EIPConfig.get_path_prefix
+ self.old_pp = util.get_path_prefix
self.old_save = EIPConfig.save
self.old_load = EIPConfig.load
self.old_si = SRPAuth.get_session_id
def tearDown(self):
- EIPConfig.get_path_prefix = self.old_pp
+ util.get_path_prefix = self.old_pp
EIPConfig.save = self.old_save
EIPConfig.load = self.old_load
SRPAuth.get_session_id = self.old_si
@@ -97,13 +98,13 @@ class EIPBootstrapperActiveTest(BaseLeapTest):
pc.get_ca_cert_path = mock.MagicMock(return_value=False)
path_prefix = tempfile.mkdtemp()
- EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix)
+ util.get_path_prefix = mock.MagicMock(return_value=path_prefix)
EIPConfig.save = mock.MagicMock()
EIPConfig.load = mock.MagicMock()
self.eb._download_if_needed = ifneeded
- provider_dir = os.path.join(EIPConfig.get_path_prefix(),
+ provider_dir = os.path.join(util.get_path_prefix(),
"leap",
"providers",
pc.get_domain())
@@ -150,10 +151,10 @@ class EIPBootstrapperActiveTest(BaseLeapTest):
def check(*args):
self.eb._eip_config.save.assert_called_once_with(
- ["leap",
+ ("leap",
"providers",
self.eb._provider_config.get_domain(),
- "eip-service.json"])
+ "eip-service.json"))
d.addCallback(check)
return d
@@ -184,13 +185,13 @@ class EIPBootstrapperActiveTest(BaseLeapTest):
pc.get_ca_cert_path = mock.MagicMock(return_value=False)
path_prefix = tempfile.mkdtemp()
- EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix)
+ util.get_path_prefix = mock.MagicMock(return_value=path_prefix)
EIPConfig.save = mock.MagicMock()
EIPConfig.load = mock.MagicMock()
self.eb._download_if_needed = ifneeded
- provider_dir = os.path.join(EIPConfig.get_path_prefix(),
+ provider_dir = os.path.join(util.get_path_prefix(),
"leap",
"providers",
"somedomain")
diff --git a/src/leap/bitmask/services/eip/tests/test_eipconfig.py b/src/leap/bitmask/services/eip/tests/test_eipconfig.py
index 76305e83..4e340f4c 100644
--- a/src/leap/bitmask/services/eip/tests/test_eipconfig.py
+++ b/src/leap/bitmask/services/eip/tests/test_eipconfig.py
@@ -262,15 +262,13 @@ class EIPConfigTest(BaseLeapTest):
def test_get_client_cert_path_as_expected(self):
config = self._get_eipconfig()
- config.get_path_prefix = Mock(return_value='test')
-
provider_config = ProviderConfig()
# mock 'get_domain' so we don't need to load a config
provider_domain = 'test.provider.com'
provider_config.get_domain = Mock(return_value=provider_domain)
- expected_path = os.path.join('test', 'leap', 'providers',
+ expected_path = os.path.join('leap', 'providers',
provider_domain, 'keys', 'client',
'openvpn.pem')
@@ -278,26 +276,24 @@ class EIPConfigTest(BaseLeapTest):
os.path.exists = Mock(return_value=True)
cert_path = config.get_client_cert_path(provider_config)
- self.assertEqual(cert_path, expected_path)
+ self.assertTrue(cert_path.endswith(expected_path))
def test_get_client_cert_path_about_to_download(self):
config = self._get_eipconfig()
- config.get_path_prefix = Mock(return_value='test')
-
provider_config = ProviderConfig()
# mock 'get_domain' so we don't need to load a config
provider_domain = 'test.provider.com'
provider_config.get_domain = Mock(return_value=provider_domain)
- expected_path = os.path.join('test', 'leap', 'providers',
+ expected_path = os.path.join('leap', 'providers',
provider_domain, 'keys', 'client',
'openvpn.pem')
cert_path = config.get_client_cert_path(
provider_config, about_to_download=True)
- self.assertEqual(cert_path, expected_path)
+ self.assertTrue(cert_path.endswith(expected_path))
def test_get_client_cert_path_fails(self):
config = self._get_eipconfig()
diff --git a/src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py b/src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py
deleted file mode 100644
index b0685676..00000000
--- a/src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py
+++ /dev/null
@@ -1,560 +0,0 @@
-# -*- coding: utf-8 -*-
-# test_providerbootstrapper.py
-# Copyright (C) 2013 LEAP
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-
-"""
-Tests for the Provider Boostrapper checks
-
-These will be whitebox tests since we want to make sure the private
-implementation is checking what we expect.
-"""
-
-import os
-import mock
-import socket
-import stat
-import tempfile
-import time
-import requests
-try:
- import unittest2 as unittest
-except ImportError:
- import unittest
-
-from nose.twistedtools import deferred, reactor
-from twisted.internet import threads
-from requests.models import Response
-
-from leap.bitmask.services.eip.providerbootstrapper import ProviderBootstrapper
-from leap.bitmask.services.eip.providerbootstrapper import \
- UnsupportedProviderAPI
-from leap.bitmask.services.eip.providerbootstrapper import WrongFingerprint
-from leap.bitmask.provider.supportedapis import SupportedAPIs
-from leap.bitmask.config.providerconfig import ProviderConfig
-from leap.bitmask.crypto.tests import fake_provider
-from leap.common.files import mkdir_p
-from leap.common.testing.https_server import where
-from leap.common.testing.basetest import BaseLeapTest
-
-
-class ProviderBootstrapperTest(BaseLeapTest):
- def setUp(self):
- self.pb = ProviderBootstrapper()
-
- def tearDown(self):
- pass
-
- def test_name_resolution_check(self):
- # Something highly likely to success
- self.pb._domain = "google.com"
- self.pb._check_name_resolution()
- # Something highly likely to fail
- self.pb._domain = "uquhqweuihowquie.abc.def"
-
- # In python 2.7.4 raises socket.error
- # In python 2.7.5 raises socket.gaierror
- with self.assertRaises((socket.gaierror, socket.error)):
- self.pb._check_name_resolution()
-
- @deferred()
- def test_run_provider_select_checks(self):
- self.pb._check_name_resolution = mock.MagicMock()
- self.pb._check_https = mock.MagicMock()
- self.pb._download_provider_info = mock.MagicMock()
-
- d = self.pb.run_provider_select_checks("somedomain")
-
- def check(*args):
- self.pb._check_name_resolution.assert_called_once_with()
- self.pb._check_https.assert_called_once_with(None)
- self.pb._download_provider_info.assert_called_once_with(None)
- d.addCallback(check)
- return d
-
- @deferred()
- def test_run_provider_setup_checks(self):
- self.pb._download_ca_cert = mock.MagicMock()
- self.pb._check_ca_fingerprint = mock.MagicMock()
- self.pb._check_api_certificate = mock.MagicMock()
-
- d = self.pb.run_provider_setup_checks(ProviderConfig())
-
- def check(*args):
- self.pb._download_ca_cert.assert_called_once_with()
- self.pb._check_ca_fingerprint.assert_called_once_with(None)
- self.pb._check_api_certificate.assert_called_once_with(None)
- d.addCallback(check)
- return d
-
- def test_should_proceed_cert(self):
- self.pb._provider_config = mock.Mock()
- self.pb._provider_config.get_ca_cert_path = mock.MagicMock(
- return_value=where("cacert.pem"))
-
- self.pb._download_if_needed = False
- self.assertTrue(self.pb._should_proceed_cert())
-
- self.pb._download_if_needed = True
- self.assertFalse(self.pb._should_proceed_cert())
-
- self.pb._provider_config.get_ca_cert_path = mock.MagicMock(
- return_value=where("somefilethatdoesntexist.pem"))
- self.assertTrue(self.pb._should_proceed_cert())
-
- def _check_download_ca_cert(self, should_proceed):
- """
- Helper to check different paths easily for the download ca
- cert check
-
- :param should_proceed: sets the _should_proceed_cert in the
- provider bootstrapper being tested
- :type should_proceed: bool
-
- :returns: The contents of the certificate, the expected
- content depending on should_proceed, and the mode of
- the file to be checked by the caller
- :rtype: tuple of str, str, int
- """
- old_content = "NOT THE NEW CERT"
- new_content = "NEW CERT"
- new_cert_path = os.path.join(tempfile.mkdtemp(),
- "mynewcert.pem")
-
- with open(new_cert_path, "w") as c:
- c.write(old_content)
-
- self.pb._provider_config = mock.Mock()
- self.pb._provider_config.get_ca_cert_path = mock.MagicMock(
- return_value=new_cert_path)
- self.pb._domain = "somedomain"
-
- self.pb._should_proceed_cert = mock.MagicMock(
- return_value=should_proceed)
-
- read = None
- content_to_check = None
- mode = None
-
- with mock.patch('requests.models.Response.content',
- new_callable=mock.PropertyMock) as \
- content:
- content.return_value = new_content
- response_obj = Response()
- response_obj.raise_for_status = mock.MagicMock()
-
- self.pb._session.get = mock.MagicMock(return_value=response_obj)
- self.pb._download_ca_cert()
- with open(new_cert_path, "r") as nc:
- read = nc.read()
- if should_proceed:
- content_to_check = new_content
- else:
- content_to_check = old_content
- mode = stat.S_IMODE(os.stat(new_cert_path).st_mode)
-
- os.unlink(new_cert_path)
- return read, content_to_check, mode
-
- def test_download_ca_cert_no_saving(self):
- read, expected_read, mode = self._check_download_ca_cert(False)
- self.assertEqual(read, expected_read)
- self.assertEqual(mode, int("600", 8))
-
- def test_download_ca_cert_saving(self):
- read, expected_read, mode = self._check_download_ca_cert(True)
- self.assertEqual(read, expected_read)
- self.assertEqual(mode, int("600", 8))
-
- def test_check_ca_fingerprint_skips(self):
- self.pb._provider_config = mock.Mock()
- self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock(
- return_value="")
- self.pb._domain = "somedomain"
-
- self.pb._should_proceed_cert = mock.MagicMock(return_value=False)
-
- self.pb._check_ca_fingerprint()
- self.assertFalse(self.pb._provider_config.
- get_ca_cert_fingerprint.called)
-
- def test_check_ca_cert_fingerprint_raises_bad_format(self):
- self.pb._provider_config = mock.Mock()
- self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock(
- return_value="wrongfprformat!!")
- self.pb._domain = "somedomain"
-
- self.pb._should_proceed_cert = mock.MagicMock(return_value=True)
-
- with self.assertRaises(WrongFingerprint):
- self.pb._check_ca_fingerprint()
-
- # This two hashes different in the last byte, but that's good enough
- # for the tests
- KNOWN_BAD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034efe" \
- "7dd1b910062ca323eb4da5c7f"
- KNOWN_GOOD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034ef" \
- "e7dd1b910062ca323eb4da5c7e"
- KNOWN_GOOD_CERT = """
------BEGIN CERTIFICATE-----
-MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt
-YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v
-Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw
-FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV
-BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
-ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai
-dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB
-7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84
-CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+
-znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4
-MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4
-lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0
-bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl
-DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB
-lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy
-YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw
-XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE
-MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w
-DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl
-cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY
-k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj
-RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG
-htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX
-EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J
-aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l
-mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK
-G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co
-Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d
-69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e
-yV8e
------END CERTIFICATE-----
-"""
-
- def _prepare_provider_config_with(self, cert_path, cert_hash):
- """
- Mocks the provider config to give the cert_path and cert_hash
- specified
-
- :param cert_path: path for the certificate
- :type cert_path: str
- :param cert_hash: hash for the certificate as it would appear
- in the provider config json
- :type cert_hash: str
- """
- self.pb._provider_config = mock.Mock()
- self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock(
- return_value=cert_hash)
- self.pb._provider_config.get_ca_cert_path = mock.MagicMock(
- return_value=cert_path)
- self.pb._domain = "somedomain"
-
- def test_check_ca_fingerprint_checksout(self):
- cert_path = os.path.join(tempfile.mkdtemp(),
- "mynewcert.pem")
-
- with open(cert_path, "w") as c:
- c.write(self.KNOWN_GOOD_CERT)
-
- self._prepare_provider_config_with(cert_path, self.KNOWN_GOOD_HASH)
-
- self.pb._should_proceed_cert = mock.MagicMock(return_value=True)
-
- self.pb._check_ca_fingerprint()
-
- os.unlink(cert_path)
-
- def test_check_ca_fingerprint_fails(self):
- cert_path = os.path.join(tempfile.mkdtemp(),
- "mynewcert.pem")
-
- with open(cert_path, "w") as c:
- c.write(self.KNOWN_GOOD_CERT)
-
- self._prepare_provider_config_with(cert_path, self.KNOWN_BAD_HASH)
-
- self.pb._should_proceed_cert = mock.MagicMock(return_value=True)
-
- with self.assertRaises(WrongFingerprint):
- self.pb._check_ca_fingerprint()
-
- os.unlink(cert_path)
-
-
-###############################################################################
-# Tests with a fake provider #
-###############################################################################
-
-class ProviderBootstrapperActiveTest(unittest.TestCase):
- @classmethod
- def setUpClass(cls):
- factory = fake_provider.get_provider_factory()
- http = reactor.listenTCP(8002, factory)
- https = reactor.listenSSL(
- 0, factory,
- fake_provider.OpenSSLServerContextFactory())
- get_port = lambda p: p.getHost().port
- cls.http_port = get_port(http)
- cls.https_port = get_port(https)
-
- def setUp(self):
- self.pb = ProviderBootstrapper()
-
- # At certain points we are going to be replacing these methods
- # directly in ProviderConfig to be able to catch calls from
- # new ProviderConfig objects inside the methods tested. We
- # need to save the old implementation and restore it in
- # tearDown so we are sure everything is as expected for each
- # test. If we do it inside each specific test, a failure in
- # the test will leave the implementation with the mock.
- self.old_gpp = ProviderConfig.get_path_prefix
- self.old_load = ProviderConfig.load
- self.old_save = ProviderConfig.save
- self.old_api_version = ProviderConfig.get_api_version
-
- def tearDown(self):
- ProviderConfig.get_path_prefix = self.old_gpp
- ProviderConfig.load = self.old_load
- ProviderConfig.save = self.old_save
- ProviderConfig.get_api_version = self.old_api_version
-
- def test_check_https_succeeds(self):
- # XXX: Need a proper CA signed cert to test this
- pass
-
- @deferred()
- def test_check_https_fails(self):
- self.pb._domain = "localhost:%s" % (self.https_port,)
-
- def check(*args):
- with self.assertRaises(requests.exceptions.SSLError):
- self.pb._check_https()
- return threads.deferToThread(check)
-
- @deferred()
- def test_second_check_https_fails(self):
- self.pb._domain = "localhost:1234"
-
- def check(*args):
- with self.assertRaises(Exception):
- self.pb._check_https()
- return threads.deferToThread(check)
-
- @deferred()
- def test_check_https_succeeds_if_danger(self):
- self.pb._domain = "localhost:%s" % (self.https_port,)
- self.pb._bypass_checks = True
-
- def check(*args):
- self.pb._check_https()
-
- return threads.deferToThread(check)
-
- def _setup_provider_config_with(self, api, path_prefix):
- """
- Sets up the ProviderConfig with mocks for the path prefix, the
- api returned and load/save methods.
- It modifies ProviderConfig directly instead of an object
- because the object used is created in the method itself and we
- cannot control that.
-
- :param api: API to return
- :type api: str
- :param path_prefix: path prefix to be used when calculating
- paths
- :type path_prefix: str
- """
- ProviderConfig.get_path_prefix = mock.MagicMock(
- return_value=path_prefix)
- ProviderConfig.get_api_version = mock.MagicMock(
- return_value=api)
- ProviderConfig.load = mock.MagicMock()
- ProviderConfig.save = mock.MagicMock()
-
- def _setup_providerbootstrapper(self, ifneeded):
- """
- Sets the provider bootstrapper's domain to
- localhost:https_port, sets it to bypass https checks and sets
- the download if needed based on the ifneeded value.
-
- :param ifneeded: Value for _download_if_needed
- :type ifneeded: bool
- """
- self.pb._domain = "localhost:%s" % (self.https_port,)
- self.pb._bypass_checks = True
- self.pb._download_if_needed = ifneeded
-
- def _produce_dummy_provider_json(self):
- """
- Creates a dummy provider json on disk in order to test
- behaviour around it (download if newer online, etc)
-
- :returns: the provider.json path used
- :rtype: str
- """
- provider_dir = os.path.join(ProviderConfig()
- .get_path_prefix(),
- "leap",
- "providers",
- self.pb._domain)
- mkdir_p(provider_dir)
- provider_path = os.path.join(provider_dir,
- "provider.json")
-
- with open(provider_path, "w") as p:
- p.write("A")
- return provider_path
-
- def test_download_provider_info_new_provider(self):
- self._setup_provider_config_with("1", tempfile.mkdtemp())
- self._setup_providerbootstrapper(True)
-
- self.pb._download_provider_info()
- self.assertTrue(ProviderConfig.save.called)
-
- @mock.patch(
- 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path',
- lambda x: where('cacert.pem'))
- def test_download_provider_info_not_modified(self):
- self._setup_provider_config_with("1", tempfile.mkdtemp())
- self._setup_providerbootstrapper(True)
- provider_path = self._produce_dummy_provider_json()
-
- # set mtime to something really new
- os.utime(provider_path, (-1, time.time()))
-
- with mock.patch.object(
- ProviderConfig, 'get_api_uri',
- return_value="https://localhost:%s" % (self.https_port,)):
- self.pb._download_provider_info()
- # we check that it doesn't save the provider
- # config, because it's new enough
- self.assertFalse(ProviderConfig.save.called)
-
- @mock.patch(
- 'leap.bitmask.config.providerconfig.ProviderConfig.get_domain',
- lambda x: where('testdomain.com'))
- def test_download_provider_info_not_modified_and_no_cacert(self):
- self._setup_provider_config_with("1", tempfile.mkdtemp())
- self._setup_providerbootstrapper(True)
- provider_path = self._produce_dummy_provider_json()
-
- # set mtime to something really new
- os.utime(provider_path, (-1, time.time()))
-
- with mock.patch.object(
- ProviderConfig, 'get_api_uri',
- return_value="https://localhost:%s" % (self.https_port,)):
- self.pb._download_provider_info()
- # we check that it doesn't save the provider
- # config, because it's new enough
- self.assertFalse(ProviderConfig.save.called)
-
- @mock.patch(
- 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path',
- lambda x: where('cacert.pem'))
- def test_download_provider_info_modified(self):
- self._setup_provider_config_with("1", tempfile.mkdtemp())
- self._setup_providerbootstrapper(True)
- provider_path = self._produce_dummy_provider_json()
-
- # set mtime to something really old
- os.utime(provider_path, (-1, 100))
-
- with mock.patch.object(
- ProviderConfig, 'get_api_uri',
- return_value="https://localhost:%s" % (self.https_port,)):
- self.pb._download_provider_info()
- self.assertTrue(ProviderConfig.load.called)
- self.assertTrue(ProviderConfig.save.called)
-
- @mock.patch(
- 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path',
- lambda x: where('cacert.pem'))
- def test_download_provider_info_unsupported_api_raises(self):
- self._setup_provider_config_with("9999999", tempfile.mkdtemp())
- self._setup_providerbootstrapper(False)
- self._produce_dummy_provider_json()
-
- with mock.patch.object(
- ProviderConfig, 'get_api_uri',
- return_value="https://localhost:%s" % (self.https_port,)):
- with self.assertRaises(UnsupportedProviderAPI):
- self.pb._download_provider_info()
-
- @mock.patch(
- 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path',
- lambda x: where('cacert.pem'))
- def test_download_provider_info_unsupported_api(self):
- self._setup_provider_config_with(SupportedAPIs.SUPPORTED_APIS[0],
- tempfile.mkdtemp())
- self._setup_providerbootstrapper(False)
- self._produce_dummy_provider_json()
-
- with mock.patch.object(
- ProviderConfig, 'get_api_uri',
- return_value="https://localhost:%s" % (self.https_port,)):
- self.pb._download_provider_info()
-
- @mock.patch(
- 'leap.bitmask.config.providerconfig.ProviderConfig.get_api_uri',
- lambda x: 'api.uri')
- @mock.patch(
- 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path',
- lambda x: '/cert/path')
- def test_check_api_certificate_skips(self):
- self.pb._provider_config = ProviderConfig()
- self.pb._session.get = mock.MagicMock(return_value=Response())
-
- self.pb._should_proceed_cert = mock.MagicMock(return_value=False)
- self.pb._check_api_certificate()
- self.assertFalse(self.pb._session.get.called)
-
- @deferred()
- def test_check_api_certificate_fails(self):
- self.pb._provider_config = ProviderConfig()
- self.pb._provider_config.get_api_uri = mock.MagicMock(
- return_value="https://localhost:%s" % (self.https_port,))
- self.pb._provider_config.get_ca_cert_path = mock.MagicMock(
- return_value=os.path.join(
- os.path.split(__file__)[0],
- "wrongcert.pem"))
- self.pb._provider_config.get_api_version = mock.MagicMock(
- return_value="1")
-
- self.pb._should_proceed_cert = mock.MagicMock(return_value=True)
-
- def check(*args):
- with self.assertRaises(requests.exceptions.SSLError):
- self.pb._check_api_certificate()
- d = threads.deferToThread(check)
- return d
-
- @deferred()
- def test_check_api_certificate_succeeds(self):
- self.pb._provider_config = ProviderConfig()
- self.pb._provider_config.get_api_uri = mock.MagicMock(
- return_value="https://localhost:%s" % (self.https_port,))
- self.pb._provider_config.get_ca_cert_path = mock.MagicMock(
- return_value=where('cacert.pem'))
- self.pb._provider_config.get_api_version = mock.MagicMock(
- return_value="1")
-
- self.pb._should_proceed_cert = mock.MagicMock(return_value=True)
-
- def check(*args):
- self.pb._check_api_certificate()
- d = threads.deferToThread(check)
- return d
diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py
new file mode 100644
index 00000000..935d75f1
--- /dev/null
+++ b/src/leap/bitmask/services/eip/vpnlauncher.py
@@ -0,0 +1,290 @@
+# -*- coding: utf-8 -*-
+# vpnlauncher.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 independant VPN launcher interface.
+"""
+import getpass
+import logging
+import os
+import stat
+
+from abc import ABCMeta, abstractmethod
+from functools import partial
+
+from leap.bitmask.config import flags
+from leap.bitmask.config.leapsettings import LeapSettings
+from leap.bitmask.config.providerconfig import ProviderConfig
+from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector
+from leap.bitmask.util import first
+from leap.bitmask.util import get_path_prefix
+from leap.common.check import leap_assert, leap_assert_type
+from leap.common.files import which
+
+logger = logging.getLogger(__name__)
+
+
+class VPNLauncherException(Exception):
+ pass
+
+
+class OpenVPNNotFoundException(VPNLauncherException):
+ pass
+
+
+def _has_updown_scripts(path, warn=True):
+ """
+ Checks the existence of the up/down scripts and its
+ exec bit if applicable.
+
+ :param path: the path to be checked
+ :type path: str
+
+ :param warn: whether we should log the absence
+ :type warn: bool
+
+ :rtype: bool
+ """
+ is_file = os.path.isfile(path)
+ if warn and not is_file:
+ logger.error("Could not find up/down script %s. "
+ "Might produce DNS leaks." % (path,))
+
+ # XXX check if applies in win
+ is_exe = False
+ try:
+ is_exe = (stat.S_IXUSR & os.stat(path)[stat.ST_MODE] != 0)
+ except OSError as e:
+ logger.warn("%s" % (e,))
+ if warn and not is_exe:
+ logger.error("Up/down script %s is not executable. "
+ "Might produce DNS leaks." % (path,))
+ return is_file and is_exe
+
+
+def _has_other_files(path, warn=True):
+ """
+ Checks the existence of other important files.
+
+ :param path: the path to be checked
+ :type path: str
+
+ :param warn: whether we should log the absence
+ :type warn: bool
+
+ :rtype: bool
+ """
+ is_file = os.path.isfile(path)
+ if warn and not is_file:
+ logger.warning("Could not find file during checks: %s. " % (
+ path,))
+ return is_file
+
+
+class VPNLauncher(object):
+ """
+ Abstract launcher class
+ """
+ __metaclass__ = ABCMeta
+
+ UPDOWN_FILES = None
+ OTHER_FILES = None
+
+ @classmethod
+ @abstractmethod
+ def get_vpn_command(kls, eipconfig, providerconfig,
+ socket_host, socket_port, openvpn_verb=1):
+ """
+ Returns the platform dependant vpn launching command
+
+ Might raise:
+ OpenVPNNotFoundException,
+ VPNLauncherException.
+
+ :param eipconfig: eip configuration object
+ :type eipconfig: EIPConfig
+ :param providerconfig: provider specific configuration
+ :type providerconfig: ProviderConfig
+ :param socket_host: either socket path (unix) or socket IP
+ :type socket_host: str
+ :param socket_port: either string "unix" if it's a unix socket,
+ or port otherwise
+ :type socket_port: str
+ :param openvpn_verb: the openvpn verbosity wanted
+ :type openvpn_verb: int
+
+ :return: A VPN command ready to be launched.
+ :rtype: list
+ """
+ leap_assert_type(eipconfig, EIPConfig)
+ leap_assert_type(providerconfig, ProviderConfig)
+
+ kwargs = {}
+ if flags.STANDALONE:
+ kwargs['path_extension'] = os.path.join(
+ get_path_prefix(), "..", "apps", "eip")
+
+ openvpn_possibilities = which(kls.OPENVPN_BIN, **kwargs)
+ if len(openvpn_possibilities) == 0:
+ raise OpenVPNNotFoundException()
+
+ openvpn = first(openvpn_possibilities)
+ args = []
+
+ args += [
+ '--setenv', "LEAPOPENVPN", "1"
+ ]
+
+ if openvpn_verb is not None:
+ args += ['--verb', '%d' % (openvpn_verb,)]
+
+ gateways = []
+ leap_settings = LeapSettings()
+ domain = providerconfig.get_domain()
+ gateway_conf = leap_settings.get_selected_gateway(domain)
+
+ if gateway_conf == leap_settings.GATEWAY_AUTOMATIC:
+ gateway_selector = VPNGatewaySelector(eipconfig)
+ gateways = gateway_selector.get_gateways()
+ else:
+ gateways = [gateway_conf]
+
+ if not gateways:
+ logger.error('No gateway was found!')
+ raise VPNLauncherException(kls.tr('No gateway was found!'))
+
+ logger.debug("Using gateways ips: {0}".format(', '.join(gateways)))
+
+ for gw in gateways:
+ args += ['--remote', gw, '1194', 'udp']
+
+ args += [
+ '--client',
+ '--dev', 'tun',
+ ##############################################################
+ # persist-tun makes ping-restart fail because it leaves a
+ # broken routing table
+ ##############################################################
+ # '--persist-tun',
+ '--persist-key',
+ '--tls-client',
+ '--remote-cert-tls',
+ 'server'
+ ]
+
+ openvpn_configuration = eipconfig.get_openvpn_configuration()
+ for key, value in openvpn_configuration.items():
+ args += ['--%s' % (key,), value]
+
+ user = getpass.getuser()
+
+ ##############################################################
+ # The down-root plugin fails in some situations, so we don't
+ # drop privs for the time being
+ ##############################################################
+ # args += [
+ # '--user', user,
+ # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name
+ # ]
+
+ if socket_port == "unix": # that's always the case for linux
+ args += [
+ '--management-client-user', user
+ ]
+
+ args += [
+ '--management-signal',
+ '--management', socket_host, socket_port,
+ '--script-security', '2'
+ ]
+
+ if _has_updown_scripts(kls.UP_SCRIPT):
+ args += [
+ '--up', '\"%s\"' % (kls.UP_SCRIPT,),
+ ]
+
+ if _has_updown_scripts(kls.DOWN_SCRIPT):
+ args += [
+ '--down', '\"%s\"' % (kls.DOWN_SCRIPT,)
+ ]
+
+ ###########################################################
+ # For the time being we are disabling the usage of the
+ # down-root plugin, because it doesn't quite work as
+ # expected (i.e. it doesn't run route -del as root
+ # when finishing, so it fails to properly
+ # restart/quit)
+ ###########################################################
+ # if _has_updown_scripts(kls.OPENVPN_DOWN_PLUGIN):
+ # args += [
+ # '--plugin', kls.OPENVPN_DOWN_ROOT,
+ # '\'%s\'' % kls.DOWN_SCRIPT # for OSX
+ # '\'script_type=down %s\'' % kls.DOWN_SCRIPT # for Linux
+ # ]
+
+ args += [
+ '--cert', eipconfig.get_client_cert_path(providerconfig),
+ '--key', eipconfig.get_client_cert_path(providerconfig),
+ '--ca', providerconfig.get_ca_cert_path()
+ ]
+
+ command_and_args = [openvpn] + args
+ logger.debug("Running VPN with command:")
+ logger.debug(" ".join(command_and_args))
+
+ return command_and_args
+
+ @classmethod
+ def get_vpn_env(kls):
+ """
+ Returns a dictionary with the custom env for the platform.
+ This is mainly used for setting LD_LIBRARY_PATH to the correct
+ path when distributing a standalone client
+
+ :rtype: dict
+ """
+ return {}
+
+ @classmethod
+ def missing_updown_scripts(kls):
+ """
+ Returns what updown scripts are missing.
+
+ :rtype: list
+ """
+ leap_assert(kls.UPDOWN_FILES is not None,
+ "Need to define UPDOWN_FILES for this particular "
+ "launcher before calling this method")
+ file_exist = partial(_has_updown_scripts, warn=False)
+ zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES))
+ missing = filter(lambda (path, exists): exists is False, zipped)
+ return [path for path, exists in missing]
+
+ @classmethod
+ def missing_other_files(kls):
+ """
+ Returns what other important files are missing during startup.
+ Same as missing_updown_scripts but does not check for exec bit.
+
+ :rtype: list
+ """
+ leap_assert(kls.OTHER_FILES is not None,
+ "Need to define OTHER_FILES for this particular "
+ "auncher before calling this method")
+ file_exist = partial(_has_other_files, warn=False)
+ zipped = zip(kls.OTHER_FILES, map(file_exist, kls.OTHER_FILES))
+ missing = filter(lambda (path, exists): exists is False, zipped)
+ return [path for path, exists in missing]
diff --git a/src/leap/bitmask/services/eip/vpnlaunchers.py b/src/leap/bitmask/services/eip/vpnlaunchers.py
deleted file mode 100644
index daa0d81f..00000000
--- a/src/leap/bitmask/services/eip/vpnlaunchers.py
+++ /dev/null
@@ -1,963 +0,0 @@
-# -*- 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 commands
-import logging
-import getpass
-import os
-import platform
-import stat
-import subprocess
-try:
- import grp
-except ImportError:
- pass # ignore, probably windows
-
-from abc import ABCMeta, abstractmethod
-from functools import partial
-from time import sleep
-
-from leap.bitmask.config import flags
-from leap.bitmask.config.leapsettings import LeapSettings
-
-from leap.bitmask.config.providerconfig import ProviderConfig
-from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector
-from leap.bitmask.util import first
-from leap.bitmask.util import get_path_prefix
-from leap.bitmask.util.privilege_policies import LinuxPolicyChecker
-from leap.bitmask.util import privilege_policies
-from leap.common.check import leap_assert, leap_assert_type
-from leap.common.files import which
-
-
-logger = logging.getLogger(__name__)
-
-
-class VPNLauncherException(Exception):
- pass
-
-
-class OpenVPNNotFoundException(VPNLauncherException):
- pass
-
-
-class EIPNoPolkitAuthAgentAvailable(VPNLauncherException):
- pass
-
-
-class EIPNoPkexecAvailable(VPNLauncherException):
- pass
-
-
-class EIPNoTunKextLoaded(VPNLauncherException):
- pass
-
-
-class VPNLauncher(object):
- """
- Abstract launcher class
- """
- __metaclass__ = ABCMeta
-
- UPDOWN_FILES = None
- OTHER_FILES = None
-
- @abstractmethod
- def get_vpn_command(self, eipconfig=None, providerconfig=None,
- socket_host=None, socket_port=None):
- """
- 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 []
-
- @abstractmethod
- def get_vpn_env(self):
- """
- Returns a dictionary with the custom env for the platform.
- This is mainly used for setting LD_LIBRARY_PATH to the correct
- path when distributing a standalone client
-
- :rtype: dict
- """
- return {}
-
- @classmethod
- def missing_updown_scripts(kls):
- """
- Returns what updown scripts are missing.
- :rtype: list
- """
- leap_assert(kls.UPDOWN_FILES is not None,
- "Need to define UPDOWN_FILES for this particular "
- "auncher before calling this method")
- file_exist = partial(_has_updown_scripts, warn=False)
- zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES))
- missing = filter(lambda (path, exists): exists is False, zipped)
- return [path for path, exists in missing]
-
- @classmethod
- def missing_other_files(kls):
- """
- Returns what other important files are missing during startup.
- Same as missing_updown_scripts but does not check for exec bit.
- :rtype: list
- """
- leap_assert(kls.UPDOWN_FILES is not None,
- "Need to define OTHER_FILES for this particular "
- "auncher before calling this method")
- file_exist = partial(_has_other_files, warn=False)
- zipped = zip(kls.OTHER_FILES, map(file_exist, kls.OTHER_FILES))
- missing = filter(lambda (path, exists): exists is False, zipped)
- return [path for path, exists in missing]
-
-
-def get_platform_launcher():
- launcher = globals()[platform.system() + "VPNLauncher"]
- leap_assert(launcher, "Unimplemented platform launcher: %s" %
- (platform.system(),))
- return launcher()
-
-
-def _is_pkexec_in_system():
- """
- Checks the existence of the pkexec binary in system.
- """
- pkexec_path = which('pkexec')
- if len(pkexec_path) == 0:
- return False
- return True
-
-
-def _has_updown_scripts(path, warn=True):
- """
- Checks the existence of the up/down scripts and its
- exec bit if applicable.
-
- :param path: the path to be checked
- :type path: str
-
- :param warn: whether we should log the absence
- :type warn: bool
-
- :rtype: bool
- """
- is_file = os.path.isfile(path)
- if warn and not is_file:
- logger.error("Could not find up/down script %s. "
- "Might produce DNS leaks." % (path,))
-
- # XXX check if applies in win
- is_exe = False
- try:
- is_exe = (stat.S_IXUSR & os.stat(path)[stat.ST_MODE] != 0)
- except OSError as e:
- logger.warn("%s" % (e,))
- if warn and not is_exe:
- logger.error("Up/down script %s is not executable. "
- "Might produce DNS leaks." % (path,))
- return is_file and is_exe
-
-
-def _has_other_files(path, warn=True):
- """
- Checks the existence of other important files.
-
- :param path: the path to be checked
- :type path: str
-
- :param warn: whether we should log the absence
- :type warn: bool
-
- :rtype: bool
- """
- is_file = os.path.isfile(path)
- if warn and not is_file:
- logger.warning("Could not find file during checks: %s. " % (
- path,))
- return is_file
-
-
-def _is_auth_agent_running():
- """
- Checks if a polkit daemon is running.
-
- :return: True if it's running, False if it's not.
- :rtype: boolean
- """
- ps = 'ps aux | grep polkit-%s-authentication-agent-1'
- opts = (ps % case for case in ['[g]nome', '[k]de'])
- is_running = map(lambda l: commands.getoutput(l), opts)
- return any(is_running)
-
-
-def _try_to_launch_agent():
- """
- Tries to launch a polkit daemon.
- """
- env = None
- if flags.STANDALONE is True:
- env = {"PYTHONPATH": os.path.abspath('../../../../lib/')}
- try:
- # We need to quote the command because subprocess call
- # will do "sh -c 'foo'", so if we do not quoute it we'll end
- # up with a invocation to the python interpreter. And that
- # is bad.
- subprocess.call(["python -m leap.bitmask.util.polkit_agent"],
- shell=True, env=env)
- except Exception as exc:
- logger.exception(exc)
-
-
-class LinuxVPNLauncher(VPNLauncher):
- """
- VPN launcher for the Linux platform
- """
-
- PKEXEC_BIN = 'pkexec'
- OPENVPN_BIN = 'openvpn'
- OPENVPN_BIN_PATH = os.path.join(
- get_path_prefix(), "..", "apps", "eip", OPENVPN_BIN)
-
- SYSTEM_CONFIG = "/etc/leap"
- UP_DOWN_FILE = "resolv-update"
- UP_DOWN_PATH = "%s/%s" % (SYSTEM_CONFIG, UP_DOWN_FILE)
-
- # We assume this is there by our openvpn dependency, and
- # we will put it there on the bundle too.
- # TODO adapt to the bundle path.
- OPENVPN_DOWN_ROOT_BASE = "/usr/lib/openvpn/"
- OPENVPN_DOWN_ROOT_FILE = "openvpn-plugin-down-root.so"
- OPENVPN_DOWN_ROOT_PATH = "%s/%s" % (
- OPENVPN_DOWN_ROOT_BASE,
- OPENVPN_DOWN_ROOT_FILE)
-
- UPDOWN_FILES = (UP_DOWN_PATH,)
- POLKIT_PATH = LinuxPolicyChecker.get_polkit_path()
- OTHER_FILES = (POLKIT_PATH, )
-
- def missing_other_files(self):
- """
- 'Extend' the VPNLauncher's missing_other_files to check if the polkit
- files is outdated. If the polkit file that is in OTHER_FILES exists but
- is not up to date, it is added to the missing list.
-
- :returns: a list of missing files
- :rtype: list of str
- """
- missing = VPNLauncher.missing_other_files.im_func(self)
- polkit_file = LinuxPolicyChecker.get_polkit_path()
- if polkit_file not in missing:
- if privilege_policies.is_policy_outdated(self.OPENVPN_BIN_PATH):
- missing.append(polkit_file)
-
- return missing
-
- @classmethod
- def cmd_for_missing_scripts(kls, frompath, pol_file):
- """
- Returns a sh script that can copy the missing files.
-
- :param frompath: The path where the up/down scripts live
- :type frompath: str
- :param pol_file: The path where the dynamically generated
- policy file lives
- :type pol_file: str
-
- :rtype: str
- """
- to = kls.SYSTEM_CONFIG
-
- cmd = '#!/bin/sh\n'
- cmd += 'mkdir -p "%s"\n' % (to, )
- cmd += 'cp "%s/%s" "%s"\n' % (frompath, kls.UP_DOWN_FILE, to)
- cmd += 'cp "%s" "%s"\n' % (pol_file, kls.POLKIT_PATH)
- cmd += 'chmod 644 "%s"\n' % (kls.POLKIT_PATH, )
-
- return cmd
-
- @classmethod
- def maybe_pkexec(kls):
- """
- Checks whether pkexec is available in the system, and
- returns the path if found.
-
- Might raise EIPNoPkexecAvailable or EIPNoPolkitAuthAgentAvailable
-
- :returns: a list of the paths where pkexec is to be found
- :rtype: list
- """
- if _is_pkexec_in_system():
- if not _is_auth_agent_running():
- _try_to_launch_agent()
- sleep(0.5)
- if _is_auth_agent_running():
- pkexec_possibilities = which(kls.PKEXEC_BIN)
- leap_assert(len(pkexec_possibilities) > 0,
- "We couldn't find pkexec")
- return pkexec_possibilities
- else:
- logger.warning("No polkit auth agent found. pkexec " +
- "will use its own auth agent.")
- raise EIPNoPolkitAuthAgentAvailable()
- else:
- logger.warning("System has no pkexec")
- raise EIPNoPkexecAvailable()
-
- @classmethod
- def maybe_down_plugin(kls):
- """
- Returns the path of the openvpn down-root-plugin, searching first
- in the relative path for the standalone bundle, and then in the system
- path where the debian package puts it.
-
- :returns: the path where the plugin was found, or None
- :rtype: str or None
- """
- cwd = os.getcwd()
- rel_path_in_bundle = os.path.join(
- 'apps', 'eip', 'files', kls.OPENVPN_DOWN_ROOT_FILE)
- abs_path_in_bundle = os.path.join(cwd, rel_path_in_bundle)
- if os.path.isfile(abs_path_in_bundle):
- return abs_path_in_bundle
- abs_path_in_system = kls.OPENVPN_DOWN_ROOT_PATH
- if os.path.isfile(abs_path_in_system):
- return abs_path_in_system
-
- logger.warning("We could not find the down-root-plugin, so no updown "
- "scripts will be run. DNS leaks are likely!")
- return None
-
- def get_vpn_command(self, eipconfig=None, providerconfig=None,
- socket_host=None, socket_port="unix", openvpn_verb=1):
- """
- Returns the platform dependant vpn launching command. It will
- look for openvpn in the regular paths and algo in
- path_prefix/apps/eip/ (in case standalone is set)
-
- Might raise:
- VPNLauncherException,
- OpenVPNNotFoundException.
-
- :param eipconfig: eip configuration object
- :type eipconfig: EIPConfig
-
- :param providerconfig: provider specific configuration
- :type providerconfig: ProviderConfig
-
- :param socket_host: either socket path (unix) or socket IP
- :type socket_host: str
-
- :param socket_port: either string "unix" if it's a unix
- socket, or port otherwise
- :type socket_port: str
-
- :param openvpn_verb: openvpn verbosity wanted
- :type openvpn_verb: int
-
- :return: A VPN command ready to be launched
- :rtype: list
- """
- leap_assert(eipconfig, "We need an eip config")
- leap_assert_type(eipconfig, EIPConfig)
- leap_assert(providerconfig, "We need a provider config")
- leap_assert_type(providerconfig, ProviderConfig)
- leap_assert(socket_host, "We need a socket host!")
- leap_assert(socket_port, "We need a socket port!")
-
- kwargs = {}
- if flags.STANDALONE:
- kwargs['path_extension'] = os.path.join(
- get_path_prefix(), "..", "apps", "eip")
-
- openvpn_possibilities = which(self.OPENVPN_BIN, **kwargs)
-
- if len(openvpn_possibilities) == 0:
- raise OpenVPNNotFoundException()
-
- openvpn = first(openvpn_possibilities)
- args = []
-
- pkexec = self.maybe_pkexec()
- if pkexec:
- args.append(openvpn)
- openvpn = first(pkexec)
-
- args += [
- '--setenv', "LEAPOPENVPN", "1"
- ]
-
- if openvpn_verb is not None:
- args += ['--verb', '%d' % (openvpn_verb,)]
-
- gateways = []
- leap_settings = LeapSettings()
- domain = providerconfig.get_domain()
- gateway_conf = leap_settings.get_selected_gateway(domain)
-
- if gateway_conf == leap_settings.GATEWAY_AUTOMATIC:
- gateway_selector = VPNGatewaySelector(eipconfig)
- gateways = gateway_selector.get_gateways()
- else:
- gateways = [gateway_conf]
-
- if not gateways:
- logger.error('No gateway was found!')
- raise VPNLauncherException(self.tr('No gateway was found!'))
-
- logger.debug("Using gateways ips: {0}".format(', '.join(gateways)))
-
- for gw in gateways:
- args += ['--remote', gw, '1194', 'udp']
-
- args += [
- '--client',
- '--dev', 'tun',
- ##############################################################
- # persist-tun makes ping-restart fail because it leaves a
- # broken routing table
- ##############################################################
- # '--persist-tun',
- '--persist-key',
- '--tls-client',
- '--remote-cert-tls',
- 'server'
- ]
-
- openvpn_configuration = eipconfig.get_openvpn_configuration()
-
- for key, value in openvpn_configuration.items():
- args += ['--%s' % (key,), value]
-
- ##############################################################
- # The down-root plugin fails in some situations, so we don't
- # drop privs for the time being
- ##############################################################
- # args += [
- # '--user', getpass.getuser(),
- # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name
- # ]
-
- if socket_port == "unix": # that's always the case for linux
- args += [
- '--management-client-user', getpass.getuser()
- ]
-
- args += [
- '--management-signal',
- '--management', socket_host, socket_port,
- '--script-security', '2'
- ]
-
- plugin_path = self.maybe_down_plugin()
- # If we do not have the down plugin neither in the bundle
- # nor in the system, we do not do updown scripts. The alternative
- # is leaving the user without the ability to restore dns and routes
- # to its original state.
-
- if plugin_path and _has_updown_scripts(self.UP_DOWN_PATH):
- args += [
- '--up', self.UP_DOWN_PATH,
- '--down', self.UP_DOWN_PATH,
- ##############################################################
- # For the time being we are disabling the usage of the
- # down-root plugin, because it doesn't quite work as
- # expected (i.e. it doesn't run route -del as root
- # when finishing, so it fails to properly
- # restart/quit)
- ##############################################################
- # '--plugin', plugin_path,
- # '\'script_type=down %s\'' % self.UP_DOWN_PATH
- ]
-
- args += [
- '--cert', eipconfig.get_client_cert_path(providerconfig),
- '--key', eipconfig.get_client_cert_path(providerconfig),
- '--ca', providerconfig.get_ca_cert_path()
- ]
-
- logger.debug("Running VPN with command:")
- logger.debug("%s %s" % (openvpn, " ".join(args)))
-
- return [openvpn] + args
-
- def get_vpn_env(self):
- """
- Returns a dictionary with the custom env for the platform.
- This is mainly used for setting LD_LIBRARY_PATH to the correct
- path when distributing a standalone client
-
- :rtype: dict
- """
- return {
- "LD_LIBRARY_PATH": os.path.join(get_path_prefix(), "..", "lib")
- }
-
-
-class DarwinVPNLauncher(VPNLauncher):
- """
- VPN launcher for the Darwin Platform
- """
-
- COCOASUDO = "cocoasudo"
- # XXX need the good old magic translate for these strings
- # (look for magic in 0.2.0 release)
- SUDO_MSG = ("Bitmask needs administrative privileges to run "
- "Encrypted Internet.")
- INSTALL_MSG = ("\"Bitmask needs administrative privileges to install "
- "missing scripts and fix permissions.\"")
-
- INSTALL_PATH = os.path.realpath(os.getcwd() + "/../../")
- INSTALL_PATH_ESCAPED = os.path.realpath(os.getcwd() + "/../../")
- OPENVPN_BIN = 'openvpn.leap'
- OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,)
- OPENVPN_PATH_ESCAPED = "%s/Contents/Resources/openvpn" % (
- INSTALL_PATH_ESCAPED,)
-
- UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,)
- DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,)
- OPENVPN_DOWN_PLUGIN = '%s/openvpn-down-root.so' % (OPENVPN_PATH,)
-
- UPDOWN_FILES = (UP_SCRIPT, DOWN_SCRIPT, OPENVPN_DOWN_PLUGIN)
- OTHER_FILES = []
-
- @classmethod
- def cmd_for_missing_scripts(kls, frompath):
- """
- Returns a command that can copy the missing scripts.
- :rtype: str
- """
- to = kls.OPENVPN_PATH_ESCAPED
- cmd = "#!/bin/sh\nmkdir -p %s\ncp \"%s/\"* %s\nchmod 744 %s/*" % (
- to, frompath, to, to)
- return cmd
-
- @classmethod
- def maybe_kextloaded(kls):
- """
- Checks if the needed kext is loaded before launching openvpn.
- """
- return bool(commands.getoutput('kextstat | grep "leap.tun"'))
-
- def _get_resource_path(self):
- """
- Returns the absolute path to the app resources directory
-
- :rtype: str
- """
- return os.path.abspath(
- os.path.join(
- os.getcwd(),
- "../../Contents/Resources"))
-
- def _get_icon_path(self):
- """
- Returns the absolute path to the app icon
-
- :rtype: str
- """
- return os.path.join(self._get_resource_path(),
- "leap-client.tiff")
-
- def get_cocoasudo_ovpn_cmd(self):
- """
- Returns a string with the cocoasudo command needed to run openvpn
- as admin with a nice password prompt. The actual command needs to be
- appended.
-
- :rtype: (str, list)
- """
- iconpath = self._get_icon_path()
- has_icon = os.path.isfile(iconpath)
- args = ["--icon=%s" % iconpath] if has_icon else []
- args.append("--prompt=%s" % (self.SUDO_MSG,))
-
- return self.COCOASUDO, args
-
- def get_cocoasudo_installmissing_cmd(self):
- """
- Returns a string with the cocoasudo command needed to install missing
- files as admin with a nice password prompt. The actual command needs to
- be appended.
-
- :rtype: (str, list)
- """
- iconpath = self._get_icon_path()
- has_icon = os.path.isfile(iconpath)
- args = ["--icon=%s" % iconpath] if has_icon else []
- args.append("--prompt=%s" % (self.INSTALL_MSG,))
-
- return self.COCOASUDO, args
-
- def get_vpn_command(self, eipconfig=None, providerconfig=None,
- socket_host=None, socket_port="unix", openvpn_verb=1):
- """
- Returns the platform dependant vpn launching command
-
- Might raise VPNException.
-
- :param eipconfig: eip configuration object
- :type eipconfig: EIPConfig
-
- :param providerconfig: provider specific configuration
- :type providerconfig: ProviderConfig
-
- :param socket_host: either socket path (unix) or socket IP
- :type socket_host: str
-
- :param socket_port: either string "unix" if it's a unix
- socket, or port otherwise
- :type socket_port: str
-
- :param openvpn_verb: openvpn verbosity wanted
- :type openvpn_verb: int
-
- :return: A VPN command ready to be launched
- :rtype: list
- """
- leap_assert(eipconfig, "We need an eip config")
- leap_assert_type(eipconfig, EIPConfig)
- leap_assert(providerconfig, "We need a provider config")
- leap_assert_type(providerconfig, ProviderConfig)
- leap_assert(socket_host, "We need a socket host!")
- leap_assert(socket_port, "We need a socket port!")
-
- if not self.maybe_kextloaded():
- raise EIPNoTunKextLoaded
-
- kwargs = {}
- if flags.STANDALONE:
- kwargs['path_extension'] = os.path.join(
- get_path_prefix(), "..", "apps", "eip")
-
- openvpn_possibilities = which(
- self.OPENVPN_BIN,
- **kwargs)
- if len(openvpn_possibilities) == 0:
- raise OpenVPNNotFoundException()
-
- openvpn = first(openvpn_possibilities)
- args = [openvpn]
-
- args += [
- '--setenv', "LEAPOPENVPN", "1"
- ]
-
- if openvpn_verb is not None:
- args += ['--verb', '%d' % (openvpn_verb,)]
-
- gateways = []
- leap_settings = LeapSettings()
- domain = providerconfig.get_domain()
- gateway_conf = leap_settings.get_selected_gateway(domain)
-
- if gateway_conf == leap_settings.GATEWAY_AUTOMATIC:
- gateway_selector = VPNGatewaySelector(eipconfig)
- gateways = gateway_selector.get_gateways()
- else:
- gateways = [gateway_conf]
-
- if not gateways:
- logger.error('No gateway was found!')
- raise VPNLauncherException(self.tr('No gateway was found!'))
-
- logger.debug("Using gateways ips: {0}".format(', '.join(gateways)))
-
- for gw in gateways:
- args += ['--remote', gw, '1194', 'udp']
-
- args += [
- '--client',
- '--dev', 'tun',
- ##############################################################
- # persist-tun makes ping-restart fail because it leaves a
- # broken routing table
- ##############################################################
- # '--persist-tun',
- '--persist-key',
- '--tls-client',
- '--remote-cert-tls',
- 'server'
- ]
-
- openvpn_configuration = eipconfig.get_openvpn_configuration()
- for key, value in openvpn_configuration.items():
- args += ['--%s' % (key,), value]
-
- user = getpass.getuser()
-
- ##############################################################
- # The down-root plugin fails in some situations, so we don't
- # drop privs for the time being
- ##############################################################
- # args += [
- # '--user', user,
- # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name
- # ]
-
- if socket_port == "unix":
- args += [
- '--management-client-user', user
- ]
-
- args += [
- '--management-signal',
- '--management', socket_host, socket_port,
- '--script-security', '2'
- ]
-
- if _has_updown_scripts(self.UP_SCRIPT):
- args += [
- '--up', '\"%s\"' % (self.UP_SCRIPT,),
- ]
-
- if _has_updown_scripts(self.DOWN_SCRIPT):
- args += [
- '--down', '\"%s\"' % (self.DOWN_SCRIPT,)
- ]
-
- # should have the down script too
- if _has_updown_scripts(self.OPENVPN_DOWN_PLUGIN):
- args += [
- ###########################################################
- # For the time being we are disabling the usage of the
- # down-root plugin, because it doesn't quite work as
- # expected (i.e. it doesn't run route -del as root
- # when finishing, so it fails to properly
- # restart/quit)
- ###########################################################
- # '--plugin', self.OPENVPN_DOWN_PLUGIN,
- # '\'%s\'' % self.DOWN_SCRIPT
- ]
-
- # we set user to be passed to the up/down scripts
- args += [
- '--setenv', "LEAPUSER", "%s" % (user,)]
-
- args += [
- '--cert', eipconfig.get_client_cert_path(providerconfig),
- '--key', eipconfig.get_client_cert_path(providerconfig),
- '--ca', providerconfig.get_ca_cert_path()
- ]
-
- command, cargs = self.get_cocoasudo_ovpn_cmd()
- cmd_args = cargs + args
-
- logger.debug("Running VPN with command:")
- logger.debug("%s %s" % (command, " ".join(cmd_args)))
-
- return [command] + cmd_args
-
- def get_vpn_env(self):
- """
- Returns a dictionary with the custom env for the platform.
- This is mainly used for setting LD_LIBRARY_PATH to the correct
- path when distributing a standalone client
-
- :rtype: dict
- """
- return {
- "DYLD_LIBRARY_PATH": os.path.join(get_path_prefix(), "..", "lib")
- }
-
-
-class WindowsVPNLauncher(VPNLauncher):
- """
- VPN launcher for the Windows platform
- """
-
- OPENVPN_BIN = 'openvpn_leap.exe'
-
- # XXX UPDOWN_FILES ... we do not have updown files defined yet!
- # (and maybe we won't)
-
- def get_vpn_command(self, eipconfig=None, providerconfig=None,
- socket_host=None, socket_port="9876", openvpn_verb=1):
- """
- Returns the platform dependant vpn launching command. It will
- look for openvpn in the regular paths and algo in
- path_prefix/apps/eip/ (in case standalone is set)
-
- Might raise VPNException.
-
- :param eipconfig: eip configuration object
- :type eipconfig: EIPConfig
-
- :param providerconfig: provider specific configuration
- :type providerconfig: ProviderConfig
-
- :param socket_host: either socket path (unix) or socket IP
- :type socket_host: str
-
- :param socket_port: either string "unix" if it's a unix
- socket, or port otherwise
- :type socket_port: str
-
- :param openvpn_verb: the openvpn verbosity wanted
- :type openvpn_verb: int
-
- :return: A VPN command ready to be launched
- :rtype: list
- """
- leap_assert(eipconfig, "We need an eip config")
- leap_assert_type(eipconfig, EIPConfig)
- leap_assert(providerconfig, "We need a provider config")
- leap_assert_type(providerconfig, ProviderConfig)
- leap_assert(socket_host, "We need a socket host!")
- leap_assert(socket_port, "We need a socket port!")
- leap_assert(socket_port != "unix",
- "We cannot use unix sockets in windows!")
-
- openvpn_possibilities = which(
- self.OPENVPN_BIN,
- path_extension=os.path.join(get_path_prefix(),
- "..", "apps", "eip"))
-
- if len(openvpn_possibilities) == 0:
- raise OpenVPNNotFoundException()
-
- openvpn = first(openvpn_possibilities)
- args = []
-
- args += [
- '--setenv', "LEAPOPENVPN", "1"
- ]
-
- if openvpn_verb is not None:
- args += ['--verb', '%d' % (openvpn_verb,)]
-
- gateways = []
- leap_settings = LeapSettings()
- domain = providerconfig.get_domain()
- gateway_conf = leap_settings.get_selected_gateway(domain)
-
- if gateway_conf == leap_settings.GATEWAY_AUTOMATIC:
- gateway_selector = VPNGatewaySelector(eipconfig)
- gateways = gateway_selector.get_gateways()
- else:
- gateways = [gateway_conf]
-
- if not gateways:
- logger.error('No gateway was found!')
- raise VPNLauncherException(self.tr('No gateway was found!'))
-
- logger.debug("Using gateways ips: {0}".format(', '.join(gateways)))
-
- for gw in gateways:
- args += ['--remote', gw, '1194', 'udp']
-
- args += [
- '--client',
- '--dev', 'tun',
- ##############################################################
- # persist-tun makes ping-restart fail because it leaves a
- # broken routing table
- ##############################################################
- # '--persist-tun',
- '--persist-key',
- '--tls-client',
- # We make it log to a file because we cannot attach to the
- # openvpn process' stdout since it's a process with more
- # privileges than we are
- '--log-append', 'eip.log',
- '--remote-cert-tls',
- 'server'
- ]
-
- openvpn_configuration = eipconfig.get_openvpn_configuration()
- for key, value in openvpn_configuration.items():
- args += ['--%s' % (key,), value]
-
- ##############################################################
- # The down-root plugin fails in some situations, so we don't
- # drop privs for the time being
- ##############################################################
- # args += [
- # '--user', getpass.getuser(),
- # #'--group', grp.getgrgid(os.getgroups()[-1]).gr_name
- # ]
-
- args += [
- '--management-signal',
- '--management', socket_host, socket_port,
- '--script-security', '2'
- ]
-
- args += [
- '--cert', eipconfig.get_client_cert_path(providerconfig),
- '--key', eipconfig.get_client_cert_path(providerconfig),
- '--ca', providerconfig.get_ca_cert_path()
- ]
-
- logger.debug("Running VPN with command:")
- logger.debug("%s %s" % (openvpn, " ".join(args)))
-
- return [openvpn] + args
-
- def get_vpn_env(self):
- """
- Returns a dictionary with the custom env for the platform.
- This is mainly used for setting LD_LIBRARY_PATH to the correct
- path when distributing a standalone client
-
- :rtype: dict
- """
- return {}
-
-
-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()
- eipconfig.set_api_version('1')
- 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")
diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py
index 15ac812b..707967e0 100644
--- a/src/leap/bitmask/services/eip/vpnprocess.py
+++ b/src/leap/bitmask/services/eip/vpnprocess.py
@@ -27,7 +27,7 @@ import socket
from PySide import QtCore
from leap.bitmask.config.providerconfig import ProviderConfig
-from leap.bitmask.services.eip.vpnlaunchers import get_platform_launcher
+from leap.bitmask.services.eip import get_vpn_launcher
from leap.bitmask.services.eip.eipconfig import EIPConfig
from leap.bitmask.services.eip.udstelnet import UDSTelnet
from leap.bitmask.util import first
@@ -697,7 +697,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
self._socket_host = socket_host
self._socket_port = socket_port
- self._launcher = get_platform_launcher()
+ self._launcher = get_vpn_launcher()
self._last_state = None
self._last_status = None
diff --git a/src/leap/bitmask/services/eip/windowsvpnlauncher.py b/src/leap/bitmask/services/eip/windowsvpnlauncher.py
new file mode 100644
index 00000000..3f1ed43b
--- /dev/null
+++ b/src/leap/bitmask/services/eip/windowsvpnlauncher.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+# windowsvpnlauncher.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/>.
+"""
+Windows VPN launcher implementation.
+"""
+import logging
+
+from leap.bitmask.services.eip.vpnlauncher import VPNLauncher
+from leap.common.check import leap_assert
+
+logger = logging.getLogger(__name__)
+
+
+class WindowsVPNLauncher(VPNLauncher):
+ """
+ VPN launcher for the Windows platform
+ """
+
+ OPENVPN_BIN = 'openvpn_leap.exe'
+
+ # XXX UPDOWN_FILES ... we do not have updown files defined yet!
+ # (and maybe we won't)
+ @classmethod
+ def get_vpn_command(kls, eipconfig, providerconfig, socket_host,
+ socket_port="9876", openvpn_verb=1):
+ """
+ Returns the Windows implementation for the vpn launching command.
+
+ Might raise:
+ OpenVPNNotFoundException,
+ VPNLauncherException.
+
+ :param eipconfig: eip configuration object
+ :type eipconfig: EIPConfig
+ :param providerconfig: provider specific configuration
+ :type providerconfig: ProviderConfig
+ :param socket_host: either socket path (unix) or socket IP
+ :type socket_host: str
+ :param socket_port: either string "unix" if it's a unix socket,
+ or port otherwise
+ :type socket_port: str
+ :param openvpn_verb: the openvpn verbosity wanted
+ :type openvpn_verb: int
+
+ :return: A VPN command ready to be launched.
+ :rtype: list
+ """
+ leap_assert(socket_port != "unix",
+ "We cannot use unix sockets in windows!")
+
+ # we use `super` in order to send the class to use
+ command = super(WindowsVPNLauncher, kls).get_vpn_command(
+ eipconfig, providerconfig, socket_host, socket_port, openvpn_verb)
+
+ return command
diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py
index cac91440..7968dd6a 100644
--- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py
+++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py
@@ -14,27 +14,28 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
"""
Soledad bootstrapping
"""
-
import logging
import os
import socket
+from ssl import SSLError
+
from PySide import QtCore
from u1db import errors as u1db_errors
from leap.bitmask.config import flags
from leap.bitmask.config.providerconfig import ProviderConfig
from leap.bitmask.crypto.srpauth import SRPAuth
+from leap.bitmask.services import download_service_config
from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper
from leap.bitmask.services.soledad.soledadconfig import SoledadConfig
-from leap.bitmask.util.request_helpers import get_content
+from leap.bitmask.util import is_file, is_empty_file
from leap.bitmask.util import get_path_prefix
-from leap.common.check import leap_assert, leap_assert_type
-from leap.common.files import get_mtime
+from leap.common.check import leap_assert, leap_assert_type, leap_check
+from leap.common.files import which
from leap.keymanager import KeyManager, openpgp
from leap.keymanager.errors import KeyNotFound
from leap.soledad.client import Soledad
@@ -42,17 +43,28 @@ from leap.soledad.client import Soledad
logger = logging.getLogger(__name__)
+# TODO these exceptions could be moved to soledad itself
+# after settling this down.
+
+class SoledadSyncError(Exception):
+ message = "Error while syncing Soledad"
+
+
+class SoledadInitError(Exception):
+ message = "Error while initializing Soledad"
+
+
class SoledadBootstrapper(AbstractBootstrapper):
"""
Soledad init procedure
"""
-
SOLEDAD_KEY = "soledad"
KEYMANAGER_KEY = "keymanager"
PUBKEY_KEY = "user[public_key]"
MAX_INIT_RETRIES = 10
+ MAX_SYNC_RETRIES = 10
# All dicts returned are of the form
# {"passed": bool, "error": str}
@@ -68,6 +80,7 @@ class SoledadBootstrapper(AbstractBootstrapper):
self._soledad_config = None
self._keymanager = None
self._download_if_needed = False
+
self._user = ""
self._password = ""
self._srpauth = None
@@ -109,68 +122,169 @@ class SoledadBootstrapper(AbstractBootstrapper):
"""
self._soledad_retries += 1
+ def _get_db_paths(self, uuid):
+ """
+ Returns the secrets and local db paths needed for soledad
+ initialization
+
+ :param uuid: uuid for user
+ :type uuid: str
+
+ :return: a tuple with secrets, local_db paths
+ :rtype: tuple
+ """
+ prefix = os.path.join(get_path_prefix(), "leap", "soledad")
+ secrets = "%s/%s.secret" % (prefix, uuid)
+ local_db = "%s/%s.db" % (prefix, uuid)
+
+ # We remove an empty file if found to avoid complains
+ # about the db not being properly initialized
+ if is_file(local_db) and is_empty_file(local_db):
+ try:
+ os.remove(local_db)
+ except OSError:
+ logger.warning("Could not remove empty file %s"
+ % local_db)
+ return secrets, local_db
+
# initialization
def load_and_sync_soledad(self):
"""
Once everthing is in the right place, we instantiate and sync
Soledad
-
- :param srp_auth: SRPAuth object used
- :type srp_auth: SRPAuth
"""
- srp_auth = self.srpauth
- uuid = srp_auth.get_uid()
+ # TODO this method is still too large
+ uuid = self.srpauth.get_uid()
+ token = self.srpauth.get_token()
- prefix = os.path.join(get_path_prefix(), "leap", "soledad")
- secrets_path = "%s/%s.secret" % (prefix, uuid)
- local_db_path = "%s/%s.db" % (prefix, uuid)
+ secrets_path, local_db_path = self._get_db_paths(uuid)
# TODO: Select server based on timezone (issue #3308)
server_dict = self._soledad_config.get_hosts()
- if server_dict.keys():
- selected_server = server_dict[server_dict.keys()[0]]
- server_url = "https://%s:%s/user-%s" % (
- selected_server["hostname"],
- selected_server["port"],
- uuid)
+ if not server_dict.keys():
+ # XXX raise more specific exception, and catch it properly!
+ raise Exception("No soledad server found")
+
+ selected_server = server_dict[server_dict.keys()[0]]
+ server_url = "https://%s:%s/user-%s" % (
+ selected_server["hostname"],
+ selected_server["port"],
+ uuid)
+ logger.debug("Using soledad server url: %s" % (server_url,))
- logger.debug("Using soledad server url: %s" % (server_url,))
+ cert_file = self._provider_config.get_ca_cert_path()
- cert_file = self._provider_config.get_ca_cert_path()
+ logger.debug('local_db:%s' % (local_db_path,))
+ logger.debug('secrets_path:%s' % (secrets_path,))
- # TODO: If selected server fails, retry with another host
- # (issue #3309)
+ try:
+ self._try_soledad_init(
+ uuid, secrets_path, local_db_path,
+ server_url, cert_file, token)
+ except:
+ # re-raise the exceptions from try_init,
+ # we're currently handling the retries from the
+ # soledad-launcher in the gui.
+ raise
+
+ leap_check(self._soledad is not None,
+ "Null soledad, error while initializing")
+
+ # and now, let's sync
+ sync_tries = self.MAX_SYNC_RETRIES
+ while sync_tries > 0:
try:
- self._soledad = Soledad(
- uuid,
- self._password.encode("utf-8"),
- secrets_path=secrets_path,
- local_db_path=local_db_path,
- server_url=server_url,
- cert_file=cert_file,
- auth_token=srp_auth.get_token())
- self._soledad.sync()
-
- # XXX All these errors should be handled by soledad itself,
- # and return a subclass of SoledadInitializationFailed
- except socket.timeout:
- logger.debug("SOLEDAD TIMED OUT...")
- self.soledad_timeout.emit()
- except socket.error as exc:
- logger.error("Socket error while initializing soledad")
- self.soledad_failed.emit()
- except u1db_errors.Unauthorized:
- logger.error("Error while initializing soledad "
- "(unauthorized).")
- self.soledad_failed.emit()
- except Exception as exc:
- logger.error("Unhandled error while initializating "
+ self._try_soledad_sync()
+
+ # at this point, sometimes the client
+ # gets stuck and does not progress to
+ # the _gen_key step. XXX investigate.
+ logger.debug("Soledad has been synced.")
+ # so long, and thanks for all the fish
+ return
+ except SoledadSyncError:
+ # maybe it's my connection, but I'm getting
+ # ssl handshake timeouts and read errors quite often.
+ # A particularly big sync is a disaster.
+ # This deserves further investigation, maybe the
+ # retry strategy can be pushed to u1db, or at least
+ # it's something worthy to talk about with the
+ # ubuntu folks.
+ sync_tries -= 1
+ continue
+
+ # reached bottom, failed to sync
+ # and there's nothing we can do...
+ self.soledad_failed.emit()
+ raise SoledadSyncError()
+
+ def _try_soledad_init(self, uuid, secrets_path, local_db_path,
+ server_url, cert_file, auth_token):
+ """
+ Tries to initialize soledad.
+
+ :param uuid: user identifier
+ :param secrets_path: path to secrets file
+ :param local_db_path: path to local db file
+ :param server_url: soledad server uri
+ :param cert_file: path to the certificate of the ca used
+ to validate the SSL certificate used by the remote
+ soledad server.
+ :type cert_file: str
+ :param auth token: auth token
+ :type auth_token: str
+ """
+ # TODO: If selected server fails, retry with another host
+ # (issue #3309)
+ try:
+ self._soledad = Soledad(
+ uuid,
+ self._password.encode("utf-8"),
+ secrets_path=secrets_path,
+ local_db_path=local_db_path,
+ server_url=server_url,
+ cert_file=cert_file,
+ auth_token=auth_token)
+
+ # XXX All these errors should be handled by soledad itself,
+ # and return a subclass of SoledadInitializationFailed
+
+ # recoverable, will guarantee retries
+ except socket.timeout:
+ logger.debug("SOLEDAD initialization TIMED OUT...")
+ self.soledad_timeout.emit()
+ except socket.error as exc:
+ logger.error("Socket error while initializing soledad")
+ self.soledad_timeout.emit()
+
+ # unrecoverable
+ except u1db_errors.Unauthorized:
+ logger.error("Error while initializing soledad "
+ "(unauthorized).")
+ self.soledad_failed.emit()
+ except Exception as exc:
+ logger.exception("Unhandled error while initializating "
"soledad: %r" % (exc,))
- raise
- else:
- raise Exception("No soledad server found")
+ self.soledad_failed.emit()
+
+ def _try_soledad_sync(self):
+ """
+ Tries to sync soledad.
+ Raises SoledadSyncError if not successful.
+ """
+ try:
+ logger.error("trying to sync soledad....")
+ self._soledad.sync()
+ except SSLError as exc:
+ logger.error("%r" % (exc,))
+ raise SoledadSyncError("Failed to sync soledad")
+ except Exception as exc:
+ logger.exception("Unhandled error while syncing"
+ "soledad: %r" % (exc,))
+ self.soledad_failed.emit()
+ raise SoledadSyncError("Failed to sync soledad")
def _download_config(self):
"""
@@ -179,86 +293,57 @@ class SoledadBootstrapper(AbstractBootstrapper):
leap_assert(self._provider_config,
"We need a provider configuration!")
-
logger.debug("Downloading Soledad config for %s" %
(self._provider_config.get_domain(),))
self._soledad_config = SoledadConfig()
-
- headers = {}
- mtime = get_mtime(
- os.path.join(get_path_prefix(), "leap", "providers",
- self._provider_config.get_domain(),
- "soledad-service.json"))
-
- if self._download_if_needed and mtime:
- headers['if-modified-since'] = mtime
-
- api_version = self._provider_config.get_api_version()
-
- # there is some confusion with this uri,
- config_uri = "%s/%s/config/soledad-service.json" % (
- self._provider_config.get_api_uri(),
- api_version)
- logger.debug('Downloading soledad config from: %s' % config_uri)
-
- # TODO factor out this srpauth protected get (make decorator)
- srp_auth = self.srpauth
- session_id = srp_auth.get_session_id()
- cookies = None
- if session_id:
- cookies = {"_session_id": session_id}
-
- res = self._session.get(config_uri,
- verify=self._provider_config
- .get_ca_cert_path(),
- headers=headers,
- cookies=cookies)
- res.raise_for_status()
-
- self._soledad_config.set_api_version(api_version)
-
- # Not modified
- if res.status_code == 304:
- logger.debug("Soledad definition has not been modified")
- self._soledad_config.load(
- os.path.join(
- "leap", "providers",
- self._provider_config.get_domain(),
- "soledad-service.json"))
- else:
- soledad_definition, mtime = get_content(res)
-
- self._soledad_config.load(data=soledad_definition, mtime=mtime)
- self._soledad_config.save(["leap",
- "providers",
- self._provider_config.get_domain(),
- "soledad-service.json"])
-
+ download_service_config(
+ self._provider_config,
+ self._soledad_config,
+ self._session,
+ self._download_if_needed)
+
+ # soledad config is ok, let's proceed to load and sync soledad
+ # XXX but honestly, this is a pretty strange entry point for that.
+ # it feels like it should be the other way around:
+ # load_and_sync, and from there, if needed, call download_config
self.load_and_sync_soledad()
- def _gen_key(self, _):
+ def _get_gpg_bin_path(self):
"""
- Generates the key pair if needed, uploads it to the webapp and
- nickserver
+ Returns the path to gpg binary.
+ :returns: the gpg binary path
+ :rtype: str
"""
- leap_assert(self._provider_config,
- "We need a provider configuration!")
-
- address = "%s@%s" % (self._user, self._provider_config.get_domain())
-
- logger.debug("Retrieving key for %s" % (address,))
-
- srp_auth = self.srpauth
-
- # TODO: use which implementation with known paths
# TODO: Fix for Windows
- gpgbin = "/usr/bin/gpg"
-
+ gpgbin = None
if flags.STANDALONE:
- gpgbin = os.path.join(get_path_prefix(),
- "..", "apps", "mail", "gpg")
-
+ gpgbin = os.path.join(
+ get_path_prefix(), "..", "apps", "mail", "gpg")
+ else:
+ try:
+ gpgbin_options = which("gpg")
+ # gnupg checks that the path to the binary is not a
+ # symlink, so we need to filter those and come up with
+ # just one option.
+ for opt in gpgbin_options:
+ if not os.path.islink(opt):
+ gpgbin = opt
+ break
+ except IndexError as e:
+ logger.debug("Couldn't find the gpg binary!")
+ logger.exception(e)
+ leap_check(gpgbin is not None, "Could not find gpg binary")
+ return gpgbin
+
+ def _init_keymanager(self, address):
+ """
+ Initializes the keymanager.
+ :param address: the address to initialize the keymanager with.
+ :type address: str
+ """
+ srp_auth = self.srpauth
+ logger.debug('initializing keymanager...')
self._keymanager = KeyManager(
address,
"https://nicknym.%s:6425" % (self._provider_config.get_domain(),),
@@ -269,15 +354,46 @@ class SoledadBootstrapper(AbstractBootstrapper):
api_uri=self._provider_config.get_api_uri(),
api_version=self._provider_config.get_api_version(),
uid=srp_auth.get_uid(),
- gpgbinary=gpgbin)
+ gpgbinary=self._get_gpg_bin_path())
+
+ def _gen_key(self, _):
+ """
+ Generates the key pair if needed, uploads it to the webapp and
+ nickserver
+ """
+ leap_assert(self._provider_config is not None,
+ "We need a provider configuration!")
+ leap_assert(self._soledad is not None,
+ "We need a non-null soledad to generate keys")
+
+ address = "%s@%s" % (self._user, self._provider_config.get_domain())
+ self._init_keymanager(address)
+ logger.debug("Retrieving key for %s" % (address,))
+
try:
- self._keymanager.get_key(address, openpgp.OpenPGPKey,
- private=True, fetch_remote=False)
+ self._keymanager.get_key(
+ address, openpgp.OpenPGPKey, private=True, fetch_remote=False)
+ return
except KeyNotFound:
logger.debug("Key not found. Generating key for %s" % (address,))
+
+ # generate key
+ try:
self._keymanager.gen_key(openpgp.OpenPGPKey)
+ except Exception as exc:
+ logger.error("error while generating key!")
+ logger.exception(exc)
+ raise
+
+ # send key
+ try:
self._keymanager.send_key(openpgp.OpenPGPKey)
- logger.debug("Key generated successfully.")
+ except Exception as exc:
+ logger.error("error while sending key!")
+ logger.exception(exc)
+ raise
+
+ logger.debug("Key generated successfully.")
def run_soledad_setup_checks(self,
provider_config,