diff options
Diffstat (limited to 'src/leap/bitmask/gui')
-rw-r--r-- | src/leap/bitmask/gui/advanced_key_management.py | 38 | ||||
-rw-r--r-- | src/leap/bitmask/gui/eip_preferenceswindow.py | 21 | ||||
-rw-r--r-- | src/leap/bitmask/gui/eip_status.py | 10 | ||||
-rw-r--r-- | src/leap/bitmask/gui/loggerwindow.py | 82 | ||||
-rw-r--r-- | src/leap/bitmask/gui/login.py | 52 | ||||
-rw-r--r-- | src/leap/bitmask/gui/mail_status.py | 55 | ||||
-rw-r--r-- | src/leap/bitmask/gui/mainwindow.py | 725 | ||||
-rw-r--r-- | src/leap/bitmask/gui/preferenceswindow.py | 10 | ||||
-rw-r--r-- | src/leap/bitmask/gui/twisted_main.py | 18 | ||||
-rw-r--r-- | src/leap/bitmask/gui/ui/advanced_key_management.ui | 79 | ||||
-rw-r--r-- | src/leap/bitmask/gui/ui/eip_status.ui | 66 | ||||
-rw-r--r-- | src/leap/bitmask/gui/ui/loggerwindow.ui | 15 | ||||
-rw-r--r-- | src/leap/bitmask/gui/ui/login.ui | 45 | ||||
-rw-r--r-- | src/leap/bitmask/gui/ui/mainwindow.ui | 195 | ||||
-rw-r--r-- | src/leap/bitmask/gui/ui/wizard.ui | 90 | ||||
-rw-r--r-- | src/leap/bitmask/gui/wizard.py | 244 |
16 files changed, 1113 insertions, 632 deletions
diff --git a/src/leap/bitmask/gui/advanced_key_management.py b/src/leap/bitmask/gui/advanced_key_management.py index 2c0fa034..cbc8c3e3 100644 --- a/src/leap/bitmask/gui/advanced_key_management.py +++ b/src/leap/bitmask/gui/advanced_key_management.py @@ -48,20 +48,25 @@ class AdvancedKeyManagement(QtGui.QWidget): self.ui = Ui_AdvancedKeyManagement() self.ui.setupUi(self) + # XXX: Temporarily disable the key import. + self.ui.pbImportKeys.setVisible(False) + # if Soledad is not started yet if sameProxiedObjects(soledad, None): - self.ui.container.setEnabled(False) + self.ui.gbMyKeyPair.setEnabled(False) + self.ui.gbStoredPublicKeys.setEnabled(False) msg = self.tr("<span style='color:#0000FF;'>NOTE</span>: " "To use this, you need to enable/start {0}.") msg = msg.format(get_service_display_name(MX_SERVICE)) self.ui.lblStatus.setText(msg) return - else: - msg = self.tr( - "<span style='color:#ff0000;'>WARNING</span>:<br>" - "This is an experimental feature, you can lose access to " - "existing e-mails.") - self.ui.lblStatus.setText(msg) + # XXX: since import is disabled this is no longer a dangerous feature. + # else: + # msg = self.tr( + # "<span style='color:#ff0000;'>WARNING</span>:<br>" + # "This is an experimental feature, you can lose access to " + # "existing e-mails.") + # self.ui.lblStatus.setText(msg) self._keymanager = keymanager self._soledad = soledad @@ -79,6 +84,12 @@ class AdvancedKeyManagement(QtGui.QWidget): self.ui.pbImportKeys.clicked.connect(self._import_keys) self.ui.pbExportKeys.clicked.connect(self._export_keys) + # Stretch columns to content + self.ui.twPublicKeys.horizontalHeader().setResizeMode( + 0, QtGui.QHeaderView.Stretch) + + self._list_keys() + def _import_keys(self): """ Imports the user's key pair. @@ -183,3 +194,16 @@ class AdvancedKeyManagement(QtGui.QWidget): return else: logger.debug('Export canceled by the user.') + + def _list_keys(self): + """ + Loads all the public keys stored in the local db to the keys table. + """ + keys = self._keymanager.get_all_keys_in_local_db() + + keys_table = self.ui.twPublicKeys + for key in keys: + row = keys_table.rowCount() + keys_table.insertRow(row) + keys_table.setItem(row, 0, QtGui.QTableWidgetItem(key.address)) + keys_table.setItem(row, 1, QtGui.QTableWidgetItem(key.key_id)) diff --git a/src/leap/bitmask/gui/eip_preferenceswindow.py b/src/leap/bitmask/gui/eip_preferenceswindow.py index 504d1cf1..dcaa8b1e 100644 --- a/src/leap/bitmask/gui/eip_preferenceswindow.py +++ b/src/leap/bitmask/gui/eip_preferenceswindow.py @@ -22,7 +22,7 @@ import os import logging from functools import partial -from PySide import QtGui +from PySide import QtCore, QtGui from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig @@ -37,10 +37,12 @@ class EIPPreferencesWindow(QtGui.QDialog): """ Window that displays the EIP preferences. """ - def __init__(self, parent): + def __init__(self, parent, domain): """ :param parent: parent object of the EIPPreferencesWindow. - :parent type: QWidget + :type parent: QWidget + :param domain: the selected by default domain. + :type domain: unicode """ QtGui.QDialog.__init__(self, parent) self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") @@ -59,7 +61,7 @@ class EIPPreferencesWindow(QtGui.QDialog): self.ui.cbGateways.currentIndexChanged[unicode].connect( lambda x: self.ui.lblProvidersGatewayStatus.setVisible(False)) - self._add_configured_providers() + self._add_configured_providers(domain) def _set_providers_gateway_status(self, status, success=False, error=False): @@ -83,9 +85,12 @@ class EIPPreferencesWindow(QtGui.QDialog): self.ui.lblProvidersGatewayStatus.setVisible(True) self.ui.lblProvidersGatewayStatus.setText(status) - def _add_configured_providers(self): + def _add_configured_providers(self, domain=None): """ Add the client's configured providers to the providers combo boxes. + + :param domain: the domain to be selected by default. + :type domain: unicode """ self.ui.cbProvidersGateway.clear() providers = self._settings.get_configured_providers() @@ -100,6 +105,12 @@ class EIPPreferencesWindow(QtGui.QDialog): label = provider + self.tr(" (uninitialized)") self.ui.cbProvidersGateway.addItem(label, userData=provider) + # Select provider by name + if domain is not None: + provider_index = self.ui.cbProvidersGateway.findText( + domain, QtCore.Qt.MatchStartsWith) + self.ui.cbProvidersGateway.setCurrentIndex(provider_index) + def _save_selected_gateway(self, provider): """ SLOT diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py index 4b4d360f..19942d9d 100644 --- a/src/leap/bitmask/gui/eip_status.py +++ b/src/leap/bitmask/gui/eip_status.py @@ -41,8 +41,8 @@ class EIPStatusWidget(QtGui.QWidget): EIP Status widget that displays the current state of the EIP service """ DISPLAY_TRAFFIC_RATES = True - RATE_STR = "%14.2f KB/s" - TOTAL_STR = "%14.2f Kb" + RATE_STR = "%1.2f KB/s" + TOTAL_STR = "%1.2f Kb" eip_connection_connected = QtCore.Signal() @@ -248,10 +248,10 @@ class EIPStatusWidget(QtGui.QWidget): Triggered when a default provider_config has not been found. Disables the start button and adds instructions to the user. """ - logger.debug('Hiding EIP start button') + #logger.debug('Hiding EIP start button') # you might be tempted to change this for a .setEnabled(False). # it won't work. it's under the claws of the state machine. - # probably the best thing would be to make a transitional + # probably the best thing would be to make a conditional # transition there, but that's more involved. self.eip_button.hide() msg = self.tr("You must login to use {0}".format(self._service_name)) @@ -272,7 +272,7 @@ class EIPStatusWidget(QtGui.QWidget): Triggered after a successful login. Enables the start button. """ - logger.debug('Showing EIP start button') + #logger.debug('Showing EIP start button') self.eip_button.show() # Restore the eip action menu diff --git a/src/leap/bitmask/gui/loggerwindow.py b/src/leap/bitmask/gui/loggerwindow.py index 6ef58558..9f396574 100644 --- a/src/leap/bitmask/gui/loggerwindow.py +++ b/src/leap/bitmask/gui/loggerwindow.py @@ -22,10 +22,13 @@ import logging import cgi from PySide import QtGui +from twisted.internet import threads from ui_loggerwindow import Ui_LoggerWindow +from leap.bitmask.util.constants import PASTEBIN_API_DEV_KEY from leap.bitmask.util.leap_log_handler import LeapLogHandler +from leap.bitmask.util.pastebin import PastebinAPI, PastebinError from leap.common.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) @@ -42,6 +45,9 @@ class LoggerWindow(QtGui.QDialog): :param handler: Custom handler that supports history and signal. :type handler: LeapLogHandler. """ + from twisted.internet import reactor + self.reactor = reactor + QtGui.QDialog.__init__(self) leap_assert(handler, "We need a handler for the logger window") leap_assert_type(handler, LeapLogHandler) @@ -59,8 +65,10 @@ class LoggerWindow(QtGui.QDialog): self.ui.btnCritical.toggled.connect(self._load_history) self.ui.leFilterBy.textEdited.connect(self._filter_by) self.ui.cbCaseInsensitive.stateChanged.connect(self._load_history) + self.ui.btnPastebin.clicked.connect(self._pastebin_this) self._current_filter = "" + self._current_history = "" # Load logging history and connect logger with the widget self._logging_handler = handler @@ -116,8 +124,13 @@ class LoggerWindow(QtGui.QDialog): self._set_logs_to_display() self.ui.txtLogHistory.clear() history = self._logging_handler.log_history + current_history = [] for line in history: self._add_log_line(line) + message = line[LeapLogHandler.MESSAGE_KEY] + current_history.append(message) + + self._current_history = "\n".join(current_history) def _set_logs_to_display(self): """ @@ -164,3 +177,72 @@ class LoggerWindow(QtGui.QDialog): logger.error("Error saving log file: %r" % (e, )) else: logger.debug('Log not saved!') + + def _set_pastebin_sending(self, sending): + """ + Define the status of the pastebin button. + Change the text and enable/disable according to the current action. + + :param sending: if we are sending to pastebin or not. + :type sending: bool + """ + if sending: + self.ui.btnPastebin.setText(self.tr("Sending to pastebin...")) + self.ui.btnPastebin.setEnabled(False) + else: + self.ui.btnPastebin.setText(self.tr("Send to Pastebin.com")) + self.ui.btnPastebin.setEnabled(True) + + def _pastebin_this(self): + """ + Send the current log history to pastebin.com and gives the user a link + to see it. + """ + def do_pastebin(): + """ + Send content to pastebin and return the link. + """ + content = self._current_history + pb = PastebinAPI() + link = pb.paste(PASTEBIN_API_DEV_KEY, content, + paste_name="Bitmask log", + paste_expire_date='1W') + + # convert to 'raw' link + link = "http://pastebin.com/raw.php?i=" + link.split('/')[-1] + + return link + + def pastebin_ok(link): + """ + Callback handler for `do_pastebin`. + + :param link: the recently created pastebin link. + :type link: str + """ + msg = self.tr("Your pastebin link <a href='{0}'>{0}</a>") + msg = msg.format(link) + show_info = lambda: QtGui.QMessageBox.information( + self, self.tr("Pastebin OK"), msg) + self._set_pastebin_sending(False) + self.reactor.callLater(0, show_info) + + def pastebin_err(failure): + """ + Errback handler for `do_pastebin`. + + :param failure: the failure that triggered the errback. + :type failure: twisted.python.failure.Failure + """ + logger.error(repr(failure)) + msg = self.tr("Sending logs to Pastebin failed!") + show_err = lambda: QtGui.QMessageBox.critical( + self, self.tr("Pastebin Error"), msg) + self._set_pastebin_sending(False) + self.reactor.callLater(0, show_err) + failure.trap(PastebinError) + + self._set_pastebin_sending(True) + d = threads.deferToThread(do_pastebin) + d.addCallback(pastebin_ok) + d.addErrback(pastebin_err) diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py index b21057f0..4a483c32 100644 --- a/src/leap/bitmask/gui/login.py +++ b/src/leap/bitmask/gui/login.py @@ -19,12 +19,13 @@ Login widget implementation """ import logging -import keyring - from PySide import QtCore, QtGui from ui_login import Ui_LoginWidget +from leap.bitmask.config import flags +from leap.bitmask.util import make_address from leap.bitmask.util.keyring_helpers import has_keyring +from leap.bitmask.util.keyring_helpers import get_keyring from leap.common.check import leap_assert_type logger = logging.getLogger(__name__) @@ -221,6 +222,15 @@ class LoginWidget(QtGui.QWidget): self._set_cancel(not enabled) + def set_logout_btn_enabled(self, enabled): + """ + Enables or disables the logout button. + + :param enabled: wether they should be enabled or not + :type enabled: bool + """ + self.ui.btnLogout.setEnabled(enabled) + def _set_cancel(self, enabled=False): """ Enables or disables the cancel action in the "log in" process. @@ -304,14 +314,15 @@ class LoginWidget(QtGui.QWidget): if self.get_remember() and has_keyring(): # in the keyring and in the settings # we store the value 'usename@provider' - username_domain = (username + '@' + provider).encode("utf8") + full_user_id = make_address(username, provider).encode("utf8") try: + keyring = get_keyring() keyring.set_password(self.KEYRING_KEY, - username_domain, + full_user_id, password.encode("utf8")) # Only save the username if it was saved correctly in # the keyring - self._settings.set_user(username_domain) + self._settings.set_user(full_user_id) except Exception as e: logger.exception("Problem saving data to keyring. %r" % (e,)) @@ -323,15 +334,19 @@ class LoginWidget(QtGui.QWidget): """ self.ui.login_widget.hide() self.ui.logged_widget.show() - self.ui.lblUser.setText("%s@%s" % (self.get_user(), - self.get_selected_provider())) - self.set_login_status("") - self.logged_in_signal.emit() + self.ui.lblUser.setText(make_address( + self.get_user(), self.get_selected_provider())) + + if flags.OFFLINE is False: + self.logged_in_signal.emit() def logged_out(self): """ Sets the widgets to the logged out state """ + # TODO consider "logging out offline" too... + # how that would be ??? + self.ui.login_widget.show() self.ui.logged_widget.hide() @@ -339,27 +354,11 @@ class LoginWidget(QtGui.QWidget): self.set_enabled(True) self.set_status("", error=False) - def set_login_status(self, msg, error=False): - """ - Sets the status label for the logged in state. - - :param msg: status message - :type msg: 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: - msg = "<font color='red'><b>%s</b></font>" % (msg,) - self.ui.lblLoginStatus.setText(msg) - self.ui.lblLoginStatus.show() - def start_logout(self): """ Sets the widgets to the logging out state """ - self.ui.btnLogout.setText(self.tr("Loggin out...")) + self.ui.btnLogout.setText(self.tr("Logging out...")) self.ui.btnLogout.setEnabled(False) def done_logout(self): @@ -396,6 +395,7 @@ class LoginWidget(QtGui.QWidget): saved_password = None try: + keyring = get_keyring() saved_password = keyring.get_password(self.KEYRING_KEY, saved_user .encode("utf8")) diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py index 3c933c9a..44a138e2 100644 --- a/src/leap/bitmask/gui/mail_status.py +++ b/src/leap/bitmask/gui/mail_status.py @@ -112,6 +112,10 @@ class MailStatusWidget(QtGui.QWidget): callback=self._mail_handle_imap_events, reqcbk=lambda req, resp: None) + register(signal=proto.SOLEDAD_INVALID_AUTH_TOKEN, + callback=self.set_soledad_invalid_auth_token, + reqcbk=lambda req, resp: None) + self._soledad_event.connect( self._mail_handle_soledad_events_slot) self._imap_event.connect( @@ -191,6 +195,17 @@ class MailStatusWidget(QtGui.QWidget): msg = self.tr("There was an unexpected problem with Soledad.") self._set_mail_status(msg, ready=-1) + def set_soledad_invalid_auth_token(self): + """ + SLOT + TRIGGER: + SoledadBootstrapper.soledad_invalid_token + + This method is called when the auth token is invalid + """ + msg = self.tr("Invalid auth token, try logging in again.") + self._set_mail_status(msg, ready=-1) + def _set_mail_status(self, status, ready=0): """ Sets the Mail status in the label and in the tray icon. @@ -213,7 +228,7 @@ class MailStatusWidget(QtGui.QWidget): self._service_name)) elif ready == 1: icon = self.CONNECTING_ICON - self._mx_status = self.tr('Starting..') + self._mx_status = self.tr('Starting…') tray_status = self.tr('Mail is starting') elif ready >= 2: icon = self.CONNECTED_ICON @@ -362,10 +377,19 @@ class MailStatusWidget(QtGui.QWidget): ext_status = None if req.event == proto.IMAP_UNREAD_MAIL: + # By now, the semantics of the UNREAD_MAIL event are + # limited to mails with the Unread flag *in the Inbox". + # We could make this configurable to include all unread mail + # or all unread mail in subscribed folders. if self._started: - if req.content != "0": - self._set_mail_status(self.tr("%s Unread Emails") % - (req.content,), ready=2) + count = req.content + if count != "0": + status = self.tr("{0} Unread Emails " + "in your Inbox").format(count) + if count == "1": + status = self.tr("1 Unread Email in your Inbox") + + self._set_mail_status(status, ready=2) else: self._set_mail_status("", ready=2) elif req.event == proto.IMAP_SERVICE_STARTED: @@ -375,7 +399,7 @@ class MailStatusWidget(QtGui.QWidget): def about_to_start(self): """ - Displays the correct UI for the point where mail components + Display the correct UI for the point where mail components haven't really started, but they are about to in a second. """ self._set_mail_status(self.tr("About to start, please wait..."), @@ -383,7 +407,7 @@ class MailStatusWidget(QtGui.QWidget): def set_disabled(self): """ - Displays the correct UI for disabled mail. + Display the correct UI for disabled mail. """ self._set_mail_status(self.tr("Disabled"), -1) @@ -394,7 +418,7 @@ class MailStatusWidget(QtGui.QWidget): @QtCore.Slot() def mail_state_disconnected(self): """ - Displays the correct UI for the disconnected state. + Display the correct UI for the disconnected state. """ # XXX this should handle the disabled state better. self._started = False @@ -406,7 +430,7 @@ class MailStatusWidget(QtGui.QWidget): @QtCore.Slot() def mail_state_connecting(self): """ - Displays the correct UI for the connecting state. + Display the correct UI for the connecting state. """ self._disabled = False self._started = True @@ -415,23 +439,32 @@ class MailStatusWidget(QtGui.QWidget): @QtCore.Slot() def mail_state_disconnecting(self): """ - Displays the correct UI for the connecting state. + Display the correct UI for the connecting state. """ self._set_mail_status(self.tr("Disconnecting..."), 1) @QtCore.Slot() def mail_state_connected(self): """ - Displays the correct UI for the connected state. + Display the correct UI for the connected state. """ self._set_mail_status(self.tr("ON"), 2) @QtCore.Slot() def mail_state_disabled(self): """ - Displays the correct UI for the disabled state. + Display the correct UI for the disabled state. """ self._disabled = True status = self.tr("You must be logged in to use {0}.").format( self._service_name) self._set_mail_status(status, -1) + + @QtCore.Slot() + def soledad_invalid_auth_token(self): + """ + Display the correct UI for the invalid token state + """ + self._disabled = True + status = self.tr("Invalid auth token, try logging in again.") + self._set_mail_status(status, -1) diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 929919ac..5abfaa67 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # mainwindow.py -# Copyright (C) 2013 LEAP +# Copyright (C) 2013, 2014 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 @@ -18,16 +18,25 @@ Main window for Bitmask. """ import logging -import os +import socket + +from threading import Condition +from datetime import datetime from PySide import QtCore, QtGui -from twisted.internet import threads from zope.proxy import ProxyBase, setProxiedObject +from twisted.internet import reactor, threads +from twisted.internet.defer import CancelledError from leap.bitmask import __version__ as VERSION +from leap.bitmask import __version_hash__ as VERSION_HASH +from leap.bitmask.config import flags from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig + +from leap.bitmask.crypto import srpauth from leap.bitmask.crypto.srpauth import SRPAuth + from leap.bitmask.gui.loggerwindow import LoggerWindow from leap.bitmask.gui.advanced_key_management import AdvancedKeyManagement from leap.bitmask.gui.login import LoginWidget @@ -40,11 +49,12 @@ from leap.bitmask.gui.wizard import Wizard from leap.bitmask.gui.systray import SysTray from leap.bitmask import provider -from leap.bitmask.platform_init import IS_WIN, IS_MAC +from leap.bitmask.platform_init import IS_WIN, IS_MAC, IS_LINUX from leap.bitmask.platform_init.initializers import init_platform -from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper -from leap.bitmask.services import get_service_display_name, EIP_SERVICE +from leap.bitmask import backend + +from leap.bitmask.services import get_service_display_name from leap.bitmask.services.mail import conductor as mail_conductor @@ -66,6 +76,7 @@ from leap.bitmask.services.eip.darwinvpnlauncher import EIPNoTunKextLoaded from leap.bitmask.services.soledad.soledadbootstrapper import \ SoledadBootstrapper +from leap.bitmask.util import make_address from leap.bitmask.util.keyring_helpers import has_keyring from leap.bitmask.util.leap_log_handler import LeapLogHandler @@ -77,6 +88,8 @@ from leap.common.check import leap_assert from leap.common.events import register from leap.common.events import events_pb2 as proto +from leap.mail.imap.service.imap import IMAP_PORT + from ui_mainwindow import Ui_MainWindow logger = logging.getLogger(__name__) @@ -93,6 +106,7 @@ class MainWindow(QtGui.QMainWindow): # Signals eip_needs_login = QtCore.Signal([]) + offline_mode_bypass_login = QtCore.Signal([]) new_updates = QtCore.Signal(object) raise_window = QtCore.Signal([]) soledad_ready = QtCore.Signal([]) @@ -102,6 +116,12 @@ class MainWindow(QtGui.QMainWindow): # We use this flag to detect abnormal terminations user_stopped_eip = False + # We give EIP some time to come up before starting soledad anyway + EIP_TIMEOUT = 60000 # in milliseconds + + # We give each service some time to come to a halt before forcing quit + SERVICE_STOP_TIMEOUT = 20 + def __init__(self, quit_callback, openvpn_verb=1, bypass_checks=False): @@ -132,11 +152,14 @@ class MainWindow(QtGui.QMainWindow): # end register leap events #################################### self._quit_callback = quit_callback - self._updates_content = "" + # setup UI self.ui = Ui_MainWindow() self.ui.setupUi(self) + self.menuBar().setNativeMenuBar(not IS_LINUX) + self._backend = backend.Backend(bypass_checks) + self._backend.start() self._settings = LeapSettings() @@ -173,6 +196,11 @@ class MainWindow(QtGui.QMainWindow): self._eip_status.eip_connection_connected.connect( self._on_eip_connected) + self._eip_status.eip_connection_connected.connect( + self._maybe_run_soledad_setup_checks) + self.offline_mode_bypass_login.connect( + self._maybe_run_soledad_setup_checks) + self.eip_needs_login.connect( self._eip_status.disable_eip_start) self.eip_needs_login.connect( @@ -180,36 +208,23 @@ class MainWindow(QtGui.QMainWindow): # This is loaded only once, there's a bug when doing that more # than once - self._provider_config = ProviderConfig() + # XXX HACK!! But we need it as long as we are using + # provider_config in here + self._provider_config = ( + self._backend._components["provider"]._provider_config) # Used for automatic start of EIP self._provisional_provider_config = ProviderConfig() self._eip_config = eipconfig.EIPConfig() self._already_started_eip = False + self._already_started_soledad = False # This is created once we have a valid provider config self._srp_auth = None self._logged_user = None + self._logged_in_offline = False - # 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) + self._backend_connect() # This thread is similar to the provider bootstrapper self._eip_bootstrapper = EIPBootstrapper() @@ -243,15 +258,23 @@ class MainWindow(QtGui.QMainWindow): self._soledad_intermediate_stage) self._soledad_bootstrapper.gen_key.connect( self._soledad_bootstrapped_stage) + self._soledad_bootstrapper.local_only_ready.connect( + self._soledad_bootstrapped_stage) self._soledad_bootstrapper.soledad_timeout.connect( self._retry_soledad_connection) + self._soledad_bootstrapper.soledad_invalid_auth_token.connect( + self._mail_status.set_soledad_invalid_auth_token) self._soledad_bootstrapper.soledad_failed.connect( self._mail_status.set_soledad_failed) + self.ui.action_preferences.triggered.connect(self._show_preferences) + self.ui.action_eip_preferences.triggered.connect( + self._show_eip_preferences) 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.ui.action_help.triggered.connect(self._help) self.ui.action_create_new_account.triggered.connect( self._launch_wizard) @@ -279,13 +302,15 @@ class MainWindow(QtGui.QMainWindow): self._action_visible = QtGui.QAction(self.tr("Hide Main Window"), self) self._action_visible.triggered.connect(self._toggle_visible) - self.ui.btnPreferences.clicked.connect(self._show_preferences) - self.ui.btnEIPPreferences.clicked.connect(self._show_eip_preferences) + # disable buttons for now, may come back later. + # self.ui.btnPreferences.clicked.connect(self._show_preferences) + # self.ui.btnEIPPreferences.clicked.connect(self._show_eip_preferences) self._enabled_services = [] - self._center_window() + # last minute UI manipulations + self._center_window() self.ui.lblNewUpdates.setVisible(False) self.ui.btnMore.setVisible(False) ######################################### @@ -294,6 +319,8 @@ class MainWindow(QtGui.QMainWindow): self.ui.btnMore.resize(0, 0) ######################################### self.ui.btnMore.clicked.connect(self._updates_details) + if flags.OFFLINE is True: + self._set_label_offline() # Services signals/slots connection self.new_updates.connect(self._react_to_new_updates) @@ -323,7 +350,7 @@ class MainWindow(QtGui.QMainWindow): self._keymanager = ProxyBase(None) self._login_defer = None - self._download_provider_defer = None + self._soledad_defer = None self._mail_conductor = mail_conductor.MailConductor( self._soledad, self._keymanager) @@ -345,7 +372,9 @@ class MainWindow(QtGui.QMainWindow): if self._first_run(): self._wizard_firstrun = True - self._wizard = Wizard(bypass_checks=bypass_checks) + self._backend_disconnect() + self._wizard = Wizard(backend=self._backend, + 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) @@ -355,6 +384,42 @@ class MainWindow(QtGui.QMainWindow): # so this has to be done after eip_machine is started self._finish_init() + def _backend_connect(self): + """ + Helper to connect to backend signals + """ + sig = self._backend.signaler + sig.prov_name_resolution.connect(self._intermediate_stage) + sig.prov_https_connection.connect(self._intermediate_stage) + sig.prov_download_ca_cert.connect(self._intermediate_stage) + + sig.prov_download_provider_info.connect(self._load_provider_config) + sig.prov_check_api_certificate.connect(self._provider_config_loaded) + + # Only used at login, no need to disconnect this like we do + # with the other + sig.prov_problem_with_provider.connect(self._login_problem_provider) + + sig.prov_unsupported_client.connect(self._needs_update) + sig.prov_unsupported_api.connect(self._incompatible_api) + + sig.prov_cancelled_setup.connect(self._set_login_cancelled) + + def _backend_disconnect(self): + """ + Helper to disconnect from backend signals. + + Some signals are emitted from the wizard, and we want to + ignore those. + """ + sig = self._backend.signaler + sig.prov_name_resolution.disconnect(self._intermediate_stage) + sig.prov_https_connection.disconnect(self._intermediate_stage) + sig.prov_download_ca_cert.disconnect(self._intermediate_stage) + + sig.prov_download_provider_info.disconnect(self._load_provider_config) + sig.prov_check_api_certificate.disconnect(self._provider_config_loaded) + def _rejected_wizard(self): """ SLOT @@ -375,7 +440,9 @@ class MainWindow(QtGui.QMainWindow): # This happens if the user finishes the provider # setup but does not register self._wizard = None - self._finish_init() + self._backend_connect() + if self._wizard_firstrun: + self._finish_init() def _launch_wizard(self): """ @@ -390,9 +457,11 @@ class MainWindow(QtGui.QMainWindow): there. """ if self._wizard is None: - self._wizard = Wizard(bypass_checks=self._bypass_checks) + self._backend_disconnect() + self._wizard = Wizard(backend=self._backend, + bypass_checks=self._bypass_checks) self._wizard.accepted.connect(self._finish_init) - self._wizard.rejected.connect(self._wizard.close) + self._wizard.rejected.connect(self._rejected_wizard) self.setVisible(False) # Do NOT use exec_, it will use a child event loop! @@ -467,26 +536,66 @@ class MainWindow(QtGui.QMainWindow): """ SLOT TRIGGERS: - self.ui.btnPreferences.clicked + self.ui.btnPreferences.clicked (disabled for now) + self.ui.action_preferences Displays the preferences window. """ - preferences_window = PreferencesWindow( + preferences = PreferencesWindow( self, self._srp_auth, self._provider_config, self._soledad, self._login_widget.get_selected_provider()) - self.soledad_ready.connect(preferences_window.set_soledad_ready) - preferences_window.show() + self.soledad_ready.connect(preferences.set_soledad_ready) + preferences.show() + preferences.preferences_saved.connect(self._update_eip_enabled_status) + + def _update_eip_enabled_status(self): + """ + SLOT + TRIGGER: + PreferencesWindow.preferences_saved + + Enable or disable the EIP start/stop actions and stop EIP if the user + disabled that service. + + :returns: if the eip actions were enabled or disabled + :rtype: bool + """ + settings = self._settings + default_provider = settings.get_defaultprovider() + enabled_services = [] + if default_provider is not None: + enabled_services = settings.get_enabled_services(default_provider) + + eip_enabled = False + if EIP_SERVICE in enabled_services: + should_autostart = settings.get_autostart_eip() + if should_autostart and default_provider is not None: + self._eip_status.enable_eip_start() + self._eip_status.set_eip_status("") + eip_enabled = True + else: + # we don't have an usable provider + # so the user needs to log in first + self._eip_status.disable_eip_start() + else: + self._stop_eip() + self._eip_status.disable_eip_start() + self._eip_status.set_eip_status(self.tr("Disabled")) + + return eip_enabled def _show_eip_preferences(self): """ SLOT TRIGGERS: self.ui.btnEIPPreferences.clicked + self.ui.action_eip_preferences (disabled for now) Displays the EIP preferences window. """ - EIPPreferencesWindow(self).show() + domain = self._login_widget.get_selected_provider() + EIPPreferencesWindow(self, domain).show() # # updates @@ -585,6 +694,7 @@ class MainWindow(QtGui.QMainWindow): self.eip_needs_login.emit() self._wizard = None + self._backend_connect() else: self._try_autostart_eip() @@ -626,6 +736,19 @@ class MainWindow(QtGui.QMainWindow): self.ui.eipWidget.setVisible(EIP_SERVICE in services) self.ui.mailWidget.setVisible(MX_SERVICE in services) + def _set_label_offline(self): + """ + Set the login label to reflect offline status. + """ + if self._logged_in_offline: + provider = "" + else: + provider = self.ui.lblLoginProvider.text() + + self.ui.lblLoginProvider.setText( + provider + + self.tr(" (offline mode)")) + # # systray # @@ -748,10 +871,13 @@ class MainWindow(QtGui.QMainWindow): Display the About Bitmask dialog """ + today = datetime.now().date() + greet = ("Happy New 1984!... or not ;)<br><br>" + if today.month == 1 and today.day < 15 else "") QtGui.QMessageBox.about( self, self.tr("About Bitmask - %s") % (VERSION,), - self.tr("Version: <b>%s</b><br>" - "<br>" + self.tr("Version: <b>%s</b> (%s)<br>" + "<br>%s" "Bitmask is the Desktop client application for " "the LEAP platform, supporting encrypted internet " "proxy, secure email, and secure chat (coming soon).<br>" @@ -763,7 +889,58 @@ class MainWindow(QtGui.QMainWindow): "and widely available. <br>" "<br>" "<a href='https://leap.se'>More about LEAP" - "</a>") % (VERSION,)) + "</a>") % (VERSION, VERSION_HASH[:10], greet)) + + def _help(self): + """ + SLOT + TRIGGERS: self.ui.action_help.triggered + + Display the Bitmask help dialog. + """ + # TODO: don't hardcode! + smtp_port = 2013 + + url = ("<a href='https://addons.mozilla.org/es/thunderbird/" + "addon/bitmask/'>bitmask addon</a>") + + msg = self.tr( + "<strong>Instructions to use mail:</strong><br>" + "If you use Thunderbird you can use the Bitmask extension helper. " + "Search for 'Bitmask' in the add-on manager or download it " + "from: {0}.<br><br>" + "You can configure Bitmask manually with these options:<br>" + "<em>" + " Incoming -> IMAP, port: {1}<br>" + " Outgoing -> SMTP, port: {2}<br>" + " Username -> your bitmask username.<br>" + " Password -> does not matter, use any text. " + " Just don't leave it empty and don't use your account's password." + "</em>").format(url, IMAP_PORT, smtp_port) + QtGui.QMessageBox.about(self, self.tr("Bitmask Help"), msg) + + def _needs_update(self): + """ + Display a warning dialog to inform the user that the app needs update. + """ + url = "https://dl.bitmask.net/" + msg = self.tr( + "The current client version is not supported " + "by this provider.<br>" + "Please update to latest version.<br><br>" + "You can get the latest version from " + "<a href='{0}'>{1}</a>").format(url, url) + QtGui.QMessageBox.warning(self, self.tr("Update Needed"), msg) + + def _incompatible_api(self): + """ + Display a warning dialog to inform the user that the provider has an + incompatible API. + """ + msg = self.tr( + "This provider is not compatible with the client.<br><br>" + "Error: API version incompatible.") + QtGui.QMessageBox.warning(self, self.tr("Incompatible Provider"), msg) def changeEvent(self, e): """ @@ -812,51 +989,36 @@ class MainWindow(QtGui.QMainWindow): """ # XXX should rename this provider, name clash. 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 + self._backend.setup_provider(provider) def _load_provider_config(self, data): """ SLOT - TRIGGER: self._provider_bootstrapper.download_provider_info + TRIGGER: self._backend.signaler.prov_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 + run_provider_select_checks :type data: dict """ - if data[self._provider_bootstrapper.PASSED_KEY]: - # XXX should rename this provider, name clash. - 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) + if data[self._backend.PASSED_KEY]: + selected_provider = self._login_widget.get_selected_provider() + self._backend.provider_bootstrap(selected_provider) else: - self._login_widget.set_status( - self.tr("Unable to login: Problem with provider")) - logger.error(data[self._provider_bootstrapper.ERROR_KEY]) + logger.error(data[self._backend.ERROR_KEY]) self._login_widget.set_enabled(True) + def _login_problem_provider(self): + """ + Warns the user about a problem with the provider during login. + """ + self._login_widget.set_status( + self.tr("Unable to login: Problem with provider")) + self._login_widget.set_enabled(True) + def _login(self): """ SLOT @@ -868,10 +1030,57 @@ class MainWindow(QtGui.QMainWindow): start the SRP authentication, and as the last step bootstrapping the EIP service """ - leap_assert(self._provider_config, "We need a provider config") + # TODO most of this could ve handled by the login widget, + # but we'd have to move lblLoginProvider into the widget itself, + # instead of having it as a top-level attribute. + if flags.OFFLINE is True: + logger.debug("OFFLINE mode! bypassing remote login") + # TODO reminder, we're not handling logout for offline + # mode. + self._login_widget.logged_in() + self._logged_in_offline = True + self._set_label_offline() + self.offline_mode_bypass_login.emit() + else: + leap_assert(self._provider_config, "We need a provider config") + if self._login_widget.start_login(): + self._download_provider_config() - if self._login_widget.start_login(): - self._download_provider_config() + def _login_errback(self, failure): + """ + Error handler for the srpauth.authenticate method. + + :param failure: failure object that Twisted generates + :type failure: twisted.python.failure.Failure + """ + # NOTE: this behavior needs to be managed through the signaler, + # as we are doing with the prov_cancelled_setup signal. + # After we move srpauth to the backend, we need to update this. + logger.error("Error logging in, {0!r}".format(failure)) + + if failure.check(CancelledError): + logger.debug("Defer cancelled.") + failure.trap(Exception) + self._set_login_cancelled() + return + elif failure.check(srpauth.SRPAuthBadUserOrPassword): + msg = self.tr("Invalid username or password.") + elif failure.check(srpauth.SRPAuthBadStatusCode, + srpauth.SRPAuthenticationError, + srpauth.SRPAuthVerificationFailed, + srpauth.SRPAuthNoSessionId, + srpauth.SRPAuthNoSalt, srpauth.SRPAuthNoB, + srpauth.SRPAuthBadDataFromServer, + srpauth.SRPAuthJSONDecodeError): + msg = self.tr("There was a server problem with authentication.") + elif failure.check(srpauth.SRPAuthConnectionError): + msg = self.tr("Could not establish a connection.") + else: + # this shouldn't happen, but just in case. + msg = self.tr("Unknown error: {0!r}".format(failure.value)) + + self._login_widget.set_status(msg) + self._login_widget.set_enabled(True) def _cancel_login(self): """ @@ -882,28 +1091,48 @@ class MainWindow(QtGui.QMainWindow): Stops the login sequence. """ logger.debug("Cancelling log in.") + self._cancel_ongoing_defers() - if self._download_provider_defer: - logger.debug("Cancelling download provider defer.") - self._download_provider_defer.cancel() + def _cancel_ongoing_defers(self): + """ + Cancel the running defers to avoid app blocking. + """ + self._backend.cancel_setup_provider() - if self._login_defer: + if self._login_defer is not None: logger.debug("Cancelling login defer.") self._login_defer.cancel() + self._login_defer = None + if self._soledad_defer is not None: + logger.debug("Cancelling soledad defer.") + self._soledad_defer.cancel() + self._soledad_defer = None + + def _set_login_cancelled(self): + """ + SLOT + TRIGGERS: + Signaler.prov_cancelled_setup fired by + self._backend.cancel_setup_provider() + + This method re-enables the login widget and display a message for + the cancelled operation. + """ self._login_widget.set_status(self.tr("Log in cancelled by the user.")) + self._login_widget.set_enabled(True) def _provider_config_loaded(self, data): """ SLOT - TRIGGER: self._provider_bootstrapper.check_api_certificate + TRIGGER: self._backend.signaler.prov_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]: + if data[self._backend.PASSED_KEY]: username = self._login_widget.get_user() password = self._login_widget.get_password() @@ -913,18 +1142,18 @@ class MainWindow(QtGui.QMainWindow): self._srp_auth = SRPAuth(self._provider_config) self._srp_auth.authentication_finished.connect( self._authentication_finished) - self._srp_auth.logout_finished.connect( - self._done_logging_out) + self._srp_auth.logout_ok.connect(self._logout_ok) + self._srp_auth.logout_error.connect(self._logout_error) - # TODO Add errback! self._login_defer = self._srp_auth.authenticate(username, password) + self._login_defer.addErrback(self._login_errback) else: self._login_widget.set_status( "Unable to login: Problem with provider") - logger.error(data[self._provider_bootstrapper.ERROR_KEY]) + logger.error(data[self._backend.ERROR_KEY]) self._login_widget.set_enabled(True) - def _authentication_finished(self, ok, message): + def _authentication_finished(self): """ SLOT TRIGGER: self._srp_auth.authentication_finished @@ -932,22 +1161,23 @@ class MainWindow(QtGui.QMainWindow): 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() - user = self._logged_user - domain = self._provider_config.get_domain() - userid = "%s@%s" % (user, domain) - self._mail_conductor.userid = userid - self._login_defer = None - self._start_eip_bootstrap() - else: - self._login_widget.set_enabled(True) + self._login_widget.set_status(self.tr("Succeeded"), error=False) + + self._logged_user = self._login_widget.get_user() + user = self._logged_user + domain = self._provider_config.get_domain() + full_user_id = make_address(user, domain) + self._mail_conductor.userid = full_user_id + self._login_defer = None + self._start_eip_bootstrap() + + # if soledad/mail is enabled: + if MX_SERVICE in self._enabled_services: + btn_enabled = self._login_widget.set_logout_btn_enabled + btn_enabled(False) + self.soledad_ready.connect(lambda: btn_enabled(True)) + self._soledad_bootstrapper.soledad_failed.connect( + lambda: btn_enabled(True)) def _start_eip_bootstrap(self): """ @@ -956,28 +1186,83 @@ class MainWindow(QtGui.QMainWindow): """ self._login_widget.logged_in() - self.ui.lblLoginProvider.setText(self._provider_config.get_name()) + provider = self._provider_config.get_domain() + self.ui.lblLoginProvider.setText(provider) self._enabled_services = self._settings.get_enabled_services( self._provider_config.get_domain()) # TODO separate UI from logic. - # TODO soledad should check if we want to run only over EIP. - if self._provider_config.provides_mx() and \ - self._enabled_services.count(MX_SERVICE) > 0: + if self._provides_mx_and_enabled(): self._mail_status.about_to_start() - - self._soledad_bootstrapper.run_soledad_setup_checks( - self._provider_config, - self._login_widget.get_user(), - self._login_widget.get_password(), - download_if_needed=True) else: self._mail_status.set_disabled() - # XXX the config should be downloaded from the start_eip - # method. - self._download_eip_config() + self._maybe_start_eip() + + def _provides_mx_and_enabled(self): + """ + Defines if the current provider provides mx and if we have it enabled. + + :returns: True if provides and is enabled, False otherwise + :rtype: bool + """ + provider_config = self._get_best_provider_config() + return (provider_config.provides_mx() and + MX_SERVICE in self._enabled_services) + + def _provides_eip_and_enabled(self): + """ + Defines if the current provider provides eip and if we have it enabled. + + :returns: True if provides and is enabled, False otherwise + :rtype: bool + """ + provider_config = self._get_best_provider_config() + return (provider_config.provides_eip() and + EIP_SERVICE in self._enabled_services) + + def _maybe_run_soledad_setup_checks(self): + """ + Conditionally start Soledad. + """ + # TODO split. + if self._already_started_soledad is True: + return + + if not self._provides_mx_and_enabled(): + return + + username = self._login_widget.get_user() + password = unicode(self._login_widget.get_password()) + provider_domain = self._login_widget.get_selected_provider() + + sb = self._soledad_bootstrapper + if flags.OFFLINE is True: + provider_domain = self._login_widget.get_selected_provider() + sb._password = password + + self._provisional_provider_config.load( + provider.get_provider_path(provider_domain)) + + full_user_id = make_address(username, provider_domain) + uuid = self._settings.get_uuid(full_user_id) + self._mail_conductor.userid = full_user_id + + if uuid is None: + # We don't need more visibility at the moment, + # this is mostly for internal use/debug for now. + logger.warning("Sorry! Log-in at least one time.") + return + fun = sb.load_offline_soledad + fun(full_user_id, password, uuid) + else: + provider_config = self._provider_config + + if self._logged_user is not None: + self._soledad_defer = sb.run_soledad_setup_checks( + provider_config, username, password, + download_if_needed=True) ################################################################### # Service control methods: soledad @@ -1010,8 +1295,9 @@ class MainWindow(QtGui.QMainWindow): logger.debug("Retrying soledad connection.") if self._soledad_bootstrapper.should_retry_initialization(): self._soledad_bootstrapper.increment_retries_count() - threads.deferToThread( - self._soledad_bootstrapper.load_and_sync_soledad) + # XXX should cancel the existing socket --- this + # is avoiding a clean termination. + self._maybe_run_soledad_setup_checks() else: logger.warning("Max number of soledad initialization " "retries reached.") @@ -1021,6 +1307,7 @@ class MainWindow(QtGui.QMainWindow): SLOT TRIGGERS: self._soledad_bootstrapper.gen_key + self._soledad_bootstrapper.local_only_ready If there was a problem, displays it, otherwise it does nothing. This is used for intermediate bootstrapping stages, in case @@ -1047,6 +1334,7 @@ class MainWindow(QtGui.QMainWindow): # Ok, now soledad is ready, so we can allow other things that # depend on soledad to start. + self._soledad_defer = None # this will trigger start_imap_service # and start_smtp_boostrapping @@ -1062,10 +1350,13 @@ class MainWindow(QtGui.QMainWindow): TRIGGERS: self.soledad_ready """ + if flags.OFFLINE is True: + logger.debug("not starting smtp in offline mode") + return + # TODO for simmetry, this should be called start_smtp_service # (and delegate all the checks to the conductor) - if self._provider_config.provides_mx() and \ - self._enabled_services.count(MX_SERVICE) > 0: + if self._provides_mx_and_enabled(): self._mail_conductor.smtp_bootstrapper.run_smtp_setup_checks( self._provider_config, self._mail_conductor.smtp_config, @@ -1093,9 +1384,22 @@ class MainWindow(QtGui.QMainWindow): TRIGGERS: self.soledad_ready """ - if self._provider_config.provides_mx() and \ - self._enabled_services.count(MX_SERVICE) > 0: - self._mail_conductor.start_imap_service() + # TODO in the OFFLINE mode we should also modify the rules + # in the mail state machine so it shows that imap is active + # (but not smtp since it's not yet ready for offline use) + start_fun = self._mail_conductor.start_imap_service + if flags.OFFLINE is True: + provider_domain = self._login_widget.get_selected_provider() + self._provider_config.load( + provider.get_provider_path(provider_domain)) + provides_mx = self._provider_config.provides_mx() + + if flags.OFFLINE is True and provides_mx: + start_fun() + return + + if self._provides_mx_and_enabled(): + start_fun() def _on_mail_client_logged_in(self, req): """ @@ -1120,8 +1424,13 @@ class MainWindow(QtGui.QMainWindow): TRIGGERS: self.logout """ + cv = Condition() + cv.acquire() # TODO call stop_mail_service - self._mail_conductor.stop_imap_service() + threads.deferToThread(self._mail_conductor.stop_imap_service, cv) + # and wait for it to be stopped + logger.debug('Waiting for imap service to stop.') + cv.wait(self.SERVICE_STOP_TIMEOUT) # end service control methods (imap) @@ -1171,27 +1480,65 @@ class MainWindow(QtGui.QMainWindow): """ self._eip_connection.qtsigs.connected_signal.emit() + # check for connectivity + provider_config = self._get_best_provider_config() + domain = provider_config.get_domain() + self._check_name_resolution(domain) + + def _check_name_resolution(self, domain): + """ + Check if we can resolve the given domain name. + + :param domain: the domain to check. + :type domain: str + """ + def do_check(): + """ + Try to resolve the domain name. + """ + socket.gethostbyname(domain.encode('idna')) + + def check_err(failure): + """ + Errback handler for `do_check`. + + :param failure: the failure that triggered the errback. + :type failure: twisted.python.failure.Failure + """ + logger.error(repr(failure)) + logger.error("Can't resolve hostname.") + + msg = self.tr( + "The server at {0} can't be found, because the DNS lookup " + "failed. DNS is the network service that translates a " + "website's name to its Internet address. Either your computer " + "is having trouble connecting to the network, or you are " + "missing some helper files that are needed to securely use " + "DNS while {1} is active. To install these helper files, quit " + "this application and start it again." + ).format(domain, self._eip_name) + + show_err = lambda: QtGui.QMessageBox.critical( + self, self.tr("Connection Error"), msg) + reactor.callLater(0, show_err) + + # python 2.7.4 raises socket.error + # python 2.7.5 raises socket.gaierror + failure.trap(socket.gaierror, socket.error) + + d = threads.deferToThread(do_check) + d.addErrback(check_err) + def _try_autostart_eip(self): """ Tries to autostart EIP """ settings = self._settings - should_autostart = settings.get_autostart_eip() - if not should_autostart: - logger.debug('Will not autostart EIP since it is setup ' - 'to not to do it') - self.eip_needs_login.emit() + if not self._update_eip_enabled_status(): return default_provider = settings.get_defaultprovider() - - if default_provider is None: - logger.info("Cannot autostart Encrypted Internet because there is " - "no default provider configured") - self.eip_needs_login.emit() - return - self._enabled_services = settings.get_enabled_services( default_provider) @@ -1202,7 +1549,7 @@ class MainWindow(QtGui.QMainWindow): # it adds some delay. # Maybe if it's the first run in a session, # or we can try only if it fails. - self._download_eip_config() + self._maybe_start_eip() else: # XXX: Display a proper message to the user self.eip_needs_login.emit() @@ -1336,8 +1683,9 @@ class MainWindow(QtGui.QMainWindow): if self._logged_user: self._eip_status.set_provider( - "%s@%s" % (self._logged_user, - self._get_best_provider_config().get_domain())) + make_address( + self._logged_user, + self._get_best_provider_config().get_domain())) self._eip_status.eip_stopped() @QtCore.Slot() @@ -1434,18 +1782,16 @@ class MainWindow(QtGui.QMainWindow): # eip boostrapping, config etc... - def _download_eip_config(self): + def _maybe_start_eip(self): """ - Starts the EIP bootstrapping sequence + Start the EIP bootstrapping sequence if the client is configured to + do so. """ 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(EIP_SERVICE) > 0 and \ - not self._already_started_eip: - + if self._provides_eip_and_enabled() and not self._already_started_eip: # XXX this should be handled by the state machine. self._eip_status.set_eip_status( self.tr("Starting...")) @@ -1453,14 +1799,22 @@ class MainWindow(QtGui.QMainWindow): provider_config, download_if_needed=True) self._already_started_eip = True - elif not self._already_started_eip: - if self._enabled_services.count(EIP_SERVICE) > 0: - self._eip_status.set_eip_status( - self.tr("Not supported"), - error=True) - else: - self._eip_status.disable_eip_start() - self._eip_status.set_eip_status(self.tr("Disabled")) + # we want to start soledad anyway after a certain timeout if eip + # fails to come up + QtCore.QTimer.singleShot( + self.EIP_TIMEOUT, + self._maybe_run_soledad_setup_checks) + else: + if not self._already_started_eip: + if EIP_SERVICE in self._enabled_services: + self._eip_status.set_eip_status( + self.tr("Not supported"), + error=True) + else: + self._eip_status.disable_eip_start() + self._eip_status.set_eip_status(self.tr("Disabled")) + # eip will not start, so we start soledad anyway + self._maybe_run_soledad_setup_checks() def _finish_eip_bootstrap(self, data): """ @@ -1506,11 +1860,11 @@ class MainWindow(QtGui.QMainWindow): This is used for intermediate bootstrapping stages, in case they fail. """ - passed = data[self._provider_bootstrapper.PASSED_KEY] + passed = data[self._backend.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]) + logger.error(data[self._backend.ERROR_KEY]) self._already_started_eip = False # end of EIP methods --------------------------------------------- @@ -1547,20 +1901,35 @@ class MainWindow(QtGui.QMainWindow): Starts the logout sequence """ - self._soledad_bootstrapper.cancel_bootstrap() setProxiedObject(self._soledad, None) + self._cancel_ongoing_defers() + + # reset soledad status flag + self._already_started_soledad = False + # XXX: If other defers are doing authenticated stuff, this # might conflict with those. CHECK! threads.deferToThread(self._srp_auth.logout) self.logout.emit() - def _done_logging_out(self, ok, message): - # TODO missing params in docstring + def _logout_error(self): + """ + SLOT + TRIGGER: self._srp_auth.logout_error + + Inform the user about a logout error. + """ + self._login_widget.done_logout() + self.ui.lblLoginProvider.setText(self.tr("Login")) + self._login_widget.set_status( + self.tr("Something went wrong with the logout.")) + + def _logout_ok(self): """ SLOT - TRIGGER: self._srp_auth.logout_finished + TRIGGER: self._srp_auth.logout_ok Switches the stackedWidget back to the login stage after logging out @@ -1568,36 +1937,30 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.done_logout() self.ui.lblLoginProvider.setText(self.tr("Login")) - if ok: - self._logged_user = None - self._login_widget.logged_out() - self._mail_status.mail_state_disabled() - - else: - self._login_widget.set_login_status( - self.tr("Something went wrong with the logout."), - error=True) + self._logged_user = None + self._login_widget.logged_out() + self._mail_status.mail_state_disabled() def _intermediate_stage(self, data): # TODO this method name is confusing as hell. """ SLOT TRIGGERS: - self._provider_bootstrapper.name_resolution - self._provider_bootstrapper.https_connection - self._provider_bootstrapper.download_ca_cert + self._backend.signaler.prov_name_resolution + self._backend.signaler.prov_https_connection + self._backend.signaler.prov_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] + passed = data[self._backend.PASSED_KEY] if not passed: + msg = self.tr("Unable to connect: Problem with provider") + self._login_widget.set_status(msg) 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]) + logger.error(data[self._backend.ERROR_KEY]) # # window handling methods @@ -1647,7 +2010,7 @@ class MainWindow(QtGui.QMainWindow): """ logger.debug('About to quit, doing cleanup...') - self._mail_conductor.stop_imap_service() + self._stop_imap_service() if self._srp_auth is not None: if self._srp_auth.get_session_id() is not None or \ @@ -1664,13 +2027,7 @@ class MainWindow(QtGui.QMainWindow): 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() + self._cancel_ongoing_defers() # TODO missing any more cancels? @@ -1687,8 +2044,8 @@ class MainWindow(QtGui.QMainWindow): # Set this in case that the app is hidden QtGui.QApplication.setQuitOnLastWindowClosed(True) + self._backend.stop() self._cleanup_and_quit() - self._really_quit = True if self._wizard: diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index b4bddef2..b2cc2236 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -18,7 +18,6 @@ """ Preferences window """ -import os import logging from functools import partial @@ -26,6 +25,7 @@ from functools import partial from PySide import QtCore, QtGui from zope.proxy import sameProxiedObjects +from leap.bitmask.provider import get_provider_path from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.gui.ui_preferences import Ui_Preferences from leap.soledad.client import NoStorageSecret @@ -42,6 +42,8 @@ class PreferencesWindow(QtGui.QDialog): """ Window that displays the preferences. """ + preferences_saved = QtCore.Signal() + def __init__(self, parent, srp_auth, provider_config, soledad, domain): """ :param parent: parent object of the PreferencesWindow. @@ -369,6 +371,7 @@ class PreferencesWindow(QtGui.QDialog): "Services settings for provider '{0}' saved.".format(provider)) logger.debug(msg) self._set_providers_services_status(msg, success=True) + self.preferences_saved.emit() def _get_provider_config(self, domain): """ @@ -380,10 +383,7 @@ class PreferencesWindow(QtGui.QDialog): :rtype: ProviderConfig or None if there is a problem loading the config """ provider_config = ProviderConfig() - provider_config_path = os.path.join( - "leap", "providers", domain, "provider.json") - - if not provider_config.load(provider_config_path): + if not provider_config.load(get_provider_path(domain)): provider_config = None return provider_config diff --git a/src/leap/bitmask/gui/twisted_main.py b/src/leap/bitmask/gui/twisted_main.py index e11af7bd..1e876c57 100644 --- a/src/leap/bitmask/gui/twisted_main.py +++ b/src/leap/bitmask/gui/twisted_main.py @@ -27,24 +27,6 @@ from twisted.internet import 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. diff --git a/src/leap/bitmask/gui/ui/advanced_key_management.ui b/src/leap/bitmask/gui/ui/advanced_key_management.ui index d61aa87e..1112670f 100644 --- a/src/leap/bitmask/gui/ui/advanced_key_management.ui +++ b/src/leap/bitmask/gui/ui/advanced_key_management.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>431</width> - <height>188</height> + <width>504</width> + <height>546</height> </rect> </property> <property name="windowTitle"> @@ -17,10 +17,13 @@ <iconset resource="../../../../../data/resources/mainwindow.qrc"> <normaloff>:/images/mask-icon.png</normaloff>:/images/mask-icon.png</iconset> </property> - <layout class="QGridLayout" name="gridLayout_3"> - <item row="0" column="0"> - <widget class="QWidget" name="container" native="true"> - <layout class="QGridLayout" name="gridLayout_2"> + <layout class="QGridLayout" name="gridLayout_4"> + <item row="1" column="0" colspan="2"> + <widget class="QGroupBox" name="gbMyKeyPair"> + <property name="title"> + <string>My key pair</string> + </property> + <layout class="QGridLayout" name="gridLayout_3"> <item row="0" column="0"> <widget class="QLabel" name="label"> <property name="text"> @@ -90,20 +93,7 @@ </property> </widget> </item> - <item row="3" column="1"> - <spacer name="verticalSpacer"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - <item row="4" column="0" colspan="2"> + <item row="3" column="0" colspan="2"> <layout class="QGridLayout" name="gridLayout"> <item row="1" column="1"> <widget class="QPushButton" name="pbExportKeys"> @@ -135,9 +125,56 @@ </layout> </item> </layout> + <zorder>leKeyID</zorder> + <zorder>leUser</zorder> + <zorder>leFingerprint</zorder> + <zorder>label_3</zorder> + <zorder>label_5</zorder> + <zorder>label</zorder> + </widget> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QGroupBox" name="gbStoredPublicKeys"> + <property name="title"> + <string>Stored public keys</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QTableWidget" name="twPublicKeys"> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="textElideMode"> + <enum>Qt::ElideRight</enum> + </property> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + <column> + <property name="text"> + <string>Email</string> + </property> + </column> + <column> + <property name="text"> + <string>Key ID</string> + </property> + </column> + </widget> + </item> + </layout> </widget> </item> - <item row="1" column="0"> + <item row="3" column="0" colspan="2"> <widget class="QLabel" name="lblStatus"> <property name="text"> <string/> diff --git a/src/leap/bitmask/gui/ui/eip_status.ui b/src/leap/bitmask/gui/ui/eip_status.ui index d078ca0c..64821ad6 100644 --- a/src/leap/bitmask/gui/ui/eip_status.ui +++ b/src/leap/bitmask/gui/ui/eip_status.ui @@ -25,6 +25,9 @@ </property> <item> <layout class="QGridLayout" name="gridLayout"> + <property name="verticalSpacing"> + <number>0</number> + </property> <item row="0" column="2"> <widget class="QPushButton" name="btnEipStartStop"> <property name="text"> @@ -75,13 +78,6 @@ <verstretch>0</verstretch> </sizepolicy> </property> - <property name="font"> - <font> - <pointsize>14</pointsize> - <weight>75</weight> - <bold>true</bold> - </font> - </property> <property name="text"> <string>Traffic is being routed in the clear</string> </property> @@ -124,12 +120,6 @@ </item> <item row="2" column="1" colspan="3"> <widget class="QWidget" name="eip_bandwidth" native="true"> - <property name="maximumSize"> - <size> - <width>16777215</width> - <height>32</height> - </size> - </property> <layout class="QHBoxLayout" name="horizontalLayout"> <property name="spacing"> <number>0</number> @@ -146,16 +136,6 @@ <enum>QLayout::SetDefaultConstraint</enum> </property> <item> - <widget class="QLabel" name="label_5"> - <property name="text"> - <string/> - </property> - <property name="pixmap"> - <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/down-arrow.png</pixmap> - </property> - </widget> - </item> - <item> <widget class="QPushButton" name="btnDownload"> <property name="sizePolicy"> <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> @@ -175,25 +155,18 @@ <height>16777215</height> </size> </property> - <property name="font"> - <font> - <pointsize>11</pointsize> - <weight>75</weight> - <bold>true</bold> - </font> - </property> <property name="cursor"> <cursorShape>PointingHandCursor</cursorShape> </property> - <property name="styleSheet"> - <string notr="true">text-align: left;</string> - </property> <property name="text"> <string>0.0 KB/s</string> </property> <property name="flat"> <bool>true</bool> </property> + <property name="icon"> + <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/down-arrow.png</pixmap> + </property> </widget> </item> <item> @@ -206,22 +179,12 @@ </property> <property name="sizeHint" stdset="0"> <size> - <width>10</width> + <width>20</width> <height>20</height> </size> </property> </spacer> </item> - <item> - <widget class="QLabel" name="label_7"> - <property name="text"> - <string/> - </property> - <property name="pixmap"> - <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/up-arrow.png</pixmap> - </property> - </widget> - </item> <item alignment="Qt::AlignLeft"> <widget class="QPushButton" name="btnUpload"> <property name="sizePolicy"> @@ -242,25 +205,18 @@ <height>16777215</height> </size> </property> - <property name="font"> - <font> - <pointsize>11</pointsize> - <weight>75</weight> - <bold>true</bold> - </font> - </property> <property name="cursor"> <cursorShape>PointingHandCursor</cursorShape> </property> - <property name="styleSheet"> - <string notr="true">text-align: left;</string> - </property> <property name="text"> <string>0.0 KB/s</string> </property> <property name="flat"> <bool>true</bool> </property> + <property name="icon"> + <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/up-arrow.png</pixmap> + </property> </widget> </item> <item> @@ -271,7 +227,7 @@ <property name="sizeHint" stdset="0"> <size> <width>0</width> - <height>20</height> + <height>0</height> </size> </property> </spacer> diff --git a/src/leap/bitmask/gui/ui/loggerwindow.ui b/src/leap/bitmask/gui/ui/loggerwindow.ui index 3de786f7..b19ed91a 100644 --- a/src/leap/bitmask/gui/ui/loggerwindow.ui +++ b/src/leap/bitmask/gui/ui/loggerwindow.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>648</width> - <height>469</height> + <width>769</width> + <height>464</height> </rect> </property> <property name="windowTitle"> @@ -154,6 +154,17 @@ </property> </widget> </item> + <item> + <widget class="QPushButton" name="btnPastebin"> + <property name="text"> + <string>Send to Pastebin.com</string> + </property> + <property name="icon"> + <iconset resource="../../../../../data/resources/loggerwindow.qrc"> + <normaloff>:/images/pastebin.png</normaloff>:/images/pastebin.png</iconset> + </property> + </widget> + </item> </layout> </item> </layout> diff --git a/src/leap/bitmask/gui/ui/login.ui b/src/leap/bitmask/gui/ui/login.ui index e7ca1652..f5725d5a 100644 --- a/src/leap/bitmask/gui/ui/login.ui +++ b/src/leap/bitmask/gui/ui/login.ui @@ -215,28 +215,8 @@ <number>0</number> </property> <property name="bottomMargin"> - <number>24</number> + <number>0</number> </property> - <item row="1" column="1"> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item row="1" column="0"> - <widget class="QPushButton" name="btnLogout"> - <property name="text"> - <string>Logout</string> - </property> - </widget> - </item> <item row="0" column="0" colspan="2"> <widget class="QLabel" name="lblUser"> <property name="font"> @@ -251,17 +231,26 @@ </property> </widget> </item> - <item row="2" column="0" colspan="2"> - <widget class="QLabel" name="lblLoginStatus"> - <property name="styleSheet"> - <string notr="true">color: rgb(132, 132, 132); -font: 75 12pt "Lucida Grande";</string> - </property> + <item row="1" column="0"> + <widget class="QPushButton" name="btnLogout"> <property name="text"> - <string/> + <string>Logout</string> </property> </widget> </item> + <item row="1" column="1"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> </layout> </widget> </item> diff --git a/src/leap/bitmask/gui/ui/mainwindow.ui b/src/leap/bitmask/gui/ui/mainwindow.ui index 3b83788e..d755115a 100644 --- a/src/leap/bitmask/gui/ui/mainwindow.ui +++ b/src/leap/bitmask/gui/ui/mainwindow.ui @@ -75,7 +75,7 @@ <x>0</x> <y>0</y> <width>524</width> - <height>651</height> + <height>667</height> </rect> </property> <layout class="QVBoxLayout" name="verticalLayout"> @@ -86,108 +86,6 @@ <number>0</number> </property> <item> - <widget class="QWidget" name="eipWidget" native="true"> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <property name="spacing"> - <number>0</number> - </property> - <property name="margin"> - <number>0</number> - </property> - <item> - <widget class="QFrame" name="frame_2"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="styleSheet"> - <string notr="true">QFrame{background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(160, 160, 160, 128), stop:1 rgba(255, 255, 255, 0));}</string> - </property> - <layout class="QHBoxLayout" name="horizontalLayout_3"> - <property name="leftMargin"> - <number>24</number> - </property> - <property name="rightMargin"> - <number>24</number> - </property> - <item> - <widget class="QLabel" name="label_2"> - <property name="font"> - <font> - <pointsize>16</pointsize> - <weight>75</weight> - <bold>true</bold> - </font> - </property> - <property name="styleSheet"> - <string notr="true">background-color: rgba(255, 255, 255, 0);</string> - </property> - <property name="text"> - <string>Encrypted Internet</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="btnEIPPreferences"> - <property name="maximumSize"> - <size> - <width>48</width> - <height>20</height> - </size> - </property> - <property name="styleSheet"> - <string notr="true"/> - </property> - <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="../../../../../data/resources/mainwindow.qrc"> - <normaloff>:/images/black/32/gear.png</normaloff>:/images/black/32/gear.png</iconset> - </property> - <property name="autoDefault"> - <bool>false</bool> - </property> - <property name="default"> - <bool>false</bool> - </property> - <property name="flat"> - <bool>false</bool> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <layout class="QVBoxLayout" name="eipLayout"> - <property name="leftMargin"> - <number>12</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>12</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - </layout> - </item> - </layout> - </widget> - </item> - <item> - <widget class="Line" name="line"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - </widget> - </item> - <item> <widget class="QFrame" name="frame"> <property name="sizePolicy"> <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> @@ -199,9 +97,7 @@ <bool>false</bool> </property> <property name="styleSheet"> - <string notr="true">QFrame{ -background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(160, 160, 160, 128), stop:1 rgba(255, 255, 255, 0)); -}</string> + <string notr="true">background-color: rgba(0,0,0,20); border-bottom: 1px solid rgba(0,0,0,30);</string> </property> <layout class="QHBoxLayout" name="horizontalLayout"> <property name="leftMargin"> @@ -214,36 +110,15 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb <widget class="QLabel" name="lblLoginProvider"> <property name="font"> <font> - <pointsize>16</pointsize> <weight>75</weight> <bold>true</bold> </font> </property> <property name="styleSheet"> - <string notr="true">background-color: rgba(255, 255, 255, 0);</string> - </property> - <property name="text"> - <string>Login</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="btnPreferences"> - <property name="maximumSize"> - <size> - <width>48</width> - <height>20</height> - </size> - </property> - <property name="styleSheet"> - <string notr="true"/> + <string notr="true">background-color: rgba(255, 255, 255, 0); border: none;</string> </property> <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="../../../../../data/resources/mainwindow.qrc"> - <normaloff>:/images/black/32/gear.png</normaloff>:/images/black/32/gear.png</iconset> + <string>Please Log In</string> </property> </widget> </item> @@ -258,7 +133,42 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb </layout> </item> <item> - <widget class="Line" name="line_2"> + <widget class="Line" name="lineUnderLogin"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="eipWidget" native="true"> + <layout class="QVBoxLayout" name="eipVerticalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="margin"> + <number>0</number> + </property> + <item> + <layout class="QVBoxLayout" name="eipLayout"> + <property name="leftMargin"> + <number>12</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>12</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="Line" name="lineUnderEIP"> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> @@ -287,6 +197,13 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb </widget> </item> <item> + <widget class="Line" name="lineUnderEmail"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> <spacer name="verticalSpacer"> <property name="orientation"> <enum>Qt::Vertical</enum> @@ -380,7 +297,7 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb <x>0</x> <y>0</y> <width>524</width> - <height>21</height> + <height>23</height> </rect> </property> <widget class="QMenu" name="menuFile"> @@ -390,11 +307,14 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb <addaction name="action_create_new_account"/> <addaction name="action_advanced_key_management"/> <addaction name="separator"/> + <addaction name="action_preferences"/> + <addaction name="action_eip_preferences"/> + <addaction name="separator"/> <addaction name="action_quit"/> </widget> <widget class="QMenu" name="menuHelp"> <property name="title"> - <string>Help</string> + <string>&Help</string> </property> <addaction name="action_help"/> <addaction name="action_show_logs"/> @@ -404,10 +324,17 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb <addaction name="menuFile"/> <addaction name="menuHelp"/> </widget> - <widget class="QStatusBar" name="statusbar"/> <action name="action_preferences"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Account Preferences...</string> + </property> + </action> + <action name="action_eip_preferences"> <property name="text"> - <string>Preferences...</string> + <string>Internet Preferences...</string> </property> </action> <action name="action_quit"> diff --git a/src/leap/bitmask/gui/ui/wizard.ui b/src/leap/bitmask/gui/ui/wizard.ui index cf591470..6c592522 100644 --- a/src/leap/bitmask/gui/ui/wizard.ui +++ b/src/leap/bitmask/gui/ui/wizard.ui @@ -59,7 +59,7 @@ <item row="0" column="0"> <widget class="QLabel" name="label_3"> <property name="text"> - <string><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></string> + <string><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;">'Bitmask -&gt; Create new account...'</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></string> </property> <property name="textFormat"> <enum>Qt::RichText</enum> @@ -269,7 +269,7 @@ <string>Configure or select a provider</string> </property> <layout class="QGridLayout" name="gridLayout_5"> - <item row="0" column="0"> + <item row="1" column="0"> <widget class="QRadioButton" name="rbNewProvider"> <property name="text"> <string>Configure new provider:</string> @@ -279,14 +279,14 @@ </property> </widget> </item> - <item row="2" column="0"> - <widget class="QRadioButton" name="rbExistingProvider"> - <property name="text"> - <string>Use existing one:</string> + <item row="0" column="2"> + <widget class="QComboBox" name="cbProviders"> + <property name="enabled"> + <bool>false</bool> </property> </widget> </item> - <item row="1" column="0"> + <item row="1" column="1"> <widget class="QLabel" name="label"> <property name="text"> <string>https://</string> @@ -296,17 +296,20 @@ </property> </widget> </item> - <item row="1" column="1"> + <item row="1" column="2"> <widget class="QLineEdit" name="lnProvider"/> </item> - <item row="1" column="2"> - <widget class="QPushButton" name="btnCheck"> + <item row="0" column="0"> + <widget class="QRadioButton" name="rbExistingProvider"> <property name="text"> - <string>Check</string> + <string>Use existing one:</string> + </property> + <property name="checked"> + <bool>false</bool> </property> </widget> </item> - <item row="3" column="0"> + <item row="0" column="1"> <widget class="QLabel" name="label_8"> <property name="text"> <string>https://</string> @@ -316,12 +319,29 @@ </property> </widget> </item> - <item row="3" column="1"> - <widget class="QComboBox" name="cbProviders"> - <property name="enabled"> - <bool>false</bool> - </property> - </widget> + <item row="2" column="2"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="btnCheck"> + <property name="text"> + <string>Check</string> + </property> + </widget> + </item> + </layout> </item> </layout> </widget> @@ -820,8 +840,8 @@ <slot>setFocus()</slot> <hints> <hint type="sourcelabel"> - <x>167</x> - <y>192</y> + <x>174</x> + <y>174</y> </hint> <hint type="destinationlabel"> <x>265</x> @@ -836,12 +856,12 @@ <slot>setFocus()</slot> <hints> <hint type="sourcelabel"> - <x>171</x> - <y>164</y> + <x>174</x> + <y>227</y> </hint> <hint type="destinationlabel"> - <x>246</x> - <y>164</y> + <x>425</x> + <y>254</y> </hint> </hints> </connection> @@ -852,12 +872,12 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>169</x> - <y>196</y> + <x>174</x> + <y>174</y> </hint> <hint type="destinationlabel"> - <x>327</x> - <y>163</y> + <x>450</x> + <y>266</y> </hint> </hints> </connection> @@ -868,8 +888,8 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>169</x> - <y>162</y> + <x>174</x> + <y>227</y> </hint> <hint type="destinationlabel"> <x>269</x> @@ -881,15 +901,15 @@ <sender>rbExistingProvider</sender> <signal>toggled(bool)</signal> <receiver>btnCheck</receiver> - <slot>setDisabled(bool)</slot> + <slot>setEnabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>154</x> - <y>193</y> + <x>169</x> + <y>174</y> </hint> <hint type="destinationlabel"> - <x>498</x> - <y>170</y> + <x>520</x> + <y>255</y> </hint> </hints> </connection> diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index 5f5224ae..e2c1a16e 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -17,22 +17,17 @@ """ First run wizard """ -import os import logging -import json import random from functools import partial from PySide import QtCore, QtGui -from twisted.internet import threads from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.crypto.srpregister import SRPRegister -from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper +from leap.bitmask.provider import get_provider_path from leap.bitmask.services import get_service_display_name, get_supported -from leap.bitmask.util.request_helpers import get_content from leap.bitmask.util.keyring_helpers import has_keyring from leap.bitmask.util.password import basic_password_checks @@ -55,12 +50,15 @@ class Wizard(QtGui.QWizard): BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" - def __init__(self, bypass_checks=False): + def __init__(self, backend, bypass_checks=False): """ Constructor for the main Wizard. + :param backend: Backend being used + :type backend: Backend :param bypass_checks: Set to true if the app should bypass - first round of checks for CA certificates at bootstrap + first round of checks for CA + certificates at bootstrap :type bypass_checks: bool """ QtGui.QWizard.__init__(self) @@ -86,23 +84,14 @@ class Wizard(QtGui.QWizard): 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._backend = backend + self._backend_connect() self._domain = None - self._provider_config = ProviderConfig() + # HACK!! We need provider_config for the time being, it'll be + # removed + self._provider_config = ( + self._backend._components["provider"]._provider_config) # We will store a reference to the defers for eventual use # (eg, to cancel them) but not doing anything with them right now. @@ -114,6 +103,8 @@ class Wizard(QtGui.QWizard): self.ui.lnProvider.textChanged.connect(self._enable_check) self.ui.rbNewProvider.toggled.connect( lambda x: self._enable_check()) + self.ui.cbProviders.currentIndexChanged[int].connect( + self._reset_provider_check) self.ui.lblUser.returnPressed.connect( self._focus_password) @@ -166,6 +157,7 @@ class Wizard(QtGui.QWizard): self._provider_setup_ok = False self.ui.lnProvider.setText('') self.ui.grpCheckProvider.setVisible(False) + self._backend_disconnect() def _load_configured_providers(self): """ @@ -199,6 +191,10 @@ class Wizard(QtGui.QWizard): random.shuffle(pinned) # don't prioritize alphabetically self.ui.cbProviders.addItems(pinned) + # We have configured providers, so by default we select the + # 'Use existing provider' option. + self.ui.rbExistingProvider.setChecked(True) + def get_domain(self): return self._domain @@ -225,7 +221,7 @@ class Wizard(QtGui.QWizard): depending on the lnProvider content. """ enabled = len(self.ui.lnProvider.text()) != 0 - enabled = enabled and self.ui.rbNewProvider.isChecked() + enabled = enabled or self.ui.rbExistingProvider.isChecked() self.ui.btnCheck.setEnabled(enabled) if reset: @@ -255,16 +251,11 @@ class Wizard(QtGui.QWizard): ok, msg = basic_password_checks(username, password, password2) if ok: - register = SRPRegister(provider_config=self._provider_config) - register.registration_finished.connect( - self._registration_finished) - - threads.deferToThread( - partial(register.register_user, username, password)) + self._set_register_status(self.tr("Starting registration...")) + self._backend.register_user(self._domain, username, password) self._username = username self._password = password - self._set_register_status(self.tr("Starting registration...")) else: self._set_register_status(msg, error=True) self._focus_password() @@ -291,42 +282,59 @@ class Wizard(QtGui.QWizard): # register button self.ui.btnRegister.setVisible(visible) - def _registration_finished(self, ok, req): - if ok: - user_domain = self._username + "@" + self._domain - message = "<font color='green'><h3>" - message += self.tr("User %s successfully registered.") % ( - user_domain, ) - message += "</h3></font>" - 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("Something has gone wrong. " - "Please try again.") - 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 _registration_finished(self): + """ + SLOT + TRIGGERS: + self._backend.signaler.srp_registration_finished + + The registration has finished successfully, so we do some final steps. + """ + user_domain = self._username + "@" + self._domain + message = "<font color='green'><h3>" + message += self.tr("User %s successfully registered.") % ( + user_domain, ) + message += "</h3></font>" + 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) + + def _registration_failed(self): + """ + SLOT + TRIGGERS: + self._backend.signaler.srp_registration_failed + + The registration has failed, so we report the problem. + """ + self._username = self._password = None + + error_msg = self.tr("Something has gone wrong. Please try again.") + self._set_register_status(error_msg, error=True) + self.ui.btnRegister.setEnabled(True) + + def _registration_taken(self): + """ + SLOT + TRIGGERS: + self._backend.signaler.srp_registration_taken + + The requested username is taken, warn the user about that. + """ + self._username = self._password = None + + error_msg = self.tr("The requested username is taken, choose another.") + self._set_register_status(error_msg, error=True) + self.ui.btnRegister.setEnabled(True) def _set_register_status(self, status, error=False): """ @@ -369,8 +377,10 @@ class Wizard(QtGui.QWizard): Starts the checks for a given provider """ - if len(self.ui.lnProvider.text()) == 0: - return + if self.ui.rbNewProvider.isChecked(): + self._domain = self.ui.lnProvider.text() + else: + self._domain = self.ui.cbProviders.currentText() self._provider_checks_ok = False @@ -382,11 +392,10 @@ class Wizard(QtGui.QWizard): 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) + self._provider_select_defer = self._backend.\ + setup_provider(self._domain) def _skip_provider_checks(self, skip): """ @@ -403,8 +412,6 @@ class Wizard(QtGui.QWizard): if skip: self._reset_provider_check() - self.page(self.SELECT_PROVIDER_PAGE).set_completed(skip) - self.button(QtGui.QWizard.NextButton).setEnabled(skip) self._use_existing_provider = skip def _complete_task(self, data, label, complete=False, complete_page=-1): @@ -423,8 +430,8 @@ class Wizard(QtGui.QWizard): :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] + passed = data[self._backend.PASSED_KEY] + error = data[self._backend.ERROR_KEY] if passed: label.setPixmap(self.OK_ICON) if complete: @@ -437,13 +444,13 @@ class Wizard(QtGui.QWizard): def _name_resolution(self, data): """ SLOT - TRIGGER: self._provider_bootstrapper.name_resolution + TRIGGER: self._backend.signaler.prov_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] + passed = data[self._backend.PASSED_KEY] if not passed: status = self.tr("<font color='red'><b>Non-existent " "provider</b></font>") @@ -456,16 +463,16 @@ class Wizard(QtGui.QWizard): def _https_connection(self, data): """ SLOT - TRIGGER: self._provider_bootstrapper.https_connection + TRIGGER: self._backend.signaler.prov_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] + passed = data[self._backend.PASSED_KEY] if not passed: status = self.tr("<font color='red'><b>%s</b></font>") \ - % (data[self._provider_bootstrapper.ERROR_KEY]) + % (data[self._backend.ERROR_KEY]) self.ui.lblProviderSelectStatus.setText(status) else: self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) @@ -475,29 +482,26 @@ class Wizard(QtGui.QWizard): def _download_provider_info(self, data): """ SLOT - TRIGGER: self._provider_bootstrapper.download_provider_info + TRIGGER: self._backend.signaler.prov_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")): + if self._provider_config.load(get_provider_path(self._domain)): self._complete_task(data, self.ui.lblProviderInfo, True, self.SELECT_PROVIDER_PAGE) self._provider_checks_ok = True else: new_data = { - self._provider_bootstrapper.PASSED_KEY: False, - self._provider_bootstrapper.ERROR_KEY: + self._backend.PASSED_KEY: False, + self._backend.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]: + if not data[self._backend.PASSED_KEY]: status = self.tr("<font color='red'><b>Not a valid provider" "</b></font>") self.ui.lblProviderSelectStatus.setText(status) @@ -507,31 +511,31 @@ class Wizard(QtGui.QWizard): def _download_ca_cert(self, data): """ SLOT - TRIGGER: self._provider_bootstrapper.download_ca_cert + TRIGGER: self._backend.signaler.prov_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] + passed = data[self._backend.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 + TRIGGER: self._backend.signaler.prov_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] + passed = data[self._backend.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 + TRIGGER: self._backend.signaler.prov_check_api_certificate Sets the status for the API certificate check. Also finishes the provider bootstrapper thread since it's not needed anymore @@ -597,6 +601,7 @@ class Wizard(QtGui.QWizard): Prepares the pages when they appear """ if pageId == self.SELECT_PROVIDER_PAGE: + self._clear_register_widgets() skip = self.ui.rbExistingProvider.isChecked() if not self._provider_checks_ok: self._enable_check() @@ -611,8 +616,8 @@ class Wizard(QtGui.QWizard): sub_title = sub_title.format(self._provider_config.get_name()) self.page(pageId).setSubTitle(sub_title) self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON) - self._provider_setup_defer = self._provider_bootstrapper.\ - run_provider_setup_checks(self._provider_config) + self._provider_setup_defer = self._backend.\ + provider_bootstrap(self._domain) if pageId == self.PRESENT_PROVIDER_PAGE: self.page(pageId).setSubTitle(self.tr("Description of services " @@ -670,3 +675,50 @@ class Wizard(QtGui.QWizard): return self.SERVICES_PAGE return QtGui.QWizard.nextId(self) + + def _clear_register_widgets(self): + """ + Clears the widgets that my be filled and a possible error message. + """ + self._set_register_status("") + self.ui.lblUser.setText("") + self.ui.lblPassword.setText("") + self.ui.lblPassword2.setText("") + + def _backend_connect(self): + """ + Connects all the backend signals with the wizard. + """ + sig = self._backend.signaler + sig.prov_name_resolution.connect(self._name_resolution) + sig.prov_https_connection.connect(self._https_connection) + sig.prov_download_provider_info.connect(self._download_provider_info) + + sig.prov_download_ca_cert.connect(self._download_ca_cert) + sig.prov_check_ca_fingerprint.connect(self._check_ca_fingerprint) + sig.prov_check_api_certificate.connect(self._check_api_certificate) + + sig.srp_registration_finished.connect(self._registration_finished) + sig.srp_registration_failed.connect(self._registration_failed) + sig.srp_registration_taken.connect(self._registration_taken) + + def _backend_disconnect(self): + """ + This method is called when the wizard dialog is closed. + We disconnect all the backend signals in here. + """ + sig = self._backend.signaler + try: + # disconnect backend signals + sig.prov_name_resolution.disconnect(self._name_resolution) + sig.prov_https_connection.disconnect(self._https_connection) + sig.prov_download_provider_info.disconnect( + self._download_provider_info) + + sig.prov_download_ca_cert.disconnect(self._download_ca_cert) + sig.prov_check_ca_fingerprint.disconnect( + self._check_ca_fingerprint) + sig.prov_check_api_certificate.disconnect( + self._check_api_certificate) + except RuntimeError: + pass # Signal was not connected |