diff options
Diffstat (limited to 'src')
26 files changed, 1372 insertions, 493 deletions
diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index 40a77075..3bb9c8c3 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -39,7 +39,6 @@  # M:::::::::::~NMMM7???7MMMM:::::::::::::::::::::::NMMMI??I7MMMM:::::::::::::M  # M::::::::::::::7MMMMMMM+:::::::::::::::::::::::::::?MMMMMMMZ:::::::::::::::M  #                (thanks to: http://www.glassgiant.com/ascii/) -  import logging  import signal  import sys @@ -105,9 +104,16 @@ def add_logger_handlers(debug=False, logfile=None):      formatter = logging.Formatter(log_format)      # Console handler -    console = logging.StreamHandler() -    console.setLevel(level) -    console.setFormatter(formatter) +    try: +        import coloredlogs +        console = coloredlogs.ColoredStreamHandler(level=level) +    except ImportError: +        console = logging.StreamHandler() +        console.setLevel(level) +        console.setFormatter(formatter) +        using_coloredlog = False +    else: +        using_coloredlog = True      silencer = log_silencer.SelectiveSilencerFilter()      console.addFilter(silencer) @@ -131,6 +137,9 @@ def add_logger_handlers(debug=False, logfile=None):          logger.addHandler(fileh)          logger.debug('File handler plugged!') +    if not using_coloredlog: +        replace_stdout_stderr_with_logging(logger) +      return logger @@ -185,7 +194,6 @@ def main():      BaseConfig.standalone = standalone      logger = add_logger_handlers(debug, logfile) -    replace_stdout_stderr_with_logging(logger)      # And then we import all the other stuff      from leap.bitmask.gui import locale_rc diff --git a/src/leap/bitmask/config/providerconfig.py b/src/leap/bitmask/config/providerconfig.py index 44698d83..e80b2337 100644 --- a/src/leap/bitmask/config/providerconfig.py +++ b/src/leap/bitmask/config/providerconfig.py @@ -169,11 +169,12 @@ class ProviderConfig(BaseConfig):                                    checking if the cert exists because we                                    are about to write it.          :type about_to_download: bool + +        :rtype: unicode          """          cert_path = os.path.join(get_path_prefix(), "leap", "providers", -                                 self.get_domain(), -                                 "keys", "ca", "cacert.pem") +                                 self.get_domain(), "keys", "ca", "cacert.pem")          if not about_to_download:              cert_exists = os.path.exists(cert_path) diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index 47ed21b0..ab98850d 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -17,6 +17,7 @@  import binascii  import logging +import sys  import requests  import srp @@ -31,6 +32,7 @@ from PySide import QtCore  from twisted.internet import threads  from leap.bitmask.util import request_helpers as reqhelper +from leap.bitmask.util.compat import requests_has_max_retries  from leap.bitmask.util.constants import REQUEST_TIMEOUT  from leap.common.check import leap_assert  from leap.common.events import signal as events_signal @@ -184,7 +186,11 @@ class SRPAuth(QtCore.QObject):              # NOTE: This is a workaround for the moment, the server              # side seems to return correctly every time, but it fails              # on the client end. -            self._session.mount('https://', HTTPAdapter(max_retries=30)) +            if requests_has_max_retries: +                adapter = HTTPAdapter(max_retries=30) +            else: +                adapter = HTTPAdapter() +            self._session.mount('https://', adapter)          def _safe_unhexlify(self, val):              """ @@ -211,10 +217,9 @@ class SRPAuth(QtCore.QObject):              """              logger.debug("Authentication preprocessing...") -            self._srp_user = self._srp.User(username, -                                            password, -                                            self._hashfun, -                                            self._ng) +            self._srp_user = self._srp.User(username.encode('utf-8'), +                                            password.encode('utf-8'), +                                            self._hashfun, self._ng)              _, A = self._srp_user.start_authentication()              self._srp_a = A @@ -249,10 +254,13 @@ class SRPAuth(QtCore.QObject):                      (self._provider_config.get_api_uri(),                       self._provider_config.get_api_version(),                       "sessions") + +                ca_cert_path = self._provider_config.get_ca_cert_path() +                ca_cert_path = ca_cert_path.encode(sys.getfilesystemencoding()) +                  init_session = self._session.post(sessions_url,                                                    data=auth_data, -                                                  verify=self._provider_config. -                                                  get_ca_cert_path(), +                                                  verify=ca_cert_path,                                                    timeout=REQUEST_TIMEOUT)                  # Clean up A value, we don't need it anymore                  self._srp_a = None @@ -478,7 +486,8 @@ class SRPAuth(QtCore.QObject):                  self.get_uid())              salt, verifier = self._srp.create_salted_verification_key( -                self._username, new_password, self._hashfun, self._ng) +                self._username.encode('utf-8'), new_password.encode('utf-8'), +                self._hashfun, self._ng)              cookies = {self.SESSION_ID_KEY: self.get_session_id()}              headers = { @@ -509,9 +518,9 @@ class SRPAuth(QtCore.QObject):              Might raise SRPAuthenticationError              :param username: username for this session -            :type username: str +            :type username: unicode              :param password: password for this user -            :type password: str +            :type password: unicode              :returns: A defer on a different thread              :rtype: twisted.internet.defer.Deferred diff --git a/src/leap/bitmask/gui/eip_preferenceswindow.py b/src/leap/bitmask/gui/eip_preferenceswindow.py index 9f8c4ff4..e0c5d51f 100644 --- a/src/leap/bitmask/gui/eip_preferenceswindow.py +++ b/src/leap/bitmask/gui/eip_preferenceswindow.py @@ -50,7 +50,6 @@ class EIPPreferencesWindow(QtGui.QDialog):          self.ui = Ui_EIPPreferences()          self.ui.setupUi(self)          self.ui.lblProvidersGatewayStatus.setVisible(False) -        self.ui.lblAutoStartEIPStatus.setVisible(False)          # Connections          self.ui.cbProvidersGateway.currentIndexChanged[unicode].connect( @@ -59,40 +58,8 @@ class EIPPreferencesWindow(QtGui.QDialog):          self.ui.cbGateways.currentIndexChanged[unicode].connect(              lambda x: self.ui.lblProvidersGatewayStatus.setVisible(False)) -        self.ui.cbProvidersEIP.currentIndexChanged[unicode].connect( -            lambda x: self.ui.lblAutoStartEIPStatus.setVisible(False)) - -        self.ui.cbAutoStartEIP.toggled.connect( -            lambda x: self.ui.lblAutoStartEIPStatus.setVisible(False)) - -        self.ui.pbSaveAutoStartEIP.clicked.connect(self._save_auto_start_eip) -          self._add_configured_providers() -        # Load auto start EIP settings -        self.ui.cbAutoStartEIP.setChecked(self._settings.get_autostart_eip()) -        default_provider = self._settings.get_defaultprovider() -        idx = self.ui.cbProvidersEIP.findText(default_provider) -        self.ui.cbProvidersEIP.setCurrentIndex(idx) - -    def _save_auto_start_eip(self): -        """ -        SLOT -        TRIGGER: -            self.ui.cbAutoStartEIP.toggled - -        Saves the automatic start of EIP user preference. -        """ -        default_provider = self.ui.cbProvidersEIP.currentText() -        enabled = self.ui.cbAutoStartEIP.isChecked() - -        self._settings.set_autostart_eip(enabled) -        self._settings.set_defaultprovider(default_provider) - -        self.ui.lblAutoStartEIPStatus.show() -        logger.debug('Auto start EIP saved: {0} {1}.'.format( -            default_provider, enabled)) -      def _set_providers_gateway_status(self, status, success=False,                                        error=False):          """ @@ -120,16 +87,13 @@ class EIPPreferencesWindow(QtGui.QDialog):          Add the client's configured providers to the providers combo boxes.          """          self.ui.cbProvidersGateway.clear() -        self.ui.cbProvidersEIP.clear()          providers = self._settings.get_configured_providers()          if not providers: -            self.ui.gbAutomaticEIP.setEnabled(False)              self.ui.gbGatewaySelector.setEnabled(False)              return          for provider in providers:              self.ui.cbProvidersGateway.addItem(provider) -            self.ui.cbProvidersEIP.addItem(provider)      def _save_selected_gateway(self, provider):          """ diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py index 77685cd3..324586c0 100644 --- a/src/leap/bitmask/gui/eip_status.py +++ b/src/leap/bitmask/gui/eip_status.py @@ -269,6 +269,10 @@ class EIPStatusWidget(QtGui.QWidget):          :type error: bool          """          leap_assert_type(error, bool) +        if error: +            logger.error(status) +        else: +            logger.debug(status)          self._eip_status = status          if error:              status = "<font color='red'>%s</font>" % (status,) diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py index ac34fe23..b21057f0 100644 --- a/src/leap/bitmask/gui/login.py +++ b/src/leap/bitmask/gui/login.py @@ -49,6 +49,9 @@ class LoginWidget(QtGui.QWidget):      BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" +    # Keyring +    KEYRING_KEY = "bitmask" +      def __init__(self, settings, parent=None):          """          Constructs the LoginWidget. @@ -168,7 +171,7 @@ class LoginWidget(QtGui.QWidget):          """          Returns the user that appears in the widget. -        :rtype: str +        :rtype: unicode          """          return self.ui.lnUser.text() @@ -177,7 +180,7 @@ class LoginWidget(QtGui.QWidget):          Sets the password for the widget          :param password: password to set -        :type password: str +        :type password: unicode          """          self.ui.lnPassword.setText(password) @@ -185,7 +188,7 @@ class LoginWidget(QtGui.QWidget):          """          Returns the password that appears in the widget -        :rtype: str +        :rtype: unicode          """          return self.ui.lnPassword.text() @@ -366,3 +369,41 @@ class LoginWidget(QtGui.QWidget):          self.ui.btnLogout.setText(self.tr("Logout"))          self.ui.btnLogout.setEnabled(True)          self.ui.clblErrorMsg.hide() + +    def load_user_from_keyring(self, saved_user): +        """ +        Tries to load a user from the keyring, returns True if it was +        loaded successfully, False otherwise. + +        :param saved_user: String containing the saved username as +                           user@domain +        :type saved_user: unicode + +        :rtype: bool +        """ +        leap_assert_type(saved_user, unicode) + +        try: +            username, domain = saved_user.split('@') +        except ValueError as e: +            # if the saved_user does not contain an '@' +            logger.error('Username@provider malformed. %r' % (e, )) +            return False + +        self.set_user(username) + +        self.set_remember(True) + +        saved_password = None +        try: +            saved_password = keyring.get_password(self.KEYRING_KEY, +                                                  saved_user +                                                  .encode("utf8")) +        except ValueError as e: +            logger.debug("Incorrect Password. %r." % (e,)) + +        if saved_password is not None: +            self.set_password(saved_password) +            return True + +        return False diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py index 83533666..c1e82d4d 100644 --- a/src/leap/bitmask/gui/mail_status.py +++ b/src/leap/bitmask/gui/mail_status.py @@ -50,6 +50,8 @@ class MailStatusWidget(QtGui.QWidget):          QtGui.QWidget.__init__(self, parent)          self._systray = None +        self._disabled = True +        self._started = False          self.ui = Ui_MailStatusWidget()          self.ui.setupUi(self) @@ -98,29 +100,16 @@ class MailStatusWidget(QtGui.QWidget):                   callback=self._mail_handle_soledad_events,                   reqcbk=lambda req, resp: None) -        register(signal=proto.SMTP_SERVICE_STARTED, -                 callback=self._mail_handle_smtp_events, -                 reqcbk=lambda req, resp: None) - -        register(signal=proto.SMTP_SERVICE_FAILED_TO_START, -                 callback=self._mail_handle_smtp_events, -                 reqcbk=lambda req, resp: None) - -        register(signal=proto.IMAP_SERVICE_STARTED, +        register(signal=proto.IMAP_UNREAD_MAIL,                   callback=self._mail_handle_imap_events,                   reqcbk=lambda req, resp: None) - -        register(signal=proto.IMAP_SERVICE_FAILED_TO_START, +        register(signal=proto.IMAP_SERVICE_STARTED,                   callback=self._mail_handle_imap_events,                   reqcbk=lambda req, resp: None) - -        register(signal=proto.IMAP_UNREAD_MAIL, +        register(signal=proto.SMTP_SERVICE_STARTED,                   callback=self._mail_handle_imap_events,                   reqcbk=lambda req, resp: None) -        self._smtp_started = False -        self._imap_started = False -          self._soledad_event.connect(              self._mail_handle_soledad_events_slot)          self._imap_event.connect( @@ -176,6 +165,9 @@ class MailStatusWidget(QtGui.QWidget):          """          # TODO: Figure out how to handle this with the two status in different          # classes +        # XXX right now we could connect the state transition signals of the +        # two connection machines (EIP/Mail) to a class that keeps track of the +        # state -- kali          # status = self.tr("Encrypted Internet: {0}").format(self._eip_status)          # status += '\n'          # status += self.tr("Mail is {0}").format(self._mx_status) @@ -292,11 +284,9 @@ class MailStatusWidget(QtGui.QWidget):          """          # We want to ignore this kind of events once everything has          # started -        if self._smtp_started and self._imap_started: +        if self._started:              return -        self._set_mail_status(self.tr("Starting..."), ready=1) -          ext_status = ""          if req.event == proto.KEYMANAGER_LOOKING_FOR_KEY: @@ -340,14 +330,9 @@ class MailStatusWidget(QtGui.QWidget):          ext_status = ""          if req.event == proto.SMTP_SERVICE_STARTED: -            ext_status = self.tr("SMTP has started...")              self._smtp_started = True -            if self._smtp_started and self._imap_started: -                self._set_mail_status(self.tr("ON"), ready=2) -                ext_status = ""          elif req.event == proto.SMTP_SERVICE_FAILED_TO_START:              ext_status = self.tr("SMTP failed to start, check the logs.") -            self._set_mail_status(self.tr("Failed"))          else:              leap_assert(False,                          "Don't know how to handle this state: %s" @@ -355,6 +340,8 @@ class MailStatusWidget(QtGui.QWidget):          self._set_mail_status(ext_status, ready=2) +    # ----- XXX deprecate (move to mail conductor) +      def _mail_handle_imap_events(self, req):          """          Callback for the IMAP events @@ -376,27 +363,15 @@ class MailStatusWidget(QtGui.QWidget):          """          ext_status = None -        if req.event == proto.IMAP_SERVICE_STARTED: -            ext_status = self.tr("IMAP has started...") -            self._imap_started = True -            if self._smtp_started and self._imap_started: -                self._set_mail_status(self.tr("ON"), ready=2) -                ext_status = "" -        elif req.event == proto.IMAP_SERVICE_FAILED_TO_START: -            ext_status = self.tr("IMAP failed to start, check the logs.") -            self._set_mail_status(self.tr("Failed")) -        elif req.event == proto.IMAP_UNREAD_MAIL: -            if self._smtp_started and self._imap_started: +        if req.event == proto.IMAP_UNREAD_MAIL: +            if self._started:                  if req.content != "0":                      self._set_mail_status(self.tr("%s Unread Emails") %                                            (req.content,), ready=2)                  else:                      self._set_mail_status("", ready=2) -        else: -            leap_assert(False,  # XXX ??? -                        "Don't know how to handle this state: %s" -                        % (req.event)) - +        elif req.event == proto.IMAP_SERVICE_STARTED: +            self._imap_started = True          if ext_status is not None:              self._set_mail_status(ext_status, ready=1) @@ -414,8 +389,50 @@ class MailStatusWidget(QtGui.QWidget):          """          self._set_mail_status(self.tr("Disabled"), -1) -    def stopped_mail(self): +    # statuses + +    # XXX make the signal emit the label and state. + +    @QtCore.Slot() +    def mail_state_disconnected(self): +        """ +        Displays the correct UI for the disconnected state. +        """ +        # XXX this should handle the disabled state better. +        self._started = False +        if self._disabled: +            self.mail_state_disabled() +        else: +            self._set_mail_status(self.tr("OFF"), -1) + +    @QtCore.Slot() +    def mail_state_connecting(self): +        """ +        Displays the correct UI for the connecting state. +        """ +        self._disabled = False +        self._started = True +        self._set_mail_status(self.tr("Starting..."), 1) + +    @QtCore.Slot() +    def mail_state_disconnecting(self): +        """ +        Displays the correct UI for the connecting state. +        """ +        self._set_mail_status(self.tr("Disconnecting..."), 1) + +    @QtCore.Slot() +    def mail_state_connected(self): +        """ +        Displays the correct UI for the connected state. +        """ +        self._set_mail_status(self.tr("ON"), 2) + +    @QtCore.Slot() +    def mail_state_disabled(self):          """ -        Displayes the correct UI for the stopped state. +        Displays the correct UI for the disabled state.          """ -        self._set_mail_status(self.tr("OFF")) +        self._disabled = True +        self._set_mail_status( +            self.tr("You must be logged in to use encrypted email."), -1) diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index f5631c69..5eb9e6dc 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -20,10 +20,9 @@ Main window for Bitmask.  import logging  import os -import keyring -  from PySide import QtCore, QtGui  from twisted.internet import threads +from zope.proxy import ProxyBase, setProxiedObject, sameProxiedObjects  from leap.bitmask import __version__ as VERSION  from leap.bitmask.config.leapsettings import LeapSettings @@ -39,18 +38,15 @@ from leap.bitmask.gui.mail_status import MailStatusWidget  from leap.bitmask.gui.wizard import Wizard  from leap.bitmask import provider -from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper -from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper -from leap.bitmask.services.eip import eipconfig -# XXX: Soledad might not work out of the box in Windows, issue #2932 -from leap.bitmask.services.soledad.soledadbootstrapper import \ -    SoledadBootstrapper -from leap.bitmask.services.mail.smtpbootstrapper import SMTPBootstrapper -from leap.bitmask.services.mail import imap  from leap.bitmask.platform_init import IS_WIN, IS_MAC  from leap.bitmask.platform_init.initializers import init_platform +from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper + +from leap.bitmask.services.mail import conductor as mail_conductor +from leap.bitmask.services.eip import eipconfig  from leap.bitmask.services.eip import get_openvpn_management +from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper  from leap.bitmask.services.eip.connection import EIPConnection  from leap.bitmask.services.eip.vpnprocess import VPN  from leap.bitmask.services.eip.vpnprocess import OpenVPNAlreadyRunning @@ -62,12 +58,12 @@ from leap.bitmask.services.eip.linuxvpnlauncher import EIPNoPkexecAvailable  from leap.bitmask.services.eip.linuxvpnlauncher import \      EIPNoPolkitAuthAgentAvailable  from leap.bitmask.services.eip.darwinvpnlauncher import EIPNoTunKextLoaded +from leap.bitmask.services.soledad.soledadbootstrapper import \ +    SoledadBootstrapper  from leap.bitmask.util.keyring_helpers import has_keyring  from leap.bitmask.util.leap_log_handler import LeapLogHandler -from leap.bitmask.services.mail.smtpconfig import SMTPConfig -  if IS_WIN:      from leap.bitmask.platform_init.locks import WindowsLock      from leap.bitmask.platform_init.locks import raise_window_ack @@ -90,13 +86,6 @@ class MainWindow(QtGui.QMainWindow):      LOGIN_INDEX = 0      EIP_STATUS_INDEX = 1 -    # Keyring -    KEYRING_KEY = "bitmask" - -    # SMTP -    PORT_KEY = "port" -    IP_KEY = "ip_address" -      OPENVPN_SERVICE = "openvpn"      MX_SERVICE = "mx" @@ -257,10 +246,6 @@ class MainWindow(QtGui.QMainWindow):          self._soledad_bootstrapper.soledad_failed.connect(              self._mail_status.set_soledad_failed) -        self._smtp_bootstrapper = SMTPBootstrapper() -        self._smtp_bootstrapper.download_config.connect( -            self._smtp_bootstrapped_stage) -          self.ui.action_about_leap.triggered.connect(self._about)          self.ui.action_quit.triggered.connect(self.quit)          self.ui.action_wizard.triggered.connect(self._launch_wizard) @@ -307,12 +292,13 @@ class MainWindow(QtGui.QMainWindow):          # Services signals/slots connection          self.new_updates.connect(self._react_to_new_updates) + +        # 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.soledad_ready.connect(self._set_soledad_ready)          self.mail_client_logged_in.connect(self._fetch_incoming_mail)          self.logout.connect(self._stop_imap_service)          self.logout.connect(self._stop_smtp_service) -        self.logout.connect(self._mail_status.stopped_mail)          ################################# end Qt Signals connection ######## @@ -325,17 +311,18 @@ class MainWindow(QtGui.QMainWindow):          self._bypass_checks = bypass_checks -        self._soledad = None -        self._soledad_ready = False -        self._keymanager = None -        self._smtp_service = None -        self._smtp_port = None -        self._imap_service = None +        # 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._login_defer = None          self._download_provider_defer = None -        self._smtp_config = SMTPConfig() +        self._mail_conductor = mail_conductor.MailConductor( +            self._soledad, self._keymanager) +        self._mail_conductor.connect_mail_signals(self._mail_status)          # Eip machine is a public attribute where the state machine for          # the eip connection will be available to the different components. @@ -347,6 +334,7 @@ class MainWindow(QtGui.QMainWindow):          self.eip_machine = None          # start event machines          self.start_eip_machine() +        self._mail_conductor.start_mail_machine(parent=self)          if self._first_run():              self._wizard_firstrun = True @@ -458,13 +446,11 @@ class MainWindow(QtGui.QMainWindow):          Displays the preferences window.          """ -        preferences_window = PreferencesWindow(self, self._srp_auth) +        preferences_window = PreferencesWindow(self, self._srp_auth, +                                               self._provider_config) -        if self._soledad_ready: -            preferences_window.set_soledad_ready(self._soledad) -        else: -            self.soledad_ready.connect( -                lambda: preferences_window.set_soledad_ready(self._soledad)) +        self.soledad_ready.connect( +            lambda: preferences_window.set_soledad_ready(self._soledad))          preferences_window.show() @@ -478,16 +464,6 @@ class MainWindow(QtGui.QMainWindow):          """          EIPPreferencesWindow(self).show() -    def _set_soledad_ready(self): -        """ -        SLOT -        TRIGGERS: -            self.soledad_ready - -        It sets the soledad object as ready to use. -        """ -        self._soledad_ready = True -      #      # updates      # @@ -562,6 +538,8 @@ class MainWindow(QtGui.QMainWindow):          if IS_MAC:              self.raise_() +        self._hide_unsupported_services() +          if self._wizard:              possible_username = self._wizard.get_username()              possible_password = self._wizard.get_password() @@ -594,32 +572,34 @@ class MainWindow(QtGui.QMainWindow):              saved_user = self._settings.get_user() -            try: -                username, domain = saved_user.split('@') -            except (ValueError, AttributeError) as e: -                # if the saved_user does not contain an '@' or its None -                logger.error('Username@provider malformed. %r' % (e, )) -                saved_user = None -              if saved_user is not None and has_keyring(): -                # fill the username -                self._login_widget.set_user(username) - -                self._login_widget.set_remember(True) - -                saved_password = None -                try: -                    saved_password = keyring.get_password(self.KEYRING_KEY, -                                                          saved_user -                                                          .encode("utf8")) -                except ValueError, e: -                    logger.debug("Incorrect Password. %r." % (e,)) - -                if saved_password is not None: -                    self._login_widget.set_password( -                        saved_password.decode("utf8")) +                if self._login_widget.load_user_from_keyring(saved_user):                      self._login() +    def _hide_unsupported_services(self): +        """ +        Given a set of configured providers, it creates a set of +        available services among all of them and displays the service +        widgets of only those. + +        This means, for example, that with just one provider with EIP +        only, the mail widget won't be displayed. +        """ +        providers = self._settings.get_configured_providers() + +        services = set() + +        for prov in providers: +            provider_config = ProviderConfig() +            loaded = provider_config.load( +                provider.get_provider_path(prov)) +            if loaded: +                for service in provider_config.get_services(): +                    services.add(service) + +        self.ui.eipWidget.setVisible(self.OPENVPN_SERVICE in services) +        self.ui.mailWidget.setVisible(self.MX_SERVICE in services) +      #      # systray      # @@ -803,6 +783,7 @@ class MainWindow(QtGui.QMainWindow):          provider configuration if it's not present, otherwise will          emit the corresponding signals inmediately          """ +        # XXX should rename this provider, name clash.          provider = self._login_widget.get_selected_provider()          pb = self._provider_bootstrapper @@ -823,6 +804,7 @@ class MainWindow(QtGui.QMainWindow):          :type data: dict          """          if data[self._provider_bootstrapper.PASSED_KEY]: +            # XXX should rename this provider, name clash.              provider = self._login_widget.get_selected_provider()              # If there's no loaded provider or @@ -895,8 +877,10 @@ class MainWindow(QtGui.QMainWindow):          leap_assert(self._provider_config, "We need a provider config!")          if data[self._provider_bootstrapper.PASSED_KEY]: -            username = self._login_widget.get_user().encode("utf8") -            password = self._login_widget.get_password().encode("utf8") +            username = self._login_widget.get_user() +            password = self._login_widget.get_password() + +            self._hide_unsupported_services()              if self._srp_auth is None:                  self._srp_auth = SRPAuth(self._provider_config) @@ -1023,114 +1007,58 @@ class MainWindow(QtGui.QMainWindow):              logger.debug("ERROR on soledad bootstrapping:")              logger.error("%r" % data[self._soledad_bootstrapper.ERROR_KEY])              return -        else: -            logger.debug("Done bootstrapping Soledad") -            self._soledad = self._soledad_bootstrapper.soledad -            self._keymanager = self._soledad_bootstrapper.keymanager +        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)          # Ok, now soledad is ready, so we can allow other things that          # depend on soledad to start.          # this will trigger start_imap_service +        # and start_smtp_boostrapping          self.soledad_ready.emit() -        # TODO connect all these activations to the soledad_ready -        # signal so the logic is clearer to follow. - -        if self._provider_config.provides_mx() and \ -                self._enabled_services.count(self.MX_SERVICE) > 0: -            self._smtp_bootstrapper.run_smtp_setup_checks( -                self._provider_config, -                self._smtp_config, -                True) -      ###################################################################      # Service control methods: smtp -    def _smtp_bootstrapped_stage(self, data): +    @QtCore.Slot() +    def _start_smtp_bootstrapping(self):          """          SLOT          TRIGGERS: -          self._smtp_bootstrapper.download_config - -        If there was a problem, displays it, otherwise it does nothing. -        This is used for intermediate bootstrapping stages, in case -        they fail. - -        :param data: result from the bootstrapping stage for Soledad -        :type data: dict -        """ -        passed = data[self._smtp_bootstrapper.PASSED_KEY] -        if not passed: -            logger.error(data[self._smtp_bootstrapper.ERROR_KEY]) -            return -        logger.debug("Done bootstrapping SMTP") -        self._check_smtp_config() - -    def _check_smtp_config(self): -        """ -        Checks smtp config and tries to download smtp client cert if needed. +            self.soledad_ready          """ -        hosts = self._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 = self._smtp_config.get_client_cert_path( +        # TODO for simmetry, this should be called start_smtp_service +        # (and delegate all the checks to the conductor) +        if self._provider_config.provides_mx() and \ +                self._enabled_services.count(self.MX_SERVICE) > 0: +            self._mail_conductor.smtp_bootstrapper.run_smtp_setup_checks(                  self._provider_config, -                about_to_download=True) - -            if not os.path.isfile(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") - -    def _start_smtp_service(self, host, port, cert): -        """ -        Starts the smtp service. -        """ -        # 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. - -        from leap.mail.smtp import setup_smtp_relay -        self._smtp_service, self._smtp_port = setup_smtp_relay( -            port=2013, -            keymanager=self._keymanager, -            smtp_host=host, -            smtp_port=port, -            smtp_cert=cert, -            smtp_key=cert, -            encrypted_only=False) +                self._mail_conductor.smtp_config, +                download_if_needed=True) +    # XXX --- should remove from here, and connecte directly to the state +    # machine. +    @QtCore.Slot()      def _stop_smtp_service(self):          """          SLOT          TRIGGERS:              self.logout          """ -        # There is a subtle difference here: -        # we are stopping the factory for the smtp service here, -        # but in the imap case we are just stopping the fetcher. -        if self._smtp_service is not None: -            logger.debug('Stopping smtp service.') -            self._smtp_port.stopListening() -            self._smtp_service.doStop() +        # TODO call stop_mail_service +        self._mail_conductor.stop_smtp_service()      ###################################################################      # Service control methods: imap +    @QtCore.Slot()      def _start_imap_service(self):          """          SLOT @@ -1139,11 +1067,7 @@ class MainWindow(QtGui.QMainWindow):          """          if self._provider_config.provides_mx() and \                  self._enabled_services.count(self.MX_SERVICE) > 0: -            logger.debug('Starting imap service') - -            self._imap_service = imap.start_imap_service( -                self._soledad, -                self._keymanager) +            self._mail_conductor.start_imap_service()      def _on_mail_client_logged_in(self, req):          """ @@ -1151,30 +1075,25 @@ class MainWindow(QtGui.QMainWindow):          """          self.mail_client_logged_in.emit() +    @QtCore.Slot()      def _fetch_incoming_mail(self):          """          SLOT          TRIGGERS:              self.mail_client_logged_in          """ -        # TODO have a mutex over fetch operation. -        if self._imap_service: -            logger.debug('Client connected, fetching mail...') -            self._imap_service.fetch() +        # TODO connect signal directly!!! +        self._mail_conductor.fetch_incoming_mail() +    @QtCore.Slot()      def _stop_imap_service(self):          """          SLOT          TRIGGERS:              self.logout          """ -        # There is a subtle difference here: -        # we are just stopping the fetcher here, -        # but in the smtp case we are stopping the factory. -        # We should homogenize both services. -        if self._imap_service is not None: -            logger.debug('Stopping imap service.') -            self._imap_service.stop() +        # TODO call stop_mail_service +        self._mail_conductor.stop_imap_service()      # end service control methods (imap) @@ -1623,8 +1542,8 @@ class MainWindow(QtGui.QMainWindow):          if ok:              self._logged_user = None -              self._login_widget.logged_out() +            self._mail_status.mail_state_disabled()          else:              self._login_widget.set_login_status( @@ -1700,8 +1619,7 @@ class MainWindow(QtGui.QMainWindow):          """          logger.debug('About to quit, doing cleanup...') -        if self._imap_service is not None: -            self._imap_service.stop() +        self._mail_conductor.stop_imap_service()          if self._srp_auth is not None:              if self._srp_auth.get_session_id() is not None or \ diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index 58cb05ba..acb39b07 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -40,12 +40,14 @@ class PreferencesWindow(QtGui.QDialog):      """      Window that displays the preferences.      """ -    def __init__(self, parent, srp_auth): +    def __init__(self, parent, srp_auth, provider_config):          """          :param parent: parent object of the PreferencesWindow.          :parent type: QWidget          :param srp_auth: SRPAuth object configured in the main app.          :type srp_auth: SRPAuth +        :param provider_config: ProviderConfig object. +        :type provider_config: ProviderConfig          """          QtGui.QDialog.__init__(self, parent)          self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") @@ -72,6 +74,36 @@ class PreferencesWindow(QtGui.QDialog):          else:              self._add_configured_providers() +        pw_enabled = False + +        # check if the user is logged in +        if srp_auth is not None and srp_auth.get_token() is not None: +            # check if provider has 'mx' ... +            domain = provider_config.get_domain() +            self._select_provider_by_name(domain) +            if provider_config.provides_mx(): +                enabled_services = self._settings.get_enabled_services(domain) +                mx_name = get_service_display_name('mx') + +                # ... and if the user have it enabled +                if 'mx' not in enabled_services: +                    msg = self.tr("You need to enable {0} in order to change " +                                  "the password.".format(mx_name)) +                    self._set_password_change_status(msg, error=True) +                else: +                    msg = self.tr( +                        "You need to wait until {0} is ready in " +                        "order to change the password.".format(mx_name)) +                    self._set_password_change_status(msg) +            else: +                pw_enabled = True +        else: +            msg = self.tr( +                "In order to change your password you need to be logged in.") +            self._set_password_change_status(msg) + +        self.ui.gbPasswordChange.setEnabled(pw_enabled) +      def set_soledad_ready(self, soledad):          """          SLOT @@ -84,6 +116,7 @@ class PreferencesWindow(QtGui.QDialog):          :type soledad: Soledad          """          self._soledad = soledad +        self.ui.lblPasswordChangeStatus.setVisible(False)          self.ui.gbPasswordChange.setEnabled(True)      def _set_password_change_status(self, status, error=False, success=False): @@ -98,6 +131,9 @@ class PreferencesWindow(QtGui.QDialog):          elif success:              status = "<font color='green'><b>%s</b></font>" % (status,) +        if not self.ui.gbPasswordChange.isEnabled(): +            status = "<font color='black'>%s</font>" % (status,) +          self.ui.lblPasswordChangeStatus.setVisible(True)          self.ui.lblPasswordChangeStatus.setText(status) @@ -156,7 +192,7 @@ class PreferencesWindow(QtGui.QDialog):          """          logger.debug("SRP password changed successfully.")          try: -            self._soledad.change_passphrase(str(new_password)) +            self._soledad.change_passphrase(new_password)              logger.debug("Soledad password changed successfully.")          except NoStorageSecret:              logger.debug( @@ -218,6 +254,16 @@ class PreferencesWindow(QtGui.QDialog):          for provider in self._settings.get_configured_providers():              self.ui.cbProvidersServices.addItem(provider) +    def _select_provider_by_name(self, name): +        """ +        Given a provider name/domain, selects it in the combobox. + +        :param name: name or domain for the provider +        :type name: str +        """ +        provider_index = self.ui.cbProvidersServices.findText(name) +        self.ui.cbProvidersServices.setCurrentIndex(provider_index) +      def _service_selection_changed(self, service, state):          """          SLOT diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py index 94726720..386cb75f 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -19,7 +19,8 @@ State machines for the Bitmask app.  """  import logging -from PySide.QtCore import QStateMachine, QState +from PySide import QtCore +from PySide.QtCore import QStateMachine, QState, Signal  from PySide.QtCore import QObject  from leap.bitmask.services import connections @@ -36,28 +37,255 @@ _CON = "connecting"  _DIS = "disconnecting" -class IntermediateState(QState): +class SignallingState(QState):      """ -    Intermediate state that emits a custom signal on entry +    A state that emits a custom signal on entry.      """ -    def __init__(self, signal): +    def __init__(self, signal, parent=None, name=None):          """          Initializer.          :param signal: the signal to be emitted on entry on this state.          :type signal: QtCore.QSignal          """ -        super(IntermediateState, self).__init__() +        super(SignallingState, self).__init__(parent)          self._signal = signal +        self._name = name      def onEntry(self, *args):          """          Emits the signal on entry.          """ -        logger.debug('IntermediateState entered. Emitting signal ...') +        logger.debug('State %s::%s entered. Emitting signal ...' +                     % (self._name, self.objectName()))          if self._signal is not None:              self._signal.emit() +class States(object): +    """ +    States for composite objects +    """ + +    class Off(SignallingState): +        pass + +    class Connecting(SignallingState): +        pass + +    class On(SignallingState): +        pass + +    class Disconnecting(SignallingState): +        pass + +    class StepsTrack(QObject): +        state_change = Signal() + +        def __init__(self, target): +            super(States.StepsTrack, self).__init__() +            self.received = set([]) +            self.target = set(target) + +        def is_all_done(self): +            return all([ev in self.target for ev in self.received]) + +        def is_any_done(self): +            return any([ev in self.target for ev in self.received]) + +        def seen(self, _type): +            if _type in self.target: +                self.received.add(_type) + +        def reset_seen(self): +            self.received = set([]) + +    class TransitionOR(QtCore.QSignalTransition): + +        def __init__(self, state): +            super(States.TransitionOR, self).__init__( +                state, QtCore.SIGNAL('state_change()')) +            self.state = state + +        def eventTest(self, e): +            self.state.seen(e.type()) +            done = self.state.is_any_done() +            if done: +                self.state.reset_seen() +            return done + +        def onTransition(self, e): +            pass + +    class TransitionAND(QtCore.QSignalTransition): + +        def __init__(self, state): +            super(States.TransitionAND, self).__init__( +                state, QtCore.SIGNAL('state_change()')) +            self.state = state + +        def eventTest(self, e): +            self.state.seen(e.type()) +            done = self.state.is_all_done() +            if done: +                self.state.reset_seen() +            return done + +        def onTransition(self, e): +            pass + + +class CompositeEvent(QtCore.QEvent): +    def __init__(self): +        super(CompositeEvent, self).__init__( +            QtCore.QEvent.Type(self.ID)) + + +class Composite(object): +    # TODO we should generate the connectingEvents dinamycally, +    # depending on how much composite states do we get. +    # This only supports up to 2 composite states. + +    class ConnectingEvent1(CompositeEvent): +        ID = QtCore.QEvent.User + 1 + +    class ConnectingEvent2(CompositeEvent): +        ID = QtCore.QEvent.User + 2 + +    class ConnectedEvent1(CompositeEvent): +        ID = QtCore.QEvent.User + 3 + +    class ConnectedEvent2(CompositeEvent): +        ID = QtCore.QEvent.User + 4 + +    class DisconnectingEvent1(CompositeEvent): +        ID = QtCore.QEvent.User + 5 + +    class DisconnectingEvent2(CompositeEvent): +        ID = QtCore.QEvent.User + 6 + +    class DisconnectedEvent1(CompositeEvent): +        ID = QtCore.QEvent.User + 7 + +    class DisconnectedEvent2(CompositeEvent): +        ID = QtCore.QEvent.User + 8 + + +class Events(QtCore.QObject): +    """ +    A Wrapper object for containing the events that will be +    posted to a composite state machine. +    """ +    def __init__(self, parent=None): +        """ +        Initializes the QObject with the given parent. +        """ +        QtCore.QObject.__init__(self, parent) + + +class CompositeMachine(QStateMachine): + +    def __init__(self, parent=None): +        QStateMachine.__init__(self, parent) + +        # events +        self.events = Events(parent) +        self.create_events() + +    def create_events(self): +        """ +        Creates a bunch of events to be posted to the state machine when +        the transitions say so. +        """ +        # XXX refactor into a dictionary? +        self.events.con_ev1 = Composite.ConnectingEvent1() +        self.events.con_ev2 = Composite.ConnectingEvent2() +        self.events.on_ev1 = Composite.ConnectedEvent1() +        self.events.on_ev2 = Composite.ConnectedEvent2() +        self.events.dis_ev1 = Composite.DisconnectingEvent1() +        self.events.dis_ev2 = Composite.DisconnectingEvent2() +        self.events.off_ev1 = Composite.DisconnectedEvent1() +        self.events.off_ev2 = Composite.DisconnectedEvent2() + +    def beginSelectTransitions(self, e): +        """ +        Weird. Having this method makes underlying backtraces +        to appear magically on the transitions. +        :param e: the received event +        :type e: QEvent +        """ +        pass + +    def _connect_children(self, child1, child2): +        """ +        Connects the state transition signals for children machines. + +        :param child1: the first child machine +        :type child1: QStateMachine +        :param child2: the second child machine +        :type child2: QStateMachine +        """ +        # TODO refactor and generalize for composites +        # of more than 2 connections. + +        c1 = child1.conn +        c1.qtsigs.connecting_signal.connect(self.con_ev1_slot) +        c1.qtsigs.connected_signal.connect(self.on_ev1_slot) +        c1.qtsigs.disconnecting_signal.connect(self.dis_ev1_slot) +        c1.qtsigs.disconnected_signal.connect(self.off_ev1_slot) + +        c2 = child2.conn +        c2.qtsigs.connecting_signal.connect(self.con_ev2_slot) +        c2.qtsigs.connected_signal.connect(self.on_ev2_slot) +        c2.qtsigs.disconnecting_signal.connect(self.dis_ev2_slot) +        c2.qtsigs.disconnected_signal.connect(self.off_ev2_slot) + +    # XXX why is this getting deletec in c++? +    #Traceback (most recent call last): +    #self.postEvent(self.events.on_ev2) +    #RuntimeError: Internal C++ object (ConnectedEvent2) already deleted. +    # XXX trying the following workaround, since +    # I cannot find why in the world this is getting deleted :( +    # XXX refactor! + +    # slots connection1 + +    def con_ev1_slot(self): +        # XXX if we just postEvent, we get the Internal C++ object deleted... +        # so the workaround is to re-create it each time. +        self.events.con_ev1 = Composite.ConnectingEvent1() +        self.postEvent(self.events.con_ev1) + +    def on_ev1_slot(self): +        self.events.on_ev1 = Composite.ConnectedEvent1() +        self.postEvent(self.events.on_ev1) + +    def dis_ev1_slot(self): +        self.events.dis_ev1 = Composite.DisconnectingEvent1() +        self.postEvent(self.events.dis_ev1) + +    def off_ev1_slot(self): +        self.events.off_ev1 = Composite.DisconnectedEvent1() +        self.postEvent(self.events.off_ev1) + +    # slots connection2 + +    def con_ev2_slot(self): +        self.events.con_ev2 = Composite.ConnectingEvent2() +        self.postEvent(self.events.con_ev2) + +    def on_ev2_slot(self): +        self.events.on_ev2 = Composite.ConnectedEvent2() +        self.postEvent(self.events.on_ev2) + +    def dis_ev2_slot(self): +        self.events.dis_ev2 = Composite.DisconnectingEvent2() +        self.postEvent(self.events.dis_ev2) + +    def off_ev2_slot(self): +        self.events.off_ev2 = Composite.DisconnectedEvent2() +        self.postEvent(self.events.off_ev2) + +  class ConnectionMachineBuilder(object):      """      Builder class for state machines made from LEAPConnections. @@ -65,16 +293,161 @@ class ConnectionMachineBuilder(object):      def __init__(self, connection):          """          :param connection: an instance of a concrete LEAPConnection -                      we will be building a state machine for. +                           we will be building a state machine for.          :type connection: AbstractLEAPConnection          """          self._conn = connection          leap_assert_type(self._conn, connections.AbstractLEAPConnection) -    def make_machine(self, button=None, action=None, label=None): +    def make_machine(self, **kwargs):          """          Creates a statemachine associated with the passed controls. +        It returns the state machine if the connection used for initializing +        the ConnectionMachineBuilder inherits exactly from +        LEAPAbstractConnection, and a tuple with the Composite Machine and its +        individual parts in case that it is a composite machine which +        connection definition inherits from more than one class that, on their +        time, inherit from LEAPAbstractConnection. + +        :params: see parameters for ``_make_simple_machine`` +        :returns: a QStateMachine, or a tuple with the form: +                  (CompositeStateMachine, (StateMachine1, StateMachine2)) +        :rtype: QStateMachine or tuple +        """ +        components = self._conn.components + +        if components is None: +        # simple case: connection definition inherits directly from +        # the abstract connection. + +            leap_assert_type(self._conn, connections.AbstractLEAPConnection) +            return self._make_simple_machine(self._conn, **kwargs) + +        if components: +            # composite case: connection definition inherits from several +            # classes, each one of which inherit from the abstract connection. +            child_machines = tuple( +                [ConnectionMachineBuilder(connection()).make_machine() +                    for connection in components]) +            composite_machine = self._make_composite_machine( +                self._conn, child_machines, **kwargs) + +            composite_machine._connect_children( +                *child_machines) + +            # XXX should also connect its own states with the signals +            # for the composite machine itself + +            return (composite_machine, child_machines) + +    def _make_composite_machine(self, conn, children, +                                **kwargs): +        """ +        Creates a composite machine. + +        :param conn: an instance of a connection definition. +        :type conn: LEAPAbstractConnection +        :param children: children machines +        :type children: tuple of state machines +        :returns: A composite state machine +        :rtype: QStateMachine +        """ +        # TODO split this method in smaller utility functions. +        parent = kwargs.get('parent', None) + +        # 1. create machine +        machine = CompositeMachine(parent=parent) + +        # 2. create states +        off = States.Off(conn.qtsigs.disconnected_signal, +                         parent=machine, +                         name=conn.name) +        off.setObjectName("off") + +        on = States.On(conn.qtsigs.connected_signal, +                       parent=machine, +                       name=conn.name) +        on.setObjectName("on") + +        connecting_state = States.Connecting( +            conn.qtsigs.connecting_signal, +            parent=machine, +            name=conn.name) +        connecting_state.setObjectName("connecting") + +        disconnecting_state = States.Disconnecting( +            conn.qtsigs.disconnecting_signal, +            parent=machine, +            name=conn.name) +        disconnecting_state.setObjectName("disconnecting") + +        # 3. TODO create as many connectingEvents as needed (dynamically create +        # classses for that) +        # (we have manually created classes for events under CompositeEvent for +        # now, to begin with the simple 2 states case for mail. + +        # 4. state tracking objects for each transition stage + +        connecting_track0 = States.StepsTrack( +            (Composite.ConnectingEvent1.ID, +             Composite.ConnectingEvent2.ID)) +        connecting_track0.setObjectName("connecting_step_0") + +        connecting_track1 = States.StepsTrack( +            (Composite.ConnectedEvent1.ID, +             Composite.ConnectedEvent2.ID)) +        connecting_track1.setObjectName("connecting_step_1") + +        disconnecting_track0 = States.StepsTrack( +            (Composite.DisconnectingEvent1.ID, +             Composite.DisconnectingEvent2.ID)) +        disconnecting_track0.setObjectName("disconnecting_step_0") + +        disconnecting_track1 = States.StepsTrack( +            (Composite.DisconnectedEvent1.ID, +             Composite.DisconnectedEvent2.ID)) +        disconnecting_track1.setObjectName("disconnecting_step_1") + +        # 5. definte the transitions with the matching state-tracking +        # objects. + +        # off -> connecting +        connecting_transition = States.TransitionOR( +            connecting_track0) +        connecting_transition.setTargetState(connecting_state) +        off.addTransition(connecting_transition) + +        # connecting -> on +        connected_transition = States.TransitionAND( +            connecting_track1) +        connected_transition.setTargetState(on) +        connecting_state.addTransition(connected_transition) + +        # on -> disconnecting +        disconnecting_transition = States.TransitionOR( +            disconnecting_track0) +        disconnecting_transition.setTargetState(disconnecting_state) +        on.addTransition(disconnecting_transition) + +        # disconnecting -> off +        disconnected_transition = States.TransitionAND( +            disconnecting_track1) +        disconnected_transition.setTargetState(off) +        disconnecting_state.addTransition(disconnected_transition) + +        machine.setInitialState(off) +        machine.conn = conn +        return machine + +    def _make_simple_machine(self, conn, +                             button=None, action=None, label=None): +        """ +        Creates a statemachine associated with the passed controls. + +        :param conn: the connection instance that defines this machine. +        :type conn: AbstractLEAPConnection +          :param button: the switch button.          :type button: QPushButton @@ -88,9 +461,7 @@ class ConnectionMachineBuilder(object):          :rtype: QStateMachine          """          machine = QStateMachine() -        conn = self._conn - -        states = self._make_states(button, action, label) +        states = self._make_states(conn, button, action, label)          # transitions: @@ -151,11 +522,17 @@ class ConnectionMachineBuilder(object):          for state in states.itervalues():              machine.addState(state)          machine.setInitialState(states[_OFF]) + +        machine.conn = conn          return machine -    def _make_states(self, button, action, label): +    def _make_states(self, conn, button, action, label):          """ -        Creates the four states for the state machine +        Creates the four states for the simple state machine. +        Adds the needed properties for the passed controls. + +        :param conn: the connection instance that defines this machine. +        :type conn: AbstractLEAPConnection          :param button: the switch button.          :type button: QPushButton @@ -169,7 +546,6 @@ class ConnectionMachineBuilder(object):          :returns: a dict of states          :rtype: dict          """ -        conn = self._conn          states = {}          # TODO add tooltip @@ -190,8 +566,9 @@ class ConnectionMachineBuilder(object):          states[_OFF] = off          # CONNECTING State ---------------- -        connecting = IntermediateState( -            conn.qtsigs.connecting_signal) +        connecting = SignallingState( +            conn.qtsigs.connecting_signal, +            name=conn.name)          on_label = _tr("Turn {0}").format(              conn.Disconnected.short_label)          if button: @@ -224,8 +601,9 @@ class ConnectionMachineBuilder(object):          states[_ON] = on          # DISCONNECTING State ------------- -        disconnecting = IntermediateState( -            conn.qtsigs.disconnecting_signal) +        disconnecting = SignallingState( +            conn.qtsigs.disconnecting_signal, +            name=conn.name)          if button:              disconnecting.assignProperty(                  button, 'enabled', False) diff --git a/src/leap/bitmask/gui/ui/eip_status.ui b/src/leap/bitmask/gui/ui/eip_status.ui index 25831118..d078ca0c 100644 --- a/src/leap/bitmask/gui/ui/eip_status.ui +++ b/src/leap/bitmask/gui/ui/eip_status.ui @@ -185,6 +185,9 @@              <property name="cursor">               <cursorShape>PointingHandCursor</cursorShape>              </property> +            <property name="styleSheet"> +             <string notr="true">text-align: left;</string> +            </property>              <property name="text">               <string>0.0 KB/s</string>              </property> @@ -249,6 +252,9 @@              <property name="cursor">               <cursorShape>PointingHandCursor</cursorShape>              </property> +            <property name="styleSheet"> +             <string notr="true">text-align: left;</string> +            </property>              <property name="text">               <string>0.0 KB/s</string>              </property> diff --git a/src/leap/bitmask/gui/ui/eippreferences.ui b/src/leap/bitmask/gui/ui/eippreferences.ui index cc77c82e..a3050683 100644 --- a/src/leap/bitmask/gui/ui/eippreferences.ui +++ b/src/leap/bitmask/gui/ui/eippreferences.ui @@ -7,7 +7,7 @@      <x>0</x>      <y>0</y>      <width>435</width> -    <height>273</height> +    <height>144</height>     </rect>    </property>    <property name="windowTitle"> @@ -18,7 +18,7 @@      <normaloff>:/images/mask-icon.png</normaloff>:/images/mask-icon.png</iconset>    </property>    <layout class="QGridLayout" name="gridLayout_2"> -   <item row="1" column="0"> +   <item row="0" column="0">      <widget class="QGroupBox" name="gbGatewaySelector">       <property name="enabled">        <bool>true</bool> @@ -33,7 +33,7 @@        <item row="0" column="0">         <widget class="QLabel" name="lblSelectProvider">          <property name="text"> -         <string>&Select provider:</string> +         <string>Select &provider:</string>          </property>          <property name="buddy">           <cstring>cbProvidersGateway</cstring> @@ -52,7 +52,7 @@        <item row="7" column="2">         <widget class="QPushButton" name="pbSaveGateway">          <property name="text"> -         <string>Save this provider settings</string> +         <string>&Save this provider settings</string>          </property>         </widget>        </item> @@ -69,7 +69,10 @@        <item row="1" column="0">         <widget class="QLabel" name="label">          <property name="text"> -         <string>Select gateway:</string> +         <string>Select &gateway:</string> +        </property> +        <property name="buddy"> +         <cstring>cbGateways</cstring>          </property>         </widget>        </item> @@ -85,76 +88,9 @@       </layout>      </widget>     </item> -   <item row="0" column="0"> -    <widget class="QGroupBox" name="gbAutomaticEIP"> -     <property name="title"> -      <string>Automatic Encrypted Internet start</string> -     </property> -     <layout class="QGridLayout" name="gridLayout_3"> -      <item row="3" column="0"> -       <widget class="QLabel" name="lblAutoStartEIPStatus"> -        <property name="layoutDirection"> -         <enum>Qt::LeftToRight</enum> -        </property> -        <property name="styleSheet"> -         <string notr="true"/> -        </property> -        <property name="frameShape"> -         <enum>QFrame::NoFrame</enum> -        </property> -        <property name="frameShadow"> -         <enum>QFrame::Plain</enum> -        </property> -        <property name="text"> -         <string><font color='green'><b>Automatic EIP start saved!</b></font></string> -        </property> -        <property name="alignment"> -         <set>Qt::AlignCenter</set> -        </property> -       </widget> -      </item> -      <item row="3" column="1"> -       <widget class="QPushButton" name="pbSaveAutoStartEIP"> -        <property name="text"> -         <string>Save auto start setting</string> -        </property> -       </widget> -      </item> -      <item row="0" column="0"> -       <widget class="QCheckBox" name="cbAutoStartEIP"> -        <property name="layoutDirection"> -         <enum>Qt::LeftToRight</enum> -        </property> -        <property name="text"> -         <string>Enable Automatic start:</string> -        </property> -        <property name="checked"> -         <bool>true</bool> -        </property> -       </widget> -      </item> -      <item row="0" column="1"> -       <widget class="QComboBox" name="cbProvidersEIP"> -        <item> -         <property name="text"> -          <string><Select provider></string> -         </property> -        </item> -       </widget> -      </item> -     </layout> -     <zorder>cbAutoStartEIP</zorder> -     <zorder>lblAutoStartEIPStatus</zorder> -     <zorder>pbSaveAutoStartEIP</zorder> -     <zorder>cbProvidersEIP</zorder> -    </widget> -   </item>    </layout>   </widget>   <tabstops> -  <tabstop>cbAutoStartEIP</tabstop> -  <tabstop>cbProvidersEIP</tabstop> -  <tabstop>pbSaveAutoStartEIP</tabstop>    <tabstop>cbProvidersGateway</tabstop>    <tabstop>cbGateways</tabstop>    <tabstop>pbSaveGateway</tabstop> @@ -162,22 +98,5 @@   <resources>    <include location="../../../../../data/resources/mainwindow.qrc"/>   </resources> - <connections> -  <connection> -   <sender>cbAutoStartEIP</sender> -   <signal>toggled(bool)</signal> -   <receiver>cbProvidersEIP</receiver> -   <slot>setEnabled(bool)</slot> -   <hints> -    <hint type="sourcelabel"> -     <x>180</x> -     <y>53</y> -    </hint> -    <hint type="destinationlabel"> -     <x>238</x> -     <y>53</y> -    </hint> -   </hints> -  </connection> - </connections> + <connections/>  </ui> diff --git a/src/leap/bitmask/gui/ui/mainwindow.ui b/src/leap/bitmask/gui/ui/mainwindow.ui index 10c77057..badd291d 100644 --- a/src/leap/bitmask/gui/ui/mainwindow.ui +++ b/src/leap/bitmask/gui/ui/mainwindow.ui @@ -75,7 +75,7 @@           <x>0</x>           <y>0</y>           <width>524</width> -         <height>635</height> +         <height>636</height>          </rect>         </property>         <layout class="QVBoxLayout" name="verticalLayout"> @@ -86,89 +86,101 @@           <number>0</number>          </property>          <item> -         <widget class="QFrame" name="frame_2"> -          <property name="sizePolicy"> -           <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> -            <horstretch>0</horstretch> -            <verstretch>0</verstretch> -           </sizepolicy> -          </property> -          <property name="styleSheet"> -           <string notr="true">QFrame{background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(160, 160, 160, 128), stop:1 rgba(255, 255, 255, 0));}</string> -          </property> -          <layout class="QHBoxLayout" name="horizontalLayout_3"> -           <property name="leftMargin"> -            <number>24</number> +         <widget class="QWidget" name="eipWidget" native="true"> +          <layout class="QVBoxLayout" name="verticalLayout_2"> +           <property name="spacing"> +            <number>0</number>             </property> -           <property name="rightMargin"> -            <number>24</number> +           <property name="margin"> +            <number>0</number>             </property>             <item> -            <widget class="QLabel" name="label_2"> -             <property name="font"> -              <font> -               <pointsize>16</pointsize> -               <weight>75</weight> -               <bold>true</bold> -              </font> +            <widget class="QFrame" name="frame_2"> +             <property name="sizePolicy"> +              <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> +               <horstretch>0</horstretch> +               <verstretch>0</verstretch> +              </sizepolicy>               </property>               <property name="styleSheet"> -              <string notr="true">background-color: rgba(255, 255, 255, 0);</string> -             </property> -             <property name="text"> -              <string>Encrypted Internet</string> +              <string notr="true">QFrame{background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(160, 160, 160, 128), stop:1 rgba(255, 255, 255, 0));}</string>               </property> +             <layout class="QHBoxLayout" name="horizontalLayout_3"> +              <property name="leftMargin"> +               <number>24</number> +              </property> +              <property name="rightMargin"> +               <number>24</number> +              </property> +              <item> +               <widget class="QLabel" name="label_2"> +                <property name="font"> +                 <font> +                  <pointsize>16</pointsize> +                  <weight>75</weight> +                  <bold>true</bold> +                 </font> +                </property> +                <property name="styleSheet"> +                 <string notr="true">background-color: rgba(255, 255, 255, 0);</string> +                </property> +                <property name="text"> +                 <string>Encrypted Internet</string> +                </property> +               </widget> +              </item> +              <item> +               <widget class="QPushButton" name="btnEIPPreferences"> +                <property name="maximumSize"> +                 <size> +                  <width>48</width> +                  <height>20</height> +                 </size> +                </property> +                <property name="styleSheet"> +                 <string notr="true"/> +                </property> +                <property name="text"> +                 <string/> +                </property> +                <property name="icon"> +                 <iconset resource="../../../../../data/resources/mainwindow.qrc"> +                  <normaloff>:/images/black/32/gear.png</normaloff>:/images/black/32/gear.png</iconset> +                </property> +                <property name="autoDefault"> +                 <bool>false</bool> +                </property> +                <property name="default"> +                 <bool>false</bool> +                </property> +                <property name="flat"> +                 <bool>false</bool> +                </property> +               </widget> +              </item> +             </layout>              </widget>             </item>             <item> -            <widget class="QPushButton" name="btnEIPPreferences"> -             <property name="maximumSize"> -              <size> -               <width>48</width> -               <height>20</height> -              </size> +            <layout class="QVBoxLayout" name="eipLayout"> +             <property name="leftMargin"> +              <number>12</number>               </property> -             <property name="styleSheet"> -              <string notr="true"/> +             <property name="topMargin"> +              <number>0</number>               </property> -             <property name="text"> -              <string/> +             <property name="rightMargin"> +              <number>12</number>               </property> -             <property name="icon"> -              <iconset resource="../../../../../data/resources/mainwindow.qrc"> -               <normaloff>:/images/black/32/gear.png</normaloff>:/images/black/32/gear.png</iconset> -             </property> -             <property name="autoDefault"> -              <bool>false</bool> +             <property name="bottomMargin"> +              <number>0</number>               </property> -             <property name="default"> -              <bool>false</bool> -             </property> -             <property name="flat"> -              <bool>false</bool> -             </property> -            </widget> +            </layout>             </item>            </layout>           </widget>          </item>          <item> -         <layout class="QVBoxLayout" name="eipLayout"> -          <property name="leftMargin"> -           <number>12</number> -          </property> -          <property name="topMargin"> -           <number>0</number> -          </property> -          <property name="rightMargin"> -           <number>12</number> -          </property> -          <property name="bottomMargin"> -           <number>0</number> -          </property> -         </layout> -        </item> -        <item>           <widget class="Line" name="line">            <property name="orientation">             <enum>Qt::Horizontal</enum> @@ -253,14 +265,26 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb           </widget>          </item>          <item> -         <layout class="QVBoxLayout" name="mailLayout" stretch=""> -          <property name="spacing"> -           <number>-1</number> -          </property> -          <property name="margin"> -           <number>12</number> -          </property> -         </layout> +         <widget class="QWidget" name="mailWidget" native="true"> +          <layout class="QVBoxLayout" name="verticalLayout_3"> +           <property name="spacing"> +            <number>0</number> +           </property> +           <property name="margin"> +            <number>0</number> +           </property> +           <item> +            <layout class="QVBoxLayout" name="mailLayout"> +             <property name="spacing"> +              <number>-1</number> +             </property> +             <property name="margin"> +              <number>12</number> +             </property> +            </layout> +           </item> +          </layout> +         </widget>          </item>          <item>           <spacer name="verticalSpacer"> diff --git a/src/leap/bitmask/provider/providerbootstrapper.py b/src/leap/bitmask/provider/providerbootstrapper.py index 1b5947e1..2adf8aa8 100644 --- a/src/leap/bitmask/provider/providerbootstrapper.py +++ b/src/leap/bitmask/provider/providerbootstrapper.py @@ -20,6 +20,7 @@ Provider bootstrapping  import logging  import socket  import os +import sys  import requests @@ -125,9 +126,13 @@ class ProviderBootstrapper(AbstractBootstrapper):          # err --- but we can do it after a failure, to diagnose what went          # wrong. Right now we're just adding connection overhead. -- kali +        verify = self.verify +        if verify: +            verify = self.verify.encode(sys.getfilesystemencoding()) +          try:              res = self._session.get("https://%s" % (self._domain,), -                                    verify=self.verify, +                                    verify=verify,                                      timeout=REQUEST_TIMEOUT)              res.raise_for_status()          except requests.exceptions.SSLError as exc: @@ -180,6 +185,8 @@ class ProviderBootstrapper(AbstractBootstrapper):                  # no ca? then download from main domain again.                  pass +        if verify: +            verify = verify.encode(sys.getfilesystemencoding())          logger.debug("Requesting for provider.json... "                       "uri: {0}, verify: {1}, headers: {2}".format(                           uri, verify, headers)) @@ -336,9 +343,9 @@ class ProviderBootstrapper(AbstractBootstrapper):          test_uri = "%s/%s/cert" % (self._provider_config.get_api_uri(),                                     self._provider_config.get_api_version()) -        res = self._session.get(test_uri, -                                verify=self._provider_config -                                .get_ca_cert_path(), +        ca_cert_path = self._provider_config.get_ca_cert_path() +        ca_cert_path = ca_cert_path.encode(sys.getfilesystemencoding()) +        res = self._session.get(test_uri, verify=ca_cert_path,                                  timeout=REQUEST_TIMEOUT)          res.raise_for_status() diff --git a/src/leap/bitmask/services/__init__.py b/src/leap/bitmask/services/__init__.py index f9456159..e62277b6 100644 --- a/src/leap/bitmask/services/__init__.py +++ b/src/leap/bitmask/services/__init__.py @@ -19,6 +19,7 @@ Services module.  """  import logging  import os +import sys  from PySide import QtCore @@ -135,8 +136,12 @@ def download_service_config(provider_config, service_config,      if token is not None:          headers["Authorization"] = 'Token token="{0}"'.format(token) +    verify = provider_config.get_ca_cert_path() +    if verify: +        verify = verify.encode(sys.getfilesystemencoding()) +      res = session.get(config_uri, -                      verify=provider_config.get_ca_cert_path(), +                      verify=verify,                        headers=headers,                        timeout=REQUEST_TIMEOUT,                        cookies=cookies) diff --git a/src/leap/bitmask/services/connections.py b/src/leap/bitmask/services/connections.py index 8aeb4e0c..ecfd35ff 100644 --- a/src/leap/bitmask/services/connections.py +++ b/src/leap/bitmask/services/connections.py @@ -41,9 +41,6 @@ The different services should declare a ServiceConnection class that  inherits from AbstractLEAPConnection, so an instance of such class  can be used to inform the StateMachineBuilder of the particularities  of the state transitions for each particular connection. - -In the future, we will extend this class to allow composites in connections, -so we can apply conditional logic to the transitions.  """ @@ -79,12 +76,7 @@ class AbstractLEAPConnection(object):          """          return self._qtsigs -    # XXX for conditional transitions with composites, -    #     we might want to add -    #     a field with dependencies: what this connection -    #     needs for (ON) state. -    # XXX Look also at child states in the state machine. -    #depends = () +    components = None      # Signals that derived classes      # have to implement. diff --git a/src/leap/bitmask/services/eip/connection.py b/src/leap/bitmask/services/eip/connection.py index 962d9cf2..8a35d550 100644 --- a/src/leap/bitmask/services/eip/connection.py +++ b/src/leap/bitmask/services/eip/connection.py @@ -46,5 +46,5 @@ class EIPConnectionSignals(QtCore.QObject):  class EIPConnection(AbstractLEAPConnection):      def __init__(self): -        # XXX this should be public instead          self._qtsigs = EIPConnectionSignals() +        self._connection_name = "Encrypted Internet" diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py index f3b6bfc8..fe3fe4c1 100644 --- a/src/leap/bitmask/services/eip/darwinvpnlauncher.py +++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py @@ -21,6 +21,7 @@ import commands  import getpass  import logging  import os +import sys  from leap.bitmask.services.eip.vpnlauncher import VPNLauncher  from leap.bitmask.services.eip.vpnlauncher import VPNLauncherException @@ -185,6 +186,8 @@ class DarwinVPNLauncher(VPNLauncher):          :rtype: dict          """ +        ld_library_path = os.path.join(get_path_prefix(), "..", "lib") +        ld_library_path.encode(sys.getfilesystemencoding())          return { -            "DYLD_LIBRARY_PATH": os.path.join(get_path_prefix(), "..", "lib") +            "DYLD_LIBRARY_PATH": ld_library_path          } diff --git a/src/leap/bitmask/services/eip/linuxvpnlauncher.py b/src/leap/bitmask/services/eip/linuxvpnlauncher.py index efb23285..d02f6f96 100644 --- a/src/leap/bitmask/services/eip/linuxvpnlauncher.py +++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py @@ -21,6 +21,7 @@ import commands  import logging  import os  import subprocess +import sys  import time  from leap.bitmask.config import flags @@ -231,6 +232,8 @@ class LinuxVPNLauncher(VPNLauncher):          :rtype: dict          """ +        ld_library_path = os.path.join(get_path_prefix(), "..", "lib") +        ld_library_path.encode(sys.getfilesystemencoding())          return { -            "LD_LIBRARY_PATH": os.path.join(get_path_prefix(), "..", "lib") +            "LD_LIBRARY_PATH": ld_library_path          } diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py index bce3599b..07497814 100644 --- a/src/leap/bitmask/services/eip/vpnlauncher.py +++ b/src/leap/bitmask/services/eip/vpnlauncher.py @@ -250,9 +250,6 @@ class VPNLauncher(object):              '--ping-restart', '30']          command_and_args = [openvpn] + args -        logger.debug("Running VPN with command:") -        logger.debug(" ".join(command_and_args)) -          return command_and_args      @classmethod diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index 19e1aa7b..51f0f738 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -23,6 +23,7 @@ import psutil  import psutil.error  import shutil  import socket +import sys  from itertools import chain, repeat @@ -864,15 +865,26 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):          """          Gets the vpn command from the aproppriate launcher. -        Might throw: VPNLauncherException, OpenVPNNotFoundException. +        Might throw: +            VPNLauncherException, +            OpenVPNNotFoundException. + +        :rtype: list of str          """ -        cmd = self._launcher.get_vpn_command( +        command = self._launcher.get_vpn_command(              eipconfig=self._eipconfig,              providerconfig=self._providerconfig,              socket_host=self._socket_host,              socket_port=self._socket_port,              openvpn_verb=self._openvpn_verb) -        return map(str, cmd) + +        encoding = sys.getfilesystemencoding() +        for i, c in enumerate(command): +            if not isinstance(c, str): +                command[i] = c.encode(encoding) + +        logger.debug("Running VPN with command: {0}".format(command)) +        return command      # shutdown diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py new file mode 100644 index 00000000..c294381b --- /dev/null +++ b/src/leap/bitmask/services/mail/conductor.py @@ -0,0 +1,384 @@ +# -*- coding: utf-8 -*- +# conductor.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/>. +""" +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.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 + +logger = logging.getLogger(__name__) + + +class IMAPControl(object): +    """ +    Methods related to IMAP control. +    """ +    def __init__(self): +        """ +        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, +                      callback=self._handle_imap_events, +                      reqcbk=lambda req, resp: None) +        leap_register(signal=leap_events.IMAP_SERVICE_FAILED_TO_START, +                      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. + +        :param imap_connection: an initialized imap connection +        :type imap_connection: IMAPConnection instance. +        """ +        self.imap_connection = imap_connection + +    def start_imap_service(self): +        """ +        Starts imap service. +        """ +        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") + +        if self.imap_service is None: +            # first time. +            self.imap_service, \ +            self.imap_port, \ +            self.imap_factory = imap.start_imap_service( +                self._soledad, +                self._keymanager) +        else: +            # we have the fetcher. just start it. +            self.imap_service.start_loop() + +    def stop_imap_service(self): +        """ +        Stops imap service (fetcher, factory and port). +        """ +        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() +            # Stop listening on the IMAP port +            self.imap_port.stopListening() +            # Stop the protocol +            self.imap_factory.doStop() + +    def fetch_incoming_mail(self): +        """ +        Fetches incoming mail. +        """ +        # TODO have a mutex over fetch operation. +        if self.imap_service: +            logger.debug('Client connected, fetching mail...') +            self.imap_service.fetch() + +    # handle events + +    def _handle_imap_events(self, req): +        """ +        Callback handler for the IMAP events + +        :param req: Request type +        :type req: leap.common.events.events_pb2.SignalRequest +        """ +        if req.event == leap_events.IMAP_SERVICE_STARTED: +            self.on_imap_connected() +        elif req.event == leap_events.IMAP_SERVICE_FAILED_TO_START: +            self.on_imap_failed() + +    # emit connection signals + +    def on_imap_connecting(self): +        """ +        Callback for IMAP connecting state. +        """ +        self.imap_connection.qtsigs.connecting_signal.emit() + +    def on_imap_connected(self): +        """ +        Callback for IMAP connected state. +        """ +        self.imap_connection.qtsigs.connected_signal.emit() + +    def on_imap_failed(self): +        """ +        Callback for IMAP failed state. +        """ +        self.imap_connection.qtsigs.connetion_aborted_signal.emit() + + +class SMTPControl(object): + +    PORT_KEY = "port" +    IP_KEY = "ip_address" + +    def __init__(self): +        """ +        Initializes smtp variables. +        """ +        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, +                      reqcbk=lambda req, resp: None) +        leap_register(signal=leap_events.SMTP_SERVICE_FAILED_TO_START, +                      callback=self._handle_smtp_events, +                      reqcbk=lambda req, resp: None) + +    def set_smtp_connection(self, smtp_connection): +        """ +        Sets the smtp connection to an initialized connection. +        :param smtp_connection: an initialized smtp connection +        :type smtp_connection: SMTPConnection instance. +        """ +        self.smtp_connection = smtp_connection + +    def start_smtp_service(self, host, port, cert): +        """ +        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 +        """ +        # 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_relay +        self._smtp_service, self._smtp_port = setup_smtp_relay( +            port=2013, +            keymanager=self._keymanager, +            smtp_host=host, +            smtp_port=port, +            smtp_cert=cert, +            smtp_key=cert, +            encrypted_only=False) + +    def stop_smtp_service(self): +        """ +        Stops the smtp service (port and factory). +        """ +        self.smtp_connection.qtsigs.disconnecting_signal.emit() +        # TODO We should homogenize both services. +        if self._smtp_service is not None: +            logger.debug('Stopping smtp service.') +            self._smtp_port.stopListening() +            self._smtp_service.doStop() + +    @QtCore.Slot() +    def smtp_bootstrapped_stage(self, data): +        """ +        SLOT +        TRIGGERS: +          self.smtp_bootstrapper.download_config + +        If there was a problem, displays it, otherwise it does nothing. +        This is used for intermediate bootstrapping stages, in case +        they fail. + +        :param data: result from the bootstrapping stage for Soledad +        :type data: dict +        """ +        passed = data[self.smtp_bootstrapper.PASSED_KEY] +        if not passed: +            logger.error(data[self.smtp_bootstrapper.ERROR_KEY]) +            return +        logger.debug("Done bootstrapping SMTP") +        self.check_smtp_config() + +    def check_smtp_config(self): +        """ +        Checks smtp config and tries to download smtp client cert if needed. +        Currently called when smtp_bootstrapped_stage has successfuly finished. +        """ +        logger.debug("Checking SMTP config...") +        leap_assert(self.smtp_bootstrapper._provider_config, +                    "smtp bootstrapper does not have a provider_config") + +        provider_config = self.smtp_bootstrapper._provider_config +        smtp_config = self.smtp_config +        hosts = smtp_config.get_hosts() +        # TODO handle more than one host and define how to choose +        if len(hosts) > 0: +            hostname = hosts.keys()[0] +            logger.debug("Using hostname %s for SMTP" % (hostname,)) +            host = hosts[hostname][self.IP_KEY].encode("utf-8") +            port = hosts[hostname][self.PORT_KEY] + +            client_cert = smtp_config.get_client_cert_path( +                provider_config, +                about_to_download=True) + +            # XXX change this logic! +            # check_config should be called from within start_service, +            # and not the other way around. +            if not is_file(client_cert): +                self.smtp_bootstrapper._download_client_certificates() +            if os.path.isfile(client_cert): +                self.start_smtp_service(host, port, client_cert) +            else: +                logger.warning("Tried to download email client " +                               "certificate, but could not find any") + +        else: +            logger.warning("No smtp hosts configured") + +    # handle smtp events + +    def _handle_smtp_events(self, req): +        """ +        Callback handler for the SMTP events. + +        :param req: Request type +        :type req: leap.common.events.events_pb2.SignalRequest +        """ +        if req.event == leap_events.SMTP_SERVICE_STARTED: +            self.on_smtp_connected() +        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. +        """ +        self.smtp_connection.qtsigs.connecting_signal.emit() + +    def on_smtp_connected(self): +        """ +        Callback for SMTP connected state. +        """ +        self.smtp_connection.qtsigs.connected_signal.emit() + +    def on_smtp_failed(self): +        """ +        Callback for SMTP failed state. +        """ +        self.smtp_connection.qtsigs.connection_aborted_signal.emit() + + +class MailConductor(IMAPControl, SMTPControl): +    """ +    This class encapsulates everything related to the initialization and +    process control for the mail services. +    Currently, it initializes IMAPConnection and SMPTConnection. +    """ +    # XXX We could consider to use composition instead of inheritance here. + +    def __init__(self, soledad, keymanager): +        """ +        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 soledad: zope.proxy.ProxyBase +        """ +        IMAPControl.__init__(self) +        SMTPControl.__init__(self) +        self._soledad = soledad +        self._keymanager = keymanager + +        self._mail_machine = None + +        self._mail_connection = mail_connection.MailConnection() + +    def start_mail_machine(self, **kwargs): +        """ +        Starts mail machine. +        """ +        logger.debug("Starting mail state machine...") +        builder = statemachines.ConnectionMachineBuilder(self._mail_connection) +        (mail, (imap, smtp)) = builder.make_machine(**kwargs) + +        # we have instantiated the connections while building the composite +        # machines, and we have to use the qtsigs instantiated there. +        # XXX we could probably use a proxy here too to make the thing +        # transparent. +        self.set_imap_connection(imap.conn) +        self.set_smtp_connection(smtp.conn) + +        self._mail_machine = mail +        # XXX ------------------- +        # need to keep a reference? +        #self._mail_events = mail.events +        self._mail_machine.start() + +        self._imap_machine = imap +        self._imap_machine.start() +        self._smtp_machine = smtp +        self._smtp_machine.start() + +    def connect_mail_signals(self, widget): +        """ +        Connects the mail signals to the mail_status widget slots. + +        :param widget: the widget containing the slots. +        :type widget: QtCore.QWidget +        """ +        qtsigs = self._mail_connection.qtsigs +        qtsigs.connected_signal.connect(widget.mail_state_connected) +        qtsigs.connecting_signal.connect(widget.mail_state_connecting) +        qtsigs.disconnecting_signal.connect(widget.mail_state_disconnecting) +        qtsigs.disconnected_signal.connect(widget.mail_state_disconnected) diff --git a/src/leap/bitmask/services/mail/connection.py b/src/leap/bitmask/services/mail/connection.py new file mode 100644 index 00000000..29378f62 --- /dev/null +++ b/src/leap/bitmask/services/mail/connection.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# connection.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/>. +""" +Email Connections +""" +from PySide import QtCore + +from leap.bitmask.services.connections import AbstractLEAPConnection + + +class IMAPConnectionSignals(QtCore.QObject): +    """ +    Qt Signals used by IMAPConnection +    """ +    # commands +    do_connect_signal = QtCore.Signal() +    do_disconnect_signal = QtCore.Signal() + +    # intermediate stages +    connecting_signal = QtCore.Signal() +    disconnecting_signal = QtCore.Signal() + +    connected_signal = QtCore.Signal() +    disconnected_signal = QtCore.Signal() + +    connection_died_signal = QtCore.Signal() +    connection_aborted_signal = QtCore.Signal() + + +class IMAPConnection(AbstractLEAPConnection): + +    _connection_name = "IMAP" + +    def __init__(self): +        self._qtsigs = IMAPConnectionSignals() + + +class SMTPConnectionSignals(QtCore.QObject): +    """ +    Qt Signals used by SMTPConnection +    """ +    # commands +    do_connect_signal = QtCore.Signal() +    do_disconnect_signal = QtCore.Signal() + +    # intermediate stages +    connecting_signal = QtCore.Signal() +    disconnecting_signal = QtCore.Signal() + +    connected_signal = QtCore.Signal() +    disconnected_signal = QtCore.Signal() + +    connection_died_signal = QtCore.Signal() +    connection_aborted_signal = QtCore.Signal() + + +class SMTPConnection(AbstractLEAPConnection): + +    _connection_name = "IMAP" + +    def __init__(self): +        self._qtsigs = SMTPConnectionSignals() + + +class MailConnectionSignals(QtCore.QObject): +    """ +    Qt Signals used by MailConnection +    """ +    # commands +    do_connect_signal = QtCore.Signal() +    do_disconnect_signal = QtCore.Signal() + +    connecting_signal = QtCore.Signal() +    disconnecting_signal = QtCore.Signal() + +    connected_signal = QtCore.Signal() +    disconnected_signal = QtCore.Signal() + +    connection_died_signal = QtCore.Signal() +    connection_aborted_signal = QtCore.Signal() + + +class MailConnection(AbstractLEAPConnection): + +    components = IMAPConnection, SMTPConnection +    _connection_name = "Mail" + +    def __init__(self): +        self._qtsigs = MailConnectionSignals() diff --git a/src/leap/bitmask/services/mail/imap.py b/src/leap/bitmask/services/mail/imap.py index 4828180e..2667f156 100644 --- a/src/leap/bitmask/services/mail/imap.py +++ b/src/leap/bitmask/services/mail/imap.py @@ -41,9 +41,10 @@ def get_mail_check_period():      try:          period = int(period_str)      except (ValueError, TypeError): -        logger.warning("BAD value found for %s: %s" % ( -            INCOMING_CHECK_PERIOD_ENV, -            period_str)) +        if period is not None: +            logger.warning("BAD value found for %s: %s" % ( +                INCOMING_CHECK_PERIOD_ENV, +                period_str))      except Exception as exc:          logger.warning("Unhandled error while getting %s: %r" % (              INCOMING_CHECK_PERIOD_ENV, diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index 4619ba80..54ef67eb 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -20,11 +20,13 @@ Soledad bootstrapping  import logging  import os  import socket +import sys  from ssl import SSLError  from PySide import QtCore  from u1db import errors as u1db_errors +from zope.proxy import sameProxiedObjects  from leap.bitmask.config import flags  from leap.bitmask.config.providerconfig import ProviderConfig @@ -39,7 +41,7 @@ from leap.common.check import leap_assert, leap_assert_type, leap_check  from leap.common.files import which  from leap.keymanager import KeyManager, openpgp  from leap.keymanager.errors import KeyNotFound -from leap.soledad.client import Soledad +from leap.soledad.client import Soledad, BootstrapSequenceError  logger = logging.getLogger(__name__) @@ -190,8 +192,8 @@ class SoledadBootstrapper(AbstractBootstrapper):              # soledad-launcher in the gui.              raise -        leap_check(self._soledad is not None, -                   "Null soledad, error while initializing") +        leap_assert(not sameProxiedObjects(self._soledad, None), +                    "Null soledad, error while initializing")          # and now, let's sync          sync_tries = self.MAX_SYNC_RETRIES @@ -239,14 +241,15 @@ class SoledadBootstrapper(AbstractBootstrapper):          """          # TODO: If selected server fails, retry with another host          # (issue #3309) +        encoding = sys.getfilesystemencoding()          try:              self._soledad = Soledad(                  uuid, -                self._password.encode("utf-8"), -                secrets_path=secrets_path, -                local_db_path=local_db_path, +                self._password, +                secrets_path=secrets_path.encode(encoding), +                local_db_path=local_db_path.encode(encoding),                  server_url=server_url, -                cert_file=cert_file, +                cert_file=cert_file.encode(encoding),                  auth_token=auth_token)          # XXX All these errors should be handled by soledad itself, @@ -257,7 +260,10 @@ class SoledadBootstrapper(AbstractBootstrapper):              logger.debug("SOLEDAD initialization TIMED OUT...")              self.soledad_timeout.emit()          except socket.error as exc: -            logger.error("Socket error while initializing soledad") +            logger.warning("Socket error while initializing soledad") +            self.soledad_timeout.emit() +        except BootstrapSequenceError as exc: +            logger.warning("Error while initializing soledad")              self.soledad_timeout.emit()          # unrecoverable @@ -280,7 +286,7 @@ class SoledadBootstrapper(AbstractBootstrapper):          Raises SoledadSyncError if not successful.          """          try: -            logger.error("trying to sync soledad....") +            logger.debug("trying to sync soledad....")              self._soledad.sync()          except SSLError as exc:              logger.error("%r" % (exc,)) @@ -412,9 +418,9 @@ class SoledadBootstrapper(AbstractBootstrapper):          :param provider_config: Provider configuration          :type provider_config: ProviderConfig          :param user: User's login -        :type user: str +        :type user: unicode          :param password: User's password -        :type password: str +        :type password: unicode          :param download_if_needed: If True, it will only download                                     files if the have changed since the                                     time it was previously downloaded. diff --git a/src/leap/bitmask/util/compat.py b/src/leap/bitmask/util/compat.py new file mode 100644 index 00000000..e34b9ead --- /dev/null +++ b/src/leap/bitmask/util/compat.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# compat.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/>. +""" +Utilities for dealing with compat versions. +""" +from distutils.version import LooseVersion as V + +from requests import __version__ as _requests_version + + +def _requests_has_max_retries(): +    """ +    Returns True if we can use the max_retries parameter +    """ +    return V(_requests_version) > V('1.1.0') + +requests_has_max_retries = _requests_has_max_retries()  | 
