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/eip/conductor.py321
-rw-r--r--src/leap/bitmask/services/eip/darwinvpnlauncher.py2
-rw-r--r--src/leap/bitmask/services/eip/eipconfig.py28
-rw-r--r--src/leap/bitmask/services/eip/linuxvpnlauncher.py8
-rw-r--r--src/leap/bitmask/services/eip/vpnlauncher.py9
-rw-r--r--src/leap/bitmask/services/eip/vpnprocess.py88
-rw-r--r--src/leap/bitmask/services/mail/conductor.py135
-rw-r--r--src/leap/bitmask/services/mail/imapcontroller.py103
-rw-r--r--src/leap/bitmask/services/mail/smtpbootstrapper.py26
-rw-r--r--src/leap/bitmask/services/soledad/soledadbootstrapper.py104
10 files changed, 636 insertions, 188 deletions
diff --git a/src/leap/bitmask/services/eip/conductor.py b/src/leap/bitmask/services/eip/conductor.py
new file mode 100644
index 00000000..a8821160
--- /dev/null
+++ b/src/leap/bitmask/services/eip/conductor.py
@@ -0,0 +1,321 @@
+# -*- coding: utf-8 -*-
+# conductor.py
+# Copyright (C) 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/>.
+"""
+EIP Conductor module.
+"""
+import logging
+
+from PySide import QtCore
+
+from leap.bitmask.gui import statemachines
+from leap.bitmask.services import EIP_SERVICE
+from leap.bitmask.services import get_service_display_name
+from leap.bitmask.services.eip.connection import EIPConnection
+from leap.bitmask.platform_init import IS_MAC
+
+QtDelayedCall = QtCore.QTimer.singleShot
+logger = logging.getLogger(__name__)
+
+
+class EIPConductor(object):
+
+ def __init__(self, settings, backend, **kwargs):
+ """
+ Initializes EIP Conductor.
+
+ :param settings:
+ :type settings:
+
+ :param backend:
+ :type backend:
+ """
+ self.eip_connection = EIPConnection()
+ self.eip_name = get_service_display_name(EIP_SERVICE)
+ self._settings = settings
+ self._backend = backend
+
+ self._eip_status = None
+
+ @property
+ def qtsigs(self):
+ return self.eip_connection.qtsigs
+
+ def add_eip_widget(self, widget):
+ """
+ Keep a reference to the passed eip status widget.
+
+ :param widget: the EIP Status widget.
+ :type widget: QWidget
+ """
+ self._eip_status = widget
+
+ def connect_signals(self):
+ """
+ Connect signals.
+ """
+ self.qtsigs.connecting_signal.connect(self._start_eip)
+
+ self.qtsigs.disconnecting_signal.connect(self._stop_eip)
+ self.qtsigs.disconnected_signal.connect(self._eip_status.eip_stopped)
+
+ def connect_backend_signals(self):
+ """
+ Connect to backend signals.
+ """
+ signaler = self._backend.signaler
+
+ # for conductor
+ signaler.eip_process_restart_tls.connect(self._do_eip_restart)
+ signaler.eip_process_restart_tls.connect(self._do_eip_failed)
+ signaler.eip_process_restart_ping.connect(self._do_eip_restart)
+ signaler.eip_process_finished.connect(self._eip_finished)
+
+ # for widget
+ self._eip_status.connect_backend_signals()
+
+ def start_eip_machine(self, action):
+ """
+ Initializes and starts the EIP state machine.
+ Needs the reference to the eip_status widget not to be empty.
+
+ :action: QtAction
+ """
+ action = action
+ button = self._eip_status.eip_button
+ label = self._eip_status.eip_label
+
+ builder = statemachines.ConnectionMachineBuilder(self.eip_connection)
+ eip_machine = builder.make_machine(button=button,
+ action=action,
+ label=label)
+ self.eip_machine = eip_machine
+ self.eip_machine.start()
+ logger.debug('eip machine started')
+
+ def do_connect(self):
+ """
+ Start the connection procedure.
+ Emits a signal that triggers the OFF -> Connecting sequence.
+ This will call _start_eip via the state machine.
+ """
+ self.qtsigs.do_connect_signal.emit()
+
+ def tear_fw_down(self):
+ """
+ Tear the firewall down.
+ """
+ self._backend.tear_fw_down()
+
+ @QtCore.Slot()
+ def _start_eip(self):
+ """
+ Starts EIP.
+ """
+ st = self._eip_status
+ is_restart = st and st.is_restart
+
+ def reconnect():
+ self.qtsigs.disconnecting_signal.connect(self._stop_eip)
+
+ if is_restart:
+ QtDelayedCall(0, reconnect)
+ else:
+ self._eip_status.eip_pre_up()
+ self.user_stopped_eip = False
+ self._eip_status.hide_fw_down_button()
+
+ # Until we set an option in the preferences window, we'll assume that
+ # by default we try to autostart. If we switch it off manually, it
+ # won't try the next time.
+ self._settings.set_autostart_eip(True)
+ self._eip_status.is_restart = False
+
+ # DO the backend call!
+ self._backend.eip_start(restart=is_restart)
+
+ def reconnect_stop_signal(self):
+ """
+ Restore the original behaviour associated with the disconnecting
+ signal, this is, trigger a normal stop, and not a restart one.
+ """
+
+ def do_stop(*args):
+ self._stop_eip(restart=False)
+
+ self.qtsigs.disconnecting_signal.disconnect()
+ self.qtsigs.disconnecting_signal.connect(do_stop)
+
+ @QtCore.Slot()
+ def _stop_eip(self, restart=False, failed=False):
+ """
+ TRIGGERS:
+ self.qsigs.do_disconnect_signal (via state machine)
+
+ Stops vpn process and makes gui adjustments to reflect
+ the change of state.
+
+ :param restart: whether this is part of a eip restart.
+ :type restart: bool
+
+ :param failed: whether this is the final step of a retry sequence
+ :type failed: bool
+ """
+ self._eip_status.is_restart = restart
+ self.user_stopped_eip = not restart and not failed
+
+ def on_disconnected_do_restart():
+ # hard restarts
+ logger.debug("HARD RESTART")
+ eip_status_label = self._eip_status.tr("{0} is restarting")
+ eip_status_label = eip_status_label.format(self.eip_name)
+ self._eip_status.eip_stopped(restart=True)
+ self._eip_status.set_eip_status(eip_status_label, error=False)
+
+ QtDelayedCall(2000, self.do_connect)
+
+ def plug_restart_on_disconnected():
+ self.qtsigs.disconnected_signal.connect(on_disconnected_do_restart)
+
+ def reconnect_disconnected_signal():
+ self.qtsigs.disconnected_signal.disconnect(
+ on_disconnected_do_restart)
+
+ def do_stop(*args):
+ self._stop_eip(restart=False)
+
+ if restart:
+ # we bypass the on_eip_disconnected here
+ plug_restart_on_disconnected()
+ self.qtsigs.disconnected_signal.emit()
+ #QtDelayedCall(0, self.qtsigs.disconnected_signal.emit)
+ # ...and reconnect the original signal again, after having used the
+ # diversion
+ QtDelayedCall(500, reconnect_disconnected_signal)
+
+ elif failed:
+ self.qtsigs.disconnected_signal.emit()
+
+ else:
+ logger.debug('Setting autostart to: False')
+ self._settings.set_autostart_eip(False)
+
+ # Call to the backend.
+ self._backend.eip_stop(restart=restart)
+
+ # ... and inform the status widget
+ self._eip_status.set_eipstatus_off(False)
+ self._eip_status.eip_stopped(restart=restart, failed=failed)
+
+ self._already_started_eip = False
+
+ # XXX needed?
+ if restart:
+ QtDelayedCall(2000, self.reconnect_stop_signal)
+
+ @QtCore.Slot()
+ def _do_eip_restart(self):
+ """
+ TRIGGERS:
+ self._eip_connection.qtsigs.process_restart
+
+ Restart the connection.
+ """
+ if self._eip_status is not None:
+ self._eip_status.is_restart = True
+
+ def do_stop(*args):
+ self._stop_eip(restart=True)
+
+ try:
+ self.qtsigs.disconnecting_signal.disconnect()
+ except Exception:
+ logger.error("cannot disconnect signals")
+
+ self.qtsigs.disconnecting_signal.connect(do_stop)
+ self.qtsigs.do_disconnect_signal.emit()
+
+ @QtCore.Slot()
+ def _do_eip_failed(self):
+ """
+ Stop EIP after a failure to start.
+
+ TRIGGERS
+ signaler.eip_process_restart_tls
+ """
+ logger.debug("TLS Error: eip_stop (failed)")
+ self.qtsigs.connection_died_signal.emit()
+ QtDelayedCall(1000, self._eip_status.eip_failed_to_connect)
+
+ @QtCore.Slot(int)
+ def _eip_finished(self, exitCode):
+ """
+ TRIGGERS:
+ Signaler.eip_process_finished
+
+ Triggered when the EIP/VPN process finishes to set the UI
+ accordingly.
+
+ Ideally we would have the right exit code here,
+ but the use of different wrappers (pkexec, cocoasudo) swallows
+ the openvpn exit code so we get zero exit in some cases where we
+ shouldn't. As a workaround we just use a flag to indicate
+ a purposeful switch off, and mark everything else as unexpected.
+
+ :param exitCode: the exit code of the eip process.
+ :type exitCode: int
+ """
+ # TODO Add error catching to the openvpn log observer
+ # so we can have a more precise idea of which type
+ # of error did we have (server side, local problem, etc)
+
+ logger.info("VPN process finished with exitCode %s..."
+ % (exitCode,))
+
+ signal = self.qtsigs.disconnected_signal
+
+ # XXX check if these exitCodes are pkexec/cocoasudo specific
+ if exitCode in (126, 127):
+ eip_status_label = self._eip_status.tr(
+ "{0} could not be launched "
+ "because you did not authenticate properly.")
+ eip_status_label = eip_status_label.format(self.eip_name)
+ self._eip_status.set_eip_status(eip_status_label, error=True)
+ signal = self.qtsigs.connection_aborted_signal
+ self._backend.eip_terminate()
+
+ # XXX FIXME --- check exitcode is != 0 really.
+ # bitmask-root is masking the exitcode, so we might need
+ # to fix it on that side.
+ #if exitCode != 0 and not self.user_stopped_eip:
+ if not self.user_stopped_eip:
+ eip_status_label = self._eip_status.tr(
+ "{0} finished in an unexpected manner!")
+ eip_status_label = eip_status_label.format(self.eip_name)
+ self._eip_status.eip_stopped()
+ self._eip_status.set_eip_status_icon("error")
+ self._eip_status.set_eip_status(eip_status_label,
+ error=True)
+ signal = self.qtsigs.connection_died_signal
+ self._eip_status.show_fw_down_button()
+ self._eip_status.eip_failed_to_connect()
+
+ if exitCode == 0 and IS_MAC:
+ # XXX remove this warning after I fix cocoasudo.
+ logger.warning("The above exit code MIGHT BE WRONG.")
+
+ # We emit signals to trigger transitions in the state machine:
+ signal.emit()
diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py
index a03bfc44..41d75052 100644
--- a/src/leap/bitmask/services/eip/darwinvpnlauncher.py
+++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py
@@ -52,6 +52,8 @@ class DarwinVPNLauncher(VPNLauncher):
OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,)
OPENVPN_PATH_ESCAPED = "%s/Contents/Resources/openvpn" % (
INSTALL_PATH_ESCAPED,)
+ OPENVPN_BIN_PATH = "%s/Contents/Resources/%s" % (INSTALL_PATH,
+ OPENVPN_BIN)
UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,)
DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,)
diff --git a/src/leap/bitmask/services/eip/eipconfig.py b/src/leap/bitmask/services/eip/eipconfig.py
index 09a3d257..e7419b22 100644
--- a/src/leap/bitmask/services/eip/eipconfig.py
+++ b/src/leap/bitmask/services/eip/eipconfig.py
@@ -110,7 +110,7 @@ class VPNGatewaySelector(object):
def get_gateways_list(self):
"""
- Returns the existing gateways, sorted by timezone proximity.
+ Return the existing gateways, sorted by timezone proximity.
:rtype: list of tuples (location, ip)
(str, IPv4Address or IPv6Address object)
@@ -148,16 +148,36 @@ class VPNGatewaySelector(object):
def get_gateways(self):
"""
- Returns the 4 best gateways, sorted by timezone proximity.
+ Return the 4 best gateways, sorted by timezone proximity.
:rtype: list of IPv4Address or IPv6Address object.
"""
gateways = [ip for location, ip in self.get_gateways_list()][:4]
return gateways
+ def get_gateways_country_code(self):
+ """
+ Return a dict with ipaddress -> country code mapping.
+
+ :rtype: dict
+ """
+ country_codes = {}
+
+ locations = self._eipconfig.get_locations()
+ gateways = self._eipconfig.get_gateways()
+
+ for idx, gateway in enumerate(gateways):
+ gateway_location = gateway.get('location')
+
+ ip = self._eipconfig.get_gateway_ip(idx)
+ if gateway_location is not None:
+ ccode = locations[gateway['location']]['country_code']
+ country_codes[ip] = ccode
+ return country_codes
+
def _get_timezone_distance(self, offset):
'''
- Returns the distance between the local timezone and
+ Return the distance between the local timezone and
the one with offset 'offset'.
:param offset: the distance of a timezone to GMT.
@@ -179,7 +199,7 @@ class VPNGatewaySelector(object):
def _get_local_offset(self):
'''
- Returns the distance between GMT and the local timezone.
+ Return the distance between GMT and the local timezone.
:rtype: int
'''
diff --git a/src/leap/bitmask/services/eip/linuxvpnlauncher.py b/src/leap/bitmask/services/eip/linuxvpnlauncher.py
index 1f0813e0..955768d1 100644
--- a/src/leap/bitmask/services/eip/linuxvpnlauncher.py
+++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py
@@ -63,14 +63,20 @@ def _is_auth_agent_running():
:return: True if it's running, False if it's not.
:rtype: boolean
"""
+ # Note that gnome-shell does not uses a separate process for the
+ # polkit-agent, it uses a polkit-agent within its own process so we can't
+ # ps-grep a polkit process, we can ps-grep gnome-shell itself.
+
# the [x] thing is to avoid grep match itself
polkit_options = [
'ps aux | grep "polkit-[g]nome-authentication-agent-1"',
'ps aux | grep "polkit-[k]de-authentication-agent-1"',
'ps aux | grep "polkit-[m]ate-authentication-agent-1"',
- 'ps aux | grep "[l]xpolkit"'
+ 'ps aux | grep "[l]xpolkit"',
+ 'ps aux | grep "[g]nome-shell"',
]
is_running = [commands.getoutput(cmd) for cmd in polkit_options]
+
return any(is_running)
diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py
index dcb48e8a..9629afae 100644
--- a/src/leap/bitmask/services/eip/vpnlauncher.py
+++ b/src/leap/bitmask/services/eip/vpnlauncher.py
@@ -25,6 +25,7 @@ import stat
from abc import ABCMeta, abstractmethod
from functools import partial
+from leap.bitmask.config import flags
from leap.bitmask.config.leapsettings import LeapSettings
from leap.bitmask.config.providerconfig import ProviderConfig
from leap.bitmask.platform_init import IS_LINUX
@@ -122,9 +123,9 @@ class VPNLauncher(object):
leap_settings = LeapSettings()
domain = providerconfig.get_domain()
gateway_conf = leap_settings.get_selected_gateway(domain)
+ gateway_selector = VPNGatewaySelector(eipconfig)
if gateway_conf == leap_settings.GATEWAY_AUTOMATIC:
- gateway_selector = VPNGatewaySelector(eipconfig)
gateways = gateway_selector.get_gateways()
else:
gateways = [gateway_conf]
@@ -133,6 +134,12 @@ class VPNLauncher(object):
logger.error('No gateway was found!')
raise VPNLauncherException('No gateway was found!')
+ # this only works for selecting the first gateway, as we're
+ # currently doing.
+ ccodes = gateway_selector.get_gateways_country_code()
+ gateway_ccode = ccodes[gateways[0]]
+ flags.CURRENT_VPN_COUNTRY = gateway_ccode
+
logger.debug("Using gateways ips: {0}".format(', '.join(gateways)))
return gateways
diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py
index 1559ea8b..f56d464e 100644
--- a/src/leap/bitmask/services/eip/vpnprocess.py
+++ b/src/leap/bitmask/services/eip/vpnprocess.py
@@ -17,6 +17,7 @@
"""
VPN Manager, spawned in a custom processProtocol.
"""
+import commands
import logging
import os
import shutil
@@ -30,9 +31,11 @@ import psutil
try:
# psutil < 2.0.0
from psutil.error import AccessDenied as psutil_AccessDenied
+ PSUTIL_2 = False
except ImportError:
# psutil >= 2.0.0
from psutil import AccessDenied as psutil_AccessDenied
+ PSUTIL_2 = True
from leap.bitmask.config import flags
from leap.bitmask.config.providerconfig import ProviderConfig
@@ -67,7 +70,7 @@ class VPNObserver(object):
'NETWORK_UNREACHABLE': (
'Network is unreachable (code=101)',),
'PROCESS_RESTART_TLS': (
- "SIGUSR1[soft,tls-error]",),
+ "SIGTERM[soft,tls-error]",),
'PROCESS_RESTART_PING': (
"SIGTERM[soft,ping-restart]",),
'INITIALIZATION_COMPLETED': (
@@ -113,10 +116,12 @@ class VPNObserver(object):
:returns: a Signaler signal or None
:rtype: str or None
"""
+ sig = self._signaler
signals = {
- "network_unreachable": self._signaler.EIP_NETWORK_UNREACHABLE,
- "process_restart_tls": self._signaler.EIP_PROCESS_RESTART_TLS,
- "process_restart_ping": self._signaler.EIP_PROCESS_RESTART_PING,
+ "network_unreachable": sig.EIP_NETWORK_UNREACHABLE,
+ "process_restart_tls": sig.EIP_PROCESS_RESTART_TLS,
+ "process_restart_ping": sig.EIP_PROCESS_RESTART_PING,
+ "initialization_completed": sig.EIP_CONNECTED
}
return signals.get(event.lower())
@@ -178,6 +183,8 @@ class VPN(object):
kwargs['openvpn_verb'] = self._openvpn_verb
kwargs['signaler'] = self._signaler
+ restart = kwargs.pop('restart', False)
+
# start the main vpn subprocess
vpnproc = VPNProcess(*args, **kwargs)
@@ -188,8 +195,9 @@ class VPN(object):
# we try to bring the firewall up
if IS_LINUX:
gateways = vpnproc.getGateways()
- firewall_up = self._launch_firewall(gateways)
- if not firewall_up:
+ firewall_up = self._launch_firewall(gateways,
+ restart=restart)
+ if not restart and not firewall_up:
logger.error("Could not bring firewall up, "
"aborting openvpn launch.")
return
@@ -211,7 +219,7 @@ class VPN(object):
self._pollers.extend(poll_list)
self._start_pollers()
- def _launch_firewall(self, gateways):
+ def _launch_firewall(self, gateways, restart=False):
"""
Launch the firewall using the privileged wrapper.
@@ -226,11 +234,24 @@ class VPN(object):
# XXX could check that the iptables rules are in place.
BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT
- exitCode = subprocess.call(["pkexec",
- BM_ROOT, "firewall", "start"] + gateways)
+ cmd = ["pkexec", BM_ROOT, "firewall", "start"]
+ if restart:
+ cmd.append("restart")
+ exitCode = subprocess.call(cmd + gateways)
return True if exitCode is 0 else False
- def _tear_down_firewall(self):
+ def is_fw_down(self):
+ """
+ Return whether the firewall is down or not.
+
+ :rtype: bool
+ """
+ BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT
+ fw_up_cmd = "pkexec {0} firewall isup".format(BM_ROOT)
+ fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256
+ return fw_is_down()
+
+ def tear_down_firewall(self):
"""
Tear the firewall down using the privileged wrapper.
"""
@@ -254,7 +275,7 @@ class VPN(object):
# we try to tear the firewall down
if IS_LINUX and self._user_stopped:
- firewall_down = self._tear_down_firewall()
+ firewall_down = self.tear_down_firewall()
if firewall_down:
logger.debug("Firewall down")
else:
@@ -286,22 +307,28 @@ class VPN(object):
self._vpnproc.aborted = True
self._vpnproc.killProcess()
- def terminate(self, shutdown=False):
+ def terminate(self, shutdown=False, restart=False):
"""
Stops the openvpn subprocess.
Attempts to send a SIGTERM first, and after a timeout
it sends a SIGKILL.
+
+ :param shutdown: whether this is the final shutdown
+ :type shutdown: bool
+ :param restart: whether this stop is part of a hard restart.
+ :type restart: bool
"""
from twisted.internet import reactor
self._stop_pollers()
- # We assume that the only valid shutodowns are initiated
- # by an user action.
- self._user_stopped = shutdown
-
# First we try to be polite and send a SIGTERM...
- if self._vpnproc:
+ if self._vpnproc is not None:
+ # We assume that the only valid stops are initiated
+ # by an user action, not hard restarts
+ self._user_stopped = not restart
+ self._vpnproc.is_restart = restart
+
self._sentterm = True
self._vpnproc.terminate_openvpn(shutdown=shutdown)
@@ -310,13 +337,12 @@ class VPN(object):
reactor.callLater(
self.TERMINATE_WAIT, self._kill_if_left_alive)
- if shutdown:
- if IS_LINUX and self._user_stopped:
- firewall_down = self._tear_down_firewall()
- if firewall_down:
- logger.debug("Firewall down")
- else:
- logger.warning("Could not tear firewall down")
+ if IS_LINUX and self._user_stopped:
+ firewall_down = self.tear_down_firewall()
+ if firewall_down:
+ logger.debug("Firewall down")
+ else:
+ logger.warning("Could not tear firewall down")
def _start_pollers(self):
"""
@@ -676,7 +702,13 @@ class VPNManager(object):
# we need to be able to filter out arguments in the form
# --openvpn-foo, since otherwise we are shooting ourselves
# in the feet.
- if any(map(lambda s: s.find("LEAPOPENVPN") != -1, p.cmdline)):
+
+ if PSUTIL_2:
+ cmdline = p.cmdline()
+ else:
+ cmdline = p.cmdline
+ if any(map(lambda s: s.find(
+ "LEAPOPENVPN") != -1, cmdline)):
openvpn_process = p
break
except psutil_AccessDenied:
@@ -731,7 +763,7 @@ class VPNManager(object):
# However, that should be a rare case right now.
self._send_command("signal SIGTERM")
self._close_management_socket(announce=True)
- except Exception as e:
+ except (Exception, AssertionError) as e:
logger.warning("Problem trying to terminate OpenVPN: %r"
% (e,))
else:
@@ -800,6 +832,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
self._openvpn_verb = openvpn_verb
self._vpn_observer = VPNObserver(signaler)
+ self.is_restart = False
# processProtocol methods
@@ -835,7 +868,8 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
exit_code = reason.value.exitCode
if isinstance(exit_code, int):
logger.debug("processExited, status %d" % (exit_code,))
- self._signaler.signal(self._signaler.EIP_PROCESS_FINISHED, exit_code)
+ self._signaler.signal(
+ self._signaler.EIP_PROCESS_FINISHED, exit_code)
self._alive = False
def processEnded(self, reason):
diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py
index 1766a39d..98b40929 100644
--- a/src/leap/bitmask/services/mail/conductor.py
+++ b/src/leap/bitmask/services/mail/conductor.py
@@ -19,15 +19,10 @@ Mail Services Conductor
"""
import logging
-from zope.proxy import sameProxiedObjects
-
+from leap.bitmask.config import flags
from leap.bitmask.gui import statemachines
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.common.check import leap_assert
from leap.common.events import events_pb2 as leap_events
from leap.common.events import register as leap_register
@@ -44,9 +39,6 @@ class IMAPControl(object):
Initializes smtp variables.
"""
self.imap_machine = None
- self.imap_service = None
- self.imap_port = None
- self.imap_factory = None
self.imap_connection = None
leap_register(signal=leap_events.IMAP_SERVICE_STARTED,
@@ -55,10 +47,13 @@ class IMAPControl(object):
leap_register(signal=leap_events.IMAP_SERVICE_FAILED_TO_START,
callback=self._handle_imap_events,
reqcbk=lambda req, resp: None)
+ leap_register(signal=leap_events.IMAP_CLIENT_LOGIN,
+ callback=self._handle_imap_events,
+ reqcbk=lambda req, resp: None)
def set_imap_connection(self, imap_connection):
"""
- Sets the imap connection to an initialized connection.
+ Set the imap connection to an initialized connection.
:param imap_connection: an initialized imap connection
:type imap_connection: IMAPConnection instance.
@@ -67,67 +62,18 @@ class IMAPControl(object):
def start_imap_service(self):
"""
- Starts imap service.
+ Start imap service.
"""
- from leap.bitmask.config import flags
-
- logger.debug('Starting imap service')
- leap_assert(sameProxiedObjects(self._soledad, None)
- is not True,
- "We need a non-null soledad for initializing imap service")
- leap_assert(sameProxiedObjects(self._keymanager, None)
- is not True,
- "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,
- offline=offline)
+ self._backend.imap_start_service(self.userid, flags.OFFLINE)
- if offline is False:
- logger.debug("Starting loop")
- self.imap_service.start_loop()
-
- def stop_imap_service(self, cv):
+ def stop_imap_service(self):
"""
- 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
+ Stop imap service.
"""
self.imap_connection.qtsigs.disconnecting_signal.emit()
- # TODO We should homogenize both services.
- if self.imap_service is not None:
- logger.debug('Stopping imap service.')
- # Stop the loop call in the fetcher
- self.imap_service.stop()
- self.imap_service = None
- # Stop listening on the IMAP port
- self.imap_port.stopListening()
- # Stop the protocol
- 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):
- """
- Fetches incoming mail.
- """
- if self.imap_service:
- logger.debug('Client connected, fetching mail...')
- self.imap_service.fetch()
-
- # handle events
+ logger.debug('Stopping imap service.')
+
+ self._backend.imap_stop_service()
def _handle_imap_events(self, req):
"""
@@ -137,25 +83,31 @@ class IMAPControl(object):
:type req: leap.common.events.events_pb2.SignalRequest
"""
if req.event == leap_events.IMAP_SERVICE_STARTED:
- self.on_imap_connected()
+ self._on_imap_connected()
elif req.event == leap_events.IMAP_SERVICE_FAILED_TO_START:
- self.on_imap_failed()
+ self._on_imap_failed()
+ elif req.event == leap_events.IMAP_CLIENT_LOGIN:
+ self._on_mail_client_logged_in()
- # emit connection signals
+ def _on_mail_client_logged_in(self):
+ """
+ On mail client logged in, fetch incoming mail.
+ """
+ self._controller.imap_service_fetch()
- def on_imap_connecting(self):
+ def _on_imap_connecting(self):
"""
Callback for IMAP connecting state.
"""
self.imap_connection.qtsigs.connecting_signal.emit()
- def on_imap_connected(self):
+ def _on_imap_connected(self):
"""
Callback for IMAP connected state.
"""
self.imap_connection.qtsigs.connected_signal.emit()
- def on_imap_failed(self):
+ def _on_imap_failed(self):
"""
Callback for IMAP failed state.
"""
@@ -167,12 +119,9 @@ class SMTPControl(object):
"""
Initializes smtp variables.
"""
- self.smtp_config = SMTPConfig()
self.smtp_connection = None
self.smtp_machine = None
- self.smtp_bootstrapper = SMTPBootstrapper()
-
leap_register(signal=leap_events.SMTP_SERVICE_STARTED,
callback=self._handle_smtp_events,
reqcbk=lambda req, resp: None)
@@ -188,29 +137,23 @@ class SMTPControl(object):
"""
self.smtp_connection = smtp_connection
- def start_smtp_service(self, provider_config, download_if_needed=False):
+ def start_smtp_service(self, download_if_needed=False):
"""
Starts the SMTP service.
- :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
"""
self.smtp_connection.qtsigs.connecting_signal.emit()
- self.smtp_bootstrapper.start_smtp_service(
- provider_config, self.smtp_config, self._keymanager,
- self.userid, download_if_needed)
+ self._backend.smtp_start_service(self.userid, download_if_needed)
def stop_smtp_service(self):
"""
Stops the SMTP service.
"""
self.smtp_connection.qtsigs.disconnecting_signal.emit()
- self.smtp_bootstrapper.stop_smtp_service()
-
- # handle smtp events
+ self._backend.smtp_stop_service()
def _handle_smtp_events(self, req):
"""
@@ -224,8 +167,6 @@ class SMTPControl(object):
elif req.event == leap_events.SMTP_SERVICE_FAILED_TO_START:
self.on_smtp_failed()
- # emit connection signals
-
def on_smtp_connecting(self):
"""
Callback for SMTP connecting state.
@@ -253,22 +194,17 @@ class MailConductor(IMAPControl, SMTPControl):
"""
# XXX We could consider to use composition instead of inheritance here.
- def __init__(self, soledad, keymanager):
+ def __init__(self, backend):
"""
Initializes the mail conductor.
- :param soledad: a transparent proxy that eventually will point to a
- Soledad Instance.
- :type soledad: zope.proxy.ProxyBase
-
- :param keymanager: a transparent proxy that eventually will point to a
- Keymanager Instance.
- :type keymanager: zope.proxy.ProxyBase
+ :param backend: Backend being used
+ :type backend: Backend
"""
IMAPControl.__init__(self)
SMTPControl.__init__(self)
- self._soledad = soledad
- self._keymanager = keymanager
+
+ self._backend = backend
self._mail_machine = None
self._mail_connection = mail_connection.MailConnection()
@@ -309,6 +245,13 @@ class MailConductor(IMAPControl, SMTPControl):
self._smtp_machine = smtp
self._smtp_machine.start()
+ def stop_mail_services(self):
+ """
+ Stop the IMAP and SMTP services.
+ """
+ self.stop_imap_service()
+ self.stop_smtp_service()
+
def connect_mail_signals(self, widget):
"""
Connects the mail signals to the mail_status widget slots.
diff --git a/src/leap/bitmask/services/mail/imapcontroller.py b/src/leap/bitmask/services/mail/imapcontroller.py
new file mode 100644
index 00000000..d0bf4c34
--- /dev/null
+++ b/src/leap/bitmask/services/mail/imapcontroller.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# imapcontroller.py
+# Copyright (C) 2013 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/>.
+"""
+IMAP service controller.
+"""
+import logging
+
+from leap.bitmask.services.mail import imap
+
+
+logger = logging.getLogger(__name__)
+
+
+class IMAPController(object):
+ """
+ IMAP Controller.
+ """
+ def __init__(self, soledad, keymanager):
+ """
+ Initialize IMAP variables.
+
+ :param soledad: a transparent proxy that eventually will point to a
+ Soledad Instance.
+ :type soledad: zope.proxy.ProxyBase
+ :param keymanager: a transparent proxy that eventually will point to a
+ Keymanager Instance.
+ :type keymanager: zope.proxy.ProxyBase
+ """
+ self._soledad = soledad
+ self._keymanager = keymanager
+
+ self.imap_service = None
+ self.imap_port = None
+ self.imap_factory = None
+
+ def start_imap_service(self, userid, offline=False):
+ """
+ Start IMAP service.
+
+ :param userid: user id, in the form "user@provider"
+ :type userid: str
+ :param offline: whether imap should start in offline mode or not.
+ :type offline: bool
+ """
+ logger.debug('Starting imap service')
+
+ self.imap_service, self.imap_port, \
+ self.imap_factory = imap.start_imap_service(
+ self._soledad,
+ self._keymanager,
+ userid=userid,
+ offline=offline)
+
+ if offline is False:
+ logger.debug("Starting loop")
+ self.imap_service.start_loop()
+
+ def stop_imap_service(self, cv):
+ """
+ Stop IMAP service (fetcher, factory and port).
+
+ :param cv: A condition variable to which we can signal when imap
+ indeed stops.
+ :type cv: threading.Condition
+ """
+ if self.imap_service is not None:
+ # Stop the loop call in the fetcher
+ self.imap_service.stop()
+ self.imap_service = None
+
+ # Stop listening on the IMAP port
+ self.imap_port.stopListening()
+
+ # Stop the protocol
+ self.imap_factory.theAccount.closed = True
+ self.imap_factory.doStop(cv)
+ else:
+ # Release the condition variable so the caller doesn't have to wait
+ cv.acquire()
+ cv.notify()
+ cv.release()
+
+ def fetch_incoming_mail(self):
+ """
+ Fetch incoming mail.
+ """
+ if self.imap_service:
+ logger.debug('Client connected, fetching mail...')
+ self.imap_service.fetch()
diff --git a/src/leap/bitmask/services/mail/smtpbootstrapper.py b/src/leap/bitmask/services/mail/smtpbootstrapper.py
index 7ecf8134..3ef755e8 100644
--- a/src/leap/bitmask/services/mail/smtpbootstrapper.py
+++ b/src/leap/bitmask/services/mail/smtpbootstrapper.py
@@ -28,7 +28,7 @@ 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.check import leap_assert
from leap.common.files import check_and_fix_urw_only
logger = logging.getLogger(__name__)
@@ -38,6 +38,10 @@ class NoSMTPHosts(Exception):
"""This is raised when there is no SMTP host to use."""
+class MalformedUserId(Exception):
+ """This is raised when an userid does not have the form user@provider."""
+
+
class SMTPBootstrapper(AbstractBootstrapper):
"""
SMTP init procedure
@@ -126,15 +130,10 @@ class SMTPBootstrapper(AbstractBootstrapper):
smtp_key=client_cert_path,
encrypted_only=False)
- def start_smtp_service(self, provider_config, smtp_config, keymanager,
- userid, download_if_needed=False):
+ def start_smtp_service(self, keymanager, userid, download_if_needed=False):
"""
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
@@ -144,13 +143,16 @@ class SMTPBootstrapper(AbstractBootstrapper):
for the file
:type download_if_needed: bool
"""
- leap_assert_type(provider_config, ProviderConfig)
- leap_assert_type(smtp_config, SMTPConfig)
+ try:
+ username, domain = userid.split('@')
+ except ValueError:
+ logger.critical("Malformed userid parameter!")
+ raise MalformedUserId()
- self._provider_config = provider_config
+ self._provider_config = ProviderConfig.get_provider_config(domain)
self._keymanager = keymanager
- self._smtp_config = smtp_config
- self._useid = userid
+ self._smtp_config = SMTPConfig()
+ self._userid = userid
self._download_if_needed = download_if_needed
try:
diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py
index 6bb7c036..db12fd80 100644
--- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py
+++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py
@@ -25,7 +25,6 @@ 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
@@ -134,16 +133,11 @@ class SoledadBootstrapper(AbstractBootstrapper):
MAX_INIT_RETRIES = 10
MAX_SYNC_RETRIES = 10
- # All dicts returned are of the form
- # {"passed": bool, "error": str}
- download_config = QtCore.Signal(dict)
- gen_key = QtCore.Signal(dict)
- local_only_ready = QtCore.Signal(dict)
- soledad_invalid_auth_token = QtCore.Signal()
- soledad_failed = QtCore.Signal()
+ def __init__(self, signaler=None):
+ AbstractBootstrapper.__init__(self, signaler)
- def __init__(self):
- AbstractBootstrapper.__init__(self)
+ if signaler is not None:
+ self._cancel_signal = signaler.SOLEDAD_CANCELLED_BOOTSTRAP
self._provider_config = None
self._soledad_config = None
@@ -181,16 +175,23 @@ class SoledadBootstrapper(AbstractBootstrapper):
Instantiate Soledad for offline use.
:param username: full user id (user@provider)
- :type username: basestring
+ :type username: str or unicode
:param password: the soledad passphrase
:type password: unicode
:param uuid: the user uuid
- :type uuid: basestring
+ :type uuid: str or unicode
"""
print "UUID ", uuid
self._address = username
+ self._password = password
self._uuid = uuid
- return self.load_and_sync_soledad(uuid, offline=True)
+ try:
+ self.load_and_sync_soledad(uuid, offline=True)
+ self._signaler.signal(self._signaler.SOLEDAD_OFFLINE_FINISHED)
+ except Exception as e:
+ # TODO: we should handle more specific exceptions in here
+ logger.exception(e)
+ self._signaler.signal(self._signaler.SOLEDAD_OFFLINE_FAILED)
def _get_soledad_local_params(self, uuid, offline=False):
"""
@@ -245,7 +246,7 @@ class SoledadBootstrapper(AbstractBootstrapper):
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
+ Initialize soledad, retry if necessary and raise an exception if we
can't succeed.
:param uuid: user identifier
@@ -263,19 +264,22 @@ class SoledadBootstrapper(AbstractBootstrapper):
:param auth token: auth token
:type auth_token: str
"""
- init_tries = self.MAX_INIT_RETRIES
- while init_tries > 0:
+ init_tries = 1
+ while init_tries <= self.MAX_INIT_RETRIES:
try:
+ logger.debug("Trying to init soledad....")
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
+ init_tries += 1
+ msg = "Init failed, retrying... (retry {0} of {1})".format(
+ init_tries, self.MAX_INIT_RETRIES)
+ logger.warning(msg)
continue
- self.soledad_failed.emit()
raise SoledadInitError()
def load_and_sync_soledad(self, uuid=None, offline=False):
@@ -306,9 +310,8 @@ class SoledadBootstrapper(AbstractBootstrapper):
leap_assert(not sameProxiedObjects(self._soledad, None),
"Null soledad, error while initializing")
- if flags.OFFLINE is True:
+ if flags.OFFLINE:
self._init_keymanager(self._address, token)
- self.local_only_ready.emit({self.PASSED_KEY: True})
else:
try:
address = make_address(
@@ -353,9 +356,10 @@ class SoledadBootstrapper(AbstractBootstrapper):
Do several retries to get an initial soledad sync.
"""
# and now, let's sync
- sync_tries = self.MAX_SYNC_RETRIES
- while sync_tries > 0:
+ sync_tries = 1
+ while sync_tries <= self.MAX_SYNC_RETRIES:
try:
+ logger.debug("Trying to sync soledad....")
self._try_soledad_sync()
logger.debug("Soledad has been synced.")
# so long, and thanks for all the fish
@@ -368,19 +372,20 @@ class SoledadBootstrapper(AbstractBootstrapper):
# retry strategy can be pushed to u1db, or at least
# it's something worthy to talk about with the
# ubuntu folks.
- sync_tries -= 1
+ sync_tries += 1
+ msg = "Sync failed, retrying... (retry {0} of {1})".format(
+ sync_tries, self.MAX_SYNC_RETRIES)
+ logger.warning(msg)
continue
except InvalidAuthTokenError:
- self.soledad_invalid_auth_token.emit()
+ self._signaler.signal(
+ self._signaler.SOLEDAD_INVALID_AUTH_TOKEN)
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...
- self.soledad_failed.emit()
raise SoledadSyncError()
def _try_soledad_init(self, uuid, secrets_path, local_db_path,
@@ -443,7 +448,6 @@ class SoledadBootstrapper(AbstractBootstrapper):
Raises SoledadSyncError if not successful.
"""
try:
- logger.debug("trying to sync soledad....")
self._soledad.sync()
except SSLError as exc:
logger.error("%r" % (exc,))
@@ -467,7 +471,6 @@ class SoledadBootstrapper(AbstractBootstrapper):
"""
Download the Soledad config for the given provider
"""
-
leap_assert(self._provider_config,
"We need a provider configuration!")
logger.debug("Downloading Soledad config for %s" %
@@ -480,14 +483,6 @@ class SoledadBootstrapper(AbstractBootstrapper):
self._session,
self._download_if_needed)
- # soledad config is ok, let's proceed to load and sync soledad
- # 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
-
- uuid = self.srpauth.get_uuid()
- self.load_and_sync_soledad(uuid)
-
def _get_gpg_bin_path(self):
"""
Return the path to gpg binary.
@@ -574,7 +569,7 @@ class SoledadBootstrapper(AbstractBootstrapper):
logger.exception(exc)
# but we do not raise
- def _gen_key(self, _):
+ def _gen_key(self):
"""
Generates the key pair if needed, uploads it to the webapp and
nickserver
@@ -613,10 +608,7 @@ class SoledadBootstrapper(AbstractBootstrapper):
logger.debug("Key generated successfully.")
- def run_soledad_setup_checks(self,
- provider_config,
- user,
- password,
+ def run_soledad_setup_checks(self, provider_config, user, password,
download_if_needed=False):
"""
Starts the checks needed for a new soledad setup
@@ -640,9 +632,27 @@ class SoledadBootstrapper(AbstractBootstrapper):
self._user = user
self._password = password
- cb_chain = [
- (self._download_config, self.download_config),
- (self._gen_key, self.gen_key)
- ]
+ if flags.OFFLINE:
+ signal_finished = self._signaler.SOLEDAD_OFFLINE_FINISHED
+ signal_failed = self._signaler.SOLEDAD_OFFLINE_FAILED
+ else:
+ signal_finished = self._signaler.SOLEDAD_BOOTSTRAP_FINISHED
+ signal_failed = self._signaler.SOLEDAD_BOOTSTRAP_FAILED
- return self.addCallbackChain(cb_chain)
+ try:
+ self._download_config()
+
+ # soledad config is ok, let's proceed to load and sync soledad
+ uuid = self.srpauth.get_uuid()
+ self.load_and_sync_soledad(uuid)
+
+ if not flags.OFFLINE:
+ self._gen_key()
+
+ self._signaler.signal(signal_finished)
+ except Exception as e:
+ # TODO: we should handle more specific exceptions in here
+ self._soledad = None
+ self._keymanager = None
+ logger.exception("Error while bootstrapping Soledad: %r" % (e, ))
+ self._signaler.signal(signal_failed)