diff options
Diffstat (limited to 'src')
31 files changed, 2365 insertions, 1358 deletions
| diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index 02e27123..e413ab4c 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -76,6 +76,16 @@ def sigint_handler(*args, **kwargs):      mainwindow = args[0]      mainwindow.quit() +def sigterm_handler(*args, **kwargs): +    """ +    Signal handler for SIGTERM. +    This handler is actually passed to twisted reactor +    """ +    logger = kwargs.get('logger', None) +    if logger: +        logger.debug("SIGTERM catched. shutting down...") +    mainwindow = args[0] +    mainwindow.quit()  def add_logger_handlers(debug=False, logfile=None, replace_stdout=True):      """ @@ -200,7 +210,7 @@ def main():      debug = opts.debug      logfile = opts.log_file      mail_logfile = opts.mail_log_file -    openvpn_verb = opts.openvpn_verb +    start_hidden = opts.start_hidden      #############################################################      # Given how paths and bundling works, we need to delay the imports @@ -213,6 +223,8 @@ def main():      flags.MAIL_LOGFILE = mail_logfile      flags.APP_VERSION_CHECK = opts.app_version_check      flags.API_VERSION_CHECK = opts.api_version_check +    flags.OPENVPN_VERBOSITY = opts.openvpn_verb +    flags.SKIP_WIZARD_CHECKS = opts.skip_wizard_checks      flags.CA_CERT_FILE = opts.ca_cert_file @@ -306,12 +318,15 @@ def main():      window = MainWindow(          lambda: twisted_main.quit(app), -        openvpn_verb=openvpn_verb, -        bypass_checks=bypass_checks) +        bypass_checks=bypass_checks, +        start_hidden=start_hidden)      sigint_window = partial(sigint_handler, window, logger=logger)      signal.signal(signal.SIGINT, sigint_window) +    # callable used in addSystemEventTrigger to handle SIGTERM +    sigterm_window = partial(sigterm_handler, window, logger=logger) +      if IS_MAC:          window.raise_() @@ -322,6 +337,12 @@ def main():      l = LoopingCall(QtCore.QCoreApplication.processEvents, 0, 10)      l.start(0.01) + +    # SIGTERM can't be handled the same way SIGINT is, since it's +    # caught by twisted. See _handleSignals method in +    # twisted/internet/base.py#L1150. So, addSystemEventTrigger +    # reactor's method is used. +    reactor.addSystemEventTrigger('before', 'shutdown', sigterm_window)      reactor.run()  if __name__ == "__main__": diff --git a/src/leap/bitmask/backend.py b/src/leap/bitmask/backend.py index 45ea451c..a2df465d 100644 --- a/src/leap/bitmask/backend.py +++ b/src/leap/bitmask/backend.py @@ -17,11 +17,15 @@  """  Backend for everything  """ +import commands  import logging +import os +import time  from functools import partial  from Queue import Queue, Empty +from twisted.internet import reactor  from twisted.internet import threads, defer  from twisted.internet.task import LoopingCall  from twisted.python import log @@ -29,9 +33,19 @@ from twisted.python import log  import zope.interface  from leap.bitmask.config.providerconfig import ProviderConfig +from leap.bitmask.crypto.srpauth import SRPAuth  from leap.bitmask.crypto.srpregister import SRPRegister +from leap.bitmask.platform_init import IS_LINUX  from leap.bitmask.provider import get_provider_path  from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper +from leap.bitmask.services.eip import eipconfig +from leap.bitmask.services.eip import get_openvpn_management +from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper + +from leap.bitmask.services.eip import vpnlauncher, vpnprocess +from leap.bitmask.services.eip import linuxvpnlauncher, darwinvpnlauncher + +from leap.common import certs as leap_certs  # Frontend side  from PySide import QtCore @@ -39,6 +53,26 @@ from PySide import QtCore  logger = logging.getLogger(__name__) +def get_provider_config(config, domain): +    """ +    Return the ProviderConfig object for the given domain. +    If it is already loaded in `config`, then don't reload. + +    :param config: a ProviderConfig object +    :type conig: ProviderConfig +    :param domain: the domain which config is required. +    :type domain: unicode + +    :returns: True if the config was loaded successfully, False otherwise. +    :rtype: bool +    """ +    # TODO: see ProviderConfig.get_provider_config +    if (not config.loaded() or config.get_domain() != domain): +        config.load(get_provider_path(domain)) + +    return config.loaded() + +  class ILEAPComponent(zope.interface.Interface):      """      Interface that every component for the backend should comply to @@ -54,7 +88,7 @@ class ILEAPService(ILEAPComponent):      def start(self):          """ -        Starts the service. +        Start the service.          """          pass @@ -66,13 +100,13 @@ class ILEAPService(ILEAPComponent):      def terminate(self):          """ -        Terminates the service, not necessarily in a nice way. +        Terminate the service, not necessarily in a nice way.          """          pass      def status(self):          """ -        Returns a json object with the current status for the service. +        Return a json object with the current status for the service.          :rtype: object (list, str, dict)          """ @@ -83,7 +117,7 @@ class ILEAPService(ILEAPComponent):      def set_configs(self, keyval):          """ -        Sets the config parameters for this Service. +        Set the config parameters for this Service.          :param keyval: values to configure          :type keyval: dict, {str: str} @@ -92,7 +126,7 @@ class ILEAPService(ILEAPComponent):      def get_configs(self, keys):          """ -        Returns the configuration values for the list of keys. +        Return the configuration values for the list of keys.          :param keys: keys to retrieve          :type keys: list of str @@ -109,8 +143,6 @@ class Provider(object):      zope.interface.implements(ILEAPComponent) -    PROBLEM_SIGNAL = "prov_problem_with_provider" -      def __init__(self, signaler=None, bypass_checks=False):          """          Constructor for the Provider component @@ -123,7 +155,6 @@ class Provider(object):                                certificates at bootstrap          :type bypass_checks: bool          """ -        object.__init__(self)          self.key = "provider"          self._provider_bootstrapper = ProviderBootstrapper(signaler,                                                             bypass_checks) @@ -132,7 +163,7 @@ class Provider(object):      def setup_provider(self, provider):          """ -        Initiates the setup for a provider +        Initiate the setup for a provider          :param provider: URL for the provider          :type provider: unicode @@ -166,19 +197,15 @@ class Provider(object):          """          d = None -        # If there's no loaded provider or -        # we want to connect to other provider... -        if (not self._provider_config.loaded() or -                self._provider_config.get_domain() != provider): -            self._provider_config.load(get_provider_path(provider)) - -        if self._provider_config.loaded(): +        config = self._provider_config +        if get_provider_config(config, provider):              d = self._provider_bootstrapper.run_provider_setup_checks(                  self._provider_config,                  download_if_needed=True)          else:              if self._signaler is not None: -                self._signaler.signal(self.PROBLEM_SIGNAL) +                self._signaler.signal( +                    self._signaler.PROV_PROBLEM_WITH_PROVIDER_KEY)              logger.error("Could not load provider configuration.")              self._login_widget.set_enabled(True) @@ -202,10 +229,8 @@ class Register(object):                           back to the frontend          :type signaler: Signaler          """ -        object.__init__(self)          self.key = "register"          self._signaler = signaler -        self._provider_config = ProviderConfig()      def register_user(self, domain, username, password):          """ @@ -221,23 +246,406 @@ class Register(object):          :returns: the defer for the operation running in a thread.          :rtype: twisted.internet.defer.Deferred          """ -        # If there's no loaded provider or -        # we want to connect to other provider... -        if (not self._provider_config.loaded() or -                self._provider_config.get_domain() != domain): -            self._provider_config.load(get_provider_path(domain)) - -        if self._provider_config.loaded(): +        config = ProviderConfig() +        if get_provider_config(config, domain):              srpregister = SRPRegister(signaler=self._signaler, -                                      provider_config=self._provider_config) +                                      provider_config=config)              return threads.deferToThread(                  partial(srpregister.register_user, username, password))          else:              if self._signaler is not None: -                self._signaler.signal(self._signaler.srp_registration_failed) +                self._signaler.signal(self._signaler.SRP_REGISTRATION_FAILED)              logger.error("Could not load provider configuration.") +class EIP(object): +    """ +    Interfaces with setup and launch of EIP +    """ + +    zope.interface.implements(ILEAPService) + +    def __init__(self, signaler=None): +        """ +        Constructor for the EIP component + +        :param signaler: Object in charge of handling communication +                         back to the frontend +        :type signaler: Signaler +        """ +        self.key = "eip" +        self._signaler = signaler +        self._eip_bootstrapper = EIPBootstrapper(signaler) +        self._eip_setup_defer = None +        self._provider_config = ProviderConfig() + +        self._vpn = vpnprocess.VPN(signaler=signaler) + +    def setup_eip(self, domain, skip_network=False): +        """ +        Initiate the setup for a provider + +        :param domain: URL for the provider +        :type domain: unicode +        :param skip_network: Whether checks that involve network should be done +                             or not +        :type skip_network: bool + +        :returns: the defer for the operation running in a thread. +        :rtype: twisted.internet.defer.Deferred +        """ +        config = self._provider_config +        if get_provider_config(config, domain): +            if skip_network: +                return defer.Deferred() +            eb = self._eip_bootstrapper +            d = eb.run_eip_setup_checks(self._provider_config, +                                        download_if_needed=True) +            self._eip_setup_defer = d +            return d +        else: +            raise Exception("No provider setup loaded") + +    def cancel_setup_eip(self): +        """ +        Cancel the ongoing setup eip defer (if any). +        """ +        d = self._eip_setup_defer +        if d is not None: +            d.cancel() + +    def _start_eip(self): +        """ +        Start EIP +        """ +        provider_config = self._provider_config +        eip_config = eipconfig.EIPConfig() +        domain = provider_config.get_domain() + +        loaded = eipconfig.load_eipconfig_if_needed( +            provider_config, eip_config, domain) + +        if not loaded: +            if self._signaler is not None: +                self._signaler.signal(self._signaler.EIP_CONNECTION_ABORTED) +            logger.error("Tried to start EIP but cannot find any " +                         "available provider!") +            return + +        host, port = get_openvpn_management() +        self._vpn.start(eipconfig=eip_config, +                        providerconfig=provider_config, +                        socket_host=host, socket_port=port) + +    def start(self): +        """ +        Start the service. +        """ +        signaler = self._signaler + +        if not self._provider_config.loaded(): +            # This means that the user didn't call setup_eip first. +            self._signaler.signal(signaler.BACKEND_BAD_CALL, "EIP.start(), " +                                  "no provider loaded") +            return + +        try: +            self._start_eip() +        except vpnprocess.OpenVPNAlreadyRunning: +            signaler.signal(signaler.EIP_OPENVPN_ALREADY_RUNNING) +        except vpnprocess.AlienOpenVPNAlreadyRunning: +            signaler.signal(signaler.EIP_ALIEN_OPENVPN_ALREADY_RUNNING) +        except vpnlauncher.OpenVPNNotFoundException: +            signaler.signal(signaler.EIP_OPENVPN_NOT_FOUND_ERROR) +        except vpnlauncher.VPNLauncherException: +            # TODO: this seems to be used for 'gateway not found' only. +            #       see vpnlauncher.py +            signaler.signal(signaler.EIP_VPN_LAUNCHER_EXCEPTION) +        except linuxvpnlauncher.EIPNoPolkitAuthAgentAvailable: +            signaler.signal(signaler.EIP_NO_POLKIT_AGENT_ERROR) +        except linuxvpnlauncher.EIPNoPkexecAvailable: +            signaler.signal(signaler.EIP_NO_PKEXEC_ERROR) +        except darwinvpnlauncher.EIPNoTunKextLoaded: +            signaler.signal(signaler.EIP_NO_TUN_KEXT_ERROR) +        except Exception as e: +            logger.error("Unexpected problem: {0!r}".format(e)) +        else: +            # TODO: are we connected here? +            signaler.signal(signaler.EIP_CONNECTED) + +    def stop(self, shutdown=False): +        """ +        Stop the service. +        """ +        self._vpn.terminate(shutdown) +        if IS_LINUX: +            self._wait_for_firewall_down() + +    def _wait_for_firewall_down(self): +        """ +        Wait for the firewall to come down. +        """ +        # Due to how we delay the resolvconf action in linux. +        # XXX this *has* to wait for a reasonable lapse, since we have some +        # delay in vpn.terminate. +        # For a better solution it should be signaled from backend that +        # everything is clear to proceed, or a timeout happened. +        MAX_FW_WAIT_RETRIES = 25 +        FW_WAIT_STEP = 0.5 + +        retry = 0 + +        fw_up_cmd = "pkexec /usr/sbin/bitmask-root firewall isup" +        fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256 + +        while retry < MAX_FW_WAIT_RETRIES: +            if fw_is_down(): +                return +            else: +                time.sleep(FW_WAIT_STEP) +                retry += 1 +        logger.warning("After waiting, firewall is not down... " +                       "You might experience lack of connectivity") + +    def terminate(self): +        """ +        Terminate the service, not necessarily in a nice way. +        """ +        self._vpn.killit() + +    def status(self): +        """ +        Return a json object with the current status for the service. + +        :rtype: object (list, str, dict) +        """ +        # XXX: Use a namedtuple or a specific object instead of a json +        # object, since parsing it will be problematic otherwise. +        # It has to be something easily serializable though. +        pass + +    def _provider_is_initialized(self, domain): +        """ +        Return whether the given domain is initialized or not. + +        :param domain: the domain to check +        :type domain: str + +        :returns: True if is initialized, False otherwise. +        :rtype: bool +        """ +        eipconfig_path = eipconfig.get_eipconfig_path(domain, relative=False) +        if os.path.isfile(eipconfig_path): +            return True +        else: +            return False + +    def get_initialized_providers(self, domains): +        """ +        Signal a list of the given domains and if they are initialized or not. + +        :param domains: the list of domains to check. +        :type domain: list of str + +        Signals: +            eip_get_initialized_providers -> list of tuple(unicode, bool) +        """ +        filtered_domains = [] +        for domain in domains: +            is_initialized = self._provider_is_initialized(domain) +            filtered_domains.append((domain, is_initialized)) + +        if self._signaler is not None: +            self._signaler.signal(self._signaler.EIP_GET_INITIALIZED_PROVIDERS, +                                  filtered_domains) + +    def get_gateways_list(self, domain): +        """ +        Signal a list of gateways for the given provider. + +        :param domain: the domain to get the gateways. +        :type domain: str + +        Signals: +            eip_get_gateways_list -> list of unicode +            eip_get_gateways_list_error +            eip_uninitialized_provider +        """ +        if not self._provider_is_initialized(domain): +            if self._signaler is not None: +                self._signaler.signal( +                    self._signaler.EIP_UNINITIALIZED_PROVIDER) +            return + +        eip_config = eipconfig.EIPConfig() +        provider_config = ProviderConfig.get_provider_config(domain) + +        api_version = provider_config.get_api_version() +        eip_config.set_api_version(api_version) +        eip_loaded = eip_config.load(eipconfig.get_eipconfig_path(domain)) + +        # check for other problems +        if not eip_loaded or provider_config is None: +            if self._signaler is not None: +                self._signaler.signal( +                    self._signaler.EIP_GET_GATEWAYS_LIST_ERROR) +            return + +        gateways = eipconfig.VPNGatewaySelector(eip_config).get_gateways_list() + +        if self._signaler is not None: +            self._signaler.signal( +                self._signaler.EIP_GET_GATEWAYS_LIST, gateways) + +    def can_start(self, domain): +        """ +        Signal whether it has everything that is needed to run EIP or not + +        :param domain: the domain for the provider to check +        :type domain: str + +        Signals: +            eip_can_start +            eip_cannot_start +        """ +        try: +            eip_config = eipconfig.EIPConfig() +            provider_config = ProviderConfig.get_provider_config(domain) + +            api_version = provider_config.get_api_version() +            eip_config.set_api_version(api_version) +            eip_loaded = eip_config.load(eipconfig.get_eipconfig_path(domain)) + +            # check for other problems +            if not eip_loaded or provider_config is None: +                raise Exception("Cannot load provider and eip config, cannot " +                                "autostart") + +            client_cert_path = eip_config.\ +                get_client_cert_path(provider_config, about_to_download=False) + +            if leap_certs.should_redownload(client_cert_path): +                raise Exception("The client should redownload the certificate," +                                " cannot autostart") + +            if not os.path.isfile(client_cert_path): +                raise Exception("Can't find the certificate, cannot autostart") + +            if self._signaler is not None: +                self._signaler.signal(self._signaler.EIP_CAN_START) +        except Exception as e: +            logger.exception(e) +            if self._signaler is not None: +                self._signaler.signal(self._signaler.EIP_CANNOT_START) + + +class Authenticate(object): +    """ +    Interfaces with setup and bootstrapping operations for a provider +    """ + +    zope.interface.implements(ILEAPComponent) + +    def __init__(self, signaler=None): +        """ +        Constructor for the Authenticate component + +        :param signaler: Object in charge of handling communication +                         back to the frontend +        :type signaler: Signaler +        """ +        self.key = "authenticate" +        self._signaler = signaler +        self._srp_auth = SRPAuth(ProviderConfig(), self._signaler) + +    def login(self, domain, username, password): +        """ +        Execute the whole authentication process for a user + +        :param domain: the domain where we need to authenticate. +        :type domain: unicode +        :param username: username for this session +        :type username: str +        :param password: password for this user +        :type password: str + +        :returns: the defer for the operation running in a thread. +        :rtype: twisted.internet.defer.Deferred +        """ +        config = ProviderConfig() +        if get_provider_config(config, domain): +            self._srp_auth = SRPAuth(config, self._signaler) +            self._login_defer = self._srp_auth.authenticate(username, password) +            return self._login_defer +        else: +            if self._signaler is not None: +                self._signaler.signal(self._signaler.SRP_AUTH_ERROR) +            logger.error("Could not load provider configuration.") + +    def cancel_login(self): +        """ +        Cancel the ongoing login defer (if any). +        """ +        d = self._login_defer +        if d is not None: +            d.cancel() + +    def change_password(self, current_password, new_password): +        """ +        Change the user's password. + +        :param current_password: the current password of the user. +        :type current_password: str +        :param new_password: the new password for the user. +        :type new_password: str + +        :returns: a defer to interact with. +        :rtype: twisted.internet.defer.Deferred +        """ +        if not self._is_logged_in(): +            if self._signaler is not None: +                self._signaler.signal(self._signaler.SRP_NOT_LOGGED_IN_ERROR) +            return + +        return self._srp_auth.change_password(current_password, new_password) + +    def logout(self): +        """ +        Log out the current session. +        Expects a session_id to exists, might raise AssertionError +        """ +        if not self._is_logged_in(): +            if self._signaler is not None: +                self._signaler.signal(self._signaler.SRP_NOT_LOGGED_IN_ERROR) +            return + +        self._srp_auth.logout() + +    def _is_logged_in(self): +        """ +        Return whether the user is logged in or not. + +        :rtype: bool +        """ +        return (self._srp_auth is not None and +                self._srp_auth.is_authenticated()) + +    def get_logged_in_status(self): +        """ +        Signal if the user is currently logged in or not. +        """ +        if self._signaler is None: +            return + +        signal = None +        if self._is_logged_in(): +            signal = self._signaler.SRP_STATUS_LOGGED_IN +        else: +            signal = self._signaler.SRP_STATUS_NOT_LOGGED_IN + +        self._signaler.signal(signal) + +  class Signaler(QtCore.QObject):      """      Signaler object, handles converting string commands to Qt signals. @@ -269,6 +677,65 @@ class Signaler(QtCore.QObject):      srp_registration_failed = QtCore.Signal(object)      srp_registration_taken = QtCore.Signal(object) +    # Signals for EIP bootstrapping +    eip_config_ready = QtCore.Signal(object) +    eip_client_certificate_ready = QtCore.Signal(object) + +    eip_cancelled_setup = QtCore.Signal(object) + +    # Signals for SRPAuth +    srp_auth_ok = QtCore.Signal(object) +    srp_auth_error = QtCore.Signal(object) +    srp_auth_server_error = QtCore.Signal(object) +    srp_auth_connection_error = QtCore.Signal(object) +    srp_auth_bad_user_or_password = QtCore.Signal(object) +    srp_logout_ok = QtCore.Signal(object) +    srp_logout_error = QtCore.Signal(object) +    srp_password_change_ok = QtCore.Signal(object) +    srp_password_change_error = QtCore.Signal(object) +    srp_password_change_badpw = QtCore.Signal(object) +    srp_not_logged_in_error = QtCore.Signal(object) +    srp_status_logged_in = QtCore.Signal(object) +    srp_status_not_logged_in = QtCore.Signal(object) + +    # Signals for EIP +    eip_connected = QtCore.Signal(object) +    eip_disconnected = QtCore.Signal(object) +    eip_connection_died = QtCore.Signal(object) +    eip_connection_aborted = QtCore.Signal(object) + +    # EIP problems +    eip_no_polkit_agent_error = QtCore.Signal(object) +    eip_no_tun_kext_error = QtCore.Signal(object) +    eip_no_pkexec_error = QtCore.Signal(object) +    eip_openvpn_not_found_error = QtCore.Signal(object) +    eip_openvpn_already_running = QtCore.Signal(object) +    eip_alien_openvpn_already_running = QtCore.Signal(object) +    eip_vpn_launcher_exception = QtCore.Signal(object) + +    eip_get_gateways_list = QtCore.Signal(object) +    eip_get_gateways_list_error = QtCore.Signal(object) +    eip_uninitialized_provider = QtCore.Signal(object) +    eip_get_initialized_providers = QtCore.Signal(object) + +    # signals from parsing openvpn output +    eip_network_unreachable = QtCore.Signal(object) +    eip_process_restart_tls = QtCore.Signal(object) +    eip_process_restart_ping = QtCore.Signal(object) + +    # signals from vpnprocess.py +    eip_state_changed = QtCore.Signal(dict) +    eip_status_changed = QtCore.Signal(dict) +    eip_process_finished = QtCore.Signal(int) + +    # signals whether the needed files to start EIP exist or not +    eip_can_start = QtCore.Signal(object) +    eip_cannot_start = QtCore.Signal(object) + +    # This signal is used to warn the backend user that is doing something +    # wrong +    backend_bad_call = QtCore.Signal(object) +      ####################      # These will exist both in the backend AND the front end.      # The frontend might choose to not "interpret" all the signals @@ -288,6 +755,53 @@ class Signaler(QtCore.QObject):      SRP_REGISTRATION_FINISHED = "srp_registration_finished"      SRP_REGISTRATION_FAILED = "srp_registration_failed"      SRP_REGISTRATION_TAKEN = "srp_registration_taken" +    SRP_AUTH_OK = "srp_auth_ok" +    SRP_AUTH_ERROR = "srp_auth_error" +    SRP_AUTH_SERVER_ERROR = "srp_auth_server_error" +    SRP_AUTH_CONNECTION_ERROR = "srp_auth_connection_error" +    SRP_AUTH_BAD_USER_OR_PASSWORD = "srp_auth_bad_user_or_password" +    SRP_LOGOUT_OK = "srp_logout_ok" +    SRP_LOGOUT_ERROR = "srp_logout_error" +    SRP_PASSWORD_CHANGE_OK = "srp_password_change_ok" +    SRP_PASSWORD_CHANGE_ERROR = "srp_password_change_error" +    SRP_PASSWORD_CHANGE_BADPW = "srp_password_change_badpw" +    SRP_NOT_LOGGED_IN_ERROR = "srp_not_logged_in_error" +    SRP_STATUS_LOGGED_IN = "srp_status_logged_in" +    SRP_STATUS_NOT_LOGGED_IN = "srp_status_not_logged_in" + +    EIP_CONFIG_READY = "eip_config_ready" +    EIP_CLIENT_CERTIFICATE_READY = "eip_client_certificate_ready" +    EIP_CANCELLED_SETUP = "eip_cancelled_setup" + +    EIP_CONNECTED = "eip_connected" +    EIP_DISCONNECTED = "eip_disconnected" +    EIP_CONNECTION_DIED = "eip_connection_died" +    EIP_CONNECTION_ABORTED = "eip_connection_aborted" +    EIP_NO_POLKIT_AGENT_ERROR = "eip_no_polkit_agent_error" +    EIP_NO_TUN_KEXT_ERROR = "eip_no_tun_kext_error" +    EIP_NO_PKEXEC_ERROR = "eip_no_pkexec_error" +    EIP_OPENVPN_NOT_FOUND_ERROR = "eip_openvpn_not_found_error" +    EIP_OPENVPN_ALREADY_RUNNING = "eip_openvpn_already_running" +    EIP_ALIEN_OPENVPN_ALREADY_RUNNING = "eip_alien_openvpn_already_running" +    EIP_VPN_LAUNCHER_EXCEPTION = "eip_vpn_launcher_exception" + +    EIP_GET_GATEWAYS_LIST = "eip_get_gateways_list" +    EIP_GET_GATEWAYS_LIST_ERROR = "eip_get_gateways_list_error" +    EIP_UNINITIALIZED_PROVIDER = "eip_uninitialized_provider" +    EIP_GET_INITIALIZED_PROVIDERS = "eip_get_initialized_providers" + +    EIP_NETWORK_UNREACHABLE = "eip_network_unreachable" +    EIP_PROCESS_RESTART_TLS = "eip_process_restart_tls" +    EIP_PROCESS_RESTART_PING = "eip_process_restart_ping" + +    EIP_STATE_CHANGED = "eip_state_changed" +    EIP_STATUS_CHANGED = "eip_status_changed" +    EIP_PROCESS_FINISHED = "eip_process_finished" + +    EIP_CAN_START = "eip_can_start" +    EIP_CANNOT_START = "eip_cannot_start" + +    BACKEND_BAD_CALL = "backend_bad_call"      def __init__(self):          """ @@ -311,6 +825,54 @@ class Signaler(QtCore.QObject):              self.SRP_REGISTRATION_FINISHED,              self.SRP_REGISTRATION_FAILED,              self.SRP_REGISTRATION_TAKEN, + +            self.EIP_CONFIG_READY, +            self.EIP_CLIENT_CERTIFICATE_READY, +            self.EIP_CANCELLED_SETUP, + +            self.EIP_CONNECTED, +            self.EIP_DISCONNECTED, +            self.EIP_CONNECTION_DIED, +            self.EIP_CONNECTION_ABORTED, +            self.EIP_NO_POLKIT_AGENT_ERROR, +            self.EIP_NO_TUN_KEXT_ERROR, +            self.EIP_NO_PKEXEC_ERROR, +            self.EIP_OPENVPN_NOT_FOUND_ERROR, +            self.EIP_OPENVPN_ALREADY_RUNNING, +            self.EIP_ALIEN_OPENVPN_ALREADY_RUNNING, +            self.EIP_VPN_LAUNCHER_EXCEPTION, + +            self.EIP_GET_GATEWAYS_LIST, +            self.EIP_GET_GATEWAYS_LIST_ERROR, +            self.EIP_UNINITIALIZED_PROVIDER, +            self.EIP_GET_INITIALIZED_PROVIDERS, + +            self.EIP_NETWORK_UNREACHABLE, +            self.EIP_PROCESS_RESTART_TLS, +            self.EIP_PROCESS_RESTART_PING, + +            self.EIP_STATE_CHANGED, +            self.EIP_STATUS_CHANGED, +            self.EIP_PROCESS_FINISHED, + +            self.EIP_CAN_START, +            self.EIP_CANNOT_START, + +            self.SRP_AUTH_OK, +            self.SRP_AUTH_ERROR, +            self.SRP_AUTH_SERVER_ERROR, +            self.SRP_AUTH_CONNECTION_ERROR, +            self.SRP_AUTH_BAD_USER_OR_PASSWORD, +            self.SRP_LOGOUT_OK, +            self.SRP_LOGOUT_ERROR, +            self.SRP_PASSWORD_CHANGE_OK, +            self.SRP_PASSWORD_CHANGE_ERROR, +            self.SRP_PASSWORD_CHANGE_BADPW, +            self.SRP_NOT_LOGGED_IN_ERROR, +            self.SRP_STATUS_LOGGED_IN, +            self.SRP_STATUS_NOT_LOGGED_IN, + +            self.BACKEND_BAD_CALL,          ]          for sig in signals: @@ -332,7 +894,6 @@ class Signaler(QtCore.QObject):          # Right now it emits Qt signals. The backend version of this          # will do zmq.send_multipart, and the frontend version will be          # similar to this -        log.msg("Signaling %s :: %s" % (key, data))          # for some reason emitting 'None' gives a segmentation fault.          if data is None: @@ -341,7 +902,7 @@ class Signaler(QtCore.QObject):          try:              self._signals[key].emit(data)          except KeyError: -            log.msg("Unknown key for signal %s!" % (key,)) +            log.err("Unknown key for signal %s!" % (key,))  class Backend(object): @@ -356,8 +917,6 @@ class Backend(object):          """          Constructor for the backend.          """ -        object.__init__(self) -          # Components map for the commands received          self._components = {} @@ -370,6 +929,8 @@ class Backend(object):          # Component registration          self._register(Provider(self._signaler, bypass_checks))          self._register(Register(self._signaler)) +        self._register(Authenticate(self._signaler)) +        self._register(EIP(self._signaler))          # We have a looping call on a thread executing all the          # commands in queue. Right now this queue is an actual Queue @@ -398,9 +959,17 @@ class Backend(object):          """          Stops the looping call and tries to cancel all the defers.          """ +        reactor.callLater(2, self._stop) + +    def _stop(self): +        """ +        Delayed stopping of worker. Called from `stop`. +        """          log.msg("Stopping worker...")          if self._lc.running:              self._lc.stop() +        else: +            logger.warning("Looping call is not running, cannot stop")          while len(self._ongoing_defers) > 0:              d = self._ongoing_defers.pop()              d.cancel() @@ -476,14 +1045,241 @@ class Backend(object):      # send_multipart and this backend class will be really simple.      def setup_provider(self, provider): +        """ +        Initiate the setup for a provider. + +        :param provider: URL for the provider +        :type provider: unicode + +        Signals: +            prov_unsupported_client +            prov_unsupported_api +            prov_name_resolution        -> { PASSED_KEY: bool, ERROR_KEY: str } +            prov_https_connection       -> { PASSED_KEY: bool, ERROR_KEY: str } +            prov_download_provider_info -> { PASSED_KEY: bool, ERROR_KEY: str } +        """          self._call_queue.put(("provider", "setup_provider", None, provider))      def cancel_setup_provider(self): +        """ +        Cancel the ongoing setup provider (if any). +        """          self._call_queue.put(("provider", "cancel_setup_provider", None))      def provider_bootstrap(self, provider): +        """ +        Second stage of bootstrapping for a provider. + +        :param provider: URL for the provider +        :type provider: unicode + +        Signals: +            prov_problem_with_provider +            prov_download_ca_cert      -> {PASSED_KEY: bool, ERROR_KEY: str} +            prov_check_ca_fingerprint  -> {PASSED_KEY: bool, ERROR_KEY: str} +            prov_check_api_certificate -> {PASSED_KEY: bool, ERROR_KEY: str} +        """          self._call_queue.put(("provider", "bootstrap", None, provider))      def register_user(self, provider, username, password): +        """ +        Register a user using the domain and password given as parameters. + +        :param domain: the domain we need to register the user. +        :type domain: unicode +        :param username: the user name +        :type username: unicode +        :param password: the password for the username +        :type password: unicode + +        Signals: +            srp_registration_finished +            srp_registration_taken +            srp_registration_failed +        """          self._call_queue.put(("register", "register_user", None, provider,                                username, password)) + +    def setup_eip(self, provider, skip_network=False): +        """ +        Initiate the setup for a provider + +        :param provider: URL for the provider +        :type provider: unicode +        :param skip_network: Whether checks that involve network should be done +                             or not +        :type skip_network: bool + +        Signals: +            eip_config_ready             -> {PASSED_KEY: bool, ERROR_KEY: str} +            eip_client_certificate_ready -> {PASSED_KEY: bool, ERROR_KEY: str} +            eip_cancelled_setup +        """ +        self._call_queue.put(("eip", "setup_eip", None, provider, +                              skip_network)) + +    def cancel_setup_eip(self): +        """ +        Cancel the ongoing setup EIP (if any). +        """ +        self._call_queue.put(("eip", "cancel_setup_eip", None)) + +    def start_eip(self): +        """ +        Start the EIP service. + +        Signals: +            backend_bad_call +            eip_alien_openvpn_already_running +            eip_connected +            eip_connection_aborted +            eip_network_unreachable +            eip_no_pkexec_error +            eip_no_polkit_agent_error +            eip_no_tun_kext_error +            eip_openvpn_already_running +            eip_openvpn_not_found_error +            eip_process_finished +            eip_process_restart_ping +            eip_process_restart_tls +            eip_state_changed -> str +            eip_status_changed -> tuple of str (download, upload) +            eip_vpn_launcher_exception +        """ +        self._call_queue.put(("eip", "start", None)) + +    def stop_eip(self, shutdown=False): +        """ +        Stop the EIP service. + +        :param shutdown: +        :type shutdown: bool +        """ +        self._call_queue.put(("eip", "stop", None, shutdown)) + +    def terminate_eip(self): +        """ +        Terminate the EIP service, not necessarily in a nice way. +        """ +        self._call_queue.put(("eip", "terminate", None)) + +    def eip_get_gateways_list(self, domain): +        """ +        Signal a list of gateways for the given provider. + +        :param domain: the domain to get the gateways. +        :type domain: str + +        # TODO discuss how to document the expected result object received of +        # the signal +        :signal type: list of str + +        Signals: +            eip_get_gateways_list -> list of unicode +            eip_get_gateways_list_error +            eip_uninitialized_provider +        """ +        self._call_queue.put(("eip", "get_gateways_list", None, domain)) + +    def eip_get_initialized_providers(self, domains): +        """ +        Signal a list of the given domains and if they are initialized or not. + +        :param domains: the list of domains to check. +        :type domain: list of str + +        Signals: +            eip_get_initialized_providers -> list of tuple(unicode, bool) + +        """ +        self._call_queue.put(("eip", "get_initialized_providers", +                              None, domains)) + +    def eip_can_start(self, domain): +        """ +        Signal whether it has everything that is needed to run EIP or not + +        :param domain: the domain for the provider to check +        :type domain: str + +        Signals: +            eip_can_start +            eip_cannot_start +        """ +        self._call_queue.put(("eip", "can_start", +                              None, domain)) + +    def login(self, provider, username, password): +        """ +        Execute the whole authentication process for a user + +        :param domain: the domain where we need to authenticate. +        :type domain: unicode +        :param username: username for this session +        :type username: str +        :param password: password for this user +        :type password: str + +        Signals: +            srp_auth_error +            srp_auth_ok +            srp_auth_bad_user_or_password +            srp_auth_server_error +            srp_auth_connection_error +            srp_auth_error +        """ +        self._call_queue.put(("authenticate", "login", None, provider, +                              username, password)) + +    def logout(self): +        """ +        Log out the current session. + +        Signals: +            srp_logout_ok +            srp_logout_error +            srp_not_logged_in_error +        """ +        self._call_queue.put(("authenticate", "logout", None)) + +    def cancel_login(self): +        """ +        Cancel the ongoing login (if any). +        """ +        self._call_queue.put(("authenticate", "cancel_login", None)) + +    def change_password(self, current_password, new_password): +        """ +        Change the user's password. + +        :param current_password: the current password of the user. +        :type current_password: str +        :param new_password: the new password for the user. +        :type new_password: str + +        Signals: +            srp_not_logged_in_error +            srp_password_change_ok +            srp_password_change_badpw +            srp_password_change_error +        """ +        self._call_queue.put(("authenticate", "change_password", None, +                              current_password, new_password)) + +    def get_logged_in_status(self): +        """ +        Signal if the user is currently logged in or not. + +        Signals: +            srp_status_logged_in +            srp_status_not_logged_in +        """ +        self._call_queue.put(("authenticate", "get_logged_in_status", None)) + +    ########################################################################### +    # XXX HACK: this section is meant to be a place to hold methods and +    # variables needed in the meantime while we migrate all to the backend. + +    def get_provider_config(self): +        provider_config = self._components["provider"]._provider_config +        return provider_config diff --git a/src/leap/bitmask/config/flags.py b/src/leap/bitmask/config/flags.py index 5d8bc9b3..6b70659d 100644 --- a/src/leap/bitmask/config/flags.py +++ b/src/leap/bitmask/config/flags.py @@ -46,7 +46,12 @@ API_VERSION_CHECK = True  # Used for skipping soledad bootstrapping/syncs.  OFFLINE = False -  # CA cert path  # used to allow self signed certs in requests that needs SSL  CA_CERT_FILE = None + +# OpenVPN verbosity level +OPENVPN_VERBOSITY = 1 + +# Skip the checks in the wizard, use for testing purposes only! +SKIP_WIZARD_CHECKS = False diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index 7cf7e55a..192a9d5c 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -17,6 +17,7 @@  import binascii  import logging +import threading  import sys  import requests @@ -28,8 +29,8 @@ from simplejson.decoder import JSONDecodeError  from functools import partial  from requests.adapters import HTTPAdapter -from PySide import QtCore  from twisted.internet import threads +from twisted.internet.defer import CancelledError  from leap.bitmask.config.leapsettings import LeapSettings  from leap.bitmask.util import request_helpers as reqhelper @@ -117,12 +118,12 @@ class SRPAuthNoSessionId(SRPAuthenticationError):      pass -class SRPAuth(QtCore.QObject): +class SRPAuth(object):      """      SRPAuth singleton      """ -    class __impl(QtCore.QObject): +    class __impl(object):          """          Implementation of the SRPAuth interface          """ @@ -135,19 +136,21 @@ class SRPAuth(QtCore.QObject):          USER_SALT_KEY = 'user[password_salt]'          AUTHORIZATION_KEY = "Authorization" -        def __init__(self, provider_config): +        def __init__(self, provider_config, signaler=None):              """              Constructor for SRPAuth implementation -            :param server: Server to which we will authenticate -            :type server: str +            :param provider_config: ProviderConfig needed to authenticate. +            :type provider_config: ProviderConfig +            :param signaler: Signaler object used to receive notifications +                            from the backend +            :type signaler: Signaler              """ -            QtCore.QObject.__init__(self) -              leap_assert(provider_config,                          "We need a provider config to authenticate")              self._provider_config = provider_config +            self._signaler = signaler              self._settings = LeapSettings()              # **************************************************** # @@ -162,11 +165,11 @@ class SRPAuth(QtCore.QObject):              self._reset_session()              self._session_id = None -            self._session_id_lock = QtCore.QMutex() +            self._session_id_lock = threading.Lock()              self._uuid = None -            self._uuid_lock = QtCore.QMutex() +            self._uuid_lock = threading.Lock()              self._token = None -            self._token_lock = QtCore.QMutex() +            self._token_lock = threading.Lock()              self._srp_user = None              self._srp_a = None @@ -448,7 +451,7 @@ class SRPAuth(QtCore.QObject):          def _threader(self, cb, res, *args, **kwargs):              return threads.deferToThread(cb, res, *args, **kwargs) -        def change_password(self, current_password, new_password): +        def _change_password(self, current_password, new_password):              """              Changes the password for the currently logged user if the current              password match. @@ -499,6 +502,43 @@ class SRPAuth(QtCore.QObject):              self._password = new_password +        def change_password(self, current_password, new_password): +            """ +            Changes the password for the currently logged user if the current +            password match. +            It requires to be authenticated. + +            :param current_password: the current password for the logged user. +            :type current_password: str +            :param new_password: the new password for the user +            :type new_password: str +            """ +            d = threads.deferToThread( +                self._change_password, current_password, new_password) +            d.addCallback(self._change_password_ok) +            d.addErrback(self._change_password_error) + +        def _change_password_ok(self, _): +            """ +            Password change callback. +            """ +            if self._signaler is not None: +                self._signaler.signal(self._signaler.SRP_PASSWORD_CHANGE_OK) + +        def _change_password_error(self, failure): +            """ +            Password change errback. +            """ +            logger.debug( +                "Error changing password. Failure: {0}".format(failure)) +            if self._signaler is None: +                return + +            if failure.check(SRPAuthBadUserOrPassword): +                self._signaler.signal(self._signaler.SRP_PASSWORD_CHANGE_BADPW) +            else: +                self._signaler.signal(self._signaler.SRP_PASSWORD_CHANGE_ERROR) +          def authenticate(self, username, password):              """              Executes the whole authentication process for a user @@ -539,8 +579,49 @@ class SRPAuth(QtCore.QObject):              d.addCallback(partial(self._threader,                                    self._verify_session)) +            d.addCallback(self._authenticate_ok) +            d.addErrback(self._authenticate_error)              return d +        def _authenticate_ok(self, _): +            """ +            Callback that notifies that the authentication was successful. + +            :param _: IGNORED, output from the previous callback (None) +            :type _: IGNORED +            """ +            logger.debug("Successful login!") +            self._signaler.signal(self._signaler.SRP_AUTH_OK) + +        def _authenticate_error(self, failure): +            """ +            Error handler for the srpauth.authenticate method. + +            :param failure: failure object that Twisted generates +            :type failure: twisted.python.failure.Failure +            """ +            logger.error("Error logging in, {0!r}".format(failure)) + +            signal = None +            if failure.check(CancelledError): +                logger.debug("Defer cancelled.") +                failure.trap(Exception) +                return + +            if self._signaler is None: +                return + +            if failure.check(SRPAuthBadUserOrPassword): +                signal = self._signaler.SRP_AUTH_BAD_USER_OR_PASSWORD +            elif failure.check(SRPAuthConnectionError): +                signal = self._signaler.SRP_AUTH_CONNECTION_ERROR +            elif failure.check(SRPAuthenticationError): +                signal = self._signaler.SRP_AUTH_SERVER_ERROR +            else: +                signal = self._signaler.SRP_AUTH_ERROR + +            self._signaler.signal(signal) +          def logout(self):              """              Logs out the current session. @@ -565,6 +646,8 @@ class SRPAuth(QtCore.QObject):              except Exception as e:                  logger.warning("Something went wrong with the logout: %r" %                                 (e,)) +                if self._signaler is not None: +                    self._signaler.signal(self._signaler.SRP_LOGOUT_ERROR)                  raise              else:                  self.set_session_id(None) @@ -573,51 +656,65 @@ class SRPAuth(QtCore.QObject):                  # Also reset the session                  self._session = self._fetcher.session()                  logger.debug("Successfully logged out.") +                if self._signaler is not None: +                    self._signaler.signal(self._signaler.SRP_LOGOUT_OK)          def set_session_id(self, session_id): -            QtCore.QMutexLocker(self._session_id_lock) -            self._session_id = session_id +            with self._session_id_lock: +                self._session_id = session_id          def get_session_id(self): -            QtCore.QMutexLocker(self._session_id_lock) -            return self._session_id +            with self._session_id_lock: +                return self._session_id          def set_uuid(self, uuid): -            QtCore.QMutexLocker(self._uuid_lock) -            full_uid = "%s@%s" % ( -                self._username, self._provider_config.get_domain()) -            if uuid is not None:  # avoid removing the uuid from settings -                self._settings.set_uuid(full_uid, uuid) -            self._uuid = uuid +            with self._uuid_lock: +                full_uid = "%s@%s" % ( +                    self._username, self._provider_config.get_domain()) +                if uuid is not None:  # avoid removing the uuid from settings +                    self._settings.set_uuid(full_uid, uuid) +                self._uuid = uuid          def get_uuid(self): -            QtCore.QMutexLocker(self._uuid_lock) -            return self._uuid +            with self._uuid_lock: +                return self._uuid          def set_token(self, token): -            QtCore.QMutexLocker(self._token_lock) -            self._token = token +            with self._token_lock: +                self._token = token          def get_token(self): -            QtCore.QMutexLocker(self._token_lock) -            return self._token +            with self._token_lock: +                return self._token -    __instance = None +        def is_authenticated(self): +            """ +            Return whether the user is authenticated or not. -    authentication_finished = QtCore.Signal() -    logout_ok = QtCore.Signal() -    logout_error = QtCore.Signal() +            :rtype: bool +            """ +            user = self._srp_user +            if user is not None: +                return user.authenticated() -    def __init__(self, provider_config): -        """ -        Creates a singleton instance if needed +            return False + +    __instance = None + +    def __init__(self, provider_config, signaler=None):          """ -        QtCore.QObject.__init__(self) +        Create a singleton instance if needed +        :param provider_config: ProviderConfig needed to authenticate. +        :type provider_config: ProviderConfig +        :param signaler: Signaler object used to send notifications +                         from the backend +        :type signaler: Signaler +        """          # Check whether we already have an instance          if SRPAuth.__instance is None:              # Create and remember instance -            SRPAuth.__instance = SRPAuth.__impl(provider_config) +            SRPAuth.__instance = SRPAuth.__impl(provider_config, signaler)          # Store instance reference as the only member in the handle          self.__dict__['_SRPAuth__instance'] = SRPAuth.__instance @@ -642,9 +739,16 @@ class SRPAuth(QtCore.QObject):          """          username = username.lower()          d = self.__instance.authenticate(username, password) -        d.addCallback(self._gui_notify)          return d +    def is_authenticated(self): +        """ +        Return whether the user is authenticated or not. + +        :rtype: bool +        """ +        return self.__instance.is_authenticated() +      def change_password(self, current_password, new_password):          """          Changes the user's password. @@ -657,8 +761,7 @@ class SRPAuth(QtCore.QObject):          :returns: a defer to interact with.          :rtype: twisted.internet.defer.Deferred          """ -        d = threads.deferToThread( -            self.__instance.change_password, current_password, new_password) +        d = self.__instance.change_password(current_password, new_password)          return d      def get_username(self): @@ -672,16 +775,6 @@ class SRPAuth(QtCore.QObject):              return None          return self.__instance._username -    def _gui_notify(self, _): -        """ -        Callback that notifies the UI with the proper signal. - -        :param _: IGNORED, output from the previous callback (None) -        :type _: IGNORED -        """ -        logger.debug("Successful login!") -        self.authentication_finished.emit() -      def get_session_id(self):          return self.__instance.get_session_id() @@ -699,9 +792,7 @@ class SRPAuth(QtCore.QObject):          try:              self.__instance.logout()              logger.debug("Logout success") -            self.logout_ok.emit()              return True          except Exception as e:              logger.debug("Logout error: {0!r}".format(e)) -            self.logout_error.emit()          return False diff --git a/src/leap/bitmask/crypto/srpregister.py b/src/leap/bitmask/crypto/srpregister.py index 4c52db42..f03dc469 100644 --- a/src/leap/bitmask/crypto/srpregister.py +++ b/src/leap/bitmask/crypto/srpregister.py @@ -46,8 +46,6 @@ class SRPRegister(QtCore.QObject):      STATUS_TAKEN = 422      STATUS_ERROR = -999  # Custom error status -    registration_finished = QtCore.Signal(bool, object) -      def __init__(self, signaler=None,                   provider_config=None, register_path="users"):          """ diff --git a/src/leap/bitmask/gui/advanced_key_management.py b/src/leap/bitmask/gui/advanced_key_management.py index cbc8c3e3..be6b4410 100644 --- a/src/leap/bitmask/gui/advanced_key_management.py +++ b/src/leap/bitmask/gui/advanced_key_management.py @@ -30,12 +30,17 @@ from ui_advanced_key_management import Ui_AdvancedKeyManagement  logger = logging.getLogger(__name__) -class AdvancedKeyManagement(QtGui.QWidget): +class AdvancedKeyManagement(QtGui.QDialog):      """      Advanced Key Management      """ -    def __init__(self, user, keymanager, soledad): +    def __init__(self, parent, has_mx, user, keymanager, soledad):          """ +        :param parent: parent object of AdvancedKeyManagement. +        :parent type: QWidget +        :param has_mx: defines whether the current provider provides email or +                       not. +        :type has_mx: bool          :param user: the current logged in user.          :type user: unicode          :param keymanager: the existing keymanager instance @@ -43,7 +48,7 @@ class AdvancedKeyManagement(QtGui.QWidget):          :param soledad: a loaded instance of Soledad          :type soledad: Soledad          """ -        QtGui.QWidget.__init__(self) +        QtGui.QDialog.__init__(self, parent)          self.ui = Ui_AdvancedKeyManagement()          self.ui.setupUi(self) @@ -52,13 +57,18 @@ class AdvancedKeyManagement(QtGui.QWidget):          self.ui.pbImportKeys.setVisible(False)          # if Soledad is not started yet +        if not has_mx: +            msg = self.tr("The provider that you are using " +                          "does not support {0}.") +            msg = msg.format(get_service_display_name(MX_SERVICE)) +            self._disable_ui(msg) +            return + +        # if Soledad is not started yet          if sameProxiedObjects(soledad, None): -            self.ui.gbMyKeyPair.setEnabled(False) -            self.ui.gbStoredPublicKeys.setEnabled(False) -            msg = self.tr("<span style='color:#0000FF;'>NOTE</span>: " -                          "To use this, you need to enable/start {0}.") +            msg = self.tr("To use this, you need to enable/start {0}.")              msg = msg.format(get_service_display_name(MX_SERVICE)) -            self.ui.lblStatus.setText(msg) +            self._disable_ui(msg)              return          # XXX: since import is disabled this is no longer a dangerous feature.          # else: @@ -90,6 +100,18 @@ class AdvancedKeyManagement(QtGui.QWidget):          self._list_keys() +    def _disable_ui(self, msg): +        """ +        Disable the UI and set a note in the status bar. + +        :param msg: note to display in the status bar. +        :type msg: unicode +        """ +        self.ui.gbMyKeyPair.setEnabled(False) +        self.ui.gbStoredPublicKeys.setEnabled(False) +        msg = self.tr("<span style='color:#0000FF;'>NOTE</span>: ") + msg +        self.ui.lblStatus.setText(msg) +      def _import_keys(self):          """          Imports the user's key pair. diff --git a/src/leap/bitmask/gui/eip_preferenceswindow.py b/src/leap/bitmask/gui/eip_preferenceswindow.py index dcaa8b1e..530cd38d 100644 --- a/src/leap/bitmask/gui/eip_preferenceswindow.py +++ b/src/leap/bitmask/gui/eip_preferenceswindow.py @@ -18,17 +18,13 @@  """  EIP Preferences window  """ -import os  import logging  from functools import partial  from PySide import QtCore, 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 -from leap.bitmask.services.eip.eipconfig import get_eipconfig_path  logger = logging.getLogger(__name__) @@ -37,17 +33,20 @@ class EIPPreferencesWindow(QtGui.QDialog):      """      Window that displays the EIP preferences.      """ -    def __init__(self, parent, domain): +    def __init__(self, parent, domain, backend):          """          :param parent: parent object of the EIPPreferencesWindow.          :type parent: QWidget          :param domain: the selected by default domain.          :type domain: unicode +        :param backend: Backend being used +        :type backend: Backend          """          QtGui.QDialog.__init__(self, parent)          self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic")          self._settings = LeapSettings() +        self._backend = backend          # Load UI          self.ui = Ui_EIPPreferences() @@ -61,7 +60,11 @@ class EIPPreferencesWindow(QtGui.QDialog):          self.ui.cbGateways.currentIndexChanged[unicode].connect(              lambda x: self.ui.lblProvidersGatewayStatus.setVisible(False)) -        self._add_configured_providers(domain) +        self._selected_domain = domain +        self._configured_providers = [] + +        self._backend_connect() +        self._add_configured_providers()      def _set_providers_gateway_status(self, status, success=False,                                        error=False): @@ -85,35 +88,49 @@ class EIPPreferencesWindow(QtGui.QDialog):          self.ui.lblProvidersGatewayStatus.setVisible(True)          self.ui.lblProvidersGatewayStatus.setText(status) -    def _add_configured_providers(self, domain=None): +    def _add_configured_providers(self):          """          Add the client's configured providers to the providers combo boxes. +        """ +        providers = self._settings.get_configured_providers() +        if not providers: +            return -        :param domain: the domain to be selected by default. -        :type domain: unicode +        self._backend.eip_get_initialized_providers(providers) + +    @QtCore.Slot(list) +    def _load_providers_in_combo(self, providers): +        """ +        TRIGGERS: +            Signaler.eip_get_initialized_providers + +        Add the client's configured providers to the providers combo boxes. + +        :param providers: the list of providers to add and whether each one is +                          initialized or not. +        :type providers: list of tuples (str, bool)          """          self.ui.cbProvidersGateway.clear() -        providers = self._settings.get_configured_providers()          if not providers:              self.ui.gbGatewaySelector.setEnabled(False)              return -        for provider in providers: +        for provider, is_initialized in providers:              label = provider -            eip_config_path = get_eipconfig_path(provider, relative=False) -            if not os.path.isfile(eip_config_path): -                label = provider + self.tr(" (uninitialized)") +            if not is_initialized: +                label += self.tr(" (uninitialized)")              self.ui.cbProvidersGateway.addItem(label, userData=provider)          # Select provider by name +        domain = self._selected_domain          if domain is not None:              provider_index = self.ui.cbProvidersGateway.findText(                  domain, QtCore.Qt.MatchStartsWith)              self.ui.cbProvidersGateway.setCurrentIndex(provider_index) +    @QtCore.Slot(str)      def _save_selected_gateway(self, provider):          """ -        SLOT          TRIGGERS:              self.ui.pbSaveGateway.clicked @@ -136,9 +153,9 @@ class EIPPreferencesWindow(QtGui.QDialog):              "Gateway settings for provider '{0}' saved.").format(provider)          self._set_providers_gateway_status(msg, success=True) +    @QtCore.Slot(int)      def _populate_gateways(self, domain_idx):          """ -        SLOT          TRIGGERS:              self.ui.cbProvidersGateway.currentIndexChanged[unicode] @@ -155,18 +172,27 @@ class EIPPreferencesWindow(QtGui.QDialog):              return          domain = self.ui.cbProvidersGateway.itemData(domain_idx) +        self._selected_domain = domain -        if not os.path.isfile(get_eipconfig_path(domain, relative=False)): -            self._set_providers_gateway_status( -                self.tr("This is an uninitialized provider, " -                        "please log in first."), -                error=True) -            self.ui.pbSaveGateway.setEnabled(False) -            self.ui.cbGateways.setEnabled(False) -            return -        else: -            self.ui.pbSaveGateway.setEnabled(True) -            self.ui.cbGateways.setEnabled(True) +        self._backend.eip_get_gateways_list(domain) + +    @QtCore.Slot(list) +    def _update_gateways_list(self, gateways): +        """ +        TRIGGERS: +            Signaler.eip_get_gateways_list + +        :param gateways: a list of gateways +        :type gateways: list of unicode + +        Add the available gateways and select the one stored in configuration +        file. +        """ +        self.ui.pbSaveGateway.setEnabled(True) +        self.ui.cbGateways.setEnabled(True) + +        self.ui.cbGateways.clear() +        self.ui.cbGateways.addItem(self.AUTOMATIC_GATEWAY_LABEL)          try:              # disconnect previously connected save method @@ -175,31 +201,13 @@ class EIPPreferencesWindow(QtGui.QDialog):              pass  # Signal was not connected          # set the proper connection for the 'save' button +        domain = self._selected_domain          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) - -        api_version = provider_config.get_api_version() -        eip_config.set_api_version(api_version) -        eip_loaded = eip_config.load(get_eipconfig_path(domain)) - -        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) +        selected_gateway = self._settings.get_selected_gateway( +            self._selected_domain) -        # 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) @@ -208,3 +216,42 @@ class EIPPreferencesWindow(QtGui.QDialog):                  index = idx + 1          self.ui.cbGateways.setCurrentIndex(index) + +    @QtCore.Slot() +    def _gateways_list_error(self): +        """ +        TRIGGERS: +            Signaler.eip_get_gateways_list_error + +        An error has occurred retrieving the gateway list so we inform the +        user. +        """ +        self._set_providers_gateway_status( +            self.tr("There was a problem with configuration files."), +            error=True) +        self.ui.pbSaveGateway.setEnabled(False) +        self.ui.cbGateways.setEnabled(False) + +    @QtCore.Slot() +    def _gateways_list_uninitialized(self): +        """ +        TRIGGERS: +            Signaler.eip_uninitialized_provider + +        The requested provider in not initialized yet, so we give the user an +        error msg. +        """ +        self._set_providers_gateway_status( +            self.tr("This is an uninitialized provider, please log in first."), +            error=True) +        self.ui.pbSaveGateway.setEnabled(False) +        self.ui.cbGateways.setEnabled(False) + +    def _backend_connect(self): +        sig = self._backend.signaler +        sig.eip_get_gateways_list.connect(self._update_gateways_list) +        sig.eip_get_gateways_list_error.connect(self._gateways_list_error) +        sig.eip_uninitialized_provider.connect( +            self._gateways_list_uninitialized) +        sig.eip_get_initialized_providers.connect( +            self._load_providers_in_combo) diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py index 19942d9d..ca28b8bf 100644 --- a/src/leap/bitmask/gui/eip_status.py +++ b/src/leap/bitmask/gui/eip_status.py @@ -25,7 +25,6 @@ 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.services import get_service_display_name, EIP_SERVICE  from leap.bitmask.platform_init import IS_LINUX  from leap.bitmask.util.averages import RateMovingAverage @@ -89,17 +88,18 @@ class EIPStatusWidget(QtGui.QWidget):          self.ui.btnUpload.clicked.connect(onclicked)          self.ui.btnDownload.clicked.connect(onclicked) +    @QtCore.Slot()      def _on_VPN_status_clicked(self):          """ -        SLOT -        TRIGGER: self.ui.btnUpload.clicked -                 self.ui.btnDownload.clicked +        TRIGGERS: +            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 +        self.update_vpn_status()  # refresh      def _set_traffic_rates(self):          """ @@ -117,7 +117,7 @@ class EIPStatusWidget(QtGui.QWidget):          """          self._up_rate.reset()          self._down_rate.reset() -        self.update_vpn_status(None) +        self.update_vpn_status()      def _update_traffic_rates(self, up, down):          """ @@ -260,11 +260,12 @@ class EIPStatusWidget(QtGui.QWidget):              self._service_name, self.tr("disabled")))          # Replace EIP tray menu with an action that displays a "disabled" text -        menu = self._systray.contextMenu() -        menu.insertAction( -            self._eip_status_menu.menuAction(), -            self._eip_disabled_action) -        self._eip_status_menu.menuAction().setVisible(False) +        if self.isVisible(): +            menu = self._systray.contextMenu() +            menu.insertAction( +                self._eip_status_menu.menuAction(), +                self._eip_disabled_action) +            self._eip_status_menu.menuAction().setVisible(False)      @QtCore.Slot()      def enable_eip_start(self): @@ -278,7 +279,8 @@ class EIPStatusWidget(QtGui.QWidget):          # Restore the eip action menu          menu = self._systray.contextMenu()          menu.removeAction(self._eip_disabled_action) -        self._eip_status_menu.menuAction().setVisible(True) +        if self.isVisible(): +            self._eip_status_menu.menuAction().setVisible(True)      # XXX disable (later) --------------------------      def set_eip_status(self, status, error=False): @@ -348,23 +350,27 @@ class EIPStatusWidget(QtGui.QWidget):              self.tr("Traffic is being routed in the clear"))          self.ui.lblEIPStatus.show() -    def update_vpn_status(self, data): +    @QtCore.Slot(dict) +    def update_vpn_status(self, data=None):          """ -        SLOT -        TRIGGER: VPN.status_changed +        TRIGGERS: +            Signaler.eip_status_changed -        Updates the download/upload labels based on the data provided -        by the VPN thread. +        Updates the download/upload labels based on the data provided by the +        VPN thread. +        If data is None, we just will refresh the display based on the previous +        data. -        :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 +        :param data: a tuple with download/upload totals (download, upload). +        :type data: tuple          """ -        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 data is not None: +            try: +                upload, download = map(float, data) +                self._update_traffic_rates(upload, download) +            except Exception: +                # discard invalid data +                return          if self.DISPLAY_TRAFFIC_RATES:              uprate, downrate = self._get_traffic_rates() @@ -379,39 +385,42 @@ class EIPStatusWidget(QtGui.QWidget):          self.ui.btnUpload.setText(upload_str)          self.ui.btnDownload.setText(download_str) -    def update_vpn_state(self, data): +    @QtCore.Slot(dict) +    def update_vpn_state(self, vpn_state):          """ -        SLOT -        TRIGGER: VPN.state_changed +        TRIGGERS: +            Signaler.eip_state_changed          Updates the displayed VPN state based on the data provided by          the VPN thread. +        :param vpn_state: the state of the VPN +        :type vpn_state: dict +          Emits: -            If the status is connected, we emit EIPConnection.qtsigs. +            If the vpn_state is connected, we emit EIPConnection.qtsigs.              connected_signal          """ -        status = data[VPNManager.STATUS_STEP_KEY] -        self.set_eip_status_icon(status) -        if status == "CONNECTED": +        self.set_eip_status_icon(vpn_state) +        if vpn_state == "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": +        # XXX should lookup vpn_state map in EIPConnection +        elif vpn_state == "AUTH":              self.set_eip_status(self.tr("Authenticating...")) -        elif status == "GET_CONFIG": +        elif vpn_state == "GET_CONFIG":              self.set_eip_status(self.tr("Retrieving configuration...")) -        elif status == "WAIT": +        elif vpn_state == "WAIT":              self.set_eip_status(self.tr("Waiting to start...")) -        elif status == "ASSIGN_IP": +        elif vpn_state == "ASSIGN_IP":              self.set_eip_status(self.tr("Assigning IP")) -        elif status == "RECONNECTING": +        elif vpn_state == "RECONNECTING":              self.set_eip_status(self.tr("Reconnecting...")) -        elif status == "ALREADYRUNNING": +        elif vpn_state == "ALREADYRUNNING":              # Put the following calls in Qt's event queue, otherwise              # the UI won't update properly              QtCore.QTimer.singleShot( @@ -419,7 +428,7 @@ class EIPStatusWidget(QtGui.QWidget):              msg = self.tr("Unable to start VPN, it's already running.")              QtCore.QTimer.singleShot(0, partial(self.set_eip_status, msg))          else: -            self.set_eip_status(status) +            self.set_eip_status(vpn_state)      def set_eip_icon(self, icon):          """ diff --git a/src/leap/bitmask/gui/loggerwindow.py b/src/leap/bitmask/gui/loggerwindow.py index 9f396574..f19b172f 100644 --- a/src/leap/bitmask/gui/loggerwindow.py +++ b/src/leap/bitmask/gui/loggerwindow.py @@ -21,14 +21,14 @@ History log window  import logging  import cgi -from PySide import QtGui +from PySide import QtCore, QtGui  from twisted.internet import threads  from ui_loggerwindow import Ui_LoggerWindow  from leap.bitmask.util.constants import PASTEBIN_API_DEV_KEY  from leap.bitmask.util.leap_log_handler import LeapLogHandler -from leap.bitmask.util.pastebin import PastebinAPI, PastebinError +from leap.bitmask.util import pastebin  from leap.common.check import leap_assert, leap_assert_type  logger = logging.getLogger(__name__) @@ -203,10 +203,10 @@ class LoggerWindow(QtGui.QDialog):              Send content to pastebin and return the link.              """              content = self._current_history -            pb = PastebinAPI() +            pb = pastebin.PastebinAPI()              link = pb.paste(PASTEBIN_API_DEV_KEY, content,                              paste_name="Bitmask log", -                            paste_expire_date='1W') +                            paste_expire_date='1M')              # convert to 'raw' link              link = "http://pastebin.com/raw.php?i=" + link.split('/')[-1] @@ -220,12 +220,16 @@ class LoggerWindow(QtGui.QDialog):              :param link: the recently created pastebin link.              :type link: str              """ +            self._set_pastebin_sending(False)              msg = self.tr("Your pastebin link <a href='{0}'>{0}</a>")              msg = msg.format(link) -            show_info = lambda: QtGui.QMessageBox.information( -                self, self.tr("Pastebin OK"), msg) -            self._set_pastebin_sending(False) -            self.reactor.callLater(0, show_info) + +            # We save the dialog in an instance member to avoid dialog being +            # deleted right after we exit this method +            self._msgBox = msgBox = QtGui.QMessageBox( +                QtGui.QMessageBox.Information, self.tr("Pastebin OK"), msg) +            msgBox.setWindowModality(QtCore.Qt.NonModal) +            msgBox.show()          def pastebin_err(failure):              """ @@ -234,13 +238,19 @@ class LoggerWindow(QtGui.QDialog):              :param failure: the failure that triggered the errback.              :type failure: twisted.python.failure.Failure              """ +            self._set_pastebin_sending(False)              logger.error(repr(failure)) +              msg = self.tr("Sending logs to Pastebin failed!") -            show_err = lambda: QtGui.QMessageBox.critical( -                self, self.tr("Pastebin Error"), msg) -            self._set_pastebin_sending(False) -            self.reactor.callLater(0, show_err) -            failure.trap(PastebinError) +            if failure.check(pastebin.PostLimitError): +                msg = self.tr('Maximum posts per day reached') + +            # We save the dialog in an instance member to avoid dialog being +            # deleted right after we exit this method +            self._msgBox = msgBox = QtGui.QMessageBox( +                QtGui.QMessageBox.Critical, self.tr("Pastebin Error"), msg) +            msgBox.setWindowModality(QtCore.Qt.NonModal) +            msgBox.show()          self._set_pastebin_sending(True)          d = threads.deferToThread(do_pastebin) diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py index 4a483c32..ac7ad878 100644 --- a/src/leap/bitmask/gui/login.py +++ b/src/leap/bitmask/gui/login.py @@ -259,12 +259,16 @@ class LoginWidget(QtGui.QWidget):          """          self.ui.lnPassword.setFocus() -    def _current_provider_changed(self, param): +    @QtCore.Slot(int) +    def _current_provider_changed(self, idx):          """ -        SLOT -        TRIGGERS: self.ui.cmbProviders.currentIndexChanged +        TRIGGERS: +            self.ui.cmbProviders.currentIndexChanged + +        :param idx: the index of the new selected item +        :type idx: int          """ -        if param == (self.ui.cmbProviders.count() - 1): +        if idx == (self.ui.cmbProviders.count() - 1):              self.show_wizard.emit()              # Leave the previously selected provider in the combobox              prev_provider = 0 @@ -274,7 +278,7 @@ class LoginWidget(QtGui.QWidget):              self.ui.cmbProviders.setCurrentIndex(prev_provider)              self.ui.cmbProviders.blockSignals(False)          else: -            self._selected_provider_index = param +            self._selected_provider_index = idx      def start_login(self):          """ diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py index 44a138e2..d3346780 100644 --- a/src/leap/bitmask/gui/mail_status.py +++ b/src/leap/bitmask/gui/mail_status.py @@ -184,10 +184,10 @@ class MailStatusWidget(QtGui.QWidget):          leap_assert_type(action_mail_status, QtGui.QAction)          self._action_mail_status = action_mail_status +    @QtCore.Slot()      def set_soledad_failed(self):          """ -        SLOT -        TRIGGER: +        TRIGGERS:              SoledadBootstrapper.soledad_failed          This method is called whenever soledad has a failure. @@ -195,10 +195,10 @@ class MailStatusWidget(QtGui.QWidget):          msg = self.tr("There was an unexpected problem with Soledad.")          self._set_mail_status(msg, ready=-1) +    @QtCore.Slot()      def set_soledad_invalid_auth_token(self):          """ -        SLOT -        TRIGGER: +        TRIGGERS:              SoledadBootstrapper.soledad_invalid_token          This method is called when the auth token is invalid @@ -250,10 +250,11 @@ class MailStatusWidget(QtGui.QWidget):          """          self._soledad_event.emit(req) +    @QtCore.Slot(object)      def _mail_handle_soledad_events_slot(self, req):          """ -        SLOT -        TRIGGER: _mail_handle_soledad_events +        TRIGGERS: +            _mail_handle_soledad_events          Reacts to an Soledad event @@ -284,10 +285,11 @@ class MailStatusWidget(QtGui.QWidget):          """          self._keymanager_event.emit(req) +    @QtCore.Slot(object)      def _mail_handle_keymanager_events_slot(self, req):          """ -        SLOT -        TRIGGER: _mail_handle_keymanager_events +        TRIGGERS: +            _mail_handle_keymanager_events          Reacts to an KeyManager event @@ -330,10 +332,11 @@ class MailStatusWidget(QtGui.QWidget):          """          self._smtp_event.emit(req) +    @QtCore.Slot(object)      def _mail_handle_smtp_events_slot(self, req):          """ -        SLOT -        TRIGGER: _mail_handle_smtp_events +        TRIGGERS: +            _mail_handle_smtp_events          Reacts to an SMTP event @@ -364,10 +367,11 @@ class MailStatusWidget(QtGui.QWidget):          """          self._imap_event.emit(req) +    @QtCore.Slot(object)      def _mail_handle_imap_events_slot(self, req):          """ -        SLOT -        TRIGGER: _mail_handle_imap_events +        TRIGGERS: +            _mail_handle_imap_events          Reacts to an IMAP event diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 5abfaa67..e3848c46 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -19,6 +19,7 @@ Main window for Bitmask.  """  import logging  import socket +import time  from threading import Condition  from datetime import datetime @@ -26,7 +27,6 @@ from datetime import datetime  from PySide import QtCore, QtGui  from zope.proxy import ProxyBase, setProxiedObject  from twisted.internet import reactor, threads -from twisted.internet.defer import CancelledError  from leap.bitmask import __version__ as VERSION  from leap.bitmask import __version_hash__ as VERSION_HASH @@ -34,19 +34,16 @@ from leap.bitmask.config import flags  from leap.bitmask.config.leapsettings import LeapSettings  from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.crypto import srpauth -from leap.bitmask.crypto.srpauth import SRPAuth - -from leap.bitmask.gui.loggerwindow import LoggerWindow +from leap.bitmask.gui import statemachines  from leap.bitmask.gui.advanced_key_management import AdvancedKeyManagement -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.eip_status import EIPStatusWidget +from leap.bitmask.gui.loggerwindow import LoggerWindow +from leap.bitmask.gui.login import LoginWidget  from leap.bitmask.gui.mail_status import MailStatusWidget -from leap.bitmask.gui.wizard import Wizard +from leap.bitmask.gui.preferenceswindow import PreferencesWindow  from leap.bitmask.gui.systray import SysTray +from leap.bitmask.gui.wizard import Wizard  from leap.bitmask import provider  from leap.bitmask.platform_init import IS_WIN, IS_MAC, IS_LINUX @@ -59,20 +56,7 @@ from leap.bitmask.services import get_service_display_name  from leap.bitmask.services.mail import conductor as mail_conductor  from leap.bitmask.services import EIP_SERVICE, MX_SERVICE -from leap.bitmask.services.eip import eipconfig -from leap.bitmask.services.eip import get_openvpn_management -from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper  from leap.bitmask.services.eip.connection import EIPConnection -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.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.darwinvpnlauncher import EIPNoTunKextLoaded  from leap.bitmask.services.soledad.soledadbootstrapper import \      SoledadBootstrapper @@ -99,11 +83,6 @@ class MainWindow(QtGui.QMainWindow):      """      Main window for login and presenting status updates to the user      """ - -    # StackedWidget indexes -    LOGIN_INDEX = 0 -    EIP_STATUS_INDEX = 1 -      # Signals      eip_needs_login = QtCore.Signal([])      offline_mode_bypass_login = QtCore.Signal([]) @@ -122,9 +101,7 @@ class MainWindow(QtGui.QMainWindow):      # We give each service some time to come to a halt before forcing quit      SERVICE_STOP_TIMEOUT = 20 -    def __init__(self, quit_callback, -                 openvpn_verb=1, -                 bypass_checks=False): +    def __init__(self, quit_callback, bypass_checks=False, start_hidden=False):          """          Constructor for the client main window @@ -132,10 +109,12 @@ class MainWindow(QtGui.QMainWindow):                                the application.          :type quit_callback: callable -        :param bypass_checks: Set to true if the app should bypass -                              first round of checks for CA -                              certificates at bootstrap +        :param bypass_checks: Set to true if the app should bypass first round +                              of checks for CA certificates at bootstrap          :type bypass_checks: bool +        :param start_hidden: Set to true if the app should not show the window +                             but just the tray. +        :type start_hidden: bool          """          QtGui.QMainWindow.__init__(self) @@ -190,31 +169,30 @@ class MainWindow(QtGui.QMainWindow):          # XXX this should be handled by EIP Conductor          self._eip_connection.qtsigs.connecting_signal.connect( -            self._start_eip) +            self._start_EIP)          self._eip_connection.qtsigs.disconnecting_signal.connect(              self._stop_eip)          self._eip_status.eip_connection_connected.connect( -            self._on_eip_connected) +            self._on_eip_connection_connected)          self._eip_status.eip_connection_connected.connect(              self._maybe_run_soledad_setup_checks)          self.offline_mode_bypass_login.connect(              self._maybe_run_soledad_setup_checks) -        self.eip_needs_login.connect( -            self._eip_status.disable_eip_start) -        self.eip_needs_login.connect( -            self._disable_eip_start_action) +        self.eip_needs_login.connect(self._eip_status.disable_eip_start) +        self.eip_needs_login.connect(self._disable_eip_start_action) + +        self._trying_to_start_eip = False          # This is loaded only once, there's a bug when doing that more          # than once          # XXX HACK!! But we need it as long as we are using          # provider_config in here -        self._provider_config = ( -            self._backend._components["provider"]._provider_config) +        self._provider_config = self._backend.get_provider_config() +          # Used for automatic start of EIP          self._provisional_provider_config = ProviderConfig() -        self._eip_config = eipconfig.EIPConfig()          self._already_started_eip = False          self._already_started_soledad = False @@ -224,35 +202,9 @@ class MainWindow(QtGui.QMainWindow):          self._logged_user = None          self._logged_in_offline = False +        self._backend_connected_signals = {}          self._backend_connect() -        # This thread is similar to the provider bootstrapper -        self._eip_bootstrapper = EIPBootstrapper() - -        # EIP signals ---- move to eip conductor. -        # TODO change the name of "download_config" signal to -        # something less confusing (config_ready maybe) -        self._eip_bootstrapper.download_config.connect( -            self._eip_intermediate_stage) -        self._eip_bootstrapper.download_client_certificate.connect( -            self._finish_eip_bootstrap) - -        self._vpn = VPN(openvpn_verb=openvpn_verb) - -        # connect vpn process signals -        self._vpn.qtsigs.state_changed.connect( -            self._eip_status.update_vpn_state) -        self._vpn.qtsigs.status_changed.connect( -            self._eip_status.update_vpn_status) -        self._vpn.qtsigs.process_finished.connect( -            self._eip_finished) -        self._vpn.qtsigs.network_unreachable.connect( -            self._on_eip_network_unreachable) -        self._vpn.qtsigs.process_restart_tls.connect( -            self._do_eip_restart) -        self._vpn.qtsigs.process_restart_ping.connect( -            self._do_eip_restart) -          self._soledad_bootstrapper = SoledadBootstrapper()          self._soledad_bootstrapper.download_config.connect(              self._soledad_intermediate_stage) @@ -260,8 +212,6 @@ class MainWindow(QtGui.QMainWindow):              self._soledad_bootstrapped_stage)          self._soledad_bootstrapper.local_only_ready.connect(              self._soledad_bootstrapped_stage) -        self._soledad_bootstrapper.soledad_timeout.connect( -            self._retry_soledad_connection)          self._soledad_bootstrapper.soledad_invalid_auth_token.connect(              self._mail_status.set_soledad_invalid_auth_token)          self._soledad_bootstrapper.soledad_failed.connect( @@ -307,6 +257,8 @@ class MainWindow(QtGui.QMainWindow):          # self.ui.btnEIPPreferences.clicked.connect(self._show_eip_preferences)          self._enabled_services = [] +        self._ui_mx_visible = True +        self._ui_eip_visible = True          # last minute UI manipulations @@ -342,6 +294,7 @@ class MainWindow(QtGui.QMainWindow):          self._logger_window = None          self._bypass_checks = bypass_checks +        self._start_hidden = start_hidden          # We initialize Soledad and Keymanager instances as          # transparent proxies, so we can pass the reference freely @@ -349,7 +302,6 @@ class MainWindow(QtGui.QMainWindow):          self._soledad = ProxyBase(None)          self._keymanager = ProxyBase(None) -        self._login_defer = None          self._soledad_defer = None          self._mail_conductor = mail_conductor.MailConductor( @@ -372,7 +324,7 @@ class MainWindow(QtGui.QMainWindow):          if self._first_run():              self._wizard_firstrun = True -            self._backend_disconnect() +            self._disconnect_and_untrack()              self._wizard = Wizard(backend=self._backend,                                    bypass_checks=bypass_checks)              # Give this window time to finish init and then show the wizard @@ -384,46 +336,151 @@ class MainWindow(QtGui.QMainWindow):              # so this has to be done after eip_machine is started              self._finish_init() +    def _not_logged_in_error(self): +        """ +        Handle the 'not logged in' backend error if we try to do an operation +        that requires to be logged in. +        """ +        logger.critical("You are trying to do an operation that requires " +                        "log in first.") +        QtGui.QMessageBox.critical( +            self, self.tr("Application error"), +            self.tr("You are trying to do an operation " +                    "that requires logging in first.")) + +    def _connect_and_track(self, signal, method): +        """ +        Helper to connect signals and keep track of them. + +        :param signal: the signal to connect to. +        :type signal: QtCore.Signal +        :param method: the method to call when the signal is triggered. +        :type method: callable, Slot or Signal +        """ +        self._backend_connected_signals[signal] = method +        signal.connect(method) + +    def _backend_bad_call(self, data): +        """ +        Callback for debugging bad backend calls + +        :param data: data from the backend about the problem +        :type data: str +        """ +        logger.error("Bad call to the backend:") +        logger.error(data) +      def _backend_connect(self):          """          Helper to connect to backend signals          """          sig = self._backend.signaler -        sig.prov_name_resolution.connect(self._intermediate_stage) -        sig.prov_https_connection.connect(self._intermediate_stage) -        sig.prov_download_ca_cert.connect(self._intermediate_stage) -        sig.prov_download_provider_info.connect(self._load_provider_config) -        sig.prov_check_api_certificate.connect(self._provider_config_loaded) +        sig.backend_bad_call.connect(self._backend_bad_call) + +        self._connect_and_track(sig.prov_name_resolution, +                                self._intermediate_stage) +        self._connect_and_track(sig.prov_https_connection, +                                self._intermediate_stage) +        self._connect_and_track(sig.prov_download_ca_cert, +                                self._intermediate_stage) + +        self._connect_and_track(sig.prov_download_provider_info, +                                self._load_provider_config) +        self._connect_and_track(sig.prov_check_api_certificate, +                                self._provider_config_loaded) + +        self._connect_and_track(sig.prov_problem_with_provider, +                                self._login_problem_provider) + +        self._connect_and_track(sig.prov_cancelled_setup, +                                self._set_login_cancelled) + +        # Login signals +        self._connect_and_track(sig.srp_auth_ok, self._authentication_finished) + +        auth_error = ( +            lambda: self._authentication_error(self.tr("Unknown error."))) +        self._connect_and_track(sig.srp_auth_error, auth_error) -        # Only used at login, no need to disconnect this like we do -        # with the other -        sig.prov_problem_with_provider.connect(self._login_problem_provider) +        auth_server_error = ( +            lambda: self._authentication_error( +                self.tr("There was a server problem with authentication."))) +        self._connect_and_track(sig.srp_auth_server_error, auth_server_error) +        auth_connection_error = ( +            lambda: self._authentication_error( +                self.tr("Could not establish a connection."))) +        self._connect_and_track(sig.srp_auth_connection_error, +                                auth_connection_error) + +        auth_bad_user_or_password = ( +            lambda: self._authentication_error( +                self.tr("Invalid username or password."))) +        self._connect_and_track(sig.srp_auth_bad_user_or_password, +                                auth_bad_user_or_password) + +        # Logout signals +        self._connect_and_track(sig.srp_logout_ok, self._logout_ok) +        self._connect_and_track(sig.srp_logout_error, self._logout_error) + +        self._connect_and_track(sig.srp_not_logged_in_error, +                                self._not_logged_in_error) + +        # EIP bootstrap signals +        self._connect_and_track(sig.eip_config_ready, +                                self._eip_intermediate_stage) +        self._connect_and_track(sig.eip_client_certificate_ready, +                                self._finish_eip_bootstrap) + +        # We don't want to disconnect some signals so don't track them:          sig.prov_unsupported_client.connect(self._needs_update)          sig.prov_unsupported_api.connect(self._incompatible_api) -        sig.prov_cancelled_setup.connect(self._set_login_cancelled) - -    def _backend_disconnect(self): -        """ -        Helper to disconnect from backend signals. +        # EIP start signals +        sig.eip_openvpn_already_running.connect( +            self._on_eip_openvpn_already_running) +        sig.eip_alien_openvpn_already_running.connect( +            self._on_eip_alien_openvpn_already_running) +        sig.eip_openvpn_not_found_error.connect( +            self._on_eip_openvpn_not_found_error) +        sig.eip_vpn_launcher_exception.connect( +            self._on_eip_vpn_launcher_exception) +        sig.eip_no_polkit_agent_error.connect( +            self._on_eip_no_polkit_agent_error) +        sig.eip_no_pkexec_error.connect(self._on_eip_no_pkexec_error) +        sig.eip_no_tun_kext_error.connect(self._on_eip_no_tun_kext_error) + +        sig.eip_state_changed.connect(self._eip_status.update_vpn_state) +        sig.eip_status_changed.connect(self._eip_status.update_vpn_status) +        sig.eip_process_finished.connect(self._eip_finished) +        sig.eip_network_unreachable.connect(self._on_eip_network_unreachable) +        sig.eip_process_restart_tls.connect(self._do_eip_restart) +        sig.eip_process_restart_ping.connect(self._do_eip_restart) + +        sig.eip_can_start.connect(self._backend_can_start_eip) +        sig.eip_cannot_start.connect(self._backend_cannot_start_eip) + +    def _disconnect_and_untrack(self): +        """ +        Helper to disconnect the tracked signals.          Some signals are emitted from the wizard, and we want to          ignore those.          """ -        sig = self._backend.signaler -        sig.prov_name_resolution.disconnect(self._intermediate_stage) -        sig.prov_https_connection.disconnect(self._intermediate_stage) -        sig.prov_download_ca_cert.disconnect(self._intermediate_stage) +        for signal, method in self._backend_connected_signals.items(): +            try: +                signal.disconnect(method) +            except RuntimeError: +                pass  # Signal was not connected -        sig.prov_download_provider_info.disconnect(self._load_provider_config) -        sig.prov_check_api_certificate.disconnect(self._provider_config_loaded) +        self._backend_connected_signals = {} +    @QtCore.Slot()      def _rejected_wizard(self):          """ -        SLOT -        TRIGGERS: self._wizard.rejected +        TRIGGERS: +            self._wizard.rejected          Called if the wizard has been cancelled or closed before          finishing. @@ -444,12 +501,12 @@ class MainWindow(QtGui.QMainWindow):              if self._wizard_firstrun:                  self._finish_init() +    @QtCore.Slot()      def _launch_wizard(self):          """ -        SLOT          TRIGGERS: -          self._login_widget.show_wizard -          self.ui.action_wizard.triggered +            self._login_widget.show_wizard +            self.ui.action_wizard.triggered          Also called in first run. @@ -457,7 +514,7 @@ class MainWindow(QtGui.QMainWindow):          there.          """          if self._wizard is None: -            self._backend_disconnect() +            self._disconnect_and_untrack()              self._wizard = Wizard(backend=self._backend,                                    bypass_checks=self._bypass_checks)              self._wizard.accepted.connect(self._finish_init) @@ -472,11 +529,11 @@ class MainWindow(QtGui.QMainWindow):          self._wizard.finished.connect(self._wizard_finished)          self._settings.set_skip_first_run(True) +    @QtCore.Slot()      def _wizard_finished(self):          """ -        SLOT -        TRIGGERS -          self._wizard.finished +        TRIGGERS: +            self._wizard.finished          Called when the wizard has finished.          """ @@ -497,11 +554,11 @@ class MainWindow(QtGui.QMainWindow):                  return h          return None +    @QtCore.Slot()      def _show_logger_window(self):          """ -        SLOT          TRIGGERS: -          self.ui.action_show_logs.triggered +            self.ui.action_show_logs.triggered          Displays the window with the history of messages logged until now          and displays the new ones on arrival. @@ -518,9 +575,9 @@ class MainWindow(QtGui.QMainWindow):          else:              self._logger_window.setVisible(not self._logger_window.isVisible()) +    @QtCore.Slot()      def _show_AKM(self):          """ -        SLOT          TRIGGERS:              self.ui.action_advanced_key_management.triggered @@ -528,30 +585,38 @@ class MainWindow(QtGui.QMainWindow):          """          domain = self._login_widget.get_selected_provider()          logged_user = "{0}@{1}".format(self._logged_user, domain) -        self._akm = AdvancedKeyManagement( -            logged_user, self._keymanager, self._soledad) -        self._akm.show() +        has_mx = True +        if self._logged_user is not None: +            provider_config = self._get_best_provider_config() +            has_mx = provider_config.provides_mx() + +        akm = AdvancedKeyManagement( +            self, has_mx, logged_user, self._keymanager, self._soledad) +        akm.show() + +    @QtCore.Slot()      def _show_preferences(self):          """ -        SLOT          TRIGGERS: -          self.ui.btnPreferences.clicked (disabled for now) -          self.ui.action_preferences +            self.ui.btnPreferences.clicked (disabled for now) +            self.ui.action_preferences          Displays the preferences window.          """ +        user = self._login_widget.get_user() +        prov = self._login_widget.get_selected_provider()          preferences = PreferencesWindow( -            self, self._srp_auth, self._provider_config, self._soledad, -            self._login_widget.get_selected_provider()) +            self, self._backend, self._provider_config, self._soledad, +            user, prov)          self.soledad_ready.connect(preferences.set_soledad_ready)          preferences.show()          preferences.preferences_saved.connect(self._update_eip_enabled_status) +    @QtCore.Slot()      def _update_eip_enabled_status(self):          """ -        SLOT          TRIGGER:              PreferencesWindow.preferences_saved @@ -563,17 +628,43 @@ class MainWindow(QtGui.QMainWindow):          """          settings = self._settings          default_provider = settings.get_defaultprovider() + +        if default_provider is None: +            logger.warning("Trying toupdate eip enabled status but there's no" +                           " default provider. Disabling EIP for the time" +                           " being...") +            self._backend_cannot_start_eip() +            return + +        self._trying_to_start_eip = settings.get_autostart_eip() +        self._backend.eip_can_start(default_provider) + +        # If we don't want to start eip, we leave everything +        # initialized to quickly start it +        if not self._trying_to_start_eip: +            self._backend.setup_eip(default_provider, skip_network=True) + +    def _backend_can_start_eip(self): +        """ +        TRIGGER: +            self._backend.signaler.eip_can_start + +        If EIP can be started right away, and the client is configured +        to do so, start it. Otherwise it leaves everything in place +        for the user to click Turn ON. +        """ +        settings = self._settings +        default_provider = settings.get_defaultprovider()          enabled_services = []          if default_provider is not None:              enabled_services = settings.get_enabled_services(default_provider)          eip_enabled = False          if EIP_SERVICE in enabled_services: -            should_autostart = settings.get_autostart_eip() -            if should_autostart and default_provider is not None: +            eip_enabled = True +            if default_provider is not None:                  self._eip_status.enable_eip_start()                  self._eip_status.set_eip_status("") -                eip_enabled = True              else:                  # we don't have an usable provider                  # so the user needs to log in first @@ -583,19 +674,44 @@ class MainWindow(QtGui.QMainWindow):              self._eip_status.disable_eip_start()              self._eip_status.set_eip_status(self.tr("Disabled")) -        return eip_enabled +        if eip_enabled and self._trying_to_start_eip: +            self._trying_to_start_eip = False +            self._try_autostart_eip() + +    def _backend_cannot_start_eip(self): +        """ +        TRIGGER: +            self._backend.signaler.eip_cannot_start +        If EIP can't be started right away, get the UI to what it +        needs to look like and waits for a proper login/eip bootstrap. +        """ +        settings = self._settings +        default_provider = settings.get_defaultprovider() +        enabled_services = [] +        if default_provider is not None: +            enabled_services = settings.get_enabled_services(default_provider) + +        if EIP_SERVICE in enabled_services: +            # we don't have a usable provider +            # so the user needs to log in first +            self._eip_status.disable_eip_start() +        else: +            self._stop_eip() +            self._eip_status.disable_eip_start() +            self._eip_status.set_eip_status(self.tr("Disabled")) + +    @QtCore.Slot()      def _show_eip_preferences(self):          """ -        SLOT          TRIGGERS: -          self.ui.btnEIPPreferences.clicked -          self.ui.action_eip_preferences (disabled for now) +            self.ui.btnEIPPreferences.clicked +            self.ui.action_eip_preferences (disabled for now)          Displays the EIP preferences window.          """          domain = self._login_widget.get_selected_provider() -        EIPPreferencesWindow(self, domain).show() +        EIPPreferencesWindow(self, domain, self._backend).show()      #      # updates @@ -610,22 +726,27 @@ class MainWindow(QtGui.QMainWindow):          """          self.new_updates.emit(req) +    @QtCore.Slot(object)      def _react_to_new_updates(self, req):          """ -        SLOT -        TRIGGER: self._new_updates_available +        TRIGGERS: +            self.new_updates          Displays the new updates label and sets the updates_content + +        :param req: Request type +        :type req: leap.common.events.events_pb2.SignalRequest          """          self.moveToThread(QtCore.QCoreApplication.instance().thread())          self.ui.lblNewUpdates.setVisible(True)          self.ui.btnMore.setVisible(True)          self._updates_content = req.content +    @QtCore.Slot()      def _updates_details(self):          """ -        SLOT -        TRIGGER: self.ui.btnMore.clicked +        TRIGGERS: +            self.ui.btnMore.clicked          Parses and displays the updates details          """ @@ -649,11 +770,11 @@ class MainWindow(QtGui.QMainWindow):                                        self.tr("Updates available"),                                        msg) +    @QtCore.Slot()      def _finish_init(self):          """ -        SLOT          TRIGGERS: -          self._wizard.accepted +            self._wizard.accepted          Also called at the end of the constructor if not first run. @@ -666,11 +787,13 @@ class MainWindow(QtGui.QMainWindow):          providers = self._settings.get_configured_providers()          self._login_widget.set_providers(providers)          self._show_systray() -        self.show() -        if IS_MAC: -            self.raise_() -        self._hide_unsupported_services() +        if not self._start_hidden: +            self.show() +            if IS_MAC: +                self.raise_() + +        self._show_hide_unsupported_services()          if self._wizard:              possible_username = self._wizard.get_username() @@ -696,7 +819,7 @@ class MainWindow(QtGui.QMainWindow):              self._wizard = None              self._backend_connect()          else: -            self._try_autostart_eip() +            self._update_eip_enabled_status()              domain = self._settings.get_provider()              if domain is not None: @@ -712,7 +835,7 @@ class MainWindow(QtGui.QMainWindow):                  if self._login_widget.load_user_from_keyring(saved_user):                      self._login() -    def _hide_unsupported_services(self): +    def _show_hide_unsupported_services(self):          """          Given a set of configured providers, it creates a set of          available services among all of them and displays the service @@ -733,8 +856,38 @@ class MainWindow(QtGui.QMainWindow):                  for service in provider_config.get_services():                      services.add(service) -        self.ui.eipWidget.setVisible(EIP_SERVICE in services) -        self.ui.mailWidget.setVisible(MX_SERVICE in services) +        self._set_eip_visible(EIP_SERVICE in services) +        self._set_mx_visible(MX_SERVICE in services) + +    def _set_mx_visible(self, visible): +        """ +        Change the visibility of MX_SERVICE related UI components. + +        :param visible: whether the components should be visible or not. +        :type visible: bool +        """ +        # only update visibility if it is something to change +        if self._ui_mx_visible ^ visible: +            self.ui.mailWidget.setVisible(visible) +            self.ui.lineUnderEmail.setVisible(visible) +            self._action_mail_status.setVisible(visible) +            self._ui_mx_visible = visible + +    def _set_eip_visible(self, visible): +        """ +        Change the visibility of EIP_SERVICE related UI components. + +        :param visible: whether the components should be visible or not. +        :type visible: bool +        """ +        # NOTE: we use xor to avoid the code being run if the visibility hasn't +        # changed. This is meant to avoid the eip menu being displayed floating +        # around at start because the systray isn't rendered yet. +        if self._ui_eip_visible ^ visible: +            self.ui.eipWidget.setVisible(visible) +            self.ui.lineUnderEIP.setVisible(visible) +            self._eip_menu.setVisible(visible) +            self._ui_eip_visible = visible      def _set_label_offline(self):          """ @@ -771,7 +924,7 @@ class MainWindow(QtGui.QMainWindow):          systrayMenu.addSeparator()          eip_status_label = "{0}: {1}".format(self._eip_name, self.tr("OFF")) -        eip_menu = systrayMenu.addMenu(eip_status_label) +        self._eip_menu = eip_menu = systrayMenu.addMenu(eip_status_label)          eip_menu.addAction(self._action_eip_startstop)          self._eip_status.set_eip_status_menu(eip_menu)          systrayMenu.addSeparator() @@ -787,10 +940,21 @@ class MainWindow(QtGui.QMainWindow):          self._mail_status.set_systray(self._systray)          self._eip_status.set_systray(self._systray) +        if self._start_hidden: +            hello = lambda: self._systray.showMessage( +                self.tr('Hello!'), +                self.tr('Bitmask has started in the tray.')) +            # we wait for the systray to be ready +            reactor.callLater(1, hello) + +    @QtCore.Slot(int)      def _tray_activated(self, reason=None):          """ -        SLOT -        TRIGGER: self._systray.activated +        TRIGGERS: +            self._systray.activated + +        :param reason: the reason why the tray got activated. +        :type reason: int          Displays the context menu from the tray icon          """ @@ -817,10 +981,11 @@ class MainWindow(QtGui.QMainWindow):          visible = self.isVisible() and self.isActiveWindow()          self._action_visible.setText(get_action(visible)) +    @QtCore.Slot()      def _toggle_visible(self):          """ -        SLOT -        TRIGGER: self._action_visible.triggered +        TRIGGERS: +            self._action_visible.triggered          Toggles the window visibility          """ @@ -864,10 +1029,11 @@ class MainWindow(QtGui.QMainWindow):          if state is not None:              self.restoreState(state) +    @QtCore.Slot()      def _about(self):          """ -        SLOT -        TRIGGERS: self.ui.action_about_leap.triggered +        TRIGGERS: +            self.ui.action_about_leap.triggered          Display the About Bitmask dialog          """ @@ -891,10 +1057,11 @@ class MainWindow(QtGui.QMainWindow):                      "<a href='https://leap.se'>More about LEAP"                      "</a>") % (VERSION, VERSION_HASH[:10], greet)) +    @QtCore.Slot()      def _help(self):          """ -        SLOT -        TRIGGERS: self.ui.action_help.triggered +        TRIGGERS: +            self.ui.action_help.triggered          Display the Bitmask help dialog.          """ @@ -991,10 +1158,11 @@ class MainWindow(QtGui.QMainWindow):          provider = self._login_widget.get_selected_provider()          self._backend.setup_provider(provider) +    @QtCore.Slot(dict)      def _load_provider_config(self, data):          """ -        SLOT -        TRIGGER: self._backend.signaler.prov_download_provider_info +        TRIGGERS: +            self._backend.signaler.prov_download_provider_info          Once the provider config has been downloaded, this loads the          self._provider_config instance with it and starts the second @@ -1009,8 +1177,9 @@ class MainWindow(QtGui.QMainWindow):              self._backend.provider_bootstrap(selected_provider)          else:              logger.error(data[self._backend.ERROR_KEY]) -            self._login_widget.set_enabled(True) +            self._login_problem_provider() +    @QtCore.Slot()      def _login_problem_provider(self):          """          Warns the user about a problem with the provider during login. @@ -1019,11 +1188,11 @@ class MainWindow(QtGui.QMainWindow):              self.tr("Unable to login: Problem with provider"))          self._login_widget.set_enabled(True) +    @QtCore.Slot()      def _login(self):          """ -        SLOT          TRIGGERS: -          self._login_widget.login +            self._login_widget.login          Starts the login sequence. Which involves bootstrapping the          selected provider if the selection is valid (not empty), then @@ -1043,50 +1212,33 @@ class MainWindow(QtGui.QMainWindow):              self.offline_mode_bypass_login.emit()          else:              leap_assert(self._provider_config, "We need a provider config") +            self.ui.action_create_new_account.setEnabled(False)              if self._login_widget.start_login():                  self._download_provider_config() -    def _login_errback(self, failure): +    @QtCore.Slot(unicode) +    def _authentication_error(self, msg):          """ -        Error handler for the srpauth.authenticate method. +        TRIGGERS: +            Signaler.srp_auth_error +            Signaler.srp_auth_server_error +            Signaler.srp_auth_connection_error +            Signaler.srp_auth_bad_user_or_password -        :param failure: failure object that Twisted generates -        :type failure: twisted.python.failure.Failure -        """ -        # NOTE: this behavior needs to be managed through the signaler, -        # as we are doing with the prov_cancelled_setup signal. -        # After we move srpauth to the backend, we need to update this. -        logger.error("Error logging in, {0!r}".format(failure)) - -        if failure.check(CancelledError): -            logger.debug("Defer cancelled.") -            failure.trap(Exception) -            self._set_login_cancelled() -            return -        elif failure.check(srpauth.SRPAuthBadUserOrPassword): -            msg = self.tr("Invalid username or password.") -        elif failure.check(srpauth.SRPAuthBadStatusCode, -                           srpauth.SRPAuthenticationError, -                           srpauth.SRPAuthVerificationFailed, -                           srpauth.SRPAuthNoSessionId, -                           srpauth.SRPAuthNoSalt, srpauth.SRPAuthNoB, -                           srpauth.SRPAuthBadDataFromServer, -                           srpauth.SRPAuthJSONDecodeError): -            msg = self.tr("There was a server problem with authentication.") -        elif failure.check(srpauth.SRPAuthConnectionError): -            msg = self.tr("Could not establish a connection.") -        else: -            # this shouldn't happen, but just in case. -            msg = self.tr("Unknown error: {0!r}".format(failure.value)) +        Handle the authentication errors. +        :param msg: the message to show to the user. +        :type msg: unicode +        """          self._login_widget.set_status(msg)          self._login_widget.set_enabled(True) +        self.ui.action_create_new_account.setEnabled(True) +    @QtCore.Slot()      def _cancel_login(self):          """ -        SLOT          TRIGGERS: -          self._login_widget.cancel_login +            self._login_widget.cancel_login          Stops the login sequence.          """ @@ -1097,21 +1249,18 @@ class MainWindow(QtGui.QMainWindow):          """          Cancel the running defers to avoid app blocking.          """ +        # XXX: Should we stop all the backend defers?          self._backend.cancel_setup_provider() - -        if self._login_defer is not None: -            logger.debug("Cancelling login defer.") -            self._login_defer.cancel() -            self._login_defer = None +        self._backend.cancel_login()          if self._soledad_defer is not None:              logger.debug("Cancelling soledad defer.")              self._soledad_defer.cancel()              self._soledad_defer = None +    @QtCore.Slot()      def _set_login_cancelled(self):          """ -        SLOT          TRIGGERS:              Signaler.prov_cancelled_setup fired by              self._backend.cancel_setup_provider() @@ -1122,10 +1271,11 @@ class MainWindow(QtGui.QMainWindow):          self._login_widget.set_status(self.tr("Log in cancelled by the user."))          self._login_widget.set_enabled(True) +    @QtCore.Slot(dict)      def _provider_config_loaded(self, data):          """ -        SLOT -        TRIGGER: self._backend.signaler.prov_check_api_certificate +        TRIGGERS: +            self._backend.signaler.prov_check_api_certificate          Once the provider configuration is loaded, this starts the SRP          authentication @@ -1136,27 +1286,19 @@ class MainWindow(QtGui.QMainWindow):              username = self._login_widget.get_user()              password = self._login_widget.get_password() -            self._hide_unsupported_services() +            self._show_hide_unsupported_services() -            if self._srp_auth is None: -                self._srp_auth = SRPAuth(self._provider_config) -                self._srp_auth.authentication_finished.connect( -                    self._authentication_finished) -                self._srp_auth.logout_ok.connect(self._logout_ok) -                self._srp_auth.logout_error.connect(self._logout_error) - -            self._login_defer = self._srp_auth.authenticate(username, password) -            self._login_defer.addErrback(self._login_errback) +            domain = self._provider_config.get_domain() +            self._backend.login(domain, username, password)          else: -            self._login_widget.set_status( -                "Unable to login: Problem with provider")              logger.error(data[self._backend.ERROR_KEY]) -            self._login_widget.set_enabled(True) +            self._login_problem_provider() +    @QtCore.Slot()      def _authentication_finished(self):          """ -        SLOT -        TRIGGER: self._srp_auth.authentication_finished +        TRIGGERS: +            self._srp_auth.authentication_finished          Once the user is properly authenticated, try starting the EIP          service @@ -1168,8 +1310,8 @@ class MainWindow(QtGui.QMainWindow):          domain = self._provider_config.get_domain()          full_user_id = make_address(user, domain)          self._mail_conductor.userid = full_user_id -        self._login_defer = None          self._start_eip_bootstrap() +        self.ui.action_create_new_account.setEnabled(True)          # if soledad/mail is enabled:          if MX_SERVICE in self._enabled_services: @@ -1179,6 +1321,9 @@ class MainWindow(QtGui.QMainWindow):              self._soledad_bootstrapper.soledad_failed.connect(                  lambda: btn_enabled(True)) +        if not self._get_best_provider_config().provides_mx(): +            self._set_mx_visible(False) +      def _start_eip_bootstrap(self):          """          Changes the stackedWidget index to the EIP status one and @@ -1267,12 +1412,12 @@ class MainWindow(QtGui.QMainWindow):      ###################################################################      # Service control methods: soledad +    @QtCore.Slot(dict)      def _soledad_intermediate_stage(self, data):          # TODO missing param docstring          """ -        SLOT          TRIGGERS: -          self._soledad_bootstrapper.download_config +            self._soledad_bootstrapper.download_config          If there was a problem, displays it, otherwise it does nothing.          This is used for intermediate bootstrapping stages, in case @@ -1285,29 +1430,13 @@ class MainWindow(QtGui.QMainWindow):              # that sets the global status              logger.error("Soledad failed to start: %s" %                           (data[self._soledad_bootstrapper.ERROR_KEY],)) -            self._retry_soledad_connection() - -    def _retry_soledad_connection(self): -        """ -        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() -            # XXX should cancel the existing socket --- this -            # is avoiding a clean termination. -            self._maybe_run_soledad_setup_checks() -        else: -            logger.warning("Max number of soledad initialization " -                           "retries reached.") +    @QtCore.Slot(dict)      def _soledad_bootstrapped_stage(self, data):          """ -        SLOT          TRIGGERS: -          self._soledad_bootstrapper.gen_key -          self._soledad_bootstrapper.local_only_ready +            self._soledad_bootstrapper.gen_key +            self._soledad_bootstrapper.local_only_ready          If there was a problem, displays it, otherwise it does nothing.          This is used for intermediate bootstrapping stages, in case @@ -1346,7 +1475,6 @@ class MainWindow(QtGui.QMainWindow):      @QtCore.Slot()      def _start_smtp_bootstrapping(self):          """ -        SLOT          TRIGGERS:              self.soledad_ready          """ @@ -1354,20 +1482,15 @@ class MainWindow(QtGui.QMainWindow):              logger.debug("not starting smtp in offline mode")              return -        # TODO for simmetry, this should be called start_smtp_service -        # (and delegate all the checks to the conductor)          if self._provides_mx_and_enabled(): -            self._mail_conductor.smtp_bootstrapper.run_smtp_setup_checks( -                self._provider_config, -                self._mail_conductor.smtp_config, -                download_if_needed=True) +            self._mail_conductor.start_smtp_service(self._provider_config, +                                                    download_if_needed=True)      # XXX --- should remove from here, and connecte directly to the state      # machine.      @QtCore.Slot()      def _stop_smtp_service(self):          """ -        SLOT          TRIGGERS:              self.logout          """ @@ -1380,7 +1503,6 @@ class MainWindow(QtGui.QMainWindow):      @QtCore.Slot()      def _start_imap_service(self):          """ -        SLOT          TRIGGERS:              self.soledad_ready          """ @@ -1410,7 +1532,6 @@ class MainWindow(QtGui.QMainWindow):      @QtCore.Slot()      def _fetch_incoming_mail(self):          """ -        SLOT          TRIGGERS:              self.mail_client_logged_in          """ @@ -1420,7 +1541,6 @@ class MainWindow(QtGui.QMainWindow):      @QtCore.Slot()      def _stop_imap_service(self):          """ -        SLOT          TRIGGERS:              self.logout          """ @@ -1467,11 +1587,11 @@ class MainWindow(QtGui.QMainWindow):          self._action_eip_startstop.setEnabled(True)      @QtCore.Slot() -    def _on_eip_connected(self): +    def _on_eip_connection_connected(self):          """ -        SLOT          TRIGGERS:              self._eip_status.eip_connection_connected +          Emits the EIPConnection.qtsigs.connected_signal          This is a little workaround for connecting the vpn-connected @@ -1480,9 +1600,14 @@ class MainWindow(QtGui.QMainWindow):          """          self._eip_connection.qtsigs.connected_signal.emit() -        # check for connectivity          provider_config = self._get_best_provider_config()          domain = provider_config.get_domain() + +        self._eip_status.set_provider(domain) +        self._settings.set_defaultprovider(domain) +        self._already_started_eip = True + +        # check for connectivity          self._check_name_resolution(domain)      def _check_name_resolution(self, domain): @@ -1534,137 +1659,112 @@ class MainWindow(QtGui.QMainWindow):          Tries to autostart EIP          """          settings = self._settings - -        if not self._update_eip_enabled_status(): -            return -          default_provider = settings.get_defaultprovider()          self._enabled_services = settings.get_enabled_services(              default_provider)          loaded = self._provisional_provider_config.load(              provider.get_provider_path(default_provider)) -        if loaded: +        if loaded and settings.get_autostart_eip():              # 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,              # or we can try only if it fails.              self._maybe_start_eip() -        else: +        elif settings.get_autostart_eip():              # XXX: Display a proper message to the user              self.eip_needs_login.emit()              logger.error("Unable to load %s config, cannot autostart." %                           (default_provider,))      @QtCore.Slot() -    def _start_eip(self): +    def _start_EIP(self):          """ -        SLOT -        TRIGGERS: -          self._eip_connection.qtsigs.do_connect_signal -          (via state machine) -        or called from _finish_eip_bootstrap -          Starts EIP          """ -        provider_config = self._get_best_provider_config() -        provider = provider_config.get_domain()          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. +        # 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) +        self._backend.start_eip() -        if not loaded: -            eip_status_label = self.tr("Could not load {0} configuration.") -            eip_status_label = eip_status_label.format(self._eip_name) -            self._eip_status.set_eip_status(eip_status_label, 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 +    @QtCore.Slot() +    def _on_eip_connection_aborted(self): +        """ +        TRIGGERS: +            Signaler.eip_connection_aborted +        """ +        logger.error("Tried to start EIP but cannot find any " +                     "available provider!") -        try: -            # XXX move this to EIPConductor -            host, port = get_openvpn_management() -            self._vpn.start(eipconfig=self._eip_config, -                            providerconfig=provider_config, -                            socket_host=host, -                            socket_port=port) -            self._settings.set_defaultprovider(provider) - -            # XXX move to the state machine too -            self._eip_status.set_provider(provider) - -        # TODO refactor exceptions so they provide translatable -        # usef-facing messages. -        except EIPNoPolkitAuthAgentAvailable: -            self._eip_status.set_eip_status( -                # XXX this should change to polkit-kde where -                # applicable. -                self.tr("We could not find any " -                        "authentication " -                        "agent in your system.<br/>" -                        "Make sure you have " -                        "<b>polkit-gnome-authentication-" -                        "agent-1</b> " -                        "running and try again."), -                error=True) -            self._set_eipstatus_off() -        except EIPNoTunKextLoaded: -            self._eip_status.set_eip_status( -                self.tr("{0} cannot be started because " -                        "the tuntap extension is not installed properly " -                        "in your system.").format(self._eip_name)) -            self._set_eipstatus_off() -        except EIPNoPkexecAvailable: -            self._eip_status.set_eip_status( -                self.tr("We could not find <b>pkexec</b> " -                        "in your system."), -                error=True) -            self._set_eipstatus_off() -        except OpenVPNNotFoundException: -            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._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._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."), -                error=True) -            self._set_eipstatus_off() -        except VPNLauncherException as e: -            # XXX We should implement again translatable exceptions so -            # we can pass a translatable string to the panel (usermessage attr) -            self._eip_status.set_eip_status("%s" % (e,), error=True) -            self._set_eipstatus_off() -        else: -            self._already_started_eip = True +        eip_status_label = self.tr("Could not load {0} configuration.") +        eip_status_label = eip_status_label.format(self._eip_name) +        self._eip_status.set_eip_status(eip_status_label, error=True) + +        # signal connection_aborted to state machine: +        qtsigs = self._eip_connection.qtsigs +        qtsigs.connection_aborted_signal.emit() + +    def _on_eip_openvpn_already_running(self): +        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() + +    def _on_eip_alien_openvpn_already_running(self): +        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."), +            error=True) +        self._set_eipstatus_off() + +    def _on_eip_openvpn_not_found_error(self): +        self._eip_status.set_eip_status( +            self.tr("We could not find openvpn binary."), +            error=True) +        self._set_eipstatus_off() + +    def _on_eip_vpn_launcher_exception(self): +        # XXX We should implement again translatable exceptions so +        # we can pass a translatable string to the panel (usermessage attr) +        self._eip_status.set_eip_status("VPN Launcher error.", error=True) +        self._set_eipstatus_off() + +    def _on_eip_no_polkit_agent_error(self): +        self._eip_status.set_eip_status( +            # XXX this should change to polkit-kde where +            # applicable. +            self.tr("We could not find any authentication agent in your " +                    "system.<br/>Make sure you have" +                    "<b>polkit-gnome-authentication-agent-1</b> running and" +                    "try again."), +            error=True) +        self._set_eipstatus_off() + +    def _on_eip_no_pkexec_error(self): +        self._eip_status.set_eip_status( +            self.tr("We could not find <b>pkexec</b> in your system."), +            error=True) +        self._set_eipstatus_off() + +    def _on_eip_no_tun_kext_error(self): +        self._eip_status.set_eip_status( +            self.tr("{0} cannot be started because the tuntap extension is " +                    "not installed properly in your " +                    "system.").format(self._eip_name)) +        self._set_eipstatus_off()      @QtCore.Slot()      def _stop_eip(self):          """ -        SLOT          TRIGGERS: -          self._eip_connection.qtsigs.do_disconnect_signal -          (via state machine) -        or called from _eip_finished +          self._eip_connection.qtsigs.do_disconnect_signal (via state machine)          Stops vpn process and makes gui adjustments to reflect          the change of state. @@ -1673,7 +1773,7 @@ class MainWindow(QtGui.QMainWindow):          :type abnormal: bool          """          self.user_stopped_eip = True -        self._vpn.terminate() +        self._backend.stop_eip()          self._set_eipstatus_off(False)          self._already_started_eip = False @@ -1692,7 +1792,6 @@ class MainWindow(QtGui.QMainWindow):      def _on_eip_network_unreachable(self):          # XXX Should move to EIP Conductor          """ -        SLOT          TRIGGERS:              self._eip_connection.qtsigs.network_unreachable @@ -1706,7 +1805,7 @@ class MainWindow(QtGui.QMainWindow):      def _do_eip_restart(self):          # XXX Should move to EIP Conductor          """ -        SLOT +        TRIGGERS:              self._eip_connection.qtsigs.process_restart          Restart the connection. @@ -1714,7 +1813,7 @@ class MainWindow(QtGui.QMainWindow):          # for some reason, emitting the do_disconnect/do_connect          # signals hangs the UI.          self._stop_eip() -        QtCore.QTimer.singleShot(2000, self._start_eip) +        QtCore.QTimer.singleShot(2000, self._start_EIP)      def _set_eipstatus_off(self, error=True):          """ @@ -1724,11 +1823,11 @@ class MainWindow(QtGui.QMainWindow):          self._eip_status.set_eip_status("", error=error)          self._eip_status.set_eip_status_icon("error") +    @QtCore.Slot(int)      def _eip_finished(self, exitCode):          """ -        SLOT          TRIGGERS: -          self._vpn.process_finished +            Signaler.eip_process_finished          Triggered when the EIP/VPN process finishes to set the UI          accordingly. @@ -1764,12 +1863,14 @@ class MainWindow(QtGui.QMainWindow):                  "because you did not authenticate properly.")              eip_status_label = eip_status_label.format(self._eip_name)              self._eip_status.set_eip_status(eip_status_label, error=True) -            self._vpn.killit()              signal = qtsigs.connection_aborted_signal +            self._backend.terminate_eip()          elif exitCode != 0 or not self.user_stopped_eip:              eip_status_label = self.tr("{0} finished in an unexpected manner!")              eip_status_label = eip_status_label.format(self._eip_name) +            self._eip_status.eip_stopped() +            self._eip_status.set_eip_status_icon("error")              self._eip_status.set_eip_status(eip_status_label, error=True)              signal = qtsigs.connection_died_signal @@ -1787,17 +1888,14 @@ class MainWindow(QtGui.QMainWindow):          Start the EIP bootstrapping sequence if the client is configured to          do so.          """ -        leap_assert(self._eip_bootstrapper, "We need an eip bootstrapper!") - -        provider_config = self._get_best_provider_config() -          if self._provides_eip_and_enabled() and not self._already_started_eip:              # XXX this should be handled by the state machine.              self._eip_status.set_eip_status(                  self.tr("Starting...")) -            self._eip_bootstrapper.run_eip_setup_checks( -                provider_config, -                download_if_needed=True) + +            domain = self._login_widget.get_selected_provider() +            self._backend.setup_eip(domain) +              self._already_started_eip = True              # we want to start soledad anyway after a certain timeout if eip              # fails to come up @@ -1816,45 +1914,33 @@ class MainWindow(QtGui.QMainWindow):              # eip will not start, so we start soledad anyway              self._maybe_run_soledad_setup_checks() +    @QtCore.Slot(dict)      def _finish_eip_bootstrap(self, data):          """ -        SLOT -        TRIGGER: self._eip_bootstrapper.download_client_certificate +        TRIGGERS: +            self._backend.signaler.eip_client_certificate_ready          Starts the VPN thread if the eip configuration is properly          loaded          """ -        leap_assert(self._eip_config, "We need an eip config!") -        passed = data[self._eip_bootstrapper.PASSED_KEY] +        passed = data[self._backend.PASSED_KEY]          if not passed:              error_msg = self.tr("There was a problem with the provider")              self._eip_status.set_eip_status(error_msg, error=True) -            logger.error(data[self._eip_bootstrapper.ERROR_KEY]) +            logger.error(data[self._backend.ERROR_KEY])              self._already_started_eip = False              return -        provider_config = self._get_best_provider_config() -        domain = provider_config.get_domain() - -        # XXX  move check to _start_eip ? -        loaded = eipconfig.load_eipconfig_if_needed( -            provider_config, self._eip_config, domain) - -        if loaded: -            # DO START EIP Connection! -            self._eip_connection.qtsigs.do_connect_signal.emit() -        else: -            eip_status_label = self.tr("Could not load {0} configuration.") -            eip_status_label = eip_status_label.format(self._eip_name) -            self._eip_status.set_eip_status(eip_status_label, error=True) +        # DO START EIP Connection! +        self._eip_connection.qtsigs.do_connect_signal.emit() +    @QtCore.Slot(dict)      def _eip_intermediate_stage(self, data):          # TODO missing param          """ -        SLOT          TRIGGERS: -          self._eip_bootstrapper.download_config +            self._backend.signaler.eip_config_ready          If there was a problem, displays it, otherwise it does nothing.          This is used for intermediate bootstrapping stages, in case @@ -1896,12 +1982,11 @@ class MainWindow(QtGui.QMainWindow):      @QtCore.Slot()      def _logout(self):          """ -        SLOT -        TRIGGER: self._login_widget.logout +        TRIGGERS: +            self._login_widget.logout          Starts the logout sequence          """ -        self._soledad_bootstrapper.cancel_bootstrap()          setProxiedObject(self._soledad, None)          self._cancel_ongoing_defers() @@ -1911,13 +1996,14 @@ class MainWindow(QtGui.QMainWindow):          # XXX: If other defers are doing authenticated stuff, this          # might conflict with those. CHECK! -        threads.deferToThread(self._srp_auth.logout) +        self._backend.logout()          self.logout.emit() +    @QtCore.Slot()      def _logout_error(self):          """ -        SLOT -        TRIGGER: self._srp_auth.logout_error +        TRIGGER: +            self._srp_auth.logout_error          Inform the user about a logout error.          """ @@ -1926,10 +2012,11 @@ class MainWindow(QtGui.QMainWindow):          self._login_widget.set_status(              self.tr("Something went wrong with the logout.")) +    @QtCore.Slot()      def _logout_ok(self):          """ -        SLOT -        TRIGGER: self._srp_auth.logout_ok +        TRIGGER: +            self._srp_auth.logout_ok          Switches the stackedWidget back to the login stage after          logging out @@ -1941,15 +2028,16 @@ class MainWindow(QtGui.QMainWindow):          self._login_widget.logged_out()          self._mail_status.mail_state_disabled() +        self._show_hide_unsupported_services() + +    @QtCore.Slot(dict)      def _intermediate_stage(self, data):          # TODO this method name is confusing as hell.          """ -        SLOT          TRIGGERS: -          self._backend.signaler.prov_name_resolution -          self._backend.signaler.prov_https_connection -          self._backend.signaler.prov_download_ca_cert -          self._eip_bootstrapper.download_config +            self._backend.signaler.prov_name_resolution +            self._backend.signaler.prov_https_connection +            self._backend.signaler.prov_download_ca_cert          If there was a problem, displays it, otherwise it does nothing.          This is used for intermediate bootstrapping stages, in case @@ -1957,10 +2045,8 @@ class MainWindow(QtGui.QMainWindow):          """          passed = data[self._backend.PASSED_KEY]          if not passed: -            msg = self.tr("Unable to connect: Problem with provider") -            self._login_widget.set_status(msg) -            self._login_widget.set_enabled(True)              logger.error(data[self._backend.ERROR_KEY]) +            self._login_problem_provider()      #      # window handling methods @@ -1974,9 +2060,9 @@ class MainWindow(QtGui.QMainWindow):              raise_window_ack()          self.raise_window.emit() +    @QtCore.Slot()      def _do_raise_mainwindow(self):          """ -        SLOT          TRIGGERS:              self._on_raise_window_event @@ -2012,11 +2098,8 @@ class MainWindow(QtGui.QMainWindow):          self._stop_imap_service() -        if self._srp_auth is not None: -            if self._srp_auth.get_session_id() is not None or \ -               self._srp_auth.get_token() is not None: -                # XXX this can timeout after loong time: See #3368 -                self._srp_auth.logout() +        if self._logged_user is not None: +            self._backend.logout()          if self._soledad_bootstrapper.soledad is not None:              logger.debug("Closing soledad...") @@ -2025,14 +2108,27 @@ class MainWindow(QtGui.QMainWindow):              logger.error("No instance of soledad was found.")          logger.debug('Terminating vpn') -        self._vpn.terminate(shutdown=True) +        self._backend.stop_eip(shutdown=True) + +        # We need to give some time to the ongoing signals for shutdown +        # to come into action. This needs to be solved using +        # back-communication from backend. +        QtCore.QTimer.singleShot(3000, self._shutdown) +    def _shutdown(self): +        """ +        Actually shutdown. +        """          self._cancel_ongoing_defers()          # TODO missing any more cancels?          logger.debug('Cleaning pidfiles')          self._cleanup_pidfiles() +        if self._quit_callback: +            self._quit_callback() + +        logger.debug('Bye.')      def quit(self):          """ @@ -2041,11 +2137,24 @@ class MainWindow(QtGui.QMainWindow):          # TODO separate the shutting down of services from the          # UI stuff. +        # first thing to do quitting, hide the mainwindow and show tooltip. +        self.hide() +        if self._systray is not None: +            self._systray.showMessage( +                self.tr('Quitting...'), +                self.tr('The app is quitting, please wait.')) + +        # explicitly process events to display tooltip immediately +        QtCore.QCoreApplication.processEvents() +          # Set this in case that the app is hidden          QtGui.QApplication.setQuitOnLastWindowClosed(True) -        self._backend.stop()          self._cleanup_and_quit() + +        # We queue the call to stop since we need to wait until EIP is stopped. +        # Otherwise we may exit leaving an unmanaged openvpn process. +        reactor.callLater(0, self._backend.stop)          self._really_quit = True          if self._wizard: @@ -2055,8 +2164,3 @@ class MainWindow(QtGui.QMainWindow):              self._logger_window.close()          self.close() - -        if self._quit_callback: -            self._quit_callback() - -        logger.debug('Bye.') diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index b2cc2236..2947c5db 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -29,7 +29,6 @@ from leap.bitmask.provider import get_provider_path  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 SRPAuthBadUserOrPassword  from leap.bitmask.util.password import basic_password_checks  from leap.bitmask.services import get_supported  from leap.bitmask.config.providerconfig import ProviderConfig @@ -44,25 +43,33 @@ class PreferencesWindow(QtGui.QDialog):      """      preferences_saved = QtCore.Signal() -    def __init__(self, parent, srp_auth, provider_config, soledad, domain): +    def __init__(self, parent, backend, provider_config, +                 soledad, username, domain):          """          :param parent: parent object of the PreferencesWindow.          :parent type: QWidget -        :param srp_auth: SRPAuth object configured in the main app. -        :type srp_auth: SRPAuth +        :param backend: Backend being used +        :type backend: Backend          :param provider_config: ProviderConfig object.          :type provider_config: ProviderConfig          :param soledad: Soledad instance          :type soledad: Soledad +        :param username: the user set in the login widget +        :type username: unicode          :param domain: the selected domain in the login widget          :type domain: unicode          """          QtGui.QDialog.__init__(self, parent)          self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") -        self._srp_auth = srp_auth +        self._backend = backend          self._settings = LeapSettings()          self._soledad = soledad +        self._provider_config = provider_config +        self._username = username +        self._domain = domain + +        self._backend_connect()          # Load UI          self.ui = Ui_Preferences() @@ -82,43 +89,60 @@ class PreferencesWindow(QtGui.QDialog):          else:              self._add_configured_providers() -        pw_enabled = False - -        # check if the user is logged in -        if srp_auth is not None and srp_auth.get_token() is not None: -            # check if provider has 'mx' ... -            if provider_config.provides_mx(): -                enabled_services = self._settings.get_enabled_services(domain) -                mx_name = get_service_display_name(MX_SERVICE) - -                # ... and if the user have it enabled -                if MX_SERVICE not in enabled_services: -                    msg = self.tr("You need to enable {0} in order to change " -                                  "the password.".format(mx_name)) -                    self._set_password_change_status(msg, error=True) -                else: -                    if sameProxiedObjects(self._soledad, None): -                        msg = self.tr( -                            "You need to wait until {0} is ready in " -                            "order to change the password.".format(mx_name)) -                        self._set_password_change_status(msg) -                    else: -                        # Soledad is bootstrapped -                        pw_enabled = True -            else: -                pw_enabled = True -        else: -            msg = self.tr( -                "In order to change your password you need to be logged in.") -            self._set_password_change_status(msg) +        self._backend.get_logged_in_status()          self._select_provider_by_name(domain) +    @QtCore.Slot() +    def _is_logged_in(self): +        """ +        TRIGGERS: +            Signaler.srp_status_logged_in + +        Actions to perform is the user is logged in. +        """ +        settings = self._settings +        pw_enabled = True + +        # check if provider has 'mx' ... +        # TODO: we should move this to the backend. +        if self._provider_config.provides_mx(): +            enabled_services = settings.get_enabled_services(self._domain) +            mx_name = get_service_display_name(MX_SERVICE) + +            # ... and if the user have it enabled +            if MX_SERVICE not in enabled_services: +                msg = self.tr("You need to enable {0} in order to change " +                              "the password.".format(mx_name)) +                self._set_password_change_status(msg, error=True) +                pw_enabled = False +            else: +                # check if Soledad is bootstrapped +                if sameProxiedObjects(self._soledad, None): +                    msg = self.tr( +                        "You need to wait until {0} is ready in " +                        "order to change the password.".format(mx_name)) +                    self._set_password_change_status(msg) +                    pw_enabled = False +          self.ui.gbPasswordChange.setEnabled(pw_enabled) +    @QtCore.Slot() +    def _not_logged_in(self): +        """ +        TRIGGERS: +            Signaler.srp_status_not_logged_in + +        Actions to perform if the user is not logged in. +        """ +        msg = self.tr( +            "In order to change your password you need to be logged in.") +        self._set_password_change_status(msg) +        self.ui.gbPasswordChange.setEnabled(False) + +    @QtCore.Slot()      def set_soledad_ready(self):          """ -        SLOT          TRIGGERS:              parent.soledad_ready @@ -163,15 +187,15 @@ class PreferencesWindow(QtGui.QDialog):          self.ui.leNewPassword2.setEnabled(not disable)          self.ui.pbChangePassword.setEnabled(not disable) +    @QtCore.Slot()      def _change_password(self):          """ -        SLOT          TRIGGERS:              self.ui.pbChangePassword.clicked          Changes the user's password if the inputboxes are correctly filled.          """ -        username = self._srp_auth.get_username() +        username = self._username          current_password = self.ui.leCurrentPassword.text()          new_password = self.ui.leNewPassword.text()          new_password2 = self.ui.leNewPassword2.text() @@ -185,19 +209,17 @@ class PreferencesWindow(QtGui.QDialog):              return          self._set_changing_password(True) -        d = self._srp_auth.change_password(current_password, new_password) -        d.addCallback(partial(self._change_password_success, new_password)) -        d.addErrback(self._change_password_problem) +        self._backend.change_password(current_password, new_password) -    def _change_password_success(self, new_password, _): +    @QtCore.Slot() +    def _change_password_ok(self):          """ -        Callback used to display a successfully performed action. +        TRIGGERS: +            self._backend.signaler.srp_password_change_ok -        :param new_password: the new password for the user. -        :type new_password: str. -        :param _: the returned data from self._srp_auth.change_password -                  Ignored +        Callback used to display a successfully changed password.          """ +        new_password = self.ui.leNewPassword.text()          logger.debug("SRP password changed successfully.")          try:              self._soledad.change_passphrase(new_password) @@ -211,24 +233,21 @@ class PreferencesWindow(QtGui.QDialog):          self._clear_password_inputs()          self._set_changing_password(False) -    def _change_password_problem(self, failure): -        """ -        Errback called if there was a problem with the deferred. -        Also is used to display an error message. - -        :param failure: the cause of the method failed. -        :type failure: twisted.python.Failure +    @QtCore.Slot(unicode) +    def _change_password_problem(self, msg):          """ -        logger.error("Error changing password: %s", (failure, )) -        problem = self.tr("There was a problem changing the password.") - -        if failure.check(SRPAuthBadUserOrPassword): -            problem = self.tr("You did not enter a correct current password.") +        TRIGGERS: +            self._backend.signaler.srp_password_change_error +            self._backend.signaler.srp_password_change_badpw -        self._set_password_change_status(problem, error=True) +        Callback used to display an error on changing password. +        :param msg: the message to show to the user. +        :type msg: unicode +        """ +        logger.error("Error changing password") +        self._set_password_change_status(msg, error=True)          self._set_changing_password(False) -        failure.trap(Exception)      def _clear_password_inputs(self):          """ @@ -272,10 +291,12 @@ class PreferencesWindow(QtGui.QDialog):          provider_index = self.ui.cbProvidersServices.findText(name)          self.ui.cbProvidersServices.setCurrentIndex(provider_index) +    @QtCore.Slot(str, int)      def _service_selection_changed(self, service, state):          """ -        SLOT -        TRIGGER: service_checkbox.stateChanged +        TRIGGERS: +            service_checkbox.stateChanged +          Adds the service to the state if the state is checked, removes          it otherwise @@ -294,9 +315,9 @@ class PreferencesWindow(QtGui.QDialog):          # We hide the maybe-visible status label after a change          self.ui.lblProvidersServicesStatus.setVisible(False) +    @QtCore.Slot(str)      def _populate_services(self, domain):          """ -        SLOT          TRIGGERS:              self.ui.cbProvidersServices.currentIndexChanged[unicode] @@ -353,9 +374,9 @@ class PreferencesWindow(QtGui.QDialog):                  logger.error("Something went wrong while trying to "                               "load service %s" % (service,)) +    @QtCore.Slot(str)      def _save_enabled_services(self, provider):          """ -        SLOT          TRIGGERS:              self.ui.pbSaveServices.clicked @@ -387,3 +408,22 @@ class PreferencesWindow(QtGui.QDialog):              provider_config = None          return provider_config + +    def _backend_connect(self): +        """ +        Helper to connect to backend signals +        """ +        sig = self._backend.signaler + +        sig.srp_status_logged_in.connect(self._is_logged_in) +        sig.srp_status_not_logged_in.connect(self._not_logged_in) + +        sig.srp_password_change_ok.connect(self._change_password_ok) + +        pwd_change_error = lambda: self._change_password_problem( +            self.tr("There was a problem changing the password.")) +        sig.srp_password_change_error.connect(pwd_change_error) + +        pwd_change_badpw = lambda: self._change_password_problem( +            self.tr("You did not enter a correct current password.")) +        sig.srp_password_change_badpw.connect(pwd_change_badpw) diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py index 93731ce0..31938a70 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -562,6 +562,8 @@ class ConnectionMachineBuilder(object):          if action:              off.assignProperty(                  action, 'text', off_label) +            off.assignProperty( +                action, 'enabled', True)          off.setObjectName(_OFF)          states[_OFF] = off diff --git a/src/leap/bitmask/gui/twisted_main.py b/src/leap/bitmask/gui/twisted_main.py index 1e876c57..dfd69033 100644 --- a/src/leap/bitmask/gui/twisted_main.py +++ b/src/leap/bitmask/gui/twisted_main.py @@ -19,14 +19,23 @@ Main functions for integration of twisted reactor  """  import logging -from twisted.internet import error - -# Resist the temptation of putting the import reactor here, -# it will raise an "reactor already imported" error. +from twisted.internet import error, reactor +from PySide import QtCore  logger = logging.getLogger(__name__) +def stop(): +    QtCore.QCoreApplication.sendPostedEvents() +    QtCore.QCoreApplication.flush() +    try: +        reactor.stop() +        logger.debug('Twisted reactor stopped') +    except error.ReactorNotRunning: +        logger.debug('Twisted reactor not running') +    logger.debug("Done stopping all the things.") + +  def quit(app):      """      Stop the mainloop. @@ -34,9 +43,4 @@ def quit(app):      :param app: the main qt QApplication instance.      :type app: QtCore.QApplication      """ -    from twisted.internet import reactor -    logger.debug('Stopping twisted reactor') -    try: -        reactor.callLater(0, reactor.stop) -    except error.ReactorNotRunning: -        logger.debug('Reactor not running') +    reactor.callLater(0, stop) diff --git a/src/leap/bitmask/gui/ui/advanced_key_management.ui b/src/leap/bitmask/gui/ui/advanced_key_management.ui index 1112670f..3b567347 100644 --- a/src/leap/bitmask/gui/ui/advanced_key_management.ui +++ b/src/leap/bitmask/gui/ui/advanced_key_management.ui @@ -1,7 +1,7 @@  <?xml version="1.0" encoding="UTF-8"?>  <ui version="4.0">   <class>AdvancedKeyManagement</class> - <widget class="QWidget" name="AdvancedKeyManagement"> + <widget class="QDialog" name="AdvancedKeyManagement">    <property name="geometry">     <rect>      <x>0</x> diff --git a/src/leap/bitmask/gui/ui/login.ui b/src/leap/bitmask/gui/ui/login.ui index f5725d5a..216eca9e 100644 --- a/src/leap/bitmask/gui/ui/login.ui +++ b/src/leap/bitmask/gui/ui/login.ui @@ -215,7 +215,7 @@         <number>0</number>        </property>        <property name="bottomMargin"> -       <number>0</number> +       <number>12</number>        </property>        <item row="0" column="0" colspan="2">         <widget class="QLabel" name="lblUser"> diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index e2c1a16e..020a58e2 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -24,6 +24,7 @@ from functools import partial  from PySide import QtCore, QtGui +from leap.bitmask.config import flags  from leap.bitmask.config.leapsettings import LeapSettings  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.provider import get_provider_path @@ -145,8 +146,7 @@ class Wizard(QtGui.QWizard):      @QtCore.Slot()      def _wizard_finished(self):          """ -        SLOT -        TRIGGER: +        TRIGGERS:              self.finished          This method is called when the wizard is accepted or rejected. @@ -210,15 +210,19 @@ class Wizard(QtGui.QWizard):      def get_services(self):          return self._selected_services -    @QtCore.Slot() +    @QtCore.Slot(unicode)      def _enable_check(self, reset=True):          """ -        SLOT -        TRIGGER: +        TRIGGERS:              self.ui.lnProvider.textChanged          Enables/disables the 'check' button in the SELECT_PROVIDER_PAGE          depending on the lnProvider content. + +        :param reset: this contains the text of the line edit, and when is +                      called directly defines whether we want to reset the +                      checks. +        :type reset: unicode or bool          """          enabled = len(self.ui.lnProvider.text()) != 0          enabled = enabled or self.ui.rbExistingProvider.isChecked() @@ -282,11 +286,11 @@ class Wizard(QtGui.QWizard):          # register button          self.ui.btnRegister.setVisible(visible) +    @QtCore.Slot()      def _registration_finished(self):          """ -        SLOT          TRIGGERS: -          self._backend.signaler.srp_registration_finished +            self._backend.signaler.srp_registration_finished          The registration has finished successfully, so we do some final steps.          """ @@ -308,11 +312,11 @@ class Wizard(QtGui.QWizard):          self.page(self.REGISTER_USER_PAGE).set_completed()          self.button(QtGui.QWizard.BackButton).setEnabled(False) +    @QtCore.Slot()      def _registration_failed(self):          """ -        SLOT          TRIGGERS: -          self._backend.signaler.srp_registration_failed +            self._backend.signaler.srp_registration_failed          The registration has failed, so we report the problem.          """ @@ -322,11 +326,11 @@ class Wizard(QtGui.QWizard):          self._set_register_status(error_msg, error=True)          self.ui.btnRegister.setEnabled(True) +    @QtCore.Slot()      def _registration_taken(self):          """ -        SLOT          TRIGGERS: -          self._backend.signaler.srp_registration_taken +            self._backend.signaler.srp_registration_taken          The requested username is taken, warn the user about that.          """ @@ -358,7 +362,8 @@ class Wizard(QtGui.QWizard):          self.ui.lblProviderSelectStatus.setText("")          self._domain = None          self.button(QtGui.QWizard.NextButton).setEnabled(False) -        self.page(self.SELECT_PROVIDER_PAGE).set_completed(False) +        self.page(self.SELECT_PROVIDER_PAGE).set_completed( +            flags.SKIP_WIZARD_CHECKS)      def _reset_provider_setup(self):          """ @@ -368,12 +373,12 @@ class Wizard(QtGui.QWizard):          self.ui.lblCheckCaFpr.setPixmap(None)          self.ui.lblCheckApiCert.setPixmap(None) +    @QtCore.Slot()      def _check_provider(self):          """ -        SLOT          TRIGGERS: -          self.ui.btnCheck.clicked -          self.ui.lnProvider.returnPressed +            self.ui.btnCheck.clicked +            self.ui.lnProvider.returnPressed          Starts the checks for a given provider          """ @@ -390,17 +395,23 @@ class Wizard(QtGui.QWizard):          self.ui.grpCheckProvider.setVisible(True)          self.ui.btnCheck.setEnabled(False) -        self.ui.lnProvider.setEnabled(False) + +        # Disable provider widget +        if self.ui.rbNewProvider.isChecked(): +            self.ui.lnProvider.setEnabled(False) +        else: +            self.ui.cbProviders.setEnabled(False) +          self.button(QtGui.QWizard.BackButton).clearFocus()          self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON)          self._provider_select_defer = self._backend.\              setup_provider(self._domain) +    @QtCore.Slot(bool)      def _skip_provider_checks(self, skip):          """ -        SLOT -        Triggered: +        TRIGGERS:              self.ui.rbExistingProvider.toggled          Allows the user to move to the next page without make any checks, @@ -441,10 +452,11 @@ class Wizard(QtGui.QWizard):              label.setPixmap(self.ERROR_ICON)              logger.error(error) +    @QtCore.Slot(dict)      def _name_resolution(self, data):          """ -        SLOT -        TRIGGER: self._backend.signaler.prov_name_resolution +        TRIGGERS: +            self._backend.signaler.prov_name_resolution          Sets the status for the name resolution check          """ @@ -460,10 +472,11 @@ class Wizard(QtGui.QWizard):          self.ui.btnCheck.setEnabled(not passed)          self.ui.lnProvider.setEnabled(not passed) +    @QtCore.Slot(dict)      def _https_connection(self, data):          """ -        SLOT -        TRIGGER: self._backend.signaler.prov_https_connection +        TRIGGERS: +            self._backend.signaler.prov_https_connection          Sets the status for the https connection check          """ @@ -479,10 +492,11 @@ class Wizard(QtGui.QWizard):          self.ui.btnCheck.setEnabled(not passed)          self.ui.lnProvider.setEnabled(not passed) +    @QtCore.Slot(dict)      def _download_provider_info(self, data):          """ -        SLOT -        TRIGGER: self._backend.signaler.prov_download_provider_info +        TRIGGERS: +            self._backend.signaler.prov_download_provider_info          Sets the status for the provider information download          check. Since this check is the last of this set, it also @@ -506,12 +520,18 @@ class Wizard(QtGui.QWizard):                               "</b></font>")              self.ui.lblProviderSelectStatus.setText(status)          self.ui.btnCheck.setEnabled(True) -        self.ui.lnProvider.setEnabled(True) +        # Enable provider widget +        if self.ui.rbNewProvider.isChecked(): +            self.ui.lnProvider.setEnabled(True) +        else: +            self.ui.cbProviders.setEnabled(True) + +    @QtCore.Slot(dict)      def _download_ca_cert(self, data):          """ -        SLOT -        TRIGGER: self._backend.signaler.prov_download_ca_cert +        TRIGGERS: +            self._backend.signaler.prov_download_ca_cert          Sets the status for the download of the CA certificate check          """ @@ -520,10 +540,11 @@ class Wizard(QtGui.QWizard):          if passed:              self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON) +    @QtCore.Slot(dict)      def _check_ca_fingerprint(self, data):          """ -        SLOT -        TRIGGER: self._backend.signaler.prov_check_ca_fingerprint +        TRIGGERS: +            self._backend.signaler.prov_check_ca_fingerprint          Sets the status for the CA fingerprint check          """ @@ -532,10 +553,11 @@ class Wizard(QtGui.QWizard):          if passed:              self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON) +    @QtCore.Slot(dict)      def _check_api_certificate(self, data):          """ -        SLOT -        TRIGGER: self._backend.signaler.prov_check_api_certificate +        TRIGGERS: +            self._backend.signaler.prov_check_api_certificate          Sets the status for the API certificate check. Also finishes          the provider bootstrapper thread since it's not needed anymore @@ -545,10 +567,12 @@ class Wizard(QtGui.QWizard):                              True, self.SETUP_PROVIDER_PAGE)          self._provider_setup_ok = True +    @QtCore.Slot(str, int)      def _service_selection_changed(self, service, state):          """ -        SLOT -        TRIGGER: service_checkbox.stateChanged +        TRIGGERS: +            service_checkbox.stateChanged +          Adds the service to the state if the state is checked, removes          it otherwise @@ -593,12 +617,16 @@ class Wizard(QtGui.QWizard):                      self.tr("Something went wrong while trying to "                              "load service %s" % (service,))) +    @QtCore.Slot(int)      def _current_id_changed(self, pageId):          """ -        SLOT -        TRIGGER: self.currentIdChanged +        TRIGGERS: +            self.currentIdChanged          Prepares the pages when they appear + +        :param pageId: the new current page id. +        :type pageId: int          """          if pageId == self.SELECT_PROVIDER_PAGE:              self._clear_register_widgets() diff --git a/src/leap/bitmask/platform_init/initializers.py b/src/leap/bitmask/platform_init/initializers.py index d93efbc6..f2710c58 100644 --- a/src/leap/bitmask/platform_init/initializers.py +++ b/src/leap/bitmask/platform_init/initializers.py @@ -366,15 +366,8 @@ def _linux_install_missing_scripts(badexec, notfound):          fd, tempscript = tempfile.mkstemp(prefix="leap_installer-")          polfd, pol_tempfile = tempfile.mkstemp(prefix="leap_installer-")          try: -            path = launcher.OPENVPN_BIN_PATH -            policy_contents = privilege_policies.get_policy_contents(path) - -            with os.fdopen(polfd, 'w') as f: -                f.write(policy_contents) -              pkexec = first(launcher.maybe_pkexec()) -            scriptlines = launcher.cmd_for_missing_scripts(installer_path, -                                                           pol_tempfile) +            scriptlines = launcher.cmd_for_missing_scripts(installer_path)              with os.fdopen(fd, 'w') as f:                  f.write(scriptlines) diff --git a/src/leap/bitmask/provider/providerbootstrapper.py b/src/leap/bitmask/provider/providerbootstrapper.py index 2a519206..6cdfe4f4 100644 --- a/src/leap/bitmask/provider/providerbootstrapper.py +++ b/src/leap/bitmask/provider/providerbootstrapper.py @@ -88,6 +88,8 @@ class ProviderBootstrapper(AbstractBootstrapper):          self._domain = None          self._provider_config = None          self._download_if_needed = False +        if signaler is not None: +            self._cancel_signal = signaler.PROV_CANCELLED_SETUP      @property      def verify(self): diff --git a/src/leap/bitmask/services/abstractbootstrapper.py b/src/leap/bitmask/services/abstractbootstrapper.py index fc6bd3e9..77929b75 100644 --- a/src/leap/bitmask/services/abstractbootstrapper.py +++ b/src/leap/bitmask/services/abstractbootstrapper.py @@ -78,6 +78,7 @@ class AbstractBootstrapper(QtCore.QObject):          self._signal_to_emit = None          self._err_msg = None          self._signaler = signaler +        self._cancel_signal = None      def _gui_errback(self, failure):          """ @@ -95,7 +96,8 @@ class AbstractBootstrapper(QtCore.QObject):          if failure.check(CancelledError):              logger.debug("Defer cancelled.")              failure.trap(Exception) -            self._signaler.signal(self._signaler.PROV_CANCELLED_SETUP) +            if self._signaler is not None and self._cancel_signal is not None: +                self._signaler.signal(self._cancel_signal)              return          if self._signal_to_emit: diff --git a/src/leap/bitmask/services/eip/eipbootstrapper.py b/src/leap/bitmask/services/eip/eipbootstrapper.py index 5a238a1c..c77977ce 100644 --- a/src/leap/bitmask/services/eip/eipbootstrapper.py +++ b/src/leap/bitmask/services/eip/eipbootstrapper.py @@ -20,8 +20,6 @@ EIP bootstrapping  import logging  import os -from PySide import QtCore -  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.crypto.certs import download_client_cert  from leap.bitmask.services import download_service_config @@ -41,17 +39,21 @@ class EIPBootstrapper(AbstractBootstrapper):      If a check fails, the subsequent checks are not executed      """ -    # All dicts returned are of the form -    # {"passed": bool, "error": str} -    download_config = QtCore.Signal(dict) -    download_client_certificate = QtCore.Signal(dict) +    def __init__(self, signaler=None): +        """ +        Constructor for the EIP bootstrapper object -    def __init__(self): -        AbstractBootstrapper.__init__(self) +        :param signaler: Signaler object used to receive notifications +                         from the backend +        :type signaler: Signaler +        """ +        AbstractBootstrapper.__init__(self, signaler)          self._provider_config = None          self._eip_config = None          self._download_if_needed = False +        if signaler is not None: +            self._cancel_signal = signaler.EIP_CANCELLED_SETUP      def _download_config(self, *args):          """ @@ -114,9 +116,9 @@ class EIPBootstrapper(AbstractBootstrapper):          self._download_if_needed = download_if_needed          cb_chain = [ -            (self._download_config, self.download_config), +            (self._download_config, self._signaler.EIP_CONFIG_READY),              (self._download_client_certificates, -             self.download_client_certificate) +             self._signaler.EIP_CLIENT_CERTIFICATE_READY)          ]          return self.addCallbackChain(cb_chain) diff --git a/src/leap/bitmask/services/eip/linuxvpnlauncher.py b/src/leap/bitmask/services/eip/linuxvpnlauncher.py index d24e7ae7..1f0813e0 100644 --- a/src/leap/bitmask/services/eip/linuxvpnlauncher.py +++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py @@ -25,7 +25,6 @@ import sys  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 @@ -36,6 +35,8 @@ from leap.bitmask.util import first  logger = logging.getLogger(__name__) +COM = commands +  class EIPNoPolkitAuthAgentAvailable(VPNLauncherException):      pass @@ -64,9 +65,10 @@ def _is_auth_agent_running():      """      # the [x] thing is to avoid grep match itself      polkit_options = [ -        'ps aux | grep polkit-[g]nome-authentication-agent-1', -        'ps aux | grep polkit-[k]de-authentication-agent-1', -        'ps aux | grep [l]xpolkit' +        'ps aux | grep "polkit-[g]nome-authentication-agent-1"', +        'ps aux | grep "polkit-[k]de-authentication-agent-1"', +        'ps aux | grep "polkit-[m]ate-authentication-agent-1"', +        'ps aux | grep "[l]xpolkit"'      ]      is_running = [commands.getoutput(cmd) for cmd in polkit_options]      return any(is_running) @@ -84,35 +86,39 @@ def _try_to_launch_agent():          # 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. +        logger.debug("Trying to launch polkit agent")          subprocess.call(["python -m leap.bitmask.util.polkit_agent"],                          shell=True, env=env)      except Exception as exc:          logger.exception(exc) +SYSTEM_CONFIG = "/etc/leap" +leapfile = lambda f: "%s/%s" % (SYSTEM_CONFIG, f) + +  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) +    BITMASK_ROOT = "/usr/sbin/bitmask-root"      # 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,) +    if flags.STANDALONE: +        OPENVPN_BIN_PATH = "/usr/sbin/leap-openvpn" +    else: +        OPENVPN_BIN_PATH = "/usr/sbin/openvpn" +      POLKIT_PATH = LinuxPolicyChecker.get_polkit_path() -    OTHER_FILES = (POLKIT_PATH, ) + +    if flags.STANDALONE: +        RESOLVCONF_BIN_PATH = "/usr/local/sbin/leap-resolvconf" +    else: +        # this only will work with debian/ubuntu distros. +        RESOLVCONF_BIN_PATH = "/sbin/resolvconf" + +    # XXX openvpn binary TOO +    OTHER_FILES = (POLKIT_PATH, BITMASK_ROOT, OPENVPN_BIN_PATH, +                   RESOLVCONF_BIN_PATH)      @classmethod      def maybe_pkexec(kls): @@ -130,7 +136,7 @@ class LinuxVPNLauncher(VPNLauncher):          if _is_pkexec_in_system():              if not _is_auth_agent_running():                  _try_to_launch_agent() -                time.sleep(0.5) +                time.sleep(2)              if _is_auth_agent_running():                  pkexec_possibilities = which(kls.PKEXEC_BIN)                  leap_assert(len(pkexec_possibilities) > 0, @@ -145,28 +151,6 @@ class LinuxVPNLauncher(VPNLauncher):              raise EIPNoPkexecAvailable()      @classmethod -    def missing_other_files(kls): -        """ -        'Extend' the VPNLauncher's missing_other_files to check if the polkit -        files is outdated, in the case of an standalone bundle. -        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() - -        if flags.STANDALONE: -            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):          """ @@ -197,6 +181,10 @@ class LinuxVPNLauncher(VPNLauncher):          command = super(LinuxVPNLauncher, kls).get_vpn_command(              eipconfig, providerconfig, socket_host, socket_port, openvpn_verb) +        command.insert(0, kls.BITMASK_ROOT) +        command.insert(1, "openvpn") +        command.insert(2, "start") +          pkexec = kls.maybe_pkexec()          if pkexec:              command.insert(0, first(pkexec)) @@ -204,26 +192,44 @@ class LinuxVPNLauncher(VPNLauncher):          return command      @classmethod -    def cmd_for_missing_scripts(kls, frompath, pol_file): +    def cmd_for_missing_scripts(kls, frompath):          """          Returns a sh script that can copy the missing files. -        :param frompath: The path where the up/down scripts live +        :param frompath: The path where the helper files 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 +        # no system config for now +        # sys_config = kls.SYSTEM_CONFIG +        (polkit_file, openvpn_bin_file, +         bitmask_root_file, resolvconf_bin_file) = map( +            lambda p: os.path.split(p)[-1], +            (kls.POLKIT_PATH, kls.OPENVPN_BIN_PATH, +             kls.BITMASK_ROOT, kls.RESOLVCONF_BIN_PATH))          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 += 'mkdir -p /usr/local/sbin\n' + +        cmd += 'cp "%s" "%s"\n' % (os.path.join(frompath, polkit_file), +                                   kls.POLKIT_PATH)          cmd += 'chmod 644 "%s"\n' % (kls.POLKIT_PATH, ) +        cmd += 'cp "%s" "%s"\n' % (os.path.join(frompath, bitmask_root_file), +                                   kls.BITMASK_ROOT) +        cmd += 'chmod 744 "%s"\n' % (kls.BITMASK_ROOT, ) + +        if flags.STANDALONE: +            cmd += 'cp "%s" "%s"\n' % ( +                os.path.join(frompath, openvpn_bin_file), +                kls.OPENVPN_BIN_PATH) +            cmd += 'chmod 744 "%s"\n' % (kls.POLKIT_PATH, ) + +            cmd += 'cp "%s" "%s"\n' % ( +                os.path.join(frompath, resolvconf_bin_file), +                kls.RESOLVCONF_BIN_PATH) +            cmd += 'chmod 744 "%s"\n' % (kls.POLKIT_PATH, )          return cmd      @classmethod diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py index 99cae7f9..dcb48e8a 100644 --- a/src/leap/bitmask/services/eip/vpnlauncher.py +++ b/src/leap/bitmask/services/eip/vpnlauncher.py @@ -25,14 +25,12 @@ 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.platform_init import IS_LINUX  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__) @@ -107,10 +105,43 @@ class VPNLauncher(object):      @classmethod      @abstractmethod +    def get_gateways(kls, eipconfig, providerconfig): +        """ +        Return the selected gateways for a given provider, looking at the EIP +        config file. + +        :param eipconfig: eip configuration object +        :type eipconfig: EIPConfig + +        :param providerconfig: provider specific configuration +        :type providerconfig: ProviderConfig + +        :rtype: list +        """ +        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('No gateway was found!') + +        logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) +        return gateways + +    @classmethod +    @abstractmethod      def get_vpn_command(kls, eipconfig, providerconfig,                          socket_host, socket_port, openvpn_verb=1):          """ -        Returns the platform dependant vpn launching command +        Return the platform-dependant vpn command for launching openvpn.          Might raise:              OpenVPNNotFoundException, @@ -134,16 +165,19 @@ class VPNLauncher(object):          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: +        # XXX this still has to be changed on osx and windows accordingly +        #kwargs = {} +        #openvpn_possibilities = which(kls.OPENVPN_BIN, **kwargs) +        #if not openvpn_possibilities: +            #raise OpenVPNNotFoundException() +        #openvpn = first(openvpn_possibilities) +        # ----------------------------------------- +        if not os.path.isfile(kls.OPENVPN_BIN_PATH): +            logger.warning("Could not find openvpn bin in path %s" % ( +                kls.OPENVPN_BIN_PATH))              raise OpenVPNNotFoundException() -        openvpn = first(openvpn_possibilities) +        openvpn = kls.OPENVPN_BIN_PATH          args = []          args += [ @@ -154,22 +188,7 @@ class VPNLauncher(object):          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('No gateway was found!') - -        logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) +        gateways = kls.get_gateways(eipconfig, providerconfig)          for gw in gateways:              args += ['--remote', gw, '1194', 'udp'] @@ -177,11 +196,6 @@ class VPNLauncher(object):          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', @@ -194,15 +208,6 @@ class VPNLauncher(object):          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 @@ -226,20 +231,6 @@ class VPNLauncher(object):                      '--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), @@ -271,13 +262,18 @@ class VPNLauncher(object):          :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] +        # FIXME +        # XXX remove method when we ditch UPDOWN in osx and win too +        if IS_LINUX: +            return [] +        else: +            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): diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index 5c100036..1559ea8b 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -21,6 +21,7 @@ import logging  import os  import shutil  import socket +import subprocess  import sys  from itertools import chain, repeat @@ -33,14 +34,14 @@ except ImportError:      # psutil >= 2.0.0      from psutil import AccessDenied as psutil_AccessDenied -from PySide import QtCore - +from leap.bitmask.config import flags  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.services.eip import get_vpn_launcher +from leap.bitmask.services.eip import linuxvpnlauncher  from leap.bitmask.services.eip.eipconfig import EIPConfig  from leap.bitmask.services.eip.udstelnet import UDSTelnet  from leap.bitmask.util import first -from leap.bitmask.platform_init import IS_MAC +from leap.bitmask.platform_init import IS_MAC, IS_LINUX  from leap.common.check import leap_assert, leap_assert_type  logger = logging.getLogger(__name__) @@ -52,28 +53,6 @@ from twisted.internet import error as internet_error  from twisted.internet.task import LoopingCall -class VPNSignals(QtCore.QObject): -    """ -    These are the signals that we use to let the UI know -    about the events we are polling. -    They are instantiated in the VPN object and passed along -    till the VPNProcess. -    """ -    # signals for the process -    state_changed = QtCore.Signal(dict) -    status_changed = QtCore.Signal(dict) -    process_finished = QtCore.Signal(int) - -    # signals that come from parsing -    # openvpn output -    network_unreachable = QtCore.Signal() -    process_restart_tls = QtCore.Signal() -    process_restart_ping = QtCore.Signal() - -    def __init__(self): -        QtCore.QObject.__init__(self) - -  class VPNObserver(object):      """      A class containing different patterns in the openvpn output that @@ -90,19 +69,13 @@ class VPNObserver(object):          'PROCESS_RESTART_TLS': (              "SIGUSR1[soft,tls-error]",),          'PROCESS_RESTART_PING': ( -            "SIGUSR1[soft,ping-restart]",), +            "SIGTERM[soft,ping-restart]",),          'INITIALIZATION_COMPLETED': (              "Initialization Sequence Completed",),      } -    def __init__(self, qtsigs): -        """ -        Initializer. Keeps a reference to the passed qtsigs object -        :param qtsigs: an object containing the different qt signals to -                       be used to communicate with different parts of -                       the application (the EIP state machine, for instance). -        """ -        self._qtsigs = qtsigs +    def __init__(self, signaler=None): +        self._signaler = signaler      def watch(self, line):          """ @@ -123,27 +96,29 @@ class VPNObserver(object):              return          sig = self._get_signal(event) -        if sig: -            sig.emit() +        if sig is not None: +            self._signaler.signal(sig)              return          else: -            logger.debug( -                'We got %s event from openvpn output but we ' -                'could not find a matching signal for it.' -                % event) +            logger.debug('We got %s event from openvpn output but we could ' +                         'not find a matching signal for it.' % event)      def _get_signal(self, event):          """          Tries to get the matching signal from the eip signals          objects based on the name of the passed event (in lowercase) -        :param event: the name of the event that we want to get a signal -                      for +        :param event: the name of the event that we want to get a signal for          :type event: str -        :returns: a QtSignal, or None -        :rtype: QtSignal or None +        :returns: a Signaler signal or None +        :rtype: str or None          """ -        return getattr(self._qtsigs, event.lower(), None) +        signals = { +            "network_unreachable": self._signaler.EIP_NETWORK_UNREACHABLE, +            "process_restart_tls": self._signaler.EIP_PROCESS_RESTART_TLS, +            "process_restart_ping": self._signaler.EIP_PROCESS_RESTART_PING, +        } +        return signals.get(event.lower())  class OpenVPNAlreadyRunning(Exception): @@ -181,14 +156,11 @@ class VPN(object):          self._vpnproc = None          self._pollers = []          self._reactor = reactor -        self._qtsigs = VPNSignals() -        # XXX should get it from config.flags -        self._openvpn_verb = kwargs.get(self.OPENVPN_VERB, None) +        self._signaler = kwargs['signaler'] +        self._openvpn_verb = flags.OPENVPN_VERBOSITY -    @property -    def qtsigs(self): -        return self._qtsigs +        self._user_stopped = False      def start(self, *args, **kwargs):          """ @@ -200,19 +172,28 @@ class VPN(object):          :param kwargs: kwargs to be passed to the VPNProcess          :type kwargs: dict          """ +        logger.debug('VPN: start') +        self._user_stopped = False          self._stop_pollers() -        kwargs['qtsigs'] = self.qtsigs          kwargs['openvpn_verb'] = self._openvpn_verb +        kwargs['signaler'] = self._signaler          # start the main vpn subprocess          vpnproc = VPNProcess(*args, **kwargs) -                             #qtsigs=self.qtsigs, -                             #openvpn_verb=self._openvpn_verb)          if vpnproc.get_openvpn_process():              logger.info("Another vpn process is running. Will try to stop it.")              vpnproc.stop_if_already_running() +        # we try to bring the firewall up +        if IS_LINUX: +            gateways = vpnproc.getGateways() +            firewall_up = self._launch_firewall(gateways) +            if not firewall_up: +                logger.error("Could not bring firewall up, " +                             "aborting openvpn launch.") +                return +          cmd = vpnproc.getCommand()          env = os.environ          for key, val in vpnproc.vpn_env.items(): @@ -230,9 +211,37 @@ class VPN(object):          self._pollers.extend(poll_list)          self._start_pollers() +    def _launch_firewall(self, gateways): +        """ +        Launch the firewall using the privileged wrapper. + +        :param gateways: +        :type gateways: list + +        :returns: True if the exitcode of calling the root helper in a +                  subprocess is 0. +        :rtype: bool +        """ +        # XXX could check for wrapper existence, check it's root owned etc. +        # XXX could check that the iptables rules are in place. + +        BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT +        exitCode = subprocess.call(["pkexec", +                                    BM_ROOT, "firewall", "start"] + gateways) +        return True if exitCode is 0 else False + +    def _tear_down_firewall(self): +        """ +        Tear the firewall down using the privileged wrapper. +        """ +        BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT +        exitCode = subprocess.call(["pkexec", +                                    BM_ROOT, "firewall", "stop"]) +        return True if exitCode is 0 else False +      def _kill_if_left_alive(self, tries=0):          """ -        Check if the process is still alive, and sends a +        Check if the process is still alive, and send a          SIGKILL after a timeout period.          :param tries: counter of tries, used in recursion @@ -242,6 +251,15 @@ class VPN(object):          while tries < self.TERMINATE_MAXTRIES:              if self._vpnproc.transport.pid is None:                  logger.debug("Process has been happily terminated.") + +                # we try to tear the firewall down +                if IS_LINUX and self._user_stopped: +                    firewall_down = self._tear_down_firewall() +                    if firewall_down: +                        logger.debug("Firewall down") +                    else: +                        logger.warning("Could not tear firewall down") +                  return              else:                  logger.debug("Process did not die, waiting...") @@ -262,8 +280,11 @@ class VPN(object):          Sends a kill signal to the process.          """          self._stop_pollers() -        self._vpnproc.aborted = True -        self._vpnproc.killProcess() +        if self._vpnproc is None: +            logger.debug("There's no vpn process running to kill.") +        else: +            self._vpnproc.aborted = True +            self._vpnproc.killProcess()      def terminate(self, shutdown=False):          """ @@ -275,6 +296,10 @@ class VPN(object):          from twisted.internet import reactor          self._stop_pollers() +        # We assume that the only valid shutodowns are initiated +        # by an user action. +        self._user_stopped = shutdown +          # First we try to be polite and send a SIGTERM...          if self._vpnproc:              self._sentterm = True @@ -282,12 +307,17 @@ class VPN(object):              # ...but we also trigger a countdown to be unpolite              # if strictly needed. - -            # XXX Watch out! This will fail NOW since we are running -            # openvpn as root as a workaround for some connection issues.              reactor.callLater(                  self.TERMINATE_WAIT, self._kill_if_left_alive) +            if shutdown: +                if IS_LINUX and self._user_stopped: +                    firewall_down = self._tear_down_firewall() +                    if firewall_down: +                        logger.debug("Firewall down") +                    else: +                        logger.warning("Could not tear firewall down") +      def _start_pollers(self):          """          Iterate through the registered observers @@ -328,37 +358,21 @@ class VPNManager(object):      POLL_TIME = 2.5 if IS_MAC else 1.0      CONNECTION_RETRY_TIME = 1 -    TS_KEY = "ts" -    STATUS_STEP_KEY = "status_step" -    OK_KEY = "ok" -    IP_KEY = "ip" -    REMOTE_KEY = "remote" - -    TUNTAP_READ_KEY = "tun_tap_read" -    TUNTAP_WRITE_KEY = "tun_tap_write" -    TCPUDP_READ_KEY = "tcp_udp_read" -    TCPUDP_WRITE_KEY = "tcp_udp_write" -    AUTH_READ_KEY = "auth_read" - -    def __init__(self, qtsigs=None): +    def __init__(self, signaler=None):          """          Initializes the VPNManager. -        :param qtsigs: a QObject containing the Qt signals used by the UI -                       to give feedback about state changes. -        :type qtsigs: QObject +        :param signaler: Signaler object used to send notifications to the +                         backend +        :type signaler: backend.Signaler          """          from twisted.internet import reactor          self._reactor = reactor          self._tn = None -        self._qtsigs = qtsigs +        self._signaler = signaler          self._aborted = False      @property -    def qtsigs(self): -        return self._qtsigs - -    @property      def aborted(self):          return self._aborted @@ -552,17 +566,10 @@ class VPNManager(object):                  continue              ts, status_step, ok, ip, remote = parts -            state_dict = { -                self.TS_KEY: ts, -                self.STATUS_STEP_KEY: status_step, -                self.OK_KEY: ok, -                self.IP_KEY: ip, -                self.REMOTE_KEY: remote -            } - -            if state_dict != self._last_state: -                self.qtsigs.state_changed.emit(state_dict) -                self._last_state = state_dict +            state = status_step +            if state != self._last_state: +                self._signaler.signal(self._signaler.EIP_STATE_CHANGED, state) +                self._last_state = state      def _parse_status_and_notify(self, output):          """ @@ -575,9 +582,7 @@ class VPNManager(object):          """          tun_tap_read = ""          tun_tap_write = "" -        tcp_udp_read = "" -        tcp_udp_write = "" -        auth_read = "" +          for line in output:              stripped = line.strip()              if stripped.endswith("STATISTICS") or stripped == "END": @@ -585,28 +590,24 @@ class VPNManager(object):              parts = stripped.split(",")              if len(parts) < 2:                  continue -            if parts[0].strip() == "TUN/TAP read bytes": -                tun_tap_read = parts[1] -            elif parts[0].strip() == "TUN/TAP write bytes": -                tun_tap_write = parts[1] -            elif parts[0].strip() == "TCP/UDP read bytes": -                tcp_udp_read = parts[1] -            elif parts[0].strip() == "TCP/UDP write bytes": -                tcp_udp_write = parts[1] -            elif parts[0].strip() == "Auth read bytes": -                auth_read = parts[1] - -        status_dict = { -            self.TUNTAP_READ_KEY: tun_tap_read, -            self.TUNTAP_WRITE_KEY: tun_tap_write, -            self.TCPUDP_READ_KEY: tcp_udp_read, -            self.TCPUDP_WRITE_KEY: tcp_udp_write, -            self.AUTH_READ_KEY: auth_read -        } -        if status_dict != self._last_status: -            self.qtsigs.status_changed.emit(status_dict) -            self._last_status = status_dict +            text, value = parts +            # text can be: +            #   "TUN/TAP read bytes" +            #   "TUN/TAP write bytes" +            #   "TCP/UDP read bytes" +            #   "TCP/UDP write bytes" +            #   "Auth read bytes" + +            if text == "TUN/TAP read bytes": +                tun_tap_read = value  # download +            elif text == "TUN/TAP write bytes": +                tun_tap_write = value  # upload + +        status = (tun_tap_read, tun_tap_write) +        if status != self._last_status: +            self._signaler.signal(self._signaler.EIP_STATUS_CHANGED, status) +            self._last_status = status      def get_state(self):          """ @@ -754,7 +755,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):      """      def __init__(self, eipconfig, providerconfig, socket_host, socket_port, -                 qtsigs, openvpn_verb): +                 signaler, openvpn_verb):          """          :param eipconfig: eip configuration object          :type eipconfig: EIPConfig @@ -769,18 +770,17 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):                              socket, or port otherwise          :type socket_port: str -        :param qtsigs: a QObject containing the Qt signals used to notify the -                       UI. -        :type qtsigs: QObject +        :param signaler: Signaler object used to receive notifications to the +                         backend +        :type signaler: backend.Signaler          :param openvpn_verb: the desired level of verbosity in the                               openvpn invocation          :type openvpn_verb: int          """ -        VPNManager.__init__(self, qtsigs=qtsigs) +        VPNManager.__init__(self, signaler=signaler)          leap_assert_type(eipconfig, EIPConfig)          leap_assert_type(providerconfig, ProviderConfig) -        leap_assert_type(qtsigs, QtCore.QObject)          #leap_assert(not self.isRunning(), "Starting process more than once!") @@ -799,7 +799,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):          # the parameter around.          self._openvpn_verb = openvpn_verb -        self._vpn_observer = VPNObserver(qtsigs) +        self._vpn_observer = VPNObserver(signaler)      # processProtocol methods @@ -835,7 +835,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):          exit_code = reason.value.exitCode          if isinstance(exit_code, int):              logger.debug("processExited, status %d" % (exit_code,)) -        self.qtsigs.process_finished.emit(exit_code) +        self._signaler.signal(self._signaler.EIP_PROCESS_FINISHED, exit_code)          self._alive = False      def processEnded(self, reason): @@ -889,9 +889,20 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):              if not isinstance(c, str):                  command[i] = c.encode(encoding) -        logger.debug("Running VPN with command: {0}".format(command)) +        logger.debug("Running VPN with command: ") +        logger.debug("{0}".format(" ".join(command)))          return command +    def getGateways(self): +        """ +        Get the gateways from the appropiate launcher. + +        :rtype: list +        """ +        gateways = self._launcher.get_gateways( +            self._eipconfig, self._providerconfig) +        return gateways +      # shutdown      def killProcess(self): diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py index 79f324dc..1766a39d 100644 --- a/src/leap/bitmask/services/mail/conductor.py +++ b/src/leap/bitmask/services/mail/conductor.py @@ -18,22 +18,18 @@  Mail Services Conductor  """  import logging -import os -from PySide import QtCore  from zope.proxy import sameProxiedObjects  from leap.bitmask.gui import statemachines -from leap.bitmask.services.mail import imap  from leap.bitmask.services.mail import connection as mail_connection +from leap.bitmask.services.mail import imap  from leap.bitmask.services.mail.smtpbootstrapper import SMTPBootstrapper  from leap.bitmask.services.mail.smtpconfig import SMTPConfig -from leap.bitmask.util import is_file  from leap.common.check import leap_assert - -from leap.common.events import register as leap_register  from leap.common.events import events_pb2 as leap_events +from leap.common.events import register as leap_register  logger = logging.getLogger(__name__) @@ -167,10 +163,6 @@ class IMAPControl(object):  class SMTPControl(object): - -    PORT_KEY = "port" -    IP_KEY = "ip_address" -      def __init__(self):          """          Initializes smtp variables. @@ -178,12 +170,8 @@ class SMTPControl(object):          self.smtp_config = SMTPConfig()          self.smtp_connection = None          self.smtp_machine = None -        self._smtp_service = None -        self._smtp_port = None          self.smtp_bootstrapper = SMTPBootstrapper() -        self.smtp_bootstrapper.download_config.connect( -            self.smtp_bootstrapped_stage)          leap_register(signal=leap_events.SMTP_SERVICE_STARTED,                        callback=self._handle_smtp_events, @@ -200,101 +188,27 @@ class SMTPControl(object):          """          self.smtp_connection = smtp_connection -    def start_smtp_service(self, host, port, cert): +    def start_smtp_service(self, provider_config, download_if_needed=False):          """ -        Starts the smtp service. +        Starts the SMTP service. -        :param host: the hostname of the remove SMTP server. -        :type host: str -        :param port: the port of the remote SMTP server -        :type port: str -        :param cert: the client certificate for authentication -        :type cert: str +        :param provider_config: Provider configuration +        :type provider_config: ProviderConfig +        :param download_if_needed: True if it should check for mtime +                                   for the file +        :type download_if_needed: bool          """ -        # TODO Make the encrypted_only configurable -        # TODO pick local smtp port in a better way -        # TODO remove hard-coded port and let leap.mail set -        # the specific default.          self.smtp_connection.qtsigs.connecting_signal.emit() -        from leap.mail.smtp import setup_smtp_gateway -        self._smtp_service, self._smtp_port = setup_smtp_gateway( -            port=2013, -            userid=self.userid, -            keymanager=self._keymanager, -            smtp_host=host, -            smtp_port=port, -            smtp_cert=cert, -            smtp_key=cert, -            encrypted_only=False) +        self.smtp_bootstrapper.start_smtp_service( +            provider_config, self.smtp_config, self._keymanager, +            self.userid, download_if_needed)      def stop_smtp_service(self):          """ -        Stops the smtp service (port and factory). +        Stops the SMTP service.          """          self.smtp_connection.qtsigs.disconnecting_signal.emit() -        # TODO We should homogenize both services. -        if self._smtp_service is not None: -            logger.debug('Stopping smtp service.') -            self._smtp_port.stopListening() -            self._smtp_service.doStop() - -    @QtCore.Slot() -    def smtp_bootstrapped_stage(self, data): -        """ -        SLOT -        TRIGGERS: -          self.smtp_bootstrapper.download_config - -        If there was a problem, displays it, otherwise it does nothing. -        This is used for intermediate bootstrapping stages, in case -        they fail. - -        :param data: result from the bootstrapping stage for Soledad -        :type data: dict -        """ -        passed = data[self.smtp_bootstrapper.PASSED_KEY] -        if not passed: -            logger.error(data[self.smtp_bootstrapper.ERROR_KEY]) -            return -        logger.debug("Done bootstrapping SMTP") -        self.check_smtp_config() - -    def check_smtp_config(self): -        """ -        Checks smtp config and tries to download smtp client cert if needed. -        Currently called when smtp_bootstrapped_stage has successfuly finished. -        """ -        logger.debug("Checking SMTP config...") -        leap_assert(self.smtp_bootstrapper._provider_config, -                    "smtp bootstrapper does not have a provider_config") - -        provider_config = self.smtp_bootstrapper._provider_config -        smtp_config = self.smtp_config -        hosts = smtp_config.get_hosts() -        # TODO handle more than one host and define how to choose -        if len(hosts) > 0: -            hostname = hosts.keys()[0] -            logger.debug("Using hostname %s for SMTP" % (hostname,)) -            host = hosts[hostname][self.IP_KEY].encode("utf-8") -            port = hosts[hostname][self.PORT_KEY] - -            client_cert = smtp_config.get_client_cert_path( -                provider_config, -                about_to_download=True) - -            # XXX change this logic! -            # check_config should be called from within start_service, -            # and not the other way around. -            if not is_file(client_cert): -                self.smtp_bootstrapper._download_client_certificates() -            if os.path.isfile(client_cert): -                self.start_smtp_service(host, port, client_cert) -            else: -                logger.warning("Tried to download email client " -                               "certificate, but could not find any") - -        else: -            logger.warning("No smtp hosts configured") +        self.smtp_bootstrapper.stop_smtp_service()      # handle smtp events @@ -349,7 +263,7 @@ class MailConductor(IMAPControl, SMTPControl):          :param keymanager: a transparent proxy that eventually will point to a                             Keymanager Instance. -        :type soledad: zope.proxy.ProxyBase +        :type keymanager: zope.proxy.ProxyBase          """          IMAPControl.__init__(self)          SMTPControl.__init__(self) @@ -407,4 +321,5 @@ class MailConductor(IMAPControl, SMTPControl):          qtsigs.connecting_signal.connect(widget.mail_state_connecting)          qtsigs.disconnecting_signal.connect(widget.mail_state_disconnecting)          qtsigs.disconnected_signal.connect(widget.mail_state_disconnected) -        qtsigs.soledad_invalid_auth_token.connect(widget.soledad_invalid_auth_token) +        qtsigs.soledad_invalid_auth_token.connect( +            widget.soledad_invalid_auth_token) diff --git a/src/leap/bitmask/services/mail/smtpbootstrapper.py b/src/leap/bitmask/services/mail/smtpbootstrapper.py index 032d6357..7ecf8134 100644 --- a/src/leap/bitmask/services/mail/smtpbootstrapper.py +++ b/src/leap/bitmask/services/mail/smtpbootstrapper.py @@ -20,12 +20,13 @@ SMTP bootstrapping  import logging  import os -from PySide import QtCore -  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.crypto.certs import download_client_cert  from leap.bitmask.services import download_service_config  from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper +from leap.bitmask.services.mail.smtpconfig import SMTPConfig +from leap.bitmask.util import is_file +  from leap.common import certs as leap_certs  from leap.common.check import leap_assert, leap_assert_type  from leap.common.files import check_and_fix_urw_only @@ -33,27 +34,33 @@ from leap.common.files import check_and_fix_urw_only  logger = logging.getLogger(__name__) +class NoSMTPHosts(Exception): +    """This is raised when there is no SMTP host to use.""" + +  class SMTPBootstrapper(AbstractBootstrapper):      """      SMTP init procedure      """ -    # All dicts returned are of the form -    # {"passed": bool, "error": str} -    download_config = QtCore.Signal(dict) +    PORT_KEY = "port" +    IP_KEY = "ip_address"      def __init__(self):          AbstractBootstrapper.__init__(self)          self._provider_config = None          self._smtp_config = None +        self._userid = None          self._download_if_needed = False -    def _download_config(self, *args): +        self._smtp_service = None +        self._smtp_port = None + +    def _download_config_and_cert(self):          """ -        Downloads the SMTP config for the given provider +        Downloads the SMTP config and cert for the given provider.          """ -          leap_assert(self._provider_config,                      "We need a provider configuration!") @@ -66,63 +73,101 @@ class SMTPBootstrapper(AbstractBootstrapper):              self._session,              self._download_if_needed) -    def _download_client_certificates(self, *args): -        """ -        Downloads the SMTP client certificate for the given provider +        hosts = self._smtp_config.get_hosts() -        We actually are downloading the certificate for the same uri as -        for the EIP config, but we duplicate these bits to allow mail -        service to be working in a provider that does not offer EIP. -        """ -        # TODO factor out with eipboostrapper.download_client_certificates -        # TODO this shouldn't be a private method, it's called from -        # mainwindow. -        leap_assert(self._provider_config, "We need a provider configuration!") -        leap_assert(self._smtp_config, "We need an smtp configuration!") +        if len(hosts) == 0: +            raise NoSMTPHosts() -        logger.debug("Downloading SMTP client certificate for %s" % -                     (self._provider_config.get_domain(),)) +        # TODO handle more than one host and define how to choose +        hostname = hosts.keys()[0] +        logger.debug("Using hostname %s for SMTP" % (hostname,)) -        client_cert_path = self._smtp_config.\ -            get_client_cert_path(self._provider_config, -                                 about_to_download=True) +        client_cert_path = self._smtp_config.get_client_cert_path( +            self._provider_config, about_to_download=True) -        # For re-download if something is wrong with the cert -        self._download_if_needed = self._download_if_needed and \ -            not leap_certs.should_redownload(client_cert_path) +        if not is_file(client_cert_path): +            # For re-download if something is wrong with the cert +            self._download_if_needed = ( +                self._download_if_needed and +                not leap_certs.should_redownload(client_cert_path)) -        if self._download_if_needed and \ -                os.path.isfile(client_cert_path): -            check_and_fix_urw_only(client_cert_path) -            return +            if self._download_if_needed and os.path.isfile(client_cert_path): +                check_and_fix_urw_only(client_cert_path) +                return -        download_client_cert(self._provider_config, -                             client_cert_path, -                             self._session) +            download_client_cert(self._provider_config, +                                 client_cert_path, +                                 self._session) -    def run_smtp_setup_checks(self, -                              provider_config, -                              smtp_config, -                              download_if_needed=False): +    def _start_smtp_service(self): +        """ +        Start the smtp service using the downloaded configurations. +        """ +        # TODO Make the encrypted_only configurable +        # TODO pick local smtp port in a better way +        # TODO remove hard-coded port and let leap.mail set +        # the specific default. +        # TODO handle more than one host and define how to choose +        hosts = self._smtp_config.get_hosts() +        hostname = hosts.keys()[0] +        host = hosts[hostname][self.IP_KEY].encode("utf-8") +        port = hosts[hostname][self.PORT_KEY] +        client_cert_path = self._smtp_config.get_client_cert_path( +            self._provider_config, about_to_download=True) + +        from leap.mail.smtp import setup_smtp_gateway +        self._smtp_service, self._smtp_port = setup_smtp_gateway( +            port=2013, +            userid=self._userid, +            keymanager=self._keymanager, +            smtp_host=host, +            smtp_port=port, +            smtp_cert=client_cert_path, +            smtp_key=client_cert_path, +            encrypted_only=False) + +    def start_smtp_service(self, provider_config, smtp_config, keymanager, +                           userid, download_if_needed=False):          """ -        Starts the checks needed for a new smtp setup +        Starts the SMTP service.          :param provider_config: Provider configuration          :type provider_config: ProviderConfig          :param smtp_config: SMTP configuration to populate          :type smtp_config: SMTPConfig +        :param keymanager: a transparent proxy that eventually will point to a +                           Keymanager Instance. +        :type keymanager: zope.proxy.ProxyBase +        :param userid: the user id, in the form "user@provider" +        :type userid: str          :param download_if_needed: True if it should check for mtime                                     for the file          :type download_if_needed: bool          """          leap_assert_type(provider_config, ProviderConfig) +        leap_assert_type(smtp_config, SMTPConfig)          self._provider_config = provider_config +        self._keymanager = keymanager          self._smtp_config = smtp_config +        self._useid = userid          self._download_if_needed = download_if_needed -        cb_chain = [ -            (self._download_config, self.download_config), -        ] - -        self.addCallbackChain(cb_chain) +        try: +            self._download_config_and_cert() +            logger.debug("Starting SMTP service.") +            self._start_smtp_service() +        except NoSMTPHosts: +            logger.warning("There is no SMTP host to use.") +        except Exception as e: +            # TODO: we should handle more specific exceptions in here +            logger.exception("Error while bootstrapping SMTP: %r" % (e, )) + +    def stop_smtp_service(self): +        """ +        Stops the smtp service (port and factory). +        """ +        if self._smtp_service is not None: +            logger.debug('Stopping SMTP service.') +            self._smtp_port.stopListening() +            self._smtp_service.doStop() diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index ad5ee4d0..6bb7c036 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -139,7 +139,6 @@ class SoledadBootstrapper(AbstractBootstrapper):      download_config = QtCore.Signal(dict)      gen_key = QtCore.Signal(dict)      local_only_ready = QtCore.Signal(dict) -    soledad_timeout = QtCore.Signal()      soledad_invalid_auth_token = QtCore.Signal()      soledad_failed = QtCore.Signal() @@ -159,8 +158,6 @@ class SoledadBootstrapper(AbstractBootstrapper):          self._srpauth = None          self._soledad = None -        self._soledad_retries = 0 -      @property      def keymanager(self):          return self._keymanager @@ -177,26 +174,6 @@ class SoledadBootstrapper(AbstractBootstrapper):                      "We need a provider config")          return SRPAuth(self._provider_config) -    # retries - -    def cancel_bootstrap(self): -        self._soledad_retries = self.MAX_INIT_RETRIES - -    def should_retry_initialization(self): -        """ -        Return True if we should retry the initialization. -        """ -        logger.debug("current retries: %s, max retries: %s" % ( -            self._soledad_retries, -            self.MAX_INIT_RETRIES)) -        return self._soledad_retries < self.MAX_INIT_RETRIES - -    def increment_retries_count(self): -        """ -        Increment the count of initialization retries. -        """ -        self._soledad_retries += 1 -      # initialization      def load_offline_soledad(self, username, password, uuid): @@ -265,6 +242,41 @@ class SoledadBootstrapper(AbstractBootstrapper):          # in the case of an invalid token we have already turned off mail and          # warned the user in _do_soledad_sync() +    def _do_soledad_init(self, uuid, secrets_path, local_db_path, +                         server_url, cert_file, token): +        """ +        Initialize soledad, retry if necessary and emit soledad_failed if we +        can't succeed. + +        :param uuid: user identifier +        :type uuid: str +        :param secrets_path: path to secrets file +        :type secrets_path: str +        :param local_db_path: path to local db file +        :type local_db_path: str +        :param server_url: soledad server uri +        :type server_url: str +        :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 +        """ +        init_tries = self.MAX_INIT_RETRIES +        while init_tries > 0: +            try: +                self._try_soledad_init( +                    uuid, secrets_path, local_db_path, +                    server_url, cert_file, token) +                logger.debug("Soledad has been initialized.") +                return +            except Exception: +                init_tries -= 1 +                continue + +        self.soledad_failed.emit() +        raise SoledadInitError()      def load_and_sync_soledad(self, uuid=None, offline=False):          """ @@ -283,10 +295,9 @@ class SoledadBootstrapper(AbstractBootstrapper):          server_url, cert_file = remote_param          try: -            self._try_soledad_init( -                uuid, secrets_path, local_db_path, -                server_url, cert_file, token) -        except Exception: +            self._do_soledad_init(uuid, secrets_path, local_db_path, +                                  server_url, cert_file, token) +        except SoledadInitError:              # re-raise the exceptions from try_init,              # we're currently handling the retries from the              # soledad-launcher in the gui. @@ -378,9 +389,13 @@ class SoledadBootstrapper(AbstractBootstrapper):          Try to initialize soledad.          :param uuid: user identifier +        :type uuid: str          :param secrets_path: path to secrets file +        :type secrets_path: str          :param local_db_path: path to local db file +        :type local_db_path: str          :param server_url: soledad server uri +        :type server_url: str          :param cert_file: path to the certificate of the ca used                            to validate the SSL certificate used by the remote                            soledad server. @@ -409,34 +424,17 @@ class SoledadBootstrapper(AbstractBootstrapper):          # and return a subclass of SoledadInitializationFailed          # recoverable, will guarantee retries -        except socket.timeout: -            logger.debug("SOLEDAD initialization TIMED OUT...") -            self.soledad_timeout.emit() -            raise -        except socket.error as exc: -            logger.warning("Socket error while initializing soledad") -            self.soledad_timeout.emit() -            raise -        except BootstrapSequenceError as exc: -            logger.warning("Error while initializing soledad") -            self.soledad_timeout.emit() +        except (socket.timeout, socket.error, BootstrapSequenceError): +            logger.warning("Error while initializing Soledad")              raise          # unrecoverable -        except u1db_errors.Unauthorized: -            logger.error("Error while initializing soledad " -                         "(unauthorized).") -            self.soledad_failed.emit() -            raise -        except u1db_errors.HTTPError as exc: -            logger.exception("Error while initializing soledad " -                             "(HTTPError)") -            self.soledad_failed.emit() +        except (u1db_errors.Unauthorized, u1db_errors.HTTPError): +            logger.error("Error while initializing Soledad (u1db error).")              raise          except Exception as exc:              logger.exception("Unhandled error while initializating " -                             "soledad: %r" % (exc,)) -            self.soledad_failed.emit() +                             "Soledad: %r" % (exc,))              raise      def _try_soledad_sync(self): diff --git a/src/leap/bitmask/util/leap_argparse.py b/src/leap/bitmask/util/leap_argparse.py index 88267ff8..84af4e8d 100644 --- a/src/leap/bitmask/util/leap_argparse.py +++ b/src/leap/bitmask/util/leap_argparse.py @@ -59,6 +59,13 @@ def build_parser():                          action="store_false", dest="api_version_check",                          help='Skip the api version compatibility check with '                          'the provider.') +    parser.add_argument('-H', '--start-hidden', default=False, +                        action="store_true", dest="start_hidden", +                        help='Starts the application just in the taskbar.') +    parser.add_argument('-S', '--skip-wizard-checks', default=False, +                        action="store_true", dest="skip_wizard_checks", +                        help='Skips the provider checks in the wizard (use ' +                             'for testing purposes only).')      # openvpn options      parser.add_argument('--openvpn-verbosity', nargs='?', diff --git a/src/leap/bitmask/util/pastebin.py b/src/leap/bitmask/util/pastebin.py index 21b8a0b7..a3bdba02 100755 --- a/src/leap/bitmask/util/pastebin.py +++ b/src/leap/bitmask/util/pastebin.py @@ -27,8 +27,8 @@  __ALL__ = ['delete_paste', 'user_details', 'trending', 'pastes_by_user',
 -           'generate_user_key', 'legacy_paste', 'paste', 'Pastebin',
 -           'PastebinError']
 +           'generate_user_key', 'paste', 'Pastebin', 'PastebinError',
 +           'PostLimitError']
  import urllib
 @@ -40,6 +40,12 @@ class PastebinError(RuntimeError):      exception message."""
 +class PostLimitError(PastebinError):
 +    """The user reached the limit of posts that can do in a day.
 +    For more information look at: http://pastebin.com/faq#11a
 +    """
 +
 +
  class PastebinAPI(object):
      """Pastebin API interaction object.
 @@ -67,6 +73,9 @@ class PastebinAPI(object):      # String to determine bad API requests
      _bad_request = 'Bad API request'
 +    # String to determine if we reached the max post limit per day
 +    _post_limit = 'Post limit, maximum pastes per 24h reached'
 +
      # Base domain name
      _base_domain = 'pastebin.com'
 @@ -708,95 +717,8 @@ class PastebinAPI(object):          # errors we are likely to encounter
          if response.startswith(self._bad_request):
              raise PastebinError(response)
 -        elif not response.startswith(self._prefix_url):
 -            raise PastebinError(response)
 -
 -        return response
 -
 -    def legacy_paste(self, paste_code,
 -                     paste_name=None, paste_private=None,
 -                     paste_expire_date=None, paste_format=None):
 -        """Unofficial python interface to the Pastebin legacy API.
 -
 -        Unlike the official API, this one doesn't require an API key, so it's
 -        virtually anonymous.
 -
 -
 -        Usage Example::
 -            from pastebin import PastebinAPI
 -            x = PastebinAPI()
 -            url = x.legacy_paste('Snippet of code to paste goes here',
 -                                paste_name = 'title of paste',
 -                                paste_private = 'unlisted',
 -                                paste_expire_date = '10M',
 -                                paste_format = 'python')
 -            print url
 -            http://pastebin.com/tawPUgqY
 -
 -
 -        @type   paste_code: string
 -        @param  paste_code: The file or string to paste to body of the
 -                            U{http://pastebin.com} paste.
 -
 -        @type   paste_name: string
 -        @param  paste_name: (Optional) Title of the paste.
 -            Default is to paste with no title.
 -
 -        @type   paste_private: string
 -        @param  paste_private: (Optional) C{'public'} if the paste is public
 -            (visible by everyone), C{'unlisted'} if it's public but not
 -            searchable. C{'private'} if the paste is private and not
 -            searchable or indexed.
 -            The Pastebin FAQ (U{http://pastebin.com/faq}) claims
 -            private pastes are not indexed by search engines (aka Google).
 -
 -        @type   paste_expire_date: string
 -        @param  paste_expire_date: (Optional) Expiration date for the paste.
 -            Once past this date the paste is deleted automatically. Valid
 -            values are found in the L{PastebinAPI.paste_expire_date} class
 -            member.
 -            If not provided, the paste never expires.
 -
 -        @type   paste_format: string
 -        @param  paste_format: (Optional) Programming language of the code being
 -            pasted. This enables syntax highlighting when reading the code in
 -            U{http://pastebin.com}. Default is no syntax highlighting (text is
 -            just text and not source code).
 -
 -        @rtype:  string
 -        @return: Returns the URL to the newly created paste.
 -        """
 -
 -        # Code snippet to submit
 -        argv = {'paste_code': str(paste_code)}
 -
 -        # Name of the poster
 -        if paste_name is not None:
 -            argv['paste_name'] = str(paste_name)
 -
 -        # Is the snippet private?
 -        if paste_private is not None:
 -            argv['paste_private'] = int(bool(int(paste_private)))
 -
 -        # Expiration for the snippet
 -        if paste_expire_date is not None:
 -            paste_expire_date = str(paste_expire_date).strip().upper()
 -            argv['paste_expire_date'] = paste_expire_date
 -
 -        # Syntax highlighting
 -        if paste_format is not None:
 -            paste_format = str(paste_format).strip().lower()
 -            argv['paste_format'] = paste_format
 -
 -        # lets try to read the URL that we've just built.
 -        data = urllib.urlencode(argv)
 -        request_string = urllib.urlopen(self._legacy_api_url, data)
 -        response = request_string.read()
 -
 -        # do some basic error checking here so we can gracefully handle any
 -        # errors we are likely to encounter
 -        if response.startswith(self._bad_request):
 -            raise PastebinError(response)
 +        elif response.startswith(self._post_limit):
 +            raise PostLimitError(response)
          elif not response.startswith(self._prefix_url):
              raise PastebinError(response)
 @@ -810,5 +732,4 @@ user_details = PastebinAPI.user_details  trending = PastebinAPI.trending
  pastes_by_user = PastebinAPI.pastes_by_user
  generate_user_key = PastebinAPI.generate_user_key
 -legacy_paste = PastebinAPI.legacy_paste
  paste = PastebinAPI.paste
 diff --git a/src/leap/bitmask/util/privilege_policies.py b/src/leap/bitmask/util/privilege_policies.py index 72442553..9d1e2c9a 100644 --- a/src/leap/bitmask/util/privilege_policies.py +++ b/src/leap/bitmask/util/privilege_policies.py @@ -27,35 +27,6 @@ from abc import ABCMeta, abstractmethod  logger = logging.getLogger(__name__) -POLICY_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE policyconfig PUBLIC - "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" - "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd"> -<policyconfig> - -  <vendor>LEAP Project</vendor> -  <vendor_url>https://leap.se/</vendor_url> - -  <action id="net.openvpn.gui.leap.run-openvpn"> -    <description>Runs the openvpn binary</description> -    <description xml:lang="es">Ejecuta el binario openvpn</description> -    <message>OpenVPN needs that you authenticate to start</message> -    <message xml:lang="es"> -      OpenVPN necesita autorizacion para comenzar -    </message> -    <icon_name>package-x-generic</icon_name> -    <defaults> -      <allow_any>yes</allow_any> -      <allow_inactive>yes</allow_inactive> -      <allow_active>yes</allow_active> -    </defaults> -    <annotate key="org.freedesktop.policykit.exec.path">{path}</annotate> -    <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate> -  </action> -</policyconfig> -""" - -  def is_missing_policy_permissions():      """      Returns True if we do not have implemented a policy checker for this @@ -76,36 +47,6 @@ def is_missing_policy_permissions():      return policy_checker().is_missing_policy_permissions() -def get_policy_contents(openvpn_path): -    """ -    Returns the contents that the policy file should have. - -    :param openvpn_path: the openvpn path to use in the polkit file -    :type openvpn_path: str -    :rtype: str -    """ -    return POLICY_TEMPLATE.format(path=openvpn_path) - - -def is_policy_outdated(path): -    """ -    Returns if the existing polkit file is outdated, comparing if the path -    is correct. - -    :param path: the path that should have the polkit file. -    :type path: str. -    :rtype: bool -    """ -    _system = platform.system() -    platform_checker = _system + "PolicyChecker" -    policy_checker = globals().get(platform_checker, None) -    if policy_checker is None: -        logger.debug("we could not find a policy checker implementation " -                     "for %s" % (_system,)) -        return False -    return policy_checker().is_outdated(path) - -  class PolicyChecker:      """      Abstract PolicyChecker class @@ -129,7 +70,7 @@ class LinuxPolicyChecker(PolicyChecker):      PolicyChecker for Linux      """      LINUX_POLKIT_FILE = ("/usr/share/polkit-1/actions/" -                         "net.openvpn.gui.leap.policy") +                         "se.leap.bitmask.policy")      @classmethod      def get_polkit_path(self): @@ -141,6 +82,8 @@ class LinuxPolicyChecker(PolicyChecker):          return self.LINUX_POLKIT_FILE      def is_missing_policy_permissions(self): +    # FIXME this name is quite confusing, it does not have anything to do with +    # file permissions.          """          Returns True if we could not find the appropriate policykit file          in place @@ -148,22 +91,3 @@ class LinuxPolicyChecker(PolicyChecker):          :rtype: bool          """          return not os.path.isfile(self.LINUX_POLKIT_FILE) - -    def is_outdated(self, path): -        """ -        Returns if the existing polkit file is outdated, comparing if the path -        is correct. - -        :param path: the path that should have the polkit file. -        :type path: str. -        :rtype: bool -        """ -        polkit = None -        try: -            with open(self.LINUX_POLKIT_FILE) as f: -                polkit = f.read() -        except IOError, e: -            logger.error("Error reading polkit file(%s): %r" % ( -                self.LINUX_POLKIT_FILE, e)) - -        return get_policy_contents(path) != polkit | 
