diff options
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 | 
