diff options
| -rw-r--r-- | changes/bug_enable_eip_whenever_possible | 1 | ||||
| -rw-r--r-- | changes/bug_fix_login_margins | 1 | ||||
| -rw-r--r-- | changes/cleanup-smtpbootstrapper | 1 | ||||
| -rw-r--r-- | changes/feature-5672_gracefully-handle-SIGTERM | 1 | ||||
| -rw-r--r-- | changes/feature_refactor-retry-to-soledadbootstrapper | 1 | ||||
| -rw-r--r-- | src/leap/bitmask/app.py | 19 | ||||
| -rw-r--r-- | src/leap/bitmask/backend.py | 89 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/mainwindow.py | 114 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/ui/login.ui | 2 | ||||
| -rw-r--r-- | src/leap/bitmask/services/mail/conductor.py | 118 | ||||
| -rw-r--r-- | src/leap/bitmask/services/mail/smtpbootstrapper.py | 135 | ||||
| -rw-r--r-- | src/leap/bitmask/services/soledad/soledadbootstrapper.py | 96 | 
12 files changed, 340 insertions, 238 deletions
| diff --git a/changes/bug_enable_eip_whenever_possible b/changes/bug_enable_eip_whenever_possible new file mode 100644 index 00000000..1065822f --- /dev/null +++ b/changes/bug_enable_eip_whenever_possible @@ -0,0 +1 @@ +- Enable Turn ON button for EIP whenever possible (json and cert are in place). Fixes #5665, #5666.
\ No newline at end of file diff --git a/changes/bug_fix_login_margins b/changes/bug_fix_login_margins new file mode 100644 index 00000000..3735d911 --- /dev/null +++ b/changes/bug_fix_login_margins @@ -0,0 +1 @@ +- Fix Logout button bottom margin. Fixes #4987.
\ No newline at end of file diff --git a/changes/cleanup-smtpbootstrapper b/changes/cleanup-smtpbootstrapper new file mode 100644 index 00000000..f1ccabfe --- /dev/null +++ b/changes/cleanup-smtpbootstrapper @@ -0,0 +1 @@ +- Refactor smtp logic into its bootstrapper. diff --git a/changes/feature-5672_gracefully-handle-SIGTERM b/changes/feature-5672_gracefully-handle-SIGTERM new file mode 100644 index 00000000..a616430d --- /dev/null +++ b/changes/feature-5672_gracefully-handle-SIGTERM @@ -0,0 +1 @@ +- Gracefully handle SIGTERM, with addSystemEventTrigger twisted reactor's method. Closes #5672. diff --git a/changes/feature_refactor-retry-to-soledadbootstrapper b/changes/feature_refactor-retry-to-soledadbootstrapper new file mode 100644 index 00000000..bd70a65f --- /dev/null +++ b/changes/feature_refactor-retry-to-soledadbootstrapper @@ -0,0 +1 @@ +- Refactor Soledad initialization retries to SoledadBootstrapper. diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index 146743b5..e413ab4c 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -76,6 +76,16 @@ def sigint_handler(*args, **kwargs):      mainwindow = args[0]      mainwindow.quit() +def sigterm_handler(*args, **kwargs): +    """ +    Signal handler for SIGTERM. +    This handler is actually passed to twisted reactor +    """ +    logger = kwargs.get('logger', None) +    if logger: +        logger.debug("SIGTERM catched. shutting down...") +    mainwindow = args[0] +    mainwindow.quit()  def add_logger_handlers(debug=False, logfile=None, replace_stdout=True):      """ @@ -314,6 +324,9 @@ def main():      sigint_window = partial(sigint_handler, window, logger=logger)      signal.signal(signal.SIGINT, sigint_window) +    # callable used in addSystemEventTrigger to handle SIGTERM +    sigterm_window = partial(sigterm_handler, window, logger=logger) +      if IS_MAC:          window.raise_() @@ -324,6 +337,12 @@ def main():      l = LoopingCall(QtCore.QCoreApplication.processEvents, 0, 10)      l.start(0.01) + +    # SIGTERM can't be handled the same way SIGINT is, since it's +    # caught by twisted. See _handleSignals method in +    # twisted/internet/base.py#L1150. So, addSystemEventTrigger +    # reactor's method is used. +    reactor.addSystemEventTrigger('before', 'shutdown', sigterm_window)      reactor.run()  if __name__ == "__main__": diff --git a/src/leap/bitmask/backend.py b/src/leap/bitmask/backend.py index a351a477..a2df465d 100644 --- a/src/leap/bitmask/backend.py +++ b/src/leap/bitmask/backend.py @@ -45,6 +45,8 @@ from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper  from leap.bitmask.services.eip import vpnlauncher, vpnprocess  from leap.bitmask.services.eip import linuxvpnlauncher, darwinvpnlauncher +from leap.common import certs as leap_certs +  # Frontend side  from PySide import QtCore @@ -279,18 +281,23 @@ class EIP(object):          self._vpn = vpnprocess.VPN(signaler=signaler) -    def setup_eip(self, domain): +    def setup_eip(self, domain, skip_network=False):          """          Initiate the setup for a provider          :param domain: URL for the provider          :type domain: unicode +        :param skip_network: Whether checks that involve network should be done +                             or not +        :type skip_network: bool          :returns: the defer for the operation running in a thread.          :rtype: twisted.internet.defer.Deferred          """          config = self._provider_config          if get_provider_config(config, domain): +            if skip_network: +                return defer.Deferred()              eb = self._eip_bootstrapper              d = eb.run_eip_setup_checks(self._provider_config,                                          download_if_needed=True) @@ -338,7 +345,8 @@ class EIP(object):          if not self._provider_config.loaded():              # This means that the user didn't call setup_eip first. -            self._signaler.signal(signaler.BACKEND_BAD_CALL) +            self._signaler.signal(signaler.BACKEND_BAD_CALL, "EIP.start(), " +                                  "no provider loaded")              return          try: @@ -489,6 +497,47 @@ class EIP(object):              self._signaler.signal(                  self._signaler.EIP_GET_GATEWAYS_LIST, gateways) +    def can_start(self, domain): +        """ +        Signal whether it has everything that is needed to run EIP or not + +        :param domain: the domain for the provider to check +        :type domain: str + +        Signals: +            eip_can_start +            eip_cannot_start +        """ +        try: +            eip_config = eipconfig.EIPConfig() +            provider_config = ProviderConfig.get_provider_config(domain) + +            api_version = provider_config.get_api_version() +            eip_config.set_api_version(api_version) +            eip_loaded = eip_config.load(eipconfig.get_eipconfig_path(domain)) + +            # check for other problems +            if not eip_loaded or provider_config is None: +                raise Exception("Cannot load provider and eip config, cannot " +                                "autostart") + +            client_cert_path = eip_config.\ +                get_client_cert_path(provider_config, about_to_download=False) + +            if leap_certs.should_redownload(client_cert_path): +                raise Exception("The client should redownload the certificate," +                                " cannot autostart") + +            if not os.path.isfile(client_cert_path): +                raise Exception("Can't find the certificate, cannot autostart") + +            if self._signaler is not None: +                self._signaler.signal(self._signaler.EIP_CAN_START) +        except Exception as e: +            logger.exception(e) +            if self._signaler is not None: +                self._signaler.signal(self._signaler.EIP_CANNOT_START) +  class Authenticate(object):      """ @@ -679,6 +728,10 @@ class Signaler(QtCore.QObject):      eip_status_changed = QtCore.Signal(dict)      eip_process_finished = QtCore.Signal(int) +    # signals whether the needed files to start EIP exist or not +    eip_can_start = QtCore.Signal(object) +    eip_cannot_start = QtCore.Signal(object) +      # This signal is used to warn the backend user that is doing something      # wrong      backend_bad_call = QtCore.Signal(object) @@ -745,6 +798,9 @@ class Signaler(QtCore.QObject):      EIP_STATUS_CHANGED = "eip_status_changed"      EIP_PROCESS_FINISHED = "eip_process_finished" +    EIP_CAN_START = "eip_can_start" +    EIP_CANNOT_START = "eip_cannot_start" +      BACKEND_BAD_CALL = "backend_bad_call"      def __init__(self): @@ -799,6 +855,9 @@ class Signaler(QtCore.QObject):              self.EIP_STATUS_CHANGED,              self.EIP_PROCESS_FINISHED, +            self.EIP_CAN_START, +            self.EIP_CANNOT_START, +              self.SRP_AUTH_OK,              self.SRP_AUTH_ERROR,              self.SRP_AUTH_SERVER_ERROR, @@ -1041,19 +1100,23 @@ class Backend(object):          self._call_queue.put(("register", "register_user", None, provider,                                username, password)) -    def setup_eip(self, provider): +    def setup_eip(self, provider, skip_network=False):          """          Initiate the setup for a provider -        :param domain: URL for the provider -        :type domain: unicode +        :param provider: URL for the provider +        :type provider: unicode +        :param skip_network: Whether checks that involve network should be done +                             or not +        :type skip_network: bool          Signals:              eip_config_ready             -> {PASSED_KEY: bool, ERROR_KEY: str}              eip_client_certificate_ready -> {PASSED_KEY: bool, ERROR_KEY: str}              eip_cancelled_setup          """ -        self._call_queue.put(("eip", "setup_eip", None, provider)) +        self._call_queue.put(("eip", "setup_eip", None, provider, +                              skip_network))      def cancel_setup_eip(self):          """ @@ -1132,6 +1195,20 @@ class Backend(object):          self._call_queue.put(("eip", "get_initialized_providers",                                None, domains)) +    def eip_can_start(self, domain): +        """ +        Signal whether it has everything that is needed to run EIP or not + +        :param domain: the domain for the provider to check +        :type domain: str + +        Signals: +            eip_can_start +            eip_cannot_start +        """ +        self._call_queue.put(("eip", "can_start", +                              None, domain)) +      def login(self, provider, username, password):          """          Execute the whole authentication process for a user diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 8b60ad8e..e3848c46 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -183,6 +183,8 @@ class MainWindow(QtGui.QMainWindow):          self.eip_needs_login.connect(self._eip_status.disable_eip_start)          self.eip_needs_login.connect(self._disable_eip_start_action) +        self._trying_to_start_eip = False +          # This is loaded only once, there's a bug when doing that more          # than once          # XXX HACK!! But we need it as long as we are using @@ -210,8 +212,6 @@ class MainWindow(QtGui.QMainWindow):              self._soledad_bootstrapped_stage)          self._soledad_bootstrapper.local_only_ready.connect(              self._soledad_bootstrapped_stage) -        self._soledad_bootstrapper.soledad_timeout.connect( -            self._retry_soledad_connection)          self._soledad_bootstrapper.soledad_invalid_auth_token.connect(              self._mail_status.set_soledad_invalid_auth_token)          self._soledad_bootstrapper.soledad_failed.connect( @@ -360,11 +360,24 @@ class MainWindow(QtGui.QMainWindow):          self._backend_connected_signals[signal] = method          signal.connect(method) +    def _backend_bad_call(self, data): +        """ +        Callback for debugging bad backend calls + +        :param data: data from the backend about the problem +        :type data: str +        """ +        logger.error("Bad call to the backend:") +        logger.error(data) +      def _backend_connect(self):          """          Helper to connect to backend signals          """          sig = self._backend.signaler + +        sig.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, @@ -445,6 +458,9 @@ class MainWindow(QtGui.QMainWindow):          sig.eip_process_restart_tls.connect(self._do_eip_restart)          sig.eip_process_restart_ping.connect(self._do_eip_restart) +        sig.eip_can_start.connect(self._backend_can_start_eip) +        sig.eip_cannot_start.connect(self._backend_cannot_start_eip) +      def _disconnect_and_untrack(self):          """          Helper to disconnect the tracked signals. @@ -612,17 +628,43 @@ class MainWindow(QtGui.QMainWindow):          """          settings = self._settings          default_provider = settings.get_defaultprovider() + +        if default_provider is None: +            logger.warning("Trying toupdate eip enabled status but there's no" +                           " default provider. Disabling EIP for the time" +                           " being...") +            self._backend_cannot_start_eip() +            return + +        self._trying_to_start_eip = settings.get_autostart_eip() +        self._backend.eip_can_start(default_provider) + +        # If we don't want to start eip, we leave everything +        # initialized to quickly start it +        if not self._trying_to_start_eip: +            self._backend.setup_eip(default_provider, skip_network=True) + +    def _backend_can_start_eip(self): +        """ +        TRIGGER: +            self._backend.signaler.eip_can_start + +        If EIP can be started right away, and the client is configured +        to do so, start it. Otherwise it leaves everything in place +        for the user to click Turn ON. +        """ +        settings = self._settings +        default_provider = settings.get_defaultprovider()          enabled_services = []          if default_provider is not None:              enabled_services = settings.get_enabled_services(default_provider)          eip_enabled = False          if EIP_SERVICE in enabled_services: -            should_autostart = settings.get_autostart_eip() -            if should_autostart and default_provider is not None: +            eip_enabled = True +            if default_provider is not None:                  self._eip_status.enable_eip_start()                  self._eip_status.set_eip_status("") -                eip_enabled = True              else:                  # we don't have an usable provider                  # so the user needs to log in first @@ -632,7 +674,32 @@ class MainWindow(QtGui.QMainWindow):              self._eip_status.disable_eip_start()              self._eip_status.set_eip_status(self.tr("Disabled")) -        return eip_enabled +        if eip_enabled and self._trying_to_start_eip: +            self._trying_to_start_eip = False +            self._try_autostart_eip() + +    def _backend_cannot_start_eip(self): +        """ +        TRIGGER: +            self._backend.signaler.eip_cannot_start + +        If EIP can't be started right away, get the UI to what it +        needs to look like and waits for a proper login/eip bootstrap. +        """ +        settings = self._settings +        default_provider = settings.get_defaultprovider() +        enabled_services = [] +        if default_provider is not None: +            enabled_services = settings.get_enabled_services(default_provider) + +        if EIP_SERVICE in enabled_services: +            # we don't have a usable provider +            # so the user needs to log in first +            self._eip_status.disable_eip_start() +        else: +            self._stop_eip() +            self._eip_status.disable_eip_start() +            self._eip_status.set_eip_status(self.tr("Disabled"))      @QtCore.Slot()      def _show_eip_preferences(self): @@ -752,7 +819,7 @@ class MainWindow(QtGui.QMainWindow):              self._wizard = None              self._backend_connect()          else: -            self._try_autostart_eip() +            self._update_eip_enabled_status()              domain = self._settings.get_provider()              if domain is not None: @@ -1363,22 +1430,6 @@ class MainWindow(QtGui.QMainWindow):              # that sets the global status              logger.error("Soledad failed to start: %s" %                           (data[self._soledad_bootstrapper.ERROR_KEY],)) -            self._retry_soledad_connection() - -    def _retry_soledad_connection(self): -        """ -        Retries soledad connection. -        """ -        # XXX should move logic to soledad boostrapper itself -        logger.debug("Retrying soledad connection.") -        if self._soledad_bootstrapper.should_retry_initialization(): -            self._soledad_bootstrapper.increment_retries_count() -            # XXX should cancel the existing socket --- this -            # is avoiding a clean termination. -            self._maybe_run_soledad_setup_checks() -        else: -            logger.warning("Max number of soledad initialization " -                           "retries reached.")      @QtCore.Slot(dict)      def _soledad_bootstrapped_stage(self, data): @@ -1431,13 +1482,9 @@ class MainWindow(QtGui.QMainWindow):              logger.debug("not starting smtp in offline mode")              return -        # TODO for simmetry, this should be called start_smtp_service -        # (and delegate all the checks to the conductor)          if self._provides_mx_and_enabled(): -            self._mail_conductor.smtp_bootstrapper.run_smtp_setup_checks( -                self._provider_config, -                self._mail_conductor.smtp_config, -                download_if_needed=True) +            self._mail_conductor.start_smtp_service(self._provider_config, +                                                    download_if_needed=True)      # XXX --- should remove from here, and connecte directly to the state      # machine. @@ -1612,23 +1659,19 @@ class MainWindow(QtGui.QMainWindow):          Tries to autostart EIP          """          settings = self._settings - -        if not self._update_eip_enabled_status(): -            return -          default_provider = settings.get_defaultprovider()          self._enabled_services = settings.get_enabled_services(              default_provider)          loaded = self._provisional_provider_config.load(              provider.get_provider_path(default_provider)) -        if loaded: +        if loaded and settings.get_autostart_eip():              # XXX I think we should not try to re-download config every time,              # it adds some delay.              # Maybe if it's the first run in a session,              # or we can try only if it fails.              self._maybe_start_eip() -        else: +        elif settings.get_autostart_eip():              # XXX: Display a proper message to the user              self.eip_needs_login.emit()              logger.error("Unable to load %s config, cannot autostart." % @@ -1944,7 +1987,6 @@ class MainWindow(QtGui.QMainWindow):          Starts the logout sequence          """ -        self._soledad_bootstrapper.cancel_bootstrap()          setProxiedObject(self._soledad, None)          self._cancel_ongoing_defers() diff --git a/src/leap/bitmask/gui/ui/login.ui b/src/leap/bitmask/gui/ui/login.ui index f5725d5a..216eca9e 100644 --- a/src/leap/bitmask/gui/ui/login.ui +++ b/src/leap/bitmask/gui/ui/login.ui @@ -215,7 +215,7 @@         <number>0</number>        </property>        <property name="bottomMargin"> -       <number>0</number> +       <number>12</number>        </property>        <item row="0" column="0" colspan="2">         <widget class="QLabel" name="lblUser"> diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py index c1761afa..1766a39d 100644 --- a/src/leap/bitmask/services/mail/conductor.py +++ b/src/leap/bitmask/services/mail/conductor.py @@ -18,22 +18,18 @@  Mail Services Conductor  """  import logging -import os -from PySide import QtCore  from zope.proxy import sameProxiedObjects  from leap.bitmask.gui import statemachines -from leap.bitmask.services.mail import imap  from leap.bitmask.services.mail import connection as mail_connection +from leap.bitmask.services.mail import imap  from leap.bitmask.services.mail.smtpbootstrapper import SMTPBootstrapper  from leap.bitmask.services.mail.smtpconfig import SMTPConfig -from leap.bitmask.util import is_file  from leap.common.check import leap_assert - -from leap.common.events import register as leap_register  from leap.common.events import events_pb2 as leap_events +from leap.common.events import register as leap_register  logger = logging.getLogger(__name__) @@ -167,10 +163,6 @@ class IMAPControl(object):  class SMTPControl(object): - -    PORT_KEY = "port" -    IP_KEY = "ip_address" -      def __init__(self):          """          Initializes smtp variables. @@ -178,12 +170,8 @@ class SMTPControl(object):          self.smtp_config = SMTPConfig()          self.smtp_connection = None          self.smtp_machine = None -        self._smtp_service = None -        self._smtp_port = None          self.smtp_bootstrapper = SMTPBootstrapper() -        self.smtp_bootstrapper.download_config.connect( -            self.smtp_bootstrapped_stage)          leap_register(signal=leap_events.SMTP_SERVICE_STARTED,                        callback=self._handle_smtp_events, @@ -200,100 +188,27 @@ class SMTPControl(object):          """          self.smtp_connection = smtp_connection -    def start_smtp_service(self, host, port, cert): +    def start_smtp_service(self, provider_config, download_if_needed=False):          """ -        Starts the smtp service. +        Starts the SMTP service. -        :param host: the hostname of the remove SMTP server. -        :type host: str -        :param port: the port of the remote SMTP server -        :type port: str -        :param cert: the client certificate for authentication -        :type cert: str +        :param provider_config: Provider configuration +        :type provider_config: ProviderConfig +        :param download_if_needed: True if it should check for mtime +                                   for the file +        :type download_if_needed: bool          """ -        # TODO Make the encrypted_only configurable -        # TODO pick local smtp port in a better way -        # TODO remove hard-coded port and let leap.mail set -        # the specific default.          self.smtp_connection.qtsigs.connecting_signal.emit() -        from leap.mail.smtp import setup_smtp_gateway -        self._smtp_service, self._smtp_port = setup_smtp_gateway( -            port=2013, -            userid=self.userid, -            keymanager=self._keymanager, -            smtp_host=host, -            smtp_port=port, -            smtp_cert=cert, -            smtp_key=cert, -            encrypted_only=False) +        self.smtp_bootstrapper.start_smtp_service( +            provider_config, self.smtp_config, self._keymanager, +            self.userid, download_if_needed)      def stop_smtp_service(self):          """ -        Stops the smtp service (port and factory). +        Stops the SMTP service.          """          self.smtp_connection.qtsigs.disconnecting_signal.emit() -        # TODO We should homogenize both services. -        if self._smtp_service is not None: -            logger.debug('Stopping smtp service.') -            self._smtp_port.stopListening() -            self._smtp_service.doStop() - -    @QtCore.Slot(dict) -    def smtp_bootstrapped_stage(self, data): -        """ -        TRIGGERS: -            self.smtp_bootstrapper.download_config - -        If there was a problem, displays it, otherwise it does nothing. -        This is used for intermediate bootstrapping stages, in case -        they fail. - -        :param data: result from the bootstrapping stage for Soledad -        :type data: dict -        """ -        passed = data[self.smtp_bootstrapper.PASSED_KEY] -        if not passed: -            logger.error(data[self.smtp_bootstrapper.ERROR_KEY]) -            return -        logger.debug("Done bootstrapping SMTP") -        self.check_smtp_config() - -    def check_smtp_config(self): -        """ -        Checks smtp config and tries to download smtp client cert if needed. -        Currently called when smtp_bootstrapped_stage has successfuly finished. -        """ -        logger.debug("Checking SMTP config...") -        leap_assert(self.smtp_bootstrapper._provider_config, -                    "smtp bootstrapper does not have a provider_config") - -        provider_config = self.smtp_bootstrapper._provider_config -        smtp_config = self.smtp_config -        hosts = smtp_config.get_hosts() -        # TODO handle more than one host and define how to choose -        if len(hosts) > 0: -            hostname = hosts.keys()[0] -            logger.debug("Using hostname %s for SMTP" % (hostname,)) -            host = hosts[hostname][self.IP_KEY].encode("utf-8") -            port = hosts[hostname][self.PORT_KEY] - -            client_cert = smtp_config.get_client_cert_path( -                provider_config, -                about_to_download=True) - -            # XXX change this logic! -            # check_config should be called from within start_service, -            # and not the other way around. -            if not is_file(client_cert): -                self.smtp_bootstrapper._download_client_certificates() -            if os.path.isfile(client_cert): -                self.start_smtp_service(host, port, client_cert) -            else: -                logger.warning("Tried to download email client " -                               "certificate, but could not find any") - -        else: -            logger.warning("No smtp hosts configured") +        self.smtp_bootstrapper.stop_smtp_service()      # handle smtp events @@ -348,7 +263,7 @@ class MailConductor(IMAPControl, SMTPControl):          :param keymanager: a transparent proxy that eventually will point to a                             Keymanager Instance. -        :type soledad: zope.proxy.ProxyBase +        :type keymanager: zope.proxy.ProxyBase          """          IMAPControl.__init__(self)          SMTPControl.__init__(self) @@ -406,4 +321,5 @@ class MailConductor(IMAPControl, SMTPControl):          qtsigs.connecting_signal.connect(widget.mail_state_connecting)          qtsigs.disconnecting_signal.connect(widget.mail_state_disconnecting)          qtsigs.disconnected_signal.connect(widget.mail_state_disconnected) -        qtsigs.soledad_invalid_auth_token.connect(widget.soledad_invalid_auth_token) +        qtsigs.soledad_invalid_auth_token.connect( +            widget.soledad_invalid_auth_token) diff --git a/src/leap/bitmask/services/mail/smtpbootstrapper.py b/src/leap/bitmask/services/mail/smtpbootstrapper.py index 032d6357..7ecf8134 100644 --- a/src/leap/bitmask/services/mail/smtpbootstrapper.py +++ b/src/leap/bitmask/services/mail/smtpbootstrapper.py @@ -20,12 +20,13 @@ SMTP bootstrapping  import logging  import os -from PySide import QtCore -  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.crypto.certs import download_client_cert  from leap.bitmask.services import download_service_config  from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper +from leap.bitmask.services.mail.smtpconfig import SMTPConfig +from leap.bitmask.util import is_file +  from leap.common import certs as leap_certs  from leap.common.check import leap_assert, leap_assert_type  from leap.common.files import check_and_fix_urw_only @@ -33,27 +34,33 @@ from leap.common.files import check_and_fix_urw_only  logger = logging.getLogger(__name__) +class NoSMTPHosts(Exception): +    """This is raised when there is no SMTP host to use.""" + +  class SMTPBootstrapper(AbstractBootstrapper):      """      SMTP init procedure      """ -    # All dicts returned are of the form -    # {"passed": bool, "error": str} -    download_config = QtCore.Signal(dict) +    PORT_KEY = "port" +    IP_KEY = "ip_address"      def __init__(self):          AbstractBootstrapper.__init__(self)          self._provider_config = None          self._smtp_config = None +        self._userid = None          self._download_if_needed = False -    def _download_config(self, *args): +        self._smtp_service = None +        self._smtp_port = None + +    def _download_config_and_cert(self):          """ -        Downloads the SMTP config for the given provider +        Downloads the SMTP config and cert for the given provider.          """ -          leap_assert(self._provider_config,                      "We need a provider configuration!") @@ -66,63 +73,101 @@ class SMTPBootstrapper(AbstractBootstrapper):              self._session,              self._download_if_needed) -    def _download_client_certificates(self, *args): -        """ -        Downloads the SMTP client certificate for the given provider +        hosts = self._smtp_config.get_hosts() -        We actually are downloading the certificate for the same uri as -        for the EIP config, but we duplicate these bits to allow mail -        service to be working in a provider that does not offer EIP. -        """ -        # TODO factor out with eipboostrapper.download_client_certificates -        # TODO this shouldn't be a private method, it's called from -        # mainwindow. -        leap_assert(self._provider_config, "We need a provider configuration!") -        leap_assert(self._smtp_config, "We need an smtp configuration!") +        if len(hosts) == 0: +            raise NoSMTPHosts() -        logger.debug("Downloading SMTP client certificate for %s" % -                     (self._provider_config.get_domain(),)) +        # TODO handle more than one host and define how to choose +        hostname = hosts.keys()[0] +        logger.debug("Using hostname %s for SMTP" % (hostname,)) -        client_cert_path = self._smtp_config.\ -            get_client_cert_path(self._provider_config, -                                 about_to_download=True) +        client_cert_path = self._smtp_config.get_client_cert_path( +            self._provider_config, about_to_download=True) -        # For re-download if something is wrong with the cert -        self._download_if_needed = self._download_if_needed and \ -            not leap_certs.should_redownload(client_cert_path) +        if not is_file(client_cert_path): +            # For re-download if something is wrong with the cert +            self._download_if_needed = ( +                self._download_if_needed and +                not leap_certs.should_redownload(client_cert_path)) -        if self._download_if_needed and \ -                os.path.isfile(client_cert_path): -            check_and_fix_urw_only(client_cert_path) -            return +            if self._download_if_needed and os.path.isfile(client_cert_path): +                check_and_fix_urw_only(client_cert_path) +                return -        download_client_cert(self._provider_config, -                             client_cert_path, -                             self._session) +            download_client_cert(self._provider_config, +                                 client_cert_path, +                                 self._session) -    def run_smtp_setup_checks(self, -                              provider_config, -                              smtp_config, -                              download_if_needed=False): +    def _start_smtp_service(self): +        """ +        Start the smtp service using the downloaded configurations. +        """ +        # TODO Make the encrypted_only configurable +        # TODO pick local smtp port in a better way +        # TODO remove hard-coded port and let leap.mail set +        # the specific default. +        # TODO handle more than one host and define how to choose +        hosts = self._smtp_config.get_hosts() +        hostname = hosts.keys()[0] +        host = hosts[hostname][self.IP_KEY].encode("utf-8") +        port = hosts[hostname][self.PORT_KEY] +        client_cert_path = self._smtp_config.get_client_cert_path( +            self._provider_config, about_to_download=True) + +        from leap.mail.smtp import setup_smtp_gateway +        self._smtp_service, self._smtp_port = setup_smtp_gateway( +            port=2013, +            userid=self._userid, +            keymanager=self._keymanager, +            smtp_host=host, +            smtp_port=port, +            smtp_cert=client_cert_path, +            smtp_key=client_cert_path, +            encrypted_only=False) + +    def start_smtp_service(self, provider_config, smtp_config, keymanager, +                           userid, download_if_needed=False):          """ -        Starts the checks needed for a new smtp setup +        Starts the SMTP service.          :param provider_config: Provider configuration          :type provider_config: ProviderConfig          :param smtp_config: SMTP configuration to populate          :type smtp_config: SMTPConfig +        :param keymanager: a transparent proxy that eventually will point to a +                           Keymanager Instance. +        :type keymanager: zope.proxy.ProxyBase +        :param userid: the user id, in the form "user@provider" +        :type userid: str          :param download_if_needed: True if it should check for mtime                                     for the file          :type download_if_needed: bool          """          leap_assert_type(provider_config, ProviderConfig) +        leap_assert_type(smtp_config, SMTPConfig)          self._provider_config = provider_config +        self._keymanager = keymanager          self._smtp_config = smtp_config +        self._useid = userid          self._download_if_needed = download_if_needed -        cb_chain = [ -            (self._download_config, self.download_config), -        ] - -        self.addCallbackChain(cb_chain) +        try: +            self._download_config_and_cert() +            logger.debug("Starting SMTP service.") +            self._start_smtp_service() +        except NoSMTPHosts: +            logger.warning("There is no SMTP host to use.") +        except Exception as e: +            # TODO: we should handle more specific exceptions in here +            logger.exception("Error while bootstrapping SMTP: %r" % (e, )) + +    def stop_smtp_service(self): +        """ +        Stops the smtp service (port and factory). +        """ +        if self._smtp_service is not None: +            logger.debug('Stopping SMTP service.') +            self._smtp_port.stopListening() +            self._smtp_service.doStop() diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index ad5ee4d0..6bb7c036 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -139,7 +139,6 @@ class SoledadBootstrapper(AbstractBootstrapper):      download_config = QtCore.Signal(dict)      gen_key = QtCore.Signal(dict)      local_only_ready = QtCore.Signal(dict) -    soledad_timeout = QtCore.Signal()      soledad_invalid_auth_token = QtCore.Signal()      soledad_failed = QtCore.Signal() @@ -159,8 +158,6 @@ class SoledadBootstrapper(AbstractBootstrapper):          self._srpauth = None          self._soledad = None -        self._soledad_retries = 0 -      @property      def keymanager(self):          return self._keymanager @@ -177,26 +174,6 @@ class SoledadBootstrapper(AbstractBootstrapper):                      "We need a provider config")          return SRPAuth(self._provider_config) -    # retries - -    def cancel_bootstrap(self): -        self._soledad_retries = self.MAX_INIT_RETRIES - -    def should_retry_initialization(self): -        """ -        Return True if we should retry the initialization. -        """ -        logger.debug("current retries: %s, max retries: %s" % ( -            self._soledad_retries, -            self.MAX_INIT_RETRIES)) -        return self._soledad_retries < self.MAX_INIT_RETRIES - -    def increment_retries_count(self): -        """ -        Increment the count of initialization retries. -        """ -        self._soledad_retries += 1 -      # initialization      def load_offline_soledad(self, username, password, uuid): @@ -265,6 +242,41 @@ class SoledadBootstrapper(AbstractBootstrapper):          # in the case of an invalid token we have already turned off mail and          # warned the user in _do_soledad_sync() +    def _do_soledad_init(self, uuid, secrets_path, local_db_path, +                         server_url, cert_file, token): +        """ +        Initialize soledad, retry if necessary and emit soledad_failed if we +        can't succeed. + +        :param uuid: user identifier +        :type uuid: str +        :param secrets_path: path to secrets file +        :type secrets_path: str +        :param local_db_path: path to local db file +        :type local_db_path: str +        :param server_url: soledad server uri +        :type server_url: str +        :param cert_file: path to the certificate of the ca used +                          to validate the SSL certificate used by the remote +                          soledad server. +        :type cert_file: str +        :param auth token: auth token +        :type auth_token: str +        """ +        init_tries = self.MAX_INIT_RETRIES +        while init_tries > 0: +            try: +                self._try_soledad_init( +                    uuid, secrets_path, local_db_path, +                    server_url, cert_file, token) +                logger.debug("Soledad has been initialized.") +                return +            except Exception: +                init_tries -= 1 +                continue + +        self.soledad_failed.emit() +        raise SoledadInitError()      def load_and_sync_soledad(self, uuid=None, offline=False):          """ @@ -283,10 +295,9 @@ class SoledadBootstrapper(AbstractBootstrapper):          server_url, cert_file = remote_param          try: -            self._try_soledad_init( -                uuid, secrets_path, local_db_path, -                server_url, cert_file, token) -        except Exception: +            self._do_soledad_init(uuid, secrets_path, local_db_path, +                                  server_url, cert_file, token) +        except SoledadInitError:              # re-raise the exceptions from try_init,              # we're currently handling the retries from the              # soledad-launcher in the gui. @@ -378,9 +389,13 @@ class SoledadBootstrapper(AbstractBootstrapper):          Try to initialize soledad.          :param uuid: user identifier +        :type uuid: str          :param secrets_path: path to secrets file +        :type secrets_path: str          :param local_db_path: path to local db file +        :type local_db_path: str          :param server_url: soledad server uri +        :type server_url: str          :param cert_file: path to the certificate of the ca used                            to validate the SSL certificate used by the remote                            soledad server. @@ -409,34 +424,17 @@ class SoledadBootstrapper(AbstractBootstrapper):          # and return a subclass of SoledadInitializationFailed          # recoverable, will guarantee retries -        except socket.timeout: -            logger.debug("SOLEDAD initialization TIMED OUT...") -            self.soledad_timeout.emit() -            raise -        except socket.error as exc: -            logger.warning("Socket error while initializing soledad") -            self.soledad_timeout.emit() -            raise -        except BootstrapSequenceError as exc: -            logger.warning("Error while initializing soledad") -            self.soledad_timeout.emit() +        except (socket.timeout, socket.error, BootstrapSequenceError): +            logger.warning("Error while initializing Soledad")              raise          # unrecoverable -        except u1db_errors.Unauthorized: -            logger.error("Error while initializing soledad " -                         "(unauthorized).") -            self.soledad_failed.emit() -            raise -        except u1db_errors.HTTPError as exc: -            logger.exception("Error while initializing soledad " -                             "(HTTPError)") -            self.soledad_failed.emit() +        except (u1db_errors.Unauthorized, u1db_errors.HTTPError): +            logger.error("Error while initializing Soledad (u1db error).")              raise          except Exception as exc:              logger.exception("Unhandled error while initializating " -                             "soledad: %r" % (exc,)) -            self.soledad_failed.emit() +                             "Soledad: %r" % (exc,))              raise      def _try_soledad_sync(self): | 
