summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKali Kaneko <kali@leap.se>2013-10-06 21:50:01 -0400
committerKali Kaneko <kali@leap.se>2013-10-08 20:21:54 -0300
commitac51cb85da434b8dfc75ffa800b6d5cbaab1c84a (patch)
tree81358b40727747ea61bab22c5019aea8345109eb
parentf1ca78e438f46d266ddcb8f5795cfc4bae4469e8 (diff)
openvpn observer
reacts to tls-restart, ping-restart and network unreachable.
-rw-r--r--src/leap/bitmask/gui/mainwindow.py299
-rw-r--r--src/leap/bitmask/services/eip/connection.py1
-rw-r--r--src/leap/bitmask/services/eip/vpnlauncher.py4
-rw-r--r--src/leap/bitmask/services/eip/vpnprocess.py97
4 files changed, 281 insertions, 120 deletions
diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py
index dd625f52..0d0c6339 100644
--- a/src/leap/bitmask/gui/mainwindow.py
+++ b/src/leap/bitmask/gui/mainwindow.py
@@ -174,10 +174,12 @@ class MainWindow(QtGui.QMainWindow):
self._eip_connection = EIPConnection()
+ # XXX this should be handled by EIP Conductor
self._eip_connection.qtsigs.connecting_signal.connect(
self._start_eip)
self._eip_connection.qtsigs.disconnecting_signal.connect(
self._stop_eip)
+
self._eip_status.eip_connection_connected.connect(
self._on_eip_connected)
self.eip_needs_login.connect(
@@ -228,13 +230,22 @@ class MainWindow(QtGui.QMainWindow):
self._eip_intermediate_stage)
self._eip_bootstrapper.download_client_certificate.connect(
self._finish_eip_bootstrap)
+
self._vpn = VPN(openvpn_verb=openvpn_verb)
+
+ # connect vpn process signals
self._vpn.qtsigs.state_changed.connect(
self._eip_status.update_vpn_state)
self._vpn.qtsigs.status_changed.connect(
self._eip_status.update_vpn_status)
self._vpn.qtsigs.process_finished.connect(
self._eip_finished)
+ self._vpn.qtsigs.network_unreachable.connect(
+ self._on_eip_network_unreachable)
+ self._vpn.qtsigs.process_restart_tls.connect(
+ self._do_eip_restart)
+ self._vpn.qtsigs.process_restart_ping.connect(
+ self._do_eip_restart)
self._soledad_bootstrapper = SoledadBootstrapper()
self._soledad_bootstrapper.download_config.connect(
@@ -267,6 +278,8 @@ class MainWindow(QtGui.QMainWindow):
self._systray = None
+ # XXX separate actions into a different
+ # module.
self._action_mail_status = QtGui.QAction(self.tr("Mail is OFF"), self)
self._mail_status.set_action_mail_status(self._action_mail_status)
@@ -398,6 +411,8 @@ class MainWindow(QtGui.QMainWindow):
:return: a logging handler or None
:rtype: LeapLogHandler or None
"""
+ # TODO this can be a function, does not need
+ # to be a method.
leap_logger = logging.getLogger('leap')
for h in leap_logger.handlers:
if isinstance(h, LeapLogHandler):
@@ -463,6 +478,10 @@ class MainWindow(QtGui.QMainWindow):
"""
self._soledad_ready = True
+ #
+ # updates
+ #
+
def _new_updates_available(self, req):
"""
Callback for the new updates event
@@ -590,43 +609,9 @@ class MainWindow(QtGui.QMainWindow):
saved_password.decode("utf8"))
self._login()
- def _try_autostart_eip(self):
- """
- Tries to autostart EIP
- """
- settings = self._settings
-
- should_autostart = settings.get_autostart_eip()
- if not should_autostart:
- logger.debug('Will not autostart EIP since it is setup '
- 'to not to do it')
- self.eip_needs_login.emit()
- return
-
- default_provider = settings.get_defaultprovider()
-
- if default_provider is None:
- logger.info("Cannot autostart Encrypted Internet because there is "
- "no default provider configured")
- self.eip_needs_login.emit()
- return
-
- self._enabled_services = settings.get_enabled_services(
- default_provider)
-
- loaded = self._provisional_provider_config.load(
- provider.get_provider_path(default_provider))
- if loaded:
- # 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._download_eip_config()
- else:
- # XXX: Display a proper message to the user
- self.eip_needs_login.emit()
- logger.error("Unable to load %s config, cannot autostart." %
- (default_provider,))
+ #
+ # systray
+ #
def _show_systray(self):
"""
@@ -970,7 +955,11 @@ class MainWindow(QtGui.QMainWindow):
self._download_eip_config()
+ ###################################################################
+ # Service control methods: soledad
+
def _soledad_intermediate_stage(self, data):
+ # TODO missing param docstring
"""
SLOT
TRIGGERS:
@@ -1221,16 +1210,53 @@ class MainWindow(QtGui.QMainWindow):
signal that currently is beeing processed under status_panel.
After the refactor to EIPConductor this should not be necessary.
"""
- logger.debug('EIP connected signal received ...')
self._eip_connection.qtsigs.connected_signal.emit()
+ def _try_autostart_eip(self):
+ """
+ Tries to autostart EIP
+ """
+ settings = self._settings
+
+ should_autostart = settings.get_autostart_eip()
+ if not should_autostart:
+ logger.debug('Will not autostart EIP since it is setup '
+ 'to not to do it')
+ self.eip_needs_login.emit()
+ return
+
+ default_provider = settings.get_defaultprovider()
+
+ if default_provider is None:
+ logger.info("Cannot autostart Encrypted Internet because there is "
+ "no default provider configured")
+ self.eip_needs_login.emit()
+ return
+
+ self._enabled_services = settings.get_enabled_services(
+ default_provider)
+
+ loaded = self._provisional_provider_config.load(
+ provider.get_provider_path(default_provider))
+ if loaded:
+ # 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._download_eip_config()
+ else:
+ # XXX: Display a proper message to the user
+ self.eip_needs_login.emit()
+ logger.error("Unable to load %s config, cannot autostart." %
+ (default_provider,))
+
@QtCore.Slot()
def _start_eip(self):
"""
SLOT
TRIGGERS:
- self._eip_status.start_eip
- self._action_eip_startstop.triggered
+ self._eip_connection.qtsigs.do_connect_signal
+ (via state machine)
or called from _finish_eip_bootstrap
Starts EIP
@@ -1331,8 +1357,8 @@ class MainWindow(QtGui.QMainWindow):
"""
SLOT
TRIGGERS:
- self._eip_status.stop_eip
- self._action_eip_startstop.triggered
+ self._eip_connection.qtsigs.do_disconnect_signal
+ (via state machine)
or called from _eip_finished
Stops vpn process and makes gui adjustments to reflect
@@ -1356,14 +1382,100 @@ class MainWindow(QtGui.QMainWindow):
self._get_best_provider_config().get_domain()))
self._eip_status.eip_stopped()
+ @QtCore.Slot()
+ def _on_eip_network_unreachable(self):
+ # XXX Should move to EIP Conductor
+ """
+ SLOT
+ TRIGGERS:
+ self._eip_connection.qtsigs.network_unreachable
+
+ Displays a "network unreachable" error in the EIP status panel.
+ """
+ self._eip_status.set_eip_status(self.tr("Network is unreachable"),
+ error=True)
+ self._eip_status.set_eip_status_icon("error")
+
+ @QtCore.Slot()
+ def _do_eip_restart(self):
+ # XXX Should move to EIP Conductor
+ """
+ SLOT
+ self._eip_connection.qtsigs.process_restart
+
+ Restart the connection.
+ """
+ # for some reason, emitting the do_disconnect/do_connect
+ # signals hangs the UI.
+ self._stop_eip()
+ QtCore.QTimer.singleShot(2000, self._start_eip)
+
def _set_eipstatus_off(self, error=True):
"""
Sets eip status to off
"""
+ # XXX this should be handled by the state machine.
self._eip_status.set_eip_status(self.tr("EIP has stopped"),
error=error)
self._eip_status.set_eip_status_icon("error")
+ def _eip_finished(self, exitCode):
+ """
+ SLOT
+ TRIGGERS:
+ self._vpn.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.
+
+ In the near future we should trigger a native notification from here,
+ since the user really really wants to know she is unprotected asap.
+ And the right thing to do will be to fail-close.
+
+ :param exitCode: the exit code of the eip process.
+ :type exitCode: int
+ """
+ # TODO move to EIPConductor.
+ # 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,))
+
+ qtsigs = self._eip_connection.qtsigs
+ signal = qtsigs.disconnected_signal
+
+ # XXX check if these exitCodes are pkexec/cocoasudo specific
+ if exitCode in (126, 127):
+ self._eip_status.set_eip_status(
+ self.tr("Encrypted Internet could not be launched "
+ "because you did not authenticate properly."),
+ error=True)
+ self._vpn.killit()
+ signal = qtsigs.connection_aborted_signal
+
+ elif exitCode != 0 or not self.user_stopped_eip:
+ self._eip_status.set_eip_status(
+ self.tr("Encrypted Internet finished in an "
+ "unexpected manner!"), error=True)
+ signal = qtsigs.connection_died_signal
+
+ 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()
+
+ # eip boostrapping, config etc...
+
def _download_eip_config(self):
"""
Starts the EIP bootstrapping sequence
@@ -1425,7 +1537,25 @@ class MainWindow(QtGui.QMainWindow):
"Configuration."),
error=True)
- # end eip methods -------------------------------------------
+ def _eip_intermediate_stage(self, data):
+ # TODO missing param
+ """
+ SLOT
+ TRIGGERS:
+ self._eip_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.
+ """
+ passed = data[self._provider_bootstrapper.PASSED_KEY]
+ if not passed:
+ self._login_widget.set_status(
+ self.tr("Unable to connect: Problem with provider"))
+ logger.error(data[self._provider_bootstrapper.ERROR_KEY])
+ self._already_started_eip = False
+
+ # end of EIP methods ---------------------------------------------
def _get_best_provider_config(self):
"""
@@ -1468,6 +1598,7 @@ class MainWindow(QtGui.QMainWindow):
self.logout.emit()
def _done_logging_out(self, ok, message):
+ # TODO missing params in docstring
"""
SLOT
TRIGGER: self._srp_auth.logout_finished
@@ -1488,6 +1619,7 @@ class MainWindow(QtGui.QMainWindow):
error=True)
def _intermediate_stage(self, data):
+ # TODO this method name is confusing as hell.
"""
SLOT
TRIGGERS:
@@ -1507,80 +1639,9 @@ class MainWindow(QtGui.QMainWindow):
self.tr("Unable to connect: Problem with provider"))
logger.error(data[self._provider_bootstrapper.ERROR_KEY])
- def _eip_intermediate_stage(self, data):
- """
- SLOT
- TRIGGERS:
- self._eip_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.
- """
- passed = data[self._provider_bootstrapper.PASSED_KEY]
- if not passed:
- self._login_widget.set_status(
- self.tr("Unable to connect: Problem with provider"))
- logger.error(data[self._provider_bootstrapper.ERROR_KEY])
- self._already_started_eip = False
-
- def _eip_finished(self, exitCode):
- """
- SLOT
- TRIGGERS:
- self._vpn.process_finished
-
- Triggered when the EIP/VPN process finishes to set the UI
- accordingly.
- """
- # TODO move to EIPConductor.
- logger.info("VPN process finished with exitCode %s..."
- % (exitCode,))
-
- # 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.
-
- # In the near future we should trigger a native notification from here,
- # since the user really really wants to know she is unprotected asap.
- # And the right thing to do will be to fail-close.
-
- # TODO we should have a way of parsing the latest lines in the vpn
- # log buffer so we can have a more precise idea of which type
- # of error did we have (server side, local problem, etc)
-
- qtsigs = self._eip_connection.qtsigs
- signal = qtsigs.disconnected_signal
-
- # XXX check if these exitCodes are pkexec/cocoasudo specific
- if exitCode in (126, 127):
- self._eip_status.set_eip_status(
- self.tr("Encrypted Internet could not be launched "
- "because you did not authenticate properly."),
- error=True)
- self._vpn.killit()
- signal = qtsigs.connection_aborted_signal
-
- elif exitCode != 0 or not self.user_stopped_eip:
- self._eip_status.set_eip_status(
- self.tr("Encrypted Internet finished in an "
- "unexpected manner!"), error=True)
- signal = qtsigs.connection_died_signal
-
- if exitCode == 0 and IS_MAC:
- # XXX remove this warning after I fix cocoasudo.
- logger.warning("The above exit code MIGHT BE WRONG.")
-
- # XXX verify that the logic kees the same w/o the abnormal flag
- # after the refactor to EIPConnection has been completed
- # (eipconductor taking the most of the logic under transitions
- # that right now are handled under status_panel)
- #self._stop_eip(abnormal)
-
- # We emit signals to trigger transitions in the state machine:
- signal.emit()
+ #
+ # window handling methods
+ #
def _on_raise_window_event(self, req):
"""
@@ -1606,6 +1667,10 @@ class MainWindow(QtGui.QMainWindow):
if IS_MAC:
self.raise_()
+ #
+ # cleanup and quit methods
+ #
+
def _cleanup_pidfiles(self):
"""
Removes lockfiles on a clean shutdown.
diff --git a/src/leap/bitmask/services/eip/connection.py b/src/leap/bitmask/services/eip/connection.py
index 08b29070..962d9cf2 100644
--- a/src/leap/bitmask/services/eip/connection.py
+++ b/src/leap/bitmask/services/eip/connection.py
@@ -46,4 +46,5 @@ class EIPConnectionSignals(QtCore.QObject):
class EIPConnection(AbstractLEAPConnection):
def __init__(self):
+ # XXX this should be public instead
self._qtsigs = EIPConnectionSignals()
diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py
index 935d75f1..82d8ea48 100644
--- a/src/leap/bitmask/services/eip/vpnlauncher.py
+++ b/src/leap/bitmask/services/eip/vpnlauncher.py
@@ -241,6 +241,10 @@ class VPNLauncher(object):
'--ca', providerconfig.get_ca_cert_path()
]
+ args += [
+ '--ping', '10',
+ '--ping-restart', '30']
+
command_and_args = [openvpn] + args
logger.debug("Running VPN with command:")
logger.debug(" ".join(command_and_args))
diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py
index 707967e0..9baa4c53 100644
--- a/src/leap/bitmask/services/eip/vpnprocess.py
+++ b/src/leap/bitmask/services/eip/vpnprocess.py
@@ -24,6 +24,8 @@ import psutil.error
import shutil
import socket
+from itertools import chain, repeat
+
from PySide import QtCore
from leap.bitmask.config.providerconfig import ProviderConfig
@@ -50,14 +52,93 @@ class VPNSignals(QtCore.QObject):
They are instantiated in the VPN object and passed along
till the VPNProcess.
"""
+ # signals for the process
state_changed = QtCore.Signal(dict)
status_changed = QtCore.Signal(dict)
process_finished = QtCore.Signal(int)
+ # signals that come from parsing
+ # openvpn output
+ network_unreachable = QtCore.Signal()
+ process_restart_tls = QtCore.Signal()
+ process_restart_ping = QtCore.Signal()
+
def __init__(self):
QtCore.QObject.__init__(self)
+class VPNObserver(object):
+ """
+ A class containing different patterns in the openvpn output that
+ we can react upon.
+ """
+
+ # TODO this is i18n-sensitive, right?
+ # in that case, we should add the translations :/
+ # until we find something better.
+
+ _events = {
+ 'NETWORK_UNREACHABLE': (
+ 'Network is unreachable (code=101)',),
+ 'PROCESS_RESTART_TLS': (
+ "SIGUSR1[soft,tls-error]",),
+ 'PROCESS_RESTART_PING': (
+ "SIGUSR1[soft,ping-restart]",),
+ 'INITIALIZATION_COMPLETED': (
+ "Initialization Sequence Completed",),
+ }
+
+ def __init__(self, qtsigs):
+ """
+ Initializer. Keeps a reference to the passed qtsigs object
+ :param qtsigs: an object containing the different qt signals to
+ be used to communicate with different parts of
+ the application (the EIP state machine, for instance).
+ """
+ self._qtsigs = qtsigs
+
+ def watch(self, line):
+ """
+ Inspects line searching for the different patterns. If a match
+ is found, try to emit the corresponding signal.
+
+ :param line: a line of openvpn output
+ :type line: str
+ """
+ chained_iter = chain(*[
+ zip(repeat(key, len(l)), l)
+ for key, l in self._events.iteritems()])
+ for event, pattern in chained_iter:
+ if pattern in line:
+ logger.debug('pattern matched! %s' % pattern)
+ break
+ else:
+ return
+
+ sig = self._get_signal(event)
+ if sig:
+ sig.emit()
+ return
+ else:
+ logger.debug(
+ 'We got %s event from openvpn output but we '
+ 'could not find a matching signal for it.'
+ % event)
+
+ def _get_signal(self, event):
+ """
+ Tries to get the matching signal from the eip signals
+ objects based on the name of the passed event (in lowercase)
+
+ :param event: the name of the event that we want to get a signal
+ for
+ :type event: str
+ :returns: a QtSignal, or None
+ :rtype: QtSignal or None
+ """
+ return getattr(self._qtsigs, event.lower(), None)
+
+
class OpenVPNAlreadyRunning(Exception):
message = ("Another openvpn instance is already running, and could "
"not be stopped.")
@@ -160,10 +241,14 @@ class VPN(object):
tries += 1
reactor.callLater(self.TERMINATE_WAIT,
self._kill_if_left_alive, tries)
+ return
# after running out of patience, we try a killProcess
logger.debug("Process did not died. Sending a SIGKILL.")
- self.killit()
+ try:
+ self.killit()
+ except OSError:
+ logger.error("Could not kill process!")
def killit(self):
"""
@@ -654,6 +739,7 @@ class VPNManager(object):
raise OpenVPNAlreadyRunning
+
class VPNProcess(protocol.ProcessProtocol, VPNManager):
"""
A ProcessProtocol class that can be used to spawn a process that will
@@ -703,8 +789,12 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
self._last_status = None
self._alive = False
+ # XXX use flags, maybe, instead of passing
+ # the parameter around.
self._openvpn_verb = openvpn_verb
+ self._vpn_observer = VPNObserver(qtsigs)
+
# processProtocol methods
def connectionMade(self):
@@ -726,8 +816,9 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
.. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa
"""
# truncate the newline
- # should send this to the logging window
- vpnlog.info(data[:-1])
+ line = data[:-1]
+ vpnlog.info(line)
+ self._vpn_observer.watch(line)
def processExited(self, reason):
"""