diff options
-rw-r--r-- | src/leap/bitmask/backend.py | 14 | ||||
-rw-r--r-- | src/leap/bitmask/gui/eip_status.py | 186 | ||||
-rw-r--r-- | src/leap/bitmask/gui/mainwindow.py | 428 | ||||
-rw-r--r-- | src/leap/bitmask/gui/statemachines.py | 13 | ||||
-rw-r--r-- | src/leap/bitmask/services/eip/conductor.py | 302 | ||||
-rw-r--r-- | src/leap/bitmask/services/eip/vpnprocess.py | 17 |
6 files changed, 593 insertions, 367 deletions
diff --git a/src/leap/bitmask/backend.py b/src/leap/bitmask/backend.py index 5e22a8c4..1ab5b40d 100644 --- a/src/leap/bitmask/backend.py +++ b/src/leap/bitmask/backend.py @@ -434,8 +434,12 @@ class EIP(object): except Exception as e: logger.error("Unexpected problem: {0!r}".format(e)) else: + logger.debug('EIP: no errors') # TODO: are we connected here? - signaler.signal(signaler.EIP_CONNECTED) + # kali -- no, we are not! CONNECTED should be passed only + # by the vpn observer. Currently handled by the state updater + # in eip_status + #signaler.signal(signaler.EIP_CONNECTED) def _do_stop(self, shutdown=False, restart=False): """ @@ -470,9 +474,9 @@ class EIP(object): self._signaler.signal(self._signaler.EIP_STOPPED) return else: - msg = "Firewall is not down yet, waiting... {0} of {1}" - msg = msg.format(retry, MAX_FW_WAIT_RETRIES) - logger.debug(msg) + #msg = "Firewall is not down yet, waiting... {0} of {1}" + #msg = msg.format(retry, MAX_FW_WAIT_RETRIES) + #logger.debug(msg) time.sleep(FW_WAIT_STEP) retry += 1 logger.warning("After waiting, firewall is not down... " @@ -1714,7 +1718,7 @@ class Backend(object): """ self._call_queue.put(("eip", "start", None)) - def eip_stop(self, shutdown=False, restart=False): + def eip_stop(self, shutdown=False, restart=False, failed=False): """ Stop the EIP service. diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py index ca28b8bf..04acc167 100644 --- a/src/leap/bitmask/gui/eip_status.py +++ b/src/leap/bitmask/gui/eip_status.py @@ -24,7 +24,6 @@ from functools import partial from PySide import QtCore, QtGui -from leap.bitmask.services.eip.connection import EIPConnection from leap.bitmask.services import get_service_display_name, EIP_SERVICE from leap.bitmask.platform_init import IS_LINUX from leap.bitmask.util.averages import RateMovingAverage @@ -43,9 +42,14 @@ class EIPStatusWidget(QtGui.QWidget): RATE_STR = "%1.2f KB/s" TOTAL_STR = "%1.2f Kb" - eip_connection_connected = QtCore.Signal() + def __init__(self, parent=None, eip_conductor=None): + """ + :param parent: the parent of the widget. + :type parent: QObject - def __init__(self, parent=None): + :param eip_conductor: an EIPConductor object. + :type eip_conductor: EIPConductor + """ QtGui.QWidget.__init__(self, parent) self._systray = None @@ -54,7 +58,8 @@ class EIPStatusWidget(QtGui.QWidget): self.ui = Ui_EIPStatus() self.ui.setupUi(self) - self.eipconnection = EIPConnection() + self.eip_conductor = eip_conductor + self.eipconnection = eip_conductor.eip_connection # set systray tooltip status self._eip_status = "" @@ -75,11 +80,36 @@ class EIPStatusWidget(QtGui.QWidget): self._make_status_clickable() self._provider = "" + self.is_restart = False # Action for the systray self._eip_disabled_action = QtGui.QAction( "{0} is {1}".format(self._service_name, self.tr("disabled")), self) + def connect_backend_signals(self): + """ + Connect backend signals. + """ + signaler = self.eip_conductor._backend.signaler + + signaler.eip_openvpn_already_running.connect( + self._on_eip_openvpn_already_running) + signaler.eip_alien_openvpn_already_running.connect( + self._on_eip_alien_openvpn_already_running) + signaler.eip_openvpn_not_found_error.connect( + self._on_eip_openvpn_not_found_error) + signaler.eip_vpn_launcher_exception.connect( + self._on_eip_vpn_launcher_exception) + signaler.eip_no_polkit_agent_error.connect( + self._on_eip_no_polkit_agent_error) + signaler.eip_no_pkexec_error.connect(self._on_eip_no_pkexec_error) + signaler.eip_no_tun_kext_error.connect(self._on_eip_no_tun_kext_error) + + signaler.eip_state_changed.connect(self.update_vpn_state) + signaler.eip_status_changed.connect(self.update_vpn_status) + signaler.eip_network_unreachable.connect( + self._on_eip_network_unreachable) + def _make_status_clickable(self): """ Makes upload and download figures clickable. @@ -238,7 +268,7 @@ class EIPStatusWidget(QtGui.QWidget): def eip_pre_up(self): """ Triggered when the app activates eip. - Hides the status box and disables the start/stop button. + Disables the start/stop button. """ self.set_startstop_enabled(False) @@ -248,7 +278,7 @@ class EIPStatusWidget(QtGui.QWidget): Triggered when a default provider_config has not been found. Disables the start button and adds instructions to the user. """ - #logger.debug('Hiding EIP start button') + logger.debug('Hiding EIP start button') # you might be tempted to change this for a .setEnabled(False). # it won't work. it's under the claws of the state machine. # probably the best thing would be to make a conditional @@ -326,30 +356,51 @@ class EIPStatusWidget(QtGui.QWidget): Sets the state of the widget to how it should look after EIP has started """ - self.ui.btnEipStartStop.setText(self.tr("Turn OFF")) self.ui.btnEipStartStop.disconnect(self) self.ui.btnEipStartStop.clicked.connect( self.eipconnection.qtsigs.do_connect_signal) - # XXX disable ----------------------------- - def eip_stopped(self): + @QtCore.Slot(dict) + def eip_stopped(self, restart=False, failed=False): """ + TRIGGERS: + EIPConductor.qtsigs.disconnected_signal + Sets the state of the widget to how it should look after EIP has stopped """ - # XXX should connect this to EIPConnection.disconnected_signal self._reset_traffic_rates() - # XXX disable ----------------------------- - self.ui.btnEipStartStop.setText(self.tr("Turn ON")) - self.ui.btnEipStartStop.disconnect(self) - self.ui.btnEipStartStop.clicked.connect( - self.eipconnection.qtsigs.do_disconnect_signal) - self.ui.eip_bandwidth.hide() - self.ui.lblEIPMessage.setText( - self.tr("Traffic is being routed in the clear")) + + # XXX FIXME ! ----------------------- this needs to + # accomodate the messages about firewall status. Right now + # we're assuming it works correctly, but we should test fw + # status positively. + # Or better call it from the conductor... + + clear_traffic = self.tr("Traffic is being routed in the clear.") + unreachable_net = self.tr("Network is unreachable.") + failed_msg = self.tr("Cannot start Encrypted Proxy.") + + if restart: + msg = unreachable_net + elif failed: + msg = failed_msg + else: + msg = clear_traffic + self.ui.lblEIPMessage.setText(msg) self.ui.lblEIPStatus.show() + def eip_failed_to_restart(self): + """ + Update EIP messages. + """ + msg = self.tr("Could not restart Encrypted Proxy") + self.ui.lblEIPMessage.setText(msg) + self.ui.lblEIPStatus.show() + + self.set_eip_status(self.tr("You can start the service manually.")) + @QtCore.Slot(dict) def update_vpn_status(self, data=None): """ @@ -407,7 +458,8 @@ class EIPStatusWidget(QtGui.QWidget): self.ui.lblEIPStatus.hide() # XXX should be handled by the state machine too. - self.eip_connection_connected.emit() + # --- is this currently being sent? + self.eipconnection.qtsigs.connected_signal.emit() # XXX should lookup vpn_state map in EIPConnection elif vpn_state == "AUTH": @@ -423,8 +475,9 @@ class EIPStatusWidget(QtGui.QWidget): elif vpn_state == "ALREADYRUNNING": # Put the following calls in Qt's event queue, otherwise # the UI won't update properly + #self.send_disconnect_signal() QtCore.QTimer.singleShot( - 0, self.eipconnection.qtsigs.do_disconnect_signal) + 0, self.eipconnection.qtsigns.do_disconnect_signal.emit) msg = self.tr("Unable to start VPN, it's already running.") QtCore.QTimer.singleShot(0, partial(self.set_eip_status, msg)) else: @@ -470,3 +523,96 @@ class EIPStatusWidget(QtGui.QWidget): self._provider = provider self.ui.lblEIPMessage.setText( self.tr("Route traffic through: {0}").format(self._provider)) + + # + # Slots for signals + # + + @QtCore.Slot() + def _on_eip_connection_aborted(self): + """ + TRIGGERS: + Signaler.eip_connection_aborted + """ + logger.error("Tried to start EIP but cannot find any " + "available provider!") + + eip_status_label = self.tr("Could not load {0} configuration.") + eip_status_label = eip_status_label.format( + self._eip_conductor.eip_name) + self.set_eip_status(eip_status_label, error=True) + + # signal connection_aborted to state machine: + qtsigs = self._eipconnection.qtsigs + qtsigs.connection_aborted_signal.emit() + + def _on_eip_openvpn_already_running(self): + self.set_eip_status( + self.tr("Another openvpn instance is already running, and " + "could not be stopped."), + error=True) + self.set_eipstatus_off() + + def _on_eip_alien_openvpn_already_running(self): + self.set_eip_status( + self.tr("Another openvpn instance is already running, and " + "could not be stopped because it was not launched by " + "Bitmask. Please stop it and try again."), + error=True) + self.set_eipstatus_off() + + def _on_eip_openvpn_not_found_error(self): + self.set_eip_status( + self.tr("We could not find openvpn binary."), + error=True) + self.set_eipstatus_off() + + def _on_eip_vpn_launcher_exception(self): + # XXX We should implement again translatable exceptions so + # we can pass a translatable string to the panel (usermessage attr) + self.set_eip_status("VPN Launcher error.", error=True) + self.set_eipstatus_off() + + def _on_eip_no_polkit_agent_error(self): + self.set_eip_status( + # XXX this should change to polkit-kde where + # applicable. + self.tr("We could not find any authentication agent in your " + "system.<br/>Make sure you have" + "<b>polkit-gnome-authentication-agent-1</b> running and" + "try again."), + error=True) + self.set_eipstatus_off() + + def _on_eip_no_pkexec_error(self): + self.set_eip_status( + self.tr("We could not find <b>pkexec</b> in your system."), + error=True) + self.set_eipstatus_off() + + def _on_eip_no_tun_kext_error(self): + self.set_eip_status( + self.tr("{0} cannot be started because the tuntap extension is " + "not installed properly in your " + "system.").format(self._eip_conductor.eip_name)) + self.set_eipstatus_off() + + @QtCore.Slot() + def _on_eip_network_unreachable(self): + """ + TRIGGERS: + self._eip_connection.qtsigs.network_unreachable + + Displays a "network unreachable" error in the EIP status panel. + """ + self.set_eip_status(self.tr("Network is unreachable"), + error=True) + self.set_eip_status_icon("error") + + def set_eipstatus_off(self, error=True): + # XXX this should be handled by the state machine. + """ + Sets eip status to off + """ + self.set_eip_status("", error=error) + self.set_eip_status_icon("error") diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index fd9a70ac..1d467e60 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -30,7 +30,6 @@ from leap.bitmask import __version_hash__ as VERSION_HASH from leap.bitmask.config import flags from leap.bitmask.config.leapsettings import LeapSettings -from leap.bitmask.gui import statemachines from leap.bitmask.gui.advanced_key_management import AdvancedKeyManagement from leap.bitmask.gui.eip_preferenceswindow import EIPPreferencesWindow from leap.bitmask.gui.eip_status import EIPStatusWidget @@ -46,12 +45,10 @@ from leap.bitmask.platform_init.initializers import init_platform from leap.bitmask import backend -from leap.bitmask.services import get_service_display_name - +from leap.bitmask.services.eip import conductor as eip_conductor from leap.bitmask.services.mail import conductor as mail_conductor from leap.bitmask.services import EIP_SERVICE, MX_SERVICE -from leap.bitmask.services.eip.connection import EIPConnection from leap.bitmask.util import make_address from leap.bitmask.util.keyring_helpers import has_keyring @@ -68,6 +65,7 @@ from leap.mail.imap.service.imap import IMAP_PORT from ui_mainwindow import Ui_MainWindow +QtDelayedCall = QtCore.QTimer.singleShot logger = logging.getLogger(__name__) @@ -131,11 +129,16 @@ class MainWindow(QtGui.QMainWindow): self._settings = LeapSettings() + # Login Widget self._login_widget = LoginWidget( self._settings, self) self.ui.loginLayout.addWidget(self._login_widget) + # Mail Widget + self._mail_status = MailStatusWidget(self) + self.ui.mailLayout.addWidget(self._mail_status) + # Qt Signal Connections ##################################### # TODO separate logic from ui signals. @@ -144,34 +147,29 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.show_wizard.connect(self._launch_wizard) self._login_widget.logout.connect(self._logout) - self._eip_status = EIPStatusWidget(self) - self.ui.eipLayout.addWidget(self._eip_status) - self._login_widget.logged_in_signal.connect( - self._eip_status.enable_eip_start) - self._login_widget.logged_in_signal.connect( - self._enable_eip_start_action) + # EIP Control redux ######################################### + self._eip_conductor = eip_conductor.EIPConductor( + self._settings, self._backend) + self._eip_status = EIPStatusWidget(self, self._eip_conductor) - self._mail_status = MailStatusWidget(self) - self.ui.mailLayout.addWidget(self._mail_status) - - 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.ui.eipLayout.addWidget(self._eip_status) + self._eip_conductor.add_eip_widget(self._eip_status) - self._eip_status.eip_connection_connected.connect( + self._eip_conductor.connect_signals() + self._eip_conductor.qtsigs.connected_signal.connect( self._on_eip_connection_connected) - self._eip_status.eip_connection_connected.connect( + self._eip_conductor.qtsigs.disconnected_signal.connect( + self._on_eip_connection_disconnected) + self._eip_conductor.qtsigs.connected_signal.connect( self._maybe_run_soledad_setup_checks) + self.offline_mode_bypass_login.connect( self._maybe_run_soledad_setup_checks) self.eip_needs_login.connect(self._eip_status.disable_eip_start) self.eip_needs_login.connect(self._disable_eip_start_action) + self._already_started_eip = False self._trying_to_start_eip = False self._already_started_eip = False @@ -215,8 +213,7 @@ class MainWindow(QtGui.QMainWindow): self._systray = None - # XXX separate actions into a different - # module. + # 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) @@ -280,20 +277,19 @@ class MainWindow(QtGui.QMainWindow): # the EIPConductor or some other clever component that we will # instantiate from here. - self.eip_machine = None # start event machines - self.start_eip_machine() + # TODO should encapsulate all actions into one object + self._eip_conductor.start_eip_machine( + action=self._action_eip_startstop) self._mail_conductor.start_mail_machine() - self._eip_name = get_service_display_name(EIP_SERVICE) - if self._first_run(): self._wizard_firstrun = True self._disconnect_and_untrack() self._wizard = Wizard(backend=self._backend, bypass_checks=bypass_checks) # Give this window time to finish init and then show the wizard - QtCore.QTimer.singleShot(1, self._launch_wizard) + QtDelayedCall(1, self._launch_wizard) self._wizard.accepted.connect(self._finish_init) self._wizard.rejected.connect(self._rejected_wizard) else: @@ -351,67 +347,50 @@ class MainWindow(QtGui.QMainWindow): :type only_tracked: bool """ sig = self._backend.signaler + conntrack = self._connect_and_track + auth_err = self._authentication_error - self._connect_and_track(sig.prov_name_resolution, - self._intermediate_stage) - self._connect_and_track(sig.prov_https_connection, - self._intermediate_stage) - self._connect_and_track(sig.prov_download_ca_cert, - self._intermediate_stage) - - self._connect_and_track(sig.prov_download_provider_info, - self._load_provider_config) - self._connect_and_track(sig.prov_check_api_certificate, - self._provider_config_loaded) + conntrack(sig.prov_name_resolution, self._intermediate_stage) + conntrack(sig.prov_https_connection, self._intermediate_stage) + conntrack(sig.prov_download_ca_cert, self._intermediate_stage) + conntrack(sig.prov_download_provider_info, self._load_provider_config) + conntrack(sig.prov_check_api_certificate, self._provider_config_loaded) - self._connect_and_track(sig.prov_problem_with_provider, - self._login_problem_provider) - - self._connect_and_track(sig.prov_cancelled_setup, - self._set_login_cancelled) + conntrack(sig.prov_problem_with_provider, self._login_problem_provider) + conntrack(sig.prov_cancelled_setup, self._set_login_cancelled) self._connect_and_track(sig.prov_get_details, self._provider_get_details) # Login signals - self._connect_and_track(sig.srp_auth_ok, self._authentication_finished) + conntrack(sig.srp_auth_ok, self._authentication_finished) - auth_error = ( - lambda: self._authentication_error(self.tr("Unknown error."))) - self._connect_and_track(sig.srp_auth_error, auth_error) + auth_error = lambda: auth_err(self.tr("Unknown error.")) + conntrack(sig.srp_auth_error, auth_error) - auth_server_error = ( - lambda: self._authentication_error( - self.tr("There was a server problem with authentication."))) - self._connect_and_track(sig.srp_auth_server_error, auth_server_error) + auth_server_error = lambda: auth_err(self.tr( + "There was a server problem with authentication.")) + conntrack(sig.srp_auth_server_error, auth_server_error) - auth_connection_error = ( - lambda: self._authentication_error( - self.tr("Could not establish a connection."))) - self._connect_and_track(sig.srp_auth_connection_error, - auth_connection_error) + auth_connection_error = lambda: auth_err(self.tr( + "Could not establish a connection.")) + conntrack(sig.srp_auth_connection_error, auth_connection_error) - auth_bad_user_or_password = ( - lambda: self._authentication_error( - self.tr("Invalid username or password."))) - self._connect_and_track(sig.srp_auth_bad_user_or_password, - auth_bad_user_or_password) + auth_bad_user_or_password = lambda: auth_err(self.tr( + "Invalid username or password.")) + conntrack(sig.srp_auth_bad_user_or_password, auth_bad_user_or_password) # Logout signals - self._connect_and_track(sig.srp_logout_ok, self._logout_ok) - self._connect_and_track(sig.srp_logout_error, self._logout_error) - - self._connect_and_track(sig.srp_not_logged_in_error, - self._not_logged_in_error) + conntrack(sig.srp_logout_ok, self._logout_ok) + conntrack(sig.srp_logout_error, self._logout_error) + conntrack(sig.srp_not_logged_in_error, self._not_logged_in_error) # EIP bootstrap signals - self._connect_and_track(sig.eip_config_ready, - self._eip_intermediate_stage) - self._connect_and_track(sig.eip_client_certificate_ready, - self._finish_eip_bootstrap) + conntrack(sig.eip_config_ready, self._eip_intermediate_stage) + conntrack(sig.eip_client_certificate_ready, self._finish_eip_bootstrap) ################################################### - # Add tracked signals above this, untracked bellow! + # Add tracked signals above this, untracked below! ################################################### if only_tracked: return @@ -421,38 +400,20 @@ class MainWindow(QtGui.QMainWindow): sig.backend_bad_call.connect(self._backend_bad_call) sig.prov_check_api_certificate.connect(self._get_provider_details) - sig.prov_unsupported_client.connect(self._needs_update) sig.prov_unsupported_api.connect(self._incompatible_api) + sig.prov_get_all_services.connect(self._provider_get_all_services) - sig.prov_get_all_services.connect( - self._provider_get_all_services) - - # EIP start signals - sig.eip_openvpn_already_running.connect( - self._on_eip_openvpn_already_running) - sig.eip_alien_openvpn_already_running.connect( - self._on_eip_alien_openvpn_already_running) - sig.eip_openvpn_not_found_error.connect( - self._on_eip_openvpn_not_found_error) - sig.eip_vpn_launcher_exception.connect( - self._on_eip_vpn_launcher_exception) - sig.eip_no_polkit_agent_error.connect( - self._on_eip_no_polkit_agent_error) - sig.eip_no_pkexec_error.connect(self._on_eip_no_pkexec_error) - sig.eip_no_tun_kext_error.connect(self._on_eip_no_tun_kext_error) - - sig.eip_state_changed.connect(self._eip_status.update_vpn_state) - sig.eip_status_changed.connect(self._eip_status.update_vpn_status) - sig.eip_process_finished.connect(self._eip_finished) - sig.eip_network_unreachable.connect(self._on_eip_network_unreachable) - sig.eip_process_restart_tls.connect(self._do_eip_restart) - sig.eip_process_restart_ping.connect(self._do_eip_restart) + # EIP start signals ============================================== + self._eip_conductor.connect_backend_signals() sig.eip_can_start.connect(self._backend_can_start_eip) sig.eip_cannot_start.connect(self._backend_cannot_start_eip) + # ================================================================== + # Soledad signals + # TODO delegate connection to soledad bootstrapper sig.soledad_bootstrap_failed.connect( self._mail_status.set_soledad_failed) sig.soledad_bootstrap_finished.connect(self._on_soledad_ready) @@ -919,7 +880,8 @@ class MainWindow(QtGui.QMainWindow): systrayMenu.addAction(self._action_visible) systrayMenu.addSeparator() - eip_status_label = "{0}: {1}".format(self._eip_name, self.tr("OFF")) + eip_status_label = "{0}: {1}".format( + self._eip_conductor.eip_name, self.tr("OFF")) self._eip_menu = eip_menu = systrayMenu.addMenu(eip_status_label) eip_menu.addAction(self._action_eip_startstop) self._eip_status.set_eip_status_menu(eip_menu) @@ -1001,7 +963,7 @@ class MainWindow(QtGui.QMainWindow): # Wait a bit until the window visibility has changed so # the menu is set with the correct value. - QtCore.QTimer.singleShot(500, self._update_hideshow_menu) + QtDelayedCall(500, self._update_hideshow_menu) def _center_window(self): """ @@ -1398,7 +1360,7 @@ class MainWindow(QtGui.QMainWindow): """ # TODO split. if not self._provides_mx_and_enabled() and not flags.OFFLINE: - logger.debug("Does not provides and enabled MX") + logger.debug("Provider does not offer MX, but it is enabled.") return username = self._login_widget.get_user() @@ -1473,21 +1435,6 @@ class MainWindow(QtGui.QMainWindow): ################################################################### # Service control methods: eip - def start_eip_machine(self): - """ - Initializes and starts the EIP state machine - """ - button = self._eip_status.eip_button - action = self._action_eip_startstop - 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') - @QtCore.Slot() def _disable_eip_start_action(self): """ @@ -1506,17 +1453,14 @@ class MainWindow(QtGui.QMainWindow): def _on_eip_connection_connected(self): """ TRIGGERS: - self._eip_status.eip_connection_connected - - Emits the EIPConnection.qtsigs.connected_signal + self._eip_conductor.qtsigs.connected_signal This is a little workaround for connecting the vpn-connected signal that currently is beeing processed under status_panel. After the refactor to EIPConductor this should not be necessary. """ - self._eip_connection.qtsigs.connected_signal.emit() - domain = self._login_widget.get_selected_provider() + self._eip_status.set_provider(domain) self._settings.set_defaultprovider(domain) self._already_started_eip = True @@ -1524,7 +1468,29 @@ class MainWindow(QtGui.QMainWindow): # check for connectivity self._check_name_resolution(domain) + @QtCore.Slot() + def _on_eip_connection_disconnected(self): + """ + TRIGGERS: + self._eip_conductor.qtsigs.disconnected_signal + + Workaround for updating the eip_status widget with + the provider when the eip connection disconnects. + """ + # TODO + # We should move this to the conductor<->widget interface. + # To do that, we need to subscribe to logged_user, + # for example by using the observer pattern or a proxy object. + user = self._logged_user + if user: + domain = self._login_widget.get_selected_provider() + full_user_id = make_address(user, domain) + self._eip_status.set_provider(full_user_id) + def _check_name_resolution(self, domain): + # FIXME this has to be moved to backend !!! + # Should move to netchecks module. + # and separate qt from reactor... """ Check if we can resolve the given domain name. @@ -1555,7 +1521,7 @@ class MainWindow(QtGui.QMainWindow): "missing some helper files that are needed to securely use " "DNS while {1} is active. To install these helper files, quit " "this application and start it again." - ).format(domain, self._eip_name) + ).format(domain, self._eip_conductor.eip_name) show_err = lambda: QtGui.QMessageBox.critical( self, self.tr("Connection Error"), msg) @@ -1580,211 +1546,6 @@ class MainWindow(QtGui.QMainWindow): if settings.get_autostart_eip(): self._maybe_start_eip(autostart=True) - @QtCore.Slot() - def _start_EIP(self): - """ - Starts EIP - """ - self._eip_status.eip_pre_up() - self.user_stopped_eip = False - - # 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._backend.eip_start() - - @QtCore.Slot() - def _on_eip_connection_aborted(self): - """ - TRIGGERS: - Signaler.eip_connection_aborted - """ - logger.error("Tried to start EIP but cannot find any " - "available provider!") - - eip_status_label = self.tr("Could not load {0} configuration.") - eip_status_label = eip_status_label.format(self._eip_name) - self._eip_status.set_eip_status(eip_status_label, error=True) - - # signal connection_aborted to state machine: - qtsigs = self._eip_connection.qtsigs - qtsigs.connection_aborted_signal.emit() - - def _on_eip_openvpn_already_running(self): - self._eip_status.set_eip_status( - self.tr("Another openvpn instance is already running, and " - "could not be stopped."), - error=True) - self._set_eipstatus_off() - - def _on_eip_alien_openvpn_already_running(self): - self._eip_status.set_eip_status( - self.tr("Another openvpn instance is already running, and " - "could not be stopped because it was not launched by " - "Bitmask. Please stop it and try again."), - error=True) - self._set_eipstatus_off() - - def _on_eip_openvpn_not_found_error(self): - self._eip_status.set_eip_status( - self.tr("We could not find openvpn binary."), - error=True) - self._set_eipstatus_off() - - def _on_eip_vpn_launcher_exception(self): - # XXX We should implement again translatable exceptions so - # we can pass a translatable string to the panel (usermessage attr) - self._eip_status.set_eip_status("VPN Launcher error.", error=True) - self._set_eipstatus_off() - - def _on_eip_no_polkit_agent_error(self): - self._eip_status.set_eip_status( - # XXX this should change to polkit-kde where - # applicable. - self.tr("We could not find any authentication agent in your " - "system.<br/>Make sure you have" - "<b>polkit-gnome-authentication-agent-1</b> running and" - "try again."), - error=True) - self._set_eipstatus_off() - - def _on_eip_no_pkexec_error(self): - self._eip_status.set_eip_status( - self.tr("We could not find <b>pkexec</b> in your system."), - error=True) - self._set_eipstatus_off() - - def _on_eip_no_tun_kext_error(self): - self._eip_status.set_eip_status( - self.tr("{0} cannot be started because the tuntap extension is " - "not installed properly in your " - "system.").format(self._eip_name)) - self._set_eipstatus_off() - - @QtCore.Slot() - def _stop_eip(self, restart=False): - """ - TRIGGERS: - self._eip_connection.qtsigs.do_disconnect_signal (via state machine) - - Stops vpn process and makes gui adjustments to reflect - the change of state. - - :param abnormal: whether this was an abnormal termination. - :type abnormal: bool - """ - self.user_stopped_eip = not restart - self._backend.eip_stop(restart=restart) - - self._set_eipstatus_off(False) - self._already_started_eip = False - - logger.debug('Setting autostart to: False') - self._settings.set_autostart_eip(False) - - user = self._logged_user - if user: - domain = self._login_widget.get_selected_provider() - full_user_id = make_address(user, domain) - self._eip_status.set_provider(full_user_id) - - self._eip_status.eip_stopped() - - @QtCore.Slot() - def _on_eip_network_unreachable(self): - # XXX Should move to EIP Conductor - """ - 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 - """ - TRIGGERS: - 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(restart=True) - 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("", error=error) - self._eip_status.set_eip_status_icon("error") - - @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. - - 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): - eip_status_label = self.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 = qtsigs.connection_aborted_signal - self._backend.eip_terminate() - - elif exitCode != 0 or not self.user_stopped_eip: - eip_status_label = self.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 = 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 _maybe_start_eip(self, autostart=False): @@ -1812,9 +1573,8 @@ class MainWindow(QtGui.QMainWindow): self._already_started_eip = True # we want to start soledad anyway after a certain timeout if eip # fails to come up - QtCore.QTimer.singleShot( - self.EIP_START_TIMEOUT, - self._maybe_run_soledad_setup_checks) + QtDelayedCall(self.EIP_START_TIMEOUT, + self._maybe_run_soledad_setup_checks) else: if not self._already_started_eip: if EIP_SERVICE in self._enabled_services: @@ -1833,8 +1593,8 @@ class MainWindow(QtGui.QMainWindow): TRIGGERS: self._backend.signaler.eip_client_certificate_ready - Starts the VPN thread if the eip configuration is properly - loaded + Start the VPN thread if the eip configuration is properly + loaded. """ passed = data[self._backend.PASSED_KEY] @@ -1846,11 +1606,11 @@ class MainWindow(QtGui.QMainWindow): return # DO START EIP Connection! - self._eip_connection.qtsigs.do_connect_signal.emit() + self._eip_conductor.do_connect() @QtCore.Slot(dict) def _eip_intermediate_stage(self, data): - # TODO missing param + # TODO missing param documentation """ TRIGGERS: self._backend.signaler.eip_config_ready @@ -2004,7 +1764,7 @@ class MainWindow(QtGui.QMainWindow): if self._systray is not None: self._systray.showMessage( self.tr('Quitting...'), - self.tr('The app is quitting, please wait.')) + self.tr('Bitmask is quitting, please wait.')) # explicitly process events to display tooltip immediately QtCore.QCoreApplication.processEvents(0, 10) diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py index 31938a70..f8e5479d 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -504,6 +504,11 @@ class ConnectionMachineBuilder(object): conn.qtsigs.connection_died_signal, states[_OFF]) + # XXX adding this--------------------- + states[_ON].addTransition( + conn.qtsigs.do_disconnect_signal, + states[_DIS]) + # * If we receive the connection_aborted, we transition # from connecting to the off state states[_CON].addTransition( @@ -551,7 +556,8 @@ class ConnectionMachineBuilder(object): # TODO add tooltip # OFF State ---------------------- - off = QState() + off = SignallingState( + None, name=conn.name) off_label = _tr("Turn {0}").format( conn.Connected.short_label) if button: @@ -587,7 +593,10 @@ class ConnectionMachineBuilder(object): states[_CON] = connecting # ON State ------------------------ - on = QState() + on = SignallingState( + None, name=conn.name) + on_label = _tr("Turn {0}").format( + conn.Disconnected.short_label) if button: on.assignProperty( button, 'text', on_label) diff --git a/src/leap/bitmask/services/eip/conductor.py b/src/leap/bitmask/services/eip/conductor.py new file mode 100644 index 00000000..0cd4c95c --- /dev/null +++ b/src/leap/bitmask/services/eip/conductor.py @@ -0,0 +1,302 @@ +# -*- 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 +from leap.bitmask.util import make_address + +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() + + @QtCore.Slot() + def _start_eip(self): + """ + Starts EIP. + """ + # FIXME --- pass is_restart parameter to here ??? + is_restart = self._eip_status and self._eip_status.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 + + # 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() + + @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) + + def reconnect_stop_signal(): + self.qtsigs.disconnecting_signal.disconnect() + self.qtsigs.disconnecting_signal.connect(do_stop) + + 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(3000, 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_restart) + + @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 + if exitCode != 0 and 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 + + 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/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index b068066f..098619be 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -70,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': ( @@ -116,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()) @@ -318,6 +320,7 @@ class VPN(object): # 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 # First we try to be polite and send a SIGTERM... if self._vpnproc: @@ -755,7 +758,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: @@ -824,6 +827,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): self._openvpn_verb = openvpn_verb self._vpn_observer = VPNObserver(signaler) + self.is_restart = False # processProtocol methods @@ -859,7 +863,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): |