summaryrefslogtreecommitdiff
path: root/src/leap/gui/mainwindow.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/gui/mainwindow.py')
-rw-r--r--src/leap/gui/mainwindow.py600
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_())