diff options
Diffstat (limited to 'src')
21 files changed, 1143 insertions, 1156 deletions
| diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index 02b1693d..c1859478 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -152,12 +152,6 @@ def main():      """      Starts the main event loop and launches the main window.      """ -    try: -        event_server.ensure_server(event_server.SERVER_PORT) -    except Exception as e: -        # We don't even have logger configured in here -        print "Could not ensure server: %r" % (e,) -      _, opts = leap_argparse.init_leapc_args()      if opts.version: @@ -170,6 +164,12 @@ def main():      logfile = opts.log_file      openvpn_verb = opts.openvpn_verb +    try: +        event_server.ensure_server(event_server.SERVER_PORT) +    except Exception as e: +        # We don't even have logger configured in here +        print "Could not ensure server: %r" % (e,) +      #############################################################      # Given how paths and bundling works, we need to delay the imports      # of certain parts that depend on this path settings. @@ -179,6 +179,9 @@ def main():      flags.STANDALONE = standalone      BaseConfig.standalone = standalone +    logger = add_logger_handlers(debug, logfile) +    replace_stdout_stderr_with_logging(logger) +      # And then we import all the other stuff      from leap.bitmask.gui import locale_rc      from leap.bitmask.gui import twisted_main @@ -190,9 +193,6 @@ def main():      # pylint: avoid unused import      assert(locale_rc) -    logger = add_logger_handlers(debug, logfile) -    replace_stdout_stderr_with_logging(logger) -      if not we_are_the_one_and_only():          # Bitmask is already running          logger.warning("Tried to launch more than one instance " @@ -211,7 +211,7 @@ def main():      # We force the style if on KDE so that it doesn't load all the kde      # libs, which causes a compatibility issue in some systems.      # For more info, see issue #3194 -    if os.environ.get("KDE_SESSION_UID") is not None: +    if flags.STANDALONE and os.environ.get("KDE_SESSION_UID") is not None:          sys.argv.append("-style")          sys.argv.append("Cleanlooks") diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 200d68aa..92d6906e 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 @@ -234,6 +234,8 @@ class MainWindow(QtGui.QMainWindow):              self._soledad_bootstrapped_stage)          self._soledad_bootstrapper.soledad_timeout.connect(              self._retry_soledad_connection) +        # XXX missing connect to soledad_failed (signal unrecoverable to user) +        # TODO wait until chiiph ui refactor.          self._smtp_bootstrapper = SMTPBootstrapper()          self._smtp_bootstrapper.download_config.connect( @@ -1007,6 +1009,7 @@ class MainWindow(QtGui.QMainWindow):          """          Retries soledad connection.          """ +        # XXX should move logic to soledad boostrapper itself          logger.debug("Retrying soledad connection.")          if self._soledad_bootstrapper.should_retry_initialization():              self._soledad_bootstrapper.increment_retries_count() @@ -1031,8 +1034,9 @@ class MainWindow(QtGui.QMainWindow):          """          passed = data[self._soledad_bootstrapper.PASSED_KEY]          if not passed: +            # TODO should actually *display* on the panel.              logger.debug("ERROR on soledad bootstrapping:") -            logger.error(data[self._soledad_bootstrapper.ERROR_KEY]) +            logger.error("%r" % data[self._soledad_bootstrapper.ERROR_KEY])              return          else:              logger.debug("Done bootstrapping Soledad") @@ -1531,7 +1535,9 @@ class MainWindow(QtGui.QMainWindow):          # TODO we should have a way of parsing the latest lines in the vpn          # log buffer so we can have a more precise idea of which type          # of error did we have (server side, local problem, etc) -        abnormal = True + +        qtsigs = self._eip_connection.qtsigs +        signal = qtsigs.disconnected_signal          # XXX check if these exitCodes are pkexec/cocoasudo specific          if exitCode in (126, 127): @@ -1540,28 +1546,25 @@ class MainWindow(QtGui.QMainWindow):                          "because you did not authenticate properly."),                  error=True)              self._vpn.killit() +            signal = qtsigs.connection_aborted_signal +          elif exitCode != 0 or not self.user_stopped_eip:              self._status_panel.set_global_status(                  self.tr("Encrypted Internet finished in an "                          "unexpected manner!"), error=True) -        else: -            abnormal = False +            signal = qtsigs.connection_died_signal +          if exitCode == 0 and IS_MAC:              # XXX remove this warning after I fix cocoasudo.              logger.warning("The above exit code MIGHT BE WRONG.") -        # We emit signals to trigger transitions in the state machine: -        qtsigs = self._eip_connection.qtsigs -        if abnormal: -            signal = qtsigs.connection_died_signal -        else: -            signal = qtsigs.disconnected_signal -          # XXX verify that the logic kees the same w/o the abnormal flag          # after the refactor to EIPConnection has been completed          # (eipconductor taking the most of the logic under transitions          # that right now are handled under status_panel)          #self._stop_eip(abnormal) + +        # We emit signals to trigger transitions in the state machine:          signal.emit()      def _on_raise_window_event(self, req): diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py index c3dd5ed3..94726720 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -128,11 +128,25 @@ class ConnectionMachineBuilder(object):              states[_OFF])          # * If we receive the connection_died, we transition -        #   to the off state +        #   from on directly to the off state          states[_ON].addTransition(              conn.qtsigs.connection_died_signal,              states[_OFF]) +        # * If we receive the connection_aborted, we transition +        #   from connecting to the off state +        states[_CON].addTransition( +            conn.qtsigs.connection_aborted_signal, +            states[_OFF]) +        # * Connection died can in some cases also be +        #   triggered while we are in CONNECTING +        #   state. I should be avoided, since connection_aborted +        #   is clearer (and reserve connection_died +        #   for transitions from on->off +        states[_CON].addTransition( +            conn.qtsigs.connection_died_signal, +            states[_OFF]) +          # adding states to the machine          for state in states.itervalues():              machine.addState(state) 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/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/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/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index cac91440..d348661d 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") -            logger.debug("Using soledad server url: %s" % (server_url,)) +        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,)) -            cert_file = self._provider_config.get_ca_cert_path() +        cert_file = self._provider_config.get_ca_cert_path() -            # TODO: If selected server fails, retry with another host -            # (issue #3309) +        logger.debug('local_db:%s' % (local_db_path,)) +        logger.debug('secrets_path:%s' % (secrets_path,)) + +        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,46 @@ 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: +            gpgbin = which("gpg") +        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,10 +343,25 @@ 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)          except KeyNotFound:              logger.debug("Key not found. Generating key for %s" % (address,))              self._keymanager.gen_key(openpgp.OpenPGPKey) diff --git a/src/leap/bitmask/util/__init__.py b/src/leap/bitmask/util/__init__.py index f762a350..b58e6e3b 100644 --- a/src/leap/bitmask/util/__init__.py +++ b/src/leap/bitmask/util/__init__.py @@ -62,3 +62,17 @@ def update_modification_ts(path):      """      os.utime(path, None)      return get_modification_ts(path) + + +def is_file(path): +    """ +    Returns True if the path exists and is a file. +    """ +    return os.path.isfile(path) + + +def is_empty_file(path): +    """ +    Returns True if the file at path is empty. +    """ +    return os.stat(path).st_size is 0 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 | 
