diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/leap/bitmask/app.py | 5 | ||||
| -rw-r--r-- | src/leap/bitmask/config/flags.py | 4 | ||||
| -rw-r--r-- | src/leap/bitmask/config/leapsettings.py | 34 | ||||
| -rw-r--r-- | src/leap/bitmask/crypto/srpauth.py | 41 | ||||
| -rw-r--r-- | src/leap/bitmask/crypto/tests/test_srpauth.py | 8 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/login.py | 24 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/mainwindow.py | 137 | ||||
| -rw-r--r-- | src/leap/bitmask/provider/providerbootstrapper.py | 2 | ||||
| -rw-r--r-- | src/leap/bitmask/services/mail/conductor.py | 13 | ||||
| -rw-r--r-- | src/leap/bitmask/services/mail/repair.py | 14 | ||||
| -rw-r--r-- | src/leap/bitmask/services/soledad/soledadbootstrapper.py | 252 | ||||
| -rw-r--r-- | src/leap/bitmask/util/__init__.py | 13 | ||||
| -rw-r--r-- | src/leap/bitmask/util/keyring_helpers.py | 43 | ||||
| -rw-r--r-- | src/leap/bitmask/util/leap_argparse.py | 63 | 
14 files changed, 492 insertions, 161 deletions
diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index d50743d6..e8423fd5 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -1,6 +1,6 @@  # -*- coding: utf-8 -*-  # app.py -# Copyright (C) 2013 LEAP +# Copyright (C) 2013, 2014 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 @@ -168,6 +168,7 @@ def main():      """      Starts the main event loop and launches the main window.      """ +    # TODO move boilerplate outa here!      _, opts = leap_argparse.init_leapc_args()      if opts.version: @@ -180,6 +181,7 @@ def main():          sys.exit(0)      standalone = opts.standalone +    offline = opts.offline      bypass_checks = getattr(opts, 'danger', False)      debug = opts.debug      logfile = opts.log_file @@ -199,6 +201,7 @@ def main():      from leap.bitmask.config import flags      from leap.common.config.baseconfig import BaseConfig      flags.STANDALONE = standalone +    flags.OFFLINE = offline      flags.MAIL_LOGFILE = mail_logfile      flags.APP_VERSION_CHECK = opts.app_version_check      flags.API_VERSION_CHECK = opts.api_version_check diff --git a/src/leap/bitmask/config/flags.py b/src/leap/bitmask/config/flags.py index b1576c32..82501fb2 100644 --- a/src/leap/bitmask/config/flags.py +++ b/src/leap/bitmask/config/flags.py @@ -41,3 +41,7 @@ MAIL_LOGFILE = None  #     since it's not released yet, and it is compatible with a newer provider.  APP_VERSION_CHECK = True  API_VERSION_CHECK = True + +# Offline mode? +# Used for skipping soledad bootstrapping/syncs. +OFFLINE = False diff --git a/src/leap/bitmask/config/leapsettings.py b/src/leap/bitmask/config/leapsettings.py index c524425e..91ff83a8 100644 --- a/src/leap/bitmask/config/leapsettings.py +++ b/src/leap/bitmask/config/leapsettings.py @@ -1,6 +1,6 @@  # -*- coding: utf-8 -*-  # leapsettings.py -# Copyright (C) 2013 LEAP +# Copyright (C) 2013, 2014 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 @@ -14,9 +14,8 @@  #  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -  """ -QSettings abstraction +QSettings abstraction.  """  import os  import logging @@ -70,6 +69,7 @@ class LeapSettings(object):      GATEWAY_KEY = "Gateway"      PINNED_KEY = "Pinned"      SKIPFIRSTRUN_KEY = "SkipFirstRun" +    UUIDFORUSER_KEY = "%s/%s_uuid"      # values      GATEWAY_AUTOMATIC = "Automatic" @@ -353,3 +353,31 @@ class LeapSettings(object):          """          leap_assert_type(skip, bool)          self._settings.setValue(self.SKIPFIRSTRUN_KEY, skip) + +    def get_uuid(self, username): +        """ +        Gets the uuid for a given username. + +        :param username: the full user identifier in the form user@provider +        :type username: basestring +        """ +        leap_assert("@" in username, +                    "Expected username in the form user@provider") +        user, provider = username.split('@') +        return self._settings.value( +            self.UUIDFORUSER_KEY % (provider, user), "") + +    def set_uuid(self, username, value): +        """ +        Sets the uuid for a given username. + +        :param username: the full user identifier in the form user@provider +        :type username: basestring +        :param value: the uuid to save +        :type value: basestring +        """ +        leap_assert("@" in username, +                    "Expected username in the form user@provider") +        user, provider = username.split('@') +        leap_assert(len(value) > 0, "We cannot save an empty uuid") +        self._settings.setValue(self.UUIDFORUSER_KEY % (provider, user), value) diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index 85b9b003..bdd38db2 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -31,6 +31,7 @@ from requests.adapters import HTTPAdapter  from PySide import QtCore  from twisted.internet import threads +from leap.bitmask.config.leapsettings import LeapSettings  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 @@ -147,6 +148,7 @@ class SRPAuth(QtCore.QObject):                          "We need a provider config to authenticate")              self._provider_config = provider_config +            self._settings = LeapSettings()              # **************************************************** #              # Dependency injection helpers, override this for more @@ -161,8 +163,8 @@ class SRPAuth(QtCore.QObject):              self._session_id = None              self._session_id_lock = QtCore.QMutex() -            self._uid = None -            self._uid_lock = QtCore.QMutex() +            self._uuid = None +            self._uuid_lock = QtCore.QMutex()              self._token = None              self._token_lock = QtCore.QMutex() @@ -394,24 +396,24 @@ class SRPAuth(QtCore.QObject):              """              try:                  M2 = json_content.get("M2", None) -                uid = json_content.get("id", None) +                uuid = json_content.get("id", None)                  token = json_content.get("token", None)              except Exception as e:                  logger.error(e)                  raise SRPAuthBadDataFromServer("Something went wrong with the "                                                 "login") -            self.set_uid(uid) +            self.set_uuid(uuid)              self.set_token(token) -            if M2 is None or self.get_uid() is None: +            if M2 is None or self.get_uuid() is None:                  logger.error("Something went wrong. Content = %r" %                               (json_content,))                  raise SRPAuthBadDataFromServer(self.tr("Problem getting data "                                                         "from server"))              events_signal( -                proto.CLIENT_UID, content=uid, +                proto.CLIENT_UID, content=uuid,                  reqcbk=lambda req, res: None)  # make the rpc call async              return M2 @@ -475,7 +477,7 @@ class SRPAuth(QtCore.QObject):              :param new_password: the new password for the user              :type new_password: str              """ -            leap_assert(self.get_uid() is not None) +            leap_assert(self.get_uuid() is not None)              if current_password != self._password:                  raise SRPAuthBadUserOrPassword @@ -483,7 +485,7 @@ class SRPAuth(QtCore.QObject):              url = "%s/%s/users/%s.json" % (                  self._provider_config.get_api_uri(),                  self._provider_config.get_api_version(), -                self.get_uid()) +                self.get_uuid())              salt, verifier = self._srp.create_salted_verification_key(                  self._username.encode('utf-8'), new_password.encode('utf-8'), @@ -580,7 +582,7 @@ class SRPAuth(QtCore.QObject):                  raise              else:                  self.set_session_id(None) -                self.set_uid(None) +                self.set_uuid(None)                  self.set_token(None)                  # Also reset the session                  self._session = self._fetcher.session() @@ -594,13 +596,16 @@ class SRPAuth(QtCore.QObject):              QtCore.QMutexLocker(self._session_id_lock)              return self._session_id -        def set_uid(self, uid): -            QtCore.QMutexLocker(self._uid_lock) -            self._uid = uid +        def set_uuid(self, uuid): +            QtCore.QMutexLocker(self._uuid_lock) +            full_uid = "%s@%s" % ( +                self._username, self._provider_config.get_domain()) +            self._settings.set_uuid(full_uid, uuid) +            self._uuid = uuid -        def get_uid(self): -            QtCore.QMutexLocker(self._uid_lock) -            return self._uid +        def get_uuid(self): +            QtCore.QMutexLocker(self._uuid_lock) +            return self._uuid          def set_token(self, token):              QtCore.QMutexLocker(self._token_lock) @@ -676,7 +681,7 @@ class SRPAuth(QtCore.QObject):          :rtype: str or None          """ -        if self.get_uid() is None: +        if self.get_uuid() is None:              return None          return self.__instance._username @@ -705,8 +710,8 @@ class SRPAuth(QtCore.QObject):      def get_session_id(self):          return self.__instance.get_session_id() -    def get_uid(self): -        return self.__instance.get_uid() +    def get_uuid(self): +        return self.__instance.get_uuid()      def get_token(self):          return self.__instance.get_token() diff --git a/src/leap/bitmask/crypto/tests/test_srpauth.py b/src/leap/bitmask/crypto/tests/test_srpauth.py index e63c1385..511a12ed 100644 --- a/src/leap/bitmask/crypto/tests/test_srpauth.py +++ b/src/leap/bitmask/crypto/tests/test_srpauth.py @@ -520,9 +520,9 @@ class SRPAuthTestCase(unittest.TestCase):          m2 = self.auth_backend._extract_data(test_data)          self.assertEqual(m2, test_m2) -        self.assertEqual(self.auth_backend.get_uid(), test_uid) -        self.assertEqual(self.auth_backend.get_uid(), -                         self.auth.get_uid()) +        self.assertEqual(self.auth_backend.get_uuid(), test_uid) +        self.assertEqual(self.auth_backend.get_uuid(), +                         self.auth.get_uuid())          self.assertEqual(self.auth_backend.get_token(), test_token)          self.assertEqual(self.auth_backend.get_token(),                           self.auth.get_token()) @@ -691,7 +691,7 @@ class SRPAuthTestCase(unittest.TestCase):              old_session = self.auth_backend._session              self.auth_backend.logout()              self.assertIsNone(self.auth_backend.get_session_id()) -            self.assertIsNone(self.auth_backend.get_uid()) +            self.assertIsNone(self.auth_backend.get_uuid())              self.assertNotEqual(old_session, self.auth_backend._session)          d = threads.deferToThread(wrapper) diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py index b21057f0..d0cb20b1 100644 --- a/src/leap/bitmask/gui/login.py +++ b/src/leap/bitmask/gui/login.py @@ -19,12 +19,13 @@ Login widget implementation  """  import logging -import keyring -  from PySide import QtCore, QtGui  from ui_login import Ui_LoginWidget +from leap.bitmask.config import flags +from leap.bitmask.util import make_address  from leap.bitmask.util.keyring_helpers import has_keyring +from leap.bitmask.util.keyring_helpers import get_keyring  from leap.common.check import leap_assert_type  logger = logging.getLogger(__name__) @@ -304,14 +305,15 @@ class LoginWidget(QtGui.QWidget):          if self.get_remember() and has_keyring():              # in the keyring and in the settings              # we store the value 'usename@provider' -            username_domain = (username + '@' + provider).encode("utf8") +            full_user_id = make_address(username, provider).encode("utf8")              try: +                keyring = get_keyring()                  keyring.set_password(self.KEYRING_KEY, -                                     username_domain, +                                     full_user_id,                                       password.encode("utf8"))                  # Only save the username if it was saved correctly in                  # the keyring -                self._settings.set_user(username_domain) +                self._settings.set_user(full_user_id)              except Exception as e:                  logger.exception("Problem saving data to keyring. %r"                                   % (e,)) @@ -323,15 +325,20 @@ class LoginWidget(QtGui.QWidget):          """          self.ui.login_widget.hide()          self.ui.logged_widget.show() -        self.ui.lblUser.setText("%s@%s" % (self.get_user(), -                                           self.get_selected_provider())) +        self.ui.lblUser.setText(make_address( +            self.get_user(), self.get_selected_provider()))          self.set_login_status("") -        self.logged_in_signal.emit() + +        if flags.OFFLINE is False: +            self.logged_in_signal.emit()      def logged_out(self):          """          Sets the widgets to the logged out state          """ +        # TODO consider "logging out offline" too... +        # how that would be ??? +          self.ui.login_widget.show()          self.ui.logged_widget.hide() @@ -396,6 +403,7 @@ class LoginWidget(QtGui.QWidget):          saved_password = None          try: +            keyring = get_keyring()              saved_password = keyring.get_password(self.KEYRING_KEY,                                                    saved_user                                                    .encode("utf8")) diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 8c512ad2..83aa47a9 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -1,6 +1,6 @@  # -*- coding: utf-8 -*-  # mainwindow.py -# Copyright (C) 2013 LEAP +# Copyright (C) 2013, 2014 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 @@ -26,6 +26,7 @@ from zope.proxy import ProxyBase, setProxiedObject  from leap.bitmask import __version__ as VERSION  from leap.bitmask import __version_hash__ as VERSION_HASH +from leap.bitmask.config import flags  from leap.bitmask.config.leapsettings import LeapSettings  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.crypto.srpauth import SRPAuth @@ -68,6 +69,7 @@ from leap.bitmask.services.eip.darwinvpnlauncher import EIPNoTunKextLoaded  from leap.bitmask.services.soledad.soledadbootstrapper import \      SoledadBootstrapper +from leap.bitmask.util import make_address  from leap.bitmask.util.keyring_helpers import has_keyring  from leap.bitmask.util.leap_log_handler import LeapLogHandler @@ -95,6 +97,7 @@ class MainWindow(QtGui.QMainWindow):      # Signals      eip_needs_login = QtCore.Signal([]) +    offline_mode_bypass_login = QtCore.Signal([])      new_updates = QtCore.Signal(object)      raise_window = QtCore.Signal([])      soledad_ready = QtCore.Signal([]) @@ -137,12 +140,11 @@ class MainWindow(QtGui.QMainWindow):          # end register leap events ####################################          self._quit_callback = quit_callback -          self._updates_content = "" +        # setup UI          self.ui = Ui_MainWindow()          self.ui.setupUi(self) -          self._backend = backend.Backend(bypass_checks)          self._backend.start() @@ -183,6 +185,9 @@ class MainWindow(QtGui.QMainWindow):              self._on_eip_connected)          self._eip_status.eip_connection_connected.connect(              self._maybe_run_soledad_setup_checks) +        self.offline_mode_bypass_login.connect( +            self._maybe_run_soledad_setup_checks) +          self.eip_needs_login.connect(              self._eip_status.disable_eip_start)          self.eip_needs_login.connect( @@ -204,6 +209,7 @@ class MainWindow(QtGui.QMainWindow):          # This is created once we have a valid provider config          self._srp_auth = None          self._logged_user = None +        self._logged_in_offline = False          self._backend_connect() @@ -239,6 +245,8 @@ class MainWindow(QtGui.QMainWindow):              self._soledad_intermediate_stage)          self._soledad_bootstrapper.gen_key.connect(              self._soledad_bootstrapped_stage) +        self._soledad_bootstrapper.local_only_ready.connect( +            self._soledad_bootstrapped_stage)          self._soledad_bootstrapper.soledad_timeout.connect(              self._retry_soledad_connection)          self._soledad_bootstrapper.soledad_failed.connect( @@ -284,8 +292,9 @@ class MainWindow(QtGui.QMainWindow):          self._enabled_services = [] -        self._center_window() +        # last minute UI manipulations +        self._center_window()          self.ui.lblNewUpdates.setVisible(False)          self.ui.btnMore.setVisible(False)          ######################################### @@ -294,6 +303,8 @@ class MainWindow(QtGui.QMainWindow):          self.ui.btnMore.resize(0, 0)          #########################################          self.ui.btnMore.clicked.connect(self._updates_details) +        if flags.OFFLINE is True: +            self._set_label_offline()          # Services signals/slots connection          self.new_updates.connect(self._react_to_new_updates) @@ -706,6 +717,19 @@ class MainWindow(QtGui.QMainWindow):          self.ui.eipWidget.setVisible(EIP_SERVICE in services)          self.ui.mailWidget.setVisible(MX_SERVICE in services) +    def _set_label_offline(self): +        """ +        Set the login label to reflect offline status. +        """ +        if self._logged_in_offline: +            provider = "" +        else: +            provider = self.ui.lblLoginProvider.text() + +        self.ui.lblLoginProvider.setText( +            provider + +            self.tr(" (offline mode)")) +      #      # systray      # @@ -857,7 +881,8 @@ class MainWindow(QtGui.QMainWindow):              "The current client version is not supported "              "by this provider.<br>"              "Please update to latest version.<br><br>" -            "You can get the latest version from {0}").format(url) +            "You can get the latest version from " +            "<a href='{0}'>{1}</a>").format(url, url)          QtGui.QMessageBox.warning(self, self.tr("Update Needed"), msg)      def _incompatible_api(self): @@ -917,7 +942,6 @@ class MainWindow(QtGui.QMainWindow):          """          # XXX should rename this provider, name clash.          provider = self._login_widget.get_selected_provider() -          self._backend.setup_provider(provider)      def _load_provider_config(self, data): @@ -930,7 +954,7 @@ class MainWindow(QtGui.QMainWindow):          part of the bootstrapping sequence          :param data: result from the last stage of the -        run_provider_select_checks +                     run_provider_select_checks          :type data: dict          """          if data[self._backend.PASSED_KEY]: @@ -959,10 +983,21 @@ class MainWindow(QtGui.QMainWindow):          start the SRP authentication, and as the last step          bootstrapping the EIP service          """ -        leap_assert(self._provider_config, "We need a provider config") - -        if self._login_widget.start_login(): -            self._download_provider_config() +        # TODO most of this could ve handled by the login widget, +        # but we'd have to move lblLoginProvider into the widget itself, +        # instead of having it as a top-level attribute. +        if flags.OFFLINE is True: +            logger.debug("OFFLINE mode! bypassing remote login") +            # TODO reminder, we're not handling logout for offline +            # mode. +            self._login_widget.logged_in() +            self._logged_in_offline = True +            self._set_label_offline() +            self.offline_mode_bypass_login.emit() +        else: +            leap_assert(self._provider_config, "We need a provider config") +            if self._login_widget.start_login(): +                self._download_provider_config()      def _cancel_login(self):          """ @@ -1033,8 +1068,8 @@ class MainWindow(QtGui.QMainWindow):              self._logged_user = self._login_widget.get_user()              user = self._logged_user              domain = self._provider_config.get_domain() -            userid = "%s@%s" % (user, domain) -            self._mail_conductor.userid = userid +            full_user_id = make_address(user, domain) +            self._mail_conductor.userid = full_user_id              self._login_defer = None              self._start_eip_bootstrap()          else: @@ -1047,7 +1082,8 @@ class MainWindow(QtGui.QMainWindow):          """          self._login_widget.logged_in() -        self.ui.lblLoginProvider.setText(self._provider_config.get_domain()) +        provider = self._provider_config.get_domain() +        self.ui.lblLoginProvider.setText(provider)          self._enabled_services = self._settings.get_enabled_services(              self._provider_config.get_domain()) @@ -1063,17 +1099,42 @@ class MainWindow(QtGui.QMainWindow):      def _maybe_run_soledad_setup_checks(self):          """ +        Conditionally start Soledad.          """ -        # TODO soledad should check if we want to run only over EIP. -        if self._already_started_soledad is False \ -                and self._logged_user is not None: -            self._already_started_soledad = True -            self._soledad_bootstrapper.run_soledad_setup_checks( -                self._provider_config, -                self._login_widget.get_user(), -                self._login_widget.get_password(), -                download_if_needed=True) +        # TODO split. +        if self._already_started_soledad is True: +            return + +        username = self._login_widget.get_user() +        password = unicode(self._login_widget.get_password()) +        provider_domain = self._login_widget.get_selected_provider() + +        sb = self._soledad_bootstrapper +        if flags.OFFLINE is True: +            provider_domain = self._login_widget.get_selected_provider() +            sb._password = password + +            self._provisional_provider_config.load( +                provider.get_provider_path(provider_domain)) + +            full_user_id = make_address(username, provider_domain) +            uuid = self._settings.get_uuid(full_user_id) +            self._mail_conductor.userid = full_user_id + +            if uuid is None: +                # We don't need more visibility at the moment, +                # this is mostly for internal use/debug for now. +                logger.warning("Sorry! Log-in at least one time.") +                return +            fun = sb.load_offline_soledad +            fun(full_user_id, password, uuid) +        else: +            provider_config = self._provider_config +            if self._logged_user is not None: +                fun = sb.run_soledad_setup_checks +                fun(provider_config, username, password, +                    download_if_needed=True)      ###################################################################      # Service control methods: soledad @@ -1119,6 +1180,7 @@ class MainWindow(QtGui.QMainWindow):          SLOT          TRIGGERS:            self._soledad_bootstrapper.gen_key +          self._soledad_bootstrapper.local_only_ready          If there was a problem, displays it, otherwise it does nothing.          This is used for intermediate bootstrapping stages, in case @@ -1160,6 +1222,10 @@ class MainWindow(QtGui.QMainWindow):          TRIGGERS:              self.soledad_ready          """ +        if flags.OFFLINE is True: +            logger.debug("not starting smtp in offline mode") +            return +          # TODO for simmetry, this should be called start_smtp_service          # (and delegate all the checks to the conductor)          if self._provider_config.provides_mx() and \ @@ -1191,9 +1257,24 @@ class MainWindow(QtGui.QMainWindow):          TRIGGERS:              self.soledad_ready          """ +        # TODO in the OFFLINE mode we should also modify the  rules +        # in the mail state machine so it shows that imap is active +        # (but not smtp since it's not yet ready for offline use) +        start_fun = self._mail_conductor.start_imap_service +        if flags.OFFLINE is True: +            provider_domain = self._login_widget.get_selected_provider() +            self._provider_config.load( +                provider.get_provider_path(provider_domain)) +        provides_mx = self._provider_config.provides_mx() + +        if flags.OFFLINE is True and provides_mx: +            start_fun() +            return + +        enabled_services = self._enabled_services          if self._provider_config.provides_mx() and \ -                self._enabled_services.count(MX_SERVICE) > 0: -            self._mail_conductor.start_imap_service() +                enabled_services.count(MX_SERVICE) > 0: +            start_fun()      def _on_mail_client_logged_in(self, req):          """ @@ -1423,8 +1504,9 @@ class MainWindow(QtGui.QMainWindow):          if self._logged_user:              self._eip_status.set_provider( -                "%s@%s" % (self._logged_user, -                           self._get_best_provider_config().get_domain())) +                make_address( +                    self._logged_user, +                    self._get_best_provider_config().get_domain()))          self._eip_status.eip_stopped()      @QtCore.Slot() @@ -1643,7 +1725,6 @@ class MainWindow(QtGui.QMainWindow):          Starts the logout sequence          """ -          self._soledad_bootstrapper.cancel_bootstrap()          setProxiedObject(self._soledad, None) diff --git a/src/leap/bitmask/provider/providerbootstrapper.py b/src/leap/bitmask/provider/providerbootstrapper.py index 531d255e..2a66b78c 100644 --- a/src/leap/bitmask/provider/providerbootstrapper.py +++ b/src/leap/bitmask/provider/providerbootstrapper.py @@ -208,6 +208,7 @@ class ProviderBootstrapper(AbstractBootstrapper):          # XXX Watch out, have to check the supported api yet.          else:              if flags.APP_VERSION_CHECK: +                # TODO split                  if not provider.supports_client(min_client_version):                      self._signaler.signal(                          self._signaler.PROV_UNSUPPORTED_CLIENT) @@ -221,6 +222,7 @@ class ProviderBootstrapper(AbstractBootstrapper):                                    domain, "provider.json"])              if flags.API_VERSION_CHECK: +                # TODO split                  api_version = provider_config.get_api_version()                  if provider.supports_api(api_version):                      logger.debug("Provider definition has been modified") diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py index 875b98ea..fc53923c 100644 --- a/src/leap/bitmask/services/mail/conductor.py +++ b/src/leap/bitmask/services/mail/conductor.py @@ -72,6 +72,8 @@ class IMAPControl(object):          """          Starts imap service.          """ +        from leap.bitmask.config import flags +          logger.debug('Starting imap service')          leap_assert(sameProxiedObjects(self._soledad, None)                      is not True, @@ -81,12 +83,17 @@ class IMAPControl(object):                      "We need a non-null keymanager for initializing imap "                      "service") +        offline = flags.OFFLINE          self.imap_service, self.imap_port, \              self.imap_factory = imap.start_imap_service(                  self._soledad,                  self._keymanager, -                userid=self.userid) -        self.imap_service.start_loop() +                userid=self.userid, +                offline=offline) + +        if offline is False: +            logger.debug("Starting loop") +            self.imap_service.start_loop()      def stop_imap_service(self):          """ @@ -340,7 +347,7 @@ class MailConductor(IMAPControl, SMTPControl):          self._mail_machine = None          self._mail_connection = mail_connection.MailConnection() -        self.userid = None +        self._userid = None      @property      def userid(self): diff --git a/src/leap/bitmask/services/mail/repair.py b/src/leap/bitmask/services/mail/repair.py index 30571adf..660e9f11 100644 --- a/src/leap/bitmask/services/mail/repair.py +++ b/src/leap/bitmask/services/mail/repair.py @@ -131,16 +131,20 @@ class MBOXPlumber(object):          Gets the user id for this account.          """          print "Got authenticated." -        self.uid = self.srp.get_uid() -        if not self.uid: -            print "Got BAD UID from provider!" + +        # XXX this won't be needed anymore after we keep a local +        # cache of user uuids, so we'll be able to pick it from +        # there. +        self.uuid = self.srp.get_uuid() +        if not self.uuid: +            print "Got BAD UUID from provider!"              return self.exit() -        print "UID: %s" % (self.uid) +        print "UUID: %s" % (self.uuid)          secrets, localdb = get_db_paths(self.uid)          self.sol = initialize_soledad( -            self.uid, self.userid, self.passwd, +            self.uuid, self.userid, self.passwd,              secrets, localdb, "/tmp", "/tmp")          self.acct = SoledadBackedAccount(self.userid, self.sol) diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index 3ab62b2e..5351bcd2 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -28,15 +28,13 @@ from PySide import QtCore  from u1db import errors as u1db_errors  from zope.proxy import sameProxiedObjects -from twisted.internet.threads import deferToThread -  from leap.bitmask.config import flags  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.crypto.srpauth import SRPAuth  from leap.bitmask.services import download_service_config  from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper  from leap.bitmask.services.soledad.soledadconfig import SoledadConfig -from leap.bitmask.util import is_file, is_empty_file +from leap.bitmask.util import first, is_file, is_empty_file, make_address  from leap.bitmask.util import get_path_prefix  from leap.bitmask.platform_init import IS_WIN  from leap.common.check import leap_assert, leap_assert_type, leap_check @@ -47,10 +45,44 @@ from leap.soledad.client import Soledad, BootstrapSequenceError  logger = logging.getLogger(__name__) +""" +These mocks are replicated from imap tests and the repair utility. +They are needed for the moment to knock out the remote capabilities of soledad +during the use of the offline mode. + +They should not be needed after we allow a null remote initialization in the +soledad client, and a switch to remote sync-able mode during runtime. +""" + + +class Mock(object): +    """ +    A generic simple mock class +    """ +    def __init__(self, return_value=None): +        self._return = return_value + +    def __call__(self, *args, **kwargs): +        return self._return + + +class MockSharedDB(object): +    """ +    Mocked  SharedDB object to replace in soledad before +    instantiating it in offline mode. +    """ +    get_doc = Mock() +    put_doc = Mock() +    lock = Mock(return_value=('atoken', 300)) +    unlock = Mock(return_value=True) + +    def __call__(self): +        return self  # TODO these exceptions could be moved to soledad itself  # after settling this down. +  class SoledadSyncError(Exception):      message = "Error while syncing Soledad" @@ -61,7 +93,7 @@ class SoledadInitError(Exception):  def get_db_paths(uuid):      """ -    Returns the secrets and local db paths needed for soledad +    Return the secrets and local db paths needed for soledad      initialization      :param uuid: uuid for user @@ -88,7 +120,7 @@ def get_db_paths(uuid):  class SoledadBootstrapper(AbstractBootstrapper):      """ -    Soledad init procedure +    Soledad init procedure.      """      SOLEDAD_KEY = "soledad"      KEYMANAGER_KEY = "keymanager" @@ -102,6 +134,7 @@ class SoledadBootstrapper(AbstractBootstrapper):      # {"passed": bool, "error": str}      download_config = QtCore.Signal(dict)      gen_key = QtCore.Signal(dict) +    local_only_ready = QtCore.Signal(dict)      soledad_timeout = QtCore.Signal()      soledad_failed = QtCore.Signal() @@ -115,6 +148,9 @@ class SoledadBootstrapper(AbstractBootstrapper):          self._user = ""          self._password = "" +        self._address = "" +        self._uuid = "" +          self._srpauth = None          self._soledad = None @@ -130,6 +166,8 @@ class SoledadBootstrapper(AbstractBootstrapper):      @property      def srpauth(self): +        if flags.OFFLINE is True: +            return None          leap_assert(self._provider_config is not None,                      "We need a provider config")          return SRPAuth(self._provider_config) @@ -141,7 +179,7 @@ class SoledadBootstrapper(AbstractBootstrapper):      def should_retry_initialization(self):          """ -        Returns True if we should retry the initialization. +        Return True if we should retry the initialization.          """          logger.debug("current retries: %s, max retries: %s" % (              self._soledad_retries, @@ -150,47 +188,94 @@ class SoledadBootstrapper(AbstractBootstrapper):      def increment_retries_count(self):          """ -        Increments the count of initialization retries. +        Increment the count of initialization retries.          """          self._soledad_retries += 1      # initialization -    def load_and_sync_soledad(self): +    def load_offline_soledad(self, username, password, uuid):          """ -        Once everthing is in the right place, we instantiate and sync -        Soledad +        Instantiate Soledad for offline use. + +        :param username: full user id (user@provider) +        :type username: basestring +        :param password: the soledad passphrase +        :type password: unicode +        :param uuid: the user uuid +        :type uuid: basestring          """ -        # TODO this method is still too large -        uuid = self.srpauth.get_uid() -        token = self.srpauth.get_token() +        print "UUID ", uuid +        self._address = username +        self._uuid = uuid +        return self.load_and_sync_soledad(uuid, offline=True) + +    def _get_soledad_local_params(self, uuid, offline=False): +        """ +        Return the locals parameters needed for the soledad initialization. + +        :param uuid: the uuid of the user, used in offline mode. +        :type uuid: unicode, or None. +        :return: secrets_path, local_db_path, token +        :rtype: tuple +        """ +        # in the future, when we want to be able to switch to +        # online mode, this should be a proxy object too. +        # Same for server_url below. + +        if offline is False: +            token = self.srpauth.get_token() +        else: +            token = ""          secrets_path, local_db_path = get_db_paths(uuid) -        # TODO: Select server based on timezone (issue #3308) -        server_dict = self._soledad_config.get_hosts() +        logger.debug('secrets_path:%s' % (secrets_path,)) +        logger.debug('local_db:%s' % (local_db_path,)) +        return (secrets_path, local_db_path, token) -        if not server_dict.keys(): -            # XXX raise more specific exception, and catch it properly! -            raise Exception("No soledad server found") +    def _get_soledad_server_params(self, uuid, offline): +        """ +        Return the remote parameters needed for the soledad initialization. -        selected_server = server_dict[server_dict.keys()[0]] -        server_url = "https://%s:%s/user-%s" % ( -            selected_server["hostname"], -            selected_server["port"], -            uuid) -        logger.debug("Using soledad server url: %s" % (server_url,)) +        :param uuid: the uuid of the user, used in offline mode. +        :type uuid: unicode, or None. +        :return: server_url, cert_file +        :rtype: tuple +        """ +        if uuid is None: +            uuid = self.srpauth.get_uuid() + +        if offline is True: +            server_url = "http://localhost:9999/" +            cert_file = "" +        else: +            server_url = self._pick_server(uuid) +            cert_file = self._provider_config.get_ca_cert_path() -        cert_file = self._provider_config.get_ca_cert_path() +        return server_url, cert_file -        logger.debug('local_db:%s' % (local_db_path,)) -        logger.debug('secrets_path:%s' % (secrets_path,)) +    def load_and_sync_soledad(self, uuid=None, offline=False): +        """ +        Once everthing is in the right place, we instantiate and sync +        Soledad + +        :param uuid: the uuid of the user, used in offline mode. +        :type uuid: unicode, or None. +        :param offline: whether to instantiate soledad for offline use. +        :type offline: bool +        """ +        local_param = self._get_soledad_local_params(uuid, offline) +        remote_param = self._get_soledad_server_params(uuid, offline) + +        secrets_path, local_db_path, token = local_param +        server_url, cert_file = remote_param          try:              self._try_soledad_init(                  uuid, secrets_path, local_db_path,                  server_url, cert_file, token) -        except: +        except Exception:              # re-raise the exceptions from try_init,              # we're currently handling the retries from the              # soledad-launcher in the gui. @@ -198,11 +283,40 @@ class SoledadBootstrapper(AbstractBootstrapper):          leap_assert(not sameProxiedObjects(self._soledad, None),                      "Null soledad, error while initializing") -        self._do_soledad_sync() + +        if flags.OFFLINE is True: +            self._init_keymanager(self._address) +            self.local_only_ready.emit({self.PASSED_KEY: True}) +        else: +            self._do_soledad_sync() + +    def _pick_server(self, uuid): +        """ +        Choose a soledad server to sync against. + +        :param uuid: the uuid for the user. +        :type uuid: unicode +        :returns: the server url +        :rtype: unicode +        """ +        # TODO: Select server based on timezone (issue #3308) +        server_dict = self._soledad_config.get_hosts() + +        if not server_dict.keys(): +            # XXX raise more specific exception, and catch it properly! +            raise Exception("No soledad server found") + +        selected_server = server_dict[first(server_dict.keys())] +        server_url = "https://%s:%s/user-%s" % ( +            selected_server["hostname"], +            selected_server["port"], +            uuid) +        logger.debug("Using soledad server url: %s" % (server_url,)) +        return server_url      def _do_soledad_sync(self):          """ -        Does several retries to get an initial soledad sync. +        Do several retries to get an initial soledad sync.          """          # and now, let's sync          sync_tries = self.MAX_SYNC_RETRIES @@ -231,7 +345,7 @@ class SoledadBootstrapper(AbstractBootstrapper):      def _try_soledad_init(self, uuid, secrets_path, local_db_path,                            server_url, cert_file, auth_token):          """ -        Tries to initialize soledad. +        Try to initialize soledad.          :param uuid: user identifier          :param secrets_path: path to secrets file @@ -247,6 +361,10 @@ class SoledadBootstrapper(AbstractBootstrapper):          # TODO: If selected server fails, retry with another host          # (issue #3309)          encoding = sys.getfilesystemencoding() + +        # XXX We should get a flag in soledad itself +        if flags.OFFLINE is True: +            Soledad._shared_db = MockSharedDB()          try:              self._soledad = Soledad(                  uuid, @@ -281,7 +399,7 @@ class SoledadBootstrapper(AbstractBootstrapper):              self.soledad_failed.emit()              raise          except u1db_errors.HTTPError as exc: -            logger.exception("Error whie initializing soledad " +            logger.exception("Error while initializing soledad "                               "(HTTPError)")              self.soledad_failed.emit()              raise @@ -293,7 +411,7 @@ class SoledadBootstrapper(AbstractBootstrapper):      def _try_soledad_sync(self):          """ -        Tries to sync soledad. +        Try to sync soledad.          Raises SoledadSyncError if not successful.          """          try: @@ -313,7 +431,7 @@ class SoledadBootstrapper(AbstractBootstrapper):      def _download_config(self):          """ -        Downloads the Soledad config for the given provider +        Download the Soledad config for the given provider          """          leap_assert(self._provider_config, @@ -332,11 +450,14 @@ class SoledadBootstrapper(AbstractBootstrapper):          # XXX but honestly, this is a pretty strange entry point for that.          # it feels like it should be the other way around:          # load_and_sync, and from there, if needed, call download_config -        self.load_and_sync_soledad() + +        uuid = self.srpauth.get_uuid() +        self.load_and_sync_soledad(uuid)      def _get_gpg_bin_path(self):          """ -        Returns the path to gpg binary. +        Return the path to gpg binary. +          :returns: the gpg binary path          :rtype: str          """ @@ -364,38 +485,54 @@ class SoledadBootstrapper(AbstractBootstrapper):      def _init_keymanager(self, address):          """ -        Initializes the keymanager. +        Initialize the keymanager. +          :param address: the address to initialize the keymanager with.          :type address: str          """          srp_auth = self.srpauth          logger.debug('initializing keymanager...') -        try: -            self._keymanager = KeyManager( + +        if flags.OFFLINE is True: +            args = (address, "https://localhost", self._soledad) +            kwargs = { +                "session_id":  "", +                "ca_cert_path":  "", +                "api_uri":  "", +                "api_version":  "", +                "uid": self._uuid, +                "gpgbinary": self._get_gpg_bin_path() +            } +        else: +            args = (                  address,                  "https://nicknym.%s:6425" % (                      self._provider_config.get_domain(),), -                self._soledad, -                #token=srp_auth.get_token(),  # TODO: enable token usage -                session_id=srp_auth.get_session_id(), -                ca_cert_path=self._provider_config.get_ca_cert_path(), -                api_uri=self._provider_config.get_api_uri(), -                api_version=self._provider_config.get_api_version(), -                uid=srp_auth.get_uid(), -                gpgbinary=self._get_gpg_bin_path()) +                self._soledad +            ) +            kwargs = { +                "session_id": srp_auth.get_session_id(), +                "ca_cert_path": self._provider_config.get_ca_cert_path(), +                "api_uri": self._provider_config.get_api_uri(), +                "api_version": self._provider_config.get_api_version(), +                "uid": srp_auth.get_uuid(), +                "gpgbinary": self._get_gpg_bin_path() +            } +        try: +            self._keymanager = KeyManager(*args, **kwargs)          except Exception as exc:              logger.exception(exc)              raise -        logger.debug('sending key to server...') - -        # make sure key is in server -        try: -            self._keymanager.send_key(openpgp.OpenPGPKey) -        except Exception as exc: -            logger.error("Error sending key to server.") -            logger.exception(exc) -            # but we do not raise +        if flags.OFFLINE is False: +            # make sure key is in server +            logger.debug('sending key to server...') +            try: +                self._keymanager.send_key(openpgp.OpenPGPKey) +            except Exception as exc: +                logger.error("Error sending key to server.") +                logger.exception(exc) +                # but we do not raise      def _gen_key(self, _):          """ @@ -407,7 +544,8 @@ class SoledadBootstrapper(AbstractBootstrapper):          leap_assert(self._soledad is not None,                      "We need a non-null soledad to generate keys") -        address = "%s@%s" % (self._user, self._provider_config.get_domain()) +        address = make_address( +            self._user, self._provider_config.get_domain())          self._init_keymanager(address)          logger.debug("Retrieving key for %s" % (address,)) diff --git a/src/leap/bitmask/util/__init__.py b/src/leap/bitmask/util/__init__.py index b58e6e3b..85676d51 100644 --- a/src/leap/bitmask/util/__init__.py +++ b/src/leap/bitmask/util/__init__.py @@ -76,3 +76,16 @@ def is_empty_file(path):      Returns True if the file at path is empty.      """      return os.stat(path).st_size is 0 + + +def make_address(user, provider): +    """ +    Return a full identifier for an user, as a email-like +    identifier. + +    :param user: the username +    :type user: basestring +    :param provider: the provider domain +    :type provider: basestring +    """ +    return "%s@%s" % (user, provider) diff --git a/src/leap/bitmask/util/keyring_helpers.py b/src/leap/bitmask/util/keyring_helpers.py index 4b3eb57f..b202d47e 100644 --- a/src/leap/bitmask/util/keyring_helpers.py +++ b/src/leap/bitmask/util/keyring_helpers.py @@ -31,18 +31,45 @@ OBSOLETE_KEYRINGS = [      PlaintextKeyring  ] +canuse = lambda kr: kr is not None and kr.__class__ not in OBSOLETE_KEYRINGS + + +def _get_keyring_with_fallback(): +    """ +    Get the default keyring, and if obsolete try to pick SecretService keyring +    if available. + +    This is a workaround for the cases in which the keyring module chooses +    an insecure keyring by default (ie, inside a virtualenv). +    """ +    kr = keyring.get_keyring() +    if not canuse(kr): +        try: +            kr_klass = keyring.backends.SecretService +            kr = kr_klass.Keyring() +        except AttributeError: +            logger.warning("Keyring cannot find SecretService Backend") +    logger.debug("Selected keyring: %s" % (kr.__class__,)) +    if not canuse(kr): +        logger.debug("Not using default keyring since it is obsolete") +    return kr +  def has_keyring():      """ -    Returns whether we have an useful keyring to use. +    Return whether we have an useful keyring to use.      :rtype: bool      """ -    kr = keyring.get_keyring() -    klass = kr.__class__ -    logger.debug("Selected keyring: %s" % (klass,)) +    kr = _get_keyring_with_fallback() +    return canuse(kr) + -    canuse = kr is not None and klass not in OBSOLETE_KEYRINGS -    if not canuse: -        logger.debug("Not using this keyring since it is obsolete") -    return canuse +def get_keyring(): +    """ +    Return an usable keyring. + +    :rtype: keyringBackend or None +    """ +    kr = _get_keyring_with_fallback() +    return kr if canuse(kr) else None diff --git a/src/leap/bitmask/util/leap_argparse.py b/src/leap/bitmask/util/leap_argparse.py index 280573f1..fb92f141 100644 --- a/src/leap/bitmask/util/leap_argparse.py +++ b/src/leap/bitmask/util/leap_argparse.py @@ -27,41 +27,29 @@ def build_parser():      All the options for the leap arg parser      Some of these could be switched on only if debug flag is present!      """ -    epilog = "Copyright 2012-2013 The LEAP Encryption Access Project" +    epilog = "Copyright 2012-2014 The LEAP Encryption Access Project"      parser = argparse.ArgumentParser(description=""" -Launches Bitmask""", epilog=epilog) +Launches the Bitmask client.""", epilog=epilog)      parser.add_argument('-d', '--debug', action="store_true", -                        help=("Launches Bitmask in debug mode, writing debug" -                              "info to stdout")) -    if not IS_RELEASE_VERSION: -        help_text = "Bypasses the certificate check for bootstrap" -        parser.add_argument('--danger', action="store_true", help=help_text) +                        help=("Launches Bitmask in debug mode, writing debug " +                              "info to stdout.")) +    parser.add_argument('-V', '--version', action="store_true", +                        help='Displays Bitmask version and exits.') +    # files      parser.add_argument('-l', '--logfile', metavar="LOG FILE", nargs='?',                          action="store", dest="log_file", -                        #type=argparse.FileType('w'), -                        help='optional log file') +                        help='Optional log file.')      parser.add_argument('-m', '--mail-logfile',                          metavar="MAIL LOG FILE", nargs='?',                          action="store", dest="mail_log_file", -                        #type=argparse.FileType('w'), -                        help='optional log file for email') -    parser.add_argument('--openvpn-verbosity', nargs='?', -                        type=int, -                        action="store", dest="openvpn_verb", -                        help='verbosity level for openvpn logs [1-6]') +                        help='Optional log file for email.') + +    # flags      parser.add_argument('-s', '--standalone', action="store_true", -                        help='Makes Bitmask use standalone' -                        'directories for configuration and binary' -                        'searching') -    parser.add_argument('-V', '--version', action="store_true", -                        help='Displays Bitmask version and exits') -    parser.add_argument('-r', '--repair-mailboxes', metavar="user@provider", -                        nargs='?', -                        action="store", dest="acct_to_repair", -                        help='Repair mailboxes for a given account. ' -                             'Use when upgrading versions after a schema ' -                             'change.') +                        help='Makes Bitmask use standalone ' +                        'directories for configuration and binary ' +                        'searching.')      parser.add_argument('-N', '--no-app-version-check', default=True,                          action="store_false", dest="app_version_check",                          help='Skip the app version compatibility check with ' @@ -71,6 +59,29 @@ Launches Bitmask""", epilog=epilog)                          help='Skip the api version compatibility check with '                          'the provider.') +    # openvpn options +    parser.add_argument('--openvpn-verbosity', nargs='?', +                        type=int, +                        action="store", dest="openvpn_verb", +                        help='Verbosity level for openvpn logs [1-6]') + +    # mail stuff +    parser.add_argument('-o', '--offline', action="store_true", +                        help='Starts Bitmask in offline mode: will not ' +                             'try to sync with remote replicas for email.') +    parser.add_argument('-r', '--repair-mailboxes', metavar="user@provider", +                        nargs='?', +                        action="store", dest="acct_to_repair", +                        help='Repair mailboxes for a given account. ' +                             'Use when upgrading versions after a schema ' +                             'change.') + +    if not IS_RELEASE_VERSION: +        help_text = ("Bypasses the certificate check during provider " +                     "bootstraping, for debugging development servers. " +                     "Use at your own risk!") +        parser.add_argument('--danger', action="store_true", help=help_text) +      # Not in use, we might want to reintroduce them.      #parser.add_argument('-i', '--no-provider-checks',                          #action="store_true", default=False,  | 
