summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--changes/bug_enable_eip_whenever_possible1
-rw-r--r--changes/bug_fix_login_margins1
-rw-r--r--changes/cleanup-smtpbootstrapper1
-rw-r--r--changes/feature-5672_gracefully-handle-SIGTERM1
-rw-r--r--changes/feature_refactor-retry-to-soledadbootstrapper1
-rw-r--r--src/leap/bitmask/app.py19
-rw-r--r--src/leap/bitmask/backend.py89
-rw-r--r--src/leap/bitmask/gui/mainwindow.py114
-rw-r--r--src/leap/bitmask/gui/ui/login.ui2
-rw-r--r--src/leap/bitmask/services/mail/conductor.py118
-rw-r--r--src/leap/bitmask/services/mail/smtpbootstrapper.py135
-rw-r--r--src/leap/bitmask/services/soledad/soledadbootstrapper.py96
12 files changed, 340 insertions, 238 deletions
diff --git a/changes/bug_enable_eip_whenever_possible b/changes/bug_enable_eip_whenever_possible
new file mode 100644
index 00000000..1065822f
--- /dev/null
+++ b/changes/bug_enable_eip_whenever_possible
@@ -0,0 +1 @@
+- Enable Turn ON button for EIP whenever possible (json and cert are in place). Fixes #5665, #5666. \ No newline at end of file
diff --git a/changes/bug_fix_login_margins b/changes/bug_fix_login_margins
new file mode 100644
index 00000000..3735d911
--- /dev/null
+++ b/changes/bug_fix_login_margins
@@ -0,0 +1 @@
+- Fix Logout button bottom margin. Fixes #4987. \ No newline at end of file
diff --git a/changes/cleanup-smtpbootstrapper b/changes/cleanup-smtpbootstrapper
new file mode 100644
index 00000000..f1ccabfe
--- /dev/null
+++ b/changes/cleanup-smtpbootstrapper
@@ -0,0 +1 @@
+- Refactor smtp logic into its bootstrapper.
diff --git a/changes/feature-5672_gracefully-handle-SIGTERM b/changes/feature-5672_gracefully-handle-SIGTERM
new file mode 100644
index 00000000..a616430d
--- /dev/null
+++ b/changes/feature-5672_gracefully-handle-SIGTERM
@@ -0,0 +1 @@
+- Gracefully handle SIGTERM, with addSystemEventTrigger twisted reactor's method. Closes #5672.
diff --git a/changes/feature_refactor-retry-to-soledadbootstrapper b/changes/feature_refactor-retry-to-soledadbootstrapper
new file mode 100644
index 00000000..bd70a65f
--- /dev/null
+++ b/changes/feature_refactor-retry-to-soledadbootstrapper
@@ -0,0 +1 @@
+- Refactor Soledad initialization retries to SoledadBootstrapper.
diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py
index 146743b5..e413ab4c 100644
--- a/src/leap/bitmask/app.py
+++ b/src/leap/bitmask/app.py
@@ -76,6 +76,16 @@ def sigint_handler(*args, **kwargs):
mainwindow = args[0]
mainwindow.quit()
+def sigterm_handler(*args, **kwargs):
+ """
+ Signal handler for SIGTERM.
+ This handler is actually passed to twisted reactor
+ """
+ logger = kwargs.get('logger', None)
+ if logger:
+ logger.debug("SIGTERM catched. shutting down...")
+ mainwindow = args[0]
+ mainwindow.quit()
def add_logger_handlers(debug=False, logfile=None, replace_stdout=True):
"""
@@ -314,6 +324,9 @@ def main():
sigint_window = partial(sigint_handler, window, logger=logger)
signal.signal(signal.SIGINT, sigint_window)
+ # callable used in addSystemEventTrigger to handle SIGTERM
+ sigterm_window = partial(sigterm_handler, window, logger=logger)
+
if IS_MAC:
window.raise_()
@@ -324,6 +337,12 @@ def main():
l = LoopingCall(QtCore.QCoreApplication.processEvents, 0, 10)
l.start(0.01)
+
+ # SIGTERM can't be handled the same way SIGINT is, since it's
+ # caught by twisted. See _handleSignals method in
+ # twisted/internet/base.py#L1150. So, addSystemEventTrigger
+ # reactor's method is used.
+ reactor.addSystemEventTrigger('before', 'shutdown', sigterm_window)
reactor.run()
if __name__ == "__main__":
diff --git a/src/leap/bitmask/backend.py b/src/leap/bitmask/backend.py
index a351a477..a2df465d 100644
--- a/src/leap/bitmask/backend.py
+++ b/src/leap/bitmask/backend.py
@@ -45,6 +45,8 @@ from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper
from leap.bitmask.services.eip import vpnlauncher, vpnprocess
from leap.bitmask.services.eip import linuxvpnlauncher, darwinvpnlauncher
+from leap.common import certs as leap_certs
+
# Frontend side
from PySide import QtCore
@@ -279,18 +281,23 @@ class EIP(object):
self._vpn = vpnprocess.VPN(signaler=signaler)
- def setup_eip(self, domain):
+ def setup_eip(self, domain, skip_network=False):
"""
Initiate the setup for a provider
:param domain: URL for the provider
:type domain: unicode
+ :param skip_network: Whether checks that involve network should be done
+ or not
+ :type skip_network: bool
:returns: the defer for the operation running in a thread.
:rtype: twisted.internet.defer.Deferred
"""
config = self._provider_config
if get_provider_config(config, domain):
+ if skip_network:
+ return defer.Deferred()
eb = self._eip_bootstrapper
d = eb.run_eip_setup_checks(self._provider_config,
download_if_needed=True)
@@ -338,7 +345,8 @@ class EIP(object):
if not self._provider_config.loaded():
# This means that the user didn't call setup_eip first.
- self._signaler.signal(signaler.BACKEND_BAD_CALL)
+ self._signaler.signal(signaler.BACKEND_BAD_CALL, "EIP.start(), "
+ "no provider loaded")
return
try:
@@ -489,6 +497,47 @@ class EIP(object):
self._signaler.signal(
self._signaler.EIP_GET_GATEWAYS_LIST, gateways)
+ def can_start(self, domain):
+ """
+ Signal whether it has everything that is needed to run EIP or not
+
+ :param domain: the domain for the provider to check
+ :type domain: str
+
+ Signals:
+ eip_can_start
+ eip_cannot_start
+ """
+ try:
+ eip_config = eipconfig.EIPConfig()
+ provider_config = ProviderConfig.get_provider_config(domain)
+
+ api_version = provider_config.get_api_version()
+ eip_config.set_api_version(api_version)
+ eip_loaded = eip_config.load(eipconfig.get_eipconfig_path(domain))
+
+ # check for other problems
+ if not eip_loaded or provider_config is None:
+ raise Exception("Cannot load provider and eip config, cannot "
+ "autostart")
+
+ client_cert_path = eip_config.\
+ get_client_cert_path(provider_config, about_to_download=False)
+
+ if leap_certs.should_redownload(client_cert_path):
+ raise Exception("The client should redownload the certificate,"
+ " cannot autostart")
+
+ if not os.path.isfile(client_cert_path):
+ raise Exception("Can't find the certificate, cannot autostart")
+
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.EIP_CAN_START)
+ except Exception as e:
+ logger.exception(e)
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.EIP_CANNOT_START)
+
class Authenticate(object):
"""
@@ -679,6 +728,10 @@ class Signaler(QtCore.QObject):
eip_status_changed = QtCore.Signal(dict)
eip_process_finished = QtCore.Signal(int)
+ # signals whether the needed files to start EIP exist or not
+ eip_can_start = QtCore.Signal(object)
+ eip_cannot_start = QtCore.Signal(object)
+
# This signal is used to warn the backend user that is doing something
# wrong
backend_bad_call = QtCore.Signal(object)
@@ -745,6 +798,9 @@ class Signaler(QtCore.QObject):
EIP_STATUS_CHANGED = "eip_status_changed"
EIP_PROCESS_FINISHED = "eip_process_finished"
+ EIP_CAN_START = "eip_can_start"
+ EIP_CANNOT_START = "eip_cannot_start"
+
BACKEND_BAD_CALL = "backend_bad_call"
def __init__(self):
@@ -799,6 +855,9 @@ class Signaler(QtCore.QObject):
self.EIP_STATUS_CHANGED,
self.EIP_PROCESS_FINISHED,
+ self.EIP_CAN_START,
+ self.EIP_CANNOT_START,
+
self.SRP_AUTH_OK,
self.SRP_AUTH_ERROR,
self.SRP_AUTH_SERVER_ERROR,
@@ -1041,19 +1100,23 @@ class Backend(object):
self._call_queue.put(("register", "register_user", None, provider,
username, password))
- def setup_eip(self, provider):
+ def setup_eip(self, provider, skip_network=False):
"""
Initiate the setup for a provider
- :param domain: URL for the provider
- :type domain: unicode
+ :param provider: URL for the provider
+ :type provider: unicode
+ :param skip_network: Whether checks that involve network should be done
+ or not
+ :type skip_network: bool
Signals:
eip_config_ready -> {PASSED_KEY: bool, ERROR_KEY: str}
eip_client_certificate_ready -> {PASSED_KEY: bool, ERROR_KEY: str}
eip_cancelled_setup
"""
- self._call_queue.put(("eip", "setup_eip", None, provider))
+ self._call_queue.put(("eip", "setup_eip", None, provider,
+ skip_network))
def cancel_setup_eip(self):
"""
@@ -1132,6 +1195,20 @@ class Backend(object):
self._call_queue.put(("eip", "get_initialized_providers",
None, domains))
+ def eip_can_start(self, domain):
+ """
+ Signal whether it has everything that is needed to run EIP or not
+
+ :param domain: the domain for the provider to check
+ :type domain: str
+
+ Signals:
+ eip_can_start
+ eip_cannot_start
+ """
+ self._call_queue.put(("eip", "can_start",
+ None, domain))
+
def login(self, provider, username, password):
"""
Execute the whole authentication process for a user
diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py
index 8b60ad8e..e3848c46 100644
--- a/src/leap/bitmask/gui/mainwindow.py
+++ b/src/leap/bitmask/gui/mainwindow.py
@@ -183,6 +183,8 @@ class MainWindow(QtGui.QMainWindow):
self.eip_needs_login.connect(self._eip_status.disable_eip_start)
self.eip_needs_login.connect(self._disable_eip_start_action)
+ self._trying_to_start_eip = False
+
# This is loaded only once, there's a bug when doing that more
# than once
# XXX HACK!! But we need it as long as we are using
@@ -210,8 +212,6 @@ class MainWindow(QtGui.QMainWindow):
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_invalid_auth_token.connect(
self._mail_status.set_soledad_invalid_auth_token)
self._soledad_bootstrapper.soledad_failed.connect(
@@ -360,11 +360,24 @@ class MainWindow(QtGui.QMainWindow):
self._backend_connected_signals[signal] = method
signal.connect(method)
+ def _backend_bad_call(self, data):
+ """
+ Callback for debugging bad backend calls
+
+ :param data: data from the backend about the problem
+ :type data: str
+ """
+ logger.error("Bad call to the backend:")
+ logger.error(data)
+
def _backend_connect(self):
"""
Helper to connect to backend signals
"""
sig = self._backend.signaler
+
+ sig.backend_bad_call.connect(self._backend_bad_call)
+
self._connect_and_track(sig.prov_name_resolution,
self._intermediate_stage)
self._connect_and_track(sig.prov_https_connection,
@@ -445,6 +458,9 @@ class MainWindow(QtGui.QMainWindow):
sig.eip_process_restart_tls.connect(self._do_eip_restart)
sig.eip_process_restart_ping.connect(self._do_eip_restart)
+ sig.eip_can_start.connect(self._backend_can_start_eip)
+ sig.eip_cannot_start.connect(self._backend_cannot_start_eip)
+
def _disconnect_and_untrack(self):
"""
Helper to disconnect the tracked signals.
@@ -612,17 +628,43 @@ class MainWindow(QtGui.QMainWindow):
"""
settings = self._settings
default_provider = settings.get_defaultprovider()
+
+ if default_provider is None:
+ logger.warning("Trying toupdate eip enabled status but there's no"
+ " default provider. Disabling EIP for the time"
+ " being...")
+ self._backend_cannot_start_eip()
+ return
+
+ self._trying_to_start_eip = settings.get_autostart_eip()
+ self._backend.eip_can_start(default_provider)
+
+ # If we don't want to start eip, we leave everything
+ # initialized to quickly start it
+ if not self._trying_to_start_eip:
+ self._backend.setup_eip(default_provider, skip_network=True)
+
+ def _backend_can_start_eip(self):
+ """
+ TRIGGER:
+ self._backend.signaler.eip_can_start
+
+ If EIP can be started right away, and the client is configured
+ to do so, start it. Otherwise it leaves everything in place
+ for the user to click Turn ON.
+ """
+ settings = self._settings
+ default_provider = settings.get_defaultprovider()
enabled_services = []
if default_provider is not None:
enabled_services = settings.get_enabled_services(default_provider)
eip_enabled = False
if EIP_SERVICE in enabled_services:
- should_autostart = settings.get_autostart_eip()
- if should_autostart and default_provider is not None:
+ eip_enabled = True
+ if default_provider is not None:
self._eip_status.enable_eip_start()
self._eip_status.set_eip_status("")
- eip_enabled = True
else:
# we don't have an usable provider
# so the user needs to log in first
@@ -632,7 +674,32 @@ class MainWindow(QtGui.QMainWindow):
self._eip_status.disable_eip_start()
self._eip_status.set_eip_status(self.tr("Disabled"))
- return eip_enabled
+ if eip_enabled and self._trying_to_start_eip:
+ self._trying_to_start_eip = False
+ self._try_autostart_eip()
+
+ def _backend_cannot_start_eip(self):
+ """
+ TRIGGER:
+ self._backend.signaler.eip_cannot_start
+
+ If EIP can't be started right away, get the UI to what it
+ needs to look like and waits for a proper login/eip bootstrap.
+ """
+ settings = self._settings
+ default_provider = settings.get_defaultprovider()
+ enabled_services = []
+ if default_provider is not None:
+ enabled_services = settings.get_enabled_services(default_provider)
+
+ if EIP_SERVICE in enabled_services:
+ # we don't have a usable provider
+ # so the user needs to log in first
+ self._eip_status.disable_eip_start()
+ else:
+ self._stop_eip()
+ self._eip_status.disable_eip_start()
+ self._eip_status.set_eip_status(self.tr("Disabled"))
@QtCore.Slot()
def _show_eip_preferences(self):
@@ -752,7 +819,7 @@ class MainWindow(QtGui.QMainWindow):
self._wizard = None
self._backend_connect()
else:
- self._try_autostart_eip()
+ self._update_eip_enabled_status()
domain = self._settings.get_provider()
if domain is not None:
@@ -1363,22 +1430,6 @@ class MainWindow(QtGui.QMainWindow):
# that sets the global status
logger.error("Soledad failed to start: %s" %
(data[self._soledad_bootstrapper.ERROR_KEY],))
- self._retry_soledad_connection()
-
- def _retry_soledad_connection(self):
- """
- Retries soledad connection.
- """
- # XXX should move logic to soledad boostrapper itself
- logger.debug("Retrying soledad connection.")
- if self._soledad_bootstrapper.should_retry_initialization():
- self._soledad_bootstrapper.increment_retries_count()
- # XXX should cancel the existing socket --- this
- # is avoiding a clean termination.
- self._maybe_run_soledad_setup_checks()
- else:
- logger.warning("Max number of soledad initialization "
- "retries reached.")
@QtCore.Slot(dict)
def _soledad_bootstrapped_stage(self, data):
@@ -1431,13 +1482,9 @@ class MainWindow(QtGui.QMainWindow):
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._provides_mx_and_enabled():
- self._mail_conductor.smtp_bootstrapper.run_smtp_setup_checks(
- self._provider_config,
- self._mail_conductor.smtp_config,
- download_if_needed=True)
+ self._mail_conductor.start_smtp_service(self._provider_config,
+ download_if_needed=True)
# XXX --- should remove from here, and connecte directly to the state
# machine.
@@ -1612,23 +1659,19 @@ class MainWindow(QtGui.QMainWindow):
Tries to autostart EIP
"""
settings = self._settings
-
- if not self._update_eip_enabled_status():
- return
-
default_provider = settings.get_defaultprovider()
self._enabled_services = settings.get_enabled_services(
default_provider)
loaded = self._provisional_provider_config.load(
provider.get_provider_path(default_provider))
- if loaded:
+ if loaded and settings.get_autostart_eip():
# XXX I think we should not try to re-download config every time,
# it adds some delay.
# Maybe if it's the first run in a session,
# or we can try only if it fails.
self._maybe_start_eip()
- else:
+ elif settings.get_autostart_eip():
# XXX: Display a proper message to the user
self.eip_needs_login.emit()
logger.error("Unable to load %s config, cannot autostart." %
@@ -1944,7 +1987,6 @@ class MainWindow(QtGui.QMainWindow):
Starts the logout sequence
"""
- self._soledad_bootstrapper.cancel_bootstrap()
setProxiedObject(self._soledad, None)
self._cancel_ongoing_defers()
diff --git a/src/leap/bitmask/gui/ui/login.ui b/src/leap/bitmask/gui/ui/login.ui
index f5725d5a..216eca9e 100644
--- a/src/leap/bitmask/gui/ui/login.ui
+++ b/src/leap/bitmask/gui/ui/login.ui
@@ -215,7 +215,7 @@
<number>0</number>
</property>
<property name="bottomMargin">
- <number>0</number>
+ <number>12</number>
</property>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="lblUser">
diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py
index c1761afa..1766a39d 100644
--- a/src/leap/bitmask/services/mail/conductor.py
+++ b/src/leap/bitmask/services/mail/conductor.py
@@ -18,22 +18,18 @@
Mail Services Conductor
"""
import logging
-import os
-from PySide import QtCore
from zope.proxy import sameProxiedObjects
from leap.bitmask.gui import statemachines
-from leap.bitmask.services.mail import imap
from leap.bitmask.services.mail import connection as mail_connection
+from leap.bitmask.services.mail import imap
from leap.bitmask.services.mail.smtpbootstrapper import SMTPBootstrapper
from leap.bitmask.services.mail.smtpconfig import SMTPConfig
-from leap.bitmask.util import is_file
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
+from leap.common.events import register as leap_register
logger = logging.getLogger(__name__)
@@ -167,10 +163,6 @@ class IMAPControl(object):
class SMTPControl(object):
-
- PORT_KEY = "port"
- IP_KEY = "ip_address"
-
def __init__(self):
"""
Initializes smtp variables.
@@ -178,12 +170,8 @@ class SMTPControl(object):
self.smtp_config = SMTPConfig()
self.smtp_connection = None
self.smtp_machine = None
- self._smtp_service = None
- self._smtp_port = None
self.smtp_bootstrapper = SMTPBootstrapper()
- self.smtp_bootstrapper.download_config.connect(
- self.smtp_bootstrapped_stage)
leap_register(signal=leap_events.SMTP_SERVICE_STARTED,
callback=self._handle_smtp_events,
@@ -200,100 +188,27 @@ class SMTPControl(object):
"""
self.smtp_connection = smtp_connection
- def start_smtp_service(self, host, port, cert):
+ def start_smtp_service(self, provider_config, download_if_needed=False):
"""
- Starts the smtp service.
+ Starts the SMTP service.
- :param host: the hostname of the remove SMTP server.
- :type host: str
- :param port: the port of the remote SMTP server
- :type port: str
- :param cert: the client certificate for authentication
- :type cert: str
+ :param provider_config: Provider configuration
+ :type provider_config: ProviderConfig
+ :param download_if_needed: True if it should check for mtime
+ for the file
+ :type download_if_needed: bool
"""
- # TODO Make the encrypted_only configurable
- # TODO pick local smtp port in a better way
- # TODO remove hard-coded port and let leap.mail set
- # the specific default.
self.smtp_connection.qtsigs.connecting_signal.emit()
- from leap.mail.smtp import setup_smtp_gateway
- self._smtp_service, self._smtp_port = setup_smtp_gateway(
- port=2013,
- userid=self.userid,
- keymanager=self._keymanager,
- smtp_host=host,
- smtp_port=port,
- smtp_cert=cert,
- smtp_key=cert,
- encrypted_only=False)
+ self.smtp_bootstrapper.start_smtp_service(
+ provider_config, self.smtp_config, self._keymanager,
+ self.userid, download_if_needed)
def stop_smtp_service(self):
"""
- Stops the smtp service (port and factory).
+ Stops the SMTP service.
"""
self.smtp_connection.qtsigs.disconnecting_signal.emit()
- # TODO We should homogenize both services.
- if self._smtp_service is not None:
- logger.debug('Stopping smtp service.')
- self._smtp_port.stopListening()
- self._smtp_service.doStop()
-
- @QtCore.Slot(dict)
- def smtp_bootstrapped_stage(self, data):
- """
- TRIGGERS:
- self.smtp_bootstrapper.download_config
-
- If there was a problem, displays it, otherwise it does nothing.
- This is used for intermediate bootstrapping stages, in case
- they fail.
-
- :param data: result from the bootstrapping stage for Soledad
- :type data: dict
- """
- passed = data[self.smtp_bootstrapper.PASSED_KEY]
- if not passed:
- logger.error(data[self.smtp_bootstrapper.ERROR_KEY])
- return
- logger.debug("Done bootstrapping SMTP")
- self.check_smtp_config()
-
- def check_smtp_config(self):
- """
- Checks smtp config and tries to download smtp client cert if needed.
- Currently called when smtp_bootstrapped_stage has successfuly finished.
- """
- logger.debug("Checking SMTP config...")
- leap_assert(self.smtp_bootstrapper._provider_config,
- "smtp bootstrapper does not have a provider_config")
-
- provider_config = self.smtp_bootstrapper._provider_config
- smtp_config = self.smtp_config
- hosts = smtp_config.get_hosts()
- # TODO handle more than one host and define how to choose
- if len(hosts) > 0:
- hostname = hosts.keys()[0]
- logger.debug("Using hostname %s for SMTP" % (hostname,))
- host = hosts[hostname][self.IP_KEY].encode("utf-8")
- port = hosts[hostname][self.PORT_KEY]
-
- client_cert = smtp_config.get_client_cert_path(
- provider_config,
- about_to_download=True)
-
- # XXX change this logic!
- # check_config should be called from within start_service,
- # and not the other way around.
- if not is_file(client_cert):
- self.smtp_bootstrapper._download_client_certificates()
- if os.path.isfile(client_cert):
- self.start_smtp_service(host, port, client_cert)
- else:
- logger.warning("Tried to download email client "
- "certificate, but could not find any")
-
- else:
- logger.warning("No smtp hosts configured")
+ self.smtp_bootstrapper.stop_smtp_service()
# handle smtp events
@@ -348,7 +263,7 @@ class MailConductor(IMAPControl, SMTPControl):
:param keymanager: a transparent proxy that eventually will point to a
Keymanager Instance.
- :type soledad: zope.proxy.ProxyBase
+ :type keymanager: zope.proxy.ProxyBase
"""
IMAPControl.__init__(self)
SMTPControl.__init__(self)
@@ -406,4 +321,5 @@ 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)
+ qtsigs.soledad_invalid_auth_token.connect(
+ widget.soledad_invalid_auth_token)
diff --git a/src/leap/bitmask/services/mail/smtpbootstrapper.py b/src/leap/bitmask/services/mail/smtpbootstrapper.py
index 032d6357..7ecf8134 100644
--- a/src/leap/bitmask/services/mail/smtpbootstrapper.py
+++ b/src/leap/bitmask/services/mail/smtpbootstrapper.py
@@ -20,12 +20,13 @@ SMTP bootstrapping
import logging
import os
-from PySide import QtCore
-
from leap.bitmask.config.providerconfig import ProviderConfig
from leap.bitmask.crypto.certs import download_client_cert
from leap.bitmask.services import download_service_config
from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper
+from leap.bitmask.services.mail.smtpconfig import SMTPConfig
+from leap.bitmask.util import is_file
+
from leap.common import certs as leap_certs
from leap.common.check import leap_assert, leap_assert_type
from leap.common.files import check_and_fix_urw_only
@@ -33,27 +34,33 @@ from leap.common.files import check_and_fix_urw_only
logger = logging.getLogger(__name__)
+class NoSMTPHosts(Exception):
+ """This is raised when there is no SMTP host to use."""
+
+
class SMTPBootstrapper(AbstractBootstrapper):
"""
SMTP init procedure
"""
- # All dicts returned are of the form
- # {"passed": bool, "error": str}
- download_config = QtCore.Signal(dict)
+ PORT_KEY = "port"
+ IP_KEY = "ip_address"
def __init__(self):
AbstractBootstrapper.__init__(self)
self._provider_config = None
self._smtp_config = None
+ self._userid = None
self._download_if_needed = False
- def _download_config(self, *args):
+ self._smtp_service = None
+ self._smtp_port = None
+
+ def _download_config_and_cert(self):
"""
- Downloads the SMTP config for the given provider
+ Downloads the SMTP config and cert for the given provider.
"""
-
leap_assert(self._provider_config,
"We need a provider configuration!")
@@ -66,63 +73,101 @@ class SMTPBootstrapper(AbstractBootstrapper):
self._session,
self._download_if_needed)
- def _download_client_certificates(self, *args):
- """
- Downloads the SMTP client certificate for the given provider
+ hosts = self._smtp_config.get_hosts()
- We actually are downloading the certificate for the same uri as
- for the EIP config, but we duplicate these bits to allow mail
- service to be working in a provider that does not offer EIP.
- """
- # TODO factor out with eipboostrapper.download_client_certificates
- # TODO this shouldn't be a private method, it's called from
- # mainwindow.
- leap_assert(self._provider_config, "We need a provider configuration!")
- leap_assert(self._smtp_config, "We need an smtp configuration!")
+ if len(hosts) == 0:
+ raise NoSMTPHosts()
- logger.debug("Downloading SMTP client certificate for %s" %
- (self._provider_config.get_domain(),))
+ # TODO handle more than one host and define how to choose
+ hostname = hosts.keys()[0]
+ logger.debug("Using hostname %s for SMTP" % (hostname,))
- client_cert_path = self._smtp_config.\
- get_client_cert_path(self._provider_config,
- about_to_download=True)
+ client_cert_path = self._smtp_config.get_client_cert_path(
+ self._provider_config, about_to_download=True)
- # For re-download if something is wrong with the cert
- self._download_if_needed = self._download_if_needed and \
- not leap_certs.should_redownload(client_cert_path)
+ if not is_file(client_cert_path):
+ # For re-download if something is wrong with the cert
+ self._download_if_needed = (
+ self._download_if_needed and
+ not leap_certs.should_redownload(client_cert_path))
- if self._download_if_needed and \
- os.path.isfile(client_cert_path):
- check_and_fix_urw_only(client_cert_path)
- return
+ if self._download_if_needed and os.path.isfile(client_cert_path):
+ check_and_fix_urw_only(client_cert_path)
+ return
- download_client_cert(self._provider_config,
- client_cert_path,
- self._session)
+ download_client_cert(self._provider_config,
+ client_cert_path,
+ self._session)
- def run_smtp_setup_checks(self,
- provider_config,
- smtp_config,
- download_if_needed=False):
+ def _start_smtp_service(self):
+ """
+ Start the smtp service using the downloaded configurations.
+ """
+ # TODO Make the encrypted_only configurable
+ # TODO pick local smtp port in a better way
+ # TODO remove hard-coded port and let leap.mail set
+ # the specific default.
+ # TODO handle more than one host and define how to choose
+ hosts = self._smtp_config.get_hosts()
+ hostname = hosts.keys()[0]
+ host = hosts[hostname][self.IP_KEY].encode("utf-8")
+ port = hosts[hostname][self.PORT_KEY]
+ client_cert_path = self._smtp_config.get_client_cert_path(
+ self._provider_config, about_to_download=True)
+
+ from leap.mail.smtp import setup_smtp_gateway
+ self._smtp_service, self._smtp_port = setup_smtp_gateway(
+ port=2013,
+ userid=self._userid,
+ keymanager=self._keymanager,
+ smtp_host=host,
+ smtp_port=port,
+ smtp_cert=client_cert_path,
+ smtp_key=client_cert_path,
+ encrypted_only=False)
+
+ def start_smtp_service(self, provider_config, smtp_config, keymanager,
+ userid, download_if_needed=False):
"""
- Starts the checks needed for a new smtp setup
+ Starts the SMTP service.
:param provider_config: Provider configuration
:type provider_config: ProviderConfig
:param smtp_config: SMTP configuration to populate
:type smtp_config: SMTPConfig
+ :param keymanager: a transparent proxy that eventually will point to a
+ Keymanager Instance.
+ :type keymanager: zope.proxy.ProxyBase
+ :param userid: the user id, in the form "user@provider"
+ :type userid: str
:param download_if_needed: True if it should check for mtime
for the file
:type download_if_needed: bool
"""
leap_assert_type(provider_config, ProviderConfig)
+ leap_assert_type(smtp_config, SMTPConfig)
self._provider_config = provider_config
+ self._keymanager = keymanager
self._smtp_config = smtp_config
+ self._useid = userid
self._download_if_needed = download_if_needed
- cb_chain = [
- (self._download_config, self.download_config),
- ]
-
- self.addCallbackChain(cb_chain)
+ try:
+ self._download_config_and_cert()
+ logger.debug("Starting SMTP service.")
+ self._start_smtp_service()
+ except NoSMTPHosts:
+ logger.warning("There is no SMTP host to use.")
+ except Exception as e:
+ # TODO: we should handle more specific exceptions in here
+ logger.exception("Error while bootstrapping SMTP: %r" % (e, ))
+
+ def stop_smtp_service(self):
+ """
+ Stops the smtp service (port and factory).
+ """
+ if self._smtp_service is not None:
+ logger.debug('Stopping SMTP service.')
+ self._smtp_port.stopListening()
+ self._smtp_service.doStop()
diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py
index ad5ee4d0..6bb7c036 100644
--- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py
+++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py
@@ -139,7 +139,6 @@ class SoledadBootstrapper(AbstractBootstrapper):
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()
@@ -159,8 +158,6 @@ class SoledadBootstrapper(AbstractBootstrapper):
self._srpauth = None
self._soledad = None
- self._soledad_retries = 0
-
@property
def keymanager(self):
return self._keymanager
@@ -177,26 +174,6 @@ class SoledadBootstrapper(AbstractBootstrapper):
"We need a provider config")
return SRPAuth(self._provider_config)
- # retries
-
- def cancel_bootstrap(self):
- self._soledad_retries = self.MAX_INIT_RETRIES
-
- def should_retry_initialization(self):
- """
- Return True if we should retry the initialization.
- """
- logger.debug("current retries: %s, max retries: %s" % (
- self._soledad_retries,
- self.MAX_INIT_RETRIES))
- return self._soledad_retries < self.MAX_INIT_RETRIES
-
- def increment_retries_count(self):
- """
- Increment the count of initialization retries.
- """
- self._soledad_retries += 1
-
# initialization
def load_offline_soledad(self, username, password, uuid):
@@ -265,6 +242,41 @@ class SoledadBootstrapper(AbstractBootstrapper):
# in the case of an invalid token we have already turned off mail and
# warned the user in _do_soledad_sync()
+ def _do_soledad_init(self, uuid, secrets_path, local_db_path,
+ server_url, cert_file, token):
+ """
+ Initialize soledad, retry if necessary and emit soledad_failed if we
+ can't succeed.
+
+ :param uuid: user identifier
+ :type uuid: str
+ :param secrets_path: path to secrets file
+ :type secrets_path: str
+ :param local_db_path: path to local db file
+ :type local_db_path: str
+ :param server_url: soledad server uri
+ :type server_url: str
+ :param cert_file: path to the certificate of the ca used
+ to validate the SSL certificate used by the remote
+ soledad server.
+ :type cert_file: str
+ :param auth token: auth token
+ :type auth_token: str
+ """
+ init_tries = self.MAX_INIT_RETRIES
+ while init_tries > 0:
+ try:
+ self._try_soledad_init(
+ uuid, secrets_path, local_db_path,
+ server_url, cert_file, token)
+ logger.debug("Soledad has been initialized.")
+ return
+ except Exception:
+ init_tries -= 1
+ continue
+
+ self.soledad_failed.emit()
+ raise SoledadInitError()
def load_and_sync_soledad(self, uuid=None, offline=False):
"""
@@ -283,10 +295,9 @@ class SoledadBootstrapper(AbstractBootstrapper):
server_url, cert_file = remote_param
try:
- self._try_soledad_init(
- uuid, secrets_path, local_db_path,
- server_url, cert_file, token)
- except Exception:
+ self._do_soledad_init(uuid, secrets_path, local_db_path,
+ server_url, cert_file, token)
+ except SoledadInitError:
# re-raise the exceptions from try_init,
# we're currently handling the retries from the
# soledad-launcher in the gui.
@@ -378,9 +389,13 @@ class SoledadBootstrapper(AbstractBootstrapper):
Try to initialize soledad.
:param uuid: user identifier
+ :type uuid: str
:param secrets_path: path to secrets file
+ :type secrets_path: str
:param local_db_path: path to local db file
+ :type local_db_path: str
:param server_url: soledad server uri
+ :type server_url: str
:param cert_file: path to the certificate of the ca used
to validate the SSL certificate used by the remote
soledad server.
@@ -409,34 +424,17 @@ class SoledadBootstrapper(AbstractBootstrapper):
# and return a subclass of SoledadInitializationFailed
# recoverable, will guarantee retries
- except socket.timeout:
- logger.debug("SOLEDAD initialization TIMED OUT...")
- self.soledad_timeout.emit()
- raise
- except socket.error as exc:
- logger.warning("Socket error while initializing soledad")
- self.soledad_timeout.emit()
- raise
- except BootstrapSequenceError as exc:
- logger.warning("Error while initializing soledad")
- self.soledad_timeout.emit()
+ except (socket.timeout, socket.error, BootstrapSequenceError):
+ logger.warning("Error while initializing Soledad")
raise
# unrecoverable
- except u1db_errors.Unauthorized:
- logger.error("Error while initializing soledad "
- "(unauthorized).")
- self.soledad_failed.emit()
- raise
- except u1db_errors.HTTPError as exc:
- logger.exception("Error while initializing soledad "
- "(HTTPError)")
- self.soledad_failed.emit()
+ except (u1db_errors.Unauthorized, u1db_errors.HTTPError):
+ logger.error("Error while initializing Soledad (u1db error).")
raise
except Exception as exc:
logger.exception("Unhandled error while initializating "
- "soledad: %r" % (exc,))
- self.soledad_failed.emit()
+ "Soledad: %r" % (exc,))
raise
def _try_soledad_sync(self):