diff options
author | Tomás Touceda <chiiph@leap.se> | 2013-10-04 12:09:58 -0300 |
---|---|---|
committer | Tomás Touceda <chiiph@leap.se> | 2013-10-04 12:09:58 -0300 |
commit | 584866689560bd2ebea01ecc5e6ae5e79ce7fc81 (patch) | |
tree | 4fe300c3c105e17721f79eeece9bbd3745b43d78 /src/leap/bitmask/services | |
parent | 1f0f8efc4cb985c082b3b8fe7b3dc45aed047a47 (diff) | |
parent | 759d73ae0728f074e0fb0740269249d0e5066574 (diff) |
Merge branch 'release-0.3.4'
Diffstat (limited to 'src/leap/bitmask/services')
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, |