From 6da8d09846db4d2eed01e488bc6a6f5ba48b959f Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 12 Aug 2013 13:25:44 +0200 Subject: move everything into bitmask namespace --- src/leap/bitmask/gui/__init__.py | 21 + src/leap/bitmask/gui/loggerwindow.py | 137 +++ src/leap/bitmask/gui/login.py | 245 +++++ src/leap/bitmask/gui/mainwindow.py | 1537 +++++++++++++++++++++++++++++++ src/leap/bitmask/gui/statuspanel.py | 459 +++++++++ src/leap/bitmask/gui/twisted_main.py | 60 ++ src/leap/bitmask/gui/ui/loggerwindow.ui | 155 ++++ src/leap/bitmask/gui/ui/login.ui | 132 +++ src/leap/bitmask/gui/ui/mainwindow.ui | 315 +++++++ src/leap/bitmask/gui/ui/statuspanel.ui | 289 ++++++ src/leap/bitmask/gui/ui/wizard.ui | 846 +++++++++++++++++ src/leap/bitmask/gui/wizard.py | 626 +++++++++++++ src/leap/bitmask/gui/wizardpage.py | 40 + 13 files changed, 4862 insertions(+) create mode 100644 src/leap/bitmask/gui/__init__.py create mode 100644 src/leap/bitmask/gui/loggerwindow.py create mode 100644 src/leap/bitmask/gui/login.py create mode 100644 src/leap/bitmask/gui/mainwindow.py create mode 100644 src/leap/bitmask/gui/statuspanel.py create mode 100644 src/leap/bitmask/gui/twisted_main.py create mode 100644 src/leap/bitmask/gui/ui/loggerwindow.ui create mode 100644 src/leap/bitmask/gui/ui/login.ui create mode 100644 src/leap/bitmask/gui/ui/mainwindow.ui create mode 100644 src/leap/bitmask/gui/ui/statuspanel.ui create mode 100644 src/leap/bitmask/gui/ui/wizard.ui create mode 100644 src/leap/bitmask/gui/wizard.py create mode 100644 src/leap/bitmask/gui/wizardpage.py (limited to 'src/leap/bitmask/gui') diff --git a/src/leap/bitmask/gui/__init__.py b/src/leap/bitmask/gui/__init__.py new file mode 100644 index 00000000..4b289442 --- /dev/null +++ b/src/leap/bitmask/gui/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# __init__.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 . +""" +init file for leap.gui +""" +app = __import__("app", globals(), locals(), [], 2) +__all__ = [app] diff --git a/src/leap/bitmask/gui/loggerwindow.py b/src/leap/bitmask/gui/loggerwindow.py new file mode 100644 index 00000000..fcbdbf19 --- /dev/null +++ b/src/leap/bitmask/gui/loggerwindow.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +# loggerwindow.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 . + +""" +History log window +""" +import logging + +from PySide import QtGui +from ui_loggerwindow import Ui_LoggerWindow +from leap.common.check import leap_assert, leap_assert_type +from leap.util.leap_log_handler import LeapLogHandler + +logger = logging.getLogger(__name__) + + +class LoggerWindow(QtGui.QDialog): + """ + Window that displays a history of the logged messages in the app. + """ + def __init__(self, handler): + """ + Initialize the widget with the custom handler. + + :param handler: Custom handler that supports history and signal. + :type handler: LeapLogHandler. + """ + QtGui.QDialog.__init__(self) + leap_assert(handler, "We need a handler for the logger window") + leap_assert_type(handler, LeapLogHandler) + + # Load UI + self.ui = Ui_LoggerWindow() + self.ui.setupUi(self) + + # Make connections + self.ui.btnSave.clicked.connect(self._save_log_to_file) + self.ui.btnDebug.toggled.connect(self._load_history), + self.ui.btnInfo.toggled.connect(self._load_history), + self.ui.btnWarning.toggled.connect(self._load_history), + self.ui.btnError.toggled.connect(self._load_history), + self.ui.btnCritical.toggled.connect(self._load_history) + + # Load logging history and connect logger with the widget + self._logging_handler = handler + self._connect_to_handler() + self._load_history() + + def _connect_to_handler(self): + """ + This method connects the loggerwindow with the handler through a + signal communicate the logger events. + """ + self._logging_handler.new_log.connect(self._add_log_line) + + def _add_log_line(self, log): + """ + Adds a line to the history, only if it's in the desired levels to show. + + :param log: a log record to be inserted in the widget + :type log: a dict with RECORD_KEY and MESSAGE_KEY. + the record contains the LogRecord of the logging module, + the message contains the formatted message for the log. + """ + html_style = { + logging.DEBUG: "background: #CDFFFF;", + logging.INFO: "background: white;", + logging.WARNING: "background: #FFFF66;", + logging.ERROR: "background: red; color: white;", + logging.CRITICAL: "background: red; color: white; font: bold;" + } + level = log[LeapLogHandler.RECORD_KEY].levelno + message = log[LeapLogHandler.MESSAGE_KEY] + message = message.replace('\n', '
\n') + + if self._logs_to_display[level]: + open_tag = "" + open_tag += "" + close_tag = "" + message = open_tag + message + close_tag + + self.ui.txtLogHistory.append(message) + + def _load_history(self): + """ + Load the previous logged messages in the widget. + They are stored in the custom handler. + """ + self._set_logs_to_display() + self.ui.txtLogHistory.clear() + history = self._logging_handler.log_history + for line in history: + self._add_log_line(line) + + def _set_logs_to_display(self): + """ + Sets the logs_to_display dict getting the toggled options from the ui + """ + self._logs_to_display = { + logging.DEBUG: self.ui.btnDebug.isChecked(), + logging.INFO: self.ui.btnInfo.isChecked(), + logging.WARNING: self.ui.btnWarning.isChecked(), + logging.ERROR: self.ui.btnError.isChecked(), + logging.CRITICAL: self.ui.btnCritical.isChecked() + } + + def _save_log_to_file(self): + """ + Lets the user save the current log to a file + """ + fileName, filtr = QtGui.QFileDialog.getSaveFileName( + self, self.tr("Save As")) + + if fileName: + try: + with open(fileName, 'w') as output: + output.write(self.ui.txtLogHistory.toPlainText()) + output.write('\n') + logger.debug('Log saved in %s' % (fileName, )) + except IOError, e: + logger.error("Error saving log file: %r" % (e, )) + else: + logger.debug('Log not saved!') diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py new file mode 100644 index 00000000..de0b2d50 --- /dev/null +++ b/src/leap/bitmask/gui/login.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- +# login.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 . + +""" +Login widget implementation +""" +import logging + +from PySide import QtCore, QtGui +from ui_login import Ui_LoginWidget + +from leap.util.keyring_helpers import has_keyring + +logger = logging.getLogger(__name__) + + +class LoginWidget(QtGui.QWidget): + """ + Login widget that emits signals to display the wizard or to + perform login. + """ + + # Emitted when the login button is clicked + login = QtCore.Signal() + cancel_login = QtCore.Signal() + + # Emitted when the user selects "Other..." in the provider + # combobox or click "Create Account" + show_wizard = QtCore.Signal() + + MAX_STATUS_WIDTH = 40 + + BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" + + def __init__(self, settings, parent=None): + """ + Constructs the LoginWidget. + + :param settings: client wide settings + :type settings: LeapSettings + :param parent: The parent widget for this widget + :type parent: QWidget or None + """ + QtGui.QWidget.__init__(self, parent) + + self._settings = settings + self._selected_provider_index = -1 + + self.ui = Ui_LoginWidget() + self.ui.setupUi(self) + + self.ui.chkRemember.stateChanged.connect( + self._remember_state_changed) + self.ui.chkRemember.setEnabled(has_keyring()) + + self.ui.lnPassword.setEchoMode(QtGui.QLineEdit.Password) + + self.ui.btnLogin.clicked.connect(self.login) + self.ui.lnPassword.returnPressed.connect(self.login) + + self.ui.lnUser.returnPressed.connect(self._focus_password) + + self.ui.cmbProviders.currentIndexChanged.connect( + self._current_provider_changed) + self.ui.btnCreateAccount.clicked.connect( + self.show_wizard) + + username_re = QtCore.QRegExp(self.BARE_USERNAME_REGEX) + self.ui.lnUser.setValidator( + QtGui.QRegExpValidator(username_re, self)) + + def _remember_state_changed(self, state): + """ + Saves the remember state in the LeapSettings + + :param state: possible stats can be Checked, Unchecked and + PartiallyChecked + :type state: QtCore.Qt.CheckState + """ + enable = True if state == QtCore.Qt.Checked else False + self._settings.set_remember(enable) + + def set_providers(self, provider_list): + """ + Set the provider list to provider_list plus an "Other..." item + that triggers the wizard + + :param provider_list: list of providers + :type provider_list: list of str + """ + self.ui.cmbProviders.blockSignals(True) + self.ui.cmbProviders.clear() + self.ui.cmbProviders.addItems(provider_list + [self.tr("Other...")]) + self.ui.cmbProviders.blockSignals(False) + + def select_provider_by_name(self, name): + """ + Given a provider name/domain, it selects it in the combobox + + :param name: name or domain for the provider + :type name: str + """ + provider_index = self.ui.cmbProviders.findText(name) + self.ui.cmbProviders.setCurrentIndex(provider_index) + + def get_selected_provider(self): + """ + Returns the selected provider in the combobox + """ + return self.ui.cmbProviders.currentText() + + def set_remember(self, value): + """ + Checks the remember user and password checkbox + + :param value: True to mark it checked, False otherwise + :type value: bool + """ + self.ui.chkRemember.setChecked(value) + + def get_remember(self): + """ + Returns the remember checkbox state + + :rtype: bool + """ + return self.ui.chkRemember.isChecked() + + def set_user(self, user): + """ + Sets the user and focuses on the next field, password. + + :param user: user to set the field to + :type user: str + """ + self.ui.lnUser.setText(user) + self._focus_password() + + def get_user(self): + """ + Returns the user that appears in the widget. + + :rtype: str + """ + return self.ui.lnUser.text() + + def set_password(self, password): + """ + Sets the password for the widget + + :param password: password to set + :type password: str + """ + self.ui.lnPassword.setText(password) + + def get_password(self): + """ + Returns the password that appears in the widget + + :rtype: str + """ + return self.ui.lnPassword.text() + + def set_status(self, status, error=True): + """ + Sets the status label at the login stage to status + + :param status: status message + :type status: str + """ + if len(status) > self.MAX_STATUS_WIDTH: + status = status[:self.MAX_STATUS_WIDTH] + "..." + if error: + status = "%s" % (status,) + self.ui.lblStatus.setText(status) + + def 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.chkRemember.setEnabled(enabled) + self.ui.cmbProviders.setEnabled(enabled) + + self._set_cancel(not enabled) + + def _set_cancel(self, enabled=False): + """ + Enables or disables the cancel action in the "log in" process. + + :param enabled: wether it should be enabled or not + :type enabled: bool + """ + text = self.tr("Cancel") + login_or_cancel = self.cancel_login + + if not enabled: + text = self.tr("Log In") + login_or_cancel = self.login + + self.ui.btnLogin.setText(text) + + self.ui.btnLogin.clicked.disconnect() + self.ui.btnLogin.clicked.connect(login_or_cancel) + + def _focus_password(self): + """ + Focuses in the password lineedit + """ + self.ui.lnPassword.setFocus() + + def _current_provider_changed(self, param): + """ + SLOT + TRIGGERS: self.ui.cmbProviders.currentIndexChanged + """ + if param == (self.ui.cmbProviders.count() - 1): + self.show_wizard.emit() + # Leave the previously selected provider in the combobox + prev_provider = 0 + if self._selected_provider_index != -1: + prev_provider = self._selected_provider_index + self.ui.cmbProviders.blockSignals(True) + self.ui.cmbProviders.setCurrentIndex(prev_provider) + self.ui.cmbProviders.blockSignals(False) + else: + self._selected_provider_index = param diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py new file mode 100644 index 00000000..5ace1043 --- /dev/null +++ b/src/leap/bitmask/gui/mainwindow.py @@ -0,0 +1,1537 @@ +# -*- 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 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.common.check import leap_assert +from leap.common.events import register +from leap.common.events import events_pb2 as proto +from leap.config.leapsettings import LeapSettings +from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpauth import SRPAuth +from leap.gui.loggerwindow import LoggerWindow +from leap.gui.wizard import Wizard +from leap.gui.login import LoginWidget +from leap.gui.statuspanel import StatusPanelWidget +from leap.services.eip.eipbootstrapper import EIPBootstrapper +from leap.services.eip.eipconfig import EIPConfig +from leap.services.eip.providerbootstrapper import ProviderBootstrapper +# XXX: Soledad might not work out of the box in Windows, issue #2932 +from leap.services.soledad.soledadbootstrapper import SoledadBootstrapper +from leap.services.mail.smtpbootstrapper import SMTPBootstrapper +from leap.services.mail import imap +from leap.platform_init import IS_WIN, IS_MAC +from leap.platform_init.initializers import init_platform + +from leap.services.eip.vpnprocess import VPN +from leap.services.eip.vpnprocess import OpenVPNAlreadyRunning +from leap.services.eip.vpnprocess import AlienOpenVPNAlreadyRunning + +from leap.services.eip.vpnlaunchers import VPNLauncherException +from leap.services.eip.vpnlaunchers import OpenVPNNotFoundException +from leap.services.eip.vpnlaunchers import EIPNoPkexecAvailable +from leap.services.eip.vpnlaunchers import EIPNoPolkitAuthAgentAvailable +from leap.services.eip.vpnlaunchers import EIPNoTunKextLoaded + +from leap.util import __version__ as VERSION +from leap.util.keyring_helpers import has_keyring + +from leap.services.mail.smtpconfig import SMTPConfig + +if IS_WIN: + from leap.platform_init.locks import WindowsLock + from leap.platform_init.locks import raise_window_ack + +from ui_mainwindow import Ui_MainWindow + +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 + + # Keyring + KEYRING_KEY = "bitmask" + + # SMTP + PORT_KEY = "port" + IP_KEY = "ip_address" + + OPENVPN_SERVICE = "openvpn" + MX_SERVICE = "mx" + + # Signals + new_updates = QtCore.Signal(object) + raise_window = QtCore.Signal([]) + soledad_ready = QtCore.Signal([]) + + # We use this flag to detect abnormal terminations + user_stopped_eip = False + + def __init__(self, quit_callback, + standalone=False, + openvpn_verb=1, + bypass_checks=False): + """ + Constructor for the client main window + + :param quit_callback: Function to be called when closing + 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 + :type bypass_checks: bool + """ + QtGui.QMainWindow.__init__(self) + + # register leap events + register(signal=proto.UPDATER_NEW_UPDATES, + callback=self._new_updates_available, + reqcbk=lambda req, resp: None) # make rpc call async + register(signal=proto.RAISE_WINDOW, + callback=self._on_raise_window_event, + reqcbk=lambda req, resp: None) # make rpc call async + + self._quit_callback = quit_callback + + self._updates_content = "" + + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + + self._settings = LeapSettings(standalone) + + self._login_widget = LoginWidget( + self._settings, + self.ui.stackedWidget.widget(self.LOGIN_INDEX)) + self.ui.loginLayout.addWidget(self._login_widget) + + # Signals + # TODO separate logic from ui signals. + + self._login_widget.login.connect(self._login) + self._login_widget.cancel_login.connect(self._cancel_login) + self._login_widget.show_wizard.connect( + self._launch_wizard) + + self.ui.btnShowLog.clicked.connect(self._show_logger_window) + + self._status_panel = StatusPanelWidget( + self.ui.stackedWidget.widget(self.EIP_STATUS_INDEX)) + self.ui.statusLayout.addWidget(self._status_panel) + + 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) + + # This is loaded only once, there's a bug when doing that more + # than once + ProviderConfig.standalone = standalone + EIPConfig.standalone = standalone + self._standalone = standalone + self._provider_config = ProviderConfig() + # Used for automatic start of EIP + self._provisional_provider_config = ProviderConfig() + self._eip_config = EIPConfig() + + self._already_started_eip = False + + # This is created once we have a valid provider config + self._srp_auth = None + self._logged_user = 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(bypass_checks) + + # 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._eip_intermediate_stage) + self._eip_bootstrapper.download_client_certificate.connect( + self._finish_eip_bootstrap) + + self._soledad_bootstrapper = SoledadBootstrapper() + self._soledad_bootstrapper.download_config.connect( + self._soledad_intermediate_stage) + self._soledad_bootstrapper.gen_key.connect( + self._soledad_bootstrapped_stage) + + self._smtp_bootstrapper = SMTPBootstrapper() + 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) + self.ui.action_quit.triggered.connect(self.quit) + self.ui.action_wizard.triggered.connect(self._launch_wizard) + self.ui.action_show_logs.triggered.connect(self._show_logger_window) + self.raise_window.connect(self._do_raise_mainwindow) + + # Used to differentiate between real quits and close to tray + self._really_quit = False + + 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_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_visible = QtGui.QAction(self.tr("Hide Main Window"), self) + self._action_visible.triggered.connect(self._toggle_visible) + + self._enabled_services = [] + + self._center_window() + + self.ui.lblNewUpdates.setVisible(False) + self.ui.btnMore.setVisible(False) + self.ui.btnMore.clicked.connect(self._updates_details) + + self.new_updates.connect(self._react_to_new_updates) + self.soledad_ready.connect(self._start_imap_service) + + init_platform() + + self._wizard = None + self._wizard_firstrun = False + + self._logger_window = None + + self._bypass_checks = bypass_checks + + self._soledad = None + self._keymanager = None + self._imap_service = None + + self._login_defer = None + self._download_provider_defer = None + + self._smtp_config = SMTPConfig() + + if self._first_run(): + self._wizard_firstrun = True + self._wizard = Wizard(standalone=standalone, + 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) + self._wizard.rejected.connect(self._rejected_wizard) + else: + self._finish_init() + + def _rejected_wizard(self): + """ + SLOT + TRIGGERS: self._wizard.rejected + + Called if the wizard has been cancelled or closed before + finishing. + """ + if self._wizard_firstrun: + self._settings.set_properprovider(False) + self.quit() + else: + self._finish_init() + + def _launch_wizard(self): + """ + SLOT + TRIGGERS: + self._login_widget.show_wizard + self.ui.action_wizard.triggered + + Also called in first run. + + Launches the wizard, creating the object itself if not already + there. + """ + if self._wizard is None: + self._wizard = Wizard(bypass_checks=self._bypass_checks) + self._wizard.accepted.connect(self._finish_init) + self._wizard.rejected.connect(self._wizard.close) + + self.setVisible(False) + # Do NOT use exec_, it will use a child event loop! + # Refer to http://www.themacaque.com/?p=1067 for funny details. + self._wizard.show() + if IS_MAC: + self._wizard.raise_() + self._wizard.finished.connect(self._wizard_finished) + + def _wizard_finished(self): + """ + SLOT + TRIGGERS + self._wizard.finished + + Called when the wizard has finished. + """ + self.setVisible(True) + + def _get_leap_logging_handler(self): + """ + Gets the leap handler from the top level logger + + :return: a logging handler or None + :rtype: LeapLogHandler or None + """ + from leap.util.leap_log_handler import LeapLogHandler + leap_logger = logging.getLogger('leap') + for h in leap_logger.handlers: + if isinstance(h, LeapLogHandler): + return h + return None + + def _show_logger_window(self): + """ + SLOT + TRIGGERS: + self.ui.action_show_logs.triggered + self.ui.btnShowLog.clicked + + Displays the window with the history of messages logged until now + and displays the new ones on arrival. + """ + if self._logger_window is None: + leap_log_handler = self._get_leap_logging_handler() + if leap_log_handler is None: + logger.error('Leap logger handler not found') + else: + self._logger_window = LoggerWindow(handler=leap_log_handler) + self._logger_window.setVisible( + not self._logger_window.isVisible()) + self.ui.btnShowLog.setChecked(self._logger_window.isVisible()) + else: + self._logger_window.setVisible(not self._logger_window.isVisible()) + self.ui.btnShowLog.setChecked(self._logger_window.isVisible()) + + self._logger_window.finished.connect(self._uncheck_logger_button) + + def _uncheck_logger_button(self): + """ + SLOT + Sets the checked state of the loggerwindow button to false. + """ + self.ui.btnShowLog.setChecked(False) + + def _new_updates_available(self, req): + """ + Callback for the new updates event + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + self.new_updates.emit(req) + + def _react_to_new_updates(self, req): + """ + SLOT + TRIGGER: self._new_updates_available + + Displays the new updates label and sets the updates_content + """ + self.moveToThread(QtCore.QCoreApplication.instance().thread()) + self.ui.lblNewUpdates.setVisible(True) + self.ui.btnMore.setVisible(True) + self._updates_content = req.content + + def _updates_details(self): + """ + SLOT + TRIGGER: self.ui.btnMore.clicked + + Parses and displays the updates details + """ + msg = self.tr("The Bitmask app is ready to update, please" + " restart the application.") + + # We assume that if there is nothing in the contents, then + # the Bitmask bundle is what needs updating. + if len(self._updates_content) > 0: + files = self._updates_content.split(", ") + files_str = "" + for f in files: + final_name = f.replace("/data/", "") + final_name = final_name.replace(".thp", "") + files_str += final_name + files_str += "\n" + msg += self.tr(" The following components will be updated:\n%s") \ + % (files_str,) + + QtGui.QMessageBox.information(self, + self.tr("Updates available"), + msg) + + def _finish_init(self): + """ + SLOT + TRIGGERS: + self._wizard.accepted + + Also called at the end of the constructor if not first run, + and after _rejected_wizard if not first run. + + Implements the behavior after either constructing the + mainwindow object, loading the saved user/password, or after + the wizard has been executed. + """ + # XXX: May be this can be divided into two methods? + + self._login_widget.set_providers(self._configured_providers()) + self._show_systray() + self.show() + if IS_MAC: + self.raise_() + + if self._wizard: + possible_username = self._wizard.get_username() + possible_password = self._wizard.get_password() + + # select the configured provider in the combo box + domain = self._wizard.get_domain() + self._login_widget.select_provider_by_name(domain) + + self._login_widget.set_remember(self._wizard.get_remember()) + self._enabled_services = list(self._wizard.get_services()) + self._settings.set_enabled_services( + self._login_widget.get_selected_provider(), + self._enabled_services) + if possible_username is not None: + self._login_widget.set_user(possible_username) + if possible_password is not None: + self._login_widget.set_password(possible_password) + self._login() + self._wizard = None + self._settings.set_properprovider(True) + else: + self._try_autostart_eip() + if not self._settings.get_remember(): + # nothing to do here + return + + saved_user = self._settings.get_user() + + try: + username, domain = saved_user.split('@') + except (ValueError, AttributeError) as e: + # if the saved_user does not contain an '@' or its None + logger.error('Username@provider malformed. %r' % (e, )) + saved_user = None + + if saved_user is not None and has_keyring(): + # fill the username + self._login_widget.set_user(username) + + # select the configured provider in the combo box + self._login_widget.select_provider_by_name(domain) + + self._login_widget.set_remember(True) + + saved_password = None + try: + saved_password = keyring.get_password(self.KEYRING_KEY, + saved_user + .encode("utf8")) + except ValueError, e: + logger.debug("Incorrect Password. %r." % (e,)) + + if saved_password is not None: + self._login_widget.set_password( + saved_password.decode("utf8")) + self._login() + + def _try_autostart_eip(self): + """ + Tries to autostart EIP + """ + default_provider = self._settings.get_defaultprovider() + + if default_provider is None: + logger.info("Cannot autostart Encrypted Internet because there is " + "no default provider configured") + return + + self._action_eip_provider.setText(default_provider) + + self._enabled_services = self._settings.get_enabled_services( + default_provider) + + if self._provisional_provider_config.load( + os.path.join("leap", + "providers", + default_provider, + "provider.json")): + self._download_eip_config() + else: + # XXX: Display a proper message to the user + logger.error("Unable to load %s config, cannot autostart." % + (default_provider,)) + + def _show_systray(self): + """ + Sets up the systray icon + """ + if self._systray is not None: + self._systray.setVisible(True) + return + + # Placeholder actions + # They are temporary to display the tray as designed + preferences_action = QtGui.QAction(self.tr("Preferences"), self) + preferences_action.setEnabled(False) + help_action = QtGui.QAction(self.tr("Help"), self) + help_action.setEnabled(False) + + 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) + systrayMenu.addSeparator() + systrayMenu.addAction(preferences_action) + systrayMenu.addAction(help_action) + systrayMenu.addSeparator() + systrayMenu.addAction(self.ui.action_log_out) + systrayMenu.addAction(self.ui.action_quit) + self._systray = QtGui.QSystemTrayIcon(self) + self._systray.setContextMenu(systrayMenu) + self._systray.setIcon(self._status_panel.ERROR_ICON_TRAY) + self._systray.setVisible(True) + self._systray.activated.connect(self._tray_activated) + + self._status_panel.set_systray(self._systray) + + def _tray_activated(self, reason=None): + """ + SLOT + TRIGGER: self._systray.activated + + Displays the context menu from the tray icon + """ + self._update_hideshow_menu() + + context_menu = self._systray.contextMenu() + if not IS_MAC: + # for some reason, context_menu.show() + # is failing in a way beyond my understanding. + # (not working the first time it's clicked). + # this works however. + context_menu.exec_(self._systray.geometry().center()) + + def _update_hideshow_menu(self): + """ + Updates the Hide/Show main window menu text based on the + visibility of the window. + """ + get_action = lambda visible: ( + self.tr("Show Main Window"), + self.tr("Hide Main Window"))[int(visible)] + + # set labels + visible = self.isVisible() + self._action_visible.setText(get_action(visible)) + + def _toggle_visible(self): + """ + SLOT + TRIGGER: self._action_visible.triggered + + Toggles the window visibility + """ + if not self.isVisible(): + self.show() + self.raise_() + else: + self.hide() + + self._update_hideshow_menu() + + def _center_window(self): + """ + Centers the mainwindow based on the desktop geometry + """ + geometry = self._settings.get_geometry() + state = self._settings.get_windowstate() + + 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): + """ + SLOT + TRIGGERS: self.ui.action_about_leap.triggered + + Display the About Bitmask dialog + """ + QtGui.QMessageBox.about( + self, self.tr("About Bitmask - %s") % (VERSION,), + self.tr("Version: %s
" + "
" + "Bitmask is the Desktop client application for " + "the LEAP platform, supporting encrypted internet " + "proxy, secure email, and secure chat (coming soon).
" + "
" + "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" + "") % (VERSION,)) + + 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 + + self._settings.set_geometry(self.saveGeometry()) + self._settings.set_windowstate(self.saveState()) + + QtGui.QMainWindow.closeEvent(self, e) + + def _configured_providers(self): + """ + Returns the available providers based on the file structure + + :rtype: list + """ + + # TODO: check which providers have a valid certificate among + # other things, not just the directories + 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 + """ + has_provider_on_disk = len(self._configured_providers()) != 0 + is_proper_provider = self._settings.get_properprovider() + return not (has_provider_on_disk and is_proper_provider) + + 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._login_widget.get_selected_provider() + + pb = self._provider_bootstrapper + d = pb.run_provider_select_checks(provider, download_if_needed=True) + self._download_provider_defer = d + + 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._login_widget.get_selected_provider() + + # If there's no loaded provider or + # we want to connect to other provider... + if (not self._provider_config.loaded() or + self._provider_config.get_domain() != provider): + self._provider_config.load( + os.path.join("leap", "providers", + provider, "provider.json")) + + if self._provider_config.loaded(): + self._provider_bootstrapper.run_provider_setup_checks( + self._provider_config, + download_if_needed=True) + else: + self._login_widget.set_status( + self.tr("Unable to login: Problem with provider")) + logger.error("Could not load provider configuration.") + self._login_widget.set_enabled(True) + else: + self._login_widget.set_status( + self.tr("Unable to login: Problem with provider")) + logger.error(data[self._provider_bootstrapper.ERROR_KEY]) + self._login_widget.set_enabled(True) + + def _login(self): + """ + SLOT + TRIGGERS: + self._login_widget.login + + 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._login_widget.get_user() + password = self._login_widget.get_password() + provider = self._login_widget.get_selected_provider() + + self._enabled_services = self._settings.get_enabled_services( + self._login_widget.get_selected_provider()) + + if len(provider) == 0: + self._login_widget.set_status( + self.tr("Please select a valid provider")) + return + + if len(username) == 0: + self._login_widget.set_status( + self.tr("Please provide a valid username")) + return + + if len(password) == 0: + self._login_widget.set_status( + self.tr("Please provide a valid Password")) + return + + self._login_widget.set_status(self.tr("Logging in..."), error=False) + self._login_widget.set_enabled(False) + + if self._login_widget.get_remember() and has_keyring(): + # in the keyring and in the settings + # we store the value 'usename@provider' + username_domain = (username + '@' + provider).encode("utf8") + try: + keyring.set_password(self.KEYRING_KEY, + username_domain, + password.encode("utf8")) + # Only save the username if it was saved correctly in + # the keyring + self._settings.set_user(username_domain) + except Exception as e: + logger.error("Problem saving data to keyring. %r" + % (e,)) + + self._download_provider_config() + + def _cancel_login(self): + """ + SLOT + TRIGGERS: + self._login_widget.cancel_login + + Stops the login sequence. + """ + logger.debug("Cancelling log in.") + + if self._download_provider_defer: + logger.debug("Cancelling download provider defer.") + self._download_provider_defer.cancel() + + if self._login_defer: + logger.debug("Cancelling login defer.") + self._login_defer.cancel() + + 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._login_widget.get_user().encode("utf8") + password = self._login_widget.get_password().encode("utf8") + + 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) + + # TODO: Add errback! + self._login_defer = self._srp_auth.authenticate(username, password) + else: + self._login_widget.set_status( + "Unable to login: Problem with provider") + logger.error(data[self._provider_bootstrapper.ERROR_KEY]) + self._login_widget.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 + """ + + # In general we want to "filter" likely complicated error + # messages, but in this case, the messages make more sense as + # they come. Since they are "Unknown user" or "Unknown + # password" + self._login_widget.set_status(message, error=not ok) + + if ok: + self._logged_user = self._login_widget.get_user() + self.ui.action_log_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) + self._login_defer = None + else: + self._login_widget.set_enabled(True) + + def _switch_to_status(self): + """ + Changes the stackedWidget index to the EIP status one and + triggers the eip bootstrapping + """ + if not self._already_started_eip: + self._status_panel.set_provider( + "%s@%s" % (self._login_widget.get_user(), + self._get_best_provider_config().get_domain())) + + self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX) + + 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) + + self._download_eip_config() + + def _soledad_intermediate_stage(self, data): + """ + SLOT + TRIGGERS: + self._soledad_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._soledad_bootstrapper.PASSED_KEY] + if not passed: + # TODO: display in the GUI: + # should pass signal to a slot in status_panel + # that sets the global status + logger.warning("Soledad failed to start: %s" % + (data[self._soledad_bootstrapper.ERROR_KEY],)) + + def _soledad_bootstrapped_stage(self, data): + """ + SLOT + TRIGGERS: + self._soledad_bootstrapper.gen_key + + If there was a problem, displays it, otherwise it does nothing. + This is used for intermediate bootstrapping stages, in case + they fail. + + :param data: result from the bootstrapping stage for Soledad + :type data: dict + """ + passed = data[self._soledad_bootstrapper.PASSED_KEY] + if not passed: + logger.error(data[self._soledad_bootstrapper.ERROR_KEY]) + return + + logger.debug("Done bootstrapping Soledad") + + self._soledad = self._soledad_bootstrapper.soledad + self._keymanager = self._soledad_bootstrapper.keymanager + + # Ok, now soledad is ready, so we can allow other things that + # depend on soledad to start. + + # this will trigger start_imap_service + self.soledad_ready.emit() + + # TODO connect all these activations to the soledad_ready + # signal so the logic is clearer to follow. + + if self._provider_config.provides_mx() and \ + self._enabled_services.count(self.MX_SERVICE) > 0: + self._smtp_bootstrapper.run_smtp_setup_checks( + 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 + + def _smtp_bootstrapped_stage(self, data): + """ + SLOT + TRIGGERS: + self._smtp_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. + + :param data: result from the bootstrapping stage for Soledad + :type data: dict + """ + passed = data[self._smtp_bootstrapper.PASSED_KEY] + if not passed: + logger.error(data[self._smtp_bootstrapper.ERROR_KEY]) + return + logger.debug("Done bootstrapping SMTP") + + hosts = self._smtp_config.get_hosts() + # TODO: handle more than one host and define how to choose + if len(hosts) > 0: + hostname = hosts.keys()[0] + logger.debug("Using hostname %s for SMTP" % (hostname,)) + host = hosts[hostname][self.IP_KEY].encode("utf-8") + port = hosts[hostname][self.PORT_KEY] + # TODO: pick local smtp port in a better way + # TODO: Make the encrypted_only configurable + + from leap.mail.smtp import setup_smtp_relay + client_cert = self._eip_config.get_client_cert_path( + self._provider_config) + 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) + + def _start_imap_service(self): + """ + SLOT + TRIGGERS: + soledad_ready + """ + logger.debug('Starting imap service') + + self._imap_service = imap.start_imap_service( + self._soledad, + self._keymanager) + + def _get_socket_host(self): + """ + Returns the socket and port to be used for VPN + + :rtype: tuple (str, str) (host, port) + """ + + # TODO: make this properly multiplatform + + if platform.system() == "Windows": + host = "localhost" + port = "9876" + else: + host = os.path.join(tempfile.mkdtemp(prefix="leap-tmp"), + 'openvpn.socket') + port = "unix" + + return host, port + + def _start_eip(self): + """ + SLOT + TRIGGERS: + self._status_panel.start_eip + self._action_eip_startstop.triggered + or called from _finish_eip_bootstrap + + Starts EIP + """ + 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() + 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() + if self._logged_user is not None: + provider = "%s@%s" % (self._logged_user, provider) + + 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) + except EIPNoPolkitAuthAgentAvailable: + self._status_panel.set_global_status( + # XXX this should change to polkit-kde where + # applicable. + self.tr("We could not find any " + "authentication " + "agent in your system.
" + "Make sure you have " + "polkit-gnome-authentication-" + "agent-1 " + "running and try again."), + error=True) + self._set_eipstatus_off() + except EIPNoTunKextLoaded: + self._status_panel.set_global_status( + self.tr("Encrypted Internet cannot be started because " + "the tuntap extension is not installed properly " + "in your system.")) + self._set_eipstatus_off() + except EIPNoPkexecAvailable: + self._status_panel.set_global_status( + self.tr("We could not find pkexec " + "in your system."), + error=True) + self._set_eipstatus_off() + except OpenVPNNotFoundException: + self._status_panel.set_global_status( + self.tr("We could not find openvpn binary."), + error=True) + self._set_eipstatus_off() + except OpenVPNAlreadyRunning as e: + self._status_panel.set_global_status( + self.tr("Another openvpn instance is already running, and " + "could not be stopped."), + error=True) + self._set_eipstatus_off() + except AlienOpenVPNAlreadyRunning as e: + self._status_panel.set_global_status( + self.tr("Another openvpn instance is already running, and " + "could not be stopped because it was not launched by " + "Bitmask. Please stop it and try again."), + error=True) + self._set_eipstatus_off() + except VPNLauncherException as e: + # XXX We should implement again translatable exceptions so + # we can pass a translatable string to the panel (usermessage attr) + self._status_panel.set_global_status("%s" % (e,), error=True) + self._set_eipstatus_off() + 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) + + def _stop_eip(self, abnormal=False): + """ + SLOT + TRIGGERS: + self._status_panel.stop_eip + self._action_eip_startstop.triggered + or called from _eip_finished + + Stops vpn process and makes gui adjustments to reflect + the change of state. + + :param abnormal: whether this was an abnormal termination. + :type abnormal: bool + """ + if abnormal: + logger.warning("Abnormal EIP termination.") + + self.user_stopped_eip = True + self._vpn.terminate() + + self._set_eipstatus_off() + + self._already_started_eip = False + 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): + """ + 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 + """ + 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 + + def _download_eip_config(self): + """ + Starts the EIP bootstrapping sequence + """ + leap_assert(self._eip_bootstrapper, "We need an eip bootstrapper!") + + provider_config = self._get_best_provider_config() + + if provider_config.provides_eip() and \ + self._enabled_services.count(self.OPENVPN_SERVICE) > 0 and \ + not self._already_started_eip: + + self._status_panel.set_eip_status( + self.tr("Starting...")) + self._eip_bootstrapper.run_eip_setup_checks( + provider_config, + download_if_needed=True) + self._already_started_eip = True + elif not self._already_started_eip: + if self._enabled_services.count(self.OPENVPN_SERVICE) > 0: + self._status_panel.set_eip_status( + self.tr("Not supported"), + 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): + """ + 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!") + passed = data[self._eip_bootstrapper.PASSED_KEY] + + if not passed: + error_msg = self.tr("There was a problem with the provider") + self._status_panel.set_eip_status(error_msg, error=True) + logger.error(data[self._eip_bootstrapper.ERROR_KEY]) + self._already_started_eip = False + return + + provider_config = self._get_best_provider_config() + + domain = provider_config.get_domain() + + loaded = self._eip_config.loaded() + if not loaded: + eip_config_path = os.path.join("leap", "providers", + domain, "eip-service.json") + api_version = provider_config.get_api_version() + self._eip_config.set_api_version(api_version) + loaded = self._eip_config.load(eip_config_path) + + if loaded: + self._start_eip() + else: + self._status_panel.set_eip_status( + self.tr("Could not load Encrypted Internet " + "Configuration."), + error=True) + + def _logout(self): + """ + SLOT + TRIGGER: self.ui.action_log_out.triggered + + Starts the logout sequence + """ + # XXX: If other defers are doing authenticated stuff, this + # might conflict with those. CHECK! + threads.deferToThread(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._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("") + + 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_widget.set_enabled(True) + self._login_widget.set_status( + self.tr("Unable to connect: Problem with provider")) + logger.error(data[self._provider_bootstrapper.ERROR_KEY]) + + def _eip_intermediate_stage(self, data): + """ + SLOT + TRIGGERS: + 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_widget.set_status( + self.tr("Unable to connect: Problem with provider")) + logger.error(data[self._provider_bootstrapper.ERROR_KEY]) + self._already_started_eip = False + + def _eip_finished(self, exitCode): + """ + SLOT + TRIGGERS: + self._vpn.process_finished + + Triggered when the EIP/VPN process finishes to set the UI + accordingly. + """ + logger.info("VPN process finished with exitCode %s..." + % (exitCode,)) + + # Ideally we would have the right exit code here, + # but the use of different wrappers (pkexec, cocoasudo) swallows + # the openvpn exit code so we get zero exit in some cases where we + # shouldn't. As a workaround we just use a flag to indicate + # a purposeful switch off, and mark everything else as unexpected. + + # In the near future we should trigger a native notification from here, + # since the user really really wants to know she is unprotected asap. + # And the right thing to do will be to fail-close. + + # TODO we should have a way of parsing the latest lines in the vpn + # log buffer so we can have a more precise idea of which type + # of error did we have (server side, local problem, etc) + abnormal = True + + # XXX check if these exitCodes are pkexec/cocoasudo specific + if exitCode in (126, 127): + self._status_panel.set_global_status( + self.tr("Encrypted Internet could not be launched " + "because you did not authenticate properly."), + error=True) + self._vpn.killit() + elif exitCode != 0 or not self.user_stopped_eip: + self._status_panel.set_global_status( + self.tr("Encrypted Internet finished in an " + "unexpected manner!"), error=True) + else: + abnormal = False + 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) + + def _on_raise_window_event(self, req): + """ + Callback for the raise window event + """ + if IS_WIN: + raise_window_ack() + self.raise_window.emit() + + def _do_raise_mainwindow(self): + """ + SLOT + TRIGGERS: + self._on_raise_window_event + + Triggered when we receive a RAISE_WINDOW event. + """ + TOPFLAG = QtCore.Qt.WindowStaysOnTopHint + self.setWindowFlags(self.windowFlags() | TOPFLAG) + self.show() + self.setWindowFlags(self.windowFlags() & ~TOPFLAG) + self.show() + if IS_MAC: + self.raise_() + + def _cleanup_pidfiles(self): + """ + Removes lockfiles on a clean shutdown. + + Triggered after aboutToQuit signal. + """ + if IS_WIN: + WindowsLock.release_all_locks() + + def _cleanup_and_quit(self): + """ + Call all the cleanup actions in a serialized way. + Should be called from the quit function. + """ + logger.debug('About to quit, doing cleanup...') + + if self._imap_service is not None: + self._imap_service.stop() + + if self._srp_auth is not None: + if self._srp_auth.get_session_id() is not None or \ + self._srp_auth.get_token() is not None: + # XXX this can timeout after loong time: See #3368 + self._srp_auth.logout() + + if self._soledad: + logger.debug("Closing soledad...") + self._soledad.close() + else: + logger.error("No instance of soledad was found.") + + logger.debug('Terminating vpn') + self._vpn.terminate(shutdown=True) + + if self._login_defer: + logger.debug("Cancelling login defer.") + self._login_defer.cancel() + + if self._download_provider_defer: + logger.debug("Cancelling download provider defer.") + self._download_provider_defer.cancel() + + # TODO missing any more cancels? + + logger.debug('Cleaning pidfiles') + self._cleanup_pidfiles() + + def quit(self): + """ + Cleanup and tidely close the main window before quitting. + """ + # TODO: separate the shutting down of services from the + # UI stuff. + self._cleanup_and_quit() + + self._really_quit = True + + if self._wizard: + self._wizard.close() + + if self._logger_window: + self._logger_window.close() + + self.close() + + if self._quit_callback: + 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/statuspanel.py b/src/leap/bitmask/gui/statuspanel.py new file mode 100644 index 00000000..f3424c7c --- /dev/null +++ b/src/leap/bitmask/gui/statuspanel.py @@ -0,0 +1,459 @@ +# -*- coding: utf-8 -*- +# statuspanel.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 . + +""" +Status Panel widget implementation +""" +import logging + +from datetime import datetime +from functools import partial +from PySide import QtCore, QtGui + +from ui_statuspanel import Ui_StatusPanel + +from leap.common.check import leap_assert_type +from leap.services.eip.vpnprocess import VPNManager +from leap.platform_init import IS_WIN, IS_LINUX +from leap.util import first + +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" + + def __init__(self, parent=None): + QtGui.QWidget.__init__(self, parent) + + self._systray = None + self._action_eip_status = None + + self.ui = Ui_StatusPanel() + self.ui.setupUi(self) + + self.ui.btnEipStartStop.setEnabled(False) + self.ui.btnEipStartStop.clicked.connect( + self.start_eip) + + self.hide_status_box() + + # Set the EIP status icons + self.CONNECTING_ICON = None + self.CONNECTED_ICON = None + self.ERROR_ICON = None + self.CONNECTING_ICON_TRAY = None + self.CONNECTED_ICON_TRAY = None + self.ERROR_ICON_TRAY = None + self._set_eip_icons() + + self._set_traffic_rates() + self._make_status_clickable() + + def _make_status_clickable(self): + """ + Makes upload and download figures clickable. + """ + onclicked = self._on_VPN_status_clicked + self.ui.btnUpload.clicked.connect(onclicked) + self.ui.btnDownload.clicked.connect(onclicked) + + def _on_VPN_status_clicked(self): + """ + SLOT + TRIGGER: self.ui.btnUpload.clicked + self.ui.btnDownload.clicked + + Toggles between rate and total throughput display for vpn + status figures. + """ + self.DISPLAY_TRAFFIC_RATES = not self.DISPLAY_TRAFFIC_RATES + self.update_vpn_status(None) # refresh + + def _set_traffic_rates(self): + """ + Initializes up and download rates. + """ + self._up_rate = RateMovingAverage() + self._down_rate = RateMovingAverage() + + self.ui.btnUpload.setText(self.RATE_STR % (0,)) + self.ui.btnDownload.setText(self.RATE_STR % (0,)) + + def _reset_traffic_rates(self): + """ + Resets up and download rates, and cleans up the labels. + """ + self._up_rate.reset() + self._down_rate.reset() + self.update_vpn_status(None) + + def _update_traffic_rates(self, up, down): + """ + Updates up and download rates. + + :param up: upload total. + :type up: int + :param down: download total. + :type down: int + """ + ts = datetime.now() + self._up_rate.append((ts, up)) + self._down_rate.append((ts, down)) + + def _get_traffic_rates(self): + """ + Gets the traffic rates (in KB/s). + + :returns: a tuple with the (up, down) rates + :rtype: tuple + """ + up = self._up_rate + down = self._down_rate + + return (up.get_average(), down.get_average()) + + def _get_traffic_totals(self): + """ + Gets the traffic total throughput (in Kb). + + :returns: a tuple with the (up, down) totals + :rtype: tuple + """ + up = self._up_rate + down = self._down_rate + + return (up.get_total(), down.get_total()) + + def _set_eip_icons(self): + """ + Sets the EIP status icons for the main window and for the tray + + MAC : dark icons + LINUX : dark icons in window, light icons in tray + WIN : light icons + """ + EIP_ICONS = EIP_ICONS_TRAY = ( + ":/images/conn_connecting-light.png", + ":/images/conn_connected-light.png", + ":/images/conn_error-light.png") + + if IS_LINUX: + EIP_ICONS_TRAY = ( + ":/images/conn_connecting.png", + ":/images/conn_connected.png", + ":/images/conn_error.png") + elif IS_WIN: + EIP_ICONS = EIP_ICONS_TRAY = ( + ":/images/conn_connecting.png", + ":/images/conn_connected.png", + ":/images/conn_error.png") + + self.CONNECTING_ICON = QtGui.QPixmap(EIP_ICONS[0]) + self.CONNECTED_ICON = QtGui.QPixmap(EIP_ICONS[1]) + self.ERROR_ICON = QtGui.QPixmap(EIP_ICONS[2]) + + self.CONNECTING_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[0]) + self.CONNECTED_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[1]) + self.ERROR_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[2]) + + def set_systray(self, systray): + """ + Sets the systray object to use. + + :param systray: Systray object + :type systray: QtGui.QSystemTrayIcon + """ + leap_assert_type(systray, QtGui.QSystemTrayIcon) + self._systray = systray + + def set_action_eip_startstop(self, action_eip_startstop): + """ + Sets the action_eip_startstop to use. + + :param action_eip_startstop: action_eip_status to be used + :type action_eip_startstop: QtGui.QAction + """ + self._action_eip_startstop = action_eip_startstop + + def set_action_eip_status(self, action_eip_status): + """ + Sets the action_eip_status to use. + + :param action_eip_status: action_eip_status to be used + :type action_eip_status: QtGui.QAction + """ + leap_assert_type(action_eip_status, QtGui.QAction) + self._action_eip_status = action_eip_status + + def set_global_status(self, status, error=False): + """ + Sets the global status label. + + :param status: status message + :type status: str or unicode + :param error: if the status is an erroneous one, then set this + to True + :type error: bool + """ + leap_assert_type(error, bool) + if error: + status = "%s" % (status,) + self.ui.lblGlobalStatus.setText(status) + self.ui.globalStatusBox.show() + + def hide_status_box(self): + """ + Hide global status box. + """ + self.ui.globalStatusBox.hide() + + def set_eip_status(self, status, error=False): + """ + Sets the status label at the VPN stage to status + + :param status: status message + :type status: str or unicode + :param error: if the status is an erroneous one, then set this + to True + :type error: bool + """ + leap_assert_type(error, bool) + + self._systray.setToolTip(status) + if error: + status = "%s" % (status,) + self.ui.lblEIPStatus.setText(status) + + def set_startstop_enabled(self, value): + """ + Enable or disable btnEipStartStop and _action_eip_startstop + based on value + + :param value: True for enabled, False otherwise + :type value: bool + """ + leap_assert_type(value, bool) + 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) + + def eip_started(self): + """ + Sets the state of the widget to how it should look after EIP + has started + """ + self.ui.btnEipStartStop.setText(self.tr("Turn OFF")) + self.ui.btnEipStartStop.disconnect(self) + self.ui.btnEipStartStop.clicked.connect( + self.stop_eip) + + def eip_stopped(self): + """ + Sets the state of the widget to how it should look after EIP + has stopped + """ + self._reset_traffic_rates() + 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) + + def update_vpn_status(self, data): + """ + SLOT + TRIGGER: VPN.status_changed + + Updates the download/upload labels based on the data provided + by the VPN thread. + + :param data: a dictionary with the tcp/udp write and read totals. + If data is None, we just will refresh the display based + on the previous data. + :type data: dict + """ + if data: + upload = float(data[VPNManager.TCPUDP_WRITE_KEY] or "0") + download = float(data[VPNManager.TCPUDP_READ_KEY] or "0") + self._update_traffic_rates(upload, download) + + if self.DISPLAY_TRAFFIC_RATES: + uprate, downrate = self._get_traffic_rates() + upload_str = self.RATE_STR % (uprate,) + download_str = self.RATE_STR % (downrate,) + + else: # display total throughput + uptotal, downtotal = self._get_traffic_totals() + upload_str = self.TOTAL_STR % (uptotal,) + download_str = self.TOTAL_STR % (downtotal,) + + self.ui.btnUpload.setText(upload_str) + self.ui.btnDownload.setText(download_str) + + def update_vpn_state(self, data): + """ + SLOT + TRIGGER: VPN.state_changed + + Updates the displayed VPN state based on the data provided by + the VPN thread + """ + status = data[VPNManager.STATUS_STEP_KEY] + self.set_eip_status_icon(status) + if status == "CONNECTED": + self.set_eip_status(self.tr("ON")) + # Only now we can properly enable the button. + self.set_startstop_enabled(True) + elif status == "AUTH": + self.set_eip_status(self.tr("Authenticating...")) + elif status == "GET_CONFIG": + self.set_eip_status(self.tr("Retrieving configuration...")) + elif status == "WAIT": + self.set_eip_status(self.tr("Waiting to start...")) + elif status == "ASSIGN_IP": + self.set_eip_status(self.tr("Assigning IP")) + 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, partial(self.set_global_status, + self.tr("Unable to start VPN, " + "it's already " + "running."))) + else: + self.set_eip_status(status) + + 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 + selected_pixmap_tray = self.ERROR_ICON_TRAY + tray_message = self.tr("Encryption 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") + elif status in ("CONNECTED"): + tray_message = self.tr("Encryption is ON") + selected_pixmap = self.CONNECTED_ICON + selected_pixmap_tray = self.CONNECTED_ICON_TRAY + + self.set_icon(selected_pixmap) + self._systray.setIcon(QtGui.QIcon(selected_pixmap_tray)) + self._action_eip_status.setText(tray_message) + + def set_provider(self, provider): + self.ui.lblProvider.setText(provider) diff --git a/src/leap/bitmask/gui/twisted_main.py b/src/leap/bitmask/gui/twisted_main.py new file mode 100644 index 00000000..c7add3ee --- /dev/null +++ b/src/leap/bitmask/gui/twisted_main.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# twisted_main.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 functions for integration of twisted reactor +""" +import logging + +from twisted.internet import error + +# Resist the temptation of putting the import reactor here, +# it will raise an "reactor already imported" error. + +logger = logging.getLogger(__name__) + + +def start(app): + """ + Start the mainloop. + + :param app: the main qt QApplication instance. + :type app: QtCore.QApplication + """ + from twisted.internet import reactor + logger.debug('starting twisted reactor') + + # this seems to be troublesome under some + # unidentified settings. + #reactor.run() + + reactor.runReturn() + app.exec_() + + +def quit(app): + """ + Stop the mainloop. + + :param app: the main qt QApplication instance. + :type app: QtCore.QApplication + """ + from twisted.internet import reactor + logger.debug('stopping twisted reactor') + try: + reactor.stop() + except error.ReactorNotRunning: + logger.debug('reactor not running') diff --git a/src/leap/bitmask/gui/ui/loggerwindow.ui b/src/leap/bitmask/gui/ui/loggerwindow.ui new file mode 100644 index 00000000..b08428a9 --- /dev/null +++ b/src/leap/bitmask/gui/ui/loggerwindow.ui @@ -0,0 +1,155 @@ + + + LoggerWindow + + + + 0 + 0 + 648 + 469 + + + + Logs + + + + :/images/mask-icon.png:/images/mask-icon.png + + + + + + + + + + + Debug + + + + :/images/oxygen-icons/script-error.png:/images/oxygen-icons/script-error.png + + + true + + + true + + + true + + + + + + + Info + + + + :/images/oxygen-icons/dialog-information.png:/images/oxygen-icons/dialog-information.png + + + true + + + true + + + true + + + + + + + Warning + + + + :/images/oxygen-icons/dialog-warning.png:/images/oxygen-icons/dialog-warning.png + + + true + + + true + + + true + + + + + + + Error + + + + :/images/oxygen-icons/dialog-error.png:/images/oxygen-icons/dialog-error.png + + + true + + + true + + + true + + + + + + + Critical + + + + :/images/oxygen-icons/edit-bomb.png:/images/oxygen-icons/edit-bomb.png + + + true + + + true + + + true + + + + + + + Save to file + + + + :/images/oxygen-icons/document-save-as.png:/images/oxygen-icons/document-save-as.png + + + + + + + + + btnDebug + btnInfo + btnWarning + btnError + btnCritical + btnSave + txtLogHistory + + + + + + + diff --git a/src/leap/bitmask/gui/ui/login.ui b/src/leap/bitmask/gui/ui/login.ui new file mode 100644 index 00000000..42a6897a --- /dev/null +++ b/src/leap/bitmask/gui/ui/login.ui @@ -0,0 +1,132 @@ + + + LoginWidget + + + + 0 + 0 + 356 + 223 + + + + Form + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Create a new account + + + + + + + <b>Provider:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + + Remember username and password + + + + + + + <b>Username:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + <b>Password:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Log In + + + + + + + + + + Qt::AlignCenter + + + true + + + + + + + cmbProviders + lnUser + lnPassword + chkRemember + btnLogin + btnCreateAccount + + + + diff --git a/src/leap/bitmask/gui/ui/mainwindow.ui b/src/leap/bitmask/gui/ui/mainwindow.ui new file mode 100644 index 00000000..ecd3cbe9 --- /dev/null +++ b/src/leap/bitmask/gui/ui/mainwindow.ui @@ -0,0 +1,315 @@ + + + MainWindow + + + + 0 + 0 + 429 + 579 + + + + Bitmask + + + + :/images/mask-icon.png:/images/mask-icon.png + + + Qt::ImhHiddenText + + + + 128 + 128 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + There are new updates available, please restart. + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + More... + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + 1 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + false + + + + + + :/images/mask-launcher.png + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Show Log + + + true + + + false + + + true + + + + + + + + + + + 0 + 0 + 429 + 21 + + + + + &Session + + + + + + + + Help + + + + + + + + + + + + Log &out + + + + + &Quit + + + + + About &Bitmask + + + + + &Help + + + + + &Wizard + + + + + Show &logs + + + + + + + + + diff --git a/src/leap/bitmask/gui/ui/statuspanel.ui b/src/leap/bitmask/gui/ui/statuspanel.ui new file mode 100644 index 00000000..3482ac7c --- /dev/null +++ b/src/leap/bitmask/gui/ui/statuspanel.ui @@ -0,0 +1,289 @@ + + + StatusPanel + + + + 0 + 0 + 384 + 477 + + + + Form + + + + + + font: bold; + + + user@domain.org + + + true + + + + + + + true + + + + + + + + + + + Encrypted Internet: + + + + + + + font: bold; + + + Off + + + Qt::AutoText + + + Qt::AlignCenter + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Turn On + + + + + + + + + Qt::Vertical + + + QSizePolicy::Preferred + + + + 0 + 11 + + + + + + + + + 64 + 64 + + + + + + + :/images/light/64/network-eip-down.png + + + Qt::AlignCenter + + + + + + + 4 + + + QLayout::SetDefaultConstraint + + + + + + + + :/images/light/16/down-arrow.png + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 120 + 16777215 + + + + PointingHandCursor + + + 0.0 KB/s + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + + + :/images/light/16/up-arrow.png + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 120 + 16777215 + + + + PointingHandCursor + + + 0.0 KB/s + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + false + + + + + + false + + + ... + + + true + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + diff --git a/src/leap/bitmask/gui/ui/wizard.ui b/src/leap/bitmask/gui/ui/wizard.ui new file mode 100644 index 00000000..a8f66bbc --- /dev/null +++ b/src/leap/bitmask/gui/ui/wizard.ui @@ -0,0 +1,846 @@ + + + Wizard + + + + 0 + 0 + 536 + 452 + + + + Bitmask first run + + + + :/images/mask-icon.png:/images/mask-icon.png + + + true + + + QWizard::ModernStyle + + + QWizard::IndependentPages + + + + Welcome + + + This is the Bitmask first run wizard + + + 0 + + + + + + Log In with my credentials + + + + + + + <html><head/><body><p>Now we will guide you through some configuration that is needed before you can connect for the first time.</p><p>If you ever need to modify these options again, you can find the wizard in the <span style=" font-style:italic;">'Settings'</span> menu from the main window.</p><p>Do you want to <span style=" font-weight:600;">sign up</span> for a new account, or <span style=" font-weight:600;">log in</span> with an already existing username?</p></body></html> + + + Qt::RichText + + + true + + + + + + + Sign up for a new account + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Provider selection + + + Please enter the domain of the provider you want to use for your connection + + + 1 + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 60 + + + + + + + + + + + Check + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + https:// + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Checking for a valid provider + + + + + + Getting provider information + + + + + + + Can we stablish a secure connection? + + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + + + + :/images/Emblem-question.png + + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + + + + :/images/Emblem-question.png + + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + + + + :/images/Emblem-question.png + + + + + + + Can we reach this provider? + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + + + + + + + + + + + + Provider Information + + + Description of services offered by this provider + + + 2 + + + + + + Name + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + Desc + + + true + + + + + + + <b>Services offered:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + services + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + <b>Enrollment policy:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + policy + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <b>URL:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + URL + + + + + + + <b>Description:</b> + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + + Provider setup + + + Gathering configuration options for this provider + + + 3 + + + + + + Qt::Vertical + + + + 20 + 60 + + + + + + + + We are downloading some bits that we need to establish a secure connection with the provider for the first time. + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Setting up provider + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + + + + :/images/Emblem-question.png + + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + + + + :/images/Emblem-question.png + + + + + + + Getting info from the Certificate Authority + + + + + + + Do we trust this Certificate Authority? + + + + + + + Establishing a trust relationship with this provider + + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + + + + :/images/Emblem-question.png + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Register new user + + + Register a new user with provider + + + 4 + + + + QLayout::SetDefaultConstraint + + + 4 + + + + + <b>Password:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + <b>Re-enter password:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Register + + + + + + + Qt::Vertical + + + + 20 + 60 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <b>Username:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + false + + + Remember my username and password + + + + + + + + + + Qt::AutoText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + Service selection + + + Please select the services you would like to have + + + 5 + + + + + + Services by PROVIDER + + + + + + + + + + + + + Congratulations! + + + You have successfully configured Bitmask. + + + 6 + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + :/images/leap-color-big.png + + + + + + + + 0 + 0 + + + + + + + :/images/Globe.png + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + WizardPage + QWizardPage +
wizardpage.h
+ 1 +
+
+ + lblUser + lblPassword + lblPassword2 + btnRegister + rdoRegister + rdoLogin + lnProvider + btnCheck + + + + + + +
diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py new file mode 100644 index 00000000..2b48fc81 --- /dev/null +++ b/src/leap/bitmask/gui/wizard.py @@ -0,0 +1,626 @@ +# -*- coding: utf-8 -*- +# wizard.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 . + +""" +First run wizard +""" +import os +import logging +import json + +from PySide import QtCore, QtGui +from functools import partial +from twisted.internet import threads + +from ui_wizard import Ui_Wizard +from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpregister import SRPRegister +from leap.util.privilege_policies import is_missing_policy_permissions +from leap.util.request_helpers import get_content +from leap.util.keyring_helpers import has_keyring +from leap.services.eip.providerbootstrapper import ProviderBootstrapper +from leap.services import get_supported + +logger = logging.getLogger(__name__) + + +class Wizard(QtGui.QWizard): + """ + First run wizard to register a user and setup a provider + """ + + INTRO_PAGE = 0 + SELECT_PROVIDER_PAGE = 1 + PRESENT_PROVIDER_PAGE = 2 + SETUP_PROVIDER_PAGE = 3 + REGISTER_USER_PAGE = 4 + SERVICES_PAGE = 5 + FINISH_PAGE = 6 + + WEAK_PASSWORDS = ("123456", "qweasd", "qwerty", + "password") + + BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" + + def __init__(self, standalone=False, 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) + + self.setPixmap(QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(":/images/leap-color-small.png")) + + self.QUESTION_ICON = QtGui.QPixmap(":/images/Emblem-question.png") + self.ERROR_ICON = QtGui.QPixmap(":/images/Dialog-error.png") + self.OK_ICON = QtGui.QPixmap(":/images/Dialog-accept.png") + + # Correspondence for services and their name to display + EIP_LABEL = self.tr("Encrypted Internet") + MX_LABEL = self.tr("Encrypted Mail") + + if self._is_need_eip_password_warning(): + EIP_LABEL += " " + self.tr( + "(will need admin password to start)") + + self.SERVICE_DISPLAY = [ + EIP_LABEL, + MX_LABEL + ] + self.SERVICE_CONFIG = [ + "openvpn", + "mx" + ] + + self._selected_services = set() + self._shown_services = set() + + self._show_register = False + + self.ui.grpCheckProvider.setVisible(False) + self.ui.btnCheck.clicked.connect(self._check_provider) + self.ui.lnProvider.returnPressed.connect(self._check_provider) + + self._provider_bootstrapper = ProviderBootstrapper(bypass_checks) + self._provider_bootstrapper.name_resolution.connect( + self._name_resolution) + self._provider_bootstrapper.https_connection.connect( + self._https_connection) + self._provider_bootstrapper.download_provider_info.connect( + self._download_provider_info) + + self._provider_bootstrapper.download_ca_cert.connect( + self._download_ca_cert) + self._provider_bootstrapper.check_ca_fingerprint.connect( + self._check_ca_fingerprint) + self._provider_bootstrapper.check_api_certificate.connect( + self._check_api_certificate) + + self._domain = None + self._provider_config = ProviderConfig() + + # We will store a reference to the defers for eventual use + # (eg, to cancel them) but not doing anything with them right now. + self._provider_select_defer = None + self._provider_setup_defer = None + + self.currentIdChanged.connect(self._current_id_changed) + + self.ui.lblPassword.setEchoMode(QtGui.QLineEdit.Password) + self.ui.lblPassword2.setEchoMode(QtGui.QLineEdit.Password) + + self.ui.lnProvider.textChanged.connect( + self._enable_check) + + self.ui.lblUser.returnPressed.connect( + self._focus_password) + self.ui.lblPassword.returnPressed.connect( + self._focus_second_password) + self.ui.lblPassword2.returnPressed.connect( + self._register) + self.ui.btnRegister.clicked.connect( + self._register) + + usernameRe = QtCore.QRegExp(self.BARE_USERNAME_REGEX) + self.ui.lblUser.setValidator( + QtGui.QRegExpValidator(usernameRe, self)) + + self.page(self.REGISTER_USER_PAGE).setCommitPage(True) + + self._username = None + self._password = None + + self.page(self.REGISTER_USER_PAGE).setButtonText( + QtGui.QWizard.CommitButton, self.tr("&Next >")) + self.page(self.FINISH_PAGE).setButtonText( + QtGui.QWizard.FinishButton, self.tr("Connect")) + + # XXX: Temporary removal for enrollment policy + # https://leap.se/code/issues/2922 + self.ui.label_12.setVisible(False) + self.ui.lblProviderPolicy.setVisible(False) + + def get_domain(self): + return self._domain + + def get_username(self): + return self._username + + def get_password(self): + return self._password + + def get_remember(self): + return has_keyring() and self.ui.chkRemember.isChecked() + + def get_services(self): + return self._selected_services + + def _enable_check(self, text): + self.ui.btnCheck.setEnabled(len(self.ui.lnProvider.text()) != 0) + self._reset_provider_check() + + def _focus_password(self): + """ + Focuses at the password lineedit for the registration page + """ + self.ui.lblPassword.setFocus() + + def _focus_second_password(self): + """ + Focuses at the second password lineedit for the registration page + """ + self.ui.lblPassword2.setFocus() + + def _basic_password_checks(self, username, password, password2): + """ + Performs basic password checks to avoid really easy passwords. + + :param username: username provided at the registrarion form + :type username: str + :param password: password from the registration form + :type password: str + :param password2: second password from the registration form + :type password: str + + :return: returns True if all the checks pass, False otherwise + :rtype: bool + """ + message = None + + if message is None and password != password2: + message = self.tr("Passwords don't match") + + if message is None and len(password) < 6: + message = self.tr("Password too short") + + if message is None and password in self.WEAK_PASSWORDS: + message = self.tr("Password too easy") + + if message is None and username == password: + message = self.tr("Password equal to username") + + if message is not None: + self._set_register_status(message, error=True) + self._focus_password() + return False + + return True + + def _register(self): + """ + Performs the registration based on the values provided in the form + """ + self.ui.btnRegister.setEnabled(False) + + username = self.ui.lblUser.text() + password = self.ui.lblPassword.text() + password2 = self.ui.lblPassword2.text() + + if self._basic_password_checks(username, password, password2): + register = SRPRegister(provider_config=self._provider_config) + register.registration_finished.connect( + self._registration_finished) + + threads.deferToThread( + partial(register.register_user, + username.encode("utf8"), + password.encode("utf8"))) + + self._username = username + self._password = password + self._set_register_status(self.tr("Starting registration...")) + else: + self.ui.btnRegister.setEnabled(True) + + def _set_registration_fields_visibility(self, visible): + """ + This method hides the username and password labels and inputboxes. + + :param visible: sets the visibility of the widgets + True: widgets are visible or False: are not + :type visible: bool + """ + # username and password inputs + self.ui.lblUser.setVisible(visible) + self.ui.lblPassword.setVisible(visible) + self.ui.lblPassword2.setVisible(visible) + + # username and password labels + self.ui.label_15.setVisible(visible) + self.ui.label_16.setVisible(visible) + self.ui.label_17.setVisible(visible) + + # register button + self.ui.btnRegister.setVisible(visible) + + def _registration_finished(self, ok, req): + if ok: + user_domain = self._username + "@" + self._domain + message = "

" + message += self.tr("User %s successfully registered.") % ( + user_domain, ) + message += "

" + self._set_register_status(message) + + self.ui.lblPassword2.clearFocus() + self._set_registration_fields_visibility(False) + + # Allow the user to remember his password + if has_keyring(): + self.ui.chkRemember.setVisible(True) + self.ui.chkRemember.setEnabled(True) + + self.page(self.REGISTER_USER_PAGE).set_completed() + self.button(QtGui.QWizard.BackButton).setEnabled(False) + else: + old_username = self._username + self._username = None + self._password = None + error_msg = self.tr("Unknown error") + try: + content, _ = get_content(req) + json_content = json.loads(content) + error_msg = json_content.get("errors").get("login")[0] + if not error_msg.istitle(): + error_msg = "%s %s" % (old_username, error_msg) + except Exception as e: + logger.error("Unknown error: %r" % (e,)) + + self._set_register_status(error_msg, error=True) + self.ui.btnRegister.setEnabled(True) + + def _set_register_status(self, status, error=False): + """ + Sets the status label in the registration page to status + + :param status: status message to display, can be HTML + :type status: str + """ + if error: + status = "%s" % (status,) + self.ui.lblRegisterStatus.setText(status) + + def _reset_provider_check(self): + """ + Resets the UI for checking a provider. Also resets the domain + in this object. + """ + self.ui.lblNameResolution.setPixmap(None) + self.ui.lblHTTPS.setPixmap(None) + self.ui.lblProviderInfo.setPixmap(None) + self.ui.lblProviderSelectStatus.setText("") + self._domain = None + self.button(QtGui.QWizard.NextButton).setEnabled(False) + self.page(self.SELECT_PROVIDER_PAGE).set_completed(False) + + def _reset_provider_setup(self): + """ + Resets the UI for setting up a provider. + """ + self.ui.lblDownloadCaCert.setPixmap(None) + self.ui.lblCheckCaFpr.setPixmap(None) + self.ui.lblCheckApiCert.setPixmap(None) + + def _check_provider(self): + """ + SLOT + TRIGGERS: + self.ui.btnCheck.clicked + self.ui.lnProvider.returnPressed + + Starts the checks for a given provider + """ + if len(self.ui.lnProvider.text()) == 0: + return + + self.ui.grpCheckProvider.setVisible(True) + self.ui.btnCheck.setEnabled(False) + self.ui.lnProvider.setEnabled(False) + self.button(QtGui.QWizard.BackButton).clearFocus() + self._domain = self.ui.lnProvider.text() + + self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) + self._provider_select_defer = self._provider_bootstrapper.\ + run_provider_select_checks(self._domain) + + def _complete_task(self, data, label, complete=False, complete_page=-1): + """ + Checks a task and completes a page if specified + + :param data: data as it comes from the bootstrapper thread for + a specific check + :type data: dict + :param label: label that displays the status icon for a + specific check that corresponds to the data + :type label: QtGui.QLabel + :param complete: if True, it completes the page specified, + which must be of type WizardPage + :type complete: bool + :param complete_page: page id to complete + :type complete_page: int + """ + passed = data[self._provider_bootstrapper.PASSED_KEY] + error = data[self._provider_bootstrapper.ERROR_KEY] + if passed: + label.setPixmap(self.OK_ICON) + if complete: + self.page(complete_page).set_completed() + self.button(QtGui.QWizard.NextButton).setFocus() + else: + label.setPixmap(self.ERROR_ICON) + logger.error(error) + + def _name_resolution(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.name_resolution + + Sets the status for the name resolution check + """ + self._complete_task(data, self.ui.lblNameResolution) + status = "" + passed = data[self._provider_bootstrapper.PASSED_KEY] + if not passed: + status = self.tr("Non-existent " + "provider") + else: + self.ui.lblHTTPS.setPixmap(self.QUESTION_ICON) + self.ui.lblProviderSelectStatus.setText(status) + self.ui.btnCheck.setEnabled(not passed) + self.ui.lnProvider.setEnabled(not passed) + + def _https_connection(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.https_connection + + Sets the status for the https connection check + """ + self._complete_task(data, self.ui.lblHTTPS) + status = "" + passed = data[self._provider_bootstrapper.PASSED_KEY] + if not passed: + status = self.tr("%s") \ + % (data[self._provider_bootstrapper.ERROR_KEY]) + self.ui.lblProviderSelectStatus.setText(status) + else: + self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) + self.ui.btnCheck.setEnabled(not passed) + self.ui.lnProvider.setEnabled(not passed) + + def _download_provider_info(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.download_provider_info + + Sets the status for the provider information download + check. Since this check is the last of this set, it also + completes the page if passed + """ + if self._provider_config.load(os.path.join("leap", + "providers", + self._domain, + "provider.json")): + self._complete_task(data, self.ui.lblProviderInfo, + True, self.SELECT_PROVIDER_PAGE) + else: + new_data = { + self._provider_bootstrapper.PASSED_KEY: False, + self._provider_bootstrapper.ERROR_KEY: + self.tr("Unable to load provider configuration") + } + self._complete_task(new_data, self.ui.lblProviderInfo) + + status = "" + if not data[self._provider_bootstrapper.PASSED_KEY]: + status = self.tr("Not a valid provider" + "") + self.ui.lblProviderSelectStatus.setText(status) + self.ui.btnCheck.setEnabled(True) + self.ui.lnProvider.setEnabled(True) + + def _download_ca_cert(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.download_ca_cert + + Sets the status for the download of the CA certificate check + """ + self._complete_task(data, self.ui.lblDownloadCaCert) + passed = data[self._provider_bootstrapper.PASSED_KEY] + if passed: + self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON) + + def _check_ca_fingerprint(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.check_ca_fingerprint + + Sets the status for the CA fingerprint check + """ + self._complete_task(data, self.ui.lblCheckCaFpr) + passed = data[self._provider_bootstrapper.PASSED_KEY] + if passed: + self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON) + + def _check_api_certificate(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.check_api_certificate + + Sets the status for the API certificate check. Also finishes + the provider bootstrapper thread since it's not needed anymore + from this point on, unless the whole check chain is restarted + """ + self._complete_task(data, self.ui.lblCheckApiCert, + True, self.SETUP_PROVIDER_PAGE) + + def _service_selection_changed(self, service, state): + """ + SLOT + TRIGGER: service_checkbox.stateChanged + Adds the service to the state if the state is checked, removes + it otherwise + + :param service: service to handle + :type service: str + :param state: state of the checkbox + :type state: int + """ + if state == QtCore.Qt.Checked: + self._selected_services = \ + self._selected_services.union(set([service])) + else: + self._selected_services = \ + self._selected_services.difference(set([service])) + + def _populate_services(self): + """ + Loads the services that the provider provides into the UI for + the user to enable or disable. + """ + self.ui.grpServices.setTitle( + self.tr("Services by %s") % + (self._provider_config.get_name(),)) + + services = get_supported( + self._provider_config.get_services()) + + for service in services: + try: + if service not in self._shown_services: + checkbox = QtGui.QCheckBox(self) + service_index = self.SERVICE_CONFIG.index(service) + checkbox.setText(self.SERVICE_DISPLAY[service_index]) + self.ui.serviceListLayout.addWidget(checkbox) + checkbox.stateChanged.connect( + partial(self._service_selection_changed, service)) + checkbox.setChecked(True) + self._shown_services.add(service) + except ValueError: + logger.error( + self.tr("Something went wrong while trying to " + "load service %s" % (service,))) + + def _current_id_changed(self, pageId): + """ + SLOT + TRIGGER: self.currentIdChanged + + Prepares the pages when they appear + """ + if pageId == self.SELECT_PROVIDER_PAGE: + self._reset_provider_check() + self._enable_check("") + + if pageId == self.SETUP_PROVIDER_PAGE: + self._reset_provider_setup() + self.page(pageId).setSubTitle(self.tr("Gathering configuration " + "options for %s") % + (self._provider_config + .get_name(),)) + self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON) + self._provider_setup_defer = self._provider_bootstrapper.\ + run_provider_setup_checks(self._provider_config) + + if pageId == self.PRESENT_PROVIDER_PAGE: + self.page(pageId).setSubTitle(self.tr("Description of services " + "offered by %s") % + (self._provider_config + .get_name(),)) + + lang = QtCore.QLocale.system().name() + self.ui.lblProviderName.setText( + "%s" % + (self._provider_config.get_name(lang=lang),)) + self.ui.lblProviderURL.setText( + "https://%s" % (self._provider_config.get_domain(),)) + self.ui.lblProviderDesc.setText( + "%s" % + (self._provider_config.get_description(lang=lang),)) + + self.ui.lblServicesOffered.setText(self._provider_config + .get_services_string()) + self.ui.lblProviderPolicy.setText(self._provider_config + .get_enrollment_policy()) + + if pageId == self.REGISTER_USER_PAGE: + self.page(pageId).setSubTitle(self.tr("Register a new user with " + "%s") % + (self._provider_config + .get_name(),)) + self.ui.chkRemember.setVisible(False) + + 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 + wants to register a new identity or uses an existing one + """ + if self.currentPage() == self.page(self.INTRO_PAGE): + self._show_register = self.ui.rdoRegister.isChecked() + + if self.currentPage() == self.page(self.SETUP_PROVIDER_PAGE): + if self._show_register: + return self.REGISTER_USER_PAGE + else: + return self.SERVICES_PAGE + + return QtGui.QWizard.nextId(self) diff --git a/src/leap/bitmask/gui/wizardpage.py b/src/leap/bitmask/gui/wizardpage.py new file mode 100644 index 00000000..b2a00028 --- /dev/null +++ b/src/leap/bitmask/gui/wizardpage.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# wizardpage.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 . + +from PySide import QtGui + + +class WizardPage(QtGui.QWizardPage): + """ + Simple wizard page helper + """ + + def __init__(self): + QtGui.QWizardPage.__init__(self) + self._completed = False + + def set_completed(self, val=True): + self._completed = val + if val: + self.completeChanged.emit() + + def isComplete(self): + return self._completed + + def cleanupPage(self): + self._completed = False + QtGui.QWizardPage.cleanupPage(self) -- cgit v1.2.3