From 394408c3d5d38d04e2135385afcbaa74c0d91450 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Tue, 24 Sep 2013 11:50:32 -0300 Subject: Move logger up to be available sooner. --- src/leap/bitmask/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index 02b1693d..37e67e61 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -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 " -- cgit v1.2.3 From 2fc512023f28210dc7957105b49cfdb8878965f9 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Tue, 24 Sep 2013 11:48:33 -0300 Subject: Only ensure server if we are running the app. This code reorder avoids to get an error message if we run some code when the only thing we want is to get the version. Closes #3914. --- src/leap/bitmask/app.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index 37e67e61..d5132731 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. -- cgit v1.2.3 From a073ad8e329a3d90427737ca698e7acaa9d11b8d Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Tue, 24 Sep 2013 13:00:47 -0300 Subject: Update standalone dependency usage. --- src/leap/bitmask/config/tests/test_leapsettings.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/config/tests/test_leapsettings.py b/src/leap/bitmask/config/tests/test_leapsettings.py index 18166923..b45abfdd 100644 --- a/src/leap/bitmask/config/tests/test_leapsettings.py +++ b/src/leap/bitmask/config/tests/test_leapsettings.py @@ -29,6 +29,7 @@ import mock from leap.common.testing.basetest import BaseLeapTest from leap.bitmask.config.leapsettings import LeapSettings +from leap.bitmask.config import flags class LeapSettingsTest(BaseLeapTest): @@ -44,6 +45,7 @@ class LeapSettingsTest(BaseLeapTest): """ Test that the config file IS NOT stored under the CWD. """ + flags.STANDALONE = False self._leapsettings = LeapSettings() with mock.patch('os.listdir') as os_listdir: # use this method only to spy where LeapSettings is looking for @@ -57,7 +59,8 @@ class LeapSettingsTest(BaseLeapTest): """ Test that the config file IS stored under the CWD. """ - self._leapsettings = LeapSettings(standalone=True) + flags.STANDALONE = True + self._leapsettings = LeapSettings() with mock.patch('os.listdir') as os_listdir: # use this method only to spy where LeapSettings is looking for self._leapsettings.get_configured_providers() -- cgit v1.2.3 From 924264776e8dab3c17c09a3188ae701edbaf797b Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Tue, 24 Sep 2013 13:01:34 -0300 Subject: Update path comparison for certs. We need to check if the path *after* the prefix is correct, assuming that the prefix methods works fine. --- src/leap/bitmask/config/tests/test_providerconfig.py | 12 ++++-------- src/leap/bitmask/services/eip/tests/test_eipconfig.py | 12 ++++-------- 2 files changed, 8 insertions(+), 16 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/config/tests/test_providerconfig.py b/src/leap/bitmask/config/tests/test_providerconfig.py index 7661a1ce..fe27e683 100644 --- a/src/leap/bitmask/config/tests/test_providerconfig.py +++ b/src/leap/bitmask/config/tests/test_providerconfig.py @@ -175,10 +175,9 @@ class ProviderConfigTest(BaseLeapTest): def test_get_ca_cert_path_as_expected(self): pc = self._provider_config - pc.get_path_prefix = Mock(return_value='test') provider_domain = sample_config['domain'] - expected_path = os.path.join('test', 'leap', 'providers', + expected_path = os.path.join('leap', 'providers', provider_domain, 'keys', 'ca', 'cacert.pem') @@ -186,24 +185,21 @@ class ProviderConfigTest(BaseLeapTest): os.path.exists = Mock(return_value=True) cert_path = pc.get_ca_cert_path() - self.assertEqual(cert_path, expected_path) + self.assertTrue(cert_path.endswith(expected_path)) def test_get_ca_cert_path_about_to_download(self): pc = self._provider_config - pc.get_path_prefix = Mock(return_value='test') provider_domain = sample_config['domain'] - expected_path = os.path.join('test', 'leap', 'providers', + expected_path = os.path.join('leap', 'providers', provider_domain, 'keys', 'ca', 'cacert.pem') cert_path = pc.get_ca_cert_path(about_to_download=True) - - self.assertEqual(cert_path, expected_path) + self.assertTrue(cert_path.endswith(expected_path)) def test_get_ca_cert_path_fails(self): pc = self._provider_config - pc.get_path_prefix = Mock(return_value='test') # mock 'get_domain' so we don't need to load a config provider_domain = 'test.provider.com' diff --git a/src/leap/bitmask/services/eip/tests/test_eipconfig.py b/src/leap/bitmask/services/eip/tests/test_eipconfig.py index 76305e83..4e340f4c 100644 --- a/src/leap/bitmask/services/eip/tests/test_eipconfig.py +++ b/src/leap/bitmask/services/eip/tests/test_eipconfig.py @@ -262,15 +262,13 @@ class EIPConfigTest(BaseLeapTest): def test_get_client_cert_path_as_expected(self): config = self._get_eipconfig() - config.get_path_prefix = Mock(return_value='test') - provider_config = ProviderConfig() # mock 'get_domain' so we don't need to load a config provider_domain = 'test.provider.com' provider_config.get_domain = Mock(return_value=provider_domain) - expected_path = os.path.join('test', 'leap', 'providers', + expected_path = os.path.join('leap', 'providers', provider_domain, 'keys', 'client', 'openvpn.pem') @@ -278,26 +276,24 @@ class EIPConfigTest(BaseLeapTest): os.path.exists = Mock(return_value=True) cert_path = config.get_client_cert_path(provider_config) - self.assertEqual(cert_path, expected_path) + self.assertTrue(cert_path.endswith(expected_path)) def test_get_client_cert_path_about_to_download(self): config = self._get_eipconfig() - config.get_path_prefix = Mock(return_value='test') - provider_config = ProviderConfig() # mock 'get_domain' so we don't need to load a config provider_domain = 'test.provider.com' provider_config.get_domain = Mock(return_value=provider_domain) - expected_path = os.path.join('test', 'leap', 'providers', + expected_path = os.path.join('leap', 'providers', provider_domain, 'keys', 'client', 'openvpn.pem') cert_path = config.get_client_cert_path( provider_config, about_to_download=True) - self.assertEqual(cert_path, expected_path) + self.assertTrue(cert_path.endswith(expected_path)) def test_get_client_cert_path_fails(self): config = self._get_eipconfig() -- cgit v1.2.3 From 9ba31164f032c304e07a063a3be3160985478982 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 25 Sep 2013 18:53:49 -0300 Subject: Refactor to be consistent with other launchers. This is done in order to make them similar and them merge as much as code as possible. --- src/leap/bitmask/services/eip/vpnlaunchers.py | 78 ++++++++++++++------------- 1 file changed, 40 insertions(+), 38 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/services/eip/vpnlaunchers.py b/src/leap/bitmask/services/eip/vpnlaunchers.py index daa0d81f..e27a48d9 100644 --- a/src/leap/bitmask/services/eip/vpnlaunchers.py +++ b/src/leap/bitmask/services/eip/vpnlaunchers.py @@ -132,7 +132,7 @@ class VPNLauncher(object): Same as missing_updown_scripts but does not check for exec bit. :rtype: list """ - leap_assert(kls.UPDOWN_FILES is not None, + 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) @@ -261,6 +261,7 @@ class LinuxVPNLauncher(VPNLauncher): 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, ) @@ -357,16 +358,17 @@ class LinuxVPNLauncher(VPNLauncher): "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): + def get_vpn_command(self, eipconfig, providerconfig, socket_host, + 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) + path_prefix/apps/eip/ (in case that standalone is set) Might raise: - VPNLauncherException, - OpenVPNNotFoundException. + EIPNoTunKextLoaded, + OpenVPNNotFoundException, + VPNLauncherException. :param eipconfig: eip configuration object :type eipconfig: EIPConfig @@ -387,12 +389,8 @@ class LinuxVPNLauncher(VPNLauncher): :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: @@ -400,18 +398,12 @@ class LinuxVPNLauncher(VPNLauncher): 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" ] @@ -454,22 +446,23 @@ class LinuxVPNLauncher(VPNLauncher): ] 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', getpass.getuser(), + # '--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', getpass.getuser() + '--management-client-user', user ] args += [ @@ -478,37 +471,46 @@ class LinuxVPNLauncher(VPNLauncher): '--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 _has_updown_scripts(self.UP_SCRIPT): + args += [ + '--up', '\"%s\"' % (self.UP_SCRIPT,), + ] - if plugin_path and _has_updown_scripts(self.UP_DOWN_PATH): + if _has_updown_scripts(self.DOWN_SCRIPT): 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 + '--down', '\"%s\"' % (self.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(self.OPENVPN_DOWN_PLUGIN): + # args += [ + # '--plugin', self.OPENVPN_DOWN_ROOT, + # '\'%s\'' % self.DOWN_SCRIPT # for OSX + # '\'script_type=down %s\'' % self.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 = [openvpn] + pkexec = self.maybe_pkexec() + if pkexec: + command.insert(0, first(pkexec)) + + command_and_args = command + args logger.debug("Running VPN with command:") - logger.debug("%s %s" % (openvpn, " ".join(args))) + logger.debug(" ".join(command_and_args)) - return [openvpn] + args + return command_and_args def get_vpn_env(self): """ -- cgit v1.2.3 From 6af326fd901243cc5e36ddbcecff0690d1abee5e Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Sat, 28 Sep 2013 11:38:46 -0400 Subject: Make providerboostrapper take the verify path from our ca-bundle. Also, move module to a more logical placement, since provier boostrapping is not dependent on eip service. --- src/leap/bitmask/gui/mainwindow.py | 2 +- src/leap/bitmask/gui/wizard.py | 7 +- src/leap/bitmask/provider/providerbootstrapper.py | 368 ++++++++++++++ src/leap/bitmask/provider/tests/__init__.py | 0 .../provider/tests/test_providerbootstrapper.py | 556 ++++++++++++++++++++ .../bitmask/services/eip/providerbootstrapper.py | 340 ------------- .../eip/tests/test_providerbootstrapper.py | 560 --------------------- 7 files changed, 927 insertions(+), 906 deletions(-) create mode 100644 src/leap/bitmask/provider/providerbootstrapper.py create mode 100644 src/leap/bitmask/provider/tests/__init__.py create mode 100644 src/leap/bitmask/provider/tests/test_providerbootstrapper.py delete mode 100644 src/leap/bitmask/services/eip/providerbootstrapper.py delete mode 100644 src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 200d68aa..d8d0ac21 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 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 . - """ 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/provider/providerbootstrapper.py b/src/leap/bitmask/provider/providerbootstrapper.py new file mode 100644 index 00000000..751da828 --- /dev/null +++ b/src/leap/bitmask/provider/providerbootstrapper.py @@ -0,0 +1,368 @@ +# -*- coding: utf-8 -*- +# providerbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Provider bootstrapping +""" +import logging +import socket +import os + +import requests + +from PySide import QtCore + +from leap.bitmask.config.providerconfig import ProviderConfig, MissingCACert +from leap.bitmask.util.request_helpers import get_content +from leap.bitmask.util import get_path_prefix +from leap.bitmask.util.constants import REQUEST_TIMEOUT +from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper +from leap.bitmask.provider.supportedapis import SupportedAPIs +from leap.common 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__) + + +class UnsupportedProviderAPI(Exception): + """ + Raised when attempting to use a provider with an incompatible API. + """ + pass + + +class WrongFingerprint(Exception): + """ + Raised when a fingerprint comparison does not match. + """ + pass + + +class ProviderBootstrapper(AbstractBootstrapper): + """ + Given a provider URL performs a series of checks and emits signals + after they are passed. + If a check fails, the subsequent checks are not executed + """ + + # All dicts returned are of the form + # {"passed": bool, "error": str} + name_resolution = QtCore.Signal(dict) + https_connection = QtCore.Signal(dict) + download_provider_info = QtCore.Signal(dict) + + download_ca_cert = QtCore.Signal(dict) + check_ca_fingerprint = QtCore.Signal(dict) + check_api_certificate = QtCore.Signal(dict) + + def __init__(self, bypass_checks=False): + """ + Constructor for provider bootstrapper object + + :param bypass_checks: Set to true if the app should bypass + first round of checks for CA certificates at bootstrap + :type bypass_checks: bool + """ + AbstractBootstrapper.__init__(self, bypass_checks) + + self._domain = None + self._provider_config = None + self._download_if_needed = False + + @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): + """ + 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. + # 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=self.verify, + timeout=REQUEST_TIMEOUT) + res.raise_for_status() + except requests.exceptions.SSLError as exc: + logger.exception(exc) + self._err_msg = self.tr("Provider certificate could " + "not be verified") + raise + 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 + + def _download_provider_info(self, *args): + """ + Downloads the provider.json defition + """ + leap_assert(self._domain, + "Cannot download provider info without a domain") + logger.debug("Downloading provider info for %s" % (self._domain)) + + # -------------------------------------------------------------- + # 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) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + uri = "https://%s/%s" % (self._domain, "provider.json") + verify = self.verify + + if mtime: # the provider.json exists + # So, we're getting it from the api.* and checking against + # the provider ca. + try: + 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: + # no ca? then download from main domain again. + pass + + logger.debug("Requesting for provider.json... " + "uri: {0}, verify: {1}, headers: {2}".format( + uri, verify, headers)) + res = self._session.get(uri, verify=verify, + headers=headers, timeout=REQUEST_TIMEOUT) + res.raise_for_status() + logger.debug("Request status code: {0}".format(res.status_code)) + + # Not modified + if res.status_code == 304: + logger.debug("Provider definition has not been modified") + # -------------------------------------------------------------- + # end refactor, more or less... + # XXX Watch out, have to check the supported api yet. + else: + provider_definition, mtime = get_content(res) + + provider_config = ProviderConfig() + provider_config.load(data=provider_definition, mtime=mtime) + provider_config.save(["leap", + "providers", + self._domain, + "provider.json"]) + + api_version = provider_config.get_api_version() + if SupportedAPIs.supports(api_version): + logger.debug("Provider definition has been modified") + else: + api_supported = ', '.join(SupportedAPIs.SUPPORTED_APIS) + error = ('Unsupported provider API version. ' + 'Supported versions are: {0}. ' + 'Found: {1}.').format(api_supported, api_version) + + logger.error(error) + raise UnsupportedProviderAPI(error) + + def run_provider_select_checks(self, domain, download_if_needed=False): + """ + Populates the check queue. + + :param domain: domain to check + :type domain: str + + :param download_if_needed: if True, makes the checks do not + overwrite already downloaded data + :type download_if_needed: bool + """ + leap_assert(domain and len(domain) > 0, "We need a domain!") + + self._domain = ProviderConfig.sanitize_path_component(domain) + self._download_if_needed = download_if_needed + + cb_chain = [ + (self._check_name_resolution, self.name_resolution), + (self._check_https, self.https_connection), + (self._download_provider_info, self.download_provider_info) + ] + + return self.addCallbackChain(cb_chain) + + def _should_proceed_cert(self): + """ + Returns False if the certificate already exists for the given + provider. True otherwise + + :rtype: bool + """ + leap_assert(self._provider_config, "We need a provider config!") + + if not self._download_if_needed: + return True + + return not os.path.exists(self._provider_config + .get_ca_cert_path(about_to_download=True)) + + def _download_ca_cert(self, *args): + """ + Downloads the CA cert that is going to be used for the api URL + """ + # 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!") + + logger.debug("Downloading ca cert for %s at %s" % + (self._domain, self._provider_config.get_ca_cert_uri())) + + if not self._should_proceed_cert(): + check_and_fix_urw_only( + self._provider_config + .get_ca_cert_path(about_to_download=True)) + return + + res = self._session.get(self._provider_config.get_ca_cert_uri(), + verify=self.verify, + timeout=REQUEST_TIMEOUT) + res.raise_for_status() + + cert_path = self._provider_config.get_ca_cert_path( + about_to_download=True) + cert_dir = os.path.dirname(cert_path) + mkdir_p(cert_dir) + with open(cert_path, "w") as f: + f.write(res.content) + + check_and_fix_urw_only(cert_path) + + def _check_ca_fingerprint(self, *args): + """ + Checks the CA cert fingerprint against the one provided in the + json definition + """ + leap_assert(self._provider_config, "Cannot check the ca cert " + "without a provider config!") + + logger.debug("Checking ca fingerprint for %s and cert %s" % + (self._domain, + self._provider_config.get_ca_cert_path())) + + if not self._should_proceed_cert(): + return + + parts = self._provider_config.get_ca_cert_fingerprint().split(":") + + error_msg = "Wrong fingerprint format" + leap_check(len(parts) == 2, error_msg, WrongFingerprint) + + method = parts[0].strip() + fingerprint = parts[1].strip() + cert_data = None + with open(self._provider_config.get_ca_cert_path()) as f: + cert_data = f.read() + + leap_assert(len(cert_data) > 0, "Could not read certificate data") + digest = get_digest(cert_data, method) + + error_msg = "Downloaded certificate has a different fingerprint!" + leap_check(digest == fingerprint, error_msg, WrongFingerprint) + + def _check_api_certificate(self, *args): + """ + Tries to make an API call with the downloaded cert and checks + if it validates against it + """ + leap_assert(self._provider_config, "Cannot check the ca cert " + "without a provider config!") + + logger.debug("Checking api certificate for %s and cert %s" % + (self._provider_config.get_api_uri(), + self._provider_config.get_ca_cert_path())) + + if not self._should_proceed_cert(): + return + + test_uri = "%s/%s/cert" % (self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + res = self._session.get(test_uri, + verify=self._provider_config + .get_ca_cert_path(), + timeout=REQUEST_TIMEOUT) + res.raise_for_status() + + def run_provider_setup_checks(self, + provider_config, + download_if_needed=False): + """ + Starts the checks needed for a new provider setup. + + :param provider_config: Provider configuration + :type provider_config: ProviderConfig + + :param download_if_needed: if True, makes the checks do not + overwrite already downloaded data. + :type download_if_needed: bool + """ + leap_assert(provider_config, "We need a provider config!") + leap_assert_type(provider_config, ProviderConfig) + + self._provider_config = provider_config + self._download_if_needed = download_if_needed + + cb_chain = [ + (self._download_ca_cert, self.download_ca_cert), + (self._check_ca_fingerprint, self.check_ca_fingerprint), + (self._check_api_certificate, self.check_api_certificate) + ] + + return self.addCallbackChain(cb_chain) diff --git a/src/leap/bitmask/provider/tests/__init__.py b/src/leap/bitmask/provider/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/provider/tests/test_providerbootstrapper.py b/src/leap/bitmask/provider/tests/test_providerbootstrapper.py new file mode 100644 index 00000000..9b47d60e --- /dev/null +++ b/src/leap/bitmask/provider/tests/test_providerbootstrapper.py @@ -0,0 +1,556 @@ +# -*- coding: utf-8 -*- +# test_providerbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Tests for the Provider Boostrapper checks + +These will be whitebox tests since we want to make sure the private +implementation is checking what we expect. +""" +import os +import mock +import socket +import stat +import tempfile +import time +import requests +try: + import unittest2 as unittest +except ImportError: + import unittest + +from nose.twistedtools import deferred, reactor +from twisted.internet import threads +from requests.models import Response + +from leap.bitmask.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 + + +class ProviderBootstrapperTest(BaseLeapTest): + def setUp(self): + self.pb = ProviderBootstrapper() + + def tearDown(self): + pass + + def test_name_resolution_check(self): + # Something highly likely to success + self.pb._domain = "google.com" + self.pb._check_name_resolution() + # Something highly likely to fail + self.pb._domain = "uquhqweuihowquie.abc.def" + + # In python 2.7.4 raises socket.error + # In python 2.7.5 raises socket.gaierror + with self.assertRaises((socket.gaierror, socket.error)): + self.pb._check_name_resolution() + + @deferred() + def test_run_provider_select_checks(self): + self.pb._check_name_resolution = mock.MagicMock() + self.pb._check_https = mock.MagicMock() + self.pb._download_provider_info = mock.MagicMock() + + d = self.pb.run_provider_select_checks("somedomain") + + def check(*args): + self.pb._check_name_resolution.assert_called_once_with() + self.pb._check_https.assert_called_once_with(None) + self.pb._download_provider_info.assert_called_once_with(None) + d.addCallback(check) + return d + + @deferred() + def test_run_provider_setup_checks(self): + self.pb._download_ca_cert = mock.MagicMock() + self.pb._check_ca_fingerprint = mock.MagicMock() + self.pb._check_api_certificate = mock.MagicMock() + + d = self.pb.run_provider_setup_checks(ProviderConfig()) + + def check(*args): + self.pb._download_ca_cert.assert_called_once_with() + self.pb._check_ca_fingerprint.assert_called_once_with(None) + self.pb._check_api_certificate.assert_called_once_with(None) + d.addCallback(check) + return d + + def test_should_proceed_cert(self): + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=where("cacert.pem")) + + self.pb._download_if_needed = False + self.assertTrue(self.pb._should_proceed_cert()) + + self.pb._download_if_needed = True + self.assertFalse(self.pb._should_proceed_cert()) + + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=where("somefilethatdoesntexist.pem")) + self.assertTrue(self.pb._should_proceed_cert()) + + def _check_download_ca_cert(self, should_proceed): + """ + Helper to check different paths easily for the download ca + cert check + + :param should_proceed: sets the _should_proceed_cert in the + provider bootstrapper being tested + :type should_proceed: bool + + :returns: The contents of the certificate, the expected + content depending on should_proceed, and the mode of + the file to be checked by the caller + :rtype: tuple of str, str, int + """ + old_content = "NOT THE NEW CERT" + new_content = "NEW CERT" + new_cert_path = os.path.join(tempfile.mkdtemp(), + "mynewcert.pem") + + with open(new_cert_path, "w") as c: + c.write(old_content) + + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=new_cert_path) + self.pb._domain = "somedomain" + + self.pb._should_proceed_cert = mock.MagicMock( + return_value=should_proceed) + + read = None + content_to_check = None + mode = None + + with mock.patch('requests.models.Response.content', + new_callable=mock.PropertyMock) as \ + content: + content.return_value = new_content + response_obj = Response() + response_obj.raise_for_status = mock.MagicMock() + + self.pb._session.get = mock.MagicMock(return_value=response_obj) + self.pb._download_ca_cert() + with open(new_cert_path, "r") as nc: + read = nc.read() + if should_proceed: + content_to_check = new_content + else: + content_to_check = old_content + mode = stat.S_IMODE(os.stat(new_cert_path).st_mode) + + os.unlink(new_cert_path) + return read, content_to_check, mode + + def test_download_ca_cert_no_saving(self): + read, expected_read, mode = self._check_download_ca_cert(False) + self.assertEqual(read, expected_read) + self.assertEqual(mode, int("600", 8)) + + def test_download_ca_cert_saving(self): + read, expected_read, mode = self._check_download_ca_cert(True) + self.assertEqual(read, expected_read) + self.assertEqual(mode, int("600", 8)) + + def test_check_ca_fingerprint_skips(self): + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( + return_value="") + self.pb._domain = "somedomain" + + self.pb._should_proceed_cert = mock.MagicMock(return_value=False) + + self.pb._check_ca_fingerprint() + self.assertFalse(self.pb._provider_config. + get_ca_cert_fingerprint.called) + + def test_check_ca_cert_fingerprint_raises_bad_format(self): + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( + return_value="wrongfprformat!!") + self.pb._domain = "somedomain" + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + with self.assertRaises(WrongFingerprint): + self.pb._check_ca_fingerprint() + + # This two hashes different in the last byte, but that's good enough + # for the tests + KNOWN_BAD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034efe" \ + "7dd1b910062ca323eb4da5c7f" + KNOWN_GOOD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034ef" \ + "e7dd1b910062ca323eb4da5c7e" + KNOWN_GOOD_CERT = """ +-----BEGIN CERTIFICATE----- +MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt +YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v +Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw +FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV +BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai +dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB +7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84 +CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+ +znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4 +MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4 +lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0 +bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl +DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB +lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy +YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw +XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE +MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w +DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl +cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY +k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj +RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG +htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX +EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J +aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l +mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK +G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co +Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d +69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e +yV8e +-----END CERTIFICATE----- +""" + + def _prepare_provider_config_with(self, cert_path, cert_hash): + """ + Mocks the provider config to give the cert_path and cert_hash + specified + + :param cert_path: path for the certificate + :type cert_path: str + :param cert_hash: hash for the certificate as it would appear + in the provider config json + :type cert_hash: str + """ + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( + return_value=cert_hash) + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=cert_path) + self.pb._domain = "somedomain" + + def test_check_ca_fingerprint_checksout(self): + cert_path = os.path.join(tempfile.mkdtemp(), + "mynewcert.pem") + + with open(cert_path, "w") as c: + c.write(self.KNOWN_GOOD_CERT) + + self._prepare_provider_config_with(cert_path, self.KNOWN_GOOD_HASH) + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + self.pb._check_ca_fingerprint() + + os.unlink(cert_path) + + def test_check_ca_fingerprint_fails(self): + cert_path = os.path.join(tempfile.mkdtemp(), + "mynewcert.pem") + + with open(cert_path, "w") as c: + c.write(self.KNOWN_GOOD_CERT) + + self._prepare_provider_config_with(cert_path, self.KNOWN_BAD_HASH) + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + with self.assertRaises(WrongFingerprint): + self.pb._check_ca_fingerprint() + + os.unlink(cert_path) + + +############################################################################### +# Tests with a fake provider # +############################################################################### + +class ProviderBootstrapperActiveTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + factory = fake_provider.get_provider_factory() + http = reactor.listenTCP(8002, factory) + https = reactor.listenSSL( + 0, factory, + fake_provider.OpenSSLServerContextFactory()) + get_port = lambda p: p.getHost().port + cls.http_port = get_port(http) + cls.https_port = get_port(https) + + def setUp(self): + self.pb = ProviderBootstrapper() + + # At certain points we are going to be replacing these methods + # directly in ProviderConfig to be able to catch calls from + # new ProviderConfig objects inside the methods tested. We + # need to save the old implementation and restore it in + # tearDown so we are sure everything is as expected for each + # test. If we do it inside each specific test, a failure in + # the test will leave the implementation with the mock. + self.old_gpp = ProviderConfig.get_path_prefix + self.old_load = ProviderConfig.load + self.old_save = ProviderConfig.save + self.old_api_version = ProviderConfig.get_api_version + + def tearDown(self): + ProviderConfig.get_path_prefix = self.old_gpp + ProviderConfig.load = self.old_load + ProviderConfig.save = self.old_save + ProviderConfig.get_api_version = self.old_api_version + + def test_check_https_succeeds(self): + # XXX: Need a proper CA signed cert to test this + pass + + @deferred() + def test_check_https_fails(self): + self.pb._domain = "localhost:%s" % (self.https_port,) + + def check(*args): + with self.assertRaises(requests.exceptions.SSLError): + self.pb._check_https() + return threads.deferToThread(check) + + @deferred() + def test_second_check_https_fails(self): + self.pb._domain = "localhost:1234" + + def check(*args): + with self.assertRaises(Exception): + self.pb._check_https() + return threads.deferToThread(check) + + @deferred() + def test_check_https_succeeds_if_danger(self): + self.pb._domain = "localhost:%s" % (self.https_port,) + self.pb._bypass_checks = True + + def check(*args): + self.pb._check_https() + + return threads.deferToThread(check) + + def _setup_provider_config_with(self, api, path_prefix): + """ + Sets up the ProviderConfig with mocks for the path prefix, the + api returned and load/save methods. + It modifies ProviderConfig directly instead of an object + because the object used is created in the method itself and we + cannot control that. + + :param api: API to return + :type api: str + :param path_prefix: path prefix to be used when calculating + paths + :type path_prefix: str + """ + ProviderConfig.get_path_prefix = mock.MagicMock( + return_value=path_prefix) + ProviderConfig.get_api_version = mock.MagicMock( + return_value=api) + ProviderConfig.load = mock.MagicMock() + ProviderConfig.save = mock.MagicMock() + + def _setup_providerbootstrapper(self, ifneeded): + """ + Sets the provider bootstrapper's domain to + localhost:https_port, sets it to bypass https checks and sets + the download if needed based on the ifneeded value. + + :param ifneeded: Value for _download_if_needed + :type ifneeded: bool + """ + self.pb._domain = "localhost:%s" % (self.https_port,) + self.pb._bypass_checks = True + self.pb._download_if_needed = ifneeded + + def _produce_dummy_provider_json(self): + """ + Creates a dummy provider json on disk in order to test + behaviour around it (download if newer online, etc) + + :returns: the provider.json path used + :rtype: str + """ + provider_dir = os.path.join(ProviderConfig() + .get_path_prefix(), + "leap", + "providers", + self.pb._domain) + mkdir_p(provider_dir) + provider_path = os.path.join(provider_dir, + "provider.json") + + with open(provider_path, "w") as p: + p.write("A") + return provider_path + + def test_download_provider_info_new_provider(self): + self._setup_provider_config_with("1", tempfile.mkdtemp()) + self._setup_providerbootstrapper(True) + + self.pb._download_provider_info() + self.assertTrue(ProviderConfig.save.called) + + @mock.patch( + 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path', + lambda x: where('cacert.pem')) + def test_download_provider_info_not_modified(self): + self._setup_provider_config_with("1", tempfile.mkdtemp()) + self._setup_providerbootstrapper(True) + provider_path = self._produce_dummy_provider_json() + + # set mtime to something really new + os.utime(provider_path, (-1, time.time())) + + with mock.patch.object( + ProviderConfig, 'get_api_uri', + return_value="https://localhost:%s" % (self.https_port,)): + self.pb._download_provider_info() + # we check that it doesn't save the provider + # config, because it's new enough + self.assertFalse(ProviderConfig.save.called) + + @mock.patch( + 'leap.bitmask.config.providerconfig.ProviderConfig.get_domain', + lambda x: where('testdomain.com')) + def test_download_provider_info_not_modified_and_no_cacert(self): + self._setup_provider_config_with("1", tempfile.mkdtemp()) + self._setup_providerbootstrapper(True) + provider_path = self._produce_dummy_provider_json() + + # set mtime to something really new + os.utime(provider_path, (-1, time.time())) + + with mock.patch.object( + ProviderConfig, 'get_api_uri', + return_value="https://localhost:%s" % (self.https_port,)): + self.pb._download_provider_info() + # we check that it doesn't save the provider + # config, because it's new enough + self.assertFalse(ProviderConfig.save.called) + + @mock.patch( + 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path', + lambda x: where('cacert.pem')) + def test_download_provider_info_modified(self): + self._setup_provider_config_with("1", tempfile.mkdtemp()) + self._setup_providerbootstrapper(True) + provider_path = self._produce_dummy_provider_json() + + # set mtime to something really old + os.utime(provider_path, (-1, 100)) + + with mock.patch.object( + ProviderConfig, 'get_api_uri', + return_value="https://localhost:%s" % (self.https_port,)): + self.pb._download_provider_info() + self.assertTrue(ProviderConfig.load.called) + self.assertTrue(ProviderConfig.save.called) + + @mock.patch( + 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path', + lambda x: where('cacert.pem')) + def test_download_provider_info_unsupported_api_raises(self): + self._setup_provider_config_with("9999999", tempfile.mkdtemp()) + self._setup_providerbootstrapper(False) + self._produce_dummy_provider_json() + + with mock.patch.object( + ProviderConfig, 'get_api_uri', + return_value="https://localhost:%s" % (self.https_port,)): + with self.assertRaises(UnsupportedProviderAPI): + self.pb._download_provider_info() + + @mock.patch( + 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path', + lambda x: where('cacert.pem')) + def test_download_provider_info_unsupported_api(self): + self._setup_provider_config_with(SupportedAPIs.SUPPORTED_APIS[0], + tempfile.mkdtemp()) + self._setup_providerbootstrapper(False) + self._produce_dummy_provider_json() + + with mock.patch.object( + ProviderConfig, 'get_api_uri', + return_value="https://localhost:%s" % (self.https_port,)): + self.pb._download_provider_info() + + @mock.patch( + 'leap.bitmask.config.providerconfig.ProviderConfig.get_api_uri', + lambda x: 'api.uri') + @mock.patch( + 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path', + lambda x: '/cert/path') + def test_check_api_certificate_skips(self): + self.pb._provider_config = ProviderConfig() + self.pb._session.get = mock.MagicMock(return_value=Response()) + + self.pb._should_proceed_cert = mock.MagicMock(return_value=False) + self.pb._check_api_certificate() + self.assertFalse(self.pb._session.get.called) + + @deferred() + def test_check_api_certificate_fails(self): + self.pb._provider_config = ProviderConfig() + self.pb._provider_config.get_api_uri = mock.MagicMock( + return_value="https://localhost:%s" % (self.https_port,)) + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=os.path.join( + os.path.split(__file__)[0], + "wrongcert.pem")) + self.pb._provider_config.get_api_version = mock.MagicMock( + return_value="1") + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + def check(*args): + with self.assertRaises(requests.exceptions.SSLError): + self.pb._check_api_certificate() + d = threads.deferToThread(check) + return d + + @deferred() + def test_check_api_certificate_succeeds(self): + self.pb._provider_config = ProviderConfig() + self.pb._provider_config.get_api_uri = mock.MagicMock( + return_value="https://localhost:%s" % (self.https_port,)) + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=where('cacert.pem')) + self.pb._provider_config.get_api_version = mock.MagicMock( + return_value="1") + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + def check(*args): + self.pb._check_api_certificate() + d = threads.deferToThread(check) + return d diff --git a/src/leap/bitmask/services/eip/providerbootstrapper.py b/src/leap/bitmask/services/eip/providerbootstrapper.py deleted file mode 100644 index 3b7c9899..00000000 --- a/src/leap/bitmask/services/eip/providerbootstrapper.py +++ /dev/null @@ -1,340 +0,0 @@ -# -*- coding: utf-8 -*- -# providerbootstrapper.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Provider bootstrapping -""" -import logging -import socket -import os - -import requests - -from PySide import QtCore - -from leap.bitmask.config.providerconfig import ProviderConfig, MissingCACert -from leap.bitmask.util.request_helpers import get_content -from leap.bitmask.util import get_path_prefix -from leap.bitmask.util.constants import REQUEST_TIMEOUT -from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper -from leap.bitmask.provider.supportedapis import SupportedAPIs -from leap.common.certs import get_digest -from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p -from leap.common.check import leap_assert, leap_assert_type, leap_check - - -logger = logging.getLogger(__name__) - - -class UnsupportedProviderAPI(Exception): - """ - Raised when attempting to use a provider with an incompatible API. - """ - pass - - -class WrongFingerprint(Exception): - """ - Raised when a fingerprint comparison does not match. - """ - pass - - -class ProviderBootstrapper(AbstractBootstrapper): - """ - Given a provider URL performs a series of checks and emits signals - after they are passed. - If a check fails, the subsequent checks are not executed - """ - - # All dicts returned are of the form - # {"passed": bool, "error": str} - name_resolution = QtCore.Signal(dict) - https_connection = QtCore.Signal(dict) - download_provider_info = QtCore.Signal(dict) - - download_ca_cert = QtCore.Signal(dict) - check_ca_fingerprint = QtCore.Signal(dict) - check_api_certificate = QtCore.Signal(dict) - - def __init__(self, bypass_checks=False): - """ - Constructor for provider bootstrapper object - - :param bypass_checks: Set to true if the app should bypass - first round of checks for CA certificates at bootstrap - :type bypass_checks: bool - """ - AbstractBootstrapper.__init__(self, bypass_checks) - - self._domain = None - self._provider_config = None - self._download_if_needed = False - - def _check_name_resolution(self): - """ - Checks that the name resolution for the provider name works - """ - leap_assert(self._domain, "Cannot check DNS without a domain") - - logger.debug("Checking name resolution for %s" % (self._domain)) - - # We don't skip this check, since it's basic for the whole - # system to work - socket.gethostbyname(self._domain) - - def _check_https(self, *args): - """ - Checks that https is working and that the provided certificate - checks out - """ - - leap_assert(self._domain, "Cannot check HTTPS without a domain") - - logger.debug("Checking https for %s" % (self._domain)) - - # We don't skip this check, since it's basic for the whole - # system to work - - try: - res = self._session.get("https://%s" % (self._domain,), - verify=not self._bypass_checks, - timeout=REQUEST_TIMEOUT) - res.raise_for_status() - except requests.exceptions.SSLError: - self._err_msg = self.tr("Provider certificate could " - "not be verified") - raise - except Exception: - self._err_msg = self.tr("Provider does not support HTTPS") - raise - - def _download_provider_info(self, *args): - """ - Downloads the provider.json defition - """ - leap_assert(self._domain, - "Cannot download provider info without a domain") - - logger.debug("Downloading provider info for %s" % (self._domain)) - - headers = {} - - provider_json = os.path.join(get_path_prefix(), "leap", "providers", - self._domain, "provider.json") - mtime = get_mtime(provider_json) - - if self._download_if_needed and mtime: - headers['if-modified-since'] = mtime - - uri = "https://%s/%s" % (self._domain, "provider.json") - verify = not self._bypass_checks - - if mtime: # the provider.json exists - provider_config = ProviderConfig() - provider_config.load(provider_json) - try: - verify = provider_config.get_ca_cert_path() - uri = provider_config.get_api_uri() + '/provider.json' - except MissingCACert: - # get_ca_cert_path fails if the certificate does not exists. - pass - - logger.debug("Requesting for provider.json... " - "uri: {0}, verify: {1}, headers: {2}".format( - uri, verify, headers)) - res = self._session.get(uri, verify=verify, - headers=headers, timeout=REQUEST_TIMEOUT) - res.raise_for_status() - logger.debug("Request status code: {0}".format(res.status_code)) - - # Not modified - if res.status_code == 304: - logger.debug("Provider definition has not been modified") - else: - provider_definition, mtime = get_content(res) - - provider_config = ProviderConfig() - provider_config.load(data=provider_definition, mtime=mtime) - provider_config.save(["leap", - "providers", - self._domain, - "provider.json"]) - - api_version = provider_config.get_api_version() - if SupportedAPIs.supports(api_version): - logger.debug("Provider definition has been modified") - else: - api_supported = ', '.join(SupportedAPIs.SUPPORTED_APIS) - error = ('Unsupported provider API version. ' - 'Supported versions are: {}. ' - 'Found: {}.').format(api_supported, api_version) - - logger.error(error) - raise UnsupportedProviderAPI(error) - - def run_provider_select_checks(self, domain, download_if_needed=False): - """ - Populates the check queue. - - :param domain: domain to check - :type domain: str - - :param download_if_needed: if True, makes the checks do not - overwrite already downloaded data - :type download_if_needed: bool - """ - leap_assert(domain and len(domain) > 0, "We need a domain!") - - self._domain = ProviderConfig.sanitize_path_component(domain) - self._download_if_needed = download_if_needed - - cb_chain = [ - (self._check_name_resolution, self.name_resolution), - (self._check_https, self.https_connection), - (self._download_provider_info, self.download_provider_info) - ] - - return self.addCallbackChain(cb_chain) - - def _should_proceed_cert(self): - """ - Returns False if the certificate already exists for the given - provider. True otherwise - - :rtype: bool - """ - leap_assert(self._provider_config, "We need a provider config!") - - if not self._download_if_needed: - return True - - return not os.path.exists(self._provider_config - .get_ca_cert_path(about_to_download=True)) - - def _download_ca_cert(self, *args): - """ - Downloads the CA cert that is going to be used for the api URL - """ - - leap_assert(self._provider_config, "Cannot download the ca cert " - "without a provider config!") - - logger.debug("Downloading ca cert for %s at %s" % - (self._domain, self._provider_config.get_ca_cert_uri())) - - if not self._should_proceed_cert(): - check_and_fix_urw_only( - self._provider_config - .get_ca_cert_path(about_to_download=True)) - return - - res = self._session.get(self._provider_config.get_ca_cert_uri(), - verify=not self._bypass_checks, - timeout=REQUEST_TIMEOUT) - res.raise_for_status() - - cert_path = self._provider_config.get_ca_cert_path( - about_to_download=True) - cert_dir = os.path.dirname(cert_path) - mkdir_p(cert_dir) - with open(cert_path, "w") as f: - f.write(res.content) - - check_and_fix_urw_only(cert_path) - - def _check_ca_fingerprint(self, *args): - """ - Checks the CA cert fingerprint against the one provided in the - json definition - """ - leap_assert(self._provider_config, "Cannot check the ca cert " - "without a provider config!") - - logger.debug("Checking ca fingerprint for %s and cert %s" % - (self._domain, - self._provider_config.get_ca_cert_path())) - - if not self._should_proceed_cert(): - return - - parts = self._provider_config.get_ca_cert_fingerprint().split(":") - - error_msg = "Wrong fingerprint format" - leap_check(len(parts) == 2, error_msg, WrongFingerprint) - - method = parts[0].strip() - fingerprint = parts[1].strip() - cert_data = None - with open(self._provider_config.get_ca_cert_path()) as f: - cert_data = f.read() - - leap_assert(len(cert_data) > 0, "Could not read certificate data") - digest = get_digest(cert_data, method) - - error_msg = "Downloaded certificate has a different fingerprint!" - leap_check(digest == fingerprint, error_msg, WrongFingerprint) - - def _check_api_certificate(self, *args): - """ - Tries to make an API call with the downloaded cert and checks - if it validates against it - """ - leap_assert(self._provider_config, "Cannot check the ca cert " - "without a provider config!") - - logger.debug("Checking api certificate for %s and cert %s" % - (self._provider_config.get_api_uri(), - self._provider_config.get_ca_cert_path())) - - if not self._should_proceed_cert(): - return - - test_uri = "%s/%s/cert" % (self._provider_config.get_api_uri(), - self._provider_config.get_api_version()) - res = self._session.get(test_uri, - verify=self._provider_config - .get_ca_cert_path(), - timeout=REQUEST_TIMEOUT) - res.raise_for_status() - - def run_provider_setup_checks(self, - provider_config, - download_if_needed=False): - """ - Starts the checks needed for a new provider setup. - - :param provider_config: Provider configuration - :type provider_config: ProviderConfig - - :param download_if_needed: if True, makes the checks do not - overwrite already downloaded data. - :type download_if_needed: bool - """ - leap_assert(provider_config, "We need a provider config!") - leap_assert_type(provider_config, ProviderConfig) - - self._provider_config = provider_config - self._download_if_needed = download_if_needed - - cb_chain = [ - (self._download_ca_cert, self.download_ca_cert), - (self._check_ca_fingerprint, self.check_ca_fingerprint), - (self._check_api_certificate, self.check_api_certificate) - ] - - return self.addCallbackChain(cb_chain) diff --git a/src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py b/src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py deleted file mode 100644 index b0685676..00000000 --- a/src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py +++ /dev/null @@ -1,560 +0,0 @@ -# -*- coding: utf-8 -*- -# test_providerbootstrapper.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -""" -Tests for the Provider Boostrapper checks - -These will be whitebox tests since we want to make sure the private -implementation is checking what we expect. -""" - -import os -import mock -import socket -import stat -import tempfile -import time -import requests -try: - import unittest2 as unittest -except ImportError: - import unittest - -from nose.twistedtools import deferred, reactor -from twisted.internet import threads -from requests.models import Response - -from leap.bitmask.services.eip.providerbootstrapper import ProviderBootstrapper -from leap.bitmask.services.eip.providerbootstrapper import \ - UnsupportedProviderAPI -from leap.bitmask.services.eip.providerbootstrapper import WrongFingerprint -from leap.bitmask.provider.supportedapis import SupportedAPIs -from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.crypto.tests import fake_provider -from leap.common.files import mkdir_p -from leap.common.testing.https_server import where -from leap.common.testing.basetest import BaseLeapTest - - -class ProviderBootstrapperTest(BaseLeapTest): - def setUp(self): - self.pb = ProviderBootstrapper() - - def tearDown(self): - pass - - def test_name_resolution_check(self): - # Something highly likely to success - self.pb._domain = "google.com" - self.pb._check_name_resolution() - # Something highly likely to fail - self.pb._domain = "uquhqweuihowquie.abc.def" - - # In python 2.7.4 raises socket.error - # In python 2.7.5 raises socket.gaierror - with self.assertRaises((socket.gaierror, socket.error)): - self.pb._check_name_resolution() - - @deferred() - def test_run_provider_select_checks(self): - self.pb._check_name_resolution = mock.MagicMock() - self.pb._check_https = mock.MagicMock() - self.pb._download_provider_info = mock.MagicMock() - - d = self.pb.run_provider_select_checks("somedomain") - - def check(*args): - self.pb._check_name_resolution.assert_called_once_with() - self.pb._check_https.assert_called_once_with(None) - self.pb._download_provider_info.assert_called_once_with(None) - d.addCallback(check) - return d - - @deferred() - def test_run_provider_setup_checks(self): - self.pb._download_ca_cert = mock.MagicMock() - self.pb._check_ca_fingerprint = mock.MagicMock() - self.pb._check_api_certificate = mock.MagicMock() - - d = self.pb.run_provider_setup_checks(ProviderConfig()) - - def check(*args): - self.pb._download_ca_cert.assert_called_once_with() - self.pb._check_ca_fingerprint.assert_called_once_with(None) - self.pb._check_api_certificate.assert_called_once_with(None) - d.addCallback(check) - return d - - def test_should_proceed_cert(self): - self.pb._provider_config = mock.Mock() - self.pb._provider_config.get_ca_cert_path = mock.MagicMock( - return_value=where("cacert.pem")) - - self.pb._download_if_needed = False - self.assertTrue(self.pb._should_proceed_cert()) - - self.pb._download_if_needed = True - self.assertFalse(self.pb._should_proceed_cert()) - - self.pb._provider_config.get_ca_cert_path = mock.MagicMock( - return_value=where("somefilethatdoesntexist.pem")) - self.assertTrue(self.pb._should_proceed_cert()) - - def _check_download_ca_cert(self, should_proceed): - """ - Helper to check different paths easily for the download ca - cert check - - :param should_proceed: sets the _should_proceed_cert in the - provider bootstrapper being tested - :type should_proceed: bool - - :returns: The contents of the certificate, the expected - content depending on should_proceed, and the mode of - the file to be checked by the caller - :rtype: tuple of str, str, int - """ - old_content = "NOT THE NEW CERT" - new_content = "NEW CERT" - new_cert_path = os.path.join(tempfile.mkdtemp(), - "mynewcert.pem") - - with open(new_cert_path, "w") as c: - c.write(old_content) - - self.pb._provider_config = mock.Mock() - self.pb._provider_config.get_ca_cert_path = mock.MagicMock( - return_value=new_cert_path) - self.pb._domain = "somedomain" - - self.pb._should_proceed_cert = mock.MagicMock( - return_value=should_proceed) - - read = None - content_to_check = None - mode = None - - with mock.patch('requests.models.Response.content', - new_callable=mock.PropertyMock) as \ - content: - content.return_value = new_content - response_obj = Response() - response_obj.raise_for_status = mock.MagicMock() - - self.pb._session.get = mock.MagicMock(return_value=response_obj) - self.pb._download_ca_cert() - with open(new_cert_path, "r") as nc: - read = nc.read() - if should_proceed: - content_to_check = new_content - else: - content_to_check = old_content - mode = stat.S_IMODE(os.stat(new_cert_path).st_mode) - - os.unlink(new_cert_path) - return read, content_to_check, mode - - def test_download_ca_cert_no_saving(self): - read, expected_read, mode = self._check_download_ca_cert(False) - self.assertEqual(read, expected_read) - self.assertEqual(mode, int("600", 8)) - - def test_download_ca_cert_saving(self): - read, expected_read, mode = self._check_download_ca_cert(True) - self.assertEqual(read, expected_read) - self.assertEqual(mode, int("600", 8)) - - def test_check_ca_fingerprint_skips(self): - self.pb._provider_config = mock.Mock() - self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( - return_value="") - self.pb._domain = "somedomain" - - self.pb._should_proceed_cert = mock.MagicMock(return_value=False) - - self.pb._check_ca_fingerprint() - self.assertFalse(self.pb._provider_config. - get_ca_cert_fingerprint.called) - - def test_check_ca_cert_fingerprint_raises_bad_format(self): - self.pb._provider_config = mock.Mock() - self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( - return_value="wrongfprformat!!") - self.pb._domain = "somedomain" - - self.pb._should_proceed_cert = mock.MagicMock(return_value=True) - - with self.assertRaises(WrongFingerprint): - self.pb._check_ca_fingerprint() - - # This two hashes different in the last byte, but that's good enough - # for the tests - KNOWN_BAD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034efe" \ - "7dd1b910062ca323eb4da5c7f" - KNOWN_GOOD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034ef" \ - "e7dd1b910062ca323eb4da5c7e" - KNOWN_GOOD_CERT = """ ------BEGIN CERTIFICATE----- -MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt -YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v -Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw -FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV -BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw -ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai -dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB -7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84 -CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+ -znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4 -MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4 -lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0 -bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl -DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB -lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy -YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw -XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE -MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w -DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl -cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY -k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj -RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG -htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX -EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J -aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l -mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK -G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co -Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d -69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e -yV8e ------END CERTIFICATE----- -""" - - def _prepare_provider_config_with(self, cert_path, cert_hash): - """ - Mocks the provider config to give the cert_path and cert_hash - specified - - :param cert_path: path for the certificate - :type cert_path: str - :param cert_hash: hash for the certificate as it would appear - in the provider config json - :type cert_hash: str - """ - self.pb._provider_config = mock.Mock() - self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( - return_value=cert_hash) - self.pb._provider_config.get_ca_cert_path = mock.MagicMock( - return_value=cert_path) - self.pb._domain = "somedomain" - - def test_check_ca_fingerprint_checksout(self): - cert_path = os.path.join(tempfile.mkdtemp(), - "mynewcert.pem") - - with open(cert_path, "w") as c: - c.write(self.KNOWN_GOOD_CERT) - - self._prepare_provider_config_with(cert_path, self.KNOWN_GOOD_HASH) - - self.pb._should_proceed_cert = mock.MagicMock(return_value=True) - - self.pb._check_ca_fingerprint() - - os.unlink(cert_path) - - def test_check_ca_fingerprint_fails(self): - cert_path = os.path.join(tempfile.mkdtemp(), - "mynewcert.pem") - - with open(cert_path, "w") as c: - c.write(self.KNOWN_GOOD_CERT) - - self._prepare_provider_config_with(cert_path, self.KNOWN_BAD_HASH) - - self.pb._should_proceed_cert = mock.MagicMock(return_value=True) - - with self.assertRaises(WrongFingerprint): - self.pb._check_ca_fingerprint() - - os.unlink(cert_path) - - -############################################################################### -# Tests with a fake provider # -############################################################################### - -class ProviderBootstrapperActiveTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - factory = fake_provider.get_provider_factory() - http = reactor.listenTCP(8002, factory) - https = reactor.listenSSL( - 0, factory, - fake_provider.OpenSSLServerContextFactory()) - get_port = lambda p: p.getHost().port - cls.http_port = get_port(http) - cls.https_port = get_port(https) - - def setUp(self): - self.pb = ProviderBootstrapper() - - # At certain points we are going to be replacing these methods - # directly in ProviderConfig to be able to catch calls from - # new ProviderConfig objects inside the methods tested. We - # need to save the old implementation and restore it in - # tearDown so we are sure everything is as expected for each - # test. If we do it inside each specific test, a failure in - # the test will leave the implementation with the mock. - self.old_gpp = ProviderConfig.get_path_prefix - self.old_load = ProviderConfig.load - self.old_save = ProviderConfig.save - self.old_api_version = ProviderConfig.get_api_version - - def tearDown(self): - ProviderConfig.get_path_prefix = self.old_gpp - ProviderConfig.load = self.old_load - ProviderConfig.save = self.old_save - ProviderConfig.get_api_version = self.old_api_version - - def test_check_https_succeeds(self): - # XXX: Need a proper CA signed cert to test this - pass - - @deferred() - def test_check_https_fails(self): - self.pb._domain = "localhost:%s" % (self.https_port,) - - def check(*args): - with self.assertRaises(requests.exceptions.SSLError): - self.pb._check_https() - return threads.deferToThread(check) - - @deferred() - def test_second_check_https_fails(self): - self.pb._domain = "localhost:1234" - - def check(*args): - with self.assertRaises(Exception): - self.pb._check_https() - return threads.deferToThread(check) - - @deferred() - def test_check_https_succeeds_if_danger(self): - self.pb._domain = "localhost:%s" % (self.https_port,) - self.pb._bypass_checks = True - - def check(*args): - self.pb._check_https() - - return threads.deferToThread(check) - - def _setup_provider_config_with(self, api, path_prefix): - """ - Sets up the ProviderConfig with mocks for the path prefix, the - api returned and load/save methods. - It modifies ProviderConfig directly instead of an object - because the object used is created in the method itself and we - cannot control that. - - :param api: API to return - :type api: str - :param path_prefix: path prefix to be used when calculating - paths - :type path_prefix: str - """ - ProviderConfig.get_path_prefix = mock.MagicMock( - return_value=path_prefix) - ProviderConfig.get_api_version = mock.MagicMock( - return_value=api) - ProviderConfig.load = mock.MagicMock() - ProviderConfig.save = mock.MagicMock() - - def _setup_providerbootstrapper(self, ifneeded): - """ - Sets the provider bootstrapper's domain to - localhost:https_port, sets it to bypass https checks and sets - the download if needed based on the ifneeded value. - - :param ifneeded: Value for _download_if_needed - :type ifneeded: bool - """ - self.pb._domain = "localhost:%s" % (self.https_port,) - self.pb._bypass_checks = True - self.pb._download_if_needed = ifneeded - - def _produce_dummy_provider_json(self): - """ - Creates a dummy provider json on disk in order to test - behaviour around it (download if newer online, etc) - - :returns: the provider.json path used - :rtype: str - """ - provider_dir = os.path.join(ProviderConfig() - .get_path_prefix(), - "leap", - "providers", - self.pb._domain) - mkdir_p(provider_dir) - provider_path = os.path.join(provider_dir, - "provider.json") - - with open(provider_path, "w") as p: - p.write("A") - return provider_path - - def test_download_provider_info_new_provider(self): - self._setup_provider_config_with("1", tempfile.mkdtemp()) - self._setup_providerbootstrapper(True) - - self.pb._download_provider_info() - self.assertTrue(ProviderConfig.save.called) - - @mock.patch( - 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path', - lambda x: where('cacert.pem')) - def test_download_provider_info_not_modified(self): - self._setup_provider_config_with("1", tempfile.mkdtemp()) - self._setup_providerbootstrapper(True) - provider_path = self._produce_dummy_provider_json() - - # set mtime to something really new - os.utime(provider_path, (-1, time.time())) - - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - self.pb._download_provider_info() - # we check that it doesn't save the provider - # config, because it's new enough - self.assertFalse(ProviderConfig.save.called) - - @mock.patch( - 'leap.bitmask.config.providerconfig.ProviderConfig.get_domain', - lambda x: where('testdomain.com')) - def test_download_provider_info_not_modified_and_no_cacert(self): - self._setup_provider_config_with("1", tempfile.mkdtemp()) - self._setup_providerbootstrapper(True) - provider_path = self._produce_dummy_provider_json() - - # set mtime to something really new - os.utime(provider_path, (-1, time.time())) - - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - self.pb._download_provider_info() - # we check that it doesn't save the provider - # config, because it's new enough - self.assertFalse(ProviderConfig.save.called) - - @mock.patch( - 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path', - lambda x: where('cacert.pem')) - def test_download_provider_info_modified(self): - self._setup_provider_config_with("1", tempfile.mkdtemp()) - self._setup_providerbootstrapper(True) - provider_path = self._produce_dummy_provider_json() - - # set mtime to something really old - os.utime(provider_path, (-1, 100)) - - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - self.pb._download_provider_info() - self.assertTrue(ProviderConfig.load.called) - self.assertTrue(ProviderConfig.save.called) - - @mock.patch( - 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path', - lambda x: where('cacert.pem')) - def test_download_provider_info_unsupported_api_raises(self): - self._setup_provider_config_with("9999999", tempfile.mkdtemp()) - self._setup_providerbootstrapper(False) - self._produce_dummy_provider_json() - - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - with self.assertRaises(UnsupportedProviderAPI): - self.pb._download_provider_info() - - @mock.patch( - 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path', - lambda x: where('cacert.pem')) - def test_download_provider_info_unsupported_api(self): - self._setup_provider_config_with(SupportedAPIs.SUPPORTED_APIS[0], - tempfile.mkdtemp()) - self._setup_providerbootstrapper(False) - self._produce_dummy_provider_json() - - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - self.pb._download_provider_info() - - @mock.patch( - 'leap.bitmask.config.providerconfig.ProviderConfig.get_api_uri', - lambda x: 'api.uri') - @mock.patch( - 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path', - lambda x: '/cert/path') - def test_check_api_certificate_skips(self): - self.pb._provider_config = ProviderConfig() - self.pb._session.get = mock.MagicMock(return_value=Response()) - - self.pb._should_proceed_cert = mock.MagicMock(return_value=False) - self.pb._check_api_certificate() - self.assertFalse(self.pb._session.get.called) - - @deferred() - def test_check_api_certificate_fails(self): - self.pb._provider_config = ProviderConfig() - self.pb._provider_config.get_api_uri = mock.MagicMock( - return_value="https://localhost:%s" % (self.https_port,)) - self.pb._provider_config.get_ca_cert_path = mock.MagicMock( - return_value=os.path.join( - os.path.split(__file__)[0], - "wrongcert.pem")) - self.pb._provider_config.get_api_version = mock.MagicMock( - return_value="1") - - self.pb._should_proceed_cert = mock.MagicMock(return_value=True) - - def check(*args): - with self.assertRaises(requests.exceptions.SSLError): - self.pb._check_api_certificate() - d = threads.deferToThread(check) - return d - - @deferred() - def test_check_api_certificate_succeeds(self): - self.pb._provider_config = ProviderConfig() - self.pb._provider_config.get_api_uri = mock.MagicMock( - return_value="https://localhost:%s" % (self.https_port,)) - self.pb._provider_config.get_ca_cert_path = mock.MagicMock( - return_value=where('cacert.pem')) - self.pb._provider_config.get_api_version = mock.MagicMock( - return_value="1") - - self.pb._should_proceed_cert = mock.MagicMock(return_value=True) - - def check(*args): - self.pb._check_api_certificate() - d = threads.deferToThread(check) - return d -- cgit v1.2.3 From 9c8c949f776d3a0377d5112e34a05499d2c97bd3 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Sat, 28 Sep 2013 12:11:23 -0400 Subject: increase timeout, getting many timeouts for european providers --- src/leap/bitmask/util/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/leap') 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 . SIGNUP_TIMEOUT = 5 -REQUEST_TIMEOUT = 10 +REQUEST_TIMEOUT = 15 -- cgit v1.2.3 From 05ec95fda3a10488ee29904668c1ef56102db0a9 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Thu, 26 Sep 2013 17:12:19 -0300 Subject: Split vpnaunchers by platform, refactor some code. --- src/leap/bitmask/services/eip/darwinvpnlauncher.py | 190 ++++++++++++++ src/leap/bitmask/services/eip/linuxvpnlauncher.py | 232 +++++++++++++++++ src/leap/bitmask/services/eip/vpnlauncher.py | 290 +++++++++++++++++++++ .../bitmask/services/eip/windowsvpnlauncher.py | 69 +++++ 4 files changed, 781 insertions(+) create mode 100644 src/leap/bitmask/services/eip/darwinvpnlauncher.py create mode 100644 src/leap/bitmask/services/eip/linuxvpnlauncher.py create mode 100644 src/leap/bitmask/services/eip/vpnlauncher.py create mode 100644 src/leap/bitmask/services/eip/windowsvpnlauncher.py (limited to 'src/leap') 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 . +""" +Darwin VPN launcher implementation. +""" +import commands +import getpass +import logging +import os + +from leap.bitmask.services.eip.vpnlauncher import VPNLauncher +from leap.bitmask.services.eip.vpnlauncher import VPNLauncherException +from leap.bitmask.util import get_path_prefix + +logger = logging.getLogger(__name__) + + +class EIPNoTunKextLoaded(VPNLauncherException): + pass + + +class DarwinVPNLauncher(VPNLauncher): + """ + VPN launcher for the Darwin Platform + """ + COCOASUDO = "cocoasudo" + # XXX need the good old magic translate for these strings + # (look for magic in 0.2.0 release) + SUDO_MSG = ("Bitmask needs administrative privileges to run " + "Encrypted Internet.") + INSTALL_MSG = ("\"Bitmask needs administrative privileges to install " + "missing scripts and fix permissions.\"") + + INSTALL_PATH = os.path.realpath(os.getcwd() + "/../../") + INSTALL_PATH_ESCAPED = os.path.realpath(os.getcwd() + "/../../") + OPENVPN_BIN = 'openvpn.leap' + OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,) + OPENVPN_PATH_ESCAPED = "%s/Contents/Resources/openvpn" % ( + INSTALL_PATH_ESCAPED,) + + UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,) + DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,) + OPENVPN_DOWN_PLUGIN = '%s/openvpn-down-root.so' % (OPENVPN_PATH,) + + UPDOWN_FILES = (UP_SCRIPT, DOWN_SCRIPT, OPENVPN_DOWN_PLUGIN) + OTHER_FILES = [] + + @classmethod + def cmd_for_missing_scripts(kls, frompath): + """ + Returns a command that can copy the missing scripts. + :rtype: str + """ + to = kls.OPENVPN_PATH_ESCAPED + + cmd = "#!/bin/sh\n" + cmd += "mkdir -p {0}\n".format(to) + cmd += "cp '{0}'/* {1}\n".format(frompath, to) + cmd += "chmod 744 {0}/*".format(to) + + return cmd + + @classmethod + def is_kext_loaded(kls): + """ + Checks if the needed kext is loaded before launching openvpn. + + :returns: True if kext is loaded, False otherwise. + :rtype: bool + """ + return bool(commands.getoutput('kextstat | grep "leap.tun"')) + + @classmethod + def _get_icon_path(kls): + """ + Returns the absolute path to the app icon. + + :rtype: str + """ + resources_path = os.path.abspath( + os.path.join(os.getcwd(), "../../Contents/Resources")) + + return os.path.join(resources_path, "leap-client.tiff") + + @classmethod + def get_cocoasudo_ovpn_cmd(kls): + """ + Returns a string with the cocoasudo command needed to run openvpn + as admin with a nice password prompt. The actual command needs to be + appended. + + :rtype: (str, list) + """ + # TODO add translation support for this + sudo_msg = ("Bitmask needs administrative privileges to run " + "Encrypted Internet.") + iconpath = kls._get_icon_path() + has_icon = os.path.isfile(iconpath) + args = ["--icon=%s" % iconpath] if has_icon else [] + args.append("--prompt=%s" % (sudo_msg,)) + + return kls.COCOASUDO, args + + @classmethod + def get_cocoasudo_installmissing_cmd(kls): + """ + Returns a string with the cocoasudo command needed to install missing + files as admin with a nice password prompt. The actual command needs to + be appended. + + :rtype: (str, list) + """ + # TODO add translation support for this + install_msg = ('"Bitmask needs administrative privileges to install ' + 'missing scripts and fix permissions."') + iconpath = kls._get_icon_path() + has_icon = os.path.isfile(iconpath) + args = ["--icon=%s" % iconpath] if has_icon else [] + args.append("--prompt=%s" % (install_msg,)) + + return kls.COCOASUDO, args + + @classmethod + def get_vpn_command(kls, eipconfig, providerconfig, socket_host, + socket_port="unix", openvpn_verb=1): + """ + Returns the OSX implementation for the vpn launching command. + + Might raise: + EIPNoTunKextLoaded, + OpenVPNNotFoundException, + VPNLauncherException. + + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + :param socket_port: either string "unix" if it's a unix socket, + or port otherwise + :type socket_port: str + :param openvpn_verb: the openvpn verbosity wanted + :type openvpn_verb: int + + :return: A VPN command ready to be launched. + :rtype: list + """ + if not kls.is_kext_loaded(): + raise EIPNoTunKextLoaded + + # we use `super` in order to send the class to use + command = super(DarwinVPNLauncher, kls).get_vpn_command( + eipconfig, providerconfig, socket_host, socket_port, openvpn_verb) + + cocoa, cargs = kls.get_cocoasudo_ovpn_cmd() + cargs.extend(command) + command = cargs + command.insert(0, cocoa) + + command.extend(['--setenv', "LEAPUSER", getpass.getuser()]) + + return command + + @classmethod + def get_vpn_env(kls): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + :rtype: dict + """ + return { + "DYLD_LIBRARY_PATH": os.path.join(get_path_prefix(), "..", "lib") + } diff --git a/src/leap/bitmask/services/eip/linuxvpnlauncher.py b/src/leap/bitmask/services/eip/linuxvpnlauncher.py new file mode 100644 index 00000000..c2c28627 --- /dev/null +++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- +# linuxvpnlauncher.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +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 . +""" +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/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 . +""" +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 -- cgit v1.2.3 From 310b49e1ce5b8adb45f86be82a9f886b2ae4715e Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Thu, 26 Sep 2013 17:57:37 -0300 Subject: Replace launcher with new implementation. --- src/leap/bitmask/gui/mainwindow.py | 10 +- src/leap/bitmask/platform_init/initializers.py | 10 +- src/leap/bitmask/services/eip/__init__.py | 27 +- src/leap/bitmask/services/eip/vpnlaunchers.py | 965 ------------------------- src/leap/bitmask/services/eip/vpnprocess.py | 4 +- 5 files changed, 39 insertions(+), 977 deletions(-) delete mode 100644 src/leap/bitmask/services/eip/vpnlaunchers.py (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 200d68aa..6022210c 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -53,12 +53,12 @@ from leap.bitmask.services.eip.vpnprocess import VPN from leap.bitmask.services.eip.vpnprocess import OpenVPNAlreadyRunning from leap.bitmask.services.eip.vpnprocess import AlienOpenVPNAlreadyRunning -from leap.bitmask.services.eip.vpnlaunchers import VPNLauncherException -from leap.bitmask.services.eip.vpnlaunchers import OpenVPNNotFoundException -from leap.bitmask.services.eip.vpnlaunchers import EIPNoPkexecAvailable -from leap.bitmask.services.eip.vpnlaunchers import \ +from leap.bitmask.services.eip.vpnlauncher import VPNLauncherException +from leap.bitmask.services.eip.vpnlauncher import OpenVPNNotFoundException +from leap.bitmask.services.eip.linuxvpnlauncher import EIPNoPkexecAvailable +from leap.bitmask.services.eip.linuxvpnlauncher import \ EIPNoPolkitAuthAgentAvailable -from leap.bitmask.services.eip.vpnlaunchers import EIPNoTunKextLoaded +from leap.bitmask.services.eip.darwinvpnlauncher import EIPNoTunKextLoaded from leap.bitmask.util.keyring_helpers import has_keyring from leap.bitmask.util.leap_log_handler import LeapLogHandler diff --git a/src/leap/bitmask/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/__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/vpnlaunchers.py b/src/leap/bitmask/services/eip/vpnlaunchers.py deleted file mode 100644 index e27a48d9..00000000 --- a/src/leap/bitmask/services/eip/vpnlaunchers.py +++ /dev/null @@ -1,965 +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 . - -""" -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.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] - - -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) - - UP_SCRIPT = DOWN_SCRIPT = UP_DOWN_PATH - 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, providerconfig, socket_host, - 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 that standalone is set) - - 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: 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(self.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(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": # 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(self.UP_SCRIPT): - args += [ - '--up', '\"%s\"' % (self.UP_SCRIPT,), - ] - - if _has_updown_scripts(self.DOWN_SCRIPT): - args += [ - '--down', '\"%s\"' % (self.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(self.OPENVPN_DOWN_PLUGIN): - # args += [ - # '--plugin', self.OPENVPN_DOWN_ROOT, - # '\'%s\'' % self.DOWN_SCRIPT # for OSX - # '\'script_type=down %s\'' % self.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 = [openvpn] - pkexec = self.maybe_pkexec() - if pkexec: - command.insert(0, first(pkexec)) - - command_and_args = command + args - logger.debug("Running VPN with command:") - logger.debug(" ".join(command_and_args)) - - return command_and_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 -- cgit v1.2.3 From 42a262e685dd616e2bdc9a6f1e092cf25c3dc4e7 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Sat, 28 Sep 2013 17:19:57 -0400 Subject: add connection_aborted signal to state machine --- src/leap/bitmask/gui/mainwindow.py | 19 +++++++++---------- src/leap/bitmask/gui/statemachines.py | 8 +++++++- src/leap/bitmask/services/connections.py | 1 + src/leap/bitmask/services/eip/connection.py | 1 + 4 files changed, 18 insertions(+), 11 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 200d68aa..1e437999 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -1531,7 +1531,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 +1542,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..c02bf9bc 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -128,11 +128,17 @@ 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]) + # adding states to the machine for state in states.itervalues(): machine.addState(state) 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/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): -- cgit v1.2.3 From 24c91beb6f7102158a37330e914e19570bb85ecf Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 30 Sep 2013 13:32:51 -0400 Subject: add connection_died transition to connecting->off too --- src/leap/bitmask/gui/statemachines.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py index c02bf9bc..94726720 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -138,6 +138,14 @@ class ConnectionMachineBuilder(object): 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(): -- cgit v1.2.3 From f2c68ba96498f8b88e415aaf27ced735c790510e Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 27 Sep 2013 16:07:29 -0400 Subject: Improve error handling during soledad boostrapping in the client. The aim is to have better logs for debugging the different problems behind issues like #3619 and #3867. As I'm finding a good quantity of SSL handshake timeouts, I'm also adding a litte retry subroutine to the load_and_sync. Also, initialization and sync calls are separeted to be able to correlate logs with server-side soledad. --- .../services/soledad/soledadbootstrapper.py | 205 ++++++++++++++++----- src/leap/bitmask/util/__init__.py | 14 ++ 2 files changed, 170 insertions(+), 49 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index cac91440..afd7d623 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -14,15 +14,15 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - """ Soledad bootstrapping """ - import logging import os import socket +from ssl import SSLError + from PySide import QtCore from u1db import errors as u1db_errors @@ -32,9 +32,10 @@ from leap.bitmask.crypto.srpauth import SRPAuth 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.keymanager import KeyManager, openpgp from leap.keymanager.errors import KeyNotFound from leap.soledad.client import Soledad @@ -42,6 +43,17 @@ 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 @@ -109,68 +121,159 @@ 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() + 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 + raise Exception("No soledad server found") + + selected_server = server_dict[server_dict.keys()[0]] + server_url = "https://%s:%s/user-%s" % ( + selected_server["hostname"], + selected_server["port"], + uuid) + + logger.debug("Using soledad server url: %s" % (server_url,)) - logger.debug("Using soledad server url: %s" % (server_url,)) + cert_file = self._provider_config.get_ca_cert_path() - cert_file = self._provider_config.get_ca_cert_path() + logger.debug('local_db:%s' % (local_db_path,)) + logger.debug('secrets_path:%s' % (secrets_path,)) - # TODO: If selected server fails, retry with another host - # (issue #3309) + try: + self._try_soledad_init( + uuid, secrets_path, local_db_path, + server_url, cert_file, token) + except: + # re-raise the exceptions from try_init, + # we're currently handling the retries from the + # soledad-launcher in the gui. + raise + + leap_check(self._soledad is not None, + "Null soledad, error while initializing") + + # and now, let's sync + sync_tries = 10 + 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() + 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 + :cert_file: + :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 + 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_failed.emit() + 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): """ @@ -185,6 +288,8 @@ class SoledadBootstrapper(AbstractBootstrapper): self._soledad_config = SoledadConfig() + # TODO factor out with eip/provider configs. + headers = {} mtime = get_mtime( os.path.join(get_path_prefix(), "leap", "providers", @@ -242,8 +347,10 @@ class SoledadBootstrapper(AbstractBootstrapper): Generates the key pair if needed, uploads it to the webapp and nickserver """ - leap_assert(self._provider_config, + 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()) 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 -- cgit v1.2.3 From e55af608c81284f50133f53df6f49d69da0756b7 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 27 Sep 2013 17:29:10 -0400 Subject: add comments --- src/leap/bitmask/gui/mainwindow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 200d68aa..33af37e3 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -1007,6 +1007,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 +1032,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") -- cgit v1.2.3 From aea7b3dde14074d7462b5a0854dfad9d5d60e330 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 27 Sep 2013 17:29:58 -0400 Subject: remove unused import --- src/leap/bitmask/services/eip/eipbootstrapper.py | 1 - 1 file changed, 1 deletion(-) (limited to 'src/leap') 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 -- cgit v1.2.3 From 7eb0052c95585cd71f61a5477a963f6357b34400 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 27 Sep 2013 17:32:40 -0400 Subject: refactor to use generic download_service_config --- .../services/soledad/soledadbootstrapper.py | 146 +++++++++------------ 1 file changed, 62 insertions(+), 84 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index afd7d623..835e4cd9 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -29,13 +29,13 @@ 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.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 @@ -58,13 +58,13 @@ 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} @@ -80,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 @@ -153,6 +154,7 @@ class SoledadBootstrapper(AbstractBootstrapper): Once everthing is in the right place, we instantiate and sync Soledad """ + # TODO this method is still too large uuid = self.srpauth.get_uid() token = self.srpauth.get_token() @@ -162,7 +164,7 @@ class SoledadBootstrapper(AbstractBootstrapper): server_dict = self._soledad_config.get_hosts() if not server_dict.keys(): - # XXX raise more specific exception + # XXX raise more specific exception, and catch it properly! raise Exception("No soledad server found") selected_server = server_dict[server_dict.keys()[0]] @@ -170,7 +172,6 @@ class SoledadBootstrapper(AbstractBootstrapper): 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() @@ -192,10 +193,14 @@ class SoledadBootstrapper(AbstractBootstrapper): "Null soledad, error while initializing") # and now, let's sync - sync_tries = 10 + sync_tries = self.MAX_SYNC_RETRIES while sync_tries > 0: try: 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 @@ -224,11 +229,13 @@ class SoledadBootstrapper(AbstractBootstrapper): :param secrets_path: path to secrets file :param local_db_path: path to local db file :param server_url: soledad server uri - :cert_file: + :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: @@ -282,90 +289,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() - - # TODO factor out with eip/provider configs. - - 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 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()) - - 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(),), @@ -376,10 +339,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) -- cgit v1.2.3 From 2f13fb345ef0d400b099956654958025c7ef7392 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 27 Sep 2013 17:46:08 -0400 Subject: make socket errors during initialization recoverable --- src/leap/bitmask/gui/mainwindow.py | 2 ++ src/leap/bitmask/services/soledad/soledadbootstrapper.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 33af37e3..947ce58c 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -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( diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index 835e4cd9..d348661d 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -250,12 +250,16 @@ class SoledadBootstrapper(AbstractBootstrapper): # 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_failed.emit() + self.soledad_timeout.emit() + + # unrecoverable except u1db_errors.Unauthorized: logger.error("Error while initializing soledad " "(unauthorized).") -- cgit v1.2.3 From f59ddb7788509de1e84fcee90be7a1df700dd7b0 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Mon, 30 Sep 2013 17:28:42 -0300 Subject: Force cleanlooks only from bundle. --- src/leap/bitmask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index d5132731..c1859478 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -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") -- cgit v1.2.3 From 10d59e81c9ae5275a92d041e36c61a0628b726f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 30 Sep 2013 12:14:23 -0300 Subject: Implement new UI Also: - Remove status_panel - Add new icons - Refactor components a bit (mostly divide functionality) --- src/leap/bitmask/config/providerconfig.py | 19 + src/leap/bitmask/gui/clickablelabel.py | 28 + src/leap/bitmask/gui/eip_preferenceswindow.py | 177 +++++++ src/leap/bitmask/gui/eip_status.py | 427 ++++++++++++++++ src/leap/bitmask/gui/login.py | 126 ++++- src/leap/bitmask/gui/mail_status.py | 399 +++++++++++++++ src/leap/bitmask/gui/mainwindow.py | 208 +++----- src/leap/bitmask/gui/preferenceswindow.py | 117 ----- src/leap/bitmask/gui/statuspanel.py | 710 -------------------------- src/leap/bitmask/gui/ui/eip_status.ui | 287 +++++++++++ src/leap/bitmask/gui/ui/eippreferences.ui | 94 ++++ src/leap/bitmask/gui/ui/login.ui | 302 ++++++++--- src/leap/bitmask/gui/ui/mail_status.ui | 98 ++++ src/leap/bitmask/gui/ui/mainwindow.ui | 437 +++++++++------- src/leap/bitmask/gui/ui/preferences.ui | 175 ++----- src/leap/bitmask/gui/ui/statuspanel.ui | 393 -------------- 16 files changed, 2265 insertions(+), 1732 deletions(-) create mode 100644 src/leap/bitmask/gui/clickablelabel.py create mode 100644 src/leap/bitmask/gui/eip_preferenceswindow.py create mode 100644 src/leap/bitmask/gui/eip_status.py create mode 100644 src/leap/bitmask/gui/mail_status.py delete mode 100644 src/leap/bitmask/gui/statuspanel.py create mode 100644 src/leap/bitmask/gui/ui/eip_status.ui create mode 100644 src/leap/bitmask/gui/ui/eippreferences.ui create mode 100644 src/leap/bitmask/gui/ui/mail_status.ui delete mode 100644 src/leap/bitmask/gui/ui/statuspanel.ui (limited to 'src/leap') diff --git a/src/leap/bitmask/config/providerconfig.py b/src/leap/bitmask/config/providerconfig.py index c8c8a59e..44698d83 100644 --- a/src/leap/bitmask/config/providerconfig.py +++ b/src/leap/bitmask/config/providerconfig.py @@ -44,6 +44,25 @@ class ProviderConfig(BaseConfig): def __init__(self): BaseConfig.__init__(self) + @classmethod + def get_provider_config(self, domain): + """ + Helper to return a valid Provider Config from the domain name. + + :param domain: the domain name of the provider. + :type domain: str + + :rtype: ProviderConfig or None if there is a problem loading the config + """ + provider_config = ProviderConfig() + provider_config_path = os.path.join( + "leap", "providers", domain, "provider.json") + + if not provider_config.load(provider_config_path): + provider_config = None + + return provider_config + def _get_schema(self): """ Returns the schema corresponding to the version given. diff --git a/src/leap/bitmask/gui/clickablelabel.py b/src/leap/bitmask/gui/clickablelabel.py new file mode 100644 index 00000000..2808a601 --- /dev/null +++ b/src/leap/bitmask/gui/clickablelabel.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# clickablelabel.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 . + +""" +Clickable label +""" +from PySide import QtCore, QtGui + + +class ClickableLabel(QtGui.QLabel): + clicked = QtCore.Signal() + + def mousePressEvent(self, event): + self.clicked.emit() diff --git a/src/leap/bitmask/gui/eip_preferenceswindow.py b/src/leap/bitmask/gui/eip_preferenceswindow.py new file mode 100644 index 00000000..0e6e8dda --- /dev/null +++ b/src/leap/bitmask/gui/eip_preferenceswindow.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# eip_preferenceswindow.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 . + +""" +EIP Preferences window +""" +import os +import logging + +from functools import partial +from PySide import QtGui + +from leap.bitmask.config.leapsettings import LeapSettings +from leap.bitmask.config.providerconfig import ProviderConfig +from leap.bitmask.gui.ui_eippreferences import Ui_EIPPreferences +from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector + +logger = logging.getLogger(__name__) + + +class EIPPreferencesWindow(QtGui.QDialog): + """ + Window that displays the EIP preferences. + """ + def __init__(self, parent): + """ + :param parent: parent object of the EIPPreferencesWindow. + :parent type: QWidget + """ + QtGui.QDialog.__init__(self, parent) + self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") + + self._settings = LeapSettings() + + # Load UI + self.ui = Ui_EIPPreferences() + self.ui.setupUi(self) + self.ui.lblProvidersGatewayStatus.setVisible(False) + + # Connections + self.ui.cbProvidersGateway.currentIndexChanged[unicode].connect( + self._populate_gateways) + + self.ui.cbGateways.currentIndexChanged[unicode].connect( + lambda x: self.ui.lblProvidersGatewayStatus.setVisible(False)) + + self._add_configured_providers() + + def _set_providers_gateway_status(self, status, success=False, + error=False): + """ + Sets the status label for the gateway change. + + :param status: status message to display, can be HTML + :type status: str + :param success: is set to True if we should display the + message as green + :type success: bool + :param error: is set to True if we should display the + message as red + :type error: bool + """ + if success: + status = "%s" % (status,) + elif error: + status = "%s" % (status,) + + self.ui.lblProvidersGatewayStatus.setVisible(True) + self.ui.lblProvidersGatewayStatus.setText(status) + + def _add_configured_providers(self): + """ + Add the client's configured providers to the providers combo boxes. + """ + self.ui.cbProvidersGateway.clear() + for provider in self._settings.get_configured_providers(): + self.ui.cbProvidersGateway.addItem(provider) + + def _save_selected_gateway(self, provider): + """ + SLOT + TRIGGERS: + self.ui.pbSaveGateway.clicked + + Saves the new gateway setting to the configuration file. + + :param provider: the provider config that we need to save. + :type provider: str + """ + gateway = self.ui.cbGateways.currentText() + + if gateway == self.AUTOMATIC_GATEWAY_LABEL: + gateway = self._settings.GATEWAY_AUTOMATIC + else: + idx = self.ui.cbGateways.currentIndex() + gateway = self.ui.cbGateways.itemData(idx) + + self._settings.set_selected_gateway(provider, gateway) + + msg = self.tr( + "Gateway settings for provider '{0}' saved.").format(provider) + self._set_providers_gateway_status(msg, success=True) + + def _populate_gateways(self, domain): + """ + SLOT + TRIGGERS: + self.ui.cbProvidersGateway.currentIndexChanged[unicode] + + Loads the gateways that the provider provides into the UI for + the user to select. + + :param domain: the domain of the provider to load gateways from. + :type domain: str + """ + # We hide the maybe-visible status label after a change + self.ui.lblProvidersGatewayStatus.setVisible(False) + + if not domain: + return + + try: + # disconnect previously connected save method + self.ui.pbSaveGateway.clicked.disconnect() + except RuntimeError: + pass # Signal was not connected + + # set the proper connection for the 'save' button + save_gateway = partial(self._save_selected_gateway, domain) + self.ui.pbSaveGateway.clicked.connect(save_gateway) + + eip_config = EIPConfig() + provider_config = ProviderConfig.get_provider_config(domain) + + eip_config_path = os.path.join("leap", "providers", + domain, "eip-service.json") + api_version = provider_config.get_api_version() + eip_config.set_api_version(api_version) + eip_loaded = eip_config.load(eip_config_path) + + if not eip_loaded or provider_config is None: + self._set_providers_gateway_status( + self.tr("There was a problem with configuration files."), + error=True) + return + + gateways = VPNGatewaySelector(eip_config).get_gateways_list() + logger.debug(gateways) + + self.ui.cbGateways.clear() + self.ui.cbGateways.addItem(self.AUTOMATIC_GATEWAY_LABEL) + + # Add the available gateways and + # select the one stored in configuration file. + selected_gateway = self._settings.get_selected_gateway(domain) + index = 0 + for idx, (gw_name, gw_ip) in enumerate(gateways): + gateway = "{0} ({1})".format(gw_name, gw_ip) + self.ui.cbGateways.addItem(gateway, gw_ip) + if gw_ip == selected_gateway: + index = idx + 1 + + self.ui.cbGateways.setCurrentIndex(index) diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py new file mode 100644 index 00000000..f7408b13 --- /dev/null +++ b/src/leap/bitmask/gui/eip_status.py @@ -0,0 +1,427 @@ +# -*- coding: utf-8 -*- +# eip_status.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 . +""" +EIP Status Panel widget implementation +""" +import logging + +from datetime import datetime +from functools import partial + +from PySide import QtCore, QtGui + +from leap.bitmask.services.eip.connection import EIPConnection +from leap.bitmask.services.eip.vpnprocess import VPNManager +from leap.bitmask.platform_init import IS_LINUX +from leap.bitmask.util.averages import RateMovingAverage +from leap.common.check import leap_assert_type + +from ui_eip_status import Ui_EIPStatus + +logger = logging.getLogger(__name__) + + +class EIPStatusWidget(QtGui.QWidget): + """ + EIP Status widget that displays the current state of the EIP service + """ + DISPLAY_TRAFFIC_RATES = True + RATE_STR = "%14.2f KB/s" + TOTAL_STR = "%14.2f Kb" + + eip_connection_connected = QtCore.Signal() + + def __init__(self, parent=None): + QtGui.QWidget.__init__(self, parent) + + self._systray = None + self._eip_status_menu = None + + self.ui = Ui_EIPStatus() + self.ui.setupUi(self) + + self.eipconnection = EIPConnection() + + # set systray tooltip status + self._eip_status = "" + + self.ui.eip_bandwidth.hide() + + # Set the EIP status icons + self.CONNECTING_ICON = None + self.CONNECTED_ICON = None + self.ERROR_ICON = None + self.CONNECTING_ICON_TRAY = None + self.CONNECTED_ICON_TRAY = None + self.ERROR_ICON_TRAY = None + self._set_eip_icons() + + self._set_traffic_rates() + self._make_status_clickable() + + self._provider = "" + + def _make_status_clickable(self): + """ + Makes upload and download figures clickable. + """ + onclicked = self._on_VPN_status_clicked + self.ui.btnUpload.clicked.connect(onclicked) + self.ui.btnDownload.clicked.connect(onclicked) + + def _on_VPN_status_clicked(self): + """ + SLOT + TRIGGER: self.ui.btnUpload.clicked + self.ui.btnDownload.clicked + + Toggles between rate and total throughput display for vpn + status figures. + """ + self.DISPLAY_TRAFFIC_RATES = not self.DISPLAY_TRAFFIC_RATES + self.update_vpn_status(None) # refresh + + def _set_traffic_rates(self): + """ + Initializes up and download rates. + """ + self._up_rate = RateMovingAverage() + self._down_rate = RateMovingAverage() + + self.ui.btnUpload.setText(self.RATE_STR % (0,)) + self.ui.btnDownload.setText(self.RATE_STR % (0,)) + + def _reset_traffic_rates(self): + """ + Resets up and download rates, and cleans up the labels. + """ + self._up_rate.reset() + self._down_rate.reset() + self.update_vpn_status(None) + + def _update_traffic_rates(self, up, down): + """ + Updates up and download rates. + + :param up: upload total. + :type up: int + :param down: download total. + :type down: int + """ + ts = datetime.now() + self._up_rate.append((ts, up)) + self._down_rate.append((ts, down)) + + def _get_traffic_rates(self): + """ + Gets the traffic rates (in KB/s). + + :returns: a tuple with the (up, down) rates + :rtype: tuple + """ + up = self._up_rate + down = self._down_rate + + return (up.get_average(), down.get_average()) + + def _get_traffic_totals(self): + """ + Gets the traffic total throughput (in Kb). + + :returns: a tuple with the (up, down) totals + :rtype: tuple + """ + up = self._up_rate + down = self._down_rate + + return (up.get_total(), down.get_total()) + + def _set_eip_icons(self): + """ + Sets the EIP status icons for the main window and for the tray + + MAC : dark icons + LINUX : dark icons in window, light icons in tray + WIN : light icons + """ + EIP_ICONS = EIP_ICONS_TRAY = ( + ":/images/black/32/wait.png", + ":/images/black/32/on.png", + ":/images/black/32/off.png") + + if IS_LINUX: + EIP_ICONS_TRAY = ( + ":/images/white/32/wait.png", + ":/images/white/32/on.png", + ":/images/white/32/off.png") + + self.CONNECTING_ICON = QtGui.QPixmap(EIP_ICONS[0]) + self.CONNECTED_ICON = QtGui.QPixmap(EIP_ICONS[1]) + self.ERROR_ICON = QtGui.QPixmap(EIP_ICONS[2]) + + self.CONNECTING_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[0]) + self.CONNECTED_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[1]) + self.ERROR_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[2]) + + # Systray and actions + + def set_systray(self, systray): + """ + Sets the systray object to use. + + :param systray: Systray object + :type systray: QtGui.QSystemTrayIcon + """ + leap_assert_type(systray, QtGui.QSystemTrayIcon) + self._systray = systray + self._systray.setToolTip(self.tr("All services are OFF")) + + def _update_systray_tooltip(self): + """ + Updates the system tray icon tooltip using the eip and mx status. + """ + status = self.tr("Encrypted Internet: {0}").format(self._eip_status) + self._systray.setToolTip(status) + + def set_action_eip_startstop(self, action_eip_startstop): + """ + Sets the action_eip_startstop to use. + + :param action_eip_startstop: action_eip_status to be used + :type action_eip_startstop: QtGui.QAction + """ + self._action_eip_startstop = action_eip_startstop + + def set_eip_status_menu(self, eip_status_menu): + """ + Sets the eip_status_menu to use. + + :param eip_status_menu: eip_status_menu to be used + :type eip_status_menu: QtGui.QMenu + """ + leap_assert_type(eip_status_menu, QtGui.QMenu) + self._eip_status_menu = eip_status_menu + + def set_eip_status(self, status, error=False): + """ + Sets the global status label. + + :param status: status message + :type status: str or unicode + :param error: if the status is an erroneous one, then set this + to True + :type error: bool + """ + leap_assert_type(error, bool) + if error: + status = "%s" % (status,) + self.ui.lblEIPStatus.setText(status) + self.ui.lblEIPStatus.show() + + # EIP status --- + + @property + def eip_button(self): + return self.ui.btnEipStartStop + + @property + def eip_label(self): + return self.ui.lblEIPStatus + + def eip_pre_up(self): + """ + Triggered when the app activates eip. + Hides the status box and disables the start/stop button. + """ + self.set_startstop_enabled(False) + + # XXX disable (later) -------------------------- + def set_eip_status(self, status, error=False): + """ + Sets the status label at the VPN stage to status + + :param status: status message + :type status: str or unicode + :param error: if the status is an erroneous one, then set this + to True + :type error: bool + """ + leap_assert_type(error, bool) + + self._eip_status = status + + if error: + status = "%s" % (status,) + self.ui.lblEIPStatus.setText(status) + self.ui.lblEIPStatus.show() + self._update_systray_tooltip() + + # XXX disable --------------------------------- + def set_startstop_enabled(self, value): + """ + Enable or disable btnEipStartStop and _action_eip_startstop + based on value + + :param value: True for enabled, False otherwise + :type value: bool + """ + leap_assert_type(value, bool) + self.ui.btnEipStartStop.setEnabled(value) + self._action_eip_startstop.setEnabled(value) + + # XXX disable ----------------------------- + def eip_started(self): + """ + Sets the state of the widget to how it should look after EIP + has started + """ + self.ui.btnEipStartStop.setText(self.tr("Turn OFF")) + self.ui.btnEipStartStop.disconnect(self) + self.ui.btnEipStartStop.clicked.connect( + self.eipconnection.qtsigs.do_connect_signal) + + # XXX disable ----------------------------- + def eip_stopped(self): + """ + Sets the state of the widget to how it should look after EIP + has stopped + """ + # XXX should connect this to EIPConnection.disconnected_signal + self._reset_traffic_rates() + # XXX disable ----------------------------- + self.ui.btnEipStartStop.setText(self.tr("Turn ON")) + self.ui.btnEipStartStop.disconnect(self) + self.ui.btnEipStartStop.clicked.connect( + self.eipconnection.qtsigs.do_disconnect_signal) + + self.ui.eip_bandwidth.hide() + self.ui.lblEIPMessage.setText( + self.tr("Traffic is being routed in the clear")) + self.ui.lblEIPStatus.show() + + def update_vpn_status(self, data): + """ + SLOT + TRIGGER: VPN.status_changed + + Updates the download/upload labels based on the data provided + by the VPN thread. + + :param data: a dictionary with the tcp/udp write and read totals. + If data is None, we just will refresh the display based + on the previous data. + :type data: dict + """ + if data: + upload = float(data[VPNManager.TCPUDP_WRITE_KEY] or "0") + download = float(data[VPNManager.TCPUDP_READ_KEY] or "0") + self._update_traffic_rates(upload, download) + + if self.DISPLAY_TRAFFIC_RATES: + uprate, downrate = self._get_traffic_rates() + upload_str = self.RATE_STR % (uprate,) + download_str = self.RATE_STR % (downrate,) + + else: # display total throughput + uptotal, downtotal = self._get_traffic_totals() + upload_str = self.TOTAL_STR % (uptotal,) + download_str = self.TOTAL_STR % (downtotal,) + + self.ui.btnUpload.setText(upload_str) + self.ui.btnDownload.setText(download_str) + + def update_vpn_state(self, data): + """ + SLOT + TRIGGER: VPN.state_changed + + Updates the displayed VPN state based on the data provided by + the VPN thread. + + Emits: + If the status is connected, we emit EIPConnection.qtsigs. + connected_signal + """ + status = data[VPNManager.STATUS_STEP_KEY] + self.set_eip_status_icon(status) + if status == "CONNECTED": + self.ui.eip_bandwidth.show() + self.ui.lblEIPStatus.hide() + + # XXX should be handled by the state machine too. + self.eip_connection_connected.emit() + + # XXX should lookup status map in EIPConnection + elif status == "AUTH": + self.set_eip_status(self.tr("Authenticating...")) + elif status == "GET_CONFIG": + self.set_eip_status(self.tr("Retrieving configuration...")) + elif status == "WAIT": + self.set_eip_status(self.tr("Waiting to start...")) + elif status == "ASSIGN_IP": + self.set_eip_status(self.tr("Assigning IP")) + elif status == "RECONNECTING": + self.set_eip_status(self.tr("Reconnecting...")) + elif status == "ALREADYRUNNING": + # Put the following calls in Qt's event queue, otherwise + # the UI won't update properly + QtCore.QTimer.singleShot( + 0, self.eipconnection.qtsigs.do_disconnect_signal) + QtCore.QTimer.singleShot(0, partial(self.set_eip_status, + self.tr("Unable to start VPN, " + "it's already " + "running."))) + else: + self.set_eip_status(status) + + def set_eip_icon(self, icon): + """ + Sets the icon to display for EIP + + :param icon: icon to display + :type icon: QPixmap + """ + self.ui.lblVPNStatusIcon.setPixmap(icon) + + def set_eip_status_icon(self, status): + """ + Given a status step from the VPN thread, set the icon properly + + :param status: status step + :type status: str + """ + selected_pixmap = self.ERROR_ICON + selected_pixmap_tray = self.ERROR_ICON_TRAY + tray_message = self.tr("Encrypted Internet: OFF") + if status in ("WAIT", "AUTH", "GET_CONFIG", + "RECONNECTING", "ASSIGN_IP"): + selected_pixmap = self.CONNECTING_ICON + selected_pixmap_tray = self.CONNECTING_ICON_TRAY + tray_message = self.tr("Encrypted Internet: Starting...") + elif status in ("CONNECTED"): + tray_message = self.tr("Encrypted Internet: ON") + selected_pixmap = self.CONNECTED_ICON + selected_pixmap_tray = self.CONNECTED_ICON_TRAY + + self.set_eip_icon(selected_pixmap) + self._systray.setIcon(QtGui.QIcon(selected_pixmap_tray)) + self._eip_status_menu.setTitle(tray_message) + + def set_provider(self, provider): + self._provider = provider + self.ui.lblEIPMessage.setText( + self.tr("Route traffic through: {0}").format(self._provider)) diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py index db7b8e2a..9a369f6d 100644 --- a/src/leap/bitmask/gui/login.py +++ b/src/leap/bitmask/gui/login.py @@ -20,10 +20,13 @@ Login widget implementation """ import logging +import keyring + from PySide import QtCore, QtGui from ui_login import Ui_LoginWidget from leap.bitmask.util.keyring_helpers import has_keyring +from leap.common.check import leap_assert_type logger = logging.getLogger(__name__) @@ -37,6 +40,7 @@ class LoginWidget(QtGui.QWidget): # Emitted when the login button is clicked login = QtCore.Signal() cancel_login = QtCore.Signal() + logout = QtCore.Signal() # Emitted when the user selects "Other..." in the provider # combobox or click "Create Account" @@ -76,13 +80,21 @@ class LoginWidget(QtGui.QWidget): self.ui.cmbProviders.currentIndexChanged.connect( self._current_provider_changed) - self.ui.btnCreateAccount.clicked.connect( - self.show_wizard) + + self.ui.btnLogout.clicked.connect( + self.logout) username_re = QtCore.QRegExp(self.BARE_USERNAME_REGEX) self.ui.lnUser.setValidator( QtGui.QRegExpValidator(username_re, self)) + self.logged_out() + + self.ui.btnLogout.clicked.connect(self.start_logout) + + self.ui.clblErrorMsg.hide() + self.ui.clblErrorMsg.clicked.connect(self.ui.clblErrorMsg.hide) + def _remember_state_changed(self, state): """ Saves the remember state in the LeapSettings @@ -185,8 +197,10 @@ class LoginWidget(QtGui.QWidget): if len(status) > self.MAX_STATUS_WIDTH: status = status[:self.MAX_STATUS_WIDTH] + "..." if error: - status = "%s" % (status,) - self.ui.lblStatus.setText(status) + self.ui.clblErrorMsg.show() + self.ui.clblErrorMsg.setText(status) + else: + self.ui.lblStatus.setText(status) def set_enabled(self, enabled=False): """ @@ -211,6 +225,7 @@ class LoginWidget(QtGui.QWidget): """ text = self.tr("Cancel") login_or_cancel = self.cancel_login + hide_remember = enabled if not enabled: text = self.tr("Log In") @@ -220,6 +235,8 @@ class LoginWidget(QtGui.QWidget): self.ui.btnLogin.clicked.disconnect() self.ui.btnLogin.clicked.connect(login_or_cancel) + self.ui.chkRemember.setVisible(not hide_remember) + self.ui.lblStatus.setVisible(hide_remember) def _focus_password(self): """ @@ -243,3 +260,104 @@ class LoginWidget(QtGui.QWidget): self.ui.cmbProviders.blockSignals(False) else: self._selected_provider_index = param + + def start_login(self): + """ + Setups the login widgets for actually performing the login and + performs some basic checks. + + :returns: True if everything's good to go, False otherwise + :rtype: bool + """ + username = self.get_user() + password = self.get_password() + provider = self.get_selected_provider() + + self._enabled_services = self._settings.get_enabled_services( + self.get_selected_provider()) + + if len(provider) == 0: + self.set_status( + self.tr("Please select a valid provider")) + return False + + if len(username) == 0: + self.set_status( + self.tr("Please provide a valid username")) + return False + + if len(password) == 0: + self.set_status( + self.tr("Please provide a valid password")) + return False + + self.set_status(self.tr("Logging in..."), error=False) + self.set_enabled(False) + + if self.get_remember() and has_keyring(): + # in the keyring and in the settings + # we store the value 'usename@provider' + username_domain = (username + '@' + provider).encode("utf8") + try: + keyring.set_password(self.KEYRING_KEY, + username_domain, + password.encode("utf8")) + # Only save the username if it was saved correctly in + # the keyring + self._settings.set_user(username_domain) + except Exception as e: + logger.exception("Problem saving data to keyring. %r" + % (e,)) + return True + + def logged_in(self): + """ + Sets the widgets to the logged in state + """ + self.ui.login_widget.hide() + self.ui.logged_widget.show() + self.ui.lblUser.setText("%s@%s" % (self.get_user(), + self.get_selected_provider())) + self.set_login_status("") + + def logged_out(self): + """ + Sets the widgets to the logged out state + """ + self.ui.login_widget.show() + self.ui.logged_widget.hide() + + self.set_password("") + self.set_enabled(True) + self.set_status("") + + def set_login_status(self, msg, error=False): + """ + Sets the status label for the logged in state. + + :param msg: status message + :type msg: str or unicode + :param error: if the status is an erroneous one, then set this + to True + :type error: bool + """ + leap_assert_type(error, bool) + if error: + msg = "%s" % (msg,) + self.ui.lblLoginStatus.setText(msg) + self.ui.lblLoginStatus.show() + + def start_logout(self): + """ + Sets the widgets to the logging out state + """ + self.ui.btnLogout.setText(self.tr("Loggin out...")) + self.ui.btnLogout.setEnabled(False) + + def done_logout(self): + """ + Sets the widgets to the logged out state + """ + self.ui.btnLogout.setText(self.tr("Logout")) + self.ui.btnLogout.setEnabled(True) + self.ui.clblErrorMsg.hide() diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py new file mode 100644 index 00000000..770d991f --- /dev/null +++ b/src/leap/bitmask/gui/mail_status.py @@ -0,0 +1,399 @@ +# -*- coding: utf-8 -*- +# mail_status.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 . +""" +Mail Status Panel widget implementation +""" +import logging + +from PySide import QtCore, QtGui + +from leap.bitmask.platform_init import IS_LINUX +from leap.common.check import leap_assert, leap_assert_type +from leap.common.events import register +from leap.common.events import events_pb2 as proto + +from ui_mail_status import Ui_MailStatusWidget + +logger = logging.getLogger(__name__) + + +class MailStatusWidget(QtGui.QWidget): + """ + Status widget that displays the state of the LEAP Mail service + """ + eip_connection_connected = QtCore.Signal() + _soledad_event = QtCore.Signal(object) + _smtp_event = QtCore.Signal(object) + _imap_event = QtCore.Signal(object) + _keymanager_event = QtCore.Signal(object) + + def __init__(self, parent=None): + """ + Constructor for MailStatusWidget + + :param parent: parent widget for this one. + :type parent: QtGui.QWidget + """ + QtGui.QWidget.__init__(self, parent) + + self._systray = None + + self.ui = Ui_MailStatusWidget() + self.ui.setupUi(self) + + # set systray tooltip status + self._mx_status = "" + + # Set the Mail status icons + self.CONNECTING_ICON = None + self.CONNECTED_ICON = None + self.ERROR_ICON = None + self.CONNECTING_ICON_TRAY = None + self.CONNECTED_ICON_TRAY = None + self.ERROR_ICON_TRAY = None + self._set_mail_icons() + + register(signal=proto.KEYMANAGER_LOOKING_FOR_KEY, + callback=self._mail_handle_keymanager_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.KEYMANAGER_KEY_FOUND, + callback=self._mail_handle_keymanager_events, + reqcbk=lambda req, resp: None) + + # register(signal=proto.KEYMANAGER_KEY_NOT_FOUND, + # callback=self._mail_handle_keymanager_events, + # reqcbk=lambda req, resp: None) + + register(signal=proto.KEYMANAGER_STARTED_KEY_GENERATION, + callback=self._mail_handle_keymanager_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.KEYMANAGER_FINISHED_KEY_GENERATION, + callback=self._mail_handle_keymanager_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.KEYMANAGER_DONE_UPLOADING_KEYS, + callback=self._mail_handle_keymanager_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.SOLEDAD_DONE_DOWNLOADING_KEYS, + callback=self._mail_handle_soledad_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.SOLEDAD_DONE_UPLOADING_KEYS, + callback=self._mail_handle_soledad_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.SMTP_SERVICE_STARTED, + callback=self._mail_handle_smtp_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.SMTP_SERVICE_FAILED_TO_START, + callback=self._mail_handle_smtp_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.IMAP_SERVICE_STARTED, + callback=self._mail_handle_imap_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.IMAP_SERVICE_FAILED_TO_START, + callback=self._mail_handle_imap_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.IMAP_UNREAD_MAIL, + callback=self._mail_handle_imap_events, + reqcbk=lambda req, resp: None) + + self._smtp_started = False + self._imap_started = False + + self._soledad_event.connect( + self._mail_handle_soledad_events_slot) + self._imap_event.connect( + self._mail_handle_imap_events_slot) + self._smtp_event.connect( + self._mail_handle_smtp_events_slot) + self._keymanager_event.connect( + self._mail_handle_keymanager_events_slot) + + def _set_mail_icons(self): + """ + Sets the Mail status icons for the main window and for the tray + + MAC : dark icons + LINUX : dark icons in window, light icons in tray + WIN : light icons + """ + EIP_ICONS = EIP_ICONS_TRAY = ( + ":/images/black/32/wait.png", + ":/images/black/32/on.png", + ":/images/black/32/off.png") + + if IS_LINUX: + EIP_ICONS_TRAY = ( + ":/images/white/32/wait.png", + ":/images/white/32/on.png", + ":/images/white/32/off.png") + + self.CONNECTING_ICON = QtGui.QPixmap(EIP_ICONS[0]) + self.CONNECTED_ICON = QtGui.QPixmap(EIP_ICONS[1]) + self.ERROR_ICON = QtGui.QPixmap(EIP_ICONS[2]) + + self.CONNECTING_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[0]) + self.CONNECTED_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[1]) + self.ERROR_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[2]) + + # Systray and actions + + def set_systray(self, systray): + """ + Sets the systray object to use. + + :param systray: Systray object + :type systray: QtGui.QSystemTrayIcon + """ + leap_assert_type(systray, QtGui.QSystemTrayIcon) + self._systray = systray + self._systray.setToolTip(self.tr("All services are OFF")) + + def _update_systray_tooltip(self): + """ + Updates the system tray icon tooltip using the eip and mx status. + """ + # TODO: Figure out how to handle this with the two status in different + # classes + # status = self.tr("Encrypted Internet: {0}").format(self._eip_status) + # status += '\n' + # status += self.tr("Mail is {0}").format(self._mx_status) + # self._systray.setToolTip(status) + pass + + def set_action_mail_status(self, action_mail_status): + """ + Sets the action_mail_status to use. + + :param action_mail_status: action_mail_status to be used + :type action_mail_status: QtGui.QAction + """ + leap_assert_type(action_mail_status, QtGui.QAction) + self._action_mail_status = action_mail_status + + def _set_mail_status(self, status, ready=0): + """ + Sets the Mail status in the label and in the tray icon. + + :param status: the status text to display + :type status: unicode + :param ready: 2 or >2 if mx is ready, 0 if stopped, 1 if it's starting. + :type ready: int + """ + self.ui.lblMailStatus.setText(status) + + self._mx_status = self.tr('OFF') + tray_status = self.tr('Mail is OFF') + + icon = self.ERROR_ICON + if ready == 0: + self.ui.lblMailStatus.setText( + self.tr("You must be logged in to use encrypted email.")) + elif ready == 1: + icon = self.CONNECTING_ICON + self._mx_status = self.tr('Starting..') + tray_status = self.tr('Mail is starting') + elif ready >= 2: + icon = self.CONNECTED_ICON + self._mx_status = self.tr('ON') + tray_status = self.tr('Mail is ON') + + self.ui.lblMailStatusIcon.setPixmap(icon) + self._action_mail_status.setText(tray_status) + self._update_systray_tooltip() + + def _mail_handle_soledad_events(self, req): + """ + Callback for handling events that are emitted from Soledad + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + self._soledad_event.emit(req) + + def _mail_handle_soledad_events_slot(self, req): + """ + SLOT + TRIGGER: _mail_handle_soledad_events + + Reacts to an Soledad event + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + self._set_mail_status(self.tr("Starting..."), ready=1) + + ext_status = "" + + if req.event == proto.SOLEDAD_DONE_UPLOADING_KEYS: + ext_status = self.tr("Soledad has started...") + elif req.event == proto.SOLEDAD_DONE_DOWNLOADING_KEYS: + ext_status = self.tr("Soledad is starting, please wait...") + else: + leap_assert(False, + "Don't know how to handle this state: %s" + % (req.event)) + + self._set_mail_status(ext_status, ready=1) + + def _mail_handle_keymanager_events(self, req): + """ + Callback for the KeyManager events + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + self._keymanager_event.emit(req) + + def _mail_handle_keymanager_events_slot(self, req): + """ + SLOT + TRIGGER: _mail_handle_keymanager_events + + Reacts to an KeyManager event + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + # We want to ignore this kind of events once everything has + # started + if self._smtp_started and self._imap_started: + return + + self._set_mail_status(self.tr("Starting..."), ready=1) + + ext_status = "" + + if req.event == proto.KEYMANAGER_LOOKING_FOR_KEY: + ext_status = self.tr("Looking for key for this user") + elif req.event == proto.KEYMANAGER_KEY_FOUND: + ext_status = self.tr("Found key! Starting mail...") + # elif req.event == proto.KEYMANAGER_KEY_NOT_FOUND: + # ext_status = self.tr("Key not found!") + elif req.event == proto.KEYMANAGER_STARTED_KEY_GENERATION: + ext_status = self.tr("Generating new key, please wait...") + elif req.event == proto.KEYMANAGER_FINISHED_KEY_GENERATION: + ext_status = self.tr("Finished generating key!") + elif req.event == proto.KEYMANAGER_DONE_UPLOADING_KEYS: + ext_status = self.tr("Starting mail...") + else: + leap_assert(False, + "Don't know how to handle this state: %s" + % (req.event)) + + self._set_mail_status(ext_status, ready=1) + + def _mail_handle_smtp_events(self, req): + """ + Callback for the SMTP events + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + self._smtp_event.emit(req) + + def _mail_handle_smtp_events_slot(self, req): + """ + SLOT + TRIGGER: _mail_handle_smtp_events + + Reacts to an SMTP event + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + ext_status = "" + + if req.event == proto.SMTP_SERVICE_STARTED: + ext_status = self.tr("SMTP has started...") + self._smtp_started = True + if self._smtp_started and self._imap_started: + self._set_mail_status(self.tr("ON"), ready=2) + ext_status = "" + elif req.event == proto.SMTP_SERVICE_FAILED_TO_START: + ext_status = self.tr("SMTP failed to start, check the logs.") + self._set_mail_status(self.tr("Failed")) + else: + leap_assert(False, + "Don't know how to handle this state: %s" + % (req.event)) + + self._set_mail_status(ext_status, ready=2) + + def _mail_handle_imap_events(self, req): + """ + Callback for the IMAP events + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + self._imap_event.emit(req) + + def _mail_handle_imap_events_slot(self, req): + """ + SLOT + TRIGGER: _mail_handle_imap_events + + Reacts to an IMAP event + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + ext_status = None + + if req.event == proto.IMAP_SERVICE_STARTED: + ext_status = self.tr("IMAP has started...") + self._imap_started = True + if self._smtp_started and self._imap_started: + self._set_mail_status(self.tr("ON"), ready=2) + ext_status = "" + elif req.event == proto.IMAP_SERVICE_FAILED_TO_START: + ext_status = self.tr("IMAP failed to start, check the logs.") + self._set_mail_status(self.tr("Failed")) + elif req.event == proto.IMAP_UNREAD_MAIL: + if self._smtp_started and self._imap_started: + self._set_mail_status(self.tr("%s Unread Emails") % + (req.content), ready=2) + else: + leap_assert(False, # XXX ??? + "Don't know how to handle this state: %s" + % (req.event)) + + if ext_status is not None: + self._set_mail_status(ext_status, ready=1) + + def about_to_start(self): + """ + Displays the correct UI for the point where mail components + haven't really started, but they are about to in a second. + """ + self._set_mail_status(self.tr("About to start, please wait..."), + ready=1) + + def stopped_mail(self): + """ + Displayes the correct UI for the stopped state. + """ + self._set_mail_status(self.tr("OFF")) diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 92d6906e..58ed3eb3 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -32,8 +32,10 @@ from leap.bitmask.crypto.srpauth import SRPAuth from leap.bitmask.gui.loggerwindow import LoggerWindow from leap.bitmask.gui.login import LoginWidget from leap.bitmask.gui.preferenceswindow import PreferencesWindow +from leap.bitmask.gui.eip_preferenceswindow import EIPPreferencesWindow from leap.bitmask.gui import statemachines -from leap.bitmask.gui.statuspanel import StatusPanelWidget +from leap.bitmask.gui.eip_status import EIPStatusWidget +from leap.bitmask.gui.mail_status import MailStatusWidget from leap.bitmask.gui.wizard import Wizard from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper @@ -147,7 +149,7 @@ class MainWindow(QtGui.QMainWindow): self._login_widget = LoginWidget( self._settings, - self.ui.stackedWidget.widget(self.LOGIN_INDEX)) + self) self.ui.loginLayout.addWidget(self._login_widget) # Qt Signal Connections ##################################### @@ -155,17 +157,14 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.login.connect(self._login) self._login_widget.cancel_login.connect(self._cancel_login) - self._login_widget.show_wizard.connect( - self._launch_wizard) - - self.ui.btnShowLog.clicked.connect(self._show_logger_window) - self.ui.btnPreferences.clicked.connect(self._show_preferences) + self._login_widget.show_wizard.connect(self._launch_wizard) + self._login_widget.logout.connect(self._logout) - self._status_panel = StatusPanelWidget( - self.ui.stackedWidget.widget(self.EIP_STATUS_INDEX)) - self.ui.statusLayout.addWidget(self._status_panel) + self._eip_status = EIPStatusWidget(self) + self.ui.eipLayout.addWidget(self._eip_status) - self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) + self._mail_status = MailStatusWidget(self) + self.ui.mailLayout.addWidget(self._mail_status) self._eip_connection = EIPConnection() @@ -173,7 +172,7 @@ class MainWindow(QtGui.QMainWindow): self._start_eip) self._eip_connection.qtsigs.disconnecting_signal.connect( self._stop_eip) - self._status_panel.eip_connection_connected.connect( + self._eip_status.eip_connection_connected.connect( self._on_eip_connected) # This is loaded only once, there's a bug when doing that more @@ -221,9 +220,9 @@ class MainWindow(QtGui.QMainWindow): self._finish_eip_bootstrap) self._vpn = VPN(openvpn_verb=openvpn_verb) self._vpn.qtsigs.state_changed.connect( - self._status_panel.update_vpn_state) + self._eip_status.update_vpn_state) self._vpn.qtsigs.status_changed.connect( - self._status_panel.update_vpn_status) + self._eip_status.update_vpn_status) self._vpn.qtsigs.process_finished.connect( self._eip_finished) @@ -241,12 +240,16 @@ class MainWindow(QtGui.QMainWindow): self._smtp_bootstrapper.download_config.connect( self._smtp_bootstrapped_stage) - self.ui.action_log_out.setEnabled(False) - self.ui.action_log_out.triggered.connect(self._logout) self.ui.action_about_leap.triggered.connect(self._about) self.ui.action_quit.triggered.connect(self.quit) self.ui.action_wizard.triggered.connect(self._launch_wizard) self.ui.action_show_logs.triggered.connect(self._show_logger_window) + self.ui.action_create_new_account.triggered.connect( + self._launch_wizard) + + if IS_MAC: + self.ui.menuFile.menuAction().setText(self.tr("Util")) + self.raise_window.connect(self._do_raise_mainwindow) # Used to differentiate between real quits and close to tray @@ -256,17 +259,17 @@ class MainWindow(QtGui.QMainWindow): self._action_mail_status = QtGui.QAction(self.tr("Mail is OFF"), self) self._action_mail_status.setEnabled(False) - self._status_panel.set_action_mail_status(self._action_mail_status) + self._mail_status.set_action_mail_status(self._action_mail_status) self._action_eip_startstop = QtGui.QAction("", self) - self._status_panel.set_action_eip_startstop(self._action_eip_startstop) - - self._action_preferences = QtGui.QAction(self.tr("Preferences"), self) - self._action_preferences.triggered.connect(self._show_preferences) + self._eip_status.set_action_eip_startstop(self._action_eip_startstop) self._action_visible = QtGui.QAction(self.tr("Hide Main Window"), self) self._action_visible.triggered.connect(self._toggle_visible) + self.ui.btnPreferences.clicked.connect(self._show_preferences) + self.ui.btnEIPPreferences.clicked.connect(self._show_eip_preferences) + self._enabled_services = [] self._center_window() @@ -282,6 +285,7 @@ class MainWindow(QtGui.QMainWindow): self.mail_client_logged_in.connect(self._fetch_incoming_mail) self.logout.connect(self._stop_imap_service) self.logout.connect(self._stop_smtp_service) + self.logout.connect(self._mail_status.stopped_mail) ################################# end Qt Signals connection ######## @@ -393,7 +397,6 @@ class MainWindow(QtGui.QMainWindow): SLOT TRIGGERS: self.ui.action_show_logs.triggered - self.ui.btnShowLog.clicked Displays the window with the history of messages logged until now and displays the new ones on arrival. @@ -407,18 +410,13 @@ class MainWindow(QtGui.QMainWindow): self._logger_window = LoggerWindow(handler=leap_log_handler) self._logger_window.setVisible( not self._logger_window.isVisible()) - self.ui.btnShowLog.setChecked(self._logger_window.isVisible()) else: self._logger_window.setVisible(not self._logger_window.isVisible()) - self.ui.btnShowLog.setChecked(self._logger_window.isVisible()) - - self._logger_window.finished.connect(self._uncheck_logger_button) def _show_preferences(self): """ SLOT TRIGGERS: - self.ui.action_show_preferences.triggered self.ui.btnPreferences.clicked Displays the preferences window. @@ -433,22 +431,25 @@ class MainWindow(QtGui.QMainWindow): preferences_window.show() - def _set_soledad_ready(self): + def _show_eip_preferences(self): """ SLOT TRIGGERS: - self.soledad_ready + self.ui.btnEIPPreferences.clicked - It sets the soledad object as ready to use. + Displays the EIP preferences window. """ - self._soledad_ready = True + EIPPreferencesWindow(self).show() - def _uncheck_logger_button(self): + def _set_soledad_ready(self): """ SLOT - Sets the checked state of the loggerwindow button to false. + TRIGGERS: + self.soledad_ready + + It sets the soledad object as ready to use. """ - self.ui.btnShowLog.setChecked(False) + self._soledad_ready = True def _new_updates_available(self, req): """ @@ -623,24 +624,20 @@ class MainWindow(QtGui.QMainWindow): systrayMenu.addAction(self._action_visible) systrayMenu.addSeparator() - eip_menu = systrayMenu.addMenu(self.tr("Encrypted Internet is OFF")) + eip_menu = systrayMenu.addMenu(self.tr("Encrypted Internet: OFF")) eip_menu.addAction(self._action_eip_startstop) - self._status_panel.set_eip_status_menu(eip_menu) + self._eip_status.set_eip_status_menu(eip_menu) systrayMenu.addAction(self._action_mail_status) systrayMenu.addSeparator() - systrayMenu.addAction(self._action_preferences) - systrayMenu.addAction(help_action) - systrayMenu.addSeparator() - systrayMenu.addAction(self.ui.action_log_out) systrayMenu.addAction(self.ui.action_quit) self._systray = QtGui.QSystemTrayIcon(self) self._systray.setContextMenu(systrayMenu) - self._systray.setIcon(self._status_panel.ERROR_ICON_TRAY) + self._systray.setIcon(self._eip_status.ERROR_ICON_TRAY) self._systray.setVisible(True) self._systray.activated.connect(self._tray_activated) - self._status_panel.set_systray(self._systray) + self._eip_status.set_systray(self._systray) def _tray_activated(self, reason=None): """ @@ -846,47 +843,8 @@ class MainWindow(QtGui.QMainWindow): """ leap_assert(self._provider_config, "We need a provider config") - username = self._login_widget.get_user() - password = self._login_widget.get_password() - provider = self._login_widget.get_selected_provider() - - self._enabled_services = self._settings.get_enabled_services( - self._login_widget.get_selected_provider()) - - if len(provider) == 0: - self._login_widget.set_status( - self.tr("Please select a valid provider")) - return - - if len(username) == 0: - self._login_widget.set_status( - self.tr("Please provide a valid username")) - return - - if len(password) == 0: - self._login_widget.set_status( - self.tr("Please provide a valid Password")) - return - - self._login_widget.set_status(self.tr("Logging in..."), error=False) - self._login_widget.set_enabled(False) - - if self._login_widget.get_remember() and has_keyring(): - # in the keyring and in the settings - # we store the value 'usename@provider' - username_domain = (username + '@' + provider).encode("utf8") - try: - keyring.set_password(self.KEYRING_KEY, - username_domain, - password.encode("utf8")) - # Only save the username if it was saved correctly in - # the keyring - self._settings.set_user(username_domain) - except Exception as e: - logger.error("Problem saving data to keyring. %r" - % (e,)) - - self._download_provider_config() + if self._login_widget.start_login(): + self._download_provider_config() def _cancel_login(self): """ @@ -954,7 +912,6 @@ class MainWindow(QtGui.QMainWindow): if ok: self._logged_user = self._login_widget.get_user() - self.ui.action_log_out.setEnabled(True) # We leave a bit of room for the user to see the # "Succeeded" message and then we switch to the EIP status # panel @@ -969,15 +926,15 @@ class MainWindow(QtGui.QMainWindow): Changes the stackedWidget index to the EIP status one and triggers the eip bootstrapping """ - if not self._already_started_eip: - self._status_panel.set_provider( - "%s@%s" % (self._login_widget.get_user(), - self._get_best_provider_config().get_domain())) - self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX) + self._login_widget.logged_in() # TODO separate UI from logic. # TODO soledad should check if we want to run only over EIP. + if self._provider_config.provides_mx() and \ + self._enabled_services.count(self.MX_SERVICE) > 0: + self._mail_status.about_to_start() + self._soledad_bootstrapper.run_soledad_setup_checks( self._provider_config, self._login_widget.get_user(), @@ -1199,9 +1156,9 @@ class MainWindow(QtGui.QMainWindow): """ Initializes and starts the EIP state machine """ - button = self._status_panel.eip_button + button = self._eip_status.eip_button action = self._action_eip_startstop - label = self._status_panel.eip_label + label = self._eip_status.eip_label builder = statemachines.ConnectionMachineBuilder(self._eip_connection) eip_machine = builder.make_machine(button=button, action=action, @@ -1214,7 +1171,7 @@ class MainWindow(QtGui.QMainWindow): """ SLOT TRIGGERS: - self._status_panel.eip_connection_connected + self._eip_status.eip_connection_connected Emits the EIPConnection.qtsigs.connected_signal This is a little workaround for connecting the vpn-connected @@ -1229,7 +1186,7 @@ class MainWindow(QtGui.QMainWindow): """ SLOT TRIGGERS: - self._status_panel.start_eip + self._eip_status.start_eip self._action_eip_startstop.triggered or called from _finish_eip_bootstrap @@ -1237,7 +1194,7 @@ class MainWindow(QtGui.QMainWindow): """ provider_config = self._get_best_provider_config() provider = provider_config.get_domain() - self._status_panel.eip_pre_up() + self._eip_status.eip_pre_up() self.user_stopped_eip = False try: @@ -1248,16 +1205,14 @@ class MainWindow(QtGui.QMainWindow): socket_host=host, socket_port=port) self._settings.set_defaultprovider(provider) - if self._logged_user is not None: - provider = "%s@%s" % (self._logged_user, provider) # XXX move to the state machine too - self._status_panel.set_provider(provider) + self._eip_status.set_provider(provider) # TODO refactor exceptions so they provide translatable # usef-facing messages. except EIPNoPolkitAuthAgentAvailable: - self._status_panel.set_global_status( + self._eip_status.set_eip_status( # XXX this should change to polkit-kde where # applicable. self.tr("We could not find any " @@ -1270,30 +1225,30 @@ class MainWindow(QtGui.QMainWindow): error=True) self._set_eipstatus_off() except EIPNoTunKextLoaded: - self._status_panel.set_global_status( + self._eip_status.set_eip_status( self.tr("Encrypted Internet cannot be started because " "the tuntap extension is not installed properly " "in your system.")) self._set_eipstatus_off() except EIPNoPkexecAvailable: - self._status_panel.set_global_status( + self._eip_status.set_eip_status( self.tr("We could not find pkexec " "in your system."), error=True) self._set_eipstatus_off() except OpenVPNNotFoundException: - self._status_panel.set_global_status( + self._eip_status.set_eip_status( self.tr("We could not find openvpn binary."), error=True) self._set_eipstatus_off() except OpenVPNAlreadyRunning as e: - self._status_panel.set_global_status( + self._eip_status.set_eip_status( self.tr("Another openvpn instance is already running, and " "could not be stopped."), error=True) self._set_eipstatus_off() except AlienOpenVPNAlreadyRunning as e: - self._status_panel.set_global_status( + self._eip_status.set_eip_status( self.tr("Another openvpn instance is already running, and " "could not be stopped because it was not launched by " "Bitmask. Please stop it and try again."), @@ -1302,7 +1257,7 @@ class MainWindow(QtGui.QMainWindow): except VPNLauncherException as e: # XXX We should implement again translatable exceptions so # we can pass a translatable string to the panel (usermessage attr) - self._status_panel.set_global_status("%s" % (e,), error=True) + self._eip_status.set_eip_status("%s" % (e,), error=True) self._set_eipstatus_off() else: self._already_started_eip = True @@ -1312,7 +1267,7 @@ class MainWindow(QtGui.QMainWindow): """ SLOT TRIGGERS: - self._status_panel.stop_eip + self._eip_status.stop_eip self._action_eip_startstop.triggered or called from _eip_finished @@ -1328,23 +1283,25 @@ class MainWindow(QtGui.QMainWindow): self.user_stopped_eip = True self._vpn.terminate() - self._set_eipstatus_off() + self._set_eipstatus_off(False) self._already_started_eip = False # XXX do via signal self._settings.set_defaultprovider(None) if self._logged_user: - self._status_panel.set_provider( + self._eip_status.set_provider( "%s@%s" % (self._logged_user, self._get_best_provider_config().get_domain())) + self._eip_status.eip_stopped() - def _set_eipstatus_off(self): + def _set_eipstatus_off(self, error=True): """ Sets eip status to off """ - self._status_panel.set_eip_status(self.tr("OFF"), error=True) - self._status_panel.set_eip_status_icon("error") + self._eip_status.set_eip_status(self.tr("EIP has stopped"), + error=error) + self._eip_status.set_eip_status_icon("error") def _download_eip_config(self): """ @@ -1359,7 +1316,7 @@ class MainWindow(QtGui.QMainWindow): not self._already_started_eip: # XXX this should be handled by the state machine. - self._status_panel.set_eip_status( + self._eip_status.set_eip_status( self.tr("Starting...")) self._eip_bootstrapper.run_eip_setup_checks( provider_config, @@ -1367,11 +1324,11 @@ class MainWindow(QtGui.QMainWindow): self._already_started_eip = True elif not self._already_started_eip: if self._enabled_services.count(self.OPENVPN_SERVICE) > 0: - self._status_panel.set_eip_status( + self._eip_status.set_eip_status( self.tr("Not supported"), error=True) else: - self._status_panel.set_eip_status(self.tr("Disabled")) + self._eip_status.set_eip_status(self.tr("Disabled")) def _finish_eip_bootstrap(self, data): """ @@ -1386,7 +1343,7 @@ class MainWindow(QtGui.QMainWindow): if not passed: error_msg = self.tr("There was a problem with the provider") - self._status_panel.set_eip_status(error_msg, error=True) + self._eip_status.set_eip_status(error_msg, error=True) logger.error(data[self._eip_bootstrapper.ERROR_KEY]) self._already_started_eip = False return @@ -1406,7 +1363,7 @@ class MainWindow(QtGui.QMainWindow): # DO START EIP Connection! self._eip_connection.qtsigs.do_connect_signal.emit() else: - self._status_panel.set_eip_status( + self._eip_status.set_eip_status( self.tr("Could not load Encrypted Internet " "Configuration."), error=True) @@ -1441,7 +1398,7 @@ class MainWindow(QtGui.QMainWindow): def _logout(self): """ SLOT - TRIGGER: self.ui.action_log_out.triggered + TRIGGER: self._login_widget.logout Starts the logout sequence """ @@ -1461,16 +1418,17 @@ class MainWindow(QtGui.QMainWindow): Switches the stackedWidget back to the login stage after logging out """ + self._login_widget.done_logout() + if ok: self._logged_user = None - self.ui.action_log_out.setEnabled(False) - self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) - self._login_widget.set_password("") - self._login_widget.set_enabled(True) - self._login_widget.set_status("") + + self._login_widget.logged_out() + else: - status_text = self.tr("Something went wrong with the logout.") - self._status_panel.set_global_status(status_text, error=True) + self._login_widget.set_login_status( + self.tr("Something went wrong with the logout."), + error=True) def _intermediate_stage(self, data): """ @@ -1541,7 +1499,7 @@ class MainWindow(QtGui.QMainWindow): # XXX check if these exitCodes are pkexec/cocoasudo specific if exitCode in (126, 127): - self._status_panel.set_global_status( + self._eip_status.set_eip_status( self.tr("Encrypted Internet could not be launched " "because you did not authenticate properly."), error=True) @@ -1549,7 +1507,7 @@ class MainWindow(QtGui.QMainWindow): signal = qtsigs.connection_aborted_signal elif exitCode != 0 or not self.user_stopped_eip: - self._status_panel.set_global_status( + self._eip_status.set_eip_status( self.tr("Encrypted Internet finished in an " "unexpected manner!"), error=True) signal = qtsigs.connection_died_signal diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index 2d17f6c2..7e281b44 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -60,7 +60,6 @@ class PreferencesWindow(QtGui.QDialog): self.ui.setupUi(self) self.ui.lblPasswordChangeStatus.setVisible(False) self.ui.lblProvidersServicesStatus.setVisible(False) - self.ui.lblProvidersGatewayStatus.setVisible(False) self._selected_services = set() @@ -68,11 +67,6 @@ class PreferencesWindow(QtGui.QDialog): self.ui.pbChangePassword.clicked.connect(self._change_password) self.ui.cbProvidersServices.currentIndexChanged[unicode].connect( self._populate_services) - self.ui.cbProvidersGateway.currentIndexChanged[unicode].connect( - self._populate_gateways) - - self.ui.cbGateways.currentIndexChanged[unicode].connect( - lambda x: self.ui.lblProvidersGatewayStatus.setVisible(False)) if not self._settings.get_configured_providers(): self.ui.gbEnabledServices.setEnabled(False) @@ -217,37 +211,13 @@ class PreferencesWindow(QtGui.QDialog): self.ui.lblProvidersServicesStatus.setVisible(True) self.ui.lblProvidersServicesStatus.setText(status) - def _set_providers_gateway_status(self, status, success=False, - error=False): - """ - Sets the status label for the gateway change. - - :param status: status message to display, can be HTML - :type status: str - :param success: is set to True if we should display the - message as green - :type success: bool - :param error: is set to True if we should display the - message as red - :type error: bool - """ - if success: - status = "%s" % (status,) - elif error: - status = "%s" % (status,) - - self.ui.lblProvidersGatewayStatus.setVisible(True) - self.ui.lblProvidersGatewayStatus.setText(status) - def _add_configured_providers(self): """ Add the client's configured providers to the providers combo boxes. """ self.ui.cbProvidersServices.clear() - self.ui.cbProvidersGateway.clear() for provider in self._settings.get_configured_providers(): self.ui.cbProvidersServices.addItem(provider) - self.ui.cbProvidersGateway.addItem(provider) def _service_selection_changed(self, service, state): """ @@ -366,90 +336,3 @@ class PreferencesWindow(QtGui.QDialog): provider_config = None return provider_config - - def _save_selected_gateway(self, provider): - """ - SLOT - TRIGGERS: - self.ui.pbSaveGateway.clicked - - Saves the new gateway setting to the configuration file. - - :param provider: the provider config that we need to save. - :type provider: str - """ - gateway = self.ui.cbGateways.currentText() - - if gateway == self.AUTOMATIC_GATEWAY_LABEL: - gateway = self._settings.GATEWAY_AUTOMATIC - else: - idx = self.ui.cbGateways.currentIndex() - gateway = self.ui.cbGateways.itemData(idx) - - self._settings.set_selected_gateway(provider, gateway) - - msg = self.tr( - "Gateway settings for provider '{0}' saved.".format(provider)) - logger.debug(msg) - self._set_providers_gateway_status(msg, success=True) - - def _populate_gateways(self, domain): - """ - SLOT - TRIGGERS: - self.ui.cbProvidersGateway.currentIndexChanged[unicode] - - Loads the gateways that the provider provides into the UI for - the user to select. - - :param domain: the domain of the provider to load gateways from. - :type domain: str - """ - # We hide the maybe-visible status label after a change - self.ui.lblProvidersGatewayStatus.setVisible(False) - - if not domain: - return - - try: - # disconnect prevoiusly connected save method - self.ui.pbSaveGateway.clicked.disconnect() - except RuntimeError: - pass # Signal was not connected - - # set the proper connection for the 'save' button - save_gateway = partial(self._save_selected_gateway, domain) - self.ui.pbSaveGateway.clicked.connect(save_gateway) - - eip_config = EIPConfig() - provider_config = self._get_provider_config(domain) - - eip_config_path = os.path.join("leap", "providers", - domain, "eip-service.json") - api_version = provider_config.get_api_version() - eip_config.set_api_version(api_version) - eip_loaded = eip_config.load(eip_config_path) - - if not eip_loaded or provider_config is None: - self._set_providers_gateway_status( - self.tr("There was a problem with configuration files."), - error=True) - return - - gateways = VPNGatewaySelector(eip_config).get_gateways_list() - logger.debug(gateways) - - self.ui.cbGateways.clear() - self.ui.cbGateways.addItem(self.AUTOMATIC_GATEWAY_LABEL) - - # Add the available gateways and - # select the one stored in configuration file. - selected_gateway = self._settings.get_selected_gateway(domain) - index = 0 - for idx, (gw_name, gw_ip) in enumerate(gateways): - gateway = "{0} ({1})".format(gw_name, gw_ip) - self.ui.cbGateways.addItem(gateway, gw_ip) - if gw_ip == selected_gateway: - index = idx + 1 - - self.ui.cbGateways.setCurrentIndex(index) diff --git a/src/leap/bitmask/gui/statuspanel.py b/src/leap/bitmask/gui/statuspanel.py deleted file mode 100644 index 679f00b1..00000000 --- a/src/leap/bitmask/gui/statuspanel.py +++ /dev/null @@ -1,710 +0,0 @@ -# -*- coding: utf-8 -*- -# statuspanel.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 . -""" -Status Panel widget implementation -""" -import logging - -from datetime import datetime -from functools import partial - -from PySide import QtCore, QtGui - -from leap.bitmask.services.eip.connection import EIPConnection -from leap.bitmask.services.eip.vpnprocess import VPNManager -from leap.bitmask.platform_init import IS_WIN, IS_LINUX -from leap.bitmask.util.averages import RateMovingAverage -from leap.common.check import leap_assert, leap_assert_type -from leap.common.events import register -from leap.common.events import events_pb2 as proto - -from ui_statuspanel import Ui_StatusPanel - -logger = logging.getLogger(__name__) - - -class StatusPanelWidget(QtGui.QWidget): - """ - Status widget that displays the current state of the LEAP services - """ - DISPLAY_TRAFFIC_RATES = True - RATE_STR = "%14.2f KB/s" - TOTAL_STR = "%14.2f Kb" - - MAIL_OFF_ICON = ":/images/mail-unlocked.png" - MAIL_ON_ICON = ":/images/mail-locked.png" - - eip_connection_connected = QtCore.Signal() - _soledad_event = QtCore.Signal(object) - _smtp_event = QtCore.Signal(object) - _imap_event = QtCore.Signal(object) - _keymanager_event = QtCore.Signal(object) - - def __init__(self, parent=None): - QtGui.QWidget.__init__(self, parent) - - self._systray = None - self._eip_status_menu = None - - self.ui = Ui_StatusPanel() - self.ui.setupUi(self) - - self.eipconnection = EIPConnection() - - self.hide_status_box() - - # set systray tooltip statuses - self._eip_status = self._mx_status = "" - - # Set the EIP status icons - self.CONNECTING_ICON = None - self.CONNECTED_ICON = None - self.ERROR_ICON = None - self.CONNECTING_ICON_TRAY = None - self.CONNECTED_ICON_TRAY = None - self.ERROR_ICON_TRAY = None - self._set_eip_icons() - - self._set_traffic_rates() - self._make_status_clickable() - - register(signal=proto.KEYMANAGER_LOOKING_FOR_KEY, - callback=self._mail_handle_keymanager_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.KEYMANAGER_KEY_FOUND, - callback=self._mail_handle_keymanager_events, - reqcbk=lambda req, resp: None) - - # register(signal=proto.KEYMANAGER_KEY_NOT_FOUND, - # callback=self._mail_handle_keymanager_events, - # reqcbk=lambda req, resp: None) - - register(signal=proto.KEYMANAGER_STARTED_KEY_GENERATION, - callback=self._mail_handle_keymanager_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.KEYMANAGER_FINISHED_KEY_GENERATION, - callback=self._mail_handle_keymanager_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.KEYMANAGER_DONE_UPLOADING_KEYS, - callback=self._mail_handle_keymanager_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.SOLEDAD_DONE_DOWNLOADING_KEYS, - callback=self._mail_handle_soledad_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.SOLEDAD_DONE_UPLOADING_KEYS, - callback=self._mail_handle_soledad_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.SMTP_SERVICE_STARTED, - callback=self._mail_handle_smtp_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.SMTP_SERVICE_FAILED_TO_START, - callback=self._mail_handle_smtp_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.IMAP_SERVICE_STARTED, - callback=self._mail_handle_imap_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.IMAP_SERVICE_FAILED_TO_START, - callback=self._mail_handle_imap_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.IMAP_UNREAD_MAIL, - callback=self._mail_handle_imap_events, - reqcbk=lambda req, resp: None) - - self._set_long_mail_status("") - self.ui.lblUnread.setVisible(False) - - self._smtp_started = False - self._imap_started = False - - self._soledad_event.connect( - self._mail_handle_soledad_events_slot) - self._imap_event.connect( - self._mail_handle_imap_events_slot) - self._smtp_event.connect( - self._mail_handle_smtp_events_slot) - self._keymanager_event.connect( - self._mail_handle_keymanager_events_slot) - - def _make_status_clickable(self): - """ - Makes upload and download figures clickable. - """ - onclicked = self._on_VPN_status_clicked - self.ui.btnUpload.clicked.connect(onclicked) - self.ui.btnDownload.clicked.connect(onclicked) - - def _on_VPN_status_clicked(self): - """ - SLOT - TRIGGER: self.ui.btnUpload.clicked - self.ui.btnDownload.clicked - - Toggles between rate and total throughput display for vpn - status figures. - """ - self.DISPLAY_TRAFFIC_RATES = not self.DISPLAY_TRAFFIC_RATES - self.update_vpn_status(None) # refresh - - def _set_traffic_rates(self): - """ - Initializes up and download rates. - """ - self._up_rate = RateMovingAverage() - self._down_rate = RateMovingAverage() - - self.ui.btnUpload.setText(self.RATE_STR % (0,)) - self.ui.btnDownload.setText(self.RATE_STR % (0,)) - - def _reset_traffic_rates(self): - """ - Resets up and download rates, and cleans up the labels. - """ - self._up_rate.reset() - self._down_rate.reset() - self.update_vpn_status(None) - - def _update_traffic_rates(self, up, down): - """ - Updates up and download rates. - - :param up: upload total. - :type up: int - :param down: download total. - :type down: int - """ - ts = datetime.now() - self._up_rate.append((ts, up)) - self._down_rate.append((ts, down)) - - def _get_traffic_rates(self): - """ - Gets the traffic rates (in KB/s). - - :returns: a tuple with the (up, down) rates - :rtype: tuple - """ - up = self._up_rate - down = self._down_rate - - return (up.get_average(), down.get_average()) - - def _get_traffic_totals(self): - """ - Gets the traffic total throughput (in Kb). - - :returns: a tuple with the (up, down) totals - :rtype: tuple - """ - up = self._up_rate - down = self._down_rate - - return (up.get_total(), down.get_total()) - - def _set_eip_icons(self): - """ - Sets the EIP status icons for the main window and for the tray - - MAC : dark icons - LINUX : dark icons in window, light icons in tray - WIN : light icons - """ - EIP_ICONS = EIP_ICONS_TRAY = ( - ":/images/conn_connecting-light.png", - ":/images/conn_connected-light.png", - ":/images/conn_error-light.png") - - if IS_LINUX: - EIP_ICONS_TRAY = ( - ":/images/conn_connecting.png", - ":/images/conn_connected.png", - ":/images/conn_error.png") - elif IS_WIN: - EIP_ICONS = EIP_ICONS_TRAY = ( - ":/images/conn_connecting.png", - ":/images/conn_connected.png", - ":/images/conn_error.png") - - self.CONNECTING_ICON = QtGui.QPixmap(EIP_ICONS[0]) - self.CONNECTED_ICON = QtGui.QPixmap(EIP_ICONS[1]) - self.ERROR_ICON = QtGui.QPixmap(EIP_ICONS[2]) - - self.CONNECTING_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[0]) - self.CONNECTED_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[1]) - self.ERROR_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[2]) - - # Systray and actions - - def set_systray(self, systray): - """ - Sets the systray object to use. - - :param systray: Systray object - :type systray: QtGui.QSystemTrayIcon - """ - leap_assert_type(systray, QtGui.QSystemTrayIcon) - self._systray = systray - self._systray.setToolTip(self.tr("All services are OFF")) - - def _update_systray_tooltip(self): - """ - Updates the system tray icon tooltip using the eip and mx statuses. - """ - status = self.tr("Encrypted Internet is {0}").format(self._eip_status) - status += '\n' - status += self.tr("Mail is {0}").format(self._mx_status) - self._systray.setToolTip(status) - - def set_action_eip_startstop(self, action_eip_startstop): - """ - Sets the action_eip_startstop to use. - - :param action_eip_startstop: action_eip_status to be used - :type action_eip_startstop: QtGui.QAction - """ - self._action_eip_startstop = action_eip_startstop - - def set_eip_status_menu(self, eip_status_menu): - """ - Sets the eip_status_menu to use. - - :param eip_status_menu: eip_status_menu to be used - :type eip_status_menu: QtGui.QMenu - """ - leap_assert_type(eip_status_menu, QtGui.QMenu) - self._eip_status_menu = eip_status_menu - - def set_action_mail_status(self, action_mail_status): - """ - Sets the action_mail_status to use. - - :param action_mail_status: action_mail_status to be used - :type action_mail_status: QtGui.QAction - """ - leap_assert_type(action_mail_status, QtGui.QAction) - self._action_mail_status = action_mail_status - - def set_global_status(self, status, error=False): - """ - Sets the global status label. - - :param status: status message - :type status: str or unicode - :param error: if the status is an erroneous one, then set this - to True - :type error: bool - """ - leap_assert_type(error, bool) - if error: - status = "%s" % (status,) - self.ui.lblGlobalStatus.setText(status) - self.ui.globalStatusBox.show() - - def hide_status_box(self): - """ - Hide global status box. - """ - self.ui.globalStatusBox.hide() - - # EIP status --- - - @property - def eip_button(self): - return self.ui.btnEipStartStop - - @property - def eip_label(self): - return self.ui.lblEIPStatus - - def eip_pre_up(self): - """ - Triggered when the app activates eip. - Hides the status box and disables the start/stop button. - """ - self.hide_status_box() - self.set_startstop_enabled(False) - - # XXX disable (later) -------------------------- - def set_eip_status(self, status, error=False): - """ - Sets the status label at the VPN stage to status - - :param status: status message - :type status: str or unicode - :param error: if the status is an erroneous one, then set this - to True - :type error: bool - """ - leap_assert_type(error, bool) - - self._eip_status = status - - if error: - status = "%s" % (status,) - self.ui.lblEIPStatus.setText(status) - self._update_systray_tooltip() - - # XXX disable --------------------------------- - def set_startstop_enabled(self, value): - """ - Enable or disable btnEipStartStop and _action_eip_startstop - based on value - - :param value: True for enabled, False otherwise - :type value: bool - """ - leap_assert_type(value, bool) - self.ui.btnEipStartStop.setEnabled(value) - self._action_eip_startstop.setEnabled(value) - - # XXX disable ----------------------------- - def eip_started(self): - """ - Sets the state of the widget to how it should look after EIP - has started - """ - self.ui.btnEipStartStop.setText(self.tr("Turn OFF")) - self.ui.btnEipStartStop.disconnect(self) - self.ui.btnEipStartStop.clicked.connect( - self.eipconnection.qtsigs.do_connect_signal) - - # XXX disable ----------------------------- - def eip_stopped(self): - """ - Sets the state of the widget to how it should look after EIP - has stopped - """ - # XXX should connect this to EIPConnection.disconnected_signal - self._reset_traffic_rates() - # XXX disable ----------------------------- - self.ui.btnEipStartStop.setText(self.tr("Turn ON")) - self.ui.btnEipStartStop.disconnect(self) - self.ui.btnEipStartStop.clicked.connect( - self.eipconnection.qtsigs.do_disconnect_signal) - - def update_vpn_status(self, data): - """ - SLOT - TRIGGER: VPN.status_changed - - Updates the download/upload labels based on the data provided - by the VPN thread. - - :param data: a dictionary with the tcp/udp write and read totals. - If data is None, we just will refresh the display based - on the previous data. - :type data: dict - """ - if data: - upload = float(data[VPNManager.TCPUDP_WRITE_KEY] or "0") - download = float(data[VPNManager.TCPUDP_READ_KEY] or "0") - self._update_traffic_rates(upload, download) - - if self.DISPLAY_TRAFFIC_RATES: - uprate, downrate = self._get_traffic_rates() - upload_str = self.RATE_STR % (uprate,) - download_str = self.RATE_STR % (downrate,) - - else: # display total throughput - uptotal, downtotal = self._get_traffic_totals() - upload_str = self.TOTAL_STR % (uptotal,) - download_str = self.TOTAL_STR % (downtotal,) - - self.ui.btnUpload.setText(upload_str) - self.ui.btnDownload.setText(download_str) - - def update_vpn_state(self, data): - """ - SLOT - TRIGGER: VPN.state_changed - - Updates the displayed VPN state based on the data provided by - the VPN thread. - - Emits: - If the status is connected, we emit EIPConnection.qtsigs. - connected_signal - """ - status = data[VPNManager.STATUS_STEP_KEY] - self.set_eip_status_icon(status) - if status == "CONNECTED": - # XXX should be handled by the state machine too. - self.set_eip_status(self.tr("ON")) - logger.debug("STATUS IS CONNECTED --- emitting signal") - self.eip_connection_connected.emit() - - # XXX should lookup status map in EIPConnection - elif status == "AUTH": - self.set_eip_status(self.tr("Authenticating...")) - elif status == "GET_CONFIG": - self.set_eip_status(self.tr("Retrieving configuration...")) - elif status == "WAIT": - self.set_eip_status(self.tr("Waiting to start...")) - elif status == "ASSIGN_IP": - self.set_eip_status(self.tr("Assigning IP")) - elif status == "RECONNECTING": - self.set_eip_status(self.tr("Reconnecting...")) - elif status == "ALREADYRUNNING": - # Put the following calls in Qt's event queue, otherwise - # the UI won't update properly - QtCore.QTimer.singleShot( - 0, self.eipconnection.qtsigs.do_disconnect_signal) - QtCore.QTimer.singleShot(0, partial(self.set_global_status, - self.tr("Unable to start VPN, " - "it's already " - "running."))) - else: - self.set_eip_status(status) - - def set_eip_icon(self, icon): - """ - Sets the icon to display for EIP - - :param icon: icon to display - :type icon: QPixmap - """ - self.ui.lblVPNStatusIcon.setPixmap(icon) - - def set_eip_status_icon(self, status): - """ - Given a status step from the VPN thread, set the icon properly - - :param status: status step - :type status: str - """ - selected_pixmap = self.ERROR_ICON - selected_pixmap_tray = self.ERROR_ICON_TRAY - tray_message = self.tr("Encrypted Internet is OFF") - if status in ("WAIT", "AUTH", "GET_CONFIG", - "RECONNECTING", "ASSIGN_IP"): - selected_pixmap = self.CONNECTING_ICON - selected_pixmap_tray = self.CONNECTING_ICON_TRAY - tray_message = self.tr("Encrypted Internet is STARTING") - elif status in ("CONNECTED"): - tray_message = self.tr("Encrypted Internet is ON") - selected_pixmap = self.CONNECTED_ICON - selected_pixmap_tray = self.CONNECTED_ICON_TRAY - - self.set_eip_icon(selected_pixmap) - self._systray.setIcon(QtGui.QIcon(selected_pixmap_tray)) - self._eip_status_menu.setTitle(tray_message) - - def set_provider(self, provider): - self.ui.lblProvider.setText(provider) - - # - # mail methods - # - - def _set_mail_status(self, status, ready=False): - """ - Sets the Mail status in the label and in the tray icon. - - :param status: the status text to display - :type status: unicode - :param ready: if mx is ready or not. - :type ready: bool - """ - self.ui.lblMailStatus.setText(status) - - self._mx_status = self.tr('OFF') - tray_status = self.tr('Mail is OFF') - - icon = QtGui.QPixmap(self.MAIL_OFF_ICON) - if ready: - icon = QtGui.QPixmap(self.MAIL_ON_ICON) - self._mx_status = self.tr('ON') - tray_status = self.tr('Mail is ON') - - self.ui.lblMailIcon.setPixmap(icon) - self._action_mail_status.setText(tray_status) - self._update_systray_tooltip() - - def _mail_handle_soledad_events(self, req): - """ - Callback for ... - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - self._soledad_event.emit(req) - - def _mail_handle_soledad_events_slot(self, req): - """ - SLOT - TRIGGER: _mail_handle_soledad_events - - Reacts to an Soledad event - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - self._set_mail_status(self.tr("Starting...")) - - ext_status = "" - - if req.event == proto.SOLEDAD_DONE_UPLOADING_KEYS: - ext_status = self.tr("Soledad has started...") - elif req.event == proto.SOLEDAD_DONE_DOWNLOADING_KEYS: - ext_status = self.tr("Soledad is starting, please wait...") - else: - leap_assert(False, - "Don't know how to handle this state: %s" - % (req.event)) - - self._set_long_mail_status(ext_status) - - def _mail_handle_keymanager_events(self, req): - """ - Callback for the KeyManager events - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - self._keymanager_event.emit(req) - - def _mail_handle_keymanager_events_slot(self, req): - """ - SLOT - TRIGGER: _mail_handle_keymanager_events - - Reacts to an KeyManager event - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - # We want to ignore this kind of events once everything has - # started - if self._smtp_started and self._imap_started: - return - - self._set_mail_status(self.tr("Starting...")) - - ext_status = "" - - if req.event == proto.KEYMANAGER_LOOKING_FOR_KEY: - ext_status = self.tr("Looking for key for this user") - elif req.event == proto.KEYMANAGER_KEY_FOUND: - ext_status = self.tr("Found key! Starting mail...") - # elif req.event == proto.KEYMANAGER_KEY_NOT_FOUND: - # ext_status = self.tr("Key not found!") - elif req.event == proto.KEYMANAGER_STARTED_KEY_GENERATION: - ext_status = self.tr("Generating new key, please wait...") - elif req.event == proto.KEYMANAGER_FINISHED_KEY_GENERATION: - ext_status = self.tr("Finished generating key!") - elif req.event == proto.KEYMANAGER_DONE_UPLOADING_KEYS: - ext_status = self.tr("Starting mail...") - else: - leap_assert(False, - "Don't know how to handle this state: %s" - % (req.event)) - - self._set_long_mail_status(ext_status) - - def _mail_handle_smtp_events(self, req): - """ - Callback for the SMTP events - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - self._smtp_event.emit(req) - - def _mail_handle_smtp_events_slot(self, req): - """ - SLOT - TRIGGER: _mail_handle_smtp_events - - Reacts to an SMTP event - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - ext_status = "" - - if req.event == proto.SMTP_SERVICE_STARTED: - ext_status = self.tr("SMTP has started...") - self._smtp_started = True - if self._smtp_started and self._imap_started: - self._set_mail_status(self.tr("ON"), ready=True) - ext_status = "" - elif req.event == proto.SMTP_SERVICE_FAILED_TO_START: - ext_status = self.tr("SMTP failed to start, check the logs.") - self._set_mail_status(self.tr("Failed")) - else: - leap_assert(False, - "Don't know how to handle this state: %s" - % (req.event)) - - self._set_long_mail_status(ext_status) - - def _mail_handle_imap_events(self, req): - """ - Callback for the IMAP events - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - self._imap_event.emit(req) - - def _mail_handle_imap_events_slot(self, req): - """ - SLOT - TRIGGER: _mail_handle_imap_events - - Reacts to an IMAP event - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - ext_status = None - - if req.event == proto.IMAP_SERVICE_STARTED: - ext_status = self.tr("IMAP has started...") - self._imap_started = True - if self._smtp_started and self._imap_started: - self._set_mail_status(self.tr("ON"), ready=True) - ext_status = "" - elif req.event == proto.IMAP_SERVICE_FAILED_TO_START: - ext_status = self.tr("IMAP failed to start, check the logs.") - self._set_mail_status(self.tr("Failed")) - elif req.event == proto.IMAP_UNREAD_MAIL: - if self._smtp_started and self._imap_started: - self.ui.lblUnread.setText( - self.tr("%s Unread Emails") % (req.content)) - self.ui.lblUnread.setVisible(req.content != "0") - self._set_mail_status(self.tr("ON"), ready=True) - else: - leap_assert(False, # XXX ??? - "Don't know how to handle this state: %s" - % (req.event)) - - if ext_status is not None: - self._set_long_mail_status(ext_status) - - def _set_long_mail_status(self, ext_status): - self.ui.lblLongMailStatus.setText(ext_status) - self.ui.grpMailStatus.setVisible(len(ext_status) > 0) diff --git a/src/leap/bitmask/gui/ui/eip_status.ui b/src/leap/bitmask/gui/ui/eip_status.ui new file mode 100644 index 00000000..27df3f31 --- /dev/null +++ b/src/leap/bitmask/gui/ui/eip_status.ui @@ -0,0 +1,287 @@ + + + EIPStatus + + + + 0 + 0 + 470 + 157 + + + + + 0 + 0 + + + + Form + + + + 24 + + + + + + + Turn On + + + + + + + + 32 + 32 + + + + + + + :/images/black/32/earth.png + + + + + + + + 16777215 + 32 + + + + color: rgb(80, 80, 80); + + + ... + + + true + + + + + + + + 0 + 0 + + + + + 14 + 75 + true + + + + Traffic is being routed in the clear + + + true + + + + + + + + 16 + 16 + + + + + + + :/images/black/32/off.png + + + true + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + + 16777215 + 32 + + + + + 0 + + + 0 + + + + + 4 + + + QLayout::SetDefaultConstraint + + + + + + + + :/images/light/16/down-arrow.png + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 120 + 16777215 + + + + + 11 + 75 + true + + + + PointingHandCursor + + + 0.0 KB/s + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + + :/images/light/16/up-arrow.png + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 120 + 16777215 + + + + + 11 + 75 + true + + + + PointingHandCursor + + + 0.0 KB/s + + + true + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + + + + + + + + + diff --git a/src/leap/bitmask/gui/ui/eippreferences.ui b/src/leap/bitmask/gui/ui/eippreferences.ui new file mode 100644 index 00000000..e9bc203d --- /dev/null +++ b/src/leap/bitmask/gui/ui/eippreferences.ui @@ -0,0 +1,94 @@ + + + EIPPreferences + + + + 0 + 0 + 400 + 170 + + + + EIP Preferences + + + + :/images/mask-icon.png:/images/mask-icon.png + + + + + + true + + + Select gateway for provider + + + false + + + + + + + <Select provider> + + + + + + + + &Select provider: + + + cbProvidersGateway + + + + + + + Select gateway: + + + + + + + + Automatic + + + + + + + + Save this provider settings + + + + + + + < Providers Gateway Status > + + + Qt::AlignCenter + + + + + + + + + + + + + diff --git a/src/leap/bitmask/gui/ui/login.ui b/src/leap/bitmask/gui/ui/login.ui index 42a6897a..a1842608 100644 --- a/src/leap/bitmask/gui/ui/login.ui +++ b/src/leap/bitmask/gui/ui/login.ui @@ -6,127 +6,273 @@ 0 0 - 356 - 223 + 468 + 350 + + + 0 + 0 + + + + + 0 + 0 + + Form + + + - - - - Qt::Horizontal + + 0 + + + + + + 0 + 0 + - + - 40 - 20 + 0 + 0 - - - - + + + 24 + + + + + + 15 + 75 + true + + + + ... + + + + + + + Logout + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + color: rgb(132, 132, 132); +font: 75 12pt "Lucida Grande"; + + + + + + + + - - - - Qt::Horizontal + + + + + 0 + 0 + - + - 40 - 20 + 16777215 + 800 - - - - - Create a new account + - - - - - - <b>Provider:</b> + + :/images/black/32/user.png - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + false - - - - - - + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - - - - - Remember username and password + + 0 - - - - <b>Username:</b> + + + + + 0 + 0 + - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 0 + 0 + + + + 24 + + + + + <b>Provider:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Log In + + + + + + + + + Remember username and password + + + + + + + + + + Qt::AlignCenter + + + true + + + + + + + + + + + + <b>Username:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + <b>Password:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + - - - - <b>Password:</b> + + + + Qt::Horizontal - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + QSizePolicy::Maximum - - - - - - Log In + + + 12 + 0 + - + - - + + + + + 0 + 50 + + + + background-color: rgb(255, 127, 114); + Qt::AlignCenter - - true - + + + ClickableLabel + QLabel +
clickablelabel.h
+
+
- cmbProviders - lnUser - lnPassword chkRemember - btnLogin - btnCreateAccount - + + + diff --git a/src/leap/bitmask/gui/ui/mail_status.ui b/src/leap/bitmask/gui/ui/mail_status.ui new file mode 100644 index 00000000..1327f9e7 --- /dev/null +++ b/src/leap/bitmask/gui/ui/mail_status.ui @@ -0,0 +1,98 @@ + + + MailStatusWidget + + + + 0 + 0 + 400 + 72 + + + + + 0 + 0 + + + + Form + + + + + + + + color: rgb(80, 80, 80); + + + You must login to use encrypted email. + + + + + + + + 0 + 0 + + + + Email + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 16 + 16 + + + + + + + :/images/black/32/off.png + + + true + + + + + + + + + + + + :/images/black/32/email.png + + + + + + + + + + diff --git a/src/leap/bitmask/gui/ui/mainwindow.ui b/src/leap/bitmask/gui/ui/mainwindow.ui index 17837642..920160b8 100644 --- a/src/leap/bitmask/gui/ui/mainwindow.ui +++ b/src/leap/bitmask/gui/ui/mainwindow.ui @@ -6,10 +6,28 @@ 0 0 - 429 - 579 + 524 + 722 + + + 0 + 0 + + + + + 524 + 0 + + + + + 524 + 16777215 + + Bitmask @@ -28,9 +46,222 @@ - + + 0 + + + 0 + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + Qt::ScrollBarAlwaysOff + + + true + + + + + 0 + 0 + 524 + 635 + + + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 24 + + + 24 + + + + + + 16 + 75 + true + + + + Encrypted Internet + + + + + + + + 48 + 20 + + + + + + + + :/images/black/32/gear.png:/images/black/32/gear.png + + + false + + + false + + + false + + + + + + + + + + 12 + + + 0 + + + 12 + + + 0 + + + + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + + 24 + + + 24 + + + + + + 16 + 75 + true + + + + Login + + + + + + + + 48 + 20 + + + + + + + + :/images/black/32/gear.png:/images/black/32/gear.png + + + + + + + + + + + + + Qt::Horizontal + + + + + + + 12 + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + - + + + + There are new updates available, please restart. + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + Qt::Horizontal @@ -43,7 +274,7 @@ - + Qt::Horizontal @@ -56,17 +287,7 @@ - - - - There are new updates available, please restart. - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - + @@ -82,7 +303,7 @@ - + Qt::Horizontal @@ -97,164 +318,6 @@ - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - 1 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - - - - - - false - - - - - - :/images/mask-launcher.png - - - Qt::AlignCenter - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - true - - - Preferences - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Show Log - - - true - - - false - - - true - - - - - @@ -262,15 +325,15 @@ 0 0 - 429 - 21 + 524 + 22 - + - &Session + &Bitmask - + @@ -279,16 +342,17 @@ Help + - + - + - Log &out + Preferences... @@ -313,7 +377,12 @@ - Show &logs + Show &Log + + + + + Create a new account... diff --git a/src/leap/bitmask/gui/ui/preferences.ui b/src/leap/bitmask/gui/ui/preferences.ui index e66a2d68..e187c016 100644 --- a/src/leap/bitmask/gui/ui/preferences.ui +++ b/src/leap/bitmask/gui/ui/preferences.ui @@ -7,7 +7,7 @@ 0 0 503 - 529 + 401 @@ -18,7 +18,7 @@ :/images/mask-icon.png:/images/mask-icon.png - + Qt::Vertical @@ -31,64 +31,80 @@ - - + + - true + false - Select gateway for provider - - - false + Password Change - - - - - - <Select provider> - - - - + + + QFormLayout::ExpandingFieldsGrow + - + - &Select provider: + &Current password: - cbProvidersGateway + leCurrentPassword + + + + + + + QLineEdit::Password - + - Select gateway: + &New password: + + + leNewPassword - - - - - Automatic - - + + + + QLineEdit::Password + - - + + - Save this provider settings + &Re-enter new password: + + + leNewPassword2 - - + + + + QLineEdit::Password + + + + + - < Providers Gateway Status > + Change + + + + + + + <Password change status> Qt::AlignCenter @@ -98,7 +114,7 @@ - + Enabled services @@ -155,89 +171,6 @@ - - - - false - - - Password Change - - - - QFormLayout::ExpandingFieldsGrow - - - - - &Current password: - - - leCurrentPassword - - - - - - - QLineEdit::Password - - - - - - - &New password: - - - leNewPassword - - - - - - - QLineEdit::Password - - - - - - - &Re-enter new password: - - - leNewPassword2 - - - - - - - QLineEdit::Password - - - - - - - Change - - - - - - - <Password change status> - - - Qt::AlignCenter - - - - - - diff --git a/src/leap/bitmask/gui/ui/statuspanel.ui b/src/leap/bitmask/gui/ui/statuspanel.ui deleted file mode 100644 index d77af1da..00000000 --- a/src/leap/bitmask/gui/ui/statuspanel.ui +++ /dev/null @@ -1,393 +0,0 @@ - - - StatusPanel - - - - 0 - 0 - 470 - 477 - - - - Form - - - - - - font: bold; - - - user@domain.org - - - true - - - - - - - true - - - - - - - - - - - 0 Unread Emails - - - - - - - font: bold; - - - Disabled - - - - - - - - 0 - 0 - - - - Encrypted Mail: - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Vertical - - - - 20 - 1 - - - - - - - - - - 4 - - - QLayout::SetDefaultConstraint - - - - - - - - :/images/light/16/down-arrow.png - - - - - - - - 0 - 0 - - - - - 100 - 0 - - - - - 120 - 16777215 - - - - PointingHandCursor - - - 0.0 KB/s - - - true - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - - - - :/images/light/16/up-arrow.png - - - - - - - - 0 - 0 - - - - - 100 - 0 - - - - - 120 - 16777215 - - - - PointingHandCursor - - - 0.0 KB/s - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Preferred - - - - 0 - 11 - - - - - - - - - 64 - 64 - - - - - - - :/images/light/64/network-eip-down.png - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - - 64 - 64 - - - - - 64 - 999 - - - - - - - :/images/mail-unlocked.png - - - - - - - - - - - - - ... - - - - - - - - - - false - - - - - - false - - - ... - - - true - - - - - - - - - - - - Encrypted Internet: - - - - - - - font: bold; - - - Off - - - Qt::AutoText - - - Qt::AlignCenter - - - false - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Turn On - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - -- cgit v1.2.3 From 98c6cfdce1e05e4e04f5683704df2dcffe24c05d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 1 Oct 2013 13:25:50 -0300 Subject: Hide error message label when starting a new login --- src/leap/bitmask/gui/login.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py index 9a369f6d..324280b9 100644 --- a/src/leap/bitmask/gui/login.py +++ b/src/leap/bitmask/gui/login.py @@ -293,6 +293,7 @@ class LoginWidget(QtGui.QWidget): self.set_status(self.tr("Logging in..."), error=False) self.set_enabled(False) + self.ui.clblErrorMsg.hide() if self.get_remember() and has_keyring(): # in the keyring and in the settings -- cgit v1.2.3 From 010c2119c787b61301dbc9b9ec4e654edd33086e Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 1 Oct 2013 12:36:20 -0400 Subject: Add more verbose error handling To help diagnose this problem. I think we might prefer to leave this on hold until we merge the new gnupg module. --- .../bitmask/services/soledad/soledadbootstrapper.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index d348661d..6731cc84 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -362,11 +362,27 @@ class SoledadBootstrapper(AbstractBootstrapper): try: self._keymanager.get_key( address, openpgp.OpenPGPKey, private=True, fetch_remote=False) + return except KeyNotFound: logger.debug("Key not found. Generating key for %s" % (address,)) + + # generate key + try: self._keymanager.gen_key(openpgp.OpenPGPKey) + except Exception as exc: + logger.error("error while generating key!") + logger.exception(exc) + raise + + # send key + try: self._keymanager.send_key(openpgp.OpenPGPKey) - logger.debug("Key generated successfully.") + except Exception as exc: + logger.error("error while sending key!") + logger.exception(exc) + raise + + logger.debug("Key generated successfully.") def run_soledad_setup_checks(self, provider_config, -- cgit v1.2.3 From d74a4c3840c95e5879c89ec9d1f6d48ab54b0f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 1 Oct 2013 14:46:53 -0300 Subject: Use the same exception for all the auth user facing errors --- src/leap/bitmask/crypto/srpauth.py | 20 ++++++-------------- src/leap/bitmask/crypto/tests/test_srpauth.py | 6 +++--- src/leap/bitmask/gui/preferenceswindow.py | 5 ++--- 3 files changed, 11 insertions(+), 20 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index bf85f75c..9c08d353 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -52,13 +52,6 @@ class SRPAuthConnectionError(SRPAuthenticationError): pass -class SRPAuthUnknownUser(SRPAuthenticationError): - """ - Exception raised when trying to authenticate an unknown user - """ - pass - - class SRPAuthBadStatusCode(SRPAuthenticationError): """ Exception raised when we received an unknown bad status code @@ -97,7 +90,7 @@ class SRPAuthJSONDecodeError(SRPAuthenticationError): pass -class SRPAuthBadPassword(SRPAuthenticationError): +class SRPAuthBadUserOrPassword(SRPAuthenticationError): """ Exception raised when the user provided a bad password to auth. """ @@ -219,7 +212,6 @@ class SRPAuth(QtCore.QObject): Might raise all SRPAuthenticationError based: SRPAuthenticationError SRPAuthConnectionError - SRPAuthUnknownUser SRPAuthBadStatusCode SRPAuthNoSalt SRPAuthNoB @@ -266,7 +258,7 @@ class SRPAuth(QtCore.QObject): "Status code = %r. Content: %r" % (init_session.status_code, content)) if init_session.status_code == 422: - raise SRPAuthUnknownUser(self._WRONG_USER_PASS) + raise SRPAuthBadUserOrPassword(self._WRONG_USER_PASS) raise SRPAuthBadStatusCode(self.tr("There was a problem with" " authentication")) @@ -296,7 +288,7 @@ class SRPAuth(QtCore.QObject): SRPAuthBadDataFromServer SRPAuthConnectionError SRPAuthJSONDecodeError - SRPAuthBadPassword + SRPAuthBadUserOrPassword :param salt_B: salt and B parameters for the username :type salt_B: tuple @@ -355,7 +347,7 @@ class SRPAuth(QtCore.QObject): "received: %s", (content,)) logger.error("[%s] Wrong password (HAMK): [%s]" % (auth_result.status_code, error)) - raise SRPAuthBadPassword(self._WRONG_USER_PASS) + raise SRPAuthBadUserOrPassword(self._WRONG_USER_PASS) if auth_result.status_code not in (200,): logger.error("No valid response (HAMK): " @@ -452,7 +444,7 @@ class SRPAuth(QtCore.QObject): It requires to be authenticated. Might raise: - SRPAuthBadPassword + SRPAuthBadUserOrPassword requests.exceptions.HTTPError :param current_password: the current password for the logged user. @@ -463,7 +455,7 @@ class SRPAuth(QtCore.QObject): leap_assert(self.get_uid() is not None) if current_password != self._password: - raise SRPAuthBadPassword + raise SRPAuthBadUserOrPassword url = "%s/%s/users/%s.json" % ( self._provider_config.get_api_uri(), diff --git a/src/leap/bitmask/crypto/tests/test_srpauth.py b/src/leap/bitmask/crypto/tests/test_srpauth.py index 6fb2b739..0cb8e79a 100644 --- a/src/leap/bitmask/crypto/tests/test_srpauth.py +++ b/src/leap/bitmask/crypto/tests/test_srpauth.py @@ -246,7 +246,7 @@ class SRPAuthTestCase(unittest.TestCase): d = self._prepare_auth_test(422) def wrapper(_): - with self.assertRaises(srpauth.SRPAuthUnknownUser): + with self.assertRaises(srpauth.SRPAuthBadUserOrPassword): with mock.patch( 'leap.bitmask.util.request_helpers.get_content', new=mock.create_autospec(get_content)) as content: @@ -425,7 +425,7 @@ class SRPAuthTestCase(unittest.TestCase): new=mock.create_autospec(get_content)) as \ content: content.return_value = ("", 0) - with self.assertRaises(srpauth.SRPAuthBadPassword): + with self.assertRaises(srpauth.SRPAuthBadUserOrPassword): self.auth_backend._process_challenge( salt_B, username=self.TEST_USER) @@ -449,7 +449,7 @@ class SRPAuthTestCase(unittest.TestCase): new=mock.create_autospec(get_content)) as \ content: content.return_value = ("[]", 0) - with self.assertRaises(srpauth.SRPAuthBadPassword): + with self.assertRaises(srpauth.SRPAuthBadUserOrPassword): self.auth_backend._process_challenge( salt_B, username=self.TEST_USER) diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index 7e281b44..58cb05ba 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -27,11 +27,10 @@ from PySide import QtCore, QtGui from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.gui.ui_preferences import Ui_Preferences from leap.soledad.client import NoStorageSecret -from leap.bitmask.crypto.srpauth import SRPAuthBadPassword +from leap.bitmask.crypto.srpauth import SRPAuthBadUserOrPassword from leap.bitmask.util.password import basic_password_checks from leap.bitmask.services import get_supported from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector from leap.bitmask.services import get_service_display_name logger = logging.getLogger(__name__) @@ -179,7 +178,7 @@ class PreferencesWindow(QtGui.QDialog): logger.error("Error changing password: %s", (failure, )) problem = self.tr("There was a problem changing the password.") - if failure.check(SRPAuthBadPassword): + if failure.check(SRPAuthBadUserOrPassword): problem = self.tr("You did not enter a correct current password.") self._set_password_change_status(problem, error=True) -- cgit v1.2.3 From 04bf753b0e120a92a4ffdbbdb0128d4f6c19db79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 2 Oct 2013 11:06:33 -0300 Subject: Hide logout error message label --- src/leap/bitmask/gui/login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py index 324280b9..c635081c 100644 --- a/src/leap/bitmask/gui/login.py +++ b/src/leap/bitmask/gui/login.py @@ -330,7 +330,7 @@ class LoginWidget(QtGui.QWidget): self.set_password("") self.set_enabled(True) - self.set_status("") + self.set_status("", error=False) def set_login_status(self, msg, error=False): """ -- cgit v1.2.3 From 85103cd2977bee78006dac15cfd33a549f6a39de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 1 Oct 2013 15:45:06 -0300 Subject: Fix failing tests/code --- src/leap/bitmask/crypto/tests/test_srpauth.py | 5 +-- src/leap/bitmask/provider/providerbootstrapper.py | 4 +- .../provider/tests/test_providerbootstrapper.py | 50 +++++++++------------- src/leap/bitmask/services/__init__.py | 4 +- .../services/eip/tests/test_eipbootstrapper.py | 4 +- 5 files changed, 30 insertions(+), 37 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/crypto/tests/test_srpauth.py b/src/leap/bitmask/crypto/tests/test_srpauth.py index 6fb2b739..5f2b44ee 100644 --- a/src/leap/bitmask/crypto/tests/test_srpauth.py +++ b/src/leap/bitmask/crypto/tests/test_srpauth.py @@ -680,10 +680,7 @@ class SRPAuthTestCase(unittest.TestCase): self.auth_backend._session.delete, side_effect=Exception()) - def wrapper(*args): - self.auth_backend.logout() - - d = threads.deferToThread(wrapper) + d = threads.deferToThread(self.auth.logout) return d @deferred() diff --git a/src/leap/bitmask/provider/providerbootstrapper.py b/src/leap/bitmask/provider/providerbootstrapper.py index 751da828..a10973f0 100644 --- a/src/leap/bitmask/provider/providerbootstrapper.py +++ b/src/leap/bitmask/provider/providerbootstrapper.py @@ -156,7 +156,9 @@ class ProviderBootstrapper(AbstractBootstrapper): # Watch out! We're handling the verify paramenter differently here. headers = {} - provider_json = os.path.join(get_path_prefix(), "leap", "providers", + provider_json = os.path.join(get_path_prefix(), + "leap", + "providers", self._domain, "provider.json") mtime = get_mtime(provider_json) diff --git a/src/leap/bitmask/provider/tests/test_providerbootstrapper.py b/src/leap/bitmask/provider/tests/test_providerbootstrapper.py index 9b47d60e..fe5b52bd 100644 --- a/src/leap/bitmask/provider/tests/test_providerbootstrapper.py +++ b/src/leap/bitmask/provider/tests/test_providerbootstrapper.py @@ -42,6 +42,8 @@ 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.bitmask import util +from leap.bitmask.util import get_path_prefix from leap.common.files import mkdir_p from leap.common.testing.https_server import where from leap.common.testing.basetest import BaseLeapTest @@ -315,16 +317,19 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): # tearDown so we are sure everything is as expected for each # test. If we do it inside each specific test, a failure in # the test will leave the implementation with the mock. - self.old_gpp = ProviderConfig.get_path_prefix + self.old_gpp = util.get_path_prefix + self.old_load = ProviderConfig.load self.old_save = ProviderConfig.save self.old_api_version = ProviderConfig.get_api_version + self.old_api_uri = ProviderConfig.get_api_uri def tearDown(self): - ProviderConfig.get_path_prefix = self.old_gpp + util.get_path_prefix = self.old_gpp ProviderConfig.load = self.old_load ProviderConfig.save = self.old_save ProviderConfig.get_api_version = self.old_api_version + ProviderConfig.get_api_uri = self.old_api_uri def test_check_https_succeeds(self): # XXX: Need a proper CA signed cert to test this @@ -372,10 +377,11 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): paths :type path_prefix: str """ - ProviderConfig.get_path_prefix = mock.MagicMock( - return_value=path_prefix) + util.get_path_prefix = mock.MagicMock(return_value=path_prefix) ProviderConfig.get_api_version = mock.MagicMock( return_value=api) + ProviderConfig.get_api_uri = mock.MagicMock( + return_value="https://localhost:%s" % (self.https_port,)) ProviderConfig.load = mock.MagicMock() ProviderConfig.save = mock.MagicMock() @@ -400,10 +406,8 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): :returns: the provider.json path used :rtype: str """ - provider_dir = os.path.join(ProviderConfig() - .get_path_prefix(), - "leap", - "providers", + provider_dir = os.path.join(get_path_prefix(), + "leap", "providers", self.pb._domain) mkdir_p(provider_dir) provider_path = os.path.join(provider_dir, @@ -413,6 +417,9 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): p.write("A") return provider_path + @mock.patch( + 'leap.bitmask.config.providerconfig.ProviderConfig.get_domain', + lambda x: where('testdomain.com')) def test_download_provider_info_new_provider(self): self._setup_provider_config_with("1", tempfile.mkdtemp()) self._setup_providerbootstrapper(True) @@ -431,10 +438,7 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): # set mtime to something really new os.utime(provider_path, (-1, time.time())) - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - self.pb._download_provider_info() + self.pb._download_provider_info() # we check that it doesn't save the provider # config, because it's new enough self.assertFalse(ProviderConfig.save.called) @@ -450,10 +454,7 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): # set mtime to something really new os.utime(provider_path, (-1, time.time())) - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - self.pb._download_provider_info() + self.pb._download_provider_info() # we check that it doesn't save the provider # config, because it's new enough self.assertFalse(ProviderConfig.save.called) @@ -469,10 +470,7 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): # set mtime to something really old os.utime(provider_path, (-1, 100)) - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - self.pb._download_provider_info() + self.pb._download_provider_info() self.assertTrue(ProviderConfig.load.called) self.assertTrue(ProviderConfig.save.called) @@ -484,11 +482,8 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): self._setup_providerbootstrapper(False) self._produce_dummy_provider_json() - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - with self.assertRaises(UnsupportedProviderAPI): - self.pb._download_provider_info() + with self.assertRaises(UnsupportedProviderAPI): + self.pb._download_provider_info() @mock.patch( 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path', @@ -499,10 +494,7 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): self._setup_providerbootstrapper(False) self._produce_dummy_provider_json() - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - self.pb._download_provider_info() + self.pb._download_provider_info() @mock.patch( 'leap.bitmask.config.providerconfig.ProviderConfig.get_api_uri', diff --git a/src/leap/bitmask/services/__init__.py b/src/leap/bitmask/services/__init__.py index afce72f6..0d74e0e2 100644 --- a/src/leap/bitmask/services/__init__.py +++ b/src/leap/bitmask/services/__init__.py @@ -105,10 +105,12 @@ def download_service_config(provider_config, service_config, service_name = service_config.name service_json = "{0}-service.json".format(service_name) headers = {} - mtime = get_mtime(os.path.join(get_path_prefix(), + mtime = get_mtime(os.path.join(service_config + .get_path_prefix(), "leap", "providers", provider_config.get_domain(), service_json)) + if download_if_needed and mtime: headers['if-modified-since'] = mtime diff --git a/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py b/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py index d0d78eed..fed4a783 100644 --- a/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py +++ b/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py @@ -150,10 +150,10 @@ class EIPBootstrapperActiveTest(BaseLeapTest): def check(*args): self.eb._eip_config.save.assert_called_once_with( - ["leap", + ("leap", "providers", self.eb._provider_config.get_domain(), - "eip-service.json"]) + "eip-service.json")) d.addCallback(check) return d -- cgit v1.2.3 From c9742f3549d07c23f5caf8a8e48317a4fb5e75a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 2 Oct 2013 12:46:14 -0300 Subject: Fix some more tests --- src/leap/bitmask/provider/providerbootstrapper.py | 4 ++-- .../bitmask/provider/tests/test_providerbootstrapper.py | 3 +-- src/leap/bitmask/services/__init__.py | 5 ++--- src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py | 13 +++++++------ 4 files changed, 12 insertions(+), 13 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/provider/providerbootstrapper.py b/src/leap/bitmask/provider/providerbootstrapper.py index a10973f0..1b5947e1 100644 --- a/src/leap/bitmask/provider/providerbootstrapper.py +++ b/src/leap/bitmask/provider/providerbootstrapper.py @@ -27,7 +27,7 @@ from PySide import QtCore from leap.bitmask.config.providerconfig import ProviderConfig, MissingCACert from leap.bitmask.util.request_helpers import get_content -from leap.bitmask.util import get_path_prefix +from leap.bitmask import util from leap.bitmask.util.constants import REQUEST_TIMEOUT from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper from leap.bitmask.provider.supportedapis import SupportedAPIs @@ -156,7 +156,7 @@ class ProviderBootstrapper(AbstractBootstrapper): # Watch out! We're handling the verify paramenter differently here. headers = {} - provider_json = os.path.join(get_path_prefix(), + provider_json = os.path.join(util.get_path_prefix(), "leap", "providers", self._domain, "provider.json") diff --git a/src/leap/bitmask/provider/tests/test_providerbootstrapper.py b/src/leap/bitmask/provider/tests/test_providerbootstrapper.py index fe5b52bd..88a4ff0b 100644 --- a/src/leap/bitmask/provider/tests/test_providerbootstrapper.py +++ b/src/leap/bitmask/provider/tests/test_providerbootstrapper.py @@ -43,7 +43,6 @@ from leap.bitmask.provider.providerbootstrapper import UnsupportedProviderAPI from leap.bitmask.provider.providerbootstrapper import WrongFingerprint from leap.bitmask.provider.supportedapis import SupportedAPIs from leap.bitmask import util -from leap.bitmask.util import get_path_prefix from leap.common.files import mkdir_p from leap.common.testing.https_server import where from leap.common.testing.basetest import BaseLeapTest @@ -406,7 +405,7 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): :returns: the provider.json path used :rtype: str """ - provider_dir = os.path.join(get_path_prefix(), + provider_dir = os.path.join(util.get_path_prefix(), "leap", "providers", self.pb._domain) mkdir_p(provider_dir) diff --git a/src/leap/bitmask/services/__init__.py b/src/leap/bitmask/services/__init__.py index 0d74e0e2..9b32c5ad 100644 --- a/src/leap/bitmask/services/__init__.py +++ b/src/leap/bitmask/services/__init__.py @@ -27,7 +27,7 @@ from leap.bitmask.crypto.srpauth import SRPAuth from leap.bitmask.util.constants import REQUEST_TIMEOUT from leap.bitmask.util.privilege_policies import is_missing_policy_permissions from leap.bitmask.util.request_helpers import get_content -from leap.bitmask.util import get_path_prefix +from leap.bitmask import util from leap.common.check import leap_assert from leap.common.config.baseconfig import BaseConfig @@ -105,8 +105,7 @@ def download_service_config(provider_config, service_config, service_name = service_config.name service_json = "{0}-service.json".format(service_name) headers = {} - mtime = get_mtime(os.path.join(service_config - .get_path_prefix(), + mtime = get_mtime(os.path.join(util.get_path_prefix(), "leap", "providers", provider_config.get_domain(), service_json)) diff --git a/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py b/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py index fed4a783..6640a860 100644 --- a/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py +++ b/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py @@ -41,6 +41,7 @@ from leap.bitmask.services.eip.eipconfig import EIPConfig from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.crypto.tests import fake_provider from leap.bitmask.crypto.srpauth import SRPAuth +from leap.bitmask import util from leap.common.testing.basetest import BaseLeapTest from leap.common.files import mkdir_p @@ -60,13 +61,13 @@ class EIPBootstrapperActiveTest(BaseLeapTest): def setUp(self): self.eb = EIPBootstrapper() - self.old_pp = EIPConfig.get_path_prefix + self.old_pp = util.get_path_prefix self.old_save = EIPConfig.save self.old_load = EIPConfig.load self.old_si = SRPAuth.get_session_id def tearDown(self): - EIPConfig.get_path_prefix = self.old_pp + util.get_path_prefix = self.old_pp EIPConfig.save = self.old_save EIPConfig.load = self.old_load SRPAuth.get_session_id = self.old_si @@ -97,13 +98,13 @@ class EIPBootstrapperActiveTest(BaseLeapTest): pc.get_ca_cert_path = mock.MagicMock(return_value=False) path_prefix = tempfile.mkdtemp() - EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix) + util.get_path_prefix = mock.MagicMock(return_value=path_prefix) EIPConfig.save = mock.MagicMock() EIPConfig.load = mock.MagicMock() self.eb._download_if_needed = ifneeded - provider_dir = os.path.join(EIPConfig.get_path_prefix(), + provider_dir = os.path.join(util.get_path_prefix(), "leap", "providers", pc.get_domain()) @@ -184,13 +185,13 @@ class EIPBootstrapperActiveTest(BaseLeapTest): pc.get_ca_cert_path = mock.MagicMock(return_value=False) path_prefix = tempfile.mkdtemp() - EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix) + util.get_path_prefix = mock.MagicMock(return_value=path_prefix) EIPConfig.save = mock.MagicMock() EIPConfig.load = mock.MagicMock() self.eb._download_if_needed = ifneeded - provider_dir = os.path.join(EIPConfig.get_path_prefix(), + provider_dir = os.path.join(util.get_path_prefix(), "leap", "providers", "somedomain") -- cgit v1.2.3 From 5fc48bf44bb127d54c001ab2e668dd5bed3c8f2d Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Mon, 30 Sep 2013 12:26:46 -0300 Subject: Move echo-mode-setting to the ui file. --- src/leap/bitmask/gui/ui/wizard.ui | 12 ++++++++++-- src/leap/bitmask/gui/wizard.py | 3 --- 2 files changed, 10 insertions(+), 5 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/ui/wizard.ui b/src/leap/bitmask/gui/ui/wizard.ui index 2a412784..420c74ae 100644 --- a/src/leap/bitmask/gui/ui/wizard.ui +++ b/src/leap/bitmask/gui/ui/wizard.ui @@ -626,10 +626,18 @@ - + + + QLineEdit::Password + + - + + + QLineEdit::Password + + diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index bb38b136..5c00f631 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -110,9 +110,6 @@ class Wizard(QtGui.QWizard): self.currentIdChanged.connect(self._current_id_changed) - self.ui.lblPassword.setEchoMode(QtGui.QLineEdit.Password) - self.ui.lblPassword2.setEchoMode(QtGui.QLineEdit.Password) - self.ui.lnProvider.textChanged.connect( self._enable_check) -- cgit v1.2.3 From 057f42dcc2c23b93b872301d01a944fde5025439 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Mon, 30 Sep 2013 18:28:00 -0300 Subject: Add providers combobox with configured providers. --- src/leap/bitmask/gui/ui/wizard.ui | 171 ++++++++++++++++++++++++++++++++------ src/leap/bitmask/gui/wizard.py | 6 ++ 2 files changed, 151 insertions(+), 26 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/ui/wizard.ui b/src/leap/bitmask/gui/ui/wizard.ui index 420c74ae..b796b795 100644 --- a/src/leap/bitmask/gui/ui/wizard.ui +++ b/src/leap/bitmask/gui/ui/wizard.ui @@ -134,17 +134,7 @@ - - - - - - - Check - - - - + Qt::Vertical @@ -157,17 +147,7 @@ - - - - https:// - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - + Checking for a valid provider @@ -276,13 +256,73 @@ - + + + + + Configure or select a provider + + + + + + https:// + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + false + + + + + + + Check + + + + + + + Configure new provider + + + true + + + + + + + Use existing one + + + + + + + https:// + + + + + + @@ -764,12 +804,91 @@ btnRegister rdoRegister rdoLogin - lnProvider - btnCheck - + + + rbExistingProvider + toggled(bool) + cbProviders + setFocus() + + + 167 + 192 + + + 265 + 191 + + + + + rbNewProvider + toggled(bool) + lnProvider + setFocus() + + + 171 + 164 + + + 246 + 164 + + + + + rbExistingProvider + toggled(bool) + lnProvider + setDisabled(bool) + + + 169 + 196 + + + 327 + 163 + + + + + rbNewProvider + toggled(bool) + cbProviders + setDisabled(bool) + + + 169 + 162 + + + 269 + 193 + + + + + rbExistingProvider + toggled(bool) + btnCheck + setDisabled(bool) + + + 154 + 193 + + + 498 + 170 + + + + diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index 5c00f631..f71ce06b 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -26,6 +26,7 @@ from functools import partial from PySide import QtCore, QtGui from twisted.internet import threads +from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.crypto.srpregister import SRPRegister from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper @@ -141,6 +142,11 @@ class Wizard(QtGui.QWizard): self.ui.label_12.setVisible(False) self.ui.lblProviderPolicy.setVisible(False) + # Load configured providers into wizard + ls = LeapSettings() + providers = ls.get_configured_providers() + self.ui.cbProviders.addItems(providers) + def get_domain(self): return self._domain -- cgit v1.2.3 From 5b2220bc0177f12c81a3dbb1ebffd3cdae8b350d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 2 Oct 2013 11:57:57 -0300 Subject: Use token header also for authenticated requests --- src/leap/bitmask/crypto/srpauth.py | 8 +++++++- src/leap/bitmask/services/__init__.py | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index 9c08d353..90d9ea0a 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -129,6 +129,7 @@ class SRPAuth(QtCore.QObject): SESSION_ID_KEY = "_session_id" USER_VERIFIER_KEY = 'user[password_verifier]' USER_SALT_KEY = 'user[password_salt]' + AUTHORIZATION_KEY = "Authorization" def __init__(self, provider_config): """ @@ -466,6 +467,10 @@ class SRPAuth(QtCore.QObject): self._username, new_password, self._hashfun, self._ng) cookies = {self.SESSION_ID_KEY: self.get_session_id()} + headers = { + self.AUTHORIZATION_KEY: + "Token token={0}".format(self.get_token()) + } user_data = { self.USER_VERIFIER_KEY: binascii.hexlify(verifier), self.USER_SALT_KEY: binascii.hexlify(salt) @@ -475,7 +480,8 @@ class SRPAuth(QtCore.QObject): url, data=user_data, verify=self._provider_config.get_ca_cert_path(), cookies=cookies, - timeout=REQUEST_TIMEOUT) + timeout=REQUEST_TIMEOUT, + headers=headers) # In case of non 2xx it raises HTTPError change_password.raise_for_status() diff --git a/src/leap/bitmask/services/__init__.py b/src/leap/bitmask/services/__init__.py index 0d74e0e2..e19b82b9 100644 --- a/src/leap/bitmask/services/__init__.py +++ b/src/leap/bitmask/services/__init__.py @@ -127,10 +127,15 @@ def download_service_config(provider_config, service_config, # XXX make and use @with_srp_auth decorator srp_auth = SRPAuth(provider_config) session_id = srp_auth.get_session_id() + token = srp_auth.get_token() cookies = None - if session_id: + if session_id is not None: cookies = {"_session_id": session_id} + # API v2 will only support token auth, but in v1 we can send both + if token is not None: + headers["Authorization"] = 'Token token="{0}"'.format(token) + res = session.get(config_uri, verify=provider_config.get_ca_cert_path(), headers=headers, -- cgit v1.2.3 From 201e9917a957b68e02a540ee395b32521f699eda Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Tue, 1 Oct 2013 12:33:17 -0300 Subject: Skip checks for an existing provider. --- src/leap/bitmask/gui/wizard.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index f71ce06b..7cff742e 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -82,6 +82,8 @@ class Wizard(QtGui.QWizard): self._show_register = False + self._use_existing_provider = False + self.ui.grpCheckProvider.setVisible(False) self.ui.btnCheck.clicked.connect(self._check_provider) self.ui.lnProvider.returnPressed.connect(self._check_provider) @@ -123,6 +125,8 @@ class Wizard(QtGui.QWizard): self.ui.btnRegister.clicked.connect( self._register) + self.ui.rbExistingProvider.toggled.connect(self._skip_provider_checks) + usernameRe = QtCore.QRegExp(self.BARE_USERNAME_REGEX) self.ui.lblUser.setValidator( QtGui.QRegExpValidator(usernameRe, self)) @@ -318,6 +322,25 @@ class Wizard(QtGui.QWizard): self._provider_select_defer = self._provider_bootstrapper.\ run_provider_select_checks(self._domain) + def _skip_provider_checks(self, skip): + """ + SLOT + Triggered: + self.ui.rbExistingProvider.toggled + + Allows the user to move to the next page without make any checks, + used when we are selecting an already configured provider. + + :param skip: if we should skip checks or not + :type skip: bool + """ + if skip: + self._reset_provider_check() + + self.page(self.SELECT_PROVIDER_PAGE).set_completed(skip) + self.button(QtGui.QWizard.NextButton).setEnabled(skip) + self._use_existing_provider = skip + def _complete_task(self, data, label, complete=False, complete_page=-1): """ Checks a task and completes a page if specified @@ -564,4 +587,14 @@ class Wizard(QtGui.QWizard): else: return self.SERVICES_PAGE + if self.currentPage() == self.page(self.SELECT_PROVIDER_PAGE): + if self._use_existing_provider: + self._domain = self.ui.cbProviders.currentText() + self._provider_config = ProviderConfig.get_provider_config( + self._domain) + if self._show_register: + return self.REGISTER_USER_PAGE + else: + return self.SERVICES_PAGE + return QtGui.QWizard.nextId(self) -- cgit v1.2.3 From e9c84a3d2d9fd3a0457f256a908cb5bce9351682 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 1 Oct 2013 15:02:09 -0400 Subject: remove duplicated method definition --- src/leap/bitmask/gui/eip_status.py | 18 ------------------ 1 file changed, 18 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py index f7408b13..6b1235a6 100644 --- a/src/leap/bitmask/gui/eip_status.py +++ b/src/leap/bitmask/gui/eip_status.py @@ -216,22 +216,6 @@ class EIPStatusWidget(QtGui.QWidget): leap_assert_type(eip_status_menu, QtGui.QMenu) self._eip_status_menu = eip_status_menu - def set_eip_status(self, status, error=False): - """ - Sets the global status label. - - :param status: status message - :type status: str or unicode - :param error: if the status is an erroneous one, then set this - to True - :type error: bool - """ - leap_assert_type(error, bool) - if error: - status = "%s" % (status,) - self.ui.lblEIPStatus.setText(status) - self.ui.lblEIPStatus.show() - # EIP status --- @property @@ -261,9 +245,7 @@ class EIPStatusWidget(QtGui.QWidget): :type error: bool """ leap_assert_type(error, bool) - self._eip_status = status - if error: status = "%s" % (status,) self.ui.lblEIPStatus.setText(status) -- cgit v1.2.3 From 40161f730310d18756123e53ea29f724eea59730 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 2 Oct 2013 16:41:23 -0300 Subject: Separate pre-seeded providers from user added ones --- src/leap/bitmask/config/leapsettings.py | 17 +++++++++++++++++ src/leap/bitmask/gui/wizard.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/config/leapsettings.py b/src/leap/bitmask/config/leapsettings.py index 338fa475..7ab1ace3 100644 --- a/src/leap/bitmask/config/leapsettings.py +++ b/src/leap/bitmask/config/leapsettings.py @@ -67,6 +67,7 @@ class LeapSettings(object): DEFAULTPROVIDER_KEY = "DefaultProvider" ALERTMISSING_KEY = "AlertMissingScripts" GATEWAY_KEY = "Gateway" + PINNED_KEY = "Pinned" # values GATEWAY_AUTOMATIC = "Automatic" @@ -134,6 +135,22 @@ class LeapSettings(object): return providers + def is_pinned_provider(self, domain): + """ + Returns True if the domain 'domain' is pinned with the application. + False otherwise. + + :param provider: provider domain + :type provider: str + + :rtype: bool + """ + leap_assert(len(domain) > 0, "We need a nonempty domain.") + pinned_key = "{0}/{1}".format(domain, self.PINNED_KEY) + result = to_bool(self._settings.value(pinned_key, False)) + + return result + def get_selected_gateway(self, provider): """ Returns the configured gateway for the given provider. diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index 7cff742e..219270c7 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -20,6 +20,7 @@ First run wizard import os import logging import json +import random from functools import partial @@ -146,10 +147,33 @@ class Wizard(QtGui.QWizard): self.ui.label_12.setVisible(False) self.ui.lblProviderPolicy.setVisible(False) - # Load configured providers into wizard + self._load_configured_providers() + + def _load_configured_providers(self): + """ + Loads the configured providers into the wizard providers combo box. + """ ls = LeapSettings() providers = ls.get_configured_providers() - self.ui.cbProviders.addItems(providers) + pinned = [] + user_added = [] + + # separate pinned providers from user added ones + for p in providers: + if ls.is_pinned_provider(p): + pinned.append(p) + else: + user_added.append(p) + + if user_added: + self.ui.cbProviders.addItems(user_added) + + if user_added and pinned: + self.ui.cbProviders.addItem('---') + + if pinned: + random.shuffle(pinned) # don't prioritize alphabetically + self.ui.cbProviders.addItems(pinned) def get_domain(self): return self._domain -- cgit v1.2.3 From b9b7b244984111ec799675dc90073396481e6173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 2 Oct 2013 21:36:37 -0300 Subject: Only pick one gnupg bin path which is not a symlink --- src/leap/bitmask/services/soledad/soledadbootstrapper.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index 6731cc84..7968dd6a 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -321,7 +321,18 @@ class SoledadBootstrapper(AbstractBootstrapper): gpgbin = os.path.join( get_path_prefix(), "..", "apps", "mail", "gpg") else: - gpgbin = which("gpg") + try: + gpgbin_options = which("gpg") + # gnupg checks that the path to the binary is not a + # symlink, so we need to filter those and come up with + # just one option. + for opt in gpgbin_options: + if not os.path.islink(opt): + gpgbin = opt + break + except IndexError as e: + logger.debug("Couldn't find the gpg binary!") + logger.exception(e) leap_check(gpgbin is not None, "Could not find gpg binary") return gpgbin -- cgit v1.2.3 From 6f932294e7bf58e66ca117fe46ebe346e10aef0f Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Thu, 3 Oct 2013 14:00:14 -0300 Subject: Reorder providers combo, disable if no providers. - Move radio buttons to get more space for the labels. - If there are no configured providers then disable the combo. --- src/leap/bitmask/gui/ui/wizard.ui | 53 +++++++++++++++++++++------------------ src/leap/bitmask/gui/wizard.py | 6 +++++ 2 files changed, 34 insertions(+), 25 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/ui/wizard.ui b/src/leap/bitmask/gui/ui/wizard.ui index b796b795..0f6eef6e 100644 --- a/src/leap/bitmask/gui/ui/wizard.ui +++ b/src/leap/bitmask/gui/ui/wizard.ui @@ -269,7 +269,24 @@ Configure or select a provider - + + + + Configure new provider: + + + true + + + + + + + Use existing one: + + + + https:// @@ -279,44 +296,30 @@ - + - - - false - - - - Check - - + + - Configure new provider - - - true + https:// - - - - - - Use existing one + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - https:// + + + + false diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index 219270c7..e3f5904e 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -155,6 +155,12 @@ class Wizard(QtGui.QWizard): """ ls = LeapSettings() providers = ls.get_configured_providers() + if not providers: + self.ui.rbExistingProvider.setEnabled(False) + self.ui.label_8.setEnabled(False) # 'https://' label + self.ui.cbProviders.setEnabled(False) + return + pinned = [] user_added = [] -- cgit v1.2.3 From 689c716b831047c8fe18945956e809b74e4250a3 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 1 Oct 2013 17:34:24 -0400 Subject: Disable EIP on/off button and action when login required. Also adds an explicit should_autostart flag in config. --- src/leap/bitmask/config/__init__.py | 0 src/leap/bitmask/config/leapsettings.py | 19 +++++ src/leap/bitmask/gui/eip_status.py | 26 +++++++ src/leap/bitmask/gui/login.py | 4 +- src/leap/bitmask/gui/mainwindow.py | 118 ++++++++++++++++++++--------- src/leap/bitmask/provider/__init__.py | 34 +++++++++ src/leap/bitmask/services/eip/eipconfig.py | 40 +++++++++- 7 files changed, 204 insertions(+), 37 deletions(-) delete mode 100644 src/leap/bitmask/config/__init__.py (limited to 'src/leap') diff --git a/src/leap/bitmask/config/__init__.py b/src/leap/bitmask/config/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/bitmask/config/leapsettings.py b/src/leap/bitmask/config/leapsettings.py index 338fa475..dc1af899 100644 --- a/src/leap/bitmask/config/leapsettings.py +++ b/src/leap/bitmask/config/leapsettings.py @@ -65,6 +65,7 @@ class LeapSettings(object): PROPERPROVIDER_KEY = "ProperProvider" REMEMBER_KEY = "RememberUserAndPass" DEFAULTPROVIDER_KEY = "DefaultProvider" + AUTOSTARTEIP_KEY = "AutoStartEIP" ALERTMISSING_KEY = "AlertMissingScripts" GATEWAY_KEY = "Gateway" @@ -285,6 +286,24 @@ class LeapSettings(object): else: self._settings.setValue(self.DEFAULTPROVIDER_KEY, provider) + def get_autostart_eip(self): + """ + Gets whether the app should autostart EIP. + + :rtype: bool + """ + return to_bool(self._settings.value(self.AUTOSTARTEIP_KEY, False)) + + def set_autostart_eip(self, autostart): + """ + Sets whether the app should autostart EIP. + + :param autostart: True if we should try to autostart EIP. + :type autostart: bool + """ + leap_assert_type(autostart, bool) + self._settings.setValue(self.AUTOSTARTEIP_KEY, autostart) + def get_alert_missing_scripts(self): """ Returns the setting for alerting of missing up/down scripts. diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py index 6b1235a6..946eaa4e 100644 --- a/src/leap/bitmask/gui/eip_status.py +++ b/src/leap/bitmask/gui/eip_status.py @@ -233,6 +233,30 @@ class EIPStatusWidget(QtGui.QWidget): """ self.set_startstop_enabled(False) + @QtCore.Slot() + def disable_eip_start(self): + """ + Triggered when a default provider_config has not been found. + Disables the start button and adds instructions to the user. + """ + logger.debug('Hiding EIP start button') + # you might be tempted to change this for a .setEnabled(False). + # it won't work. it's under the claws of the state machine. + # probably the best thing would be to make a transitional + # transition there, but that's more involved. + self.eip_button.hide() + msg = self.tr("You must login to use Encrypted Internet") + self.eip_label.setText(msg) + + @QtCore.Slot() + def enable_eip_start(self): + """ + Triggered after a successful login. + Enables the start button. + """ + logger.debug('Showing EIP start button') + self.eip_button.show() + # XXX disable (later) -------------------------- def set_eip_status(self, status, error=False): """ @@ -261,6 +285,8 @@ class EIPStatusWidget(QtGui.QWidget): :param value: True for enabled, False otherwise :type value: bool """ + # TODO use disable_eip_start instead + # this should be handled by the state machine leap_assert_type(value, bool) self.ui.btnEipStartStop.setEnabled(value) self._action_eip_startstop.setEnabled(value) diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py index c635081c..582f26be 100644 --- a/src/leap/bitmask/gui/login.py +++ b/src/leap/bitmask/gui/login.py @@ -14,7 +14,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - """ Login widget implementation """ @@ -36,9 +35,9 @@ class LoginWidget(QtGui.QWidget): Login widget that emits signals to display the wizard or to perform login. """ - # Emitted when the login button is clicked login = QtCore.Signal() + logged_in_signal = QtCore.Signal() cancel_login = QtCore.Signal() logout = QtCore.Signal() @@ -320,6 +319,7 @@ class LoginWidget(QtGui.QWidget): self.ui.lblUser.setText("%s@%s" % (self.get_user(), self.get_selected_provider())) self.set_login_status("") + self.logged_in_signal.emit() def logged_out(self): """ diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 58ed3eb3..7129b670 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -38,9 +38,10 @@ from leap.bitmask.gui.eip_status import EIPStatusWidget from leap.bitmask.gui.mail_status import MailStatusWidget from leap.bitmask.gui.wizard import Wizard +from leap.bitmask import provider 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 import eipconfig # XXX: Soledad might not work out of the box in Windows, issue #2932 from leap.bitmask.services.soledad.soledadbootstrapper import \ SoledadBootstrapper @@ -100,6 +101,7 @@ class MainWindow(QtGui.QMainWindow): MX_SERVICE = "mx" # Signals + eip_needs_login = QtCore.Signal([]) new_updates = QtCore.Signal(object) raise_window = QtCore.Signal([]) soledad_ready = QtCore.Signal([]) @@ -162,6 +164,10 @@ class MainWindow(QtGui.QMainWindow): self._eip_status = EIPStatusWidget(self) self.ui.eipLayout.addWidget(self._eip_status) + self._login_widget.logged_in_signal.connect( + self._eip_status.enable_eip_start) + self._login_widget.logged_in_signal.connect( + self._enable_eip_start_action) self._mail_status = MailStatusWidget(self) self.ui.mailLayout.addWidget(self._mail_status) @@ -174,13 +180,17 @@ class MainWindow(QtGui.QMainWindow): self._stop_eip) self._eip_status.eip_connection_connected.connect( self._on_eip_connected) + self.eip_needs_login.connect( + self._eip_status.disable_eip_start) + self.eip_needs_login.connect( + self._disable_eip_start_action) # This is loaded only once, there's a bug when doing that more # than once self._provider_config = ProviderConfig() # Used for automatic start of EIP self._provisional_provider_config = ProviderConfig() - self._eip_config = EIPConfig() + self._eip_config = eipconfig.EIPConfig() self._already_started_eip = False @@ -309,27 +319,29 @@ class MainWindow(QtGui.QMainWindow): self._smtp_config = SMTPConfig() - if self._first_run(): - self._wizard_firstrun = True - self._wizard = Wizard(bypass_checks=bypass_checks) - # Give this window time to finish init and then show the wizard - QtCore.QTimer.singleShot(1, self._launch_wizard) - self._wizard.accepted.connect(self._finish_init) - self._wizard.rejected.connect(self._rejected_wizard) - else: - self._finish_init() - # Eip machine is a public attribute where the state machine for # the eip connection will be available to the different components. # Remember that this will not live in the +1600LOC mainwindow for # all the eternity, so at some point we will be moving this to # the EIPConductor or some other clever component that we will # instantiate from here. - self.eip_machine = None + self.eip_machine = None # start event machines self.start_eip_machine() + if self._first_run(): + self._wizard_firstrun = True + self._wizard = Wizard(bypass_checks=bypass_checks) + # Give this window time to finish init and then show the wizard + QtCore.QTimer.singleShot(1, self._launch_wizard) + self._wizard.accepted.connect(self._finish_init) + self._wizard.rejected.connect(self._rejected_wizard) + else: + # during finish_init, we disable the eip start button + # so this has to be done after eip_machine is started + self._finish_init() + def _rejected_wizard(self): """ SLOT @@ -582,21 +594,29 @@ class MainWindow(QtGui.QMainWindow): """ Tries to autostart EIP """ - default_provider = self._settings.get_defaultprovider() + settings = self._settings + + should_autostart = settings.get_autostart_eip() + if not should_autostart: + logger.debug('Will not autostart EIP since it is setup ' + 'to not to do it') + self.eip_needs_login.emit() + return + + default_provider = settings.get_defaultprovider() if default_provider is None: logger.info("Cannot autostart Encrypted Internet because there is " "no default provider configured") + self.eip_needs_login.emit() return - self._enabled_services = self._settings.get_enabled_services( + self._enabled_services = settings.get_enabled_services( default_provider) - if self._provisional_provider_config.load( - os.path.join("leap", - "providers", - default_provider, - "provider.json")): + loaded = self._provisional_provider_config.load( + provider.get_provider_path(default_provider)) + if loaded: # XXX I think we should not try to re-download config every time, # it adds some delay. # Maybe if it's the first run in a session, @@ -604,6 +624,7 @@ class MainWindow(QtGui.QMainWindow): self._download_eip_config() else: # XXX: Display a proper message to the user + self.eip_needs_login.emit() logger.error("Unable to load %s config, cannot autostart." % (default_provider,)) @@ -1165,6 +1186,21 @@ class MainWindow(QtGui.QMainWindow): label=label) self.eip_machine = eip_machine self.eip_machine.start() + logger.debug('eip machine started') + + @QtCore.Slot() + def _disable_eip_start_action(self): + """ + Disables the EIP start action in the systray menu. + """ + self._action_eip_startstop.setEnabled(False) + + @QtCore.Slot() + def _enable_eip_start_action(self): + """ + Enables the EIP start action in the systray menu. + """ + self._action_eip_startstop.setEnabled(True) @QtCore.Slot() def _on_eip_connected(self): @@ -1197,6 +1233,27 @@ class MainWindow(QtGui.QMainWindow): self._eip_status.eip_pre_up() self.user_stopped_eip = False + # until we set an option in the preferences window, + # we'll assume that by default we try to autostart. + # If we switch it off manually, it won't try the next + # time. + self._settings.set_autostart_eip(True) + + loaded = eipconfig.load_eipconfig_if_needed( + provider_config, self._eip_config, provider) + + if not loaded: + self._eip_status.set_eip_status( + self.tr("Could not load Encrypted Internet " + "Configuration."), + error=True) + # signal connection aborted to state machine + qtsigs = self._eip_connection.qtsigs + qtsigs.connection_aborted_signal.emit() + logger.error("Tried to start EIP but cannot find any " + "available provider!") + return + try: # XXX move this to EIPConductor host, port = get_openvpn_management() @@ -1263,7 +1320,7 @@ class MainWindow(QtGui.QMainWindow): self._already_started_eip = True @QtCore.Slot() - def _stop_eip(self, abnormal=False): + def _stop_eip(self): """ SLOT TRIGGERS: @@ -1277,18 +1334,15 @@ class MainWindow(QtGui.QMainWindow): :param abnormal: whether this was an abnormal termination. :type abnormal: bool """ - if abnormal: - logger.warning("Abnormal EIP termination.") - self.user_stopped_eip = True self._vpn.terminate() self._set_eipstatus_off(False) - self._already_started_eip = False - # XXX do via signal - self._settings.set_defaultprovider(None) + logger.debug('Setting autostart to: False') + self._settings.set_autostart_eip(False) + if self._logged_user: self._eip_status.set_provider( "%s@%s" % (self._logged_user, @@ -1351,13 +1405,9 @@ class MainWindow(QtGui.QMainWindow): provider_config = self._get_best_provider_config() domain = provider_config.get_domain() - loaded = self._eip_config.loaded() - if not loaded: - eip_config_path = os.path.join("leap", "providers", - domain, "eip-service.json") - api_version = provider_config.get_api_version() - self._eip_config.set_api_version(api_version) - loaded = self._eip_config.load(eip_config_path) + # XXX move check to _start_eip ? + loaded = eipconfig.load_eipconfig_if_needed( + provider_config, self._eip_config, domain) if loaded: # DO START EIP Connection! diff --git a/src/leap/bitmask/provider/__init__.py b/src/leap/bitmask/provider/__init__.py index e69de29b..53587d65 100644 --- a/src/leap/bitmask/provider/__init__.py +++ b/src/leap/bitmask/provider/__init__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# __init.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 . +""" +Module initialization for leap.bitmask.provider +""" +import os +from leap.common.check import leap_assert + + +def get_provider_path(domain): + """ + Returns relative path for provider config. + + :param domain: the domain to which this providerconfig belongs to. + :type domain: str + :returns: the path + :rtype: str + """ + leap_assert(domain is not None, "get_provider_path: We need a domain") + return os.path.join("leap", "providers", domain, "provider.json") diff --git a/src/leap/bitmask/services/eip/eipconfig.py b/src/leap/bitmask/services/eip/eipconfig.py index 7d8995b4..16ed4cc0 100644 --- a/src/leap/bitmask/services/eip/eipconfig.py +++ b/src/leap/bitmask/services/eip/eipconfig.py @@ -33,6 +33,45 @@ from leap.common.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) +def get_eipconfig_path(domain): + """ + Returns relative path for EIP config. + + :param domain: the domain to which this eipconfig belongs to. + :type domain: str + :returns: the path + :rtype: str + """ + leap_assert(domain is not None, "get_eipconfig_path: We need a domain") + return os.path.join("leap", "providers", domain, "eip-service.json") + + +def load_eipconfig_if_needed(provider_config, eip_config, domain): + """ + Utility function to prime a eip_config object from a loaded + provider_config and the chosen provider domain. + + :param provider_config: a loaded instance of ProviderConfig + :type provider_config: ProviderConfig + + :param eip_config: the eipconfig object to be primed. + :type eip_config: EIPConfig + + :param domain: the chosen provider domain + :type domain: str + + :returns: Whether the eip_config object has been succesfully loaded + :rtype: bool + """ + loaded = eip_config.loaded() + if not loaded: + eip_config_path = get_eipconfig_path(domain) + api_version = provider_config.get_api_version() + eip_config.set_api_version(api_version) + loaded = eip_config.load(eip_config_path) + return loaded + + class VPNGatewaySelector(object): """ VPN Gateway selector. @@ -59,7 +98,6 @@ class VPNGatewaySelector(object): tz_offset = self.equivalent_timezones[tz_offset] self._local_offset = tz_offset - self._eipconfig = eipconfig def get_gateways_list(self): -- cgit v1.2.3 From 5e418935bdc5c64bc1cef8d5f440dc79cc6e2892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 3 Oct 2013 13:45:11 -0300 Subject: Update provider_config in SRPAuth initialization --- src/leap/bitmask/crypto/srpauth.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src/leap') diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index 90d9ea0a..42262610 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -603,6 +603,13 @@ class SRPAuth(QtCore.QObject): # Store instance reference as the only member in the handle self.__dict__['_SRPAuth__instance'] = SRPAuth.__instance + # Generally, we initialize this with a provider_config once, + # and after that initialize it without one and use the one + # that was assigned before. But we need to update it if we + # want to be able to logout and login into another provider. + if provider_config is not None: + SRPAuth.__instance._provider_config = provider_config + def authenticate(self, username, password): """ Executes the whole authentication process for a user -- cgit v1.2.3 From b6d7ffdb354ad4727f6a4dd158d439a2e768d68c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 3 Oct 2013 14:32:47 -0300 Subject: Reset the session on every login attempt --- src/leap/bitmask/crypto/srpauth.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/leap') diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index 42262610..cbff4b49 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -508,6 +508,8 @@ class SRPAuth(QtCore.QObject): self._username = username self._password = password + self._session = self._fetcher.session() + d = threads.deferToThread(self._authentication_preprocessing, username=username, password=password) -- cgit v1.2.3 From e05d074d5458bc3cce5519227c9971d4ffa09280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 3 Oct 2013 15:36:05 -0300 Subject: Start Soledad only if Mail is enabled for the current provider Also update enabled_services in mainwindow right after login to have an updated list of services to launch from that point on. --- src/leap/bitmask/gui/mail_status.py | 11 ++++++++++- src/leap/bitmask/gui/mainwindow.py | 15 ++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py index 770d991f..ab9052d7 100644 --- a/src/leap/bitmask/gui/mail_status.py +++ b/src/leap/bitmask/gui/mail_status.py @@ -199,7 +199,8 @@ class MailStatusWidget(QtGui.QWidget): :param status: the status text to display :type status: unicode - :param ready: 2 or >2 if mx is ready, 0 if stopped, 1 if it's starting. + :param ready: 2 or >2 if mx is ready, 0 if stopped, 1 if it's + starting, < 0 if disabled. :type ready: int """ self.ui.lblMailStatus.setText(status) @@ -219,6 +220,8 @@ class MailStatusWidget(QtGui.QWidget): icon = self.CONNECTED_ICON self._mx_status = self.tr('ON') tray_status = self.tr('Mail is ON') + elif ready < 0: + tray_status = self.tr("Mail is disabled") self.ui.lblMailStatusIcon.setPixmap(icon) self._action_mail_status.setText(tray_status) @@ -392,6 +395,12 @@ class MailStatusWidget(QtGui.QWidget): self._set_mail_status(self.tr("About to start, please wait..."), ready=1) + def set_disabled(self): + """ + Displays the correct UI for disabled mail. + """ + self._set_mail_status(self.tr("Disabled"), -1) + def stopped_mail(self): """ Displayes the correct UI for the stopped state. diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 7129b670..dd4a341e 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -950,17 +950,22 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.logged_in() + self._enabled_services = self._settings.get_enabled_services( + self._provider_config.get_domain()) + # TODO separate UI from logic. # TODO soledad should check if we want to run only over EIP. if self._provider_config.provides_mx() and \ self._enabled_services.count(self.MX_SERVICE) > 0: self._mail_status.about_to_start() - self._soledad_bootstrapper.run_soledad_setup_checks( - self._provider_config, - self._login_widget.get_user(), - self._login_widget.get_password(), - download_if_needed=True) + self._soledad_bootstrapper.run_soledad_setup_checks( + self._provider_config, + self._login_widget.get_user(), + self._login_widget.get_password(), + download_if_needed=True) + else: + self._mail_status.set_disabled() self._download_eip_config() -- cgit v1.2.3 From 1f36c21ca532e39ef80b4a27f79bcad66ed335b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 3 Oct 2013 18:44:21 -0300 Subject: Allow window minization in OSX --- src/leap/bitmask/gui/mainwindow.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index dd4a341e..79ff68c4 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -766,9 +766,10 @@ class MainWindow(QtGui.QMainWindow): """ Reimplements the changeEvent method to minimize to tray """ - if QtGui.QSystemTrayIcon.isSystemTrayAvailable() and \ - e.type() == QtCore.QEvent.WindowStateChange and \ - self.isMinimized(): + if not IS_MAC and \ + QtGui.QSystemTrayIcon.isSystemTrayAvailable() and \ + e.type() == QtCore.QEvent.WindowStateChange and \ + self.isMinimized(): self._toggle_visible() e.accept() return -- cgit v1.2.3 From effec111c39110f5057f49224f07bdd0730e7862 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Thu, 3 Oct 2013 17:19:50 -0300 Subject: Add preference to set autostart EIP option. --- src/leap/bitmask/gui/eip_preferenceswindow.py | 43 ++++++++- src/leap/bitmask/gui/ui/eippreferences.ui | 125 ++++++++++++++++++++++---- 2 files changed, 149 insertions(+), 19 deletions(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/eip_preferenceswindow.py b/src/leap/bitmask/gui/eip_preferenceswindow.py index 0e6e8dda..9f8c4ff4 100644 --- a/src/leap/bitmask/gui/eip_preferenceswindow.py +++ b/src/leap/bitmask/gui/eip_preferenceswindow.py @@ -50,6 +50,7 @@ class EIPPreferencesWindow(QtGui.QDialog): self.ui = Ui_EIPPreferences() self.ui.setupUi(self) self.ui.lblProvidersGatewayStatus.setVisible(False) + self.ui.lblAutoStartEIPStatus.setVisible(False) # Connections self.ui.cbProvidersGateway.currentIndexChanged[unicode].connect( @@ -58,8 +59,40 @@ class EIPPreferencesWindow(QtGui.QDialog): self.ui.cbGateways.currentIndexChanged[unicode].connect( lambda x: self.ui.lblProvidersGatewayStatus.setVisible(False)) + self.ui.cbProvidersEIP.currentIndexChanged[unicode].connect( + lambda x: self.ui.lblAutoStartEIPStatus.setVisible(False)) + + self.ui.cbAutoStartEIP.toggled.connect( + lambda x: self.ui.lblAutoStartEIPStatus.setVisible(False)) + + self.ui.pbSaveAutoStartEIP.clicked.connect(self._save_auto_start_eip) + self._add_configured_providers() + # Load auto start EIP settings + self.ui.cbAutoStartEIP.setChecked(self._settings.get_autostart_eip()) + default_provider = self._settings.get_defaultprovider() + idx = self.ui.cbProvidersEIP.findText(default_provider) + self.ui.cbProvidersEIP.setCurrentIndex(idx) + + def _save_auto_start_eip(self): + """ + SLOT + TRIGGER: + self.ui.cbAutoStartEIP.toggled + + Saves the automatic start of EIP user preference. + """ + default_provider = self.ui.cbProvidersEIP.currentText() + enabled = self.ui.cbAutoStartEIP.isChecked() + + self._settings.set_autostart_eip(enabled) + self._settings.set_defaultprovider(default_provider) + + self.ui.lblAutoStartEIPStatus.show() + logger.debug('Auto start EIP saved: {0} {1}.'.format( + default_provider, enabled)) + def _set_providers_gateway_status(self, status, success=False, error=False): """ @@ -87,8 +120,16 @@ class EIPPreferencesWindow(QtGui.QDialog): Add the client's configured providers to the providers combo boxes. """ self.ui.cbProvidersGateway.clear() - for provider in self._settings.get_configured_providers(): + self.ui.cbProvidersEIP.clear() + providers = self._settings.get_configured_providers() + if not providers: + self.ui.gbAutomaticEIP.setEnabled(False) + self.ui.gbGatewaySelector.setEnabled(False) + return + + for provider in providers: self.ui.cbProvidersGateway.addItem(provider) + self.ui.cbProvidersEIP.addItem(provider) def _save_selected_gateway(self, provider): """ diff --git a/src/leap/bitmask/gui/ui/eippreferences.ui b/src/leap/bitmask/gui/ui/eippreferences.ui index e9bc203d..9493d330 100644 --- a/src/leap/bitmask/gui/ui/eippreferences.ui +++ b/src/leap/bitmask/gui/ui/eippreferences.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 170 + 398 + 262 @@ -17,8 +17,8 @@ :/images/mask-icon.png:/images/mask-icon.png - - + + true @@ -30,6 +30,16 @@ false + + + + &Select provider: + + + cbProvidersGateway + + + @@ -39,13 +49,20 @@ - - + + - &Select provider: + Save this provider settings - - cbProvidersGateway + + + + + + < Providers Gateway Status > + + + Qt::AlignCenter @@ -65,30 +82,102 @@ - - + + + + + + + Automatic EIP start + + + + + + Qt::LeftToRight + + + + + + QFrame::NoFrame + + + QFrame::Plain + - Save this provider settings + <font color='green'><b>Automatic EIP start saved!</b></font> + + + Qt::AlignCenter - - + + - < Providers Gateway Status > + Save auto start setting - - Qt::AlignCenter + + + + + + Qt::LeftToRight + + + Enable Automatic start of EIP + + + true + + + + + <Select provider> + + + + + cbAutoStartEIP + lblAutoStartEIPStatus + pbSaveAutoStartEIP + cbProvidersEIP + + cbAutoStartEIP + cbProvidersEIP + pbSaveAutoStartEIP + cbProvidersGateway + cbGateways + pbSaveGateway + - + + + cbAutoStartEIP + toggled(bool) + cbProvidersEIP + setEnabled(bool) + + + 180 + 53 + + + 238 + 53 + + + + -- cgit v1.2.3 From 90a731e8a5f7e8b44b5ad76262ff01fb98f8e18c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 3 Oct 2013 20:41:31 -0300 Subject: Properly stop the smtp daemon --- src/leap/bitmask/gui/mainwindow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/leap') diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 79ff68c4..84f09fd9 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -312,6 +312,7 @@ class MainWindow(QtGui.QMainWindow): self._soledad_ready = False self._keymanager = None self._smtp_service = None + self._smtp_port = None self._imap_service = None self._login_defer = None @@ -1104,7 +1105,7 @@ class MainWindow(QtGui.QMainWindow): # the specific default. from leap.mail.smtp import setup_smtp_relay - self._smtp_service = setup_smtp_relay( + self._smtp_service, self._smtp_port = setup_smtp_relay( port=2013, keymanager=self._keymanager, smtp_host=host, @@ -1124,6 +1125,7 @@ class MainWindow(QtGui.QMainWindow): # but in the imap case we are just stopping the fetcher. if self._smtp_service is not None: logger.debug('Stopping smtp service.') + self._smtp_port.stopListening() self._smtp_service.doStop() ################################################################### -- cgit v1.2.3