diff options
Diffstat (limited to 'src/leap/gui/mainwindow.py')
| -rw-r--r-- | src/leap/gui/mainwindow.py | 600 | 
1 files changed, 600 insertions, 0 deletions
diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py new file mode 100644 index 00000000..1821e4a6 --- /dev/null +++ b/src/leap/gui/mainwindow.py @@ -0,0 +1,600 @@ +# -*- coding: utf-8 -*- +# mainwindow.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/>. + +""" +Main window for the leap client +""" +import os +import logging + +from PySide import QtCore, QtGui + +from ui_mainwindow import Ui_MainWindow +from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpauth import SRPAuth +from leap.services.eip.vpn import VPN +from leap.services.eip.providerbootstrapper import ProviderBootstrapper +from leap.services.eip.eipbootstrapper import EIPBootstrapper +from leap.services.eip.eipconfig import EIPConfig +from leap.gui.wizard import Wizard + +logger = logging.getLogger(__name__) + + +class MainWindow(QtGui.QMainWindow): +    """ +    Main window for login and presenting status updates to the user +    """ + +    # StackedWidget indexes +    LOGIN_INDEX = 0 +    EIP_STATUS_INDEX = 1 + +    def __init__(self): +        QtGui.QMainWindow.__init__(self) + +        self.CONNECTING_ICON = QtGui.QPixmap(":/images/conn_connecting.png") +        self.CONNECTED_ICON = QtGui.QPixmap(":/images/conn_connected.png") +        self.ERROR_ICON = QtGui.QPixmap(":/images/conn_error.png") + +        self.ui = Ui_MainWindow() +        self.ui.setupUi(self) + +        self.ui.lnPassword.setEchoMode(QtGui.QLineEdit.Password) + +        self.ui.btnLogin.clicked.connect(self._login) +        self.ui.lnUser.returnPressed.connect(self._focus_password) +        self.ui.lnPassword.returnPressed.connect(self._login) + +        self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) + +        # This is loaded only once, there's a bug when doing that more +        # than once +        self._provider_config = ProviderConfig() +        self._eip_config = EIPConfig() +        # This is created once we have a valid provider config +        self._srp_auth = None + +        # This thread is always running, although it's quite +        # lightweight when it's done setting up provider +        # configuration and certificate. +        self._provider_bootstrapper = ProviderBootstrapper() + +        # TODO: add sigint handler + +        # Intermediate stages, only do something if there was an error +        self._provider_bootstrapper.name_resolution.connect( +            self._intermediate_stage) +        self._provider_bootstrapper.https_connection.connect( +            self._intermediate_stage) +        self._provider_bootstrapper.download_ca_cert.connect( +            self._intermediate_stage) + +        # Important stages, loads the provider config and checks +        # certificates +        self._provider_bootstrapper.download_provider_info.connect( +            self._load_provider_config) +        self._provider_bootstrapper.check_api_certificate.connect( +            self._provider_config_loaded) + +        # This thread is similar to the provider bootstrapper +        self._eip_bootstrapper = EIPBootstrapper() + +        self._eip_bootstrapper.download_config.connect( +            self._intermediate_stage) +        self._eip_bootstrapper.download_client_certificate.connect( +            self._start_eip) + +        self._vpn = VPN() +        self._vpn.state_changed.connect(self._update_vpn_state) +        self._vpn.status_changed.connect(self._update_vpn_status) + +        QtCore.QCoreApplication.instance().connect( +            QtCore.QCoreApplication.instance(), +            QtCore.SIGNAL("aboutToQuit()"), +            self._vpn.set_should_quit) +        QtCore.QCoreApplication.instance().connect( +            QtCore.QCoreApplication.instance(), +            QtCore.SIGNAL("aboutToQuit()"), +            self._provider_bootstrapper.set_should_quit) +        QtCore.QCoreApplication.instance().connect( +            QtCore.QCoreApplication.instance(), +            QtCore.SIGNAL("aboutToQuit()"), +            self._eip_bootstrapper.set_should_quit) + +        self.ui.action_sign_out.setEnabled(False) +        self.ui.action_sign_out.triggered.connect(self._logout) +        self.ui.action_about_leap.triggered.connect(self._about) +        self.ui.action_quit.triggered.connect(self.quit) +        self.ui.action_wizard.triggered.connect(self._launch_wizard) + +        # Used to differentiate between real quits and close to tray +        self._really_quit = False + +        self._systray = None +        self._action_visible = QtGui.QAction("Hide", self) +        self._action_visible.triggered.connect(self._toggle_visible) + +        self._center_window() +        self._wizard = None +        if self._first_run(): +            self._wizard = Wizard() +            # Give this window time to finish init and then show the wizard +            QtCore.QTimer.singleShot(1, self._launch_wizard) +            self._wizard.finished.connect(self._finish_init) +        else: +            self._finish_init() + +    def _launch_wizard(self): +        if self._wizard is None: +            self._wizard = Wizard() +        self._wizard.exec_() + +    def _finish_init(self): +        self.ui.cmbProviders.addItems(self._configured_providers()) +        self._show_systray() +        self.show() +        if self._wizard: +            possible_username = self._wizard.get_username() +            if possible_username is not None: +                self.ui.lnUser.setText(possible_username) +                self._focus_password() +            self._wizard = None + +    def _show_systray(self): +        """ +        Sets up the systray icon +        """ +        systrayMenu = QtGui.QMenu(self) +        systrayMenu.addAction(self._action_visible) +        systrayMenu.addAction(self.ui.action_sign_out) +        systrayMenu.addSeparator() +        systrayMenu.addAction(self.ui.action_quit) +        self._systray = QtGui.QSystemTrayIcon(self) +        self._systray.setContextMenu(systrayMenu) +        self._systray.setIcon(QtGui.QIcon(self.ERROR_ICON)) +        self._systray.setVisible(True) +        self._systray.activated.connect(self._toggle_visible) + +    def _toggle_visible(self): +        """ +        SLOT +        TRIGGER: self._systray.activated + +        Toggles the window visibility +        """ +        self.setVisible(not self.isVisible()) +        action_visible_text = "Hide" +        if not self.isVisible(): +            action_visible_text = "Show" +        self._action_visible.setText(action_visible_text) + +    def _center_window(self): +        """ +        Centers the mainwindow based on the desktop geometry +        """ +        app = QtGui.QApplication.instance() +        width = app.desktop().width() +        height = app.desktop().height() +        window_width = self.size().width() +        window_height = self.size().height() +        x = (width / 2.0) - (window_width / 2.0) +        y = (height / 2.0) - (window_height / 2.0) +        self.move(x, y) + +    def _about(self): +        """ +        Display the About LEAP dialog +        """ +        QtGui.QMessageBox.about(self, "About LEAP", +                                "LEAP is a non-profit dedicated to giving " +                                "all internet users access to secure " +                                "communication. Our focus is on adapting " +                                "encryption technology to make it easy to use " +                                "and widely available. " +                                "<a href=\"https://leap.se\">More about LEAP" +                                "</a>") + +    def quit(self): +        self._really_quit = True +        if self._wizard: +            self._wizard.accept() +        self.close() + +    def changeEvent(self, e): +        """ +        Reimplements the changeEvent method to minimize to tray +        """ +        if QtGui.QSystemTrayIcon.isSystemTrayAvailable() and \ +                e.type() == QtCore.QEvent.WindowStateChange and \ +                self.isMinimized(): +            self._toggle_visible() +            e.accept() +            return +        QtGui.QMainWindow.changeEvent(self, e) + +    def closeEvent(self, e): +        """ +        Reimplementation of closeEvent to close to tray +        """ +        if QtGui.QSystemTrayIcon.isSystemTrayAvailable() and \ +                not self._really_quit: +            self._toggle_visible() +            e.ignore() +            return +        QtGui.QMainWindow.closeEvent(self, e) + +    def _configured_providers(self): +        """ +        Returns the available providers based on the file structure + +        @rtype: list +        """ +        providers = os.listdir( +            os.path.join(self._provider_config.get_path_prefix(), +                         "leap", +                         "providers")) +        return providers + +    def _first_run(self): +        """ +        Returns True if there are no configured providers. False otherwise + +        @rtype: bool +        """ +        return len(self._configured_providers()) == 0 + +    def _focus_password(self): +        """ +        Focuses in the password lineedit +        """ +        self.ui.lnPassword.setFocus() + +    def _set_status(self, status): +        """ +        Sets the status label at the login stage to status + +        @param status: status message +        @type status: str +        """ +        self.ui.lblStatus.setText(status) + +    def _set_eip_status(self, status): +        """ +        Sets the status label at the VPN stage to status + +        @param status: status message +        @type status: str +        """ +        self.ui.lblEIPStatus.setText(status) + +    def _login_set_enabled(self, enabled=False): +        """ +        Enables or disables all the login widgets + +        @param enabled: wether they should be enabled or not +        @type enabled: bool +        """ +        self.ui.lnUser.setEnabled(enabled) +        self.ui.lnPassword.setEnabled(enabled) +        self.ui.btnLogin.setEnabled(enabled) +        self.ui.chkRemember.setEnabled(enabled) +        self.ui.cmbProviders.setEnabled(enabled) + +    def _download_provider_config(self): +        """ +        Starts the bootstrapping sequence. It will download the +        provider configuration if it's not present, otherwise will +        emit the corresponding signals inmediately +        """ +        provider = self.ui.cmbProviders.currentText() + +        self._provider_bootstrapper.start() +        self._provider_bootstrapper.run_provider_select_checks( +            provider, +            download_if_needed=True) + +    def _load_provider_config(self, data): +        """ +        SLOT +        TRIGGER: self._provider_bootstrapper.download_provider_info + +        Once the provider config has been downloaded, this loads the +        self._provider_config instance with it and starts the second +        part of the bootstrapping sequence + +        @param data: result from the last stage of the +        run_provider_select_checks +        @type data: dict +        """ +        if data[self._provider_bootstrapper.PASSED_KEY]: +            provider = self.ui.cmbProviders.currentText() +            if self._provider_config.loaded() or \ +                    self._provider_config.load(os.path.join("leap", +                                                            "providers", +                                                            provider, +                                                            "provider.json")): +                self._provider_bootstrapper.run_provider_setup_checks( +                    self._provider_config, +                    download_if_needed=True) +            else: +                self._set_status("Could not load provider configuration") +                self._login_set_enabled(True) +        else: +            self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) +            self._login_set_enabled(True) + +    def _login(self): +        """ +        SLOT +        TRIGGERS: +          self.ui.btnLogin.clicked +          self.ui.lnPassword.returnPressed + +        Starts the login sequence. Which involves bootstrapping the +        selected provider if the selection is valid (not empty), then +        start the SRP authentication, and as the last step +        bootstrapping the EIP service +        """ +        assert self._provider_config, "We need a provider config" + +        username = self.ui.lnUser.text() +        password = self.ui.lnPassword.text() +        provider = self.ui.cmbProviders.currentText() + +        if len(provider) == 0: +            self._set_status("Please select a valid provider") +            return + +        if len(username) == 0: +            self._set_status("Please provide a valid username") +            return + +        if len(password) == 0: +            self._set_status("Please provide a valid Password") +            return + +        self._set_status("Logging in...") +        self._login_set_enabled(False) + +        self._download_provider_config() + +    def _provider_config_loaded(self, data): +        """ +        SLOT +        TRIGGER: self._provider_bootstrapper.check_api_certificate + +        Once the provider configuration is loaded, this starts the SRP +        authentication +        """ +        assert self._provider_config, "We need a provider config!" + +        self._provider_bootstrapper.set_should_quit() + +        if data[self._provider_bootstrapper.PASSED_KEY]: +            username = self.ui.lnUser.text() +            password = self.ui.lnPassword.text() + +            if self._srp_auth is None: +                self._srp_auth = SRPAuth(self._provider_config) +                self._srp_auth.authentication_finished.connect( +                    self._authentication_finished) +                self._srp_auth.logout_finished.connect( +                    self._done_logging_out) + +            self._srp_auth.authenticate(username, password) +        else: +            self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) +            self._login_set_enabled(True) + +    def _authentication_finished(self, ok, message): +        """ +        SLOT +        TRIGGER: self._srp_auth.authentication_finished + +        Once the user is properly authenticated, try starting the EIP +        service +        """ +        self._set_status(message) +        if ok: +            self.ui.action_sign_out.setEnabled(True) +            # We leave a bit of room for the user to see the +            # "Succeeded" message and then we switch to the EIP status +            # panel +            QtCore.QTimer.singleShot(1000, self._switch_to_status) +        else: +            self._login_set_enabled(True) + +    def _switch_to_status(self): +        """ +        Changes the stackedWidget index to the EIP status one and +        triggers the eip bootstrapping +        """ +        self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX) +        self._download_eip_config() + +    def _download_eip_config(self): +        """ +        Starts the EIP bootstrapping sequence +        """ +        assert self._eip_bootstrapper, "We need an eip bootstrapper!" +        assert self._provider_config, "We need a provider config" + +        self._set_eip_status("Checking configuration, please wait...") + +        if self._provider_config.provides_eip(): +            self._eip_bootstrapper.start() +            self._eip_bootstrapper.run_eip_setup_checks( +                self._provider_config, +                download_if_needed=True) +        else: +            self._set_eip_status("%s does not support EIP" % +                                 (self._provider_config.get_domain(),)) + +    def _set_eip_status_icon(self, status): +        """ +        Given a status step from the VPN thread, set the icon properly + +        @param status: status step +        @type status: str +        """ +        selected_pixmap = self.ERROR_ICON +        if status in ("AUTH", "GET_CONFIG"): +            selected_pixmap = self.CONNECTING_ICON +        elif status in ("CONNECTED"): +            selected_pixmap = self.CONNECTED_ICON + +        self.ui.lblVPNStatusIcon.setPixmap(selected_pixmap) +        self._systray.setIcon(QtGui.QIcon(selected_pixmap)) + +    def _update_vpn_state(self, data): +        """ +        SLOT +        TRIGGER: self._vpn.state_changed + +        Updates the displayed VPN state based on the data provided by +        the VPN thread +        """ +        status = data[self._vpn.STATUS_STEP_KEY] +        self._set_eip_status_icon(status) +        if status == "AUTH": +            self._set_eip_status("VPN: Authenticating...") +        elif status == "GET_CONFIG": +            self._set_eip_status("VPN: Retrieving configuration...") +        elif status == "CONNECTED": +            self._set_eip_status("VPN: Connected!") +        else: +            self._set_eip_status(status) + +    def _update_vpn_status(self, data): +        """ +        SLOT +        TRIGGER: self._vpn.status_changed + +        Updates the download/upload labels based on the data provided +        by the VPN thread +        """ +        upload = float(data[self._vpn.TUNTAP_WRITE_KEY]) +        upload = upload / 1000.0 +        self.ui.lblUpload.setText("%s Kb" % (upload,)) +        download = float(data[self._vpn.TUNTAP_READ_KEY]) +        download = download / 1000.0 +        self.ui.lblDownload.setText("%s Kb" % (download,)) + +    def _start_eip(self, data): +        """ +        SLOT +        TRIGGER: self._eip_bootstrapper.download_client_certificate + +        Starts the VPN thread if the eip configuration is properly +        loaded +        """ +        assert self._eip_config, "We need an eip config!" +        assert self._provider_config, "We need a provider config!" + +        self._eip_bootstrapper.set_should_quit() +        if self._eip_config.loaded() or \ +                self._eip_config.load(os.path.join("leap", +                                                   "providers", +                                                   self._provider_config +                                                   .get_domain(), +                                                   "eip-service.json")): +            self._vpn.start(eipconfig=self._eip_config, +                            providerconfig=self._provider_config, +                            socket_host="/home/chiiph/vpnsock", +                            socket_port="unix") +        # TODO: display a message if the EIP configuration cannot be +        # loaded + +    def _logout(self): +        """ +        SLOT +        TRIGGER: self.ui.action_sign_out.triggered + +        Starts the logout sequence +        """ +        self._set_eip_status_icon("error") +        self._set_eip_status("Signing out...") +        self._srp_auth.logout() + +    def _done_logging_out(self, ok, message): +        """ +        SLOT +        TRIGGER: self._srp_auth.logout_finished + +        Switches the stackedWidget back to the login stage after +        logging out +        """ +        self._set_status(message) +        self.ui.action_sign_out.setEnabled(False) +        self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) +        self.ui.lnPassword.setText("") +        self._login_set_enabled(True) +        self._set_status("") +        self._vpn.set_should_quit() + +    def _intermediate_stage(self, data): +        """ +        SLOT +        TRIGGERS: +          self._provider_bootstrapper.name_resolution +          self._provider_bootstrapper.https_connection +          self._provider_bootstrapper.download_ca_cert +          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_set_enabled(True) +            self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) + +if __name__ == "__main__": +    import signal +    from functools import partial + +    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_())  | 
