diff options
| -rw-r--r-- | changes/refactor-mail-soledad | 5 | ||||
| -rw-r--r-- | src/leap/bitmask/app.py | 2 | ||||
| -rw-r--r-- | src/leap/bitmask/backend.py | 503 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/advanced_key_management.py | 12 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/mail_status.py | 2 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/mainwindow.py | 391 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/preferenceswindow.py | 72 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/wizard.py | 4 | ||||
| -rw-r--r-- | src/leap/bitmask/services/eip/vpnprocess.py | 12 | ||||
| -rw-r--r-- | src/leap/bitmask/services/mail/conductor.py | 135 | ||||
| -rw-r--r-- | src/leap/bitmask/services/mail/imapcontroller.py | 103 | ||||
| -rw-r--r-- | src/leap/bitmask/services/mail/smtpbootstrapper.py | 24 | ||||
| -rw-r--r-- | src/leap/bitmask/services/soledad/soledadbootstrapper.py | 103 | 
13 files changed, 914 insertions, 454 deletions
| diff --git a/changes/refactor-mail-soledad b/changes/refactor-mail-soledad new file mode 100644 index 00000000..32b1ab5b --- /dev/null +++ b/changes/refactor-mail-soledad @@ -0,0 +1,5 @@ +- Improve wait and quit process. +- Move soledad password change to backend. +- Move Mail logic to backend. +- Separate imap/smtp logic from conductor. +- Refactor SoledadBootstrapper to backend. Closes #5481. diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index e413ab4c..05f81f0b 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -76,6 +76,7 @@ def sigint_handler(*args, **kwargs):      mainwindow = args[0]      mainwindow.quit() +  def sigterm_handler(*args, **kwargs):      """      Signal handler for SIGTERM. @@ -87,6 +88,7 @@ def sigterm_handler(*args, **kwargs):      mainwindow = args[0]      mainwindow.quit() +  def add_logger_handlers(debug=False, logfile=None, replace_stdout=True):      """      Create the logger and attach the handlers. diff --git a/src/leap/bitmask/backend.py b/src/leap/bitmask/backend.py index a2df465d..0ab7040b 100644 --- a/src/leap/bitmask/backend.py +++ b/src/leap/bitmask/backend.py @@ -17,13 +17,13 @@  """  Backend for everything  """ -import commands  import logging  import os  import time  from functools import partial  from Queue import Queue, Empty +from threading import Condition  from twisted.internet import reactor  from twisted.internet import threads, defer @@ -31,6 +31,7 @@ from twisted.internet.task import LoopingCall  from twisted.python import log  import zope.interface +import zope.proxy  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.crypto.srpauth import SRPAuth @@ -45,8 +46,18 @@ 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.bitmask.services.mail.imapcontroller import IMAPController +from leap.bitmask.services.mail.smtpbootstrapper import SMTPBootstrapper +from leap.bitmask.services.mail.smtpconfig import SMTPConfig + +from leap.bitmask.services.soledad.soledadbootstrapper import \ +    SoledadBootstrapper +  from leap.common import certs as leap_certs +from leap.soledad.client import NoStorageSecret, PassphraseTooShort +  # Frontend side  from PySide import QtCore @@ -197,11 +208,15 @@ class Provider(object):          """          d = None +        # TODO: use this commented code when we don't need the provider config +        # in the maiwindow. +        # config = ProviderConfig.get_provider_config(provider) +        # self._provider_config = config +        # if config is not None:          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) +                config, download_if_needed=True)          else:              if self._signaler is not None:                  self._signaler.signal( @@ -246,8 +261,9 @@ class Register(object):          :returns: the defer for the operation running in a thread.          :rtype: twisted.internet.defer.Deferred          """ -        config = ProviderConfig() -        if get_provider_config(config, domain): +        config = ProviderConfig.get_provider_config(domain) +        self._provider_config = config +        if config is not None:              srpregister = SRPRegister(signaler=self._signaler,                                        provider_config=config)              return threads.deferToThread( @@ -294,8 +310,9 @@ class EIP(object):          :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): +        config = ProviderConfig.get_provider_config(domain) +        self._provider_config = config +        if config is not None:              if skip_network:                  return defer.Deferred()              eb = self._eip_bootstrapper @@ -373,14 +390,20 @@ class EIP(object):              # TODO: are we connected here?              signaler.signal(signaler.EIP_CONNECTED) -    def stop(self, shutdown=False): +    def _do_stop(self, shutdown=False):          """ -        Stop the service. +        Stop the service. This is run in a thread to avoid blocking.          """          self._vpn.terminate(shutdown)          if IS_LINUX:              self._wait_for_firewall_down() +    def stop(self, shutdown=False): +        """ +        Stop the service. +        """ +        return threads.deferToThread(self._do_stop, shutdown) +      def _wait_for_firewall_down(self):          """          Wait for the firewall to come down. @@ -393,15 +416,16 @@ class EIP(object):          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 +        retry = 1 -        while retry < MAX_FW_WAIT_RETRIES: -            if fw_is_down(): +        while retry <= MAX_FW_WAIT_RETRIES: +            if self._vpn.is_fw_down(): +                self._signaler.signal(self._signaler.EIP_STOPPED)                  return              else: +                msg = "Firewall is not down yet, waiting... {0} of {1}" +                msg = msg.format(retry, MAX_FW_WAIT_RETRIES) +                logger.debug(msg)                  time.sleep(FW_WAIT_STEP)                  retry += 1          logger.warning("After waiting, firewall is not down... " @@ -539,6 +563,237 @@ class EIP(object):                  self._signaler.signal(self._signaler.EIP_CANNOT_START) +class Soledad(object): +    """ +    Interfaces with setup of Soledad. +    """ +    zope.interface.implements(ILEAPComponent) + +    def __init__(self, soledad_proxy, keymanager_proxy, signaler=None): +        """ +        Constructor for the Soledad component. + +        :param soledad_proxy: proxy to pass around a Soledad object. +        :type soledad_proxy: zope.ProxyBase +        :param keymanager_proxy: proxy to pass around a Keymanager object. +        :type keymanager_proxy: zope.ProxyBase +        :param signaler: Object in charge of handling communication +                         back to the frontend +        :type signaler: Signaler +        """ +        self.key = "soledad" +        self._soledad_proxy = soledad_proxy +        self._keymanager_proxy = keymanager_proxy +        self._signaler = signaler +        self._soledad_bootstrapper = SoledadBootstrapper(signaler) +        self._soledad_defer = None + +    def bootstrap(self, username, domain, password): +        """ +        Bootstrap Soledad with the user credentials. + +        Signals: +            soledad_download_config +            soledad_gen_key + +        :param user: user's login +        :type user: unicode +        :param domain: the domain that we are using. +        :type domain: unicode +        :param password: user's password +        :type password: unicode +        """ +        provider_config = ProviderConfig.get_provider_config(domain) +        if provider_config is not None: +            self._soledad_defer = threads.deferToThread( +                self._soledad_bootstrapper.run_soledad_setup_checks, +                provider_config, username, password, +                download_if_needed=True) +            self._soledad_defer.addCallback(self._set_proxies_cb) +        else: +            if self._signaler is not None: +                self._signaler.signal(self._signaler.SOLEDAD_BOOTSTRAP_FAILED) +            logger.error("Could not load provider configuration.") + +        return self._soledad_defer + +    def _set_proxies_cb(self, _): +        """ +        Update the soledad and keymanager proxies to reference the ones created +        in the bootstrapper. +        """ +        zope.proxy.setProxiedObject(self._soledad_proxy, +                                    self._soledad_bootstrapper.soledad) +        zope.proxy.setProxiedObject(self._keymanager_proxy, +                                    self._soledad_bootstrapper.keymanager) + +    def load_offline(self, username, password, uuid): +        """ +        Load the soledad database in offline mode. + +        :param username: full user id (user@provider) +        :type username: str or unicode +        :param password: the soledad passphrase +        :type password: unicode +        :param uuid: the user uuid +        :type uuid: str or unicode + +        Signals: +            Signaler.soledad_offline_finished +            Signaler.soledad_offline_failed +        """ +        self._soledad_bootstrapper.load_offline_soledad( +            username, password, uuid) + +    def cancel_bootstrap(self): +        """ +        Cancel the ongoing soledad bootstrap (if any). +        """ +        if self._soledad_defer is not None: +            logger.debug("Cancelling soledad defer.") +            self._soledad_defer.cancel() +            self._soledad_defer = None +            zope.proxy.setProxiedObject(self._soledad_proxy, None) + +    def close(self): +        """ +        Close soledad database. +        """ +        if not zope.proxy.sameProxiedObjects(self._soledad_proxy, None): +            self._soledad_proxy.close() +            zope.proxy.setProxiedObject(self._soledad_proxy, None) + +    def _change_password_ok(self, _): +        """ +        Password change callback. +        """ +        if self._signaler is not None: +            self._signaler.signal(self._signaler.SOLEDAD_PASSWORD_CHANGE_OK) + +    def _change_password_error(self, failure): +        """ +        Password change errback. + +        :param failure: failure object containing problem. +        :type failure: twisted.python.failure.Failure +        """ +        if failure.check(NoStorageSecret): +            logger.error("No storage secret for password change in Soledad.") +        if failure.check(PassphraseTooShort): +            logger.error("Passphrase too short.") + +        if self._signaler is not None: +            self._signaler.signal(self._signaler.SOLEDAD_PASSWORD_CHANGE_ERROR) + +    def change_password(self, new_password): +        """ +        Change the database's password. + +        :param new_password: the new password. +        :type new_password: unicode + +        :returns: a defer to interact with. +        :rtype: twisted.internet.defer.Deferred +        """ +        d = threads.deferToThread(self._soledad_proxy.change_passphrase, +                                  new_password) +        d.addCallback(self._change_password_ok) +        d.addErrback(self._change_password_error) + + +class Mail(object): +    """ +    Interfaces with setup and launch of Mail. +    """ +    # We give each service some time to come to a halt before forcing quit +    SERVICE_STOP_TIMEOUT = 20 + +    zope.interface.implements(ILEAPComponent) + +    def __init__(self, soledad_proxy, keymanager_proxy, signaler=None): +        """ +        Constructor for the Mail component. + +        :param soledad_proxy: proxy to pass around a Soledad object. +        :type soledad_proxy: zope.ProxyBase +        :param keymanager_proxy: proxy to pass around a Keymanager object. +        :type keymanager_proxy: zope.ProxyBase +        :param signaler: Object in charge of handling communication +                         back to the frontend +        :type signaler: Signaler +        """ +        self.key = "mail" +        self._signaler = signaler +        self._soledad_proxy = soledad_proxy +        self._keymanager_proxy = keymanager_proxy +        self._imap_controller = IMAPController(self._soledad_proxy, +                                               self._keymanager_proxy) +        self._smtp_bootstrapper = SMTPBootstrapper() +        self._smtp_config = SMTPConfig() + +    def start_smtp_service(self, full_user_id, download_if_needed=False): +        """ +        Start the SMTP service. + +        :param full_user_id: user id, in the form "user@provider" +        :type full_user_id: str +        :param download_if_needed: True if it should check for mtime +                                   for the file +        :type download_if_needed: bool + +        :returns: a defer to interact with. +        :rtype: twisted.internet.defer.Deferred +        """ +        return threads.deferToThread( +            self._smtp_bootstrapper.start_smtp_service, +            self._keymanager_proxy, full_user_id, download_if_needed) + +    def start_imap_service(self, full_user_id, offline=False): +        """ +        Start the IMAP service. + +        :param full_user_id: user id, in the form "user@provider" +        :type full_user_id: str +        :param offline: whether imap should start in offline mode or not. +        :type offline: bool + +        :returns: a defer to interact with. +        :rtype: twisted.internet.defer.Deferred +        """ +        return threads.deferToThread( +            self._imap_controller.start_imap_service, +            full_user_id, offline) + +    def stop_smtp_service(self): +        """ +        Stop the SMTP service. + +        :returns: a defer to interact with. +        :rtype: twisted.internet.defer.Deferred +        """ +        return threads.deferToThread(self._smtp_bootstrapper.stop_smtp_service) + +    def _stop_imap_service(self): +        """ +        Stop imap and wait until the service is stopped to signal that is done. +        """ +        cv = Condition() +        cv.acquire() +        threads.deferToThread(self._imap_controller.stop_imap_service, cv) +        logger.debug('Waiting for imap service to stop.') +        cv.wait(self.SERVICE_STOP_TIMEOUT) +        self._signaler.signal(self._signaler.IMAP_STOPPED) + +    def stop_imap_service(self): +        """ +        Stop imap service (fetcher, factory and port). + +        :returns: a defer to interact with. +        :rtype: twisted.internet.defer.Deferred +        """ +        return threads.deferToThread(self._stop_imap_service) + +  class Authenticate(object):      """      Interfaces with setup and bootstrapping operations for a provider @@ -556,6 +811,7 @@ class Authenticate(object):          """          self.key = "authenticate"          self._signaler = signaler +        self._login_defer = None          self._srp_auth = SRPAuth(ProviderConfig(), self._signaler)      def login(self, domain, username, password): @@ -572,8 +828,8 @@ class Authenticate(object):          :returns: the defer for the operation running in a thread.          :rtype: twisted.internet.defer.Deferred          """ -        config = ProviderConfig() -        if get_provider_config(config, domain): +        config = ProviderConfig.get_provider_config(domain) +        if config is not None:              self._srp_auth = SRPAuth(config, self._signaler)              self._login_defer = self._srp_auth.authenticate(username, password)              return self._login_defer @@ -703,6 +959,7 @@ class Signaler(QtCore.QObject):      eip_disconnected = QtCore.Signal(object)      eip_connection_died = QtCore.Signal(object)      eip_connection_aborted = QtCore.Signal(object) +    eip_stopped = QtCore.Signal(object)      # EIP problems      eip_no_polkit_agent_error = QtCore.Signal(object) @@ -732,6 +989,19 @@ class Signaler(QtCore.QObject):      eip_can_start = QtCore.Signal(object)      eip_cannot_start = QtCore.Signal(object) +    # Signals for Soledad +    soledad_bootstrap_failed = QtCore.Signal(object) +    soledad_bootstrap_finished = QtCore.Signal(object) +    soledad_offline_failed = QtCore.Signal(object) +    soledad_offline_finished = QtCore.Signal(object) +    soledad_invalid_auth_token = QtCore.Signal(object) +    soledad_cancelled_bootstrap = QtCore.Signal(object) +    soledad_password_change_ok = QtCore.Signal(object) +    soledad_password_change_error = QtCore.Signal(object) + +    # mail related signals +    imap_stopped = QtCore.Signal(object) +      # This signal is used to warn the backend user that is doing something      # wrong      backend_bad_call = QtCore.Signal(object) @@ -777,6 +1047,8 @@ class Signaler(QtCore.QObject):      EIP_DISCONNECTED = "eip_disconnected"      EIP_CONNECTION_DIED = "eip_connection_died"      EIP_CONNECTION_ABORTED = "eip_connection_aborted" +    EIP_STOPPED = "eip_stopped" +      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" @@ -801,6 +1073,19 @@ class Signaler(QtCore.QObject):      EIP_CAN_START = "eip_can_start"      EIP_CANNOT_START = "eip_cannot_start" +    SOLEDAD_BOOTSTRAP_FAILED = "soledad_bootstrap_failed" +    SOLEDAD_BOOTSTRAP_FINISHED = "soledad_bootstrap_finished" +    SOLEDAD_OFFLINE_FAILED = "soledad_offline_failed" +    SOLEDAD_OFFLINE_FINISHED = "soledad_offline_finished" +    SOLEDAD_INVALID_AUTH_TOKEN = "soledad_invalid_auth_token" + +    SOLEDAD_PASSWORD_CHANGE_OK = "soledad_password_change_ok" +    SOLEDAD_PASSWORD_CHANGE_ERROR = "soledad_password_change_error" + +    SOLEDAD_CANCELLED_BOOTSTRAP = "soledad_cancelled_bootstrap" + +    IMAP_STOPPED = "imap_stopped" +      BACKEND_BAD_CALL = "backend_bad_call"      def __init__(self): @@ -834,6 +1119,8 @@ class Signaler(QtCore.QObject):              self.EIP_DISCONNECTED,              self.EIP_CONNECTION_DIED,              self.EIP_CONNECTION_ABORTED, +            self.EIP_STOPPED, +              self.EIP_NO_POLKIT_AGENT_ERROR,              self.EIP_NO_TUN_KEXT_ERROR,              self.EIP_NO_PKEXEC_ERROR, @@ -872,6 +1159,18 @@ class Signaler(QtCore.QObject):              self.SRP_STATUS_LOGGED_IN,              self.SRP_STATUS_NOT_LOGGED_IN, +            self.SOLEDAD_BOOTSTRAP_FAILED, +            self.SOLEDAD_BOOTSTRAP_FINISHED, +            self.SOLEDAD_OFFLINE_FAILED, +            self.SOLEDAD_OFFLINE_FINISHED, +            self.SOLEDAD_INVALID_AUTH_TOKEN, +            self.SOLEDAD_CANCELLED_BOOTSTRAP, + +            self.SOLEDAD_PASSWORD_CHANGE_OK, +            self.SOLEDAD_PASSWORD_CHANGE_ERROR, + +            self.IMAP_STOPPED, +              self.BACKEND_BAD_CALL,          ] @@ -926,11 +1225,22 @@ class Backend(object):          # Signaler object to translate commands into Qt signals          self._signaler = Signaler() +        # Objects needed by several components, so we make a proxy and pass +        # them around +        self._soledad_proxy = zope.proxy.ProxyBase(None) +        self._keymanager_proxy = zope.proxy.ProxyBase(None) +          # Component registration          self._register(Provider(self._signaler, bypass_checks))          self._register(Register(self._signaler))          self._register(Authenticate(self._signaler))          self._register(EIP(self._signaler)) +        self._register(Soledad(self._soledad_proxy, +                               self._keymanager_proxy, +                               self._signaler)) +        self._register(Mail(self._soledad_proxy, +                            self._keymanager_proxy, +                            self._signaler))          # We have a looping call on a thread executing all the          # commands in queue. Right now this queue is an actual Queue @@ -952,7 +1262,7 @@ class Backend(object):          """          Starts the looping call          """ -        log.msg("Starting worker...") +        logger.debug("Starting worker...")          self._lc.start(0.01)      def stop(self): @@ -965,14 +1275,17 @@ class Backend(object):          """          Delayed stopping of worker. Called from `stop`.          """ -        log.msg("Stopping worker...") +        logger.debug("Stopping worker...")          if self._lc.running:              self._lc.stop()          else:              logger.warning("Looping call is not running, cannot stop") + +        logger.debug("Cancelling ongoing defers...")          while len(self._ongoing_defers) > 0:              d = self._ongoing_defers.pop()              d.cancel() +        logger.debug("Defers cancelled.")      def _register(self, component):          """ @@ -986,8 +1299,7 @@ class Backend(object):          try:              self._components[component.key] = component          except Exception: -            log.msg("There was a problem registering %s" % (component,)) -            log.err() +            logger.error("There was a problem registering %s" % (component,))      def _signal_back(self, _, signal):          """ @@ -1015,19 +1327,19 @@ class Backend(object):                  # A call might not have a callback signal, but if it does,                  # we add it to the chain                  if cmd[2] is not None: -                    d.addCallbacks(self._signal_back, log.err, cmd[2]) -                d.addCallbacks(self._done_action, log.err, +                    d.addCallbacks(self._signal_back, logger.error, cmd[2]) +                d.addCallbacks(self._done_action, logger.error,                                 callbackKeywords={"d": d}) -                d.addErrback(log.err) +                d.addErrback(logger.error)                  self._ongoing_defers.append(d)          except Empty:              # If it's just empty we don't have anything to do.              pass          except defer.CancelledError:              logger.debug("defer cancelled somewhere (CancelledError).") -        except Exception: +        except Exception as e:              # But we log the rest -            log.err() +            logger.exception("Unexpected exception: {0!r}".format(e))      def _done_action(self, _, d):          """ @@ -1044,7 +1356,7 @@ class Backend(object):      # this in two processes, the methods bellow can be changed to      # send_multipart and this backend class will be really simple. -    def setup_provider(self, provider): +    def provider_setup(self, provider):          """          Initiate the setup for a provider. @@ -1060,7 +1372,7 @@ class Backend(object):          """          self._call_queue.put(("provider", "setup_provider", None, provider)) -    def cancel_setup_provider(self): +    def provider_cancel_setup(self):          """          Cancel the ongoing setup provider (if any).          """ @@ -1081,7 +1393,7 @@ class Backend(object):          """          self._call_queue.put(("provider", "bootstrap", None, provider)) -    def register_user(self, provider, username, password): +    def user_register(self, provider, username, password):          """          Register a user using the domain and password given as parameters. @@ -1100,7 +1412,7 @@ class Backend(object):          self._call_queue.put(("register", "register_user", None, provider,                                username, password)) -    def setup_eip(self, provider, skip_network=False): +    def eip_setup(self, provider, skip_network=False):          """          Initiate the setup for a provider @@ -1118,13 +1430,13 @@ class Backend(object):          self._call_queue.put(("eip", "setup_eip", None, provider,                                skip_network)) -    def cancel_setup_eip(self): +    def eip_cancel_setup(self):          """          Cancel the ongoing setup EIP (if any).          """          self._call_queue.put(("eip", "cancel_setup_eip", None)) -    def start_eip(self): +    def eip_start(self):          """          Start the EIP service. @@ -1148,7 +1460,7 @@ class Backend(object):          """          self._call_queue.put(("eip", "start", None)) -    def stop_eip(self, shutdown=False): +    def eip_stop(self, shutdown=False):          """          Stop the EIP service. @@ -1157,7 +1469,7 @@ class Backend(object):          """          self._call_queue.put(("eip", "stop", None, shutdown)) -    def terminate_eip(self): +    def eip_terminate(self):          """          Terminate the EIP service, not necessarily in a nice way.          """ @@ -1209,7 +1521,7 @@ class Backend(object):          self._call_queue.put(("eip", "can_start",                                None, domain)) -    def login(self, provider, username, password): +    def user_login(self, provider, username, password):          """          Execute the whole authentication process for a user @@ -1231,7 +1543,7 @@ class Backend(object):          self._call_queue.put(("authenticate", "login", None, provider,                                username, password)) -    def logout(self): +    def user_logout(self):          """          Log out the current session. @@ -1242,13 +1554,13 @@ class Backend(object):          """          self._call_queue.put(("authenticate", "logout", None)) -    def cancel_login(self): +    def user_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): +    def user_change_password(self, current_password, new_password):          """          Change the user's password. @@ -1266,7 +1578,23 @@ class Backend(object):          self._call_queue.put(("authenticate", "change_password", None,                                current_password, new_password)) -    def get_logged_in_status(self): +    def soledad_change_password(self, new_password): +        """ +        Change the database's password. + +        :param new_password: the new password for the user. +        :type new_password: unicode + +        Signals: +            srp_not_logged_in_error +            srp_password_change_ok +            srp_password_change_badpw +            srp_password_change_error +        """ +        self._call_queue.put(("soledad", "change_password", None, +                              new_password)) + +    def user_get_logged_in_status(self):          """          Signal if the user is currently logged in or not. @@ -1276,10 +1604,107 @@ class Backend(object):          """          self._call_queue.put(("authenticate", "get_logged_in_status", None)) +    def soledad_bootstrap(self, username, domain, password): +        """ +        Bootstrap the soledad database. + +        :param username: the user name +        :type username: unicode +        :param domain: the domain that we are using. +        :type domain: unicode +        :param password: the password for the username +        :type password: unicode + +        Signals: +            soledad_bootstrap_finished +            soledad_bootstrap_failed +            soledad_invalid_auth_token +        """ +        self._call_queue.put(("soledad", "bootstrap", None, +                              username, domain, password)) + +    def soledad_load_offline(self, username, password, uuid): +        """ +        Load the soledad database in offline mode. + +        :param username: full user id (user@provider) +        :type username: str or unicode +        :param password: the soledad passphrase +        :type password: unicode +        :param uuid: the user uuid +        :type uuid: str or unicode + +        Signals: +        """ +        self._call_queue.put(("soledad", "load_offline", None, +                              username, password, uuid)) + +    def soledad_cancel_bootstrap(self): +        """ +        Cancel the ongoing soledad bootstrapping process (if any). +        """ +        self._call_queue.put(("soledad", "cancel_bootstrap", None)) + +    def soledad_close(self): +        """ +        Close soledad database. +        """ +        self._call_queue.put(("soledad", "close", None)) + +    def smtp_start_service(self, full_user_id, download_if_needed=False): +        """ +        Start the SMTP service. + +        :param full_user_id: user id, in the form "user@provider" +        :type full_user_id: str +        :param download_if_needed: True if it should check for mtime +                                   for the file +        :type download_if_needed: bool +        """ +        self._call_queue.put(("mail", "start_smtp_service", None, +                              full_user_id, download_if_needed)) + +    def imap_start_service(self, full_user_id, offline=False): +        """ +        Start the IMAP service. + +        :param full_user_id: user id, in the form "user@provider" +        :type full_user_id: str +        :param offline: whether imap should start in offline mode or not. +        :type offline: bool +        """ +        self._call_queue.put(("mail", "start_imap_service", None, +                              full_user_id, offline)) + +    def smtp_stop_service(self): +        """ +        Stop the SMTP service. +        """ +        self._call_queue.put(("mail", "stop_smtp_service", None)) + +    def imap_stop_service(self): +        """ +        Stop imap service. + +        Signals: +            imap_stopped +        """ +        self._call_queue.put(("mail", "stop_imap_service", 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): +        # TODO: refactor the provider config into a singleton/global loading it +        # every time from the file.          provider_config = self._components["provider"]._provider_config          return provider_config + +    def get_soledad(self): +        soledad = self._components["soledad"]._soledad_bootstrapper._soledad +        return soledad + +    def get_keymanager(self): +        km = self._components["soledad"]._soledad_bootstrapper._keymanager +        return km diff --git a/src/leap/bitmask/gui/advanced_key_management.py b/src/leap/bitmask/gui/advanced_key_management.py index be6b4410..1681caca 100644 --- a/src/leap/bitmask/gui/advanced_key_management.py +++ b/src/leap/bitmask/gui/advanced_key_management.py @@ -20,7 +20,6 @@ Advanced Key Management  import logging  from PySide import QtGui -from zope.proxy import sameProxiedObjects  from leap.keymanager import openpgp  from leap.keymanager.errors import KeyAddressMismatch, KeyFingerprintMismatch @@ -34,7 +33,7 @@ class AdvancedKeyManagement(QtGui.QDialog):      """      Advanced Key Management      """ -    def __init__(self, parent, has_mx, user, keymanager, soledad): +    def __init__(self, parent, has_mx, user, keymanager, soledad_started):          """          :param parent: parent object of AdvancedKeyManagement.          :parent type: QWidget @@ -45,8 +44,8 @@ class AdvancedKeyManagement(QtGui.QDialog):          :type user: unicode          :param keymanager: the existing keymanager instance          :type keymanager: KeyManager -        :param soledad: a loaded instance of Soledad -        :type soledad: Soledad +        :param soledad_started: whether soledad has started or not +        :type soledad_started: bool          """          QtGui.QDialog.__init__(self, parent) @@ -56,7 +55,6 @@ class AdvancedKeyManagement(QtGui.QDialog):          # XXX: Temporarily disable the key import.          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}.") @@ -64,8 +62,7 @@ class AdvancedKeyManagement(QtGui.QDialog):              self._disable_ui(msg)              return -        # if Soledad is not started yet -        if sameProxiedObjects(soledad, None): +        if not soledad_started:              msg = self.tr("To use this, you need to enable/start {0}.")              msg = msg.format(get_service_display_name(MX_SERVICE))              self._disable_ui(msg) @@ -79,7 +76,6 @@ class AdvancedKeyManagement(QtGui.QDialog):          #     self.ui.lblStatus.setText(msg)          self._keymanager = keymanager -        self._soledad = soledad          self._key = keymanager.get_key(user, openpgp.OpenPGPKey)          self._key_priv = keymanager.get_key( diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py index d3346780..5caef745 100644 --- a/src/leap/bitmask/gui/mail_status.py +++ b/src/leap/bitmask/gui/mail_status.py @@ -188,7 +188,7 @@ class MailStatusWidget(QtGui.QWidget):      def set_soledad_failed(self):          """          TRIGGERS: -            SoledadBootstrapper.soledad_failed +            Signaler.soledad_bootstrap_failed          This method is called whenever soledad has a failure.          """ diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index e3848c46..fc4b4d75 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -19,9 +19,7 @@ Main window for Bitmask.  """  import logging  import socket -import time -from threading import Condition  from datetime import datetime  from PySide import QtCore, QtGui @@ -57,8 +55,6 @@ from leap.bitmask.services.mail import conductor as mail_conductor  from leap.bitmask.services import EIP_SERVICE, MX_SERVICE  from leap.bitmask.services.eip.connection import EIPConnection -from leap.bitmask.services.soledad.soledadbootstrapper import \ -    SoledadBootstrapper  from leap.bitmask.util import make_address  from leap.bitmask.util.keyring_helpers import has_keyring @@ -89,17 +85,17 @@ class MainWindow(QtGui.QMainWindow):      new_updates = QtCore.Signal(object)      raise_window = QtCore.Signal([])      soledad_ready = QtCore.Signal([]) -    mail_client_logged_in = QtCore.Signal([])      logout = QtCore.Signal([]) +    all_services_stopped = QtCore.Signal()      # We use this flag to detect abnormal terminations      user_stopped_eip = False      # We give EIP some time to come up before starting soledad anyway -    EIP_TIMEOUT = 60000  # in milliseconds +    EIP_START_TIMEOUT = 60000  # in milliseconds -    # We give each service some time to come to a halt before forcing quit -    SERVICE_STOP_TIMEOUT = 20 +    # We give the services some time to a halt before forcing quit. +    SERVICES_STOP_TIMEOUT = 20      def __init__(self, quit_callback, bypass_checks=False, start_hidden=False):          """ @@ -125,9 +121,6 @@ class MainWindow(QtGui.QMainWindow):          register(signal=proto.RAISE_WINDOW,                   callback=self._on_raise_window_event,                   reqcbk=lambda req, resp: None)  # make rpc call async -        register(signal=proto.IMAP_CLIENT_LOGIN, -                 callback=self._on_mail_client_logged_in, -                 reqcbk=lambda req, resp: None)  # make rpc call async          # end register leap events ####################################          self._quit_callback = quit_callback @@ -195,28 +188,22 @@ class MainWindow(QtGui.QMainWindow):          self._provisional_provider_config = ProviderConfig()          self._already_started_eip = False -        self._already_started_soledad = False +        self._soledad_started = False          # This is created once we have a valid provider config          self._srp_auth = None          self._logged_user = None          self._logged_in_offline = False +        # Set used to track the services being stopped and need wait. +        self._services_being_stopped = {} + +        # timeout object used to trigger quit +        self._quit_timeout_callater = None +          self._backend_connected_signals = {}          self._backend_connect() -        self._soledad_bootstrapper = SoledadBootstrapper() -        self._soledad_bootstrapper.download_config.connect( -            self._soledad_intermediate_stage) -        self._soledad_bootstrapper.gen_key.connect( -            self._soledad_bootstrapped_stage) -        self._soledad_bootstrapper.local_only_ready.connect( -            self._soledad_bootstrapped_stage) -        self._soledad_bootstrapper.soledad_invalid_auth_token.connect( -            self._mail_status.set_soledad_invalid_auth_token) -        self._soledad_bootstrapper.soledad_failed.connect( -            self._mail_status.set_soledad_failed) -          self.ui.action_preferences.triggered.connect(self._show_preferences)          self.ui.action_eip_preferences.triggered.connect(              self._show_eip_preferences) @@ -280,10 +267,6 @@ class MainWindow(QtGui.QMainWindow):          # XXX should connect to mail_conductor.start_mail_service instead          self.soledad_ready.connect(self._start_smtp_bootstrapping)          self.soledad_ready.connect(self._start_imap_service) -        self.mail_client_logged_in.connect(self._fetch_incoming_mail) -        self.logout.connect(self._stop_imap_service) -        self.logout.connect(self._stop_smtp_service) -          ################################# end Qt Signals connection ########          init_platform() @@ -296,18 +279,13 @@ class MainWindow(QtGui.QMainWindow):          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 -        # around. -        self._soledad = ProxyBase(None)          self._keymanager = ProxyBase(None) -        self._soledad_defer = None - -        self._mail_conductor = mail_conductor.MailConductor( -            self._soledad, self._keymanager) +        self._mail_conductor = mail_conductor.MailConductor(self._backend)          self._mail_conductor.connect_mail_signals(self._mail_status) +        self.logout.connect(self._mail_conductor.stop_mail_services) +          # Eip machine is a public attribute where the state machine for          # the eip connection will be available to the different components.          # Remember that this will not live in the  +1600LOC mainwindow for @@ -370,14 +348,23 @@ class MainWindow(QtGui.QMainWindow):          logger.error("Bad call to the backend:")          logger.error(data) -    def _backend_connect(self): +    def _backend_connect(self, only_tracked=False):          """ -        Helper to connect to backend signals +        Connect to backend signals. + +        We track some signals in order to disconnect them on demand. +        For instance, in the wizard we need to connect to some signals that are +        already connected in the mainwindow, so to avoid conflicts we do: +            - disconnect signals needed in wizard (`_disconnect_and_untrack`) +            - use wizard +            - reconnect disconnected signals (we use the `only_tracked` param) + +        :param only_tracked: whether or not we should connect only the signals +                             that we are tracking to disconnect later. +        :type only_tracked: bool          """          sig = self._backend.signaler -        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, @@ -433,7 +420,16 @@ class MainWindow(QtGui.QMainWindow):          self._connect_and_track(sig.eip_client_certificate_ready,                                  self._finish_eip_bootstrap) +        ################################################### +        # Add tracked signals above this, untracked bellow! +        ################################################### +        if only_tracked: +            return +          # We don't want to disconnect some signals so don't track them: + +        sig.backend_bad_call.connect(self._backend_bad_call) +          sig.prov_unsupported_client.connect(self._needs_update)          sig.prov_unsupported_api.connect(self._incompatible_api) @@ -461,6 +457,21 @@ class MainWindow(QtGui.QMainWindow):          sig.eip_can_start.connect(self._backend_can_start_eip)          sig.eip_cannot_start.connect(self._backend_cannot_start_eip) +        # Soledad signals +        sig.soledad_bootstrap_failed.connect( +            self._mail_status.set_soledad_failed) +        sig.soledad_bootstrap_finished.connect(self._on_soledad_ready) + +        sig.soledad_offline_failed.connect( +            self._mail_status.set_soledad_failed) +        sig.soledad_offline_finished.connect(self._on_soledad_ready) + +        sig.soledad_invalid_auth_token.connect( +            self._mail_status.set_soledad_invalid_auth_token) + +        # TODO: connect this with something +        # sig.soledad_cancelled_bootstrap.connect() +      def _disconnect_and_untrack(self):          """          Helper to disconnect the tracked signals. @@ -497,7 +508,7 @@ class MainWindow(QtGui.QMainWindow):              # This happens if the user finishes the provider              # setup but does not register              self._wizard = None -            self._backend_connect() +            self._backend_connect(only_tracked=True)              if self._wizard_firstrun:                  self._finish_init() @@ -591,8 +602,8 @@ class MainWindow(QtGui.QMainWindow):              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 = AdvancedKeyManagement(self, has_mx, logged_user, +                                    self._keymanager, self._soledad_started)          akm.show()      @QtCore.Slot() @@ -607,8 +618,8 @@ class MainWindow(QtGui.QMainWindow):          user = self._login_widget.get_user()          prov = self._login_widget.get_selected_provider()          preferences = PreferencesWindow( -            self, self._backend, self._provider_config, self._soledad, -            user, prov) +            self, self._backend, self._provider_config, +            self._soledad_started, user, prov)          self.soledad_ready.connect(preferences.set_soledad_ready)          preferences.show() @@ -642,7 +653,7 @@ class MainWindow(QtGui.QMainWindow):          # 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) +            self._backend.eip_setup(default_provider, skip_network=True)      def _backend_can_start_eip(self):          """ @@ -817,7 +828,7 @@ class MainWindow(QtGui.QMainWindow):                  self.eip_needs_login.emit()              self._wizard = None -            self._backend_connect() +            self._backend_connect(only_tracked=True)          else:              self._update_eip_enabled_status() @@ -1156,7 +1167,7 @@ class MainWindow(QtGui.QMainWindow):          """          # XXX should rename this provider, name clash.          provider = self._login_widget.get_selected_provider() -        self._backend.setup_provider(provider) +        self._backend.provider_setup(provider)      @QtCore.Slot(dict)      def _load_provider_config(self, data): @@ -1250,20 +1261,19 @@ 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() -        self._backend.cancel_login() +        self._backend.provider_cancel_setup() +        self._backend.user_cancel_login() +        self._backend.soledad_cancel_bootstrap() +        self._backend.soledad_close() -        if self._soledad_defer is not None: -            logger.debug("Cancelling soledad defer.") -            self._soledad_defer.cancel() -            self._soledad_defer = None +        self._soledad_started = False      @QtCore.Slot()      def _set_login_cancelled(self):          """          TRIGGERS:              Signaler.prov_cancelled_setup fired by -            self._backend.cancel_setup_provider() +            self._backend.provider_cancel_setup()          This method re-enables the login widget and display a message for          the cancelled operation. @@ -1289,7 +1299,7 @@ class MainWindow(QtGui.QMainWindow):              self._show_hide_unsupported_services()              domain = self._provider_config.get_domain() -            self._backend.login(domain, username, password) +            self._backend.user_login(domain, username, password)          else:              logger.error(data[self._backend.ERROR_KEY])              self._login_problem_provider() @@ -1317,9 +1327,9 @@ class MainWindow(QtGui.QMainWindow):          if MX_SERVICE in self._enabled_services:              btn_enabled = self._login_widget.set_logout_btn_enabled              btn_enabled(False) -            self.soledad_ready.connect(lambda: btn_enabled(True)) -            self._soledad_bootstrapper.soledad_failed.connect( -                lambda: btn_enabled(True)) +            sig = self._backend.signaler +            sig.soledad_bootstrap_failed.connect(lambda: btn_enabled(True)) +            sig.soledad_bootstrap_finished.connect(lambda: btn_enabled(True))          if not self._get_best_provider_config().provides_mx():              self._set_mx_visible(False) @@ -1372,9 +1382,6 @@ class MainWindow(QtGui.QMainWindow):          Conditionally start Soledad.          """          # TODO split. -        if self._already_started_soledad is True: -            return -          if not self._provides_mx_and_enabled():              return @@ -1382,11 +1389,7 @@ class MainWindow(QtGui.QMainWindow):          password = unicode(self._login_widget.get_password())          provider_domain = self._login_widget.get_selected_provider() -        sb = self._soledad_bootstrapper -        if flags.OFFLINE is True: -            provider_domain = self._login_widget.get_selected_provider() -            sb._password = password - +        if flags.OFFLINE:              self._provisional_provider_config.load(                  provider.get_provider_path(provider_domain)) @@ -1399,74 +1402,31 @@ class MainWindow(QtGui.QMainWindow):                  # this is mostly for internal use/debug for now.                  logger.warning("Sorry! Log-in at least one time.")                  return -            fun = sb.load_offline_soledad -            fun(full_user_id, password, uuid) +            self._backend.soledad_load_offline(full_user_id, password, uuid)          else: -            provider_config = self._provider_config -              if self._logged_user is not None: -                self._soledad_defer = sb.run_soledad_setup_checks( -                    provider_config, username, password, -                    download_if_needed=True) +                domain = self._provider_config.get_domain() +                self._backend.soledad_bootstrap(username, domain, password)      ###################################################################      # Service control methods: soledad -    @QtCore.Slot(dict) -    def _soledad_intermediate_stage(self, data): -        # TODO missing param docstring +    @QtCore.Slot() +    def _on_soledad_ready(self):          """          TRIGGERS: -            self._soledad_bootstrapper.download_config +            Signaler.soledad_bootstrap_finished -        If there was a problem, displays it, otherwise it does nothing. -        This is used for intermediate bootstrapping stages, in case -        they fail. +        Actions to take when Soledad is ready.          """ -        passed = data[self._soledad_bootstrapper.PASSED_KEY] -        if not passed: -            # TODO display in the GUI: -            # should pass signal to a slot in status_panel -            # that sets the global status -            logger.error("Soledad failed to start: %s" % -                         (data[self._soledad_bootstrapper.ERROR_KEY],)) - -    @QtCore.Slot(dict) -    def _soledad_bootstrapped_stage(self, data): -        """ -        TRIGGERS: -            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 -        they fail. - -        :param data: result from the bootstrapping stage for Soledad -        :type data: dict -        """ -        passed = data[self._soledad_bootstrapper.PASSED_KEY] -        if not passed: -            # TODO should actually *display* on the panel. -            logger.debug("ERROR on soledad bootstrapping:") -            logger.error("%r" % data[self._soledad_bootstrapper.ERROR_KEY]) -            return -          logger.debug("Done bootstrapping Soledad") -        # Update the proxy objects to point to -        # the initialized instances. -        setProxiedObject(self._soledad, -                         self._soledad_bootstrapper.soledad) -        setProxiedObject(self._keymanager, -                         self._soledad_bootstrapper.keymanager) +        # Update the proxy objects to point to the initialized instances. +        # setProxiedObject(self._soledad, self._backend.get_soledad()) +        setProxiedObject(self._keymanager, self._backend.get_keymanager()) -        # Ok, now soledad is ready, so we can allow other things that -        # depend on soledad to start. -        self._soledad_defer = None +        self._soledad_started = True -        # this will trigger start_imap_service -        # and start_smtp_boostrapping          self.soledad_ready.emit()      ################################################################### @@ -1483,19 +1443,7 @@ class MainWindow(QtGui.QMainWindow):              return          if self._provides_mx_and_enabled(): -            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): -        """ -        TRIGGERS: -            self.logout -        """ -        # TODO call stop_mail_service -        self._mail_conductor.stop_smtp_service() +            self._mail_conductor.start_smtp_service(download_if_needed=True)      ###################################################################      # Service control methods: imap @@ -1510,48 +1458,19 @@ class MainWindow(QtGui.QMainWindow):          # in the mail state machine so it shows that imap is active          # (but not smtp since it's not yet ready for offline use)          start_fun = self._mail_conductor.start_imap_service -        if flags.OFFLINE is True: +        if flags.OFFLINE:              provider_domain = self._login_widget.get_selected_provider()              self._provider_config.load(                  provider.get_provider_path(provider_domain))          provides_mx = self._provider_config.provides_mx() -        if flags.OFFLINE is True and provides_mx: +        if flags.OFFLINE and provides_mx:              start_fun()              return          if self._provides_mx_and_enabled():              start_fun() -    def _on_mail_client_logged_in(self, req): -        """ -        Triggers qt signal when client login event is received. -        """ -        self.mail_client_logged_in.emit() - -    @QtCore.Slot() -    def _fetch_incoming_mail(self): -        """ -        TRIGGERS: -            self.mail_client_logged_in -        """ -        # TODO connect signal directly!!! -        self._mail_conductor.fetch_incoming_mail() - -    @QtCore.Slot() -    def _stop_imap_service(self): -        """ -        TRIGGERS: -            self.logout -        """ -        cv = Condition() -        cv.acquire() -        # TODO call stop_mail_service -        threads.deferToThread(self._mail_conductor.stop_imap_service, cv) -        # and wait for it to be stopped -        logger.debug('Waiting for imap service to stop.') -        cv.wait(self.SERVICE_STOP_TIMEOUT) -      # end service control methods (imap)      ################################################################### @@ -1690,7 +1609,7 @@ class MainWindow(QtGui.QMainWindow):          # won't try the next time.          self._settings.set_autostart_eip(True) -        self._backend.start_eip() +        self._backend.eip_start()      @QtCore.Slot()      def _on_eip_connection_aborted(self): @@ -1773,7 +1692,7 @@ class MainWindow(QtGui.QMainWindow):          :type abnormal: bool          """          self.user_stopped_eip = True -        self._backend.stop_eip() +        self._backend.eip_stop()          self._set_eipstatus_off(False)          self._already_started_eip = False @@ -1864,7 +1783,7 @@ class MainWindow(QtGui.QMainWindow):              eip_status_label = eip_status_label.format(self._eip_name)              self._eip_status.set_eip_status(eip_status_label, error=True)              signal = qtsigs.connection_aborted_signal -            self._backend.terminate_eip() +            self._backend.eip_terminate()          elif exitCode != 0 or not self.user_stopped_eip:              eip_status_label = self.tr("{0} finished in an unexpected manner!") @@ -1894,13 +1813,13 @@ class MainWindow(QtGui.QMainWindow):                  self.tr("Starting..."))              domain = self._login_widget.get_selected_provider() -            self._backend.setup_eip(domain) +            self._backend.eip_setup(domain)              self._already_started_eip = True              # we want to start soledad anyway after a certain timeout if eip              # fails to come up              QtCore.QTimer.singleShot( -                self.EIP_TIMEOUT, +                self.EIP_START_TIMEOUT,                  self._maybe_run_soledad_setup_checks)          else:              if not self._already_started_eip: @@ -1987,16 +1906,11 @@ class MainWindow(QtGui.QMainWindow):          Starts the logout sequence          """ -        setProxiedObject(self._soledad, None) -          self._cancel_ongoing_defers() -        # reset soledad status flag -        self._already_started_soledad = False -          # XXX: If other defers are doing authenticated stuff, this          # might conflict with those. CHECK! -        self._backend.logout() +        self._backend.user_logout()          self.logout.emit()      @QtCore.Slot() @@ -2080,59 +1994,37 @@ class MainWindow(QtGui.QMainWindow):      # cleanup and quit methods      # -    def _cleanup_pidfiles(self): +    def _stop_services(self):          """ -        Removes lockfiles on a clean shutdown. - -        Triggered after aboutToQuit signal. +        Stop services and cancel ongoing actions (if any).          """ -        if IS_WIN: -            WindowsLock.release_all_locks() - -    def _cleanup_and_quit(self): -        """ -        Call all the cleanup actions in a serialized way. -        Should be called from the quit function. -        """ -        logger.debug('About to quit, doing cleanup...') - -        self._stop_imap_service() - -        if self._logged_user is not None: -            self._backend.logout() +        logger.debug('About to quit, doing cleanup.') -        if self._soledad_bootstrapper.soledad is not None: -            logger.debug("Closing soledad...") -            self._soledad_bootstrapper.soledad.close() -        else: -            logger.error("No instance of soledad was found.") +        self._cancel_ongoing_defers() -        logger.debug('Terminating vpn') -        self._backend.stop_eip(shutdown=True) +        self._services_being_stopped = {'imap', 'eip'} -        # 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) +        imap_stopped = lambda: self._remove_service('imap') +        self._backend.signaler.imap_stopped.connect(imap_stopped) -    def _shutdown(self): -        """ -        Actually shutdown. -        """ -        self._cancel_ongoing_defers() +        eip_stopped = lambda: self._remove_service('eip') +        self._backend.signaler.eip_stopped.connect(eip_stopped) -        # TODO missing any more cancels? +        logger.debug('Stopping mail services') +        self._backend.imap_stop_service() +        self._backend.smtp_stop_service() -        logger.debug('Cleaning pidfiles') -        self._cleanup_pidfiles() -        if self._quit_callback: -            self._quit_callback() +        if self._logged_user is not None: +            logger.debug("Doing logout") +            self._backend.user_logout() -        logger.debug('Bye.') +        logger.debug('Terminating vpn') +        self._backend.eip_stop(shutdown=True)      def quit(self):          """ -        Cleanup and tidely close the main window before quitting. +        Start the quit sequence and wait for services to finish. +        Cleanup and close the main window before quitting.          """          # TODO separate the shutting down of services from the          # UI stuff. @@ -2145,22 +2037,69 @@ class MainWindow(QtGui.QMainWindow):                  self.tr('The app is quitting, please wait.'))          # explicitly process events to display tooltip immediately -        QtCore.QCoreApplication.processEvents() +        QtCore.QCoreApplication.processEvents(0, 10) + +        # Close other windows if any. +        if self._wizard: +            self._wizard.close() + +        if self._logger_window: +            self._logger_window.close()          # Set this in case that the app is hidden          QtGui.QApplication.setQuitOnLastWindowClosed(True) -        self._cleanup_and_quit() +        self._stop_services() -        # 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: -            self._wizard.close() +        # call final quit when all the services are stopped +        self.all_services_stopped.connect(self.final_quit) +        # or if we reach the timeout +        self._quit_timeout_callater = reactor.callLater( +            self.SERVICES_STOP_TIMEOUT, self.final_quit) -        if self._logger_window: -            self._logger_window.close() +    @QtCore.Slot() +    def _remove_service(self, service): +        """ +        Remove the given service from the waiting list and check if we have +        running services that we need to wait until we quit. +        Emit self.all_services_stopped signal if we don't need to keep waiting. +        :param service: the service that we want to remove +        :type service: str +        """ +        self._services_being_stopped.discard(service) + +        if not self._services_being_stopped: +            logger.debug("All services stopped.") +            self.all_services_stopped.emit() + +    @QtCore.Slot() +    def final_quit(self): +        """ +        Final steps to quit the app, starting from here we don't care about +        running services or user interaction, just quitting. +        """ +        logger.debug('Final quit...') + +        try: +            # disconnect signal if we get here due a timeout. +            self.all_services_stopped.disconnect(self.final_quit) +        except RuntimeError: +            pass  # Signal was not connected + +        # Cancel timeout to avoid being called if we reached here through the +        # signal +        if self._quit_timeout_callater.active(): +            self._quit_timeout_callater.cancel() + +        # Remove lockfiles on a clean shutdown. +        logger.debug('Cleaning pidfiles') +        if IS_WIN: +            WindowsLock.release_all_locks() + +        self._backend.stop()          self.close() + +        reactor.callLater(1, self._quit_callback) diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index 2947c5db..47011a85 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -23,12 +23,10 @@ import logging  from functools import partial  from PySide import QtCore, QtGui -from zope.proxy import sameProxiedObjects  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.util.password import basic_password_checks  from leap.bitmask.services import get_supported  from leap.bitmask.config.providerconfig import ProviderConfig @@ -43,8 +41,8 @@ class PreferencesWindow(QtGui.QDialog):      """      preferences_saved = QtCore.Signal() -    def __init__(self, parent, backend, provider_config, -                 soledad, username, domain): +    def __init__(self, parent, backend, provider_config, soledad_started, +                 username, domain):          """          :param parent: parent object of the PreferencesWindow.          :parent type: QWidget @@ -52,8 +50,8 @@ class PreferencesWindow(QtGui.QDialog):          :type backend: Backend          :param provider_config: ProviderConfig object.          :type provider_config: ProviderConfig -        :param soledad: Soledad instance -        :type soledad: Soledad +        :param soledad_started: whether soledad has started or not +        :type soledad_started: bool          :param username: the user set in the login widget          :type username: unicode          :param domain: the selected domain in the login widget @@ -64,8 +62,8 @@ class PreferencesWindow(QtGui.QDialog):          self._backend = backend          self._settings = LeapSettings() -        self._soledad = soledad          self._provider_config = provider_config +        self._soledad_started = soledad_started          self._username = username          self._domain = domain @@ -89,7 +87,7 @@ class PreferencesWindow(QtGui.QDialog):          else:              self._add_configured_providers() -        self._backend.get_logged_in_status() +        self._backend.user_get_logged_in_status()          self._select_provider_by_name(domain) @@ -118,7 +116,7 @@ class PreferencesWindow(QtGui.QDialog):                  pw_enabled = False              else:                  # check if Soledad is bootstrapped -                if sameProxiedObjects(self._soledad, None): +                if not self._soledad_started:                      msg = self.tr(                          "You need to wait until {0} is ready in "                          "order to change the password.".format(mx_name)) @@ -209,10 +207,10 @@ class PreferencesWindow(QtGui.QDialog):              return          self._set_changing_password(True) -        self._backend.change_password(current_password, new_password) +        self._backend.user_change_password(current_password, new_password)      @QtCore.Slot() -    def _change_password_ok(self): +    def _srp_change_password_ok(self):          """          TRIGGERS:              self._backend.signaler.srp_password_change_ok @@ -221,12 +219,33 @@ class PreferencesWindow(QtGui.QDialog):          """          new_password = self.ui.leNewPassword.text()          logger.debug("SRP password changed successfully.") -        try: -            self._soledad.change_passphrase(new_password) -            logger.debug("Soledad password changed successfully.") -        except NoStorageSecret: -            logger.debug( -                "No storage secret for password change in Soledad.") +        self._backend.soledad_change_password(new_password) + +    @QtCore.Slot(unicode) +    def _srp_change_password_problem(self, msg): +        """ +        TRIGGERS: +            self._backend.signaler.srp_password_change_error +            self._backend.signaler.srp_password_change_badpw + +        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) + +    @QtCore.Slot() +    def _soledad_change_password_ok(self): +        """ +        TRIGGERS: +            Signaler.soledad_password_change_ok + +        Callback used to display a successfully changed password. +        """ +        logger.debug("Soledad password changed successfully.")          self._set_password_change_status(              self.tr("Password changed successfully."), success=True) @@ -234,18 +253,17 @@ class PreferencesWindow(QtGui.QDialog):          self._set_changing_password(False)      @QtCore.Slot(unicode) -    def _change_password_problem(self, msg): +    def _soledad_change_password_problem(self, msg):          """          TRIGGERS: -            self._backend.signaler.srp_password_change_error -            self._backend.signaler.srp_password_change_badpw +            Signaler.soledad_password_change_error          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") +        logger.error("Error changing soledad password")          self._set_password_change_status(msg, error=True)          self._set_changing_password(False) @@ -418,12 +436,18 @@ class PreferencesWindow(QtGui.QDialog):          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) +        sig.srp_password_change_ok.connect(self._srp_change_password_ok) -        pwd_change_error = lambda: self._change_password_problem( +        pwd_change_error = lambda: self._srp_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( +        pwd_change_badpw = lambda: self._srp_change_password_problem(              self.tr("You did not enter a correct current password."))          sig.srp_password_change_badpw.connect(pwd_change_badpw) + +        sig.soledad_password_change_ok.connect( +            self._soledad_change_password_ok) + +        sig.soledad_password_change_error.connect( +            self._soledad_change_password_problem) diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index 020a58e2..316b2bff 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -257,7 +257,7 @@ class Wizard(QtGui.QWizard):          if ok:              self._set_register_status(self.tr("Starting registration...")) -            self._backend.register_user(self._domain, username, password) +            self._backend.user_register(self._domain, username, password)              self._username = username              self._password = password          else: @@ -406,7 +406,7 @@ class Wizard(QtGui.QWizard):          self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON)          self._provider_select_defer = self._backend.\ -            setup_provider(self._domain) +            provider_setup(self._domain)      @QtCore.Slot(bool)      def _skip_provider_checks(self, skip): diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index 734b88df..81eac6d9 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -17,6 +17,7 @@  """  VPN Manager, spawned in a custom processProtocol.  """ +import commands  import logging  import os  import shutil @@ -232,6 +233,17 @@ class VPN(object):                                      BM_ROOT, "firewall", "start"] + gateways)          return True if exitCode is 0 else False +    def is_fw_down(self): +        """ +        Return whether the firewall is down or not. + +        :rtype: bool +        """ +        BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT +        fw_up_cmd = "pkexec {0} firewall isup".format(BM_ROOT) +        fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256 +        return fw_is_down() +      def _tear_down_firewall(self):          """          Tear the firewall down using the privileged wrapper. diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py index 1766a39d..7fc19f1c 100644 --- a/src/leap/bitmask/services/mail/conductor.py +++ b/src/leap/bitmask/services/mail/conductor.py @@ -19,15 +19,10 @@ Mail Services Conductor  """  import logging -from zope.proxy import sameProxiedObjects - +from leap.bitmask.config import flags  from leap.bitmask.gui import statemachines  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.common.check import leap_assert  from leap.common.events import events_pb2 as leap_events  from leap.common.events import register as leap_register @@ -44,9 +39,6 @@ class IMAPControl(object):          Initializes smtp variables.          """          self.imap_machine = None -        self.imap_service = None -        self.imap_port = None -        self.imap_factory = None          self.imap_connection = None          leap_register(signal=leap_events.IMAP_SERVICE_STARTED, @@ -55,10 +47,13 @@ class IMAPControl(object):          leap_register(signal=leap_events.IMAP_SERVICE_FAILED_TO_START,                        callback=self._handle_imap_events,                        reqcbk=lambda req, resp: None) +        leap_register(signal=leap_events.IMAP_CLIENT_LOGIN, +                      callback=self._handle_imap_events, +                      reqcbk=lambda req, resp: None)      def set_imap_connection(self, imap_connection):          """ -        Sets the imap connection to an initialized connection. +        Set the imap connection to an initialized connection.          :param imap_connection: an initialized imap connection          :type imap_connection: IMAPConnection instance. @@ -67,67 +62,18 @@ class IMAPControl(object):      def start_imap_service(self):          """ -        Starts imap service. +        Start imap service.          """ -        from leap.bitmask.config import flags - -        logger.debug('Starting imap service') -        leap_assert(sameProxiedObjects(self._soledad, None) -                    is not True, -                    "We need a non-null soledad for initializing imap service") -        leap_assert(sameProxiedObjects(self._keymanager, None) -                    is not True, -                    "We need a non-null keymanager for initializing imap " -                    "service") - -        offline = flags.OFFLINE -        self.imap_service, self.imap_port, \ -            self.imap_factory = imap.start_imap_service( -                self._soledad, -                self._keymanager, -                userid=self.userid, -                offline=offline) +        self._backend.imap_start_service(self.userid, flags.OFFLINE) -        if offline is False: -            logger.debug("Starting loop") -            self.imap_service.start_loop() - -    def stop_imap_service(self, cv): +    def stop_imap_service(self):          """ -        Stops imap service (fetcher, factory and port). - -        :param cv: A condition variable to which we can signal when imap -                   indeed stops. -        :type cv: threading.Condition +        Stop imap service.          """          self.imap_connection.qtsigs.disconnecting_signal.emit() -        # TODO We should homogenize both services. -        if self.imap_service is not None: -            logger.debug('Stopping imap service.') -            # Stop the loop call in the fetcher -            self.imap_service.stop() -            self.imap_service = None -            # Stop listening on the IMAP port -            self.imap_port.stopListening() -            # Stop the protocol -            self.imap_factory.theAccount.closed = True -            self.imap_factory.doStop(cv) -        else: -            # main window does not have to wait because there's no service to -            # be stopped, so we release the condition variable -            cv.acquire() -            cv.notify() -            cv.release() - -    def fetch_incoming_mail(self): -        """ -        Fetches incoming mail. -        """ -        if self.imap_service: -            logger.debug('Client connected, fetching mail...') -            self.imap_service.fetch() - -    # handle events +        logger.debug('Stopping imap service.') + +        self._backend.imap_stop_service()      def _handle_imap_events(self, req):          """ @@ -137,25 +83,31 @@ class IMAPControl(object):          :type req: leap.common.events.events_pb2.SignalRequest          """          if req.event == leap_events.IMAP_SERVICE_STARTED: -            self.on_imap_connected() +            self._on_imap_connected()          elif req.event == leap_events.IMAP_SERVICE_FAILED_TO_START: -            self.on_imap_failed() +            self._on_imap_failed() +        elif req.event == leap_events.IMAP_CLIENT_LOGIN: +            self._on_mail_client_logged_in() -    # emit connection signals +    def _on_mail_client_logged_in(self): +        """ +        On mail client logged in, fetch incoming mail. +        """ +        self._controller.imap_service_fetch() -    def on_imap_connecting(self): +    def _on_imap_connecting(self):          """          Callback for IMAP connecting state.          """          self.imap_connection.qtsigs.connecting_signal.emit() -    def on_imap_connected(self): +    def _on_imap_connected(self):          """          Callback for IMAP connected state.          """          self.imap_connection.qtsigs.connected_signal.emit() -    def on_imap_failed(self): +    def _on_imap_failed(self):          """          Callback for IMAP failed state.          """ @@ -167,12 +119,9 @@ class SMTPControl(object):          """          Initializes smtp variables.          """ -        self.smtp_config = SMTPConfig()          self.smtp_connection = None          self.smtp_machine = None -        self.smtp_bootstrapper = SMTPBootstrapper() -          leap_register(signal=leap_events.SMTP_SERVICE_STARTED,                        callback=self._handle_smtp_events,                        reqcbk=lambda req, resp: None) @@ -188,29 +137,23 @@ class SMTPControl(object):          """          self.smtp_connection = smtp_connection -    def start_smtp_service(self, provider_config, download_if_needed=False): +    def start_smtp_service(self, download_if_needed=False):          """          Starts the SMTP service. -        :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          """          self.smtp_connection.qtsigs.connecting_signal.emit() -        self.smtp_bootstrapper.start_smtp_service( -            provider_config, self.smtp_config, self._keymanager, -            self.userid, download_if_needed) +        self._backend.smtp_start_service(self.userid, download_if_needed)      def stop_smtp_service(self):          """          Stops the SMTP service.          """          self.smtp_connection.qtsigs.disconnecting_signal.emit() -        self.smtp_bootstrapper.stop_smtp_service() - -    # handle smtp events +        self._backend.smtp_stop_service()      def _handle_smtp_events(self, req):          """ @@ -224,8 +167,6 @@ class SMTPControl(object):          elif req.event == leap_events.SMTP_SERVICE_FAILED_TO_START:              self.on_smtp_failed() -    # emit connection signals -      def on_smtp_connecting(self):          """          Callback for SMTP connecting state. @@ -253,22 +194,17 @@ class MailConductor(IMAPControl, SMTPControl):      """      # XXX We could consider to use composition instead of inheritance here. -    def __init__(self, soledad, keymanager): +    def __init__(self, backend):          """          Initializes the mail conductor. -        :param soledad: a transparent proxy that eventually will point to a -                        Soledad Instance. -        :type soledad: zope.proxy.ProxyBase - -        :param keymanager: a transparent proxy that eventually will point to a -                           Keymanager Instance. -        :type keymanager: zope.proxy.ProxyBase +        :param backend: Backend being used +        :type backend: Backend          """          IMAPControl.__init__(self)          SMTPControl.__init__(self) -        self._soledad = soledad -        self._keymanager = keymanager + +        self._backend = backend          self._mail_machine = None          self._mail_connection = mail_connection.MailConnection() @@ -309,6 +245,13 @@ class MailConductor(IMAPControl, SMTPControl):          self._smtp_machine = smtp          self._smtp_machine.start() +    def stop_mail_services(self): +        """ +        Stop the IMAP and SMTP services. +        """ +        self.imap_stop_service() +        self.smtp_stop_service() +      def connect_mail_signals(self, widget):          """          Connects the mail signals to the mail_status widget slots. diff --git a/src/leap/bitmask/services/mail/imapcontroller.py b/src/leap/bitmask/services/mail/imapcontroller.py new file mode 100644 index 00000000..d0bf4c34 --- /dev/null +++ b/src/leap/bitmask/services/mail/imapcontroller.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# imapcontroller.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. +""" +IMAP service controller. +""" +import logging + +from leap.bitmask.services.mail import imap + + +logger = logging.getLogger(__name__) + + +class IMAPController(object): +    """ +    IMAP Controller. +    """ +    def __init__(self, soledad, keymanager): +        """ +        Initialize IMAP variables. + +        :param soledad: a transparent proxy that eventually will point to a +                        Soledad Instance. +        :type soledad: zope.proxy.ProxyBase +        :param keymanager: a transparent proxy that eventually will point to a +                           Keymanager Instance. +        :type keymanager: zope.proxy.ProxyBase +        """ +        self._soledad = soledad +        self._keymanager = keymanager + +        self.imap_service = None +        self.imap_port = None +        self.imap_factory = None + +    def start_imap_service(self, userid, offline=False): +        """ +        Start IMAP service. + +        :param userid: user id, in the form "user@provider" +        :type userid: str +        :param offline: whether imap should start in offline mode or not. +        :type offline: bool +        """ +        logger.debug('Starting imap service') + +        self.imap_service, self.imap_port, \ +            self.imap_factory = imap.start_imap_service( +                self._soledad, +                self._keymanager, +                userid=userid, +                offline=offline) + +        if offline is False: +            logger.debug("Starting loop") +            self.imap_service.start_loop() + +    def stop_imap_service(self, cv): +        """ +        Stop IMAP service (fetcher, factory and port). + +        :param cv: A condition variable to which we can signal when imap +                   indeed stops. +        :type cv: threading.Condition +        """ +        if self.imap_service is not None: +            # Stop the loop call in the fetcher +            self.imap_service.stop() +            self.imap_service = None + +            # Stop listening on the IMAP port +            self.imap_port.stopListening() + +            # Stop the protocol +            self.imap_factory.theAccount.closed = True +            self.imap_factory.doStop(cv) +        else: +            # Release the condition variable so the caller doesn't have to wait +            cv.acquire() +            cv.notify() +            cv.release() + +    def fetch_incoming_mail(self): +        """ +        Fetch incoming mail. +        """ +        if self.imap_service: +            logger.debug('Client connected, fetching mail...') +            self.imap_service.fetch() diff --git a/src/leap/bitmask/services/mail/smtpbootstrapper.py b/src/leap/bitmask/services/mail/smtpbootstrapper.py index 7ecf8134..785fe404 100644 --- a/src/leap/bitmask/services/mail/smtpbootstrapper.py +++ b/src/leap/bitmask/services/mail/smtpbootstrapper.py @@ -28,7 +28,7 @@ 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.check import leap_assert  from leap.common.files import check_and_fix_urw_only  logger = logging.getLogger(__name__) @@ -38,6 +38,10 @@ class NoSMTPHosts(Exception):      """This is raised when there is no SMTP host to use.""" +class MalformedUserId(Exception): +    """This is raised when an userid does not have the form user@provider.""" + +  class SMTPBootstrapper(AbstractBootstrapper):      """      SMTP init procedure @@ -126,15 +130,10 @@ class SMTPBootstrapper(AbstractBootstrapper):              smtp_key=client_cert_path,              encrypted_only=False) -    def start_smtp_service(self, provider_config, smtp_config, keymanager, -                           userid, download_if_needed=False): +    def start_smtp_service(self, keymanager, userid, download_if_needed=False):          """          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 @@ -144,12 +143,15 @@ class SMTPBootstrapper(AbstractBootstrapper):                                     for the file          :type download_if_needed: bool          """ -        leap_assert_type(provider_config, ProviderConfig) -        leap_assert_type(smtp_config, SMTPConfig) +        try: +            username, domain = userid.split('@') +        except ValueError: +            logger.critical("Malformed userid parameter!") +            raise MalformedUserId() -        self._provider_config = provider_config +        self._provider_config = ProviderConfig.get_provider_config(domain)          self._keymanager = keymanager -        self._smtp_config = smtp_config +        self._smtp_config = SMTPConfig()          self._useid = userid          self._download_if_needed = download_if_needed diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index 6bb7c036..2bdad7e2 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -25,7 +25,6 @@ import sys  from ssl import SSLError  from sqlite3 import ProgrammingError as sqlite_ProgrammingError -from PySide import QtCore  from u1db import errors as u1db_errors  from twisted.internet import threads  from zope.proxy import sameProxiedObjects @@ -134,16 +133,11 @@ class SoledadBootstrapper(AbstractBootstrapper):      MAX_INIT_RETRIES = 10      MAX_SYNC_RETRIES = 10 -    # All dicts returned are of the form -    # {"passed": bool, "error": str} -    download_config = QtCore.Signal(dict) -    gen_key = QtCore.Signal(dict) -    local_only_ready = QtCore.Signal(dict) -    soledad_invalid_auth_token = QtCore.Signal() -    soledad_failed = QtCore.Signal() +    def __init__(self, signaler=None): +        AbstractBootstrapper.__init__(self, signaler) -    def __init__(self): -        AbstractBootstrapper.__init__(self) +        if signaler is not None: +            self._cancel_signal = signaler.SOLEDAD_CANCELLED_BOOTSTRAP          self._provider_config = None          self._soledad_config = None @@ -181,16 +175,22 @@ class SoledadBootstrapper(AbstractBootstrapper):          Instantiate Soledad for offline use.          :param username: full user id (user@provider) -        :type username: basestring +        :type username: str or unicode          :param password: the soledad passphrase          :type password: unicode          :param uuid: the user uuid -        :type uuid: basestring +        :type uuid: str or unicode          """          print "UUID ", uuid          self._address = username +        self._password = password          self._uuid = uuid -        return self.load_and_sync_soledad(uuid, offline=True) +        try: +            self.load_and_sync_soledad(uuid, offline=True) +            self._signaler.signal(self._signaler.SOLEDAD_OFFLINE_FINISHED) +        except Exception: +            # TODO: we should handle more specific exceptions in here +            self._signaler.signal(self._signaler.SOLEDAD_OFFLINE_FAILED)      def _get_soledad_local_params(self, uuid, offline=False):          """ @@ -245,7 +245,7 @@ class SoledadBootstrapper(AbstractBootstrapper):      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 +        Initialize soledad, retry if necessary and raise an exception if we          can't succeed.          :param uuid: user identifier @@ -263,19 +263,22 @@ class SoledadBootstrapper(AbstractBootstrapper):          :param auth token: auth token          :type auth_token: str          """ -        init_tries = self.MAX_INIT_RETRIES -        while init_tries > 0: +        init_tries = 1 +        while init_tries <= self.MAX_INIT_RETRIES:              try: +                logger.debug("Trying to init soledad....")                  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 +                init_tries += 1 +                msg = "Init failed, retrying... (retry {0} of {1})".format( +                    init_tries, self.MAX_INIT_RETRIES) +                logger.warning(msg)                  continue -        self.soledad_failed.emit()          raise SoledadInitError()      def load_and_sync_soledad(self, uuid=None, offline=False): @@ -306,9 +309,8 @@ class SoledadBootstrapper(AbstractBootstrapper):          leap_assert(not sameProxiedObjects(self._soledad, None),                      "Null soledad, error while initializing") -        if flags.OFFLINE is True: +        if flags.OFFLINE:              self._init_keymanager(self._address, token) -            self.local_only_ready.emit({self.PASSED_KEY: True})          else:              try:                  address = make_address( @@ -353,9 +355,10 @@ class SoledadBootstrapper(AbstractBootstrapper):          Do several retries to get an initial soledad sync.          """          # and now, let's sync -        sync_tries = self.MAX_SYNC_RETRIES -        while sync_tries > 0: +        sync_tries = 1 +        while sync_tries <= self.MAX_SYNC_RETRIES:              try: +                logger.debug("Trying to sync soledad....")                  self._try_soledad_sync()                  logger.debug("Soledad has been synced.")                  # so long, and thanks for all the fish @@ -368,19 +371,20 @@ class SoledadBootstrapper(AbstractBootstrapper):                  # retry strategy can be pushed to u1db, or at least                  # it's something worthy to talk about with the                  # ubuntu folks. -                sync_tries -= 1 +                sync_tries += 1 +                msg = "Sync failed, retrying... (retry {0} of {1})".format( +                    sync_tries, self.MAX_SYNC_RETRIES) +                logger.warning(msg)                  continue              except InvalidAuthTokenError: -                self.soledad_invalid_auth_token.emit() +                self._signaler.signal( +                    self._signaler.SOLEDAD_INVALID_AUTH_TOKEN)                  raise              except Exception as e:                  logger.exception("Unhandled error while syncing "                                   "soledad: %r" % (e,))                  break -        # reached bottom, failed to sync -        # and there's nothing we can do... -        self.soledad_failed.emit()          raise SoledadSyncError()      def _try_soledad_init(self, uuid, secrets_path, local_db_path, @@ -443,7 +447,6 @@ class SoledadBootstrapper(AbstractBootstrapper):          Raises SoledadSyncError if not successful.          """          try: -            logger.debug("trying to sync soledad....")              self._soledad.sync()          except SSLError as exc:              logger.error("%r" % (exc,)) @@ -467,7 +470,6 @@ class SoledadBootstrapper(AbstractBootstrapper):          """          Download the Soledad config for the given provider          """ -          leap_assert(self._provider_config,                      "We need a provider configuration!")          logger.debug("Downloading Soledad config for %s" % @@ -480,14 +482,6 @@ class SoledadBootstrapper(AbstractBootstrapper):              self._session,              self._download_if_needed) -        # soledad config is ok, let's proceed to load and sync soledad -        # XXX but honestly, this is a pretty strange entry point for that. -        # it feels like it should be the other way around: -        # load_and_sync, and from there, if needed, call download_config - -        uuid = self.srpauth.get_uuid() -        self.load_and_sync_soledad(uuid) -      def _get_gpg_bin_path(self):          """          Return the path to gpg binary. @@ -574,7 +568,7 @@ class SoledadBootstrapper(AbstractBootstrapper):                  logger.exception(exc)                  # but we do not raise -    def _gen_key(self, _): +    def _gen_key(self):          """          Generates the key pair if needed, uploads it to the webapp and          nickserver @@ -613,10 +607,7 @@ class SoledadBootstrapper(AbstractBootstrapper):          logger.debug("Key generated successfully.") -    def run_soledad_setup_checks(self, -                                 provider_config, -                                 user, -                                 password, +    def run_soledad_setup_checks(self, provider_config, user, password,                                   download_if_needed=False):          """          Starts the checks needed for a new soledad setup @@ -640,9 +631,27 @@ class SoledadBootstrapper(AbstractBootstrapper):          self._user = user          self._password = password -        cb_chain = [ -            (self._download_config, self.download_config), -            (self._gen_key, self.gen_key) -        ] +        if flags.OFFLINE: +            signal_finished = self._signaler.SOLEDAD_OFFLINE_FINISHED +            signal_failed = self._signaler.SOLEDAD_OFFLINE_FAILED +        else: +            signal_finished = self._signaler.SOLEDAD_BOOTSTRAP_FINISHED +            signal_failed = self._signaler.SOLEDAD_BOOTSTRAP_FAILED -        return self.addCallbackChain(cb_chain) +        try: +            self._download_config() + +            # soledad config is ok, let's proceed to load and sync soledad +            uuid = self.srpauth.get_uuid() +            self.load_and_sync_soledad(uuid) + +            if not flags.OFFLINE: +                self._gen_key() + +            self._signaler.signal(signal_finished) +        except Exception as e: +            # TODO: we should handle more specific exceptions in here +            self._soledad = None +            self._keymanager = None +            logger.exception("Error while bootstrapping Soledad: %r" % (e, )) +            self._signaler.signal(signal_failed) | 
