summaryrefslogtreecommitdiff
path: root/src/leap/bitmask/services
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/bitmask/services')
-rw-r--r--src/leap/bitmask/services/abstractbootstrapper.py40
-rw-r--r--src/leap/bitmask/services/eip/darwinvpnlauncher.py2
-rw-r--r--src/leap/bitmask/services/eip/vpnprocess.py12
-rw-r--r--src/leap/bitmask/services/mail/conductor.py30
-rw-r--r--src/leap/bitmask/services/mail/connection.py2
-rw-r--r--src/leap/bitmask/services/mail/imap.py12
-rw-r--r--src/leap/bitmask/services/mail/plumber.py341
-rw-r--r--src/leap/bitmask/services/soledad/soledadbootstrapper.py343
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)