summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--changes/feature-4943_offline-mode1
-rw-r--r--docs/dev/signals.rst12
-rw-r--r--src/leap/bitmask/app.py5
-rw-r--r--src/leap/bitmask/config/flags.py4
-rw-r--r--src/leap/bitmask/config/leapsettings.py34
-rw-r--r--src/leap/bitmask/crypto/srpauth.py41
-rw-r--r--src/leap/bitmask/crypto/tests/test_srpauth.py8
-rw-r--r--src/leap/bitmask/gui/login.py24
-rw-r--r--src/leap/bitmask/gui/mainwindow.py137
-rw-r--r--src/leap/bitmask/provider/providerbootstrapper.py2
-rw-r--r--src/leap/bitmask/services/mail/conductor.py13
-rw-r--r--src/leap/bitmask/services/mail/repair.py14
-rw-r--r--src/leap/bitmask/services/soledad/soledadbootstrapper.py252
-rw-r--r--src/leap/bitmask/util/__init__.py13
-rw-r--r--src/leap/bitmask/util/keyring_helpers.py43
-rw-r--r--src/leap/bitmask/util/leap_argparse.py63
16 files changed, 505 insertions, 161 deletions
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/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,