# -*- 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 . """ 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 from leap.util.check import leap_assert from leap.util.checkerthread import CheckerThread 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 GEOMETRY_KEY = "Geometry" WINDOWSTATE_KEY = "WindowState" 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 self._checker_thread = CheckerThread() self._checker_thread.start() # 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._checker_thread.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 self._wizard_firstrun = False if self._first_run(): self._wizard_firstrun = True self._wizard = Wizard(self._checker_thread) # 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) self._wizard.rejected.connect(self._rejected_wizard) else: self._finish_init() def _rejected_wizard(self): if self._wizard_firstrun: self.quit() else: self._finish_init() def _launch_wizard(self): if self._wizard is None: self._wizard = Wizard(self._checker_thread) 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 """ settings = QtCore.QSettings() geometry = settings.value(self.GEOMETRY_KEY, None) state = settings.value(self.WINDOWSTATE_KEY, None) if geometry is None: 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) else: self.restoreGeometry(geometry) if state is not None: self.restoreState(state) 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. " "More about LEAP" "") 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 settings = QtCore.QSettings() settings.setValue(self.GEOMETRY_KEY, self.saveGeometry()) settings.setValue(self.WINDOWSTATE_KEY, self.saveState()) QtGui.QMainWindow.closeEvent(self, e) def _configured_providers(self): """ Returns the available providers based on the file structure @rtype: list """ providers = [] try: providers = os.listdir( os.path.join(self._provider_config.get_path_prefix(), "leap", "providers")) except Exception as e: logger.debug("Error listing providers, assume there are none. %r" % (e,)) 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.run_provider_select_checks( self._checker_thread, 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._checker_thread, 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 """ leap_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 """ leap_assert(self._provider_config, "We need a provider config!") 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 """ leap_assert(self._eip_bootstrapper, "We need an eip bootstrapper!") leap_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.run_eip_setup_checks( self._checker_thread, 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 """ leap_assert(self._eip_config, "We need an eip config!") leap_assert(self._provider_config, "We need a provider config!") 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_())