From ac51cb85da434b8dfc75ffa800b6d5cbaab1c84a Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Sun, 6 Oct 2013 21:50:01 -0400 Subject: openvpn observer reacts to tls-restart, ping-restart and network unreachable. --- src/leap/bitmask/gui/mainwindow.py | 299 ++++++++++++++++----------- src/leap/bitmask/services/eip/connection.py | 1 + src/leap/bitmask/services/eip/vpnlauncher.py | 4 + src/leap/bitmask/services/eip/vpnprocess.py | 97 ++++++++- 4 files changed, 281 insertions(+), 120 deletions(-) (limited to 'src/leap/bitmask') 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): """ -- cgit v1.2.3