From a1db341a39ec336ab62e89280f9bfb315420bfb5 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Sat, 11 Jan 2014 23:10:09 -0400 Subject: offline mode This will skip: * srp authentication with server * remote soledad configuration * keymanager sending key to server * imap fetches. Its main goal is to help us while debugging imap accounts, by cutting almost all communication with server. It will break havoc if you use it without having local keys configured. So, basically, use with care. --- changes/feature-4943_offline-mode | 1 + docs/dev/signals.rst | 12 + src/leap/bitmask/config/leapsettings.py | 34 ++- src/leap/bitmask/crypto/srpauth.py | 41 ++-- src/leap/bitmask/crypto/tests/test_srpauth.py | 8 +- src/leap/bitmask/gui/login.py | 24 +- src/leap/bitmask/gui/mainwindow.py | 134 ++++++++--- src/leap/bitmask/provider/providerbootstrapper.py | 2 + src/leap/bitmask/services/mail/conductor.py | 13 +- src/leap/bitmask/services/mail/repair.py | 14 +- .../services/soledad/soledadbootstrapper.py | 252 ++++++++++++++++----- src/leap/bitmask/util/__init__.py | 13 ++ 12 files changed, 423 insertions(+), 125 deletions(-) create mode 100644 changes/feature-4943_offline-mode create mode 100644 docs/dev/signals.rst diff --git a/changes/feature-4943_offline-mode b/changes/feature-4943_offline-mode new file mode 100644 index 00000000..02fda893 --- /dev/null +++ b/changes/feature-4943_offline-mode @@ -0,0 +1 @@ +- Offline mode for debugging. Closes: #4943 diff --git a/docs/dev/signals.rst b/docs/dev/signals.rst new file mode 100644 index 00000000..536a3746 --- /dev/null +++ b/docs/dev/signals.rst @@ -0,0 +1,12 @@ +Startup process +--------------- + +mainwindow._login -> backend.run_provider_setup_checks +[...provider bootstrap...] +self._provider_config_loaded +[...login...] +authentication_finished +_start_eip_bootstrap +_maybe_start_eip +_maybe_run_soledad_setup_checks +soledadbootstrapper 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 . - """ -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..18ef56e5 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 # @@ -917,7 +941,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 +953,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 +982,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 +1067,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 +1081,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 +1098,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 +1179,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 +1221,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 +1256,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 +1503,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 +1724,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 addf9bef..c16801d4 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): """ @@ -339,7 +346,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) -- cgit v1.2.3