diff options
author | Tomás Touceda <chiiph@leap.se> | 2014-04-04 16:58:18 -0300 |
---|---|---|
committer | Tomás Touceda <chiiph@leap.se> | 2014-04-04 16:58:18 -0300 |
commit | 81715dc47d77934c4f67d2527a56c28f58f0345d (patch) | |
tree | 6e66940af735e089803c5ce05ad1ce1df16c9d1e /src/leap/bitmask/services | |
parent | 496036f15cf257d16b6594770812da64a249280c (diff) | |
parent | eb4cdab9c6b8ff66bb4667cc6195d2c366122540 (diff) |
Merge branch 'release-0.5.0'0.5.0
Diffstat (limited to 'src/leap/bitmask/services')
-rw-r--r-- | src/leap/bitmask/services/abstractbootstrapper.py | 40 | ||||
-rw-r--r-- | src/leap/bitmask/services/eip/darwinvpnlauncher.py | 2 | ||||
-rw-r--r-- | src/leap/bitmask/services/eip/vpnprocess.py | 12 | ||||
-rw-r--r-- | src/leap/bitmask/services/mail/conductor.py | 30 | ||||
-rw-r--r-- | src/leap/bitmask/services/mail/connection.py | 2 | ||||
-rw-r--r-- | src/leap/bitmask/services/mail/imap.py | 12 | ||||
-rw-r--r-- | src/leap/bitmask/services/mail/plumber.py | 341 | ||||
-rw-r--r-- | src/leap/bitmask/services/soledad/soledadbootstrapper.py | 343 |
8 files changed, 683 insertions, 99 deletions
diff --git a/src/leap/bitmask/services/abstractbootstrapper.py b/src/leap/bitmask/services/abstractbootstrapper.py index 6d4d319b..fc6bd3e9 100644 --- a/src/leap/bitmask/services/abstractbootstrapper.py +++ b/src/leap/bitmask/services/abstractbootstrapper.py @@ -25,7 +25,10 @@ import requests from functools import partial from PySide import QtCore + +from twisted.python import log from twisted.internet import threads +from twisted.internet.defer import CancelledError from leap.common.check import leap_assert, leap_assert_type @@ -40,10 +43,13 @@ class AbstractBootstrapper(QtCore.QObject): PASSED_KEY = "passed" ERROR_KEY = "error" - def __init__(self, bypass_checks=False): + def __init__(self, signaler=None, bypass_checks=False): """ Constructor for the abstract bootstrapper + :param signaler: Signaler object used to receive notifications + from the backend + :type signaler: Signaler :param bypass_checks: Set to true if the app should bypass first round of checks for CA certificates at bootstrap @@ -71,6 +77,7 @@ class AbstractBootstrapper(QtCore.QObject): self._bypass_checks = bypass_checks self._signal_to_emit = None self._err_msg = None + self._signaler = signaler def _gui_errback(self, failure): """ @@ -85,14 +92,30 @@ class AbstractBootstrapper(QtCore.QObject): :param failure: failure object that Twisted generates :type failure: twisted.python.failure.Failure """ + if failure.check(CancelledError): + logger.debug("Defer cancelled.") + failure.trap(Exception) + self._signaler.signal(self._signaler.PROV_CANCELLED_SETUP) + return + if self._signal_to_emit: err_msg = self._err_msg \ if self._err_msg is not None \ else str(failure.value) - self._signal_to_emit.emit({ + data = { self.PASSED_KEY: False, self.ERROR_KEY: err_msg - }) + } + # TODO: Remove this check when all the bootstrappers are + # in the backend form + if isinstance(self._signal_to_emit, basestring): + if self._signaler is not None: + self._signaler.signal(self._signal_to_emit, data) + else: + logger.warning("Tried to notify but no signaler found") + else: + self._signal_to_emit.emit(data) + log.err(failure) failure.trap(Exception) def _errback(self, failure, signal=None): @@ -127,8 +150,15 @@ class AbstractBootstrapper(QtCore.QObject): :param signal: Signal to emit if it fails here first :type signal: QtCore.SignalInstance """ - if signal: - signal.emit({self.PASSED_KEY: True, self.ERROR_KEY: ""}) + if signal is not None: + data = {self.PASSED_KEY: True, self.ERROR_KEY: ""} + if isinstance(signal, basestring): + if self._signaler is not None: + self._signaler.signal(signal, data) + else: + logger.warning("Tried to notify but no signaler found") + else: + signal.emit(data) def _callback_threader(self, cb, res, *args, **kwargs): return threads.deferToThread(cb, res, *args, **kwargs) diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py index fe3fe4c1..a03bfc44 100644 --- a/src/leap/bitmask/services/eip/darwinvpnlauncher.py +++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py @@ -95,7 +95,7 @@ class DarwinVPNLauncher(VPNLauncher): resources_path = os.path.abspath( os.path.join(os.getcwd(), "../../Contents/Resources")) - return os.path.join(resources_path, "leap-client.tiff") + return os.path.join(resources_path, "bitmask.tiff") @classmethod def get_cocoasudo_ovpn_cmd(kls): diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index 51f0f738..5c100036 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -19,14 +19,20 @@ VPN Manager, spawned in a custom processProtocol. """ import logging import os -import psutil -import psutil.error import shutil import socket import sys from itertools import chain, repeat +import psutil +try: + # psutil < 2.0.0 + from psutil.error import AccessDenied as psutil_AccessDenied +except ImportError: + # psutil >= 2.0.0 + from psutil import AccessDenied as psutil_AccessDenied + from PySide import QtCore from leap.bitmask.config.providerconfig import ProviderConfig @@ -672,7 +678,7 @@ class VPNManager(object): if any(map(lambda s: s.find("LEAPOPENVPN") != -1, p.cmdline)): openvpn_process = p break - except psutil.error.AccessDenied: + except psutil_AccessDenied: pass return openvpn_process diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py index addf9bef..79f324dc 100644 --- a/src/leap/bitmask/services/mail/conductor.py +++ b/src/leap/bitmask/services/mail/conductor.py @@ -35,6 +35,7 @@ from leap.common.check import leap_assert from leap.common.events import register as leap_register from leap.common.events import events_pb2 as leap_events + logger = logging.getLogger(__name__) @@ -72,6 +73,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,16 +84,25 @@ 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): + def stop_imap_service(self, cv): """ Stops imap service (fetcher, factory and port). + + :param cv: A condition variable to which we can signal when imap + indeed stops. + :type cv: threading.Condition """ self.imap_connection.qtsigs.disconnecting_signal.emit() # TODO We should homogenize both services. @@ -102,7 +114,14 @@ class IMAPControl(object): # Stop listening on the IMAP port self.imap_port.stopListening() # Stop the protocol - self.imap_factory.doStop() + self.imap_factory.theAccount.closed = True + self.imap_factory.doStop(cv) + else: + # main window does not have to wait because there's no service to + # be stopped, so we release the condition variable + cv.acquire() + cv.notify() + cv.release() def fetch_incoming_mail(self): """ @@ -339,7 +358,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): @@ -388,3 +407,4 @@ class MailConductor(IMAPControl, SMTPControl): qtsigs.connecting_signal.connect(widget.mail_state_connecting) qtsigs.disconnecting_signal.connect(widget.mail_state_disconnecting) qtsigs.disconnected_signal.connect(widget.mail_state_disconnected) + qtsigs.soledad_invalid_auth_token.connect(widget.soledad_invalid_auth_token) diff --git a/src/leap/bitmask/services/mail/connection.py b/src/leap/bitmask/services/mail/connection.py index 29378f62..fdc28fe4 100644 --- a/src/leap/bitmask/services/mail/connection.py +++ b/src/leap/bitmask/services/mail/connection.py @@ -93,6 +93,8 @@ class MailConnectionSignals(QtCore.QObject): connection_died_signal = QtCore.Signal() connection_aborted_signal = QtCore.Signal() + soledad_invalid_auth_token = QtCore.Signal() + class MailConnection(AbstractLEAPConnection): diff --git a/src/leap/bitmask/services/mail/imap.py b/src/leap/bitmask/services/mail/imap.py index 2667f156..5db18cb9 100644 --- a/src/leap/bitmask/services/mail/imap.py +++ b/src/leap/bitmask/services/mail/imap.py @@ -19,10 +19,10 @@ Initialization of imap service """ import logging import os -#import sys +import sys from leap.mail.imap.service import imap -#from twisted.python import log +from twisted.python import log logger = logging.getLogger(__name__) @@ -58,15 +58,15 @@ def start_imap_service(*args, **kwargs): :returns: twisted.internet.task.LoopingCall instance """ + from leap.bitmask.config import flags logger.debug('Launching imap service') override_period = get_mail_check_period() if override_period: kwargs['check_period'] = override_period - # Uncomment the next two lines to get a separate debugging log - # TODO handle this by a separate flag. - #log.startLogging(open('/tmp/leap-imap.log', 'w')) - #log.startLogging(sys.stdout) + if flags.MAIL_LOGFILE: + log.startLogging(open(flags.MAIL_LOGFILE, 'w')) + log.startLogging(sys.stdout) return imap.run_service(*args, **kwargs) diff --git a/src/leap/bitmask/services/mail/plumber.py b/src/leap/bitmask/services/mail/plumber.py new file mode 100644 index 00000000..c16a1fed --- /dev/null +++ b/src/leap/bitmask/services/mail/plumber.py @@ -0,0 +1,341 @@ +# -*- coding: utf-8 -*- +# plumber.py +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Utils for manipulating local mailboxes. +""" +import getpass +import logging +import os + +from collections import defaultdict +from functools import partial + +from twisted.internet import defer + +from leap.bitmask.config.leapsettings import LeapSettings +from leap.bitmask.config.providerconfig import ProviderConfig +from leap.bitmask.provider import get_provider_path +from leap.bitmask.services.soledad.soledadbootstrapper import get_db_paths +from leap.bitmask.util import flatten, get_path_prefix + +from leap.mail.imap.account import SoledadBackedAccount +from leap.mail.imap.memorystore import MemoryStore +from leap.mail.imap.soledadstore import SoledadStore +from leap.soledad.client import Soledad + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + + +def initialize_soledad(uuid, email, passwd, + secrets, localdb, + gnupg_home, tempdir): + """ + Initializes soledad by hand + + :param email: ID for the user + :param gnupg_home: path to home used by gnupg + :param tempdir: path to temporal dir + :rtype: Soledad instance + """ + # XXX TODO unify with an authoritative source of mocks + # for soledad (or partial initializations). + # This is copied from the imap tests. + + server_url = "http://provider" + cert_file = "" + + class Mock(object): + def __init__(self, return_value=None): + self._return = return_value + + def __call__(self, *args, **kwargs): + return self._return + + class MockSharedDB(object): + + get_doc = Mock() + put_doc = Mock() + lock = Mock(return_value=('atoken', 300)) + unlock = Mock(return_value=True) + + def __call__(self): + return self + + Soledad._shared_db = MockSharedDB() + soledad = Soledad( + uuid, + passwd, + secrets, + localdb, + server_url, + cert_file) + + return soledad + + +class MBOXPlumber(object): + """ + An class that can fix things inside a soledadbacked account. + The idea is to gather in this helper different fixes for mailboxes + that can be invoked when data migration in the client is needed. + """ + + def __init__(self, userid, passwd, mdir=None): + """ + Initialize the plumber with all that's needed to authenticate + against the provider. + + :param userid: user identifier, foo@bar + :type userid: basestring + :param passwd: the soledad passphrase + :type passwd: basestring + :param mdir: a path to a maildir to import + :type mdir: str or None + """ + self.userid = userid + self.passwd = passwd + user, provider = userid.split('@') + self.user = user + self.mdir = mdir + self.sol = None + self._settings = LeapSettings() + + provider_config_path = os.path.join(get_path_prefix(), + get_provider_path(provider)) + provider_config = ProviderConfig() + loaded = provider_config.load(provider_config_path) + if not loaded: + print "could not load provider config!" + return self.exit() + + def _init_local_soledad(self): + """ + Initialize local Soledad instance. + """ + self.uuid = self._settings.get_uuid(self.userid) + if not self.uuid: + print "Cannot get UUID from settings. Log in at least once." + return False + + print "UUID: %s" % (self.uuid) + + secrets, localdb = get_db_paths(self.uuid) + + self.sol = initialize_soledad( + self.uuid, self.userid, self.passwd, + secrets, localdb, "/tmp", "/tmp") + memstore = MemoryStore( + permanent_store=SoledadStore(self.sol), + write_period=5) + self.acct = SoledadBackedAccount(self.userid, self.sol, + memstore=memstore) + return True + + # + # Account repairing + # + + def repair_account(self, *args): + """ + Repair mbox uids for all mboxes in this account. + """ + init = self._init_local_soledad() + if not init: + return self.exit() + + for mbox_name in self.acct.mailboxes: + self.repair_mbox_uids(mbox_name) + print "done." + self.exit() + + def repair_mbox_uids(self, mbox_name): + """ + Repair indexes for a given mbox. + + :param mbox_name: mailbox to repair + :type mbox_name: basestring + """ + print + print "REPAIRING INDEXES FOR MAILBOX %s" % (mbox_name,) + print "----------------------------------------------" + mbox = self.acct.getMailbox(mbox_name) + len_mbox = mbox.getMessageCount() + print "There are %s messages" % (len_mbox,) + + last_ok = True if mbox.last_uid == len_mbox else False + uids_iter = mbox.messages.all_msg_iter() + dupes = self._has_dupes(uids_iter) + if last_ok and not dupes: + print "Mbox does not need repair." + return + + # XXX CHANGE? ---- + msgs = mbox.messages.get_all() + for zindex, doc in enumerate(msgs): + mindex = zindex + 1 + old_uid = doc.content['uid'] + doc.content['uid'] = mindex + self.sol.put_doc(doc) + if mindex != old_uid: + print "%s -> %s (%s)" % (mindex, doc.content['uid'], old_uid) + + old_last_uid = mbox.last_uid + mbox.last_uid = len_mbox + print "LAST UID: %s (%s)" % (mbox.last_uid, old_last_uid) + + def _has_dupes(self, sequence): + """ + Return True if the given sequence of ints has duplicates. + + :param sequence: a sequence of ints + :type sequence: sequence + :rtype: bool + """ + d = defaultdict(lambda: 0) + for uid in sequence: + d[uid] += 1 + if d[uid] != 1: + return True + return False + + # + # Maildir import + # + def import_mail(self, mail_filename): + """ + Import a single mail into a mailbox. + + :param mbox: the Mailbox instance to save in. + :type mbox: SoledadMailbox + :param mail_filename: the filename to the mail file to save + :type mail_filename: basestring + :return: a deferred + """ + def saved(_): + print "message added" + + with open(mail_filename) as f: + mail_string = f.read() + #uid = self._mbox.getUIDNext() + #print "saving with UID: %s" % uid + d = self._mbox.messages.add_msg( + mail_string, notify_on_disk=True) + return d + + def import_maildir(self, mbox_name="INBOX"): + """ + Import all mails in a maildir. + + We will process all subfolders as beloging + to the same mailbox (cur, new, tmp). + """ + # TODO parse hierarchical subfolders into + # inferior mailboxes. + + if not os.path.isdir(self.mdir): + print "ERROR: maildir path does not exist." + return + + init = self._init_local_soledad() + if not init: + return self.exit() + + mbox = self.acct.getMailbox(mbox_name) + self._mbox = mbox + len_mbox = mbox.getMessageCount() + + mail_files_g = flatten( + map(partial(os.path.join, f), files) + for f, _, files in os.walk(self.mdir)) + + # we only coerce the generator to give the + # len, but we could skip than and inform at the end. + mail_files = list(mail_files_g) + print "Got %s mails to import into %s (%s)" % ( + len(mail_files), mbox_name, len_mbox) + + def all_saved(_): + print "all messages imported" + + deferreds = [] + for f_name in mail_files: + deferreds.append(self.import_mail(f_name)) + print "deferreds: ", deferreds + + d1 = defer.gatherResults(deferreds, consumeErrors=False) + d1.addCallback(all_saved) + d1.addCallback(self._cbExit) + + def _cbExit(self, ignored): + return self.exit() + + def exit(self): + from twisted.internet import reactor + try: + if self.sol: + self.sol.close() + reactor.stop() + except Exception: + pass + return + + +def repair_account(userid): + """ + Start repair process for a given account. + + :param userid: the user id (email-like) + """ + from twisted.internet import reactor + passwd = unicode(getpass.getpass("Passphrase: ")) + + # go mario! + plumber = MBOXPlumber(userid, passwd) + reactor.callLater(1, plumber.repair_account) + reactor.run() + + +def import_maildir(userid, maildir_path): + """ + Start import-maildir process for a given account. + + :param userid: the user id (email-like) + """ + from twisted.internet import reactor + passwd = unicode(getpass.getpass("Passphrase: ")) + + # go mario! + plumber = MBOXPlumber(userid, passwd, mdir=maildir_path) + reactor.callLater(1, plumber.import_maildir) + reactor.run() + + +if __name__ == "__main__": + import sys + + logging.basicConfig() + + if len(sys.argv) != 3: + print "Usage: plumber [repair|import] <username>" + sys.exit(1) + + # this would be better with a dict if it grows + if sys.argv[1] == "repair": + repair_account(sys.argv[2]) + if sys.argv[1] == "import": + print "Not implemented yet." diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index d078ae96..ad5ee4d0 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -23,12 +23,13 @@ import socket import sys from ssl import SSLError +from sqlite3 import ProgrammingError as sqlite_ProgrammingError from PySide import QtCore from u1db import errors as u1db_errors +from twisted.internet import threads from zope.proxy import sameProxiedObjects - -from twisted.internet.threads import deferToThread +from pysqlcipher.dbapi2 import ProgrammingError as sqlcipher_ProgrammingError from leap.bitmask.config import flags from leap.bitmask.config.providerconfig import ProviderConfig @@ -36,21 +37,56 @@ 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 from leap.common.files import which from leap.keymanager import KeyManager, openpgp from leap.keymanager.errors import KeyNotFound +from leap.soledad.common.errors import InvalidAuthTokenError 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" @@ -59,9 +95,36 @@ class SoledadInitError(Exception): message = "Error while initializing Soledad" +def get_db_paths(uuid): + """ + Return the secrets and local db paths needed for soledad + initialization + + :param uuid: uuid for user + :type uuid: str + + :return: a tuple with secrets, local_db paths + :rtype: tuple + """ + prefix = os.path.join(get_path_prefix(), "leap", "soledad") + secrets = "%s/%s.secret" % (prefix, uuid) + local_db = "%s/%s.db" % (prefix, uuid) + + # We remove an empty file if found to avoid complains + # about the db not being properly initialized + if is_file(local_db) and is_empty_file(local_db): + try: + os.remove(local_db) + except OSError: + logger.warning( + "Could not remove empty file %s" + % local_db) + return secrets, local_db + + class SoledadBootstrapper(AbstractBootstrapper): """ - Soledad init procedure + Soledad init procedure. """ SOLEDAD_KEY = "soledad" KEYMANAGER_KEY = "keymanager" @@ -75,7 +138,9 @@ 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_invalid_auth_token = QtCore.Signal() soledad_failed = QtCore.Signal() def __init__(self): @@ -88,6 +153,9 @@ class SoledadBootstrapper(AbstractBootstrapper): self._user = "" self._password = "" + self._address = "" + self._uuid = "" + self._srpauth = None self._soledad = None @@ -103,6 +171,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) @@ -114,7 +184,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, @@ -123,72 +193,100 @@ class SoledadBootstrapper(AbstractBootstrapper): def increment_retries_count(self): """ - Increments the count of initialization retries. + Increment the count of initialization retries. """ self._soledad_retries += 1 - def _get_db_paths(self, uuid): + # initialization + + def load_offline_soledad(self, username, password, uuid): + """ + 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 """ - Returns the secrets and local db paths needed for soledad - initialization + print "UUID ", uuid + self._address = username + self._uuid = uuid + return self.load_and_sync_soledad(uuid, offline=True) - :param uuid: uuid for user - :type uuid: str + def _get_soledad_local_params(self, uuid, offline=False): + """ + Return the locals parameters needed for the soledad initialization. - :return: a tuple with secrets, local_db paths + :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 """ - prefix = os.path.join(get_path_prefix(), "leap", "soledad") - secrets = "%s/%s.secret" % (prefix, uuid) - local_db = "%s/%s.db" % (prefix, uuid) + # 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. - # We remove an empty file if found to avoid complains - # about the db not being properly initialized - if is_file(local_db) and is_empty_file(local_db): - try: - os.remove(local_db) - except OSError: - logger.warning("Could not remove empty file %s" - % local_db) - return secrets, local_db + if offline is False: + token = self.srpauth.get_token() + else: + token = "" - # initialization + secrets_path, local_db_path = get_db_paths(uuid) + + logger.debug('secrets_path:%s' % (secrets_path,)) + logger.debug('local_db:%s' % (local_db_path,)) + return (secrets_path, local_db_path, token) - def load_and_sync_soledad(self): + def _get_soledad_server_params(self, uuid, offline): """ - Once everthing is in the right place, we instantiate and sync - Soledad + Return the remote parameters needed for the soledad initialization. + + :param uuid: the uuid of the user, used in offline mode. + :type uuid: unicode, or None. + :return: server_url, cert_file + :rtype: tuple """ - # TODO this method is still too large - uuid = self.srpauth.get_uid() - token = self.srpauth.get_token() + if uuid is None: + uuid = self.srpauth.get_uuid() - secrets_path, local_db_path = self._get_db_paths(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() - # TODO: Select server based on timezone (issue #3308) - server_dict = self._soledad_config.get_hosts() + return server_url, cert_file - if not server_dict.keys(): - # XXX raise more specific exception, and catch it properly! - raise Exception("No soledad server found") + def _soledad_sync_errback(self, failure): + failure.trap(InvalidAuthTokenError) + # in the case of an invalid token we have already turned off mail and + # warned the user in _do_soledad_sync() - 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,)) - cert_file = self._provider_config.get_ca_cert_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) - logger.debug('local_db:%s' % (local_db_path,)) - logger.debug('secrets_path:%s' % (secrets_path,)) + 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. @@ -196,11 +294,52 @@ 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, token) + self.local_only_ready.emit({self.PASSED_KEY: True}) + else: + try: + address = make_address( + self._user, self._provider_config.get_domain()) + self._init_keymanager(address, token) + self._keymanager.get_key( + address, openpgp.OpenPGPKey, + private=True, fetch_remote=False) + d = threads.deferToThread(self._do_soledad_sync) + d.addErrback(self._soledad_sync_errback) + except KeyNotFound: + logger.debug("Key not found. Generating key for %s" % + (address,)) + 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 @@ -220,6 +359,13 @@ class SoledadBootstrapper(AbstractBootstrapper): # ubuntu folks. sync_tries -= 1 continue + except InvalidAuthTokenError: + self.soledad_invalid_auth_token.emit() + raise + except Exception as e: + logger.exception("Unhandled error while syncing " + "soledad: %r" % (e,)) + break # reached bottom, failed to sync # and there's nothing we can do... @@ -229,7 +375,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 @@ -245,6 +391,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, @@ -279,7 +429,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 @@ -291,7 +441,7 @@ class SoledadBootstrapper(AbstractBootstrapper): def _try_soledad_sync(self): """ - Tries to sync soledad. + Try to sync soledad. Raises SoledadSyncError if not successful. """ try: @@ -300,6 +450,16 @@ class SoledadBootstrapper(AbstractBootstrapper): except SSLError as exc: logger.error("%r" % (exc,)) raise SoledadSyncError("Failed to sync soledad") + except u1db_errors.InvalidGeneration as exc: + logger.error("%r" % (exc,)) + raise SoledadSyncError("u1db: InvalidGeneration") + except (sqlite_ProgrammingError, sqlcipher_ProgrammingError) as e: + logger.exception("%r" % (e,)) + raise + except InvalidAuthTokenError: + # token is invalid, probably expired + logger.error('Invalid auth token while trying to sync Soledad') + raise except Exception as exc: logger.exception("Unhandled error while syncing " "soledad: %r" % (exc,)) @@ -307,7 +467,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, @@ -326,11 +486,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 """ @@ -356,40 +519,62 @@ class SoledadBootstrapper(AbstractBootstrapper): leap_check(gpgbin is not None, "Could not find gpg binary") return gpgbin - def _init_keymanager(self, address): + def _init_keymanager(self, address, token): """ - Initializes the keymanager. + Initialize the keymanager. + :param address: the address to initialize the keymanager with. :type address: str + :param token: the auth token for accessing webapp. + :type token: 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 = { + "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 = { + "token": token, + "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 KeyNotFound: + logger.debug('key for %s not found.' % address) 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('Trying to send key to server...') + try: + self._keymanager.send_key(openpgp.OpenPGPKey) + except KeyNotFound: + logger.debug('No key found for %s, will generate soon.' + % address) + except Exception as exc: + logger.error("Error sending key to server.") + logger.exception(exc) + # but we do not raise def _gen_key(self, _): """ @@ -401,8 +586,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()) - self._init_keymanager(address) + address = make_address( + self._user, self._provider_config.get_domain()) logger.debug("Retrieving key for %s" % (address,)) try: @@ -462,4 +647,4 @@ class SoledadBootstrapper(AbstractBootstrapper): (self._gen_key, self.gen_key) ] - self.addCallbackChain(cb_chain) + return self.addCallbackChain(cb_chain) |