diff options
author | Tomás Touceda <chiiph@leap.se> | 2013-11-15 10:35:43 -0300 |
---|---|---|
committer | Tomás Touceda <chiiph@leap.se> | 2013-11-15 10:35:43 -0300 |
commit | d51d6bedfecd41491f2c8243235e7d3db043a4d7 (patch) | |
tree | 3d6152565be983d0e6ddc1fb0ce764bee060fc25 | |
parent | 23e9034664edf6b2a01a677c4fa4e6efd880e364 (diff) | |
parent | 72f53cc32bf20b00bcbd5f28bab5fc25250215bf (diff) |
Merge branch 'release-0.3.7'0.3.7
25 files changed, 758 insertions, 163 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 09ea992e..4fc4246e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,7 +3,34 @@ History ------- +0.3.7 Nov 15 -- the "The Big Lebowsky" release: ++++++++++++++++++++++++++++++++++++++++++++++++ +- Use custom SysTray in order to display per-service tooltip easily. + Closes #3998. +- Escape logs with html contents so they get displayed in plaintext + on the log viewer. Closes #4146. +- Wizard now behaves correctly in provider selection after click + 'cancel' or 'back'. Closes #4148. +- Handle Timeout errors during register process. Closes #4358. +- Send user's key to nickserver whenever keymanager is + initialized. Closes #4364. +- Password change dialog is now properly enabled. Closes #4449. +- Remember provider checks in wizard, do not re-run them if the user + goes back and forth through the wizard. Closes #3814 and #3815. +- Improve compatibility with OSX Mavericks. Fixes #4379. +- Initialize mail service with the userid after login, to allow + multiple accounts. Closes: #4394 +- Give SMTP the current logged in userid. Related to #3952. +- Do not wait for initial soledad sync to complete to launch mail + services. Closes: #4452 +- Add hint to user about the duration of the key generation. Closes + #3958. +- Add advanced key management feature. Closes #4448. +- Properly log EIP status changes. + 0.3.6 Nov 1 -- the "bạn có thể đọc này?" release: ++++++++++++++++++++++++++++++++++++++++++++++++++ + - Fix problem changing a non-ascii password. Closes #4003. - Enable password change in the client only if it has started the correct services. Closes #4093. @@ -20,7 +20,7 @@ TRANSLAT_DIR = data/translations PROJFILE = data/bitmask.pro #UI files to compile -UI_FILES = loggerwindow.ui mainwindow.ui wizard.ui login.ui preferences.ui eip_status.ui mail_status.ui eippreferences.ui +UI_FILES = loggerwindow.ui mainwindow.ui wizard.ui login.ui preferences.ui eip_status.ui mail_status.ui eippreferences.ui advanced_key_management.ui #Qt resource files to compile RESOURCES = locale.qrc loggerwindow.qrc mainwindow.qrc icons.qrc diff --git a/changes/log_eip_status b/changes/log_eip_status deleted file mode 100644 index 32ac2b42..00000000 --- a/changes/log_eip_status +++ /dev/null @@ -1 +0,0 @@ -- Properly log EIP status changes. diff --git a/docs/dev/encodings.rst b/docs/dev/encodings.rst new file mode 100644 index 00000000..a3fc4b70 --- /dev/null +++ b/docs/dev/encodings.rst @@ -0,0 +1,64 @@ +.. _encodings: + +Strings encoding problems +========================= + +This document is meant to avoid ``UnicodeError`` (``UnicodeEncodeError`` , ``UnicodeDecodeError``) and to set a base that allows the users to keep away headaches. + + +First approach +-------------- + +One of the problems with python 2 that makes hard to find out problems is the implicit conversion between ``str`` and ``unicode``. + +Look at this code:: + + >>> u'ä'.encode('utf-8') + '\xc3\xa4' + >>> + >>> u'ä'.decode('utf-8') + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "/usr/lib/python2.7/encodings/utf_8.py", line 16, in decode + return codecs.utf_8_decode(input, errors, True) + UnicodeEncodeError: 'ascii' codec can't encode character u'\xe4' in position 0: ordinal not in range(128) + +A situation like this could happen if the user confuse one type for another. 'encode' is a method of ``unicode`` and 'decode' is a method of ``str``, since you call 'decode', python "knows" how to convert from ``unicode`` to ``str`` and then call the 'decode' method, *that* conversion is made with the safe default "ascii" which raises an exception. + + +We need to know which one we are using **every time**. A possible way to avoid mistakes is to use ``leap_assert_type`` at the beginning of each method that has a ``str``/``unicode`` parameter. +The best approach we need to use ``unicode`` internally and when we read/write/transmit data, encode it to bytes (``str``). + + +Examples of problems found +-------------------------- + +* **logging data**: ``logger.debug("some string {0}".format(some_data))`` may fail if we have an ``unicode`` parameter because of the conversion needed to output it. + We need to use ``repr(some_data)`` to avoid encoding problems when sending data to the stdout. An easy way to do it is: ``logger.debug("some string {0!r}".format(some_data))`` + +- **paths encoding**: we should return always ``unicode`` values from helpers and encode them when we need to use it. + The stdlib handles correctly ``unicode`` strings path parameters. + If we want to do something else with the paths, we need to convert them manually using the system encoding. + +Regarding the encoding, use a hardcoded encoding may be wrong. +Instead of encode/decode using for instance 'utf-8', we should use this ``sys.getfilesystemencoding()`` + +For the data stored in a db (or something that is some way isolated from the system) we may want to choose 'utf-8' explicitly. + +Steps to improve code +--------------------- + +#. From now on, keep in mind the difference between ``str`` and ``unicode`` and write code consequently. +#. For each method we can add a ``leap_assert_type(parameter_name, unicode)`` (or ``str``) to avoid type problems. +#. Each time that is possible move towards the unicode 'frontier' (``unicode`` inside, ``str`` (bytes) outside). +#. When is possible update the methods parameters in order to be certain of the data types that we are handling. + +Recommended info +---------------- + +* PyCon 2012 talk: https://www.youtube.com/watch?v=sgHbC6udIqc + * article and transcription: http://nedbatchelder.com/text/unipain.html +* PyConAr 2012 (Spanish): http://www.youtube.com/watch?v=pQJ0emlYv50 +* Overcoming frustrations: http://pythonhosted.org/kitchen/unicode-frustrations.html +* Python's Unicode howto: http://docs.python.org/2/howto/unicode.html +* An encoding primer: http://www.danielmiessler.com/study/encoding/ diff --git a/docs/dev/quickstart.rst b/docs/dev/quickstart.rst index 8ef7dfb8..96dcaeb9 100644 --- a/docs/dev/quickstart.rst +++ b/docs/dev/quickstart.rst @@ -61,7 +61,7 @@ also install the needed dependencies:: Compile the resource files:: - (bitmask)$ make resources + (bitmask)$ make Copy necessary files into system folders, with root privileges:: diff --git a/docs/index.rst b/docs/index.rst index f210be8c..acf97ff3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -50,6 +50,7 @@ If you want to contribute to the project, we wrote this for you. dev/workflow dev/resources dev/internationalization + dev/encodings .. dev/internals dev/authors diff --git a/pkg/requirements.pip b/pkg/requirements.pip index 885b19f8..04cd33de 100644 --- a/pkg/requirements.pip +++ b/pkg/requirements.pip @@ -20,8 +20,8 @@ zope.proxy leap.common>=0.3.4 leap.soledad.client>=0.4.2 -leap.keymanager>=0.3.4 -leap.mail>=0.3.6 +leap.keymanager>=0.3.6 +leap.mail>=0.3.7 # Remove this when u1db fixes its dependency on oauth oauth diff --git a/pkg/scripts/bootstrap_develop.sh b/pkg/scripts/bootstrap_develop.sh index 4660c499..07063a81 100755 --- a/pkg/scripts/bootstrap_develop.sh +++ b/pkg/scripts/bootstrap_develop.sh @@ -38,12 +38,21 @@ clone_repos() { echo "${cc_green}Status: $status...${cc_normal}" set -x # show commands - # clone the leap.se repos - git clone ssh://gitolite@leap.se/bitmask_client - git clone ssh://gitolite@leap.se/leap_pycommon - git clone ssh://gitolite@leap.se/soledad - git clone ssh://gitolite@leap.se/keymanager - git clone ssh://gitolite@leap.se/leap_mail + if [[ "$1" == "ro" ]]; then + # read-only remotes: + git clone https://leap.se/git/bitmask_client + git clone https://leap.se/git/leap_pycommon + git clone https://leap.se/git/soledad + git clone https://leap.se/git/keymanager + git clone https://leap.se/git/leap_mail + else + # read-write remotes: + git clone ssh://gitolite@leap.se/bitmask_client + git clone ssh://gitolite@leap.se/leap_pycommon + git clone ssh://gitolite@leap.se/soledad + git clone ssh://gitolite@leap.se/keymanager + git clone ssh://gitolite@leap.se/leap_mail + fi set +x echo "${cc_green}Status: $status done!${cc_normal}" @@ -129,7 +138,7 @@ finish(){ } initialize() { - clone_repos + clone_repos $1 checkout_develop create_venv setup_develop @@ -169,6 +178,7 @@ help() { echo "Usage: $0 {init | update | help}" echo echo " init : Initialize repositories, create virtualenv and \`python setup.py develop\` all." + echo " You can use \`init ro\` in order to use the https remotes if you don't have rw access." echo " update : Update the repositories and install new deps (if needed)." echo " run : Runs the client (any extra parameters will be sent to the app)." echo " help : Show this help" @@ -177,7 +187,7 @@ help() { case "$1" in init) - initialize + initialize $2 ;; update) update diff --git a/relnotes.txt b/relnotes.txt index 2e32ca6c..99898aa2 100644 --- a/relnotes.txt +++ b/relnotes.txt @@ -1,8 +1,8 @@ -ANNOUNCING Bitmask, the internet encryption toolkit, release 0.3.5 +ANNOUNCING Bitmask, the internet encryption toolkit, release 0.3.7 The LEAP team is pleased to announce the immediate availability of -version 0.3.5 of Bitmask, the Internet Encryption Toolkit, codename -"I can stand on one foot". +version 0.3.7 of Bitmask, the Internet Encryption Toolkit, codename +"The Big Lebowsky". https://downloads.leap.se/client/ @@ -34,14 +34,9 @@ NOT trust your life to it (yet!). WHAT CAN THIS VERSION OF BITMASK DO FOR ME? -Bitmask 0.3.5 is a bugfix release but it also has improvements to the -UI design, it's still a work in progress since it will change after -testing in this release, but it's progressing nicely. We have again, -better mail support, with detached signatures and compliance with a -couple of RFC. You can refer to the CHANGELOG for the meat. - -We also re-added support for Windows, although it might be a bumpy -ride until we smooth the corners a little bit more. +Bitmask 0.3.7 improves greatly its mail support and stability in +general, among other various bug fixes. You can refer to the CHANGELOG +for the meat. As always, you can connect to the Encrypted Internet Proxy service offered by a provider of your choice, and enjoy a encrypted internet @@ -98,6 +93,6 @@ beyond any border. The LEAP team, -Oct 18, 2013 +Nov 15, 2013 Somewhere in the middle of the intertubes. EOF diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index ab98850d..85b9b003 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -581,6 +581,7 @@ class SRPAuth(QtCore.QObject): else: self.set_session_id(None) self.set_uid(None) + self.set_token(None) # Also reset the session self._session = self._fetcher.session() logger.debug("Successfully logged out.") diff --git a/src/leap/bitmask/crypto/srpregister.py b/src/leap/bitmask/crypto/srpregister.py index 5c0a8efc..02a1ea63 100644 --- a/src/leap/bitmask/crypto/srpregister.py +++ b/src/leap/bitmask/crypto/srpregister.py @@ -108,7 +108,8 @@ class SRPRegister(QtCore.QObject): :rparam: (ok, request) """ - username = username.lower() + username = username.lower().encode('utf-8') + password = password.encode('utf-8') salt, verifier = self._srp.create_salted_verification_key( username, @@ -140,8 +141,7 @@ class SRPRegister(QtCore.QObject): verify=self._provider_config. get_ca_cert_path()) - except (requests.exceptions.SSLError, - requests.exceptions.ConnectionError) as exc: + except requests.exceptions.RequestException as exc: logger.error(exc.message) ok = False else: diff --git a/src/leap/bitmask/gui/advanced_key_management.py b/src/leap/bitmask/gui/advanced_key_management.py new file mode 100644 index 00000000..2c0fa034 --- /dev/null +++ b/src/leap/bitmask/gui/advanced_key_management.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +# advanced_key_management.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Advanced Key Management +""" +import logging + +from PySide import QtGui +from zope.proxy import sameProxiedObjects + +from leap.keymanager import openpgp +from leap.keymanager.errors import KeyAddressMismatch, KeyFingerprintMismatch +from leap.bitmask.services import get_service_display_name, MX_SERVICE +from ui_advanced_key_management import Ui_AdvancedKeyManagement + +logger = logging.getLogger(__name__) + + +class AdvancedKeyManagement(QtGui.QWidget): + """ + Advanced Key Management + """ + def __init__(self, user, keymanager, soledad): + """ + :param user: the current logged in user. + :type user: unicode + :param keymanager: the existing keymanager instance + :type keymanager: KeyManager + :param soledad: a loaded instance of Soledad + :type soledad: Soledad + """ + QtGui.QWidget.__init__(self) + + self.ui = Ui_AdvancedKeyManagement() + self.ui.setupUi(self) + + # if Soledad is not started yet + if sameProxiedObjects(soledad, None): + self.ui.container.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) + + self._keymanager = keymanager + self._soledad = soledad + + self._key = keymanager.get_key(user, openpgp.OpenPGPKey) + self._key_priv = keymanager.get_key( + user, openpgp.OpenPGPKey, private=True) + + # show current key information + self.ui.leUser.setText(user) + self.ui.leKeyID.setText(self._key.key_id) + self.ui.leFingerprint.setText(self._key.fingerprint) + + # set up connections + self.ui.pbImportKeys.clicked.connect(self._import_keys) + self.ui.pbExportKeys.clicked.connect(self._export_keys) + + def _import_keys(self): + """ + Imports the user's key pair. + Those keys need to be ascii armored. + """ + fileName, filtr = QtGui.QFileDialog.getOpenFileName( + self, self.tr("Open keys file"), + options=QtGui.QFileDialog.DontUseNativeDialog) + + if fileName: + new_key = '' + try: + with open(fileName, 'r') as keys_file: + new_key = keys_file.read() + except IOError as e: + logger.error("IOError importing key. {0!r}".format(e)) + QtGui.QMessageBox.critical( + self, self.tr("Input/Output error"), + self.tr("There was an error accessing the file.\n" + "Import canceled.")) + return + + keymanager = self._keymanager + try: + public_key, private_key = keymanager.parse_openpgp_ascii_key( + new_key) + except (KeyAddressMismatch, KeyFingerprintMismatch) as e: + logger.error(repr(e)) + QtGui.QMessageBox.warning( + self, self.tr("Data mismatch"), + self.tr("The public and private key should have the " + "same address and fingerprint.\n" + "Import canceled.")) + return + + if public_key is None or private_key is None: + QtGui.QMessageBox.warning( + self, self.tr("Missing key"), + self.tr("You need to provide the public AND private " + "key in the same file.\n" + "Import canceled.")) + return + + if public_key.address != self._key.address: + logger.error("The key does not match the ID") + QtGui.QMessageBox.warning( + self, self.tr("Address mismatch"), + self.tr("The identity for the key needs to be the same " + "as your user address.\n" + "Import canceled.")) + return + + question = self.tr("Are you sure that you want to replace " + "the current key pair whith the imported?") + res = QtGui.QMessageBox.question( + None, "Change key pair", question, + QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, + QtGui.QMessageBox.No) # default No + + if res == QtGui.QMessageBox.No: + return + + keymanager.delete_key(self._key) + keymanager.delete_key(self._key_priv) + keymanager.put_key(public_key) + keymanager.put_key(private_key) + keymanager.send_key(openpgp.OpenPGPKey) + + logger.debug('Import ok') + + QtGui.QMessageBox.information( + self, self.tr("Import Successful"), + self.tr("The key pair was imported successfully.")) + else: + logger.debug('Import canceled by the user.') + + def _export_keys(self): + """ + Exports the user's key pair. + """ + fileName, filtr = QtGui.QFileDialog.getSaveFileName( + self, self.tr("Save keys file"), + options=QtGui.QFileDialog.DontUseNativeDialog) + + if fileName: + try: + with open(fileName, 'w') as keys_file: + keys_file.write(self._key.key_data) + keys_file.write(self._key_priv.key_data) + + logger.debug('Export ok') + QtGui.QMessageBox.information( + self, self.tr("Export Successful"), + self.tr("The key pair was exported successfully.\n" + "Please, store your private key in a safe place.")) + except IOError as e: + logger.error("IOError exporting key. {0!r}".format(e)) + QtGui.QMessageBox.critical( + self, self.tr("Input/Output error"), + self.tr("There was an error accessing the file.\n" + "Export canceled.")) + return + else: + logger.debug('Export canceled by the user.') diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py index 324586c0..1899d6a4 100644 --- a/src/leap/bitmask/gui/eip_status.py +++ b/src/leap/bitmask/gui/eip_status.py @@ -26,6 +26,7 @@ from PySide import QtCore, QtGui from leap.bitmask.services.eip.connection import EIPConnection from leap.bitmask.services.eip.vpnprocess import VPNManager +from leap.bitmask.services import get_service_display_name, EIP_SERVICE from leap.bitmask.platform_init import IS_LINUX from leap.bitmask.util.averages import RateMovingAverage from leap.common.check import leap_assert_type @@ -58,6 +59,7 @@ class EIPStatusWidget(QtGui.QWidget): # set systray tooltip status self._eip_status = "" + self._service_name = get_service_display_name(EIP_SERVICE) self.ui.eip_bandwidth.hide() @@ -181,21 +183,24 @@ class EIPStatusWidget(QtGui.QWidget): def set_systray(self, systray): """ - Sets the systray object to use. + Sets the systray object to use and adds the service line for EIP. :param systray: Systray object :type systray: QtGui.QSystemTrayIcon """ leap_assert_type(systray, QtGui.QSystemTrayIcon) self._systray = systray - self._systray.setToolTip(self.tr("All services are OFF")) + eip_status = self.tr("{0}: OFF").format(self._service_name) + self._systray.set_service_tooltip(EIP_SERVICE, eip_status) def _update_systray_tooltip(self): """ - Updates the system tray icon tooltip using the eip and mx status. + Updates the system tray tooltip using the eip status. """ - status = self.tr("Encrypted Internet: {0}").format(self._eip_status) - self._systray.setToolTip(status) + if self._systray is not None: + eip_status = u"{0}: {1}".format( + self._service_name, self._eip_status) + self._systray.set_service_tooltip(EIP_SERVICE, eip_status) def set_action_eip_startstop(self, action_eip_startstop): """ @@ -245,7 +250,7 @@ class EIPStatusWidget(QtGui.QWidget): # probably the best thing would be to make a transitional # transition there, but that's more involved. self.eip_button.hide() - msg = self.tr("You must login to use Encrypted Internet") + msg = self.tr("You must login to use {0}".format(self._service_name)) self.eip_label.setText(msg) @QtCore.Slot() @@ -393,10 +398,8 @@ class EIPStatusWidget(QtGui.QWidget): # the UI won't update properly QtCore.QTimer.singleShot( 0, self.eipconnection.qtsigs.do_disconnect_signal) - QtCore.QTimer.singleShot(0, partial(self.set_eip_status, - self.tr("Unable to start VPN, " - "it's already " - "running."))) + msg = self.tr("Unable to start VPN, it's already running.") + QtCore.QTimer.singleShot(0, partial(self.set_eip_status, msg)) else: self.set_eip_status(status) @@ -418,14 +421,15 @@ class EIPStatusWidget(QtGui.QWidget): """ selected_pixmap = self.ERROR_ICON selected_pixmap_tray = self.ERROR_ICON_TRAY - tray_message = self.tr("Encrypted Internet: OFF") + tray_message = self.tr("{0}: OFF".format(self._service_name)) if status in ("WAIT", "AUTH", "GET_CONFIG", "RECONNECTING", "ASSIGN_IP"): selected_pixmap = self.CONNECTING_ICON selected_pixmap_tray = self.CONNECTING_ICON_TRAY - tray_message = self.tr("Encrypted Internet: Starting...") + tray_message = self.tr("{0}: Starting...").format( + self._service_name) elif status in ("CONNECTED"): - tray_message = self.tr("Encrypted Internet: ON") + tray_message = self.tr("{0}: ON".format(self._service_name)) selected_pixmap = self.CONNECTED_ICON selected_pixmap_tray = self.CONNECTED_ICON_TRAY self._eip_status = 'ON' diff --git a/src/leap/bitmask/gui/loggerwindow.py b/src/leap/bitmask/gui/loggerwindow.py index ad2ceded..6ef58558 100644 --- a/src/leap/bitmask/gui/loggerwindow.py +++ b/src/leap/bitmask/gui/loggerwindow.py @@ -19,6 +19,7 @@ History log window """ import logging +import cgi from PySide import QtGui @@ -90,7 +91,7 @@ class LoggerWindow(QtGui.QDialog): logging.CRITICAL: "background: red; color: white; font: bold;" } level = log[LeapLogHandler.RECORD_KEY].levelno - message = log[LeapLogHandler.MESSAGE_KEY] + message = cgi.escape(log[LeapLogHandler.MESSAGE_KEY]) if self._logs_to_display[level]: open_tag = "<tr style='" + html_style[level] + "'>" diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py index c1e82d4d..3c933c9a 100644 --- a/src/leap/bitmask/gui/mail_status.py +++ b/src/leap/bitmask/gui/mail_status.py @@ -22,6 +22,7 @@ import logging from PySide import QtCore, QtGui from leap.bitmask.platform_init import IS_LINUX +from leap.bitmask.services import get_service_display_name, MX_SERVICE from leap.common.check import leap_assert, leap_assert_type from leap.common.events import register from leap.common.events import events_pb2 as proto @@ -58,6 +59,7 @@ class MailStatusWidget(QtGui.QWidget): # set systray tooltip status self._mx_status = "" + self._service_name = get_service_display_name(MX_SERVICE) # Set the Mail status icons self.CONNECTING_ICON = None @@ -150,29 +152,23 @@ class MailStatusWidget(QtGui.QWidget): def set_systray(self, systray): """ - Sets the systray object to use. + Sets the systray object to use and adds the service line for MX. :param systray: Systray object :type systray: QtGui.QSystemTrayIcon """ leap_assert_type(systray, QtGui.QSystemTrayIcon) self._systray = systray - self._systray.setToolTip(self.tr("All services are OFF")) + mx_status = self.tr("{0}: OFF").format(self._service_name) + self._systray.set_service_tooltip(MX_SERVICE, mx_status) def _update_systray_tooltip(self): """ - Updates the system tray icon tooltip using the eip and mx status. + Updates the system tray tooltip using the mx status. """ - # TODO: Figure out how to handle this with the two status in different - # classes - # XXX right now we could connect the state transition signals of the - # two connection machines (EIP/Mail) to a class that keeps track of the - # state -- kali - # status = self.tr("Encrypted Internet: {0}").format(self._eip_status) - # status += '\n' - # status += self.tr("Mail is {0}").format(self._mx_status) - # self._systray.setToolTip(status) - pass + if self._systray is not None: + mx_status = u"{0}: {1}".format(self._service_name, self._mx_status) + self._systray.set_service_tooltip(MX_SERVICE, mx_status) def set_action_mail_status(self, action_mail_status): """ @@ -213,7 +209,8 @@ class MailStatusWidget(QtGui.QWidget): icon = self.ERROR_ICON if ready == 0: self.ui.lblMailStatus.setText( - self.tr("You must be logged in to use encrypted email.")) + self.tr("You must be logged in to use {0}.").format( + self._service_name)) elif ready == 1: icon = self.CONNECTING_ICON self._mx_status = self.tr('Starting..') @@ -296,7 +293,8 @@ class MailStatusWidget(QtGui.QWidget): # elif req.event == proto.KEYMANAGER_KEY_NOT_FOUND: # ext_status = self.tr("Key not found!") elif req.event == proto.KEYMANAGER_STARTED_KEY_GENERATION: - ext_status = self.tr("Generating new key, please wait...") + ext_status = self.tr( + "Generating new key, this may take a few minutes.") elif req.event == proto.KEYMANAGER_FINISHED_KEY_GENERATION: ext_status = self.tr("Finished generating key!") elif req.event == proto.KEYMANAGER_DONE_UPLOADING_KEYS: @@ -434,5 +432,6 @@ class MailStatusWidget(QtGui.QWidget): Displays the correct UI for the disabled state. """ self._disabled = True - self._set_mail_status( - self.tr("You must be logged in to use encrypted email."), -1) + status = self.tr("You must be logged in to use {0}.").format( + self._service_name) + self._set_mail_status(status, -1) diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 5eb9e6dc..b0f25af1 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -29,6 +29,7 @@ from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig 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 from leap.bitmask.gui.preferenceswindow import PreferencesWindow from leap.bitmask.gui.eip_preferenceswindow import EIPPreferencesWindow @@ -36,6 +37,7 @@ from leap.bitmask.gui import statemachines from leap.bitmask.gui.eip_status import EIPStatusWidget from leap.bitmask.gui.mail_status import MailStatusWidget 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 @@ -44,6 +46,7 @@ from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper from leap.bitmask.services.mail import conductor as mail_conductor +from leap.bitmask.services import EIP_SERVICE, MX_SERVICE from leap.bitmask.services.eip import eipconfig from leap.bitmask.services.eip import get_openvpn_management from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper @@ -86,9 +89,6 @@ class MainWindow(QtGui.QMainWindow): LOGIN_INDEX = 0 EIP_STATUS_INDEX = 1 - OPENVPN_SERVICE = "openvpn" - MX_SERVICE = "mx" - # Signals eip_needs_login = QtCore.Signal([]) new_updates = QtCore.Signal(object) @@ -253,6 +253,9 @@ class MainWindow(QtGui.QMainWindow): self.ui.action_create_new_account.triggered.connect( self._launch_wizard) + self.ui.action_advanced_key_management.triggered.connect( + self._show_AKM) + if IS_MAC: self.ui.menuFile.menuAction().setText(self.tr("File")) @@ -334,7 +337,7 @@ class MainWindow(QtGui.QMainWindow): self.eip_machine = None # start event machines self.start_eip_machine() - self._mail_conductor.start_mail_machine(parent=self) + self._mail_conductor.start_mail_machine() if self._first_run(): self._wizard_firstrun = True @@ -438,6 +441,20 @@ class MainWindow(QtGui.QMainWindow): else: self._logger_window.setVisible(not self._logger_window.isVisible()) + def _show_AKM(self): + """ + SLOT + TRIGGERS: + self.ui.action_advanced_key_management.triggered + + Displays the Advanced Key Management dialog. + """ + domain = self._login_widget.get_selected_provider() + logged_user = "{0}@{1}".format(self._logged_user, domain) + self._akm = AdvancedKeyManagement( + logged_user, self._keymanager, self._soledad) + self._akm.show() + def _show_preferences(self): """ SLOT @@ -447,11 +464,10 @@ class MainWindow(QtGui.QMainWindow): Displays the preferences window. """ preferences_window = PreferencesWindow(self, self._srp_auth, - self._provider_config) - - self.soledad_ready.connect( - lambda: preferences_window.set_soledad_ready(self._soledad)) + self._provider_config, + self._soledad) + self.soledad_ready.connect(preferences_window.set_soledad_ready) preferences_window.show() def _show_eip_preferences(self): @@ -597,8 +613,8 @@ class MainWindow(QtGui.QMainWindow): for service in provider_config.get_services(): services.add(service) - self.ui.eipWidget.setVisible(self.OPENVPN_SERVICE in services) - self.ui.mailWidget.setVisible(self.MX_SERVICE in services) + self.ui.eipWidget.setVisible(EIP_SERVICE in services) + self.ui.mailWidget.setVisible(MX_SERVICE in services) # # systray @@ -628,12 +644,13 @@ class MainWindow(QtGui.QMainWindow): systrayMenu.addAction(self._action_mail_status) systrayMenu.addSeparator() systrayMenu.addAction(self.ui.action_quit) - self._systray = QtGui.QSystemTrayIcon(self) + self._systray = SysTray(self) self._systray.setContextMenu(systrayMenu) self._systray.setIcon(self._eip_status.ERROR_ICON_TRAY) self._systray.setVisible(True) self._systray.activated.connect(self._tray_activated) + self._mail_status.set_systray(self._systray) self._eip_status.set_systray(self._systray) def _tray_activated(self, reason=None): @@ -674,10 +691,9 @@ class MainWindow(QtGui.QMainWindow): Toggles the window visibility """ visible = self.isVisible() and self.isActiveWindow() - qApp = QtCore.QCoreApplication.instance() if not visible: - qApp.setQuitOnLastWindowClosed(True) + QtGui.QApplication.setQuitOnLastWindowClosed(True) self.show() self.activateWindow() self.raise_() @@ -685,7 +701,7 @@ class MainWindow(QtGui.QMainWindow): # We set this in order to avoid dialogs shutting down the # app on close, as they will be the only visible window. # e.g.: PreferencesWindow, LoggerWindow - qApp.setQuitOnLastWindowClosed(False) + QtGui.QApplication.setQuitOnLastWindowClosed(False) self.hide() # Wait a bit until the window visibility has changed so @@ -905,7 +921,6 @@ 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 @@ -914,19 +929,19 @@ class MainWindow(QtGui.QMainWindow): if ok: self._logged_user = self._login_widget.get_user() - # We leave a bit of room for the user to see the - # "Succeeded" message and then we switch to the EIP status - # panel - QtCore.QTimer.singleShot(1000, self._switch_to_status) + 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) - def _switch_to_status(self): - # TODO this method name is confusing as hell. + def _start_eip_bootstrap(self): """ Changes the stackedWidget index to the EIP status one and - triggers the eip bootstrapping + triggers the eip bootstrapping. """ self._login_widget.logged_in() @@ -938,7 +953,7 @@ class MainWindow(QtGui.QMainWindow): # 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(self.MX_SERVICE) > 0: + self._enabled_services.count(MX_SERVICE) > 0: self._mail_status.about_to_start() self._soledad_bootstrapper.run_soledad_setup_checks( @@ -949,6 +964,8 @@ class MainWindow(QtGui.QMainWindow): else: self._mail_status.set_disabled() + # XXX the config should be downloaded from the start_eip + # method. self._download_eip_config() ################################################################### @@ -1037,7 +1054,7 @@ class MainWindow(QtGui.QMainWindow): # 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(self.MX_SERVICE) > 0: + self._enabled_services.count(MX_SERVICE) > 0: self._mail_conductor.smtp_bootstrapper.run_smtp_setup_checks( self._provider_config, self._mail_conductor.smtp_config, @@ -1066,7 +1083,7 @@ class MainWindow(QtGui.QMainWindow): self.soledad_ready """ if self._provider_config.provides_mx() and \ - self._enabled_services.count(self.MX_SERVICE) > 0: + self._enabled_services.count(MX_SERVICE) > 0: self._mail_conductor.start_imap_service() def _on_mail_client_logged_in(self, req): @@ -1416,7 +1433,7 @@ class MainWindow(QtGui.QMainWindow): provider_config = self._get_best_provider_config() if provider_config.provides_eip() and \ - self._enabled_services.count(self.OPENVPN_SERVICE) > 0 and \ + self._enabled_services.count(EIP_SERVICE) > 0 and \ not self._already_started_eip: # XXX this should be handled by the state machine. @@ -1427,7 +1444,7 @@ class MainWindow(QtGui.QMainWindow): download_if_needed=True) self._already_started_eip = True elif not self._already_started_eip: - if self._enabled_services.count(self.OPENVPN_SERVICE) > 0: + if self._enabled_services.count(EIP_SERVICE) > 0: self._eip_status.set_eip_status( self.tr("Not supported"), error=True) @@ -1522,6 +1539,7 @@ class MainWindow(QtGui.QMainWindow): """ self._soledad_bootstrapper.cancel_bootstrap() + setProxiedObject(self._soledad, None) # XXX: If other defers are doing authenticated stuff, this # might conflict with those. CHECK! @@ -1657,8 +1675,7 @@ class MainWindow(QtGui.QMainWindow): # UI stuff. # Set this in case that the app is hidden - qApp = QtCore.QCoreApplication.instance() - qApp.setQuitOnLastWindowClosed(True) + QtGui.QApplication.setQuitOnLastWindowClosed(True) self._cleanup_and_quit() diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index acb39b07..8e9ef95a 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -22,7 +22,9 @@ import os import logging from functools import partial + from PySide import QtCore, QtGui +from zope.proxy import sameProxiedObjects from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.gui.ui_preferences import Ui_Preferences @@ -31,7 +33,7 @@ from leap.bitmask.crypto.srpauth import SRPAuthBadUserOrPassword from leap.bitmask.util.password import basic_password_checks from leap.bitmask.services import get_supported from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.services import get_service_display_name +from leap.bitmask.services import get_service_display_name, MX_SERVICE logger = logging.getLogger(__name__) @@ -40,7 +42,7 @@ class PreferencesWindow(QtGui.QDialog): """ Window that displays the preferences. """ - def __init__(self, parent, srp_auth, provider_config): + def __init__(self, parent, srp_auth, provider_config, soledad): """ :param parent: parent object of the PreferencesWindow. :parent type: QWidget @@ -48,13 +50,15 @@ class PreferencesWindow(QtGui.QDialog): :type srp_auth: SRPAuth :param provider_config: ProviderConfig object. :type provider_config: ProviderConfig + :param soledad: Soledad instance + :type soledad: Soledad """ QtGui.QDialog.__init__(self, parent) self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") self._srp_auth = srp_auth self._settings = LeapSettings() - self._soledad = None + self._soledad = soledad # Load UI self.ui = Ui_Preferences() @@ -81,20 +85,25 @@ class PreferencesWindow(QtGui.QDialog): # check if provider has 'mx' ... domain = provider_config.get_domain() self._select_provider_by_name(domain) + if provider_config.provides_mx(): enabled_services = self._settings.get_enabled_services(domain) - mx_name = get_service_display_name('mx') + mx_name = get_service_display_name(MX_SERVICE) # ... and if the user have it enabled - if 'mx' not in enabled_services: + if MX_SERVICE not in enabled_services: msg = self.tr("You need to enable {0} in order to change " "the password.".format(mx_name)) self._set_password_change_status(msg, error=True) else: - msg = self.tr( - "You need to wait until {0} is ready in " - "order to change the password.".format(mx_name)) - self._set_password_change_status(msg) + if sameProxiedObjects(self._soledad, None): + msg = self.tr( + "You need to wait until {0} is ready in " + "order to change the password.".format(mx_name)) + self._set_password_change_status(msg) + else: + # Soledad is bootstrapped + pw_enabled = True else: pw_enabled = True else: @@ -104,18 +113,14 @@ class PreferencesWindow(QtGui.QDialog): self.ui.gbPasswordChange.setEnabled(pw_enabled) - def set_soledad_ready(self, soledad): + def set_soledad_ready(self): """ SLOT TRIGGERS: parent.soledad_ready - It sets the soledad object as ready to use. - - :param soledad: Soledad object configured in the main app. - :type soledad: Soledad + It notifies when the soledad object as ready to use. """ - self._soledad = soledad self.ui.lblPasswordChangeStatus.setVisible(False) self.ui.gbPasswordChange.setEnabled(True) diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py index 386cb75f..93731ce0 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -357,7 +357,7 @@ class ConnectionMachineBuilder(object): parent = kwargs.get('parent', None) # 1. create machine - machine = CompositeMachine(parent=parent) + machine = CompositeMachine() # 2. create states off = States.Off(conn.qtsigs.disconnected_signal, diff --git a/src/leap/bitmask/gui/systray.py b/src/leap/bitmask/gui/systray.py new file mode 100644 index 00000000..d30b5f32 --- /dev/null +++ b/src/leap/bitmask/gui/systray.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# systray.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Custom system tray manager. +""" + +from PySide import QtGui + +from leap.common.check import leap_assert_type + + +class SysTray(QtGui.QSystemTrayIcon): + """ + Custom system tray that allows us to use a more 'intelligent' tooltip. + """ + + def __init__(self, parent=None): + QtGui.QSystemTrayIcon.__init__(self, parent) + self._services = {} + + def set_service_tooltip(self, service, tooltip): + """ + Sets the service tooltip. + + :param service: service name identifier + :type service: unicode + :param tooltip: tooltip to display for that service + :type tooltip: unicode + """ + leap_assert_type(service, unicode) + leap_assert_type(tooltip, unicode) + + self._services[service] = tooltip + tooltip = "\n".join(self._services.values()) + self.setToolTip(tooltip) diff --git a/src/leap/bitmask/gui/ui/advanced_key_management.ui b/src/leap/bitmask/gui/ui/advanced_key_management.ui new file mode 100644 index 00000000..d61aa87e --- /dev/null +++ b/src/leap/bitmask/gui/ui/advanced_key_management.ui @@ -0,0 +1,153 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AdvancedKeyManagement</class> + <widget class="QWidget" name="AdvancedKeyManagement"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>431</width> + <height>188</height> + </rect> + </property> + <property name="windowTitle"> + <string>Advanced Key Management</string> + </property> + <property name="windowIcon"> + <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"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>User:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="leUser"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>user_name@provider</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Key ID:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="leKeyID"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>key ID</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Key fingerprint:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="leFingerprint"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>fingerprint</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="readOnly"> + <bool>true</bool> + </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"> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="1"> + <widget class="QPushButton" name="pbExportKeys"> + <property name="text"> + <string>Export current key pair</string> + </property> + </widget> + </item> + <item row="1" column="0" rowspan="2"> + <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="2" column="1"> + <widget class="QPushButton" name="pbImportKeys"> + <property name="text"> + <string>Import custom key pair</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="lblStatus"> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + <resources> + <include location="../../../../../data/resources/mainwindow.qrc"/> + </resources> + <connections/> +</ui> diff --git a/src/leap/bitmask/gui/ui/mainwindow.ui b/src/leap/bitmask/gui/ui/mainwindow.ui index badd291d..3b83788e 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>636</height> + <height>651</height> </rect> </property> <layout class="QVBoxLayout" name="verticalLayout"> @@ -380,7 +380,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>22</height> + <height>21</height> </rect> </property> <widget class="QMenu" name="menuFile"> @@ -388,6 +388,7 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb <string>&Bitmask</string> </property> <addaction name="action_create_new_account"/> + <addaction name="action_advanced_key_management"/> <addaction name="separator"/> <addaction name="action_quit"/> </widget> @@ -439,6 +440,14 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb <string>Create a new account...</string> </property> </action> + <action name="action_advanced_key_management"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Advanced Key Management</string> + </property> + </action> </widget> <resources> <include location="../../../../../data/resources/mainwindow.qrc"/> diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index 6ba65410..5f5224ae 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -111,8 +111,9 @@ class Wizard(QtGui.QWizard): self.currentIdChanged.connect(self._current_id_changed) - self.ui.lnProvider.textChanged.connect( - self._enable_check) + self.ui.lnProvider.textChanged.connect(self._enable_check) + self.ui.rbNewProvider.toggled.connect( + lambda x: self._enable_check()) self.ui.lblUser.returnPressed.connect( self._focus_password) @@ -146,6 +147,26 @@ class Wizard(QtGui.QWizard): self._load_configured_providers() + self._provider_checks_ok = False + self._provider_setup_ok = False + self.finished.connect(self._wizard_finished) + + @QtCore.Slot() + def _wizard_finished(self): + """ + SLOT + TRIGGER: + self.finished + + This method is called when the wizard is accepted or rejected. + Here we do the cleanup needed to use the wizard again reusing the + instance. + """ + self._provider_checks_ok = False + self._provider_setup_ok = False + self.ui.lnProvider.setText('') + self.ui.grpCheckProvider.setVisible(False) + def _load_configured_providers(self): """ Loads the configured providers into the wizard providers combo box. @@ -193,9 +214,22 @@ class Wizard(QtGui.QWizard): def get_services(self): return self._selected_services - def _enable_check(self, text): - self.ui.btnCheck.setEnabled(len(self.ui.lnProvider.text()) != 0) - self._reset_provider_check() + @QtCore.Slot() + def _enable_check(self, reset=True): + """ + SLOT + TRIGGER: + self.ui.lnProvider.textChanged + + Enables/disables the 'check' button in the SELECT_PROVIDER_PAGE + depending on the lnProvider content. + """ + enabled = len(self.ui.lnProvider.text()) != 0 + enabled = enabled and self.ui.rbNewProvider.isChecked() + self.ui.btnCheck.setEnabled(enabled) + + if reset: + self._reset_provider_check() def _focus_password(self): """ @@ -226,9 +260,7 @@ class Wizard(QtGui.QWizard): self._registration_finished) threads.deferToThread( - partial(register.register_user, - username.encode("utf8"), - password.encode("utf8"))) + partial(register.register_user, username, password)) self._username = username self._password = password @@ -282,7 +314,8 @@ class Wizard(QtGui.QWizard): old_username = self._username self._username = None self._password = None - error_msg = self.tr("Unknown error") + error_msg = self.tr("Something has gone wrong. " + "Please try again.") try: content, _ = get_content(req) json_content = json.loads(content) @@ -339,6 +372,12 @@ class Wizard(QtGui.QWizard): if len(self.ui.lnProvider.text()) == 0: return + self._provider_checks_ok = False + + # just in case that the user has already setup a provider and + # go 'back' to check a provider + self._provider_setup_ok = False + self.ui.grpCheckProvider.setVisible(True) self.ui.btnCheck.setEnabled(False) self.ui.lnProvider.setEnabled(False) @@ -448,6 +487,7 @@ class Wizard(QtGui.QWizard): "provider.json")): 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, @@ -499,6 +539,7 @@ class Wizard(QtGui.QWizard): """ self._complete_task(data, self.ui.lblCheckApiCert, True, self.SETUP_PROVIDER_PAGE) + self._provider_setup_ok = True def _service_selection_changed(self, service, state): """ @@ -556,18 +597,22 @@ class Wizard(QtGui.QWizard): Prepares the pages when they appear """ if pageId == self.SELECT_PROVIDER_PAGE: - self._reset_provider_check() - self._enable_check("") + skip = self.ui.rbExistingProvider.isChecked() + if not self._provider_checks_ok: + self._enable_check() + self._skip_provider_checks(skip) + else: + self._enable_check(reset=False) if pageId == self.SETUP_PROVIDER_PAGE: - self._reset_provider_setup() - self.page(pageId).setSubTitle(self.tr("Gathering configuration " - "options for %s") % - (self._provider_config - .get_name(),)) - self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON) - self._provider_setup_defer = self._provider_bootstrapper.\ - run_provider_setup_checks(self._provider_config) + if not self._provider_setup_ok: + self._reset_provider_setup() + sub_title = self.tr("Gathering configuration options for {0}") + 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) if pageId == self.PRESENT_PROVIDER_PAGE: self.page(pageId).setSubTitle(self.tr("Description of services " diff --git a/src/leap/bitmask/services/__init__.py b/src/leap/bitmask/services/__init__.py index e62277b6..ba12ba4e 100644 --- a/src/leap/bitmask/services/__init__.py +++ b/src/leap/bitmask/services/__init__.py @@ -37,7 +37,9 @@ from leap.common.files import get_mtime logger = logging.getLogger(__name__) -DEPLOYED = ["openvpn", "mx"] +EIP_SERVICE = u"openvpn" +MX_SERVICE = u"mx" +DEPLOYED = [EIP_SERVICE, MX_SERVICE] def get_service_display_name(service): diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py index c294381b..addf9bef 100644 --- a/src/leap/bitmask/services/mail/conductor.py +++ b/src/leap/bitmask/services/mail/conductor.py @@ -81,16 +81,12 @@ class IMAPControl(object): "We need a non-null keymanager for initializing imap " "service") - if self.imap_service is None: - # first time. - self.imap_service, \ - self.imap_port, \ + self.imap_service, self.imap_port, \ self.imap_factory = imap.start_imap_service( self._soledad, - self._keymanager) - else: - # we have the fetcher. just start it. - self.imap_service.start_loop() + self._keymanager, + userid=self.userid) + self.imap_service.start_loop() def stop_imap_service(self): """ @@ -102,6 +98,7 @@ class IMAPControl(object): logger.debug('Stopping imap service.') # Stop the loop call in the fetcher self.imap_service.stop() + self.imap_service = None # Stop listening on the IMAP port self.imap_port.stopListening() # Stop the protocol @@ -111,7 +108,6 @@ class IMAPControl(object): """ Fetches incoming mail. """ - # TODO have a mutex over fetch operation. if self.imap_service: logger.debug('Client connected, fetching mail...') self.imap_service.fetch() @@ -201,9 +197,10 @@ class SMTPControl(object): # TODO remove hard-coded port and let leap.mail set # the specific default. self.smtp_connection.qtsigs.connecting_signal.emit() - from leap.mail.smtp import setup_smtp_relay - self._smtp_service, self._smtp_port = setup_smtp_relay( + from leap.mail.smtp import setup_smtp_gateway + self._smtp_service, self._smtp_port = setup_smtp_gateway( port=2013, + userid=self.userid, keymanager=self._keymanager, smtp_host=host, smtp_port=port, @@ -339,11 +336,25 @@ class MailConductor(IMAPControl, SMTPControl): SMTPControl.__init__(self) self._soledad = soledad self._keymanager = keymanager - self._mail_machine = None - self._mail_connection = mail_connection.MailConnection() + self.userid = None + + @property + def userid(self): + return self._userid + + @userid.setter + def userid(self, userid): + """ + Sets the user id this conductor is configured for. + + :param userid: the user id, in the form "user@provider" + :type userid: str + """ + self._userid = userid + def start_mail_machine(self, **kwargs): """ Starts mail machine. @@ -354,15 +365,10 @@ class MailConductor(IMAPControl, SMTPControl): # we have instantiated the connections while building the composite # machines, and we have to use the qtsigs instantiated there. - # XXX we could probably use a proxy here too to make the thing - # transparent. self.set_imap_connection(imap.conn) self.set_smtp_connection(smtp.conn) self._mail_machine = mail - # XXX ------------------- - # need to keep a reference? - #self._mail_events = mail.events self._mail_machine.start() self._imap_machine = imap diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index 54ef67eb..e8c7e9ce 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -28,6 +28,8 @@ from PySide import QtCore from u1db import errors as u1db_errors from zope.proxy import sameProxiedObjects +from twisted.internet.threads import deferToThread + from leap.bitmask.config import flags from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.crypto.srpauth import SRPAuth @@ -194,16 +196,17 @@ class SoledadBootstrapper(AbstractBootstrapper): leap_assert(not sameProxiedObjects(self._soledad, None), "Null soledad, error while initializing") + self.deferred = deferToThread(self._do_soledad_sync) + def _do_soledad_sync(self): + """ + Does several retries to get an initial soledad sync. + """ # and now, let's sync sync_tries = self.MAX_SYNC_RETRIES while sync_tries > 0: try: self._try_soledad_sync() - - # at this point, sometimes the client - # gets stuck and does not progress to - # the _gen_key step. XXX investigate. logger.debug("Soledad has been synced.") # so long, and thanks for all the fish return @@ -259,26 +262,32 @@ class SoledadBootstrapper(AbstractBootstrapper): except socket.timeout: logger.debug("SOLEDAD initialization TIMED OUT...") self.soledad_timeout.emit() + raise except socket.error as exc: logger.warning("Socket error while initializing soledad") self.soledad_timeout.emit() + raise except BootstrapSequenceError as exc: logger.warning("Error while initializing soledad") self.soledad_timeout.emit() + raise # unrecoverable except u1db_errors.Unauthorized: logger.error("Error while initializing soledad " "(unauthorized).") self.soledad_failed.emit() + raise except u1db_errors.HTTPError as exc: logger.exception("Error whie initializing soledad " "(HTTPError)") self.soledad_failed.emit() + raise except Exception as exc: logger.exception("Unhandled error while initializating " "soledad: %r" % (exc,)) self.soledad_failed.emit() + raise def _try_soledad_sync(self): """ @@ -292,9 +301,8 @@ class SoledadBootstrapper(AbstractBootstrapper): logger.error("%r" % (exc,)) raise SoledadSyncError("Failed to sync soledad") except Exception as exc: - logger.exception("Unhandled error while syncing" + logger.exception("Unhandled error while syncing " "soledad: %r" % (exc,)) - self.soledad_failed.emit() raise SoledadSyncError("Failed to sync soledad") def _download_config(self): @@ -356,17 +364,32 @@ class SoledadBootstrapper(AbstractBootstrapper): """ srp_auth = self.srpauth logger.debug('initializing keymanager...') - self._keymanager = KeyManager( - address, - "https://nicknym.%s:6425" % (self._provider_config.get_domain(),), - self._soledad, - #token=srp_auth.get_token(), # TODO: enable token usage - session_id=srp_auth.get_session_id(), - ca_cert_path=self._provider_config.get_ca_cert_path(), - api_uri=self._provider_config.get_api_uri(), - api_version=self._provider_config.get_api_version(), - uid=srp_auth.get_uid(), - gpgbinary=self._get_gpg_bin_path()) + try: + self._keymanager = KeyManager( + address, + "https://nicknym.%s:6425" % ( + self._provider_config.get_domain(),), + self._soledad, + #token=srp_auth.get_token(), # TODO: enable token usage + session_id=srp_auth.get_session_id(), + ca_cert_path=self._provider_config.get_ca_cert_path(), + api_uri=self._provider_config.get_api_uri(), + api_version=self._provider_config.get_api_version(), + uid=srp_auth.get_uid(), + gpgbinary=self._get_gpg_bin_path()) + except Exception as exc: + logger.exception(exc) + raise + + logger.debug('sending key to server...') + + # make sure key is in server + try: + self._keymanager.send_key(openpgp.OpenPGPKey) + except Exception as exc: + logger.error("Error sending key to server.") + logger.exception(exc) + # but we do not raise def _gen_key(self, _): """ @@ -393,7 +416,7 @@ class SoledadBootstrapper(AbstractBootstrapper): try: self._keymanager.gen_key(openpgp.OpenPGPKey) except Exception as exc: - logger.error("error while generating key!") + logger.error("Error while generating key!") logger.exception(exc) raise @@ -401,7 +424,7 @@ class SoledadBootstrapper(AbstractBootstrapper): try: self._keymanager.send_key(openpgp.OpenPGPKey) except Exception as exc: - logger.error("error while sending key!") + logger.error("Error while sending key!") logger.exception(exc) raise |