diff options
17 files changed, 881 insertions, 1010 deletions
| diff --git a/changes/VERSION_COMPAT b/changes/VERSION_COMPAT index cc00ecf7..425478c8 100644 --- a/changes/VERSION_COMPAT +++ b/changes/VERSION_COMPAT @@ -8,3 +8,4 @@  #  # BEGIN DEPENDENCY LIST -------------------------  # leap.foo.bar>=x.y.z +leap.common >= 0.3.4  # because the ca_bundle diff --git a/changes/feature-2858_refactor-vpnlaunchers b/changes/feature-2858_refactor-vpnlaunchers new file mode 100644 index 00000000..27106f7a --- /dev/null +++ b/changes/feature-2858_refactor-vpnlaunchers @@ -0,0 +1,2 @@ +  o Refactor vpn launchers, reuse code, improve implementations, update +    documentation. Closes #2858. diff --git a/changes/feature_provider-check-against-ca-bundle b/changes/feature_provider-check-against-ca-bundle new file mode 100644 index 00000000..b3f9042f --- /dev/null +++ b/changes/feature_provider-check-against-ca-bundle @@ -0,0 +1,2 @@ +  o Make the initial provider cert verifications against our modified +    CA-bundle (includes ca-cert certificates, for now). Closes: #3850 diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 1e437999..e74258a8 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -36,9 +36,9 @@ from leap.bitmask.gui import statemachines  from leap.bitmask.gui.statuspanel import StatusPanelWidget  from leap.bitmask.gui.wizard import Wizard +from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper  from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper  from leap.bitmask.services.eip.eipconfig import EIPConfig -from leap.bitmask.services.eip.providerbootstrapper import ProviderBootstrapper  # XXX: Soledad might not work out of the box in Windows, issue #2932  from leap.bitmask.services.soledad.soledadbootstrapper import \      SoledadBootstrapper @@ -53,12 +53,12 @@ from leap.bitmask.services.eip.vpnprocess import VPN  from leap.bitmask.services.eip.vpnprocess import OpenVPNAlreadyRunning  from leap.bitmask.services.eip.vpnprocess import AlienOpenVPNAlreadyRunning -from leap.bitmask.services.eip.vpnlaunchers import VPNLauncherException -from leap.bitmask.services.eip.vpnlaunchers import OpenVPNNotFoundException -from leap.bitmask.services.eip.vpnlaunchers import EIPNoPkexecAvailable -from leap.bitmask.services.eip.vpnlaunchers import \ +from leap.bitmask.services.eip.vpnlauncher import VPNLauncherException +from leap.bitmask.services.eip.vpnlauncher import OpenVPNNotFoundException +from leap.bitmask.services.eip.linuxvpnlauncher import EIPNoPkexecAvailable +from leap.bitmask.services.eip.linuxvpnlauncher import \      EIPNoPolkitAuthAgentAvailable -from leap.bitmask.services.eip.vpnlaunchers import EIPNoTunKextLoaded +from leap.bitmask.services.eip.darwinvpnlauncher import EIPNoTunKextLoaded  from leap.bitmask.util.keyring_helpers import has_keyring  from leap.bitmask.util.leap_log_handler import LeapLogHandler diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index 45734b81..bb38b136 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -14,7 +14,6 @@  #  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -  """  First run wizard  """ @@ -27,15 +26,13 @@ from functools import partial  from PySide import QtCore, QtGui  from twisted.internet import threads -from leap.bitmask.config import flags  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.crypto.srpregister import SRPRegister -from leap.bitmask.util.privilege_policies import is_missing_policy_permissions +from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper +from leap.bitmask.services import get_service_display_name, get_supported  from leap.bitmask.util.request_helpers import get_content  from leap.bitmask.util.keyring_helpers import has_keyring  from leap.bitmask.util.password import basic_password_checks -from leap.bitmask.services.eip.providerbootstrapper import ProviderBootstrapper -from leap.bitmask.services import get_service_display_name, get_supported  from ui_wizard import Ui_Wizard diff --git a/src/leap/bitmask/platform_init/initializers.py b/src/leap/bitmask/platform_init/initializers.py index 831c6a1c..d93efbc6 100644 --- a/src/leap/bitmask/platform_init/initializers.py +++ b/src/leap/bitmask/platform_init/initializers.py @@ -29,7 +29,9 @@ import tempfile  from PySide import QtGui  from leap.bitmask.config.leapsettings import LeapSettings -from leap.bitmask.services.eip import vpnlaunchers +from leap.bitmask.services.eip import get_vpn_launcher +from leap.bitmask.services.eip.linuxvpnlauncher import LinuxVPNLauncher +from leap.bitmask.services.eip.darwinvpnlauncher import DarwinVPNLauncher  from leap.bitmask.util import first  from leap.bitmask.util import privilege_policies @@ -106,7 +108,7 @@ def check_missing():      config = LeapSettings()      alert_missing = config.get_alert_missing_scripts() -    launcher = vpnlaunchers.get_platform_launcher() +    launcher = get_vpn_launcher()      missing_scripts = launcher.missing_updown_scripts      missing_other = launcher.missing_other_files @@ -251,7 +253,7 @@ def _darwin_install_missing_scripts(badexec, notfound):          "..",          "Resources",          "openvpn") -    launcher = vpnlaunchers.DarwinVPNLauncher +    launcher = DarwinVPNLauncher      if os.path.isdir(installer_path):          fd, tempscript = tempfile.mkstemp(prefix="leap_installer-") @@ -356,7 +358,7 @@ def _linux_install_missing_scripts(badexec, notfound):      """      success = False      installer_path = os.path.join(os.getcwd(), "apps", "eip", "files") -    launcher = vpnlaunchers.LinuxVPNLauncher +    launcher = LinuxVPNLauncher      # XXX refactor with darwin, same block. diff --git a/src/leap/bitmask/services/eip/providerbootstrapper.py b/src/leap/bitmask/provider/providerbootstrapper.py index 3b7c9899..751da828 100644 --- a/src/leap/bitmask/services/eip/providerbootstrapper.py +++ b/src/leap/bitmask/provider/providerbootstrapper.py @@ -14,7 +14,6 @@  #  # 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  """ @@ -32,11 +31,11 @@ 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 import ca_bundle  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__) @@ -85,16 +84,32 @@ class ProviderBootstrapper(AbstractBootstrapper):          self._provider_config = None          self._download_if_needed = False +    @property +    def verify(self): +        """ +        Verify parameter for requests. + +        :returns: either False, if checks are skipped, or the +                  path to the ca bundle. +        :rtype: bool or str +        """ +        if self._bypass_checks: +            verify = False +        else: +            verify = ca_bundle.where() +        return verify +      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 +        # err --- but we can do it after a failure, to diagnose what went +        # wrong. Right now we're just adding connection overhead. -- kali          socket.gethostbyname(self._domain)      def _check_https(self, *args): @@ -102,24 +117,29 @@ class ProviderBootstrapper(AbstractBootstrapper):          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 +        # system to work. +        # err --- but we can do it after a failure, to diagnose what went +        # wrong. Right now we're just adding connection overhead. -- kali          try:              res = self._session.get("https://%s" % (self._domain,), -                                    verify=not self._bypass_checks, +                                    verify=self.verify,                                      timeout=REQUEST_TIMEOUT)              res.raise_for_status() -        except requests.exceptions.SSLError: +        except requests.exceptions.SSLError as exc: +            logger.exception(exc)              self._err_msg = self.tr("Provider certificate could "                                      "not be verified")              raise -        except Exception: +        except Exception as exc: +            # XXX careful!. The error might be also a SSL handshake +            # timeout error, in which case we should retry a couple of times +            # more, for cases where the ssl server gives high latencies. +            logger.exception(exc)              self._err_msg = self.tr("Provider does not support HTTPS")              raise @@ -129,11 +149,13 @@ class ProviderBootstrapper(AbstractBootstrapper):          """          leap_assert(self._domain,                      "Cannot download provider info without a domain") -          logger.debug("Downloading provider info for %s" % (self._domain)) -        headers = {} +        # -------------------------------------------------------------- +        # TODO factor out with the download routines in services. +        # Watch out! We're handling the verify paramenter differently here. +        headers = {}          provider_json = os.path.join(get_path_prefix(), "leap", "providers",                                       self._domain, "provider.json")          mtime = get_mtime(provider_json) @@ -142,16 +164,18 @@ class ProviderBootstrapper(AbstractBootstrapper):              headers['if-modified-since'] = mtime          uri = "https://%s/%s" % (self._domain, "provider.json") -        verify = not self._bypass_checks +        verify = self.verify          if mtime:  # the provider.json exists -            provider_config = ProviderConfig() -            provider_config.load(provider_json) +        # So, we're getting it from the api.* and checking against +        # the provider ca.              try: -                verify = provider_config.get_ca_cert_path() +                provider_config = ProviderConfig() +                provider_config.load(provider_json)                  uri = provider_config.get_api_uri() + '/provider.json' +                verify = provider_config.get_ca_cert_path()              except MissingCACert: -                # get_ca_cert_path fails if the certificate does not exists. +                # no ca? then download from main domain again.                  pass          logger.debug("Requesting for provider.json... " @@ -165,6 +189,9 @@ class ProviderBootstrapper(AbstractBootstrapper):          # Not modified          if res.status_code == 304:              logger.debug("Provider definition has not been modified") +        # -------------------------------------------------------------- +        # end refactor, more or less... +        # XXX Watch out, have to check the supported api yet.          else:              provider_definition, mtime = get_content(res) @@ -181,8 +208,8 @@ class ProviderBootstrapper(AbstractBootstrapper):              else:                  api_supported = ', '.join(SupportedAPIs.SUPPORTED_APIS)                  error = ('Unsupported provider API version. ' -                         'Supported versions are: {}. ' -                         'Found: {}.').format(api_supported, api_version) +                         'Supported versions are: {0}. ' +                         'Found: {1}.').format(api_supported, api_version)                  logger.error(error)                  raise UnsupportedProviderAPI(error) @@ -230,7 +257,8 @@ class ProviderBootstrapper(AbstractBootstrapper):          """          Downloads the CA cert that is going to be used for the api URL          """ - +        # XXX maybe we can skip this step if +        # we have a fresh one.          leap_assert(self._provider_config, "Cannot download the ca cert "                      "without a provider config!") @@ -244,7 +272,7 @@ class ProviderBootstrapper(AbstractBootstrapper):              return          res = self._session.get(self._provider_config.get_ca_cert_uri(), -                                verify=not self._bypass_checks, +                                verify=self.verify,                                  timeout=REQUEST_TIMEOUT)          res.raise_for_status() diff --git a/src/leap/bitmask/provider/tests/__init__.py b/src/leap/bitmask/provider/tests/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/leap/bitmask/provider/tests/__init__.py diff --git a/src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py b/src/leap/bitmask/provider/tests/test_providerbootstrapper.py index b0685676..9b47d60e 100644 --- a/src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py +++ b/src/leap/bitmask/provider/tests/test_providerbootstrapper.py @@ -14,15 +14,12 @@  #  # 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 @@ -39,13 +36,12 @@ 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.bitmask.provider.providerbootstrapper import ProviderBootstrapper +from leap.bitmask.provider.providerbootstrapper import UnsupportedProviderAPI +from leap.bitmask.provider.providerbootstrapper import WrongFingerprint +from leap.bitmask.provider.supportedapis import SupportedAPIs  from leap.common.files import mkdir_p  from leap.common.testing.https_server import where  from leap.common.testing.basetest import BaseLeapTest 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/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/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/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/util/constants.py b/src/leap/bitmask/util/constants.py index 63f6b1f7..e6a6bdce 100644 --- a/src/leap/bitmask/util/constants.py +++ b/src/leap/bitmask/util/constants.py @@ -16,4 +16,4 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  SIGNUP_TIMEOUT = 5 -REQUEST_TIMEOUT = 10 +REQUEST_TIMEOUT = 15 | 
