diff options
Diffstat (limited to 'src/leap/bitmask/gui')
-rw-r--r-- | src/leap/bitmask/gui/loggerwindow.py | 10 | ||||
-rw-r--r-- | src/leap/bitmask/gui/mainwindow.py | 405 | ||||
-rw-r--r-- | src/leap/bitmask/gui/preferenceswindow.py | 16 | ||||
-rw-r--r-- | src/leap/bitmask/gui/statemachines.py | 223 | ||||
-rw-r--r-- | src/leap/bitmask/gui/statuspanel.py | 210 | ||||
-rw-r--r-- | src/leap/bitmask/gui/wizard.py | 20 |
6 files changed, 532 insertions, 352 deletions
diff --git a/src/leap/bitmask/gui/loggerwindow.py b/src/leap/bitmask/gui/loggerwindow.py index ece4cad6..ad2ceded 100644 --- a/src/leap/bitmask/gui/loggerwindow.py +++ b/src/leap/bitmask/gui/loggerwindow.py @@ -91,7 +91,6 @@ class LoggerWindow(QtGui.QDialog): } level = log[LeapLogHandler.RECORD_KEY].levelno message = log[LeapLogHandler.MESSAGE_KEY] - message = message.replace('\n', '<br>\n') if self._logs_to_display[level]: open_tag = "<tr style='" + html_style[level] + "'>" @@ -152,8 +151,13 @@ class LoggerWindow(QtGui.QDialog): if fileName: try: with open(fileName, 'w') as output: - output.write(self.ui.txtLogHistory.toPlainText()) - output.write('\n') + history = self.ui.txtLogHistory.toPlainText() + # Chop some \n. + # html->plain adds several \n because the html is made + # using table cells. + history = history.replace('\n\n\n', '\n') + + output.write(history) logger.debug('Log saved in %s' % (fileName, )) except IOError, e: logger.error("Error saving log file: %r" % (e, )) diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 0950462b..200d68aa 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -19,23 +19,23 @@ Main window for Bitmask. """ import logging import os -import platform -import tempfile -from functools import partial import keyring from PySide import QtCore, QtGui from twisted.internet import threads +from leap.bitmask import __version__ as VERSION from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.crypto.srpauth import SRPAuth from leap.bitmask.gui.loggerwindow import LoggerWindow -from leap.bitmask.gui.preferenceswindow import PreferencesWindow -from leap.bitmask.gui.wizard import Wizard from leap.bitmask.gui.login import LoginWidget +from leap.bitmask.gui.preferenceswindow import PreferencesWindow +from leap.bitmask.gui import statemachines from leap.bitmask.gui.statuspanel import StatusPanelWidget +from leap.bitmask.gui.wizard import Wizard + from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper from leap.bitmask.services.eip.eipconfig import EIPConfig from leap.bitmask.services.eip.providerbootstrapper import ProviderBootstrapper @@ -47,6 +47,8 @@ from leap.bitmask.services.mail import imap from leap.bitmask.platform_init import IS_WIN, IS_MAC from leap.bitmask.platform_init.initializers import init_platform +from leap.bitmask.services.eip import get_openvpn_management +from leap.bitmask.services.eip.connection import EIPConnection from leap.bitmask.services.eip.vpnprocess import VPN from leap.bitmask.services.eip.vpnprocess import OpenVPNAlreadyRunning from leap.bitmask.services.eip.vpnprocess import AlienOpenVPNAlreadyRunning @@ -58,7 +60,6 @@ from leap.bitmask.services.eip.vpnlaunchers import \ EIPNoPolkitAuthAgentAvailable from leap.bitmask.services.eip.vpnlaunchers import EIPNoTunKextLoaded -from leap.bitmask import __version__ as VERSION from leap.bitmask.util.keyring_helpers import has_keyring from leap.bitmask.util.leap_log_handler import LeapLogHandler @@ -107,7 +108,6 @@ class MainWindow(QtGui.QMainWindow): user_stopped_eip = False def __init__(self, quit_callback, - standalone=False, openvpn_verb=1, bypass_checks=False): """ @@ -117,10 +117,6 @@ class MainWindow(QtGui.QMainWindow): the application. :type quit_callback: callable - :param standalone: Set to true if the app should use configs - inside its pwd - :type standalone: bool - :param bypass_checks: Set to true if the app should bypass first round of checks for CA certificates at bootstrap @@ -147,7 +143,7 @@ class MainWindow(QtGui.QMainWindow): self.ui = Ui_MainWindow() self.ui.setupUi(self) - self._settings = LeapSettings(standalone) + self._settings = LeapSettings() self._login_widget = LoginWidget( self._settings, @@ -171,12 +167,17 @@ class MainWindow(QtGui.QMainWindow): self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) - self._status_panel.start_eip.connect(self._start_eip) - self._status_panel.stop_eip.connect(self._stop_eip) + self._eip_connection = EIPConnection() + + self._eip_connection.qtsigs.connecting_signal.connect( + self._start_eip) + self._eip_connection.qtsigs.disconnecting_signal.connect( + self._stop_eip) + self._status_panel.eip_connection_connected.connect( + self._on_eip_connected) # This is loaded only once, there's a bug when doing that more # than once - self._standalone = standalone self._provider_config = ProviderConfig() # Used for automatic start of EIP self._provisional_provider_config = ProviderConfig() @@ -211,12 +212,20 @@ class MainWindow(QtGui.QMainWindow): # This thread is similar to the provider bootstrapper self._eip_bootstrapper = EIPBootstrapper() + # EIP signals ---- move to eip conductor. # TODO change the name of "download_config" signal to # something less confusing (config_ready maybe) self._eip_bootstrapper.download_config.connect( self._eip_intermediate_stage) self._eip_bootstrapper.download_client_certificate.connect( self._finish_eip_bootstrap) + self._vpn = VPN(openvpn_verb=openvpn_verb) + self._vpn.qtsigs.state_changed.connect( + self._status_panel.update_vpn_state) + self._vpn.qtsigs.status_changed.connect( + self._status_panel.update_vpn_status) + self._vpn.qtsigs.process_finished.connect( + self._eip_finished) self._soledad_bootstrapper = SoledadBootstrapper() self._soledad_bootstrapper.download_config.connect( @@ -230,14 +239,6 @@ class MainWindow(QtGui.QMainWindow): self._smtp_bootstrapper.download_config.connect( self._smtp_bootstrapped_stage) - self._vpn = VPN(openvpn_verb=openvpn_verb) - self._vpn.qtsigs.state_changed.connect( - self._status_panel.update_vpn_state) - self._vpn.qtsigs.status_changed.connect( - self._status_panel.update_vpn_status) - self._vpn.qtsigs.process_finished.connect( - self._eip_finished) - self.ui.action_log_out.setEnabled(False) self.ui.action_log_out.triggered.connect(self._logout) self.ui.action_about_leap.triggered.connect(self._about) @@ -251,30 +252,12 @@ class MainWindow(QtGui.QMainWindow): self._systray = None - self._action_eip_provider = QtGui.QAction( - self.tr("No default provider"), self) - self._action_eip_provider.setEnabled(False) - - self._action_eip_status = QtGui.QAction( - self.tr("Encrypted internet is OFF"), - self) - self._action_eip_status.setEnabled(False) - self._status_panel.set_action_eip_status( - self._action_eip_status) - - self._action_mail_status = QtGui.QAction( - self.tr("Encrypted Mail is OFF"), self) + self._action_mail_status = QtGui.QAction(self.tr("Mail is OFF"), self) self._action_mail_status.setEnabled(False) - self._status_panel.set_action_mail_status( - self._action_mail_status) + self._status_panel.set_action_mail_status(self._action_mail_status) - self._action_eip_startstop = QtGui.QAction( - self.tr("Turn OFF"), self) - self._action_eip_startstop.triggered.connect( - self._stop_eip) - self._action_eip_startstop.setEnabled(False) - self._status_panel.set_action_eip_startstop( - self._action_eip_startstop) + self._action_eip_startstop = QtGui.QAction("", self) + self._status_panel.set_action_eip_startstop(self._action_eip_startstop) self._action_preferences = QtGui.QAction(self.tr("Preferences"), self) self._action_preferences.triggered.connect(self._show_preferences) @@ -322,8 +305,7 @@ class MainWindow(QtGui.QMainWindow): if self._first_run(): self._wizard_firstrun = True - self._wizard = Wizard(standalone=standalone, - bypass_checks=bypass_checks) + self._wizard = Wizard(bypass_checks=bypass_checks) # Give this window time to finish init and then show the wizard QtCore.QTimer.singleShot(1, self._launch_wizard) self._wizard.accepted.connect(self._finish_init) @@ -331,6 +313,17 @@ class MainWindow(QtGui.QMainWindow): else: self._finish_init() + # Eip machine is a public attribute where the state machine for + # the eip connection will be available to the different components. + # Remember that this will not live in the +1600LOC mainwindow for + # all the eternity, so at some point we will be moving this to + # the EIPConductor or some other clever component that we will + # instantiate from here. + self.eip_machine = None + + # start event machines + self.start_eip_machine() + def _rejected_wizard(self): """ SLOT @@ -428,8 +421,7 @@ class MainWindow(QtGui.QMainWindow): Displays the preferences window. """ - preferences_window = PreferencesWindow( - self, self._srp_auth, self._settings, self._standalone) + preferences_window = PreferencesWindow(self, self._srp_auth) if self._soledad_ready: preferences_window.set_soledad_ready(self._soledad) @@ -594,8 +586,6 @@ class MainWindow(QtGui.QMainWindow): "no default provider configured") return - self._action_eip_provider.setText(default_provider) - self._enabled_services = self._settings.get_enabled_services( default_provider) @@ -604,6 +594,10 @@ class MainWindow(QtGui.QMainWindow): "providers", default_provider, "provider.json")): + # 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 @@ -626,9 +620,11 @@ class MainWindow(QtGui.QMainWindow): systrayMenu = QtGui.QMenu(self) systrayMenu.addAction(self._action_visible) systrayMenu.addSeparator() - systrayMenu.addAction(self._action_eip_provider) - systrayMenu.addAction(self._action_eip_status) - systrayMenu.addAction(self._action_eip_startstop) + + eip_menu = systrayMenu.addMenu(self.tr("Encrypted Internet is OFF")) + eip_menu.addAction(self._action_eip_startstop) + self._status_panel.set_eip_status_menu(eip_menu) + systrayMenu.addAction(self._action_mail_status) systrayMenu.addSeparator() systrayMenu.addAction(self._action_preferences) @@ -682,14 +678,23 @@ class MainWindow(QtGui.QMainWindow): Toggles the window visibility """ visible = self.isVisible() and self.isActiveWindow() + qApp = QtCore.QCoreApplication.instance() + if not visible: + qApp.setQuitOnLastWindowClosed(True) self.show() self.activateWindow() self.raise_() else: + # We set this in order to avoid dialogs shutting down the + # app on close, as they will be the only visible window. + # e.g.: PreferencesWindow, LoggerWindow + qApp.setQuitOnLastWindowClosed(False) self.hide() - self._update_hideshow_menu() + # 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) def _center_window(self): """ @@ -957,6 +962,7 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.set_enabled(True) def _switch_to_status(self): + # TODO this method name is confusing as hell. """ Changes the stackedWidget index to the EIP status one and triggers the eip bootstrapping @@ -968,12 +974,13 @@ class MainWindow(QtGui.QMainWindow): self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX) + # TODO separate UI from logic. + # TODO soledad should check if we want to run only over EIP. self._soledad_bootstrapper.run_soledad_setup_checks( self._provider_config, self._login_widget.get_user(), self._login_widget.get_password(), - download_if_needed=True, - standalone=self._standalone) + download_if_needed=True) self._download_eip_config() @@ -1048,17 +1055,6 @@ class MainWindow(QtGui.QMainWindow): self._provider_config, self._smtp_config, True) - else: - if self._enabled_services.count(self.MX_SERVICE) > 0: - pass # TODO show MX status - #self._status_panel.set_eip_status( - # self.tr("%s does not support MX") % - # (self._provider_config.get_domain(),), - # error=True) - else: - pass # TODO show MX status - #self._status_panel.set_eip_status( - # self.tr("MX is disabled")) ################################################################### # Service control methods: smtp @@ -1081,7 +1077,12 @@ class MainWindow(QtGui.QMainWindow): logger.error(data[self._smtp_bootstrapper.ERROR_KEY]) return logger.debug("Done bootstrapping SMTP") + self._check_smtp_config() + def _check_smtp_config(self): + """ + Checks smtp config and tries to download smtp client cert if needed. + """ hosts = self._smtp_config.get_hosts() # TODO handle more than one host and define how to choose if len(hosts) > 0: @@ -1089,24 +1090,40 @@ class MainWindow(QtGui.QMainWindow): logger.debug("Using hostname %s for SMTP" % (hostname,)) host = hosts[hostname][self.IP_KEY].encode("utf-8") port = hosts[hostname][self.PORT_KEY] - # TODO move the start to _start_smtp_service - - # TODO Make the encrypted_only configurable - # TODO pick local smtp port in a better way - # TODO remove hard-coded port and let leap.mail set - # the specific default. - - from leap.mail.smtp import setup_smtp_relay - client_cert = self._eip_config.get_client_cert_path( - self._provider_config) - self._smtp_service = setup_smtp_relay( - port=2013, - keymanager=self._keymanager, - smtp_host=host, - smtp_port=port, - smtp_cert=client_cert, - smtp_key=client_cert, - encrypted_only=False) + + client_cert = self._smtp_config.get_client_cert_path( + self._provider_config, + about_to_download=True) + + if not os.path.isfile(client_cert): + self._smtp_bootstrapper._download_client_certificates() + if os.path.isfile(client_cert): + self._start_smtp_service(host, port, client_cert) + else: + logger.warning("Tried to download email client " + "certificate, but could not find any") + + else: + logger.warning("No smtp hosts configured") + + def _start_smtp_service(self, host, port, cert): + """ + Starts the smtp service. + """ + # TODO Make the encrypted_only configurable + # TODO pick local smtp port in a better way + # TODO remove hard-coded port and let leap.mail set + # the specific default. + + from leap.mail.smtp import setup_smtp_relay + self._smtp_service = setup_smtp_relay( + port=2013, + keymanager=self._keymanager, + smtp_host=host, + smtp_port=port, + smtp_cert=cert, + smtp_key=cert, + encrypted_only=False) def _stop_smtp_service(self): """ @@ -1174,26 +1191,36 @@ class MainWindow(QtGui.QMainWindow): ################################################################### # Service control methods: eip - def _get_socket_host(self): + def start_eip_machine(self): """ - Returns the socket and port to be used for VPN - - :rtype: tuple (str, str) (host, port) + Initializes and starts the EIP state machine """ - # TODO make this properly multiplatform - # TODO get this out of gui/ + button = self._status_panel.eip_button + action = self._action_eip_startstop + label = self._status_panel.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() - if platform.system() == "Windows": - host = "localhost" - port = "9876" - else: - # XXX cleanup this on exit too - host = os.path.join(tempfile.mkdtemp(prefix="leap-tmp"), - 'openvpn.socket') - port = "unix" + @QtCore.Slot() + def _on_eip_connected(self): + """ + SLOT + TRIGGERS: + self._status_panel.eip_connection_connected + Emits the EIPConnection.qtsigs.connected_signal - return host, port + 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. + """ + logger.debug('EIP connected signal received ...') + self._eip_connection.qtsigs.connected_signal.emit() + @QtCore.Slot() def _start_eip(self): """ SLOT @@ -1204,35 +1231,27 @@ class MainWindow(QtGui.QMainWindow): Starts EIP """ + provider_config = self._get_best_provider_config() + provider = provider_config.get_domain() self._status_panel.eip_pre_up() self.user_stopped_eip = False - provider_config = self._get_best_provider_config() try: - host, port = self._get_socket_host() + # XXX move this to EIPConductor + host, port = get_openvpn_management() self._vpn.start(eipconfig=self._eip_config, providerconfig=provider_config, socket_host=host, socket_port=port) - - self._settings.set_defaultprovider( - provider_config.get_domain()) - - provider = provider_config.get_domain() + self._settings.set_defaultprovider(provider) if self._logged_user is not None: provider = "%s@%s" % (self._logged_user, provider) + # XXX move to the state machine too self._status_panel.set_provider(provider) - self._action_eip_provider.setText(provider_config.get_domain()) - - self._status_panel.eip_started() - - # XXX refactor into status_panel method? - self._action_eip_startstop.setText(self.tr("Turn OFF")) - self._action_eip_startstop.disconnect(self) - self._action_eip_startstop.triggered.connect( - self._stop_eip) + # TODO refactor exceptions so they provide translatable + # usef-facing messages. except EIPNoPolkitAuthAgentAvailable: self._status_panel.set_global_status( # XXX this should change to polkit-kde where @@ -1284,26 +1303,7 @@ class MainWindow(QtGui.QMainWindow): else: self._already_started_eip = True - def _set_eipstatus_off(self): - """ - Sets eip status to off - """ - self._status_panel.set_eip_status(self.tr("OFF"), error=True) - self._status_panel.set_eip_status_icon("error") - self._status_panel.set_startstop_enabled(True) - self._status_panel.eip_stopped() - - self._set_action_eipstart_off() - - def _set_action_eipstart_off(self): - """ - Sets eip startstop action to OFF status. - """ - self._action_eip_startstop.setText(self.tr("Turn ON")) - self._action_eip_startstop.disconnect(self) - self._action_eip_startstop.triggered.connect( - self._start_eip) - + @QtCore.Slot() def _stop_eip(self, abnormal=False): """ SLOT @@ -1327,34 +1327,20 @@ class MainWindow(QtGui.QMainWindow): self._set_eipstatus_off() self._already_started_eip = False + + # XXX do via signal self._settings.set_defaultprovider(None) if self._logged_user: self._status_panel.set_provider( "%s@%s" % (self._logged_user, self._get_best_provider_config().get_domain())) - def _get_best_provider_config(self): + def _set_eipstatus_off(self): """ - Returns the best ProviderConfig to use at a moment. We may - have to use self._provider_config or - self._provisional_provider_config depending on the start - status. - - :rtype: ProviderConfig + Sets eip status to off """ - leap_assert(self._provider_config is not None or - self._provisional_provider_config is not None, - "We need a provider config") - - provider_config = None - if self._provider_config.loaded(): - provider_config = self._provider_config - elif self._provisional_provider_config.loaded(): - provider_config = self._provisional_provider_config - else: - leap_assert(False, "We could not find any usable ProviderConfig.") - - return provider_config + self._status_panel.set_eip_status(self.tr("OFF"), error=True) + self._status_panel.set_eip_status_icon("error") def _download_eip_config(self): """ @@ -1368,6 +1354,7 @@ class MainWindow(QtGui.QMainWindow): self._enabled_services.count(self.OPENVPN_SERVICE) > 0 and \ not self._already_started_eip: + # XXX this should be handled by the state machine. self._status_panel.set_eip_status( self.tr("Starting...")) self._eip_bootstrapper.run_eip_setup_checks( @@ -1381,7 +1368,6 @@ class MainWindow(QtGui.QMainWindow): error=True) else: self._status_panel.set_eip_status(self.tr("Disabled")) - self._status_panel.set_startstop_enabled(False) def _finish_eip_bootstrap(self, data): """ @@ -1402,7 +1388,6 @@ class MainWindow(QtGui.QMainWindow): return provider_config = self._get_best_provider_config() - domain = provider_config.get_domain() loaded = self._eip_config.loaded() @@ -1414,13 +1399,41 @@ class MainWindow(QtGui.QMainWindow): loaded = self._eip_config.load(eip_config_path) if loaded: - self._start_eip() + # DO START EIP Connection! + self._eip_connection.qtsigs.do_connect_signal.emit() else: self._status_panel.set_eip_status( self.tr("Could not load Encrypted Internet " "Configuration."), error=True) + # end eip methods ------------------------------------------- + + def _get_best_provider_config(self): + """ + Returns the best ProviderConfig to use at a moment. We may + have to use self._provider_config or + self._provisional_provider_config depending on the start + status. + + :rtype: ProviderConfig + """ + # TODO move this out of gui. + leap_assert(self._provider_config is not None or + self._provisional_provider_config is not None, + "We need a provider config") + + provider_config = None + if self._provider_config.loaded(): + provider_config = self._provider_config + elif self._provisional_provider_config.loaded(): + provider_config = self._provisional_provider_config + else: + leap_assert(False, "We could not find any usable ProviderConfig.") + + return provider_config + + @QtCore.Slot() def _logout(self): """ SLOT @@ -1444,13 +1457,16 @@ class MainWindow(QtGui.QMainWindow): Switches the stackedWidget back to the login stage after logging out """ - self._logged_user = None - self.ui.action_log_out.setEnabled(False) - self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) - self._login_widget.set_password("") - self._login_widget.set_enabled(True) - self._login_widget.set_status("") - self.ui.btnPreferences.setEnabled(False) + if ok: + self._logged_user = None + self.ui.action_log_out.setEnabled(False) + self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) + self._login_widget.set_password("") + self._login_widget.set_enabled(True) + self._login_widget.set_status("") + else: + status_text = self.tr("Something went wrong with the logout.") + self._status_panel.set_global_status(status_text, error=True) def _intermediate_stage(self, data): """ @@ -1498,6 +1514,7 @@ class MainWindow(QtGui.QMainWindow): 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,)) @@ -1532,7 +1549,20 @@ class MainWindow(QtGui.QMainWindow): if exitCode == 0 and IS_MAC: # XXX remove this warning after I fix cocoasudo. logger.warning("The above exit code MIGHT BE WRONG.") - self._stop_eip(abnormal) + + # We emit signals to trigger transitions in the state machine: + qtsigs = self._eip_connection.qtsigs + if abnormal: + signal = qtsigs.connection_died_signal + else: + signal = qtsigs.disconnected_signal + + # 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) + signal.emit() def _on_raise_window_event(self, req): """ @@ -1611,6 +1641,11 @@ class MainWindow(QtGui.QMainWindow): """ # TODO separate the shutting down of services from the # UI stuff. + + # Set this in case that the app is hidden + qApp = QtCore.QCoreApplication.instance() + qApp.setQuitOnLastWindowClosed(True) + self._cleanup_and_quit() self._really_quit = True @@ -1627,37 +1662,3 @@ class MainWindow(QtGui.QMainWindow): self._quit_callback() logger.debug('Bye.') - - -if __name__ == "__main__": - import signal - - def sigint_handler(*args, **kwargs): - logger.debug('SIGINT catched. shutting down...') - mainwindow = args[0] - mainwindow.quit() - - import sys - - logger = logging.getLogger(name='leap') - logger.setLevel(logging.DEBUG) - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - app = QtGui.QApplication(sys.argv) - mainwindow = MainWindow() - mainwindow.show() - - timer = QtCore.QTimer() - timer.start(500) - timer.timeout.connect(lambda: None) - - sigint = partial(sigint_handler, mainwindow) - signal.signal(signal.SIGINT, sigint) - - sys.exit(app.exec_()) diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index 1becfb18..2d17f6c2 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -24,6 +24,7 @@ import logging from functools import partial from PySide import QtCore, QtGui +from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.gui.ui_preferences import Ui_Preferences from leap.soledad.client import NoStorageSecret from leap.bitmask.crypto.srpauth import SRPAuthBadPassword @@ -40,26 +41,18 @@ class PreferencesWindow(QtGui.QDialog): """ Window that displays the preferences. """ - - WEAK_PASSWORDS = ("123456", "qweasd", "qwerty", "password") - - def __init__(self, parent, srp_auth, leap_settings, standalone): + def __init__(self, parent, srp_auth): """ :param parent: parent object of the PreferencesWindow. :parent type: QWidget :param srp_auth: SRPAuth object configured in the main app. :type srp_auth: SRPAuth - :param standalone: If True, the application is running as standalone - and the preferences dialog should display some - messages according to this. - :type standalone: bool """ QtGui.QDialog.__init__(self, parent) self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") self._srp_auth = srp_auth - self._settings = leap_settings - self._standalone = standalone + self._settings = LeapSettings() self._soledad = None # Load UI @@ -325,8 +318,7 @@ class PreferencesWindow(QtGui.QDialog): for service in services: try: checkbox = QtGui.QCheckBox(self) - service_label = get_service_display_name( - service, self._standalone) + service_label = get_service_display_name(service) checkbox.setText(service_label) self.ui.vlServices.addWidget(checkbox) diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py new file mode 100644 index 00000000..c3dd5ed3 --- /dev/null +++ b/src/leap/bitmask/gui/statemachines.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +# statemachines.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/>. +""" +State machines for the Bitmask app. +""" +import logging + +from PySide.QtCore import QStateMachine, QState +from PySide.QtCore import QObject + +from leap.bitmask.services import connections +from leap.common.check import leap_assert_type + +logger = logging.getLogger(__name__) + +_tr = QObject().tr + +# Indexes for the state dict +_ON = "on" +_OFF = "off" +_CON = "connecting" +_DIS = "disconnecting" + + +class IntermediateState(QState): + """ + Intermediate state that emits a custom signal on entry + """ + def __init__(self, signal): + """ + Initializer. + :param signal: the signal to be emitted on entry on this state. + :type signal: QtCore.QSignal + """ + super(IntermediateState, self).__init__() + self._signal = signal + + def onEntry(self, *args): + """ + Emits the signal on entry. + """ + logger.debug('IntermediateState entered. Emitting signal ...') + if self._signal is not None: + self._signal.emit() + + +class ConnectionMachineBuilder(object): + """ + Builder class for state machines made from LEAPConnections. + """ + def __init__(self, connection): + """ + :param connection: an instance of a concrete LEAPConnection + we will be building a state machine for. + :type connection: AbstractLEAPConnection + """ + self._conn = connection + leap_assert_type(self._conn, connections.AbstractLEAPConnection) + + def make_machine(self, button=None, action=None, label=None): + """ + Creates a statemachine associated with the passed controls. + + :param button: the switch button. + :type button: QPushButton + + :param action: the actionh that controls connection switch in a menu. + :type action: QAction + + :param label: the label that displays the connection state + :type label: QLabel + + :returns: a state machine + :rtype: QStateMachine + """ + machine = QStateMachine() + conn = self._conn + + states = self._make_states(button, action, label) + + # transitions: + + states[_OFF].addTransition( + conn.qtsigs.do_connect_signal, + states[_CON]) + + # * Clicking the buttons or actions transitions to the + # intermediate stage. + if button: + states[_OFF].addTransition( + button.clicked, + states[_CON]) + states[_ON].addTransition( + button.clicked, + states[_DIS]) + + if action: + states[_OFF].addTransition( + action.triggered, + states[_CON]) + states[_ON].addTransition( + action.triggered, + states[_DIS]) + + # * We transition to the completed stages when + # we receive the matching signal from the underlying + # conductor. + + states[_CON].addTransition( + conn.qtsigs.connected_signal, + states[_ON]) + states[_DIS].addTransition( + conn.qtsigs.disconnected_signal, + states[_OFF]) + + # * If we receive the connection_died, we transition + # to the off state + states[_ON].addTransition( + conn.qtsigs.connection_died_signal, + states[_OFF]) + + # adding states to the machine + for state in states.itervalues(): + machine.addState(state) + machine.setInitialState(states[_OFF]) + return machine + + def _make_states(self, button, action, label): + """ + Creates the four states for the state machine + + :param button: the switch button. + :type button: QPushButton + + :param action: the actionh that controls connection switch in a menu. + :type action: QAction + + :param label: the label that displays the connection state + :type label: QLabel + + :returns: a dict of states + :rtype: dict + """ + conn = self._conn + states = {} + + # TODO add tooltip + + # OFF State ---------------------- + off = QState() + off_label = _tr("Turn {0}").format( + conn.Connected.short_label) + if button: + off.assignProperty( + button, 'text', off_label) + off.assignProperty( + button, 'enabled', True) + if action: + off.assignProperty( + action, 'text', off_label) + off.setObjectName(_OFF) + states[_OFF] = off + + # CONNECTING State ---------------- + connecting = IntermediateState( + conn.qtsigs.connecting_signal) + on_label = _tr("Turn {0}").format( + conn.Disconnected.short_label) + if button: + connecting.assignProperty( + button, 'text', on_label) + connecting.assignProperty( + button, 'enabled', False) + if action: + connecting.assignProperty( + action, 'text', on_label) + connecting.assignProperty( + action, 'enabled', False) + connecting.setObjectName(_CON) + states[_CON] = connecting + + # ON State ------------------------ + on = QState() + if button: + on.assignProperty( + button, 'text', on_label) + on.assignProperty( + button, 'enabled', True) + if action: + on.assignProperty( + action, 'text', on_label) + on.assignProperty( + action, 'enabled', True) + # TODO set label for ON state + on.setObjectName(_ON) + states[_ON] = on + + # DISCONNECTING State ------------- + disconnecting = IntermediateState( + conn.qtsigs.disconnecting_signal) + if button: + disconnecting.assignProperty( + button, 'enabled', False) + # XXX complete disconnecting + # TODO disable button + disconnecting.setObjectName(_DIS) + states[_DIS] = disconnecting + + return states diff --git a/src/leap/bitmask/gui/statuspanel.py b/src/leap/bitmask/gui/statuspanel.py index 3a91f08e..679f00b1 100644 --- a/src/leap/bitmask/gui/statuspanel.py +++ b/src/leap/bitmask/gui/statuspanel.py @@ -14,7 +14,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - """ Status Panel widget implementation """ @@ -25,9 +24,10 @@ from functools import partial from PySide import QtCore, QtGui +from leap.bitmask.services.eip.connection import EIPConnection from leap.bitmask.services.eip.vpnprocess import VPNManager from leap.bitmask.platform_init import IS_WIN, IS_LINUX -from leap.bitmask.util import first +from leap.bitmask.util.averages import RateMovingAverage from leap.common.check import leap_assert, leap_assert_type from leap.common.events import register from leap.common.events import events_pb2 as proto @@ -37,83 +37,10 @@ from ui_statuspanel import Ui_StatusPanel logger = logging.getLogger(__name__) -class RateMovingAverage(object): - """ - Moving window average for calculating - upload and download rates. - """ - SAMPLE_SIZE = 5 - - def __init__(self): - """ - Initializes an empty array of fixed size - """ - self.reset() - - def reset(self): - self._data = [None for i in xrange(self.SAMPLE_SIZE)] - - def append(self, x): - """ - Appends a new data point to the collection. - - :param x: A tuple containing timestamp and traffic points - in the form (timestamp, traffic) - :type x: tuple - """ - self._data.pop(0) - self._data.append(x) - - def get(self): - """ - Gets the collection. - """ - return self._data - - def get_average(self): - """ - Gets the moving average. - """ - data = filter(None, self.get()) - traff = [traffic for (ts, traffic) in data] - times = [ts for (ts, traffic) in data] - - try: - deltatraffic = traff[-1] - first(traff) - deltat = (times[-1] - first(times)).seconds - except IndexError: - deltatraffic = 0 - deltat = 0 - - try: - rate = float(deltatraffic) / float(deltat) / 1024 - except ZeroDivisionError: - rate = 0 - - # In some cases we get negative rates - if rate < 0: - rate = 0 - - return rate - - def get_total(self): - """ - Gets the total accumulated throughput. - """ - try: - return self._data[-1][1] / 1024 - except TypeError: - return 0 - - class StatusPanelWidget(QtGui.QWidget): """ Status widget that displays the current state of the LEAP services """ - - start_eip = QtCore.Signal() - stop_eip = QtCore.Signal() - DISPLAY_TRAFFIC_RATES = True RATE_STR = "%14.2f KB/s" TOTAL_STR = "%14.2f Kb" @@ -121,6 +48,7 @@ class StatusPanelWidget(QtGui.QWidget): MAIL_OFF_ICON = ":/images/mail-unlocked.png" MAIL_ON_ICON = ":/images/mail-locked.png" + eip_connection_connected = QtCore.Signal() _soledad_event = QtCore.Signal(object) _smtp_event = QtCore.Signal(object) _imap_event = QtCore.Signal(object) @@ -130,17 +58,18 @@ class StatusPanelWidget(QtGui.QWidget): QtGui.QWidget.__init__(self, parent) self._systray = None - self._action_eip_status = None + self._eip_status_menu = None self.ui = Ui_StatusPanel() self.ui.setupUi(self) - self.ui.btnEipStartStop.setEnabled(False) - self.ui.btnEipStartStop.clicked.connect( - self.start_eip) + self.eipconnection = EIPConnection() self.hide_status_box() + # set systray tooltip statuses + self._eip_status = self._mx_status = "" + # Set the EIP status icons self.CONNECTING_ICON = None self.CONNECTED_ICON = None @@ -327,6 +256,8 @@ class StatusPanelWidget(QtGui.QWidget): self.CONNECTED_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[1]) self.ERROR_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[2]) + # Systray and actions + def set_systray(self, systray): """ Sets the systray object to use. @@ -336,6 +267,16 @@ class StatusPanelWidget(QtGui.QWidget): """ leap_assert_type(systray, QtGui.QSystemTrayIcon) self._systray = systray + self._systray.setToolTip(self.tr("All services are OFF")) + + def _update_systray_tooltip(self): + """ + Updates the system tray icon tooltip using the eip and mx statuses. + """ + status = self.tr("Encrypted Internet is {0}").format(self._eip_status) + status += '\n' + status += self.tr("Mail is {0}").format(self._mx_status) + self._systray.setToolTip(status) def set_action_eip_startstop(self, action_eip_startstop): """ @@ -346,15 +287,15 @@ class StatusPanelWidget(QtGui.QWidget): """ self._action_eip_startstop = action_eip_startstop - def set_action_eip_status(self, action_eip_status): + def set_eip_status_menu(self, eip_status_menu): """ - Sets the action_eip_status to use. + Sets the eip_status_menu to use. - :param action_eip_status: action_eip_status to be used - :type action_eip_status: QtGui.QAction + :param eip_status_menu: eip_status_menu to be used + :type eip_status_menu: QtGui.QMenu """ - leap_assert_type(action_eip_status, QtGui.QAction) - self._action_eip_status = action_eip_status + leap_assert_type(eip_status_menu, QtGui.QMenu) + self._eip_status_menu = eip_status_menu def set_action_mail_status(self, action_mail_status): """ @@ -388,6 +329,25 @@ class StatusPanelWidget(QtGui.QWidget): """ self.ui.globalStatusBox.hide() + # EIP status --- + + @property + def eip_button(self): + return self.ui.btnEipStartStop + + @property + def eip_label(self): + return self.ui.lblEIPStatus + + def eip_pre_up(self): + """ + Triggered when the app activates eip. + Hides the status box and disables the start/stop button. + """ + self.hide_status_box() + self.set_startstop_enabled(False) + + # XXX disable (later) -------------------------- def set_eip_status(self, status, error=False): """ Sets the status label at the VPN stage to status @@ -400,11 +360,14 @@ class StatusPanelWidget(QtGui.QWidget): """ leap_assert_type(error, bool) - self._systray.setToolTip(status) + self._eip_status = status + if error: status = "<font color='red'>%s</font>" % (status,) self.ui.lblEIPStatus.setText(status) + self._update_systray_tooltip() + # XXX disable --------------------------------- def set_startstop_enabled(self, value): """ Enable or disable btnEipStartStop and _action_eip_startstop @@ -417,14 +380,7 @@ class StatusPanelWidget(QtGui.QWidget): self.ui.btnEipStartStop.setEnabled(value) self._action_eip_startstop.setEnabled(value) - def eip_pre_up(self): - """ - Triggered when the app activates eip. - Hides the status box and disables the start/stop button. - """ - self.hide_status_box() - self.set_startstop_enabled(False) - + # XXX disable ----------------------------- def eip_started(self): """ Sets the state of the widget to how it should look after EIP @@ -433,27 +389,21 @@ class StatusPanelWidget(QtGui.QWidget): self.ui.btnEipStartStop.setText(self.tr("Turn OFF")) self.ui.btnEipStartStop.disconnect(self) self.ui.btnEipStartStop.clicked.connect( - self.stop_eip) + self.eipconnection.qtsigs.do_connect_signal) + # XXX disable ----------------------------- def eip_stopped(self): """ 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.start_eip) - - def set_icon(self, icon): - """ - Sets the icon to display for EIP - - :param icon: icon to display - :type icon: QPixmap - """ - self.ui.lblVPNStatusIcon.setPixmap(icon) + self.eipconnection.qtsigs.do_disconnect_signal) def update_vpn_status(self, data): """ @@ -492,14 +442,21 @@ class StatusPanelWidget(QtGui.QWidget): TRIGGER: VPN.state_changed Updates the displayed VPN state based on the data provided by - the VPN thread + the VPN thread. + + Emits: + If the status is connected, we emit EIPConnection.qtsigs. + connected_signal """ status = data[VPNManager.STATUS_STEP_KEY] self.set_eip_status_icon(status) if status == "CONNECTED": + # XXX should be handled by the state machine too. self.set_eip_status(self.tr("ON")) - # Only now we can properly enable the button. - self.set_startstop_enabled(True) + logger.debug("STATUS IS CONNECTED --- emitting signal") + self.eip_connection_connected.emit() + + # XXX should lookup status map in EIPConnection elif status == "AUTH": self.set_eip_status(self.tr("Authenticating...")) elif status == "GET_CONFIG": @@ -513,7 +470,8 @@ class StatusPanelWidget(QtGui.QWidget): elif status == "ALREADYRUNNING": # Put the following calls in Qt's event queue, otherwise # the UI won't update properly - QtCore.QTimer.singleShot(0, self.stop_eip) + QtCore.QTimer.singleShot( + 0, self.eipconnection.qtsigs.do_disconnect_signal) QtCore.QTimer.singleShot(0, partial(self.set_global_status, self.tr("Unable to start VPN, " "it's already " @@ -521,6 +479,15 @@ class StatusPanelWidget(QtGui.QWidget): else: self.set_eip_status(status) + def set_eip_icon(self, icon): + """ + Sets the icon to display for EIP + + :param icon: icon to display + :type icon: QPixmap + """ + self.ui.lblVPNStatusIcon.setPixmap(icon) + def set_eip_status_icon(self, status): """ Given a status step from the VPN thread, set the icon properly @@ -530,27 +497,31 @@ class StatusPanelWidget(QtGui.QWidget): """ selected_pixmap = self.ERROR_ICON selected_pixmap_tray = self.ERROR_ICON_TRAY - tray_message = self.tr("Encryption is OFF") + tray_message = self.tr("Encrypted Internet is OFF") if status in ("WAIT", "AUTH", "GET_CONFIG", "RECONNECTING", "ASSIGN_IP"): selected_pixmap = self.CONNECTING_ICON selected_pixmap_tray = self.CONNECTING_ICON_TRAY - tray_message = self.tr("Turning ON") + tray_message = self.tr("Encrypted Internet is STARTING") elif status in ("CONNECTED"): - tray_message = self.tr("Encryption is ON") + tray_message = self.tr("Encrypted Internet is ON") selected_pixmap = self.CONNECTED_ICON selected_pixmap_tray = self.CONNECTED_ICON_TRAY - self.set_icon(selected_pixmap) + self.set_eip_icon(selected_pixmap) self._systray.setIcon(QtGui.QIcon(selected_pixmap_tray)) - self._action_eip_status.setText(tray_message) + self._eip_status_menu.setTitle(tray_message) def set_provider(self, provider): self.ui.lblProvider.setText(provider) + # + # mail methods + # + def _set_mail_status(self, status, ready=False): """ - Sets the Encrypted Mail status in the label and in the tray icon. + Sets the Mail status in the label and in the tray icon. :param status: the status text to display :type status: unicode @@ -559,15 +530,18 @@ class StatusPanelWidget(QtGui.QWidget): """ self.ui.lblMailStatus.setText(status) - tray_status = self.tr('Encrypted Mail is OFF') + self._mx_status = self.tr('OFF') + tray_status = self.tr('Mail is OFF') icon = QtGui.QPixmap(self.MAIL_OFF_ICON) if ready: icon = QtGui.QPixmap(self.MAIL_ON_ICON) - tray_status = self.tr('Encrypted Mail is ON') + self._mx_status = self.tr('ON') + tray_status = self.tr('Mail is ON') self.ui.lblMailIcon.setPixmap(icon) self._action_mail_status.setText(tray_status) + self._update_systray_tooltip() def _mail_handle_soledad_events(self, req): """ @@ -724,7 +698,7 @@ class StatusPanelWidget(QtGui.QWidget): self.ui.lblUnread.setVisible(req.content != "0") self._set_mail_status(self.tr("ON"), ready=True) else: - leap_assert(False, + leap_assert(False, # XXX ??? "Don't know how to handle this state: %s" % (req.event)) diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index e004e6cf..45734b81 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -27,6 +27,7 @@ from functools import partial from PySide import QtCore, QtGui from twisted.internet import threads +from leap.bitmask.config import flags from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.crypto.srpregister import SRPRegister from leap.bitmask.util.privilege_policies import is_missing_policy_permissions @@ -58,21 +59,16 @@ class Wizard(QtGui.QWizard): BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" - def __init__(self, standalone=False, bypass_checks=False): + def __init__(self, bypass_checks=False): """ Constructor for the main Wizard. - :param standalone: If True, the application is running as standalone - and the wizard should display some messages according to this. - :type standalone: bool :param bypass_checks: Set to true if the app should bypass first round of checks for CA certificates at bootstrap :type bypass_checks: bool """ QtGui.QWizard.__init__(self) - self.standalone = standalone - self.ui = Ui_Wizard() self.ui.setupUi(self) @@ -489,8 +485,7 @@ class Wizard(QtGui.QWizard): try: if service not in self._shown_services: checkbox = QtGui.QCheckBox(self) - service_label = get_service_display_name( - service, self.standalone) + service_label = get_service_display_name(service) checkbox.setText(service_label) self.ui.serviceListLayout.addWidget(checkbox) @@ -555,15 +550,6 @@ class Wizard(QtGui.QWizard): if pageId == self.SERVICES_PAGE: self._populate_services() - def _is_need_eip_password_warning(self): - """ - Returns True if we need to add a warning about eip needing - administrative permissions to start. That can be either - because we are running in standalone mode, or because we could - not find the needed privilege escalation mechanisms being operative. - """ - return self.standalone or is_missing_policy_permissions() - def nextId(self): """ Sets the next page id for the wizard based on wether the user |