diff options
49 files changed, 2918 insertions, 1482 deletions
| diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b429595b..4faceb98 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,40 @@ History  2014  ==== +0.5.2 June 6 -- the "are we there yet" release: ++++++++++++++++++++++++++++++++++++++++++++++++ + +- Unblock local multicast IPs from linux firewall, to allow SSDP and +  Bonjour/mDNS to work. +- Add support for gnome-shell polkit agent. Closes #4144, #4218. +- Update username regex to support the same as webapp. Closes #5965. +- Wrong error message for username too short. Fixes #5697. +- Cleanup and refactor username/password validators. +- Fix EIP autostart failing. Closes #5721. +- Block ipv6 traffic for the moment. Closes #5693 +- Fix bug with ipv6 blocking that caused block to not get removed from +  firewall when Bitmask quit. +- Bring firewall down when switching EIP off. Closes #5687 +- Add OPENVPN_BIN_PATH for OSX so that EIP starts properly. +- Allow usernames to end in a digit. +- Improve signal handling in the mainwindow and wizard. +- Enable UI when OpenVPN bin is not found, plus check before starting +  EIP. Fixes #5619. +- Properly set the userid for SMTP. +- Update EIP UI if it fails to download the config. +- Make use of cmdline in psutil backwards-compatible. Closes #5689 +- Add versioning support to bitmask-root. +- Show flag of country for eip exit node, if available. Related #1232 +- Fix nameserver restoring. Closes #5692 +- Warn user if resolvconf cannot be found. +- Refactor Keymanager to backend. Closes #5711. +- Cleanup backend from hacks. Closes #5698. +- Improve wait and quit process. +- Move soledad password change to backend. +- Move Mail logic to backend. +- Separate imap/smtp logic from conductor. +- Refactor SoledadBootstrapper to backend. Closes #5481. +  0.5.1 May 16 -- the "lil less leaky" release:  +++++++++++++++++++++++++++++++++++++++++++++ @@ -22,7 +22,7 @@ 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 advanced_key_management.ui  #Qt resource files to compile -RESOURCES = locale.qrc loggerwindow.qrc mainwindow.qrc icons.qrc +RESOURCES = locale.qrc loggerwindow.qrc mainwindow.qrc icons.qrc eipstatus.qrc  #pyuic4 and pyrcc4 binaries  PYUIC = pyside-uic diff --git a/data/images/countries/nl.png b/data/images/countries/nl.pngBinary files differ new file mode 100644 index 00000000..fe44791e --- /dev/null +++ b/data/images/countries/nl.png diff --git a/data/images/countries/tr.png b/data/images/countries/tr.pngBinary files differ new file mode 100644 index 00000000..be32f77e --- /dev/null +++ b/data/images/countries/tr.png diff --git a/data/images/countries/us.png b/data/images/countries/us.pngBinary files differ new file mode 100644 index 00000000..10f451fe --- /dev/null +++ b/data/images/countries/us.png diff --git a/data/resources/eipstatus.qrc b/data/resources/eipstatus.qrc new file mode 100644 index 00000000..5d0f2924 --- /dev/null +++ b/data/resources/eipstatus.qrc @@ -0,0 +1,7 @@ +<RCC> +  <qresource prefix="/"> +    <file>../images/countries/nl.png</file> +    <file>../images/countries/tr.png</file> +    <file>../images/countries/us.png</file> +  </qresource> +</RCC> diff --git a/docs/man/bitmask-root.1.rst b/docs/man/bitmask-root.1.rst new file mode 100644 index 00000000..c18cc4d6 --- /dev/null +++ b/docs/man/bitmask-root.1.rst @@ -0,0 +1,61 @@ +============ +bitmask-root +============ + +------------------------------------------------------------------------ +privileged helper for bitmask, the encrypted internet access toolkit. +------------------------------------------------------------------------ + +:Author: LEAP Encryption Access Project https://leap.se +:Date:   2014-06-05 +:Copyright: GPLv3+ +:Version: 0.5.2 +:Manual section: 1 +:Manual group: General Commands Manual + +SYNOPSIS +======== + +bitmask-root [openvpn | firewall | version] [start | stop | isup] [ARGS] + +DESCRIPTION +=========== + +*bitmask-root* is a privileged helper for bitmask. + +It is used to start or stop openvpn and the bitmask firewall. To operate, it +needs to be executed with root privileges. + + +OPTIONS +======= + +openvpn +-------- + +**start** [ARGS]       Starts openvpn. All args are passed to openvpn, and +                       filtered against a list of allowed args. If the next +                       argument is `restart`, the firewall will not be teared +                       down in the case of errors lauching openvpn. + +**stop**               Stops openvpn. + + +firewall +--------- + +**start** [GATEWAYS]   Starts the firewall. GATEWAYS is a list of EIP +                       gateways to allow in the firewall. + +**stop**               Stops the firewall. + +version +-------- + +**version**             Prints the `bitmask-root` version string. + + +BUGS +==== + +Please report any bugs to https://leap.se/code diff --git a/docs/man/bitmask.1.rst b/docs/man/bitmask.1.rst index ed4f7133..6eae7ff5 100644 --- a/docs/man/bitmask.1.rst +++ b/docs/man/bitmask.1.rst @@ -7,9 +7,9 @@ graphical client to control LEAP, the encrypted internet access toolkit.  ------------------------------------------------------------------------  :Author: LEAP Encryption Access Project https://leap.se -:Date:   2013-08-23 +:Date:   2014-06-05  :Copyright: GPLv3+ -:Version: 0.3.1 +:Version: 0.5.2  :Manual section: 1  :Manual group: General Commands Manual @@ -80,26 +80,20 @@ WARNING  This software is still in its early phases of testing. So don't trust your life to it!  -At the current time, Bitmask is not compatible with ``openresolv``, but it works with ``resolvconf``.  FILES  ===== -/etc/leap/resolv-update ------------------------ -Post up/down script passed to openvpn. It writes /etc/resolv.conf to avoid dns leaks, and restores the original resolv.conf on exit. -/etc/leap/resolv-head ---------------------- -/etc/leap/resolv-tail ---------------------- +/usr/share/polkit-1/actions/se.leap.bitmask.policy +------------------------------------------------------- -Custom entries that will appear in the written resolv.conf +PolicyKit policy file, used for granting access to bitmask-root without the need of entering a password each time. -/usr/share/polkit-1/actions/net.openvpn.gui.leap.policy -------------------------------------------------------- +/usr/sbin/bitmask-root +------------------------ -PolicyKit policy file, used for granting access to openvpn without the need of entering a password each time. +Helper to launch and stop openvpn and the bitmask firewall.  ~/.config/leap/  --------------- diff --git a/docs/release_checklist.wiki b/docs/release_checklist.wiki index fc99fdf0..075591a7 100644 --- a/docs/release_checklist.wiki +++ b/docs/release_checklist.wiki @@ -1,5 +1,6 @@  = Bitmask Release Checklist (*) =    * [ ] Check that all tests are passing! +  * [ ] Check that the version in bitmask_client/pkg/linux/bitmask-root is bumped if needed.    * [ ] Tag everything      * Should be done for the following packages, in order:        * [ ] 1. leap.common diff --git a/pkg/linux/bitmask-root b/pkg/linux/bitmask-root index 136fd6a4..1929b51b 100755 --- a/pkg/linux/bitmask-root +++ b/pkg/linux/bitmask-root @@ -22,14 +22,15 @@ It should only be called by the Bitmask application.  USAGE:    bitmask-root firewall stop -  bitmask-root firewall start GATEWAY1 GATEWAY2 ... +  bitmask-root firewall start [restart] GATEWAY1 GATEWAY2 ...    bitmask-root openvpn stop    bitmask-root openvpn start CONFIG1 CONFIG1 ...  All actions return exit code 0 for success, non-zero otherwise.  The `openvpn start` action is special: it calls exec on openvpn and replaces -the current process. +the current process. If the `restart` parameter is passed, the firewall will +not be teared down in the case of an error during launch.  """  # TODO should be tested with python3, which can be the default on some distro.  from __future__ import print_function @@ -38,18 +39,19 @@ import os  import re  import signal  import socket +import syslog  import subprocess  import sys  import time  import traceback -  cmdcheck = subprocess.check_output  ##  ## CONSTANTS  ## +VERSION = "1"  SCRIPT = "bitmask-root"  NAMESERVER = "10.42.0.1"  BITMASK_CHAIN = "bitmask" @@ -129,6 +131,8 @@ if DEBUG:      logger.setLevel(logging.DEBUG)      logger.addHandler(ch) +syslog.openlog(SCRIPT) +  ##  ## UTILITY  ## @@ -413,6 +417,7 @@ def bail(msg=None, exception=None):      """      if msg is not None:          print("%s: %s" % (SCRIPT, msg)) +        syslog.syslog(syslog.LOG_ERR, msg)      if exception is not None:          traceback.print_exc()      exit(1) @@ -566,7 +571,7 @@ class NameserverRestorer(Daemon):      A daemon that will restore the previous nameservers.      """ -    def run(self): +    def run(self, *args):          """          Run when daemonized.          """ @@ -614,7 +619,7 @@ def get_default_device():      """      routes = subprocess.check_output([IP, "route", "show"])      match = re.search("^default .*dev ([^\s]*) .*$", routes, flags=re.M) -    if match.groups(): +    if match and match.groups():          return match.group(1)      else:          bail("Could not find default device") @@ -629,7 +634,7 @@ def get_local_network_ipv4(device):      """      addresses = cmdcheck([IP, "-o", "address", "show", "dev", device])      match = re.search("^.*inet ([^ ]*) .*$", addresses, flags=re.M) -    if match.groups(): +    if match and match.groups():          return match.group(1)      else:          return None @@ -644,7 +649,7 @@ def get_local_network_ipv6(device):      """      addresses = cmdcheck([IP, "-o", "address", "show", "dev", device])      match = re.search("^.*inet6 ([^ ]*) .*$", addresses, flags=re.M) -    if match.groups(): +    if match and match.groups():          return match.group(1)      else:          return None @@ -653,6 +658,7 @@ def get_local_network_ipv6(device):  def run_iptable_with_check(cmd, *args, **options):      """      Run an iptables command checking to see if it should: +      for --append: run only if rule does not already exist.        for --insert: run only if rule does not already exist.        for --delete: run only if rule does exist.      other commands are run normally. @@ -662,6 +668,11 @@ def run_iptable_with_check(cmd, *args, **options):          check_code = run(cmd, *check_args, exitcode=True)          if check_code != 0:              run(cmd, *args, **options) +    elif "--append" in args: +        check_args = [arg.replace("--append", "--check") for arg in args] +        check_code = run(cmd, *check_args, exitcode=True) +        if check_code != 0: +            run(cmd, *args, **options)      elif "--delete" in args:          check_args = [arg.replace("--delete", "--check") for arg in args]          check_code = run(cmd, *check_args, exitcode=True) @@ -729,41 +740,74 @@ def firewall_start(args):      local_network_ipv6 = get_local_network_ipv6(default_device)      gateways = get_gateways(args) -    # add custom chain "bitmask" +    # add custom chain "bitmask" to front of OUTPUT chain      if not ipv4_chain_exists(BITMASK_CHAIN):          ip4tables("--new-chain", BITMASK_CHAIN)      if not ipv6_chain_exists(BITMASK_CHAIN):          ip6tables("--new-chain", BITMASK_CHAIN)      iptables("--insert", "OUTPUT", "--jump", BITMASK_CHAIN) -    # reject everything -    iptables("--insert", BITMASK_CHAIN, "-o", default_device, -             "--jump", "REJECT") +    # allow DNS over VPN +    for allowed_dns in [NAMESERVER, "127.0.0.1", "127.0.1.1"]: +        ip4tables("--append", BITMASK_CHAIN, "--protocol", "udp", +                  "--dport", "53", "--destination", allowed_dns, +                  "--jump", "ACCEPT") -    # allow traffic to gateways -    for gateway in gateways: -        ip4tables("--insert", BITMASK_CHAIN, "--destination", gateway, -                  "-o", default_device, "--jump", "ACCEPT") +    # block DNS requests to anyone but the service provider or localhost +    # (when we actually route ipv6, we will need DNS rules for it too) +    ip4tables("--append", BITMASK_CHAIN, "--protocol", "udp", "--dport", "53", +              "--jump", "REJECT")      # allow traffic to IPs on local network      if local_network_ipv4: -        ip4tables("--insert", BITMASK_CHAIN, +        ip4tables("--append", BITMASK_CHAIN,                    "--destination", local_network_ipv4, "-o", default_device,                    "--jump", "ACCEPT") +        # allow multicast Simple Service Discovery Protocol +        ip4tables("--append", BITMASK_CHAIN, +                  "--protocol", "udp", +                  "--destination", "239.255.255.250", "--dport", "1900", +                  "-o", default_device, "--jump", "RETURN") +        # allow multicast Bonjour/mDNS +        ip4tables("--append", BITMASK_CHAIN, +                  "--protocol", "udp", +                  "--destination", "224.0.0.251", "--dport", "5353", +                  "-o", default_device, "--jump", "RETURN")      if local_network_ipv6: -        ip6tables("--insert", BITMASK_CHAIN, +        ip6tables("--append", BITMASK_CHAIN,                    "--destination", local_network_ipv6, "-o", default_device,                    "--jump", "ACCEPT") +        # allow multicast Simple Service Discovery Protocol +        ip6tables("--append", BITMASK_CHAIN, +                  "--protocol", "udp", +                  "--destination", "FF05::C", "--dport", "1900", +                  "-o", default_device, "--jump", "RETURN") +        # allow multicast Bonjour/mDNS +        ip6tables("--append", BITMASK_CHAIN, +                  "--protocol", "udp", +                  "--destination", "FF02::FB", "--dport", "5353", +                  "-o", default_device, "--jump", "RETURN") + +    # allow ipv4 traffic to gateways +    for gateway in gateways: +        ip4tables("--append", BITMASK_CHAIN, "--destination", gateway, +                  "-o", default_device, "--jump", "ACCEPT") -    # block DNS requests to anyone but the service provider or localhost -    # when we actually route ipv6, we will need dns rules for it too -    ip4tables("--insert", BITMASK_CHAIN, "--protocol", "udp", "--dport", "53", -              "--jump", "REJECT") +    # log rejected packets to syslog +    if DEBUG: +        iptables("--append", BITMASK_CHAIN, "-o", default_device, +                 "--jump", "LOG", "--log-prefix", "iptables denied: ", +                 "--log-level", "7") -    for allowed_dns in [NAMESERVER, "127.0.0.1", "127.0.1.1"]: -        ip4tables("--insert", BITMASK_CHAIN, "--protocol", "udp", -                  "--dport", "53", "--destination", allowed_dns, -                  "--jump", "ACCEPT") +    # for now, ensure all other ipv6 packets get rejected (regardless of +    # device) +    # (not sure why, but "-p any" doesn't work) +    ip6tables("--append", BITMASK_CHAIN, "-p", "tcp", "--jump", "REJECT") +    ip6tables("--append", BITMASK_CHAIN, "-p", "udp", "--jump", "REJECT") + +    # reject all other ipv4 sent over the default device +    ip4tables("--append", BITMASK_CHAIN, "-o", +              default_device, "--jump", "REJECT")  def firewall_stop(): @@ -784,10 +828,27 @@ def firewall_stop():  def main(): -    if len(sys.argv) >= 3: +    """ +    Entry point for cmdline execution. +    """ +    # TODO use argparse instead. + +    if len(sys.argv) >= 2:          command = "_".join(sys.argv[1:3])          args = sys.argv[3:] +        is_restart = False +        if args and args[0] == "restart": +            is_restart = True +            args.remove('restart') + +        if command == "version": +            print(VERSION) +            exit(0) + +        if os.getuid() != 0: +            bail("ERROR: must be run as root") +          if command == "openvpn_start":              openvpn_start(args) @@ -799,8 +860,9 @@ def main():                  firewall_start(args)                  nameserver_setter.start(NAMESERVER)              except Exception as ex: -                nameserver_restorer.start() -                firewall_stop() +                if not is_restart: +                    nameserver_restorer.start() +                    firewall_stop()                  bail("ERROR: could not start firewall", ex)          elif command == "firewall_stop": diff --git a/pkg/requirements.pip b/pkg/requirements.pip index 70427e63..3d6b33a3 100644 --- a/pkg/requirements.pip +++ b/pkg/requirements.pip @@ -11,10 +11,7 @@ srp>=1.0.2  pyopenssl  python-dateutil -# since gnupg requires exactly 1.2.1, this chokes if we -# don't specify a version. Selecting something lesser than -# 2.0 is equivalent to pick 1.2.1. See #5489 -psutil<2.0 +psutil  ipaddr  twisted diff --git a/relnotes.txt b/relnotes.txt index a658a782..e95e8c15 100644 --- a/relnotes.txt +++ b/relnotes.txt @@ -1,8 +1,8 @@ -ANNOUNCING Bitmask, the Internet Encryption Toolkit, release 0.5.1 +ANNOUNCING Bitmask, the Internet Encryption Toolkit, release 0.5.2  The LEAP  team is  pleased to announce  the immediate  availability of -version 0.5.1  of Bitmask,  the Internet Encryption  Toolkit, codename -"lil less leaky". +version 0.5.2  of Bitmask,  the Internet Encryption  Toolkit, codename +"are we there yet".  https://downloads.leap.se/client/ @@ -43,13 +43,9 @@ NOT trust your life to it.  WHAT CAN THIS VERSION OF BITMASK DO FOR ME? -Bitmask  0.5.1 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 -connection that the spying eyes can only track back to your provider. +Bitmask  0.5.2 improves  greatly  its Encrypted  internet support  and +stability in general, among other various  bug fixes. You can refer to +the CHANGELOG for the meat.  Encrypted Internet on Linux now  helps you don't shoot yourself in the  foot  by   leaking  traffic  outside  of  the   secure  connection  it @@ -108,6 +104,6 @@ beyond any border.  The LEAP team, -May 16, 2014 +June 6, 2014  Somewhere in the middle of the intertubes.  EOF @@ -203,9 +203,9 @@ if IS_LINUX:      # globally. Or make specific install command. See #3805      data_files = [          ("share/polkit-1/actions", -         ["pkg/linux/polkit/net.openvpn.gui.leap.policy"]), -        ("etc/leap/", -         ["pkg/linux/resolv-update"]), +         ["pkg/linux/polkit/se.leap.bitmask.policy"]), +        ("/usr/sbin", +         ["pkg/linux/bitmask-root"]),      ] diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index e413ab4c..e965604a 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -39,7 +39,6 @@  # M:::::::::::~NMMM7???7MMMM:::::::::::::::::::::::NMMMI??I7MMMM:::::::::::::M  # M::::::::::::::7MMMMMMM+:::::::::::::::::::::::::::?MMMMMMMZ:::::::::::::::M  #                (thanks to: http://www.glassgiant.com/ascii/) -import logging  import signal  import sys  import os @@ -50,10 +49,7 @@ from PySide import QtCore, QtGui  from leap.bitmask import __version__ as VERSION  from leap.bitmask.util import leap_argparse -from leap.bitmask.util import log_silencer, LOG_FORMAT -from leap.bitmask.util.leap_log_handler import LeapLogHandler -from leap.bitmask.util.streamtologger import StreamToLogger -from leap.bitmask.platform_init import IS_WIN +from leap.bitmask.logs.utils import get_logger  from leap.bitmask.services.mail import plumber  from leap.common.events import server as event_server  from leap.mail import __version__ as MAIL_VERSION @@ -76,6 +72,7 @@ def sigint_handler(*args, **kwargs):      mainwindow = args[0]      mainwindow.quit() +  def sigterm_handler(*args, **kwargs):      """      Signal handler for SIGTERM. @@ -87,89 +84,6 @@ def sigterm_handler(*args, **kwargs):      mainwindow = args[0]      mainwindow.quit() -def add_logger_handlers(debug=False, logfile=None, replace_stdout=True): -    """ -    Create the logger and attach the handlers. - -    :param debug: the level of the messages that we should log -    :type debug: bool -    :param logfile: the file name of where we should to save the logs -    :type logfile: str -    :return: the new logger with the attached handlers. -    :rtype: logging.Logger -    """ -    # TODO: get severity from command line args -    if debug: -        level = logging.DEBUG -    else: -        level = logging.WARNING - -    # Create logger and formatter -    logger = logging.getLogger(name='leap') -    logger.setLevel(level) -    formatter = logging.Formatter(LOG_FORMAT) - -    # Console handler -    try: -        import coloredlogs -        console = coloredlogs.ColoredStreamHandler(level=level) -    except ImportError: -        console = logging.StreamHandler() -        console.setLevel(level) -        console.setFormatter(formatter) -        using_coloredlog = False -    else: -        using_coloredlog = True - -    if using_coloredlog: -        replace_stdout = False - -    silencer = log_silencer.SelectiveSilencerFilter() -    console.addFilter(silencer) -    logger.addHandler(console) -    logger.debug('Console handler plugged!') - -    # LEAP custom handler -    leap_handler = LeapLogHandler() -    leap_handler.setLevel(level) -    leap_handler.addFilter(silencer) -    logger.addHandler(leap_handler) -    logger.debug('Leap handler plugged!') - -    # File handler -    if logfile is not None: -        logger.debug('Setting logfile to %s ', logfile) -        fileh = logging.FileHandler(logfile) -        fileh.setLevel(logging.DEBUG) -        fileh.setFormatter(formatter) -        fileh.addFilter(silencer) -        logger.addHandler(fileh) -        logger.debug('File handler plugged!') - -    if replace_stdout: -        replace_stdout_stderr_with_logging(logger) - -    return logger - - -def replace_stdout_stderr_with_logging(logger): -    """ -    Replace: -        - the standard output -        - the standard error -        - the twisted log output -    with a custom one that writes to the logger. -    """ -    # Disabling this on windows since it breaks ALL THE THINGS -    # The issue for this is #4149 -    if not IS_WIN: -        sys.stdout = StreamToLogger(logger, logging.DEBUG) -        sys.stderr = StreamToLogger(logger, logging.ERROR) - -        # Replace twisted's logger to use our custom output. -        from twisted.python import log -        log.startLogging(sys.stdout) -  def do_display_version(opts):      """ @@ -212,6 +126,14 @@ def main():      mail_logfile = opts.mail_log_file      start_hidden = opts.start_hidden +    replace_stdout = True +    if opts.repair or opts.import_maildir: +        # We don't want too much clutter on the comand mode +        # this could be more generic with a Command class. +        replace_stdout = False + +    logger = get_logger(debug, logfile, replace_stdout) +      #############################################################      # Given how paths and bundling works, we need to delay the imports      # of certain parts that depend on this path settings. @@ -230,13 +152,6 @@ def main():      BaseConfig.standalone = standalone -    replace_stdout = True -    if opts.repair or opts.import_maildir: -        # We don't want too much clutter on the comand mode -        # this could be more generic with a Command class. -        replace_stdout = False -    logger = add_logger_handlers(debug, logfile, replace_stdout) -      # ok, we got logging in place, we can satisfy mail plumbing requests      # and show logs there. it normally will exit there if we got that path.      do_mail_plumbing(opts) diff --git a/src/leap/bitmask/backend.py b/src/leap/bitmask/backend.py index a2df465d..3c97c797 100644 --- a/src/leap/bitmask/backend.py +++ b/src/leap/bitmask/backend.py @@ -17,13 +17,13 @@  """  Backend for everything  """ -import commands  import logging  import os  import time  from functools import partial  from Queue import Queue, Empty +from threading import Condition  from twisted.internet import reactor  from twisted.internet import threads, defer @@ -31,46 +31,40 @@ from twisted.internet.task import LoopingCall  from twisted.python import log  import zope.interface +import zope.proxy  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.crypto.srpauth import SRPAuth  from leap.bitmask.crypto.srpregister import SRPRegister  from leap.bitmask.platform_init import IS_LINUX -from leap.bitmask.provider import get_provider_path  from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper +from leap.bitmask.services import get_supported  from leap.bitmask.services.eip import eipconfig  from leap.bitmask.services.eip import get_openvpn_management  from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper  from leap.bitmask.services.eip import vpnlauncher, vpnprocess  from leap.bitmask.services.eip import linuxvpnlauncher, darwinvpnlauncher +from leap.bitmask.services.eip import get_vpn_launcher -from leap.common import certs as leap_certs - -# Frontend side -from PySide import QtCore +from leap.bitmask.services.mail.imapcontroller import IMAPController +from leap.bitmask.services.mail.smtpbootstrapper import SMTPBootstrapper +from leap.bitmask.services.mail.smtpconfig import SMTPConfig -logger = logging.getLogger(__name__) +from leap.bitmask.services.soledad.soledadbootstrapper import \ +    SoledadBootstrapper +from leap.common import certs as leap_certs -def get_provider_config(config, domain): -    """ -    Return the ProviderConfig object for the given domain. -    If it is already loaded in `config`, then don't reload. +from leap.keymanager import openpgp +from leap.keymanager.errors import KeyAddressMismatch, KeyFingerprintMismatch -    :param config: a ProviderConfig object -    :type conig: ProviderConfig -    :param domain: the domain which config is required. -    :type domain: unicode +from leap.soledad.client import NoStorageSecret, PassphraseTooShort -    :returns: True if the config was loaded successfully, False otherwise. -    :rtype: bool -    """ -    # TODO: see ProviderConfig.get_provider_config -    if (not config.loaded() or config.get_domain() != domain): -        config.load(get_provider_path(domain)) +# Frontend side +from PySide import QtCore -    return config.loaded() +logger = logging.getLogger(__name__)  class ILEAPComponent(zope.interface.Interface): @@ -86,13 +80,13 @@ class ILEAPService(ILEAPComponent):      Interface that every Service needs to implement      """ -    def start(self): +    def start(self, *args, **kwargs):          """          Start the service.          """          pass -    def stop(self): +    def stop(self, *args, **kwargs):          """          Stops the service.          """ @@ -156,6 +150,7 @@ class Provider(object):          :type bypass_checks: bool          """          self.key = "provider" +        self._signaler = signaler          self._provider_bootstrapper = ProviderBootstrapper(signaler,                                                             bypass_checks)          self._download_provider_defer = None @@ -197,11 +192,11 @@ class Provider(object):          """          d = None -        config = self._provider_config -        if get_provider_config(config, provider): +        config = ProviderConfig.get_provider_config(provider) +        self._provider_config = config +        if config is not None:              d = self._provider_bootstrapper.run_provider_setup_checks( -                self._provider_config, -                download_if_needed=True) +                config, download_if_needed=True)          else:              if self._signaler is not None:                  self._signaler.signal( @@ -213,6 +208,73 @@ class Provider(object):              d = defer.Deferred()          return d +    def _get_services(self, domain): +        """ +        Returns a list of services provided by the given provider. + +        :param domain: the provider to get the services from. +        :type domain: str + +        :rtype: list of str +        """ +        services = [] +        provider_config = ProviderConfig.get_provider_config(domain) +        if provider_config is not None: +            services = provider_config.get_services() + +        return services + +    def get_supported_services(self, domain): +        """ +        Signal a list of supported services provided by the given provider. + +        :param domain: the provider to get the services from. +        :type domain: str + +        Signals: +            prov_get_supported_services -> list of unicode +        """ +        services = get_supported(self._get_services(domain)) + +        self._signaler.signal( +            self._signaler.PROV_GET_SUPPORTED_SERVICES, services) + +    def get_all_services(self, providers): +        """ +        Signal a list of services provided by all the configured providers. + +        :param providers: the list of providers to get the services. +        :type providers: list + +        Signals: +            prov_get_all_services -> list of unicode +        """ +        services_all = set() + +        for domain in providers: +            services = self._get_services(domain) +            services_all = services_all.union(set(services)) + +        self._signaler.signal( +            self._signaler.PROV_GET_ALL_SERVICES, services_all) + +    def get_details(self, domain, lang=None): +        """ +        Signal a ProviderConfigLight object with the current ProviderConfig +        settings. + +        :param domain: the domain name of the provider. +        :type domain: str +        :param lang: the language to use for localized strings. +        :type lang: str + +        Signals: +            prov_get_details -> ProviderConfigLight +        """ +        self._signaler.signal( +            self._signaler.PROV_GET_DETAILS, +            self._provider_config.get_light_config(domain, lang)) +  class Register(object):      """ @@ -246,8 +308,9 @@ class Register(object):          :returns: the defer for the operation running in a thread.          :rtype: twisted.internet.defer.Deferred          """ -        config = ProviderConfig() -        if get_provider_config(config, domain): +        config = ProviderConfig.get_provider_config(domain) +        self._provider_config = config +        if config is not None:              srpregister = SRPRegister(signaler=self._signaler,                                        provider_config=config)              return threads.deferToThread( @@ -294,8 +357,9 @@ class EIP(object):          :returns: the defer for the operation running in a thread.          :rtype: twisted.internet.defer.Deferred          """ -        config = self._provider_config -        if get_provider_config(config, domain): +        config = ProviderConfig.get_provider_config(domain) +        self._provider_config = config +        if config is not None:              if skip_network:                  return defer.Deferred()              eb = self._eip_bootstrapper @@ -314,9 +378,12 @@ class EIP(object):          if d is not None:              d.cancel() -    def _start_eip(self): +    def _start_eip(self, restart=False):          """          Start EIP + +        :param restart: whether is is a restart. +        :type restart: bool          """          provider_config = self._provider_config          eip_config = eipconfig.EIPConfig() @@ -325,6 +392,11 @@ class EIP(object):          loaded = eipconfig.load_eipconfig_if_needed(              provider_config, eip_config, domain) +        if not self._can_start(domain): +            if self._signaler is not None: +                self._signaler.signal(self._signaler.EIP_CONNECTION_ABORTED) +            return +          if not loaded:              if self._signaler is not None:                  self._signaler.signal(self._signaler.EIP_CONNECTION_ABORTED) @@ -335,9 +407,10 @@ class EIP(object):          host, port = get_openvpn_management()          self._vpn.start(eipconfig=eip_config,                          providerconfig=provider_config, -                        socket_host=host, socket_port=port) +                        socket_host=host, socket_port=port, +                        restart=restart) -    def start(self): +    def start(self, *args, **kwargs):          """          Start the service.          """ @@ -350,7 +423,7 @@ class EIP(object):              return          try: -            self._start_eip() +            self._start_eip(*args, **kwargs)          except vpnprocess.OpenVPNAlreadyRunning:              signaler.signal(signaler.EIP_OPENVPN_ALREADY_RUNNING)          except vpnprocess.AlienOpenVPNAlreadyRunning: @@ -370,17 +443,22 @@ class EIP(object):          except Exception as e:              logger.error("Unexpected problem: {0!r}".format(e))          else: -            # TODO: are we connected here? -            signaler.signal(signaler.EIP_CONNECTED) +            logger.debug('EIP: no errors') -    def stop(self, shutdown=False): +    def _do_stop(self, shutdown=False, restart=False):          """ -        Stop the service. +        Stop the service. This is run in a thread to avoid blocking.          """ -        self._vpn.terminate(shutdown) +        self._vpn.terminate(shutdown, restart)          if IS_LINUX:              self._wait_for_firewall_down() +    def stop(self, shutdown=False, restart=False): +        """ +        Stop the service. +        """ +        return threads.deferToThread(self._do_stop, shutdown, restart) +      def _wait_for_firewall_down(self):          """          Wait for the firewall to come down. @@ -393,15 +471,16 @@ class EIP(object):          MAX_FW_WAIT_RETRIES = 25          FW_WAIT_STEP = 0.5 -        retry = 0 - -        fw_up_cmd = "pkexec /usr/sbin/bitmask-root firewall isup" -        fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256 +        retry = 1 -        while retry < MAX_FW_WAIT_RETRIES: -            if fw_is_down(): +        while retry <= MAX_FW_WAIT_RETRIES: +            if self._vpn.is_fw_down(): +                self._signaler.signal(self._signaler.EIP_STOPPED)                  return              else: +                #msg = "Firewall is not down yet, waiting... {0} of {1}" +                #msg = msg.format(retry, MAX_FW_WAIT_RETRIES) +                #logger.debug(msg)                  time.sleep(FW_WAIT_STEP)                  retry += 1          logger.warning("After waiting, firewall is not down... " @@ -459,6 +538,12 @@ class EIP(object):              self._signaler.signal(self._signaler.EIP_GET_INITIALIZED_PROVIDERS,                                    filtered_domains) +    def tear_fw_down(self): +        """ +        Tear the firewall down. +        """ +        self._vpn.tear_down_firewall() +      def get_gateways_list(self, domain):          """          Signal a list of gateways for the given provider. @@ -497,6 +582,46 @@ class EIP(object):              self._signaler.signal(                  self._signaler.EIP_GET_GATEWAYS_LIST, gateways) +    def _can_start(self, domain): +        """ +        Returns True if it has everything that is needed to run EIP, +        False otherwise + +        :param domain: the domain for the provider to check +        :type domain: str +        """ +        eip_config = eipconfig.EIPConfig() +        provider_config = ProviderConfig.get_provider_config(domain) + +        api_version = provider_config.get_api_version() +        eip_config.set_api_version(api_version) +        eip_loaded = eip_config.load(eipconfig.get_eipconfig_path(domain)) + +        launcher = get_vpn_launcher() +        if not os.path.isfile(launcher.OPENVPN_BIN_PATH): +            logger.error("Cannot start OpenVPN, binary not found") +            return False + +        # check for other problems +        if not eip_loaded or provider_config is None: +            logger.error("Cannot load provider and eip config, cannot " +                         "autostart") +            return False + +        client_cert_path = eip_config.\ +            get_client_cert_path(provider_config, about_to_download=False) + +        if leap_certs.should_redownload(client_cert_path): +            logger.error("The client should redownload the certificate," +                         " cannot autostart") +            return False + +        if not os.path.isfile(client_cert_path): +            logger.error("Can't find the certificate, cannot autostart") +            return False + +        return True +      def can_start(self, domain):          """          Signal whether it has everything that is needed to run EIP or not @@ -508,35 +633,363 @@ class EIP(object):              eip_can_start              eip_cannot_start          """ -        try: -            eip_config = eipconfig.EIPConfig() -            provider_config = ProviderConfig.get_provider_config(domain) +        if self._can_start(domain): +            if self._signaler is not None: +                self._signaler.signal(self._signaler.EIP_CAN_START) +        else: +            if self._signaler is not None: +                self._signaler.signal(self._signaler.EIP_CANNOT_START) -            api_version = provider_config.get_api_version() -            eip_config.set_api_version(api_version) -            eip_loaded = eip_config.load(eipconfig.get_eipconfig_path(domain)) -            # check for other problems -            if not eip_loaded or provider_config is None: -                raise Exception("Cannot load provider and eip config, cannot " -                                "autostart") +class Soledad(object): +    """ +    Interfaces with setup of Soledad. +    """ +    zope.interface.implements(ILEAPComponent) -            client_cert_path = eip_config.\ -                get_client_cert_path(provider_config, about_to_download=False) +    def __init__(self, soledad_proxy, keymanager_proxy, signaler=None): +        """ +        Constructor for the Soledad component. -            if leap_certs.should_redownload(client_cert_path): -                raise Exception("The client should redownload the certificate," -                                " cannot autostart") +        :param soledad_proxy: proxy to pass around a Soledad object. +        :type soledad_proxy: zope.ProxyBase +        :param keymanager_proxy: proxy to pass around a Keymanager object. +        :type keymanager_proxy: zope.ProxyBase +        :param signaler: Object in charge of handling communication +                         back to the frontend +        :type signaler: Signaler +        """ +        self.key = "soledad" +        self._soledad_proxy = soledad_proxy +        self._keymanager_proxy = keymanager_proxy +        self._signaler = signaler +        self._soledad_bootstrapper = SoledadBootstrapper(signaler) +        self._soledad_defer = None -            if not os.path.isfile(client_cert_path): -                raise Exception("Can't find the certificate, cannot autostart") +    def bootstrap(self, username, domain, password): +        """ +        Bootstrap Soledad with the user credentials. +        Signals: +            soledad_download_config +            soledad_gen_key + +        :param user: user's login +        :type user: unicode +        :param domain: the domain that we are using. +        :type domain: unicode +        :param password: user's password +        :type password: unicode +        """ +        provider_config = ProviderConfig.get_provider_config(domain) +        if provider_config is not None: +            self._soledad_defer = threads.deferToThread( +                self._soledad_bootstrapper.run_soledad_setup_checks, +                provider_config, username, password, +                download_if_needed=True) +            self._soledad_defer.addCallback(self._set_proxies_cb) +        else:              if self._signaler is not None: -                self._signaler.signal(self._signaler.EIP_CAN_START) -        except Exception as e: -            logger.exception(e) -            if self._signaler is not None: -                self._signaler.signal(self._signaler.EIP_CANNOT_START) +                self._signaler.signal(self._signaler.SOLEDAD_BOOTSTRAP_FAILED) +            logger.error("Could not load provider configuration.") + +        return self._soledad_defer + +    def _set_proxies_cb(self, _): +        """ +        Update the soledad and keymanager proxies to reference the ones created +        in the bootstrapper. +        """ +        zope.proxy.setProxiedObject(self._soledad_proxy, +                                    self._soledad_bootstrapper.soledad) +        zope.proxy.setProxiedObject(self._keymanager_proxy, +                                    self._soledad_bootstrapper.keymanager) + +    def load_offline(self, username, password, uuid): +        """ +        Load the soledad database in offline mode. + +        :param username: full user id (user@provider) +        :type username: str or unicode +        :param password: the soledad passphrase +        :type password: unicode +        :param uuid: the user uuid +        :type uuid: str or unicode + +        Signals: +            Signaler.soledad_offline_finished +            Signaler.soledad_offline_failed +        """ +        self._soledad_bootstrapper.load_offline_soledad( +            username, password, uuid) + +    def cancel_bootstrap(self): +        """ +        Cancel the ongoing soledad bootstrap (if any). +        """ +        if self._soledad_defer is not None: +            logger.debug("Cancelling soledad defer.") +            self._soledad_defer.cancel() +            self._soledad_defer = None +            zope.proxy.setProxiedObject(self._soledad_proxy, None) + +    def close(self): +        """ +        Close soledad database. +        """ +        if not zope.proxy.sameProxiedObjects(self._soledad_proxy, None): +            self._soledad_proxy.close() +            zope.proxy.setProxiedObject(self._soledad_proxy, None) + +    def _change_password_ok(self, _): +        """ +        Password change callback. +        """ +        if self._signaler is not None: +            self._signaler.signal(self._signaler.SOLEDAD_PASSWORD_CHANGE_OK) + +    def _change_password_error(self, failure): +        """ +        Password change errback. + +        :param failure: failure object containing problem. +        :type failure: twisted.python.failure.Failure +        """ +        if failure.check(NoStorageSecret): +            logger.error("No storage secret for password change in Soledad.") +        if failure.check(PassphraseTooShort): +            logger.error("Passphrase too short.") + +        if self._signaler is not None: +            self._signaler.signal(self._signaler.SOLEDAD_PASSWORD_CHANGE_ERROR) + +    def change_password(self, new_password): +        """ +        Change the database's password. + +        :param new_password: the new password. +        :type new_password: unicode + +        :returns: a defer to interact with. +        :rtype: twisted.internet.defer.Deferred +        """ +        d = threads.deferToThread(self._soledad_proxy.change_passphrase, +                                  new_password) +        d.addCallback(self._change_password_ok) +        d.addErrback(self._change_password_error) + + +class Keymanager(object): +    """ +    Interfaces with KeyManager. +    """ +    zope.interface.implements(ILEAPComponent) + +    def __init__(self, keymanager_proxy, signaler=None): +        """ +        Constructor for the Keymanager component. + +        :param keymanager_proxy: proxy to pass around a Keymanager object. +        :type keymanager_proxy: zope.ProxyBase +        :param signaler: Object in charge of handling communication +                         back to the frontend +        :type signaler: Signaler +        """ +        self.key = "keymanager" +        self._keymanager_proxy = keymanager_proxy +        self._signaler = signaler + +    def import_keys(self, username, filename): +        """ +        Imports the username's key pair. +        Those keys need to be ascii armored. + +        :param username: the user that will have the imported pair of keys. +        :type username: str +        :param filename: the name of the file where the key pair is stored. +        :type filename: str +        """ +        # NOTE: This feature is disabled right now since is dangerous +        return + +        new_key = '' +        signal = None +        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)) +            signal = self._signaler.KEYMANAGER_IMPORT_IOERROR +            self._signaler.signal(signal) +            return + +        keymanager = self._keymanager_proxy +        try: +            public_key, private_key = keymanager.parse_openpgp_ascii_key( +                new_key) +        except (KeyAddressMismatch, KeyFingerprintMismatch) as e: +            logger.error(repr(e)) +            signal = self._signaler.KEYMANAGER_IMPORT_DATAMISMATCH +            self._signaler.signal(signal) +            return + +        if public_key is None or private_key is None: +            signal = self._signaler.KEYMANAGER_IMPORT_MISSINGKEY +            self._signaler.signal(signal) +            return + +        current_public_key = keymanager.get_key(username, openpgp.OpenPGPKey) +        if public_key.address != current_public_key.address: +            logger.error("The key does not match the ID") +            signal = self._signaler.KEYMANAGER_IMPORT_ADDRESSMISMATCH +            self._signaler.signal(signal) +            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') +        signal = self._signaler.KEYMANAGER_IMPORT_OK + +        self._signaler.signal(signal) + +    def export_keys(self, username, filename): +        """ +        Export the given username's keys to a file. + +        :param username: the username whos keys we need to export. +        :type username: str +        :param filename: the name of the file where we want to save the keys. +        :type filename: str +        """ +        keymanager = self._keymanager_proxy + +        public_key = keymanager.get_key(username, openpgp.OpenPGPKey) +        private_key = keymanager.get_key(username, openpgp.OpenPGPKey, +                                         private=True) +        try: +            with open(filename, 'w') as keys_file: +                keys_file.write(public_key.key_data) +                keys_file.write(private_key.key_data) + +            logger.debug('Export ok') +            self._signaler.signal(self._signaler.KEYMANAGER_EXPORT_OK) +        except IOError as e: +            logger.error("IOError exporting key. {0!r}".format(e)) +            self._signaler.signal(self._signaler.KEYMANAGER_EXPORT_ERROR) + +    def list_keys(self): +        """ +        List all the keys stored in the local DB. +        """ +        keys = self._keymanager_proxy.get_all_keys_in_local_db() +        self._signaler.signal(self._signaler.KEYMANAGER_KEYS_LIST, keys) + +    def get_key_details(self, username): +        """ +        List all the keys stored in the local DB. +        """ +        public_key = self._keymanager_proxy.get_key(username, +                                                    openpgp.OpenPGPKey) +        details = (public_key.key_id, public_key.fingerprint) +        self._signaler.signal(self._signaler.KEYMANAGER_KEY_DETAILS, details) + + +class Mail(object): +    """ +    Interfaces with setup and launch of Mail. +    """ +    # We give each service some time to come to a halt before forcing quit +    SERVICE_STOP_TIMEOUT = 20 + +    zope.interface.implements(ILEAPComponent) + +    def __init__(self, soledad_proxy, keymanager_proxy, signaler=None): +        """ +        Constructor for the Mail component. + +        :param soledad_proxy: proxy to pass around a Soledad object. +        :type soledad_proxy: zope.ProxyBase +        :param keymanager_proxy: proxy to pass around a Keymanager object. +        :type keymanager_proxy: zope.ProxyBase +        :param signaler: Object in charge of handling communication +                         back to the frontend +        :type signaler: Signaler +        """ +        self.key = "mail" +        self._signaler = signaler +        self._soledad_proxy = soledad_proxy +        self._keymanager_proxy = keymanager_proxy +        self._imap_controller = IMAPController(self._soledad_proxy, +                                               self._keymanager_proxy) +        self._smtp_bootstrapper = SMTPBootstrapper() +        self._smtp_config = SMTPConfig() + +    def start_smtp_service(self, full_user_id, download_if_needed=False): +        """ +        Start the SMTP service. + +        :param full_user_id: user id, in the form "user@provider" +        :type full_user_id: str +        :param download_if_needed: True if it should check for mtime +                                   for the file +        :type download_if_needed: bool + +        :returns: a defer to interact with. +        :rtype: twisted.internet.defer.Deferred +        """ +        return threads.deferToThread( +            self._smtp_bootstrapper.start_smtp_service, +            self._keymanager_proxy, full_user_id, download_if_needed) + +    def start_imap_service(self, full_user_id, offline=False): +        """ +        Start the IMAP service. + +        :param full_user_id: user id, in the form "user@provider" +        :type full_user_id: str +        :param offline: whether imap should start in offline mode or not. +        :type offline: bool + +        :returns: a defer to interact with. +        :rtype: twisted.internet.defer.Deferred +        """ +        return threads.deferToThread( +            self._imap_controller.start_imap_service, +            full_user_id, offline) + +    def stop_smtp_service(self): +        """ +        Stop the SMTP service. + +        :returns: a defer to interact with. +        :rtype: twisted.internet.defer.Deferred +        """ +        return threads.deferToThread(self._smtp_bootstrapper.stop_smtp_service) + +    def _stop_imap_service(self): +        """ +        Stop imap and wait until the service is stopped to signal that is done. +        """ +        cv = Condition() +        cv.acquire() +        threads.deferToThread(self._imap_controller.stop_imap_service, cv) +        logger.debug('Waiting for imap service to stop.') +        cv.wait(self.SERVICE_STOP_TIMEOUT) +        logger.debug('IMAP stopped') +        self._signaler.signal(self._signaler.IMAP_STOPPED) + +    def stop_imap_service(self): +        """ +        Stop imap service (fetcher, factory and port). + +        :returns: a defer to interact with. +        :rtype: twisted.internet.defer.Deferred +        """ +        return threads.deferToThread(self._stop_imap_service)  class Authenticate(object): @@ -556,6 +1009,7 @@ class Authenticate(object):          """          self.key = "authenticate"          self._signaler = signaler +        self._login_defer = None          self._srp_auth = SRPAuth(ProviderConfig(), self._signaler)      def login(self, domain, username, password): @@ -572,8 +1026,8 @@ class Authenticate(object):          :returns: the defer for the operation running in a thread.          :rtype: twisted.internet.defer.Deferred          """ -        config = ProviderConfig() -        if get_provider_config(config, domain): +        config = ProviderConfig.get_provider_config(domain) +        if config is not None:              self._srp_auth = SRPAuth(config, self._signaler)              self._login_defer = self._srp_auth.authenticate(username, password)              return self._login_defer @@ -670,6 +1124,10 @@ class Signaler(QtCore.QObject):      prov_unsupported_client = QtCore.Signal(object)      prov_unsupported_api = QtCore.Signal(object) +    prov_get_all_services = QtCore.Signal(object) +    prov_get_supported_services = QtCore.Signal(object) +    prov_get_details = QtCore.Signal(object) +      prov_cancelled_setup = QtCore.Signal(object)      # Signals for SRPRegister @@ -703,6 +1161,7 @@ class Signaler(QtCore.QObject):      eip_disconnected = QtCore.Signal(object)      eip_connection_died = QtCore.Signal(object)      eip_connection_aborted = QtCore.Signal(object) +    eip_stopped = QtCore.Signal(object)      # EIP problems      eip_no_polkit_agent_error = QtCore.Signal(object) @@ -727,11 +1186,38 @@ class Signaler(QtCore.QObject):      eip_state_changed = QtCore.Signal(dict)      eip_status_changed = QtCore.Signal(dict)      eip_process_finished = QtCore.Signal(int) +    eip_tear_fw_down = QtCore.Signal(object)      # signals whether the needed files to start EIP exist or not      eip_can_start = QtCore.Signal(object)      eip_cannot_start = QtCore.Signal(object) +    # Signals for Soledad +    soledad_bootstrap_failed = QtCore.Signal(object) +    soledad_bootstrap_finished = QtCore.Signal(object) +    soledad_offline_failed = QtCore.Signal(object) +    soledad_offline_finished = QtCore.Signal(object) +    soledad_invalid_auth_token = QtCore.Signal(object) +    soledad_cancelled_bootstrap = QtCore.Signal(object) +    soledad_password_change_ok = QtCore.Signal(object) +    soledad_password_change_error = QtCore.Signal(object) + +    # Keymanager signals +    keymanager_export_ok = QtCore.Signal(object) +    keymanager_export_error = QtCore.Signal(object) +    keymanager_keys_list = QtCore.Signal(object) + +    keymanager_import_ioerror = QtCore.Signal(object) +    keymanager_import_datamismatch = QtCore.Signal(object) +    keymanager_import_missingkey = QtCore.Signal(object) +    keymanager_import_addressmismatch = QtCore.Signal(object) +    keymanager_import_ok = QtCore.Signal(object) + +    keymanager_key_details = QtCore.Signal(object) + +    # mail related signals +    imap_stopped = QtCore.Signal(object) +      # This signal is used to warn the backend user that is doing something      # wrong      backend_bad_call = QtCore.Signal(object) @@ -751,6 +1237,9 @@ class Signaler(QtCore.QObject):      PROV_UNSUPPORTED_CLIENT = "prov_unsupported_client"      PROV_UNSUPPORTED_API = "prov_unsupported_api"      PROV_CANCELLED_SETUP = "prov_cancelled_setup" +    PROV_GET_ALL_SERVICES = "prov_get_all_services" +    PROV_GET_SUPPORTED_SERVICES = "prov_get_supported_services" +    PROV_GET_DETAILS = "prov_get_details"      SRP_REGISTRATION_FINISHED = "srp_registration_finished"      SRP_REGISTRATION_FAILED = "srp_registration_failed" @@ -777,6 +1266,8 @@ class Signaler(QtCore.QObject):      EIP_DISCONNECTED = "eip_disconnected"      EIP_CONNECTION_DIED = "eip_connection_died"      EIP_CONNECTION_ABORTED = "eip_connection_aborted" +    EIP_STOPPED = "eip_stopped" +      EIP_NO_POLKIT_AGENT_ERROR = "eip_no_polkit_agent_error"      EIP_NO_TUN_KEXT_ERROR = "eip_no_tun_kext_error"      EIP_NO_PKEXEC_ERROR = "eip_no_pkexec_error" @@ -797,10 +1288,35 @@ class Signaler(QtCore.QObject):      EIP_STATE_CHANGED = "eip_state_changed"      EIP_STATUS_CHANGED = "eip_status_changed"      EIP_PROCESS_FINISHED = "eip_process_finished" +    EIP_TEAR_FW_DOWN = "eip_tear_fw_down"      EIP_CAN_START = "eip_can_start"      EIP_CANNOT_START = "eip_cannot_start" +    SOLEDAD_BOOTSTRAP_FAILED = "soledad_bootstrap_failed" +    SOLEDAD_BOOTSTRAP_FINISHED = "soledad_bootstrap_finished" +    SOLEDAD_OFFLINE_FAILED = "soledad_offline_failed" +    SOLEDAD_OFFLINE_FINISHED = "soledad_offline_finished" +    SOLEDAD_INVALID_AUTH_TOKEN = "soledad_invalid_auth_token" + +    SOLEDAD_PASSWORD_CHANGE_OK = "soledad_password_change_ok" +    SOLEDAD_PASSWORD_CHANGE_ERROR = "soledad_password_change_error" + +    SOLEDAD_CANCELLED_BOOTSTRAP = "soledad_cancelled_bootstrap" + +    KEYMANAGER_EXPORT_OK = "keymanager_export_ok" +    KEYMANAGER_EXPORT_ERROR = "keymanager_export_error" +    KEYMANAGER_KEYS_LIST = "keymanager_keys_list" + +    KEYMANAGER_IMPORT_IOERROR = "keymanager_import_ioerror" +    KEYMANAGER_IMPORT_DATAMISMATCH = "keymanager_import_datamismatch" +    KEYMANAGER_IMPORT_MISSINGKEY = "keymanager_import_missingkey" +    KEYMANAGER_IMPORT_ADDRESSMISMATCH = "keymanager_import_addressmismatch" +    KEYMANAGER_IMPORT_OK = "keymanager_import_ok" +    KEYMANAGER_KEY_DETAILS = "keymanager_key_details" + +    IMAP_STOPPED = "imap_stopped" +      BACKEND_BAD_CALL = "backend_bad_call"      def __init__(self): @@ -821,6 +1337,9 @@ class Signaler(QtCore.QObject):              self.PROV_UNSUPPORTED_CLIENT,              self.PROV_UNSUPPORTED_API,              self.PROV_CANCELLED_SETUP, +            self.PROV_GET_ALL_SERVICES, +            self.PROV_GET_SUPPORTED_SERVICES, +            self.PROV_GET_DETAILS,              self.SRP_REGISTRATION_FINISHED,              self.SRP_REGISTRATION_FAILED, @@ -834,6 +1353,8 @@ class Signaler(QtCore.QObject):              self.EIP_DISCONNECTED,              self.EIP_CONNECTION_DIED,              self.EIP_CONNECTION_ABORTED, +            self.EIP_STOPPED, +              self.EIP_NO_POLKIT_AGENT_ERROR,              self.EIP_NO_TUN_KEXT_ERROR,              self.EIP_NO_PKEXEC_ERROR, @@ -872,6 +1393,29 @@ class Signaler(QtCore.QObject):              self.SRP_STATUS_LOGGED_IN,              self.SRP_STATUS_NOT_LOGGED_IN, +            self.SOLEDAD_BOOTSTRAP_FAILED, +            self.SOLEDAD_BOOTSTRAP_FINISHED, +            self.SOLEDAD_OFFLINE_FAILED, +            self.SOLEDAD_OFFLINE_FINISHED, +            self.SOLEDAD_INVALID_AUTH_TOKEN, +            self.SOLEDAD_CANCELLED_BOOTSTRAP, + +            self.SOLEDAD_PASSWORD_CHANGE_OK, +            self.SOLEDAD_PASSWORD_CHANGE_ERROR, + +            self.KEYMANAGER_EXPORT_OK, +            self.KEYMANAGER_EXPORT_ERROR, +            self.KEYMANAGER_KEYS_LIST, + +            self.KEYMANAGER_IMPORT_IOERROR, +            self.KEYMANAGER_IMPORT_DATAMISMATCH, +            self.KEYMANAGER_IMPORT_MISSINGKEY, +            self.KEYMANAGER_IMPORT_ADDRESSMISMATCH, +            self.KEYMANAGER_IMPORT_OK, +            self.KEYMANAGER_KEY_DETAILS, + +            self.IMAP_STOPPED, +              self.BACKEND_BAD_CALL,          ] @@ -926,11 +1470,24 @@ class Backend(object):          # Signaler object to translate commands into Qt signals          self._signaler = Signaler() +        # Objects needed by several components, so we make a proxy and pass +        # them around +        self._soledad_proxy = zope.proxy.ProxyBase(None) +        self._keymanager_proxy = zope.proxy.ProxyBase(None) +          # Component registration          self._register(Provider(self._signaler, bypass_checks))          self._register(Register(self._signaler))          self._register(Authenticate(self._signaler))          self._register(EIP(self._signaler)) +        self._register(Soledad(self._soledad_proxy, +                               self._keymanager_proxy, +                               self._signaler)) +        self._register(Keymanager(self._keymanager_proxy, +                                  self._signaler)) +        self._register(Mail(self._soledad_proxy, +                            self._keymanager_proxy, +                            self._signaler))          # We have a looping call on a thread executing all the          # commands in queue. Right now this queue is an actual Queue @@ -952,7 +1509,7 @@ class Backend(object):          """          Starts the looping call          """ -        log.msg("Starting worker...") +        logger.debug("Starting worker...")          self._lc.start(0.01)      def stop(self): @@ -965,14 +1522,17 @@ class Backend(object):          """          Delayed stopping of worker. Called from `stop`.          """ -        log.msg("Stopping worker...") +        logger.debug("Stopping worker...")          if self._lc.running:              self._lc.stop()          else:              logger.warning("Looping call is not running, cannot stop") + +        logger.debug("Cancelling ongoing defers...")          while len(self._ongoing_defers) > 0:              d = self._ongoing_defers.pop()              d.cancel() +        logger.debug("Defers cancelled.")      def _register(self, component):          """ @@ -986,8 +1546,7 @@ class Backend(object):          try:              self._components[component.key] = component          except Exception: -            log.msg("There was a problem registering %s" % (component,)) -            log.err() +            logger.error("There was a problem registering %s" % (component,))      def _signal_back(self, _, signal):          """ @@ -1015,19 +1574,19 @@ class Backend(object):                  # A call might not have a callback signal, but if it does,                  # we add it to the chain                  if cmd[2] is not None: -                    d.addCallbacks(self._signal_back, log.err, cmd[2]) -                d.addCallbacks(self._done_action, log.err, +                    d.addCallbacks(self._signal_back, logger.error, cmd[2]) +                d.addCallbacks(self._done_action, logger.error,                                 callbackKeywords={"d": d}) -                d.addErrback(log.err) +                d.addErrback(logger.error)                  self._ongoing_defers.append(d)          except Empty:              # If it's just empty we don't have anything to do.              pass          except defer.CancelledError:              logger.debug("defer cancelled somewhere (CancelledError).") -        except Exception: +        except Exception as e:              # But we log the rest -            log.err() +            logger.exception("Unexpected exception: {0!r}".format(e))      def _done_action(self, _, d):          """ @@ -1044,7 +1603,7 @@ class Backend(object):      # this in two processes, the methods bellow can be changed to      # send_multipart and this backend class will be really simple. -    def setup_provider(self, provider): +    def provider_setup(self, provider):          """          Initiate the setup for a provider. @@ -1060,7 +1619,7 @@ class Backend(object):          """          self._call_queue.put(("provider", "setup_provider", None, provider)) -    def cancel_setup_provider(self): +    def provider_cancel_setup(self):          """          Cancel the ongoing setup provider (if any).          """ @@ -1081,7 +1640,48 @@ class Backend(object):          """          self._call_queue.put(("provider", "bootstrap", None, provider)) -    def register_user(self, provider, username, password): +    def provider_get_supported_services(self, domain): +        """ +        Signal a list of supported services provided by the given provider. + +        :param domain: the provider to get the services from. +        :type domain: str + +        Signals: +            prov_get_supported_services -> list of unicode +        """ +        self._call_queue.put(("provider", "get_supported_services", None, +                              domain)) + +    def provider_get_all_services(self, providers): +        """ +        Signal a list of services provided by all the configured providers. + +        :param providers: the list of providers to get the services. +        :type providers: list + +        Signals: +            prov_get_all_services -> list of unicode +        """ +        self._call_queue.put(("provider", "get_all_services", None, +                              providers)) + +    def provider_get_details(self, domain, lang): +        """ +        Signal a ProviderConfigLight object with the current ProviderConfig +        settings. + +        :param domain: the domain name of the provider. +        :type domain: str +        :param lang: the language to use for localized strings. +        :type lang: str + +        Signals: +            prov_get_details -> ProviderConfigLight +        """ +        self._call_queue.put(("provider", "get_details", None, domain, lang)) + +    def user_register(self, provider, username, password):          """          Register a user using the domain and password given as parameters. @@ -1100,7 +1700,7 @@ class Backend(object):          self._call_queue.put(("register", "register_user", None, provider,                                username, password)) -    def setup_eip(self, provider, skip_network=False): +    def eip_setup(self, provider, skip_network=False):          """          Initiate the setup for a provider @@ -1118,13 +1718,13 @@ class Backend(object):          self._call_queue.put(("eip", "setup_eip", None, provider,                                skip_network)) -    def cancel_setup_eip(self): +    def eip_cancel_setup(self):          """          Cancel the ongoing setup EIP (if any).          """          self._call_queue.put(("eip", "cancel_setup_eip", None)) -    def start_eip(self): +    def eip_start(self, restart=False):          """          Start the EIP service. @@ -1145,19 +1745,25 @@ class Backend(object):              eip_state_changed -> str              eip_status_changed -> tuple of str (download, upload)              eip_vpn_launcher_exception + +        :param restart: whether is is a restart. +        :type restart: bool          """ -        self._call_queue.put(("eip", "start", None)) +        self._call_queue.put(("eip", "start", None, restart)) -    def stop_eip(self, shutdown=False): +    def eip_stop(self, shutdown=False, restart=False, failed=False):          """          Stop the EIP service. -        :param shutdown: +        :param shutdown: whether this is the final shutdown.          :type shutdown: bool + +        :param restart: whether this is part of a restart. +        :type restart: bool          """ -        self._call_queue.put(("eip", "stop", None, shutdown)) +        self._call_queue.put(("eip", "stop", None, shutdown, restart)) -    def terminate_eip(self): +    def eip_terminate(self):          """          Terminate the EIP service, not necessarily in a nice way.          """ @@ -1209,7 +1815,13 @@ class Backend(object):          self._call_queue.put(("eip", "can_start",                                None, domain)) -    def login(self, provider, username, password): +    def tear_fw_down(self): +        """ +        Signal the need to tear the fw down. +        """ +        self._call_queue.put(("eip", "tear_fw_down", None)) + +    def user_login(self, provider, username, password):          """          Execute the whole authentication process for a user @@ -1231,7 +1843,7 @@ class Backend(object):          self._call_queue.put(("authenticate", "login", None, provider,                                username, password)) -    def logout(self): +    def user_logout(self):          """          Log out the current session. @@ -1242,13 +1854,13 @@ class Backend(object):          """          self._call_queue.put(("authenticate", "logout", None)) -    def cancel_login(self): +    def user_cancel_login(self):          """          Cancel the ongoing login (if any).          """          self._call_queue.put(("authenticate", "cancel_login", None)) -    def change_password(self, current_password, new_password): +    def user_change_password(self, current_password, new_password):          """          Change the user's password. @@ -1266,7 +1878,23 @@ class Backend(object):          self._call_queue.put(("authenticate", "change_password", None,                                current_password, new_password)) -    def get_logged_in_status(self): +    def soledad_change_password(self, new_password): +        """ +        Change the database's password. + +        :param new_password: the new password for the user. +        :type new_password: unicode + +        Signals: +            srp_not_logged_in_error +            srp_password_change_ok +            srp_password_change_badpw +            srp_password_change_error +        """ +        self._call_queue.put(("soledad", "change_password", None, +                              new_password)) + +    def user_get_logged_in_status(self):          """          Signal if the user is currently logged in or not. @@ -1276,10 +1904,126 @@ class Backend(object):          """          self._call_queue.put(("authenticate", "get_logged_in_status", None)) -    ########################################################################### -    # XXX HACK: this section is meant to be a place to hold methods and -    # variables needed in the meantime while we migrate all to the backend. +    def soledad_bootstrap(self, username, domain, password): +        """ +        Bootstrap the soledad database. + +        :param username: the user name +        :type username: unicode +        :param domain: the domain that we are using. +        :type domain: unicode +        :param password: the password for the username +        :type password: unicode + +        Signals: +            soledad_bootstrap_finished +            soledad_bootstrap_failed +            soledad_invalid_auth_token +        """ +        self._call_queue.put(("soledad", "bootstrap", None, +                              username, domain, password)) + +    def soledad_load_offline(self, username, password, uuid): +        """ +        Load the soledad database in offline mode. + +        :param username: full user id (user@provider) +        :type username: str or unicode +        :param password: the soledad passphrase +        :type password: unicode +        :param uuid: the user uuid +        :type uuid: str or unicode + +        Signals: +        """ +        self._call_queue.put(("soledad", "load_offline", None, +                              username, password, uuid)) + +    def soledad_cancel_bootstrap(self): +        """ +        Cancel the ongoing soledad bootstrapping process (if any). +        """ +        self._call_queue.put(("soledad", "cancel_bootstrap", None)) + +    def soledad_close(self): +        """ +        Close soledad database. +        """ +        self._call_queue.put(("soledad", "close", None)) + +    def keymanager_list_keys(self): +        """ +        Signal a list of public keys locally stored. + +        Signals: +            keymanager_keys_list -> list +        """ +        self._call_queue.put(("keymanager", "list_keys", None)) + +    def keymanager_export_keys(self, username, filename): +        """ +        Export the given username's keys to a file. + +        :param username: the username whos keys we need to export. +        :type username: str +        :param filename: the name of the file where we want to save the keys. +        :type filename: str + +        Signals: +            keymanager_export_ok +            keymanager_export_error +        """ +        self._call_queue.put(("keymanager", "export_keys", None, +                              username, filename)) -    def get_provider_config(self): -        provider_config = self._components["provider"]._provider_config -        return provider_config +    def keymanager_get_key_details(self, username): +        """ +        Signal the given username's key details. + +        :param username: the username whos keys we need to get details. +        :type username: str + +        Signals: +            keymanager_key_details +        """ +        self._call_queue.put(("keymanager", "get_key_details", None, username)) + +    def smtp_start_service(self, full_user_id, download_if_needed=False): +        """ +        Start the SMTP service. + +        :param full_user_id: user id, in the form "user@provider" +        :type full_user_id: str +        :param download_if_needed: True if it should check for mtime +                                   for the file +        :type download_if_needed: bool +        """ +        self._call_queue.put(("mail", "start_smtp_service", None, +                              full_user_id, download_if_needed)) + +    def imap_start_service(self, full_user_id, offline=False): +        """ +        Start the IMAP service. + +        :param full_user_id: user id, in the form "user@provider" +        :type full_user_id: str +        :param offline: whether imap should start in offline mode or not. +        :type offline: bool +        """ +        self._call_queue.put(("mail", "start_imap_service", None, +                              full_user_id, offline)) + +    def smtp_stop_service(self): +        """ +        Stop the SMTP service. +        """ +        self._call_queue.put(("mail", "stop_smtp_service", None)) + +    def imap_stop_service(self): +        """ +        Stop imap service. + +        Signals: +            imap_stopped +        """ +        self._call_queue.put(("mail", "stop_imap_service", None)) diff --git a/src/leap/bitmask/backend_app.py b/src/leap/bitmask/backend_app.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/leap/bitmask/backend_app.py diff --git a/src/leap/bitmask/config/flags.py b/src/leap/bitmask/config/flags.py index 6b70659d..2f3fdde4 100644 --- a/src/leap/bitmask/config/flags.py +++ b/src/leap/bitmask/config/flags.py @@ -55,3 +55,5 @@ OPENVPN_VERBOSITY = 1  # Skip the checks in the wizard, use for testing purposes only!  SKIP_WIZARD_CHECKS = False + +CURRENT_VPN_COUNTRY = None diff --git a/src/leap/bitmask/config/providerconfig.py b/src/leap/bitmask/config/providerconfig.py index 2ebe05ce..cf31b3b2 100644 --- a/src/leap/bitmask/config/providerconfig.py +++ b/src/leap/bitmask/config/providerconfig.py @@ -38,6 +38,35 @@ class MissingCACert(Exception):      pass +class ProviderConfigLight(object): +    """ +    A light config object to hold some provider settings needed by the GUI. +    """ +    def __init__(self): +        """ +        Define the public attributes. +        """ +        self.domain = "" +        self.name = "" +        self.description = "" +        self.enrollment_policy = "" +        self.services = [] + +    @property +    def services_string(self): +        """ +        Return a comma separated list of serices provided by this provider. + +        :rtype: str +        """ +        services = [] +        for service in self.services: +            services.append(get_service_display_name(service)) + +        services_str = ", ".join(services) +        return services_str + +  class ProviderConfig(BaseConfig):      """      Provider configuration abstraction class @@ -45,6 +74,29 @@ class ProviderConfig(BaseConfig):      def __init__(self):          BaseConfig.__init__(self) +    def get_light_config(self, domain, lang=None): +        """ +        Return a ProviderConfigLight object with the data for the loaded +        object. + +        :param domain: the domain name of the provider. +        :type domain: str +        :param lang: the language to use for localized strings. +        :type lang: str + +        :rtype: ProviderConfigLight or None if the ProviderConfig isn't loaded. +        """ +        config = self.get_provider_config(domain) +        details = ProviderConfigLight() + +        details.domain = config.get_domain() +        details.name = config.get_name(lang=lang) +        details.description = config.get_description(lang=lang) +        details.enrollment_policy = config.get_enrollment_policy() +        details.services = config.get_services() + +        return details +      @classmethod      def get_provider_config(self, domain):          """ @@ -144,18 +196,6 @@ class ProviderConfig(BaseConfig):          services = self._safe_get_value("services")          return services -    def get_services_string(self): -        """ -        Returns a string with the available services in the current -        provider, ready to be shown to the user. -        """ -        services = [] -        for service in self.get_services(): -            services.append(get_service_display_name(service)) - -        services_str = ", ".join(services) -        return services_str -      def get_ca_cert_path(self, about_to_download=False):          """          Returns the path to the certificate for the current provider. @@ -199,39 +239,3 @@ class ProviderConfig(BaseConfig):          :rtype: bool          """          return "mx" in self.get_services() - - -if __name__ == "__main__": -    logger = logging.getLogger(name='leap') -    logger.setLevel(logging.DEBUG) -    console = logging.StreamHandler() -    console.setLevel(logging.DEBUG) -    formatter = logging.Formatter( -        '%(asctime)s ' -        '- %(name)s - %(levelname)s - %(message)s') -    console.setFormatter(formatter) -    logger.addHandler(console) - -    provider = ProviderConfig() - -    try: -        provider.get_api_version() -    except Exception as e: -        assert isinstance(e, AssertionError), "Expected an assert" -        print "Safe value getting is working" - -    # standalone minitest -    #if provider.load("provider_bad.json"): -    if provider.load("leap/providers/bitmask.net/provider.json"): -        print provider.get_api_version() -        print provider.get_ca_cert_fingerprint() -        print provider.get_ca_cert_uri() -        print provider.get_default_language() -        print provider.get_description() -        print provider.get_description(lang="asd") -        print provider.get_domain() -        print provider.get_enrollment_policy() -        print provider.get_languages() -        print provider.get_name() -        print provider.get_services() -        print provider.get_services_string() diff --git a/src/leap/bitmask/frontend_app.py b/src/leap/bitmask/frontend_app.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/leap/bitmask/frontend_app.py diff --git a/src/leap/bitmask/gui/advanced_key_management.py b/src/leap/bitmask/gui/advanced_key_management.py index be6b4410..b3a4ed8e 100644 --- a/src/leap/bitmask/gui/advanced_key_management.py +++ b/src/leap/bitmask/gui/advanced_key_management.py @@ -19,11 +19,8 @@ Advanced Key Management  """  import logging -from PySide import QtGui -from zope.proxy import sameProxiedObjects +from PySide import QtCore, QtGui -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 @@ -34,7 +31,7 @@ class AdvancedKeyManagement(QtGui.QDialog):      """      Advanced Key Management      """ -    def __init__(self, parent, has_mx, user, keymanager, soledad): +    def __init__(self, parent, has_mx, user, backend, soledad_started):          """          :param parent: parent object of AdvancedKeyManagement.          :parent type: QWidget @@ -43,10 +40,10 @@ class AdvancedKeyManagement(QtGui.QDialog):          :type has_mx: bool          :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 +        :param backend: Backend being used +        :type backend: Backend +        :param soledad_started: whether soledad has started or not +        :type soledad_started: bool          """          QtGui.QDialog.__init__(self, parent) @@ -56,7 +53,6 @@ class AdvancedKeyManagement(QtGui.QDialog):          # XXX: Temporarily disable the key import.          self.ui.pbImportKeys.setVisible(False) -        # if Soledad is not started yet          if not has_mx:              msg = self.tr("The provider that you are using "                            "does not support {0}.") @@ -64,8 +60,7 @@ class AdvancedKeyManagement(QtGui.QDialog):              self._disable_ui(msg)              return -        # if Soledad is not started yet -        if sameProxiedObjects(soledad, None): +        if not soledad_started:              msg = self.tr("To use this, you need to enable/start {0}.")              msg = msg.format(get_service_display_name(MX_SERVICE))              self._disable_ui(msg) @@ -78,17 +73,12 @@ class AdvancedKeyManagement(QtGui.QDialog):          #         "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) +        self._user = user +        self._backend = backend +        self._backend_connect()          # 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) @@ -98,7 +88,15 @@ class AdvancedKeyManagement(QtGui.QDialog):          self.ui.twPublicKeys.horizontalHeader().setResizeMode(              0, QtGui.QHeaderView.Stretch) -        self._list_keys() +        self._backend.keymanager_get_key_details(user) +        self._backend.keymanager_list_keys() + +    def _keymanager_key_details(self, details): +        """ +        Set the current user's key details into the gui. +        """ +        self.ui.leKeyID.setText(details[0]) +        self.ui.leFingerprint.setText(details[1])      def _disable_ui(self, msg):          """ @@ -117,53 +115,11 @@ class AdvancedKeyManagement(QtGui.QDialog):          Imports the user's key pair.          Those keys need to be ascii armored.          """ -        fileName, filtr = QtGui.QFileDialog.getOpenFileName( +        file_name, 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 - +        if file_name:              question = self.tr("Are you sure that you want to replace "                                 "the current key pair whith the imported?")              res = QtGui.QMessageBox.question( @@ -171,61 +127,152 @@ class AdvancedKeyManagement(QtGui.QDialog):                  QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,                  QtGui.QMessageBox.No)  # default No -            if res == QtGui.QMessageBox.No: -                return +            if res == QtGui.QMessageBox.Yes: +                self._backend.keymanager_import_keys(self._user, file_name) +        else: +            logger.debug('Import canceled by the user.') -            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) +    @QtCore.Slot() +    def _keymanager_import_ok(self): +        """ +        TRIGGERS: +            Signaler.keymanager_import_ok -            logger.debug('Import ok') +        Notify the user that the key import went OK. +        """ +        QtGui.QMessageBox.information( +            self, self.tr("Import Successful"), +            self.tr("The key pair was imported successfully.")) -            QtGui.QMessageBox.information( -                self, self.tr("Import Successful"), -                self.tr("The key pair was imported successfully.")) -        else: -            logger.debug('Import canceled by the user.') +    @QtCore.Slot() +    def _import_ioerror(self): +        """ +        TRIGGERS: +            Signaler.keymanager_import_ioerror + +        Notify the user that the key import had an IOError problem. +        """ +        QtGui.QMessageBox.critical( +            self, self.tr("Input/Output error"), +            self.tr("There was an error accessing the file.\n" +                    "Import canceled.")) + +    @QtCore.Slot() +    def _import_datamismatch(self): +        """ +        TRIGGERS: +            Signaler.keymanager_import_datamismatch + +        Notify the user that the key import had an data mismatch problem. +        """ +        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.")) + +    @QtCore.Slot() +    def _import_missingkey(self): +        """ +        TRIGGERS: +            Signaler.keymanager_import_missingkey + +        Notify the user that the key import failed due a missing key. +        """ +        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.")) + +    @QtCore.Slot() +    def _import_addressmismatch(self): +        """ +        TRIGGERS: +            Signaler.keymanager_import_addressmismatch + +        Notify the user that the key import failed due an address mismatch. +        """ +        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."))      def _export_keys(self):          """          Exports the user's key pair.          """ -        fileName, filtr = QtGui.QFileDialog.getSaveFileName( +        file_name, 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 +        if file_name: +            self._backend.keymanager_export_keys(self._user, file_name)          else:              logger.debug('Export canceled by the user.') -    def _list_keys(self): +    @QtCore.Slot() +    def _keymanager_export_ok(self): +        """ +        TRIGGERS: +            Signaler.keymanager_export_ok + +        Notify the user that the key export went OK.          """ -        Loads all the public keys stored in the local db to the keys table. +        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.")) + +    @QtCore.Slot() +    def _keymanager_export_error(self): +        """ +        TRIGGERS: +            Signaler.keymanager_export_error + +        Notify the user that the key export didn't go well. +        """ +        QtGui.QMessageBox.critical( +            self, self.tr("Input/Output error"), +            self.tr("There was an error accessing the file.\n" +                    "Export canceled.")) + +    @QtCore.Slot() +    def _keymanager_keys_list(self, keys):          """ -        keys = self._keymanager.get_all_keys_in_local_db() +        TRIGGERS: +            Signaler.keymanager_keys_list +        Load the keys given as parameter in the table. + +        :param keys: the list of keys to load. +        :type keys: list +        """          keys_table = self.ui.twPublicKeys +          for key in keys:              row = keys_table.rowCount()              keys_table.insertRow(row)              keys_table.setItem(row, 0, QtGui.QTableWidgetItem(key.address))              keys_table.setItem(row, 1, QtGui.QTableWidgetItem(key.key_id)) + +    def _backend_connect(self): +        """ +        Connect to backend signals. +        """ +        sig = self._backend.signaler + +        sig.keymanager_export_ok.connect(self._keymanager_export_ok) +        sig.keymanager_export_error.connect(self._keymanager_export_error) +        sig.keymanager_keys_list.connect(self._keymanager_keys_list) + +        sig.keymanager_key_details.connect(self._keymanager_key_details) + +        sig.keymanager_import_ok.connect(self._keymanager_import_ok) + +        sig.keymanager_import_ioerror.connect(self._import_ioerror) +        sig.keymanager_import_datamismatch.connect(self._import_datamismatch) +        sig.keymanager_import_missingkey.connect(self._import_missingkey) +        sig.keymanager_import_addressmismatch.connect( +            self._import_addressmismatch) diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py index ca28b8bf..8b9f2d44 100644 --- a/src/leap/bitmask/gui/eip_status.py +++ b/src/leap/bitmask/gui/eip_status.py @@ -24,7 +24,7 @@ from functools import partial  from PySide import QtCore, QtGui -from leap.bitmask.services.eip.connection import EIPConnection +from leap.bitmask.config import flags  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 @@ -32,6 +32,7 @@ from leap.common.check import leap_assert_type  from ui_eip_status import Ui_EIPStatus +QtDelayedCall = QtCore.QTimer.singleShot  logger = logging.getLogger(__name__) @@ -43,9 +44,14 @@ class EIPStatusWidget(QtGui.QWidget):      RATE_STR = "%1.2f KB/s"      TOTAL_STR = "%1.2f Kb" -    eip_connection_connected = QtCore.Signal() +    def __init__(self, parent=None, eip_conductor=None): +        """ +        :param parent: the parent of the widget. +        :type parent: QObject -    def __init__(self, parent=None): +        :param eip_conductor: an EIPConductor object. +        :type eip_conductor: EIPConductor +        """          QtGui.QWidget.__init__(self, parent)          self._systray = None @@ -54,13 +60,17 @@ class EIPStatusWidget(QtGui.QWidget):          self.ui = Ui_EIPStatus()          self.ui.setupUi(self) -        self.eipconnection = EIPConnection() +        self.eip_conductor = eip_conductor +        self.eipconnection = eip_conductor.eip_connection          # set systray tooltip status          self._eip_status = ""          self._service_name = get_service_display_name(EIP_SERVICE)          self.ui.eip_bandwidth.hide() +        self.hide_fw_down_button() +        self.ui.btnFwDown.clicked.connect( +            self._on_fw_down_button_clicked)          # Set the EIP status icons          self.CONNECTING_ICON = None @@ -75,11 +85,43 @@ class EIPStatusWidget(QtGui.QWidget):          self._make_status_clickable()          self._provider = "" +        self.is_restart = False +        self.is_cold_start = True          # Action for the systray          self._eip_disabled_action = QtGui.QAction(              "{0} is {1}".format(self._service_name, self.tr("disabled")), self) +    def connect_backend_signals(self): +        """ +        Connect backend signals. +        """ +        signaler = self.eip_conductor._backend.signaler + +        signaler.eip_openvpn_already_running.connect( +            self._on_eip_openvpn_already_running) +        signaler.eip_alien_openvpn_already_running.connect( +            self._on_eip_alien_openvpn_already_running) +        signaler.eip_openvpn_not_found_error.connect( +            self._on_eip_openvpn_not_found_error) +        signaler.eip_vpn_launcher_exception.connect( +            self._on_eip_vpn_launcher_exception) +        signaler.eip_no_polkit_agent_error.connect( +            self._on_eip_no_polkit_agent_error) +        signaler.eip_connection_aborted.connect( +            self._on_eip_connection_aborted) +        signaler.eip_no_pkexec_error.connect(self._on_eip_no_pkexec_error) +        signaler.eip_no_tun_kext_error.connect(self._on_eip_no_tun_kext_error) + +        signaler.eip_state_changed.connect(self.update_vpn_state) +        signaler.eip_status_changed.connect(self.update_vpn_status) + +        # XXX we cannot connect this signal now because +        # it interferes with the proper notifications during restarts +        # without available network. +        #signaler.eip_network_unreachable.connect( +            #self._on_eip_network_unreachable) +      def _make_status_clickable(self):          """          Makes upload and download figures clickable. @@ -208,7 +250,7 @@ class EIPStatusWidget(QtGui.QWidget):      def set_action_eip_startstop(self, action_eip_startstop):          """ -        Sets the action_eip_startstop to use. +        Set the action_eip_startstop to use.          :param action_eip_startstop: action_eip_status to be used          :type action_eip_startstop: QtGui.QAction @@ -238,9 +280,11 @@ class EIPStatusWidget(QtGui.QWidget):      def eip_pre_up(self):          """          Triggered when the app activates eip. -        Hides the status box and disables the start/stop button. +        Disables the start/stop button.          """          self.set_startstop_enabled(False) +        msg = self.tr("Encrypted Internet is starting") +        self.set_eip_message(msg)      @QtCore.Slot()      def disable_eip_start(self): @@ -248,7 +292,7 @@ class EIPStatusWidget(QtGui.QWidget):          Triggered when a default provider_config has not been found.          Disables the start button and adds instructions to the user.          """ -        #logger.debug('Hiding EIP start button') +        logger.debug('Hiding EIP start button')          # you might be tempted to change this for a .setEnabled(False).          # it won't work. it's under the claws of the state machine.          # probably the best thing would be to make a conditional @@ -282,10 +326,19 @@ class EIPStatusWidget(QtGui.QWidget):          if self.isVisible():              self._eip_status_menu.menuAction().setVisible(True) -    # XXX disable (later) -------------------------- +    def set_eip_message(self, message): +        """ +        Set the EIP Widget main message. + +        :param message: the message to set in the widget +        :type message: str or unicode +        """ +        self.ui.lblEIPMessage.setText(message) +        self.ui.lblEIPMessage.show() +      def set_eip_status(self, status, error=False):          """ -        Sets the status label at the VPN stage to status +        Set the status label at the VPN stage to status.          :param status: status message          :type status: str or unicode @@ -326,29 +379,80 @@ class EIPStatusWidget(QtGui.QWidget):          Sets the state of the widget to how it should look after EIP          has started          """ -        self.ui.btnEipStartStop.setText(self.tr("Turn OFF"))          self.ui.btnEipStartStop.disconnect(self)          self.ui.btnEipStartStop.clicked.connect(              self.eipconnection.qtsigs.do_connect_signal) -    # XXX disable ----------------------------- -    def eip_stopped(self): +    def hide_fw_down_button(self): +        """ +        Hide firewall-down button. +        """ +        self.ui.btnFwDown.hide() + +    def show_fw_down_button(self): +        """ +        Enable firewall-down button.          """ +        retry_msg = self.tr("Retry") +        self.ui.btnEipStartStop.setText(retry_msg) +        self._action_eip_startstop.setText(retry_msg) +        self.ui.btnFwDown.show() + +    def _on_fw_down_button_clicked(self): +        """ +        Raise a signal for tearing down the firewall, and hide the button +        afterwards. +        """ +        self.eip_conductor._backend.tear_fw_down() +        QtDelayedCall(50, self.hide_fw_down_button) + +        # XXX do actual check +        msg = "Traffic is being routed in the clear." +        self.ui.btnEipStartStop.setText(self.tr("Turn ON")) +        self.set_eip_message(msg) +        self.set_eip_status("") + +    @QtCore.Slot(dict) +    def eip_stopped(self, restart=False, failed=False): +        """ +        TRIGGERS: +            EIPConductor.qtsigs.disconnected_signal +          Sets the state of the widget to how it should look after EIP          has stopped          """ -        # XXX should connect this to EIPConnection.disconnected_signal +        self.set_country_code("")          self._reset_traffic_rates() -        # XXX disable ----------------------------- -        self.ui.btnEipStartStop.setText(self.tr("Turn ON")) -        self.ui.btnEipStartStop.disconnect(self) -        self.ui.btnEipStartStop.clicked.connect( -            self.eipconnection.qtsigs.do_disconnect_signal) -          self.ui.eip_bandwidth.hide() -        self.ui.lblEIPMessage.setText( -            self.tr("Traffic is being routed in the clear")) + +        # This is assuming the firewall works correctly, but we should test fw +        # status positively. +        # Or better call it from the conductor... + +        clear_traffic = self.tr("Traffic is being routed in the clear.") +        unreachable_net = self.tr("Network is unreachable.") +        failed_msg = self.tr("Error connecting") + +        if restart: +            msg = unreachable_net +        elif failed: +            msg = failed_msg +        else: +            msg = clear_traffic +        self.set_eip_message(msg) +        self.ui.lblEIPStatus.show() +        self.show() + +    def eip_failed_to_connect(self): +        """ +        Update EIP messages with error during (re)connection. +        """ +        msg = self.tr("Error connecting.") +        self.ui.lblEIPMessage.setText(msg)          self.ui.lblEIPStatus.show() +        self.set_eip_status(self.tr("Bitmask is blocking " +                                    "unencrypted traffic.")) +        self.show_fw_down_button()      @QtCore.Slot(dict)      def update_vpn_status(self, data=None): @@ -407,11 +511,20 @@ class EIPStatusWidget(QtGui.QWidget):              self.ui.lblEIPStatus.hide()              # XXX should be handled by the state machine too. -            self.eip_connection_connected.emit() +            # --- is this currently being sent? +            self.eipconnection.qtsigs.connected_signal.emit() +            self._on_eip_connected() +            self.is_cold_start = False          # XXX should lookup vpn_state map in EIPConnection          elif vpn_state == "AUTH":              self.set_eip_status(self.tr("Authenticating...")) +            # we wipe up any previous error info in the EIP message +            # when we detect vpn authentication is happening +            msg = self.tr("Encrypted Internet is starting") +            self.set_eip_message(msg) +            # on the first-run path, we hadn't showed the button yet. +            self.eip_button.show()          elif vpn_state == "GET_CONFIG":              self.set_eip_status(self.tr("Retrieving configuration..."))          elif vpn_state == "WAIT": @@ -423,10 +536,11 @@ class EIPStatusWidget(QtGui.QWidget):          elif vpn_state == "ALREADYRUNNING":              # Put the following calls in Qt's event queue, otherwise              # the UI won't update properly -            QtCore.QTimer.singleShot( -                0, self.eipconnection.qtsigs.do_disconnect_signal) +            #self.send_disconnect_signal() +            QtDelayedCall( +                0, self.eipconnection.qtsigns.do_disconnect_signal.emit)              msg = self.tr("Unable to start VPN, it's already running.") -            QtCore.QTimer.singleShot(0, partial(self.set_eip_status, msg)) +            QtDelayedCall(0, partial(self.set_eip_status, msg))          else:              self.set_eip_status(vpn_state) @@ -468,5 +582,152 @@ class EIPStatusWidget(QtGui.QWidget):      def set_provider(self, provider):          self._provider = provider +          self.ui.lblEIPMessage.setText( -            self.tr("Route traffic through: {0}").format(self._provider)) +            self.tr("Routing traffic through: <b>{0}</b>").format( +                provider)) + +        ccode = flags.CURRENT_VPN_COUNTRY +        if ccode is not None: +            self.set_country_code(ccode) + +    def set_country_code(self, code): +        """ +        Set the pixmap of the given country code + +        :param code: the country code +        :type code: str +        """ +        if code is not None and len(code) == 2: +            img = ":/images/countries/%s.png" % (code.lower(),) +        else: +            img = None +        cc = self.ui.lblGatewayCountryCode +        cc.setPixmap(QtGui.QPixmap(img)) +        cc.setToolTip(code) + +    def aborted(self): +        """ +        Notify the state machine that EIP was aborted for some reason. +        """ +        # signal connection_aborted to state machine: +        qtsigs = self.eipconnection.qtsigs +        qtsigs.connection_aborted_signal.emit() + +    # +    # Slots for signals +    # + +    @QtCore.Slot() +    def _on_eip_connection_aborted(self): +        """ +        TRIGGERS: +            Signaler.eip_connection_aborted +        """ +        # TODO this name is very misleading, since there's a generic signal +        # that's called connection_aborted / connection_died... +        # should rename to something more specific about missing config. +        logger.error("Tried to start EIP but cannot find any " +                     "available provider!") + +        eip_status_label = self.tr("Could not load {0} configuration.") +        eip_status_label = eip_status_label.format( +            self.eip_conductor.eip_name) +        self.set_eip_status(eip_status_label, error=True) + +        self.aborted() + +    def _on_eip_openvpn_already_running(self): +        self.set_eip_status( +            self.tr("Another openvpn instance is already running, and " +                    "could not be stopped."), +            error=True) +        self.set_eipstatus_off() + +        self.aborted() + +    def _on_eip_alien_openvpn_already_running(self): +        self.set_eip_status( +            self.tr("Another openvpn instance is already running, and " +                    "could not be stopped because it was not launched by " +                    "Bitmask. Please stop it and try again."), +            error=True) +        self.set_eipstatus_off() + +        self.aborted() + +    def _on_eip_openvpn_not_found_error(self): +        self.set_eip_status( +            self.tr("We could not find openvpn binary."), +            error=True) +        self.set_eipstatus_off() + +        self.aborted() + +    def _on_eip_vpn_launcher_exception(self): +        # XXX We should implement again translatable exceptions so +        # we can pass a translatable string to the panel (usermessage attr) +        self.set_eip_status("VPN Launcher error.", error=True) +        self.set_eipstatus_off() + +        self.aborted() + +    def _on_eip_no_polkit_agent_error(self): +        self.set_eip_status( +            # XXX this should change to polkit-kde where +            # applicable. +            self.tr("We could not find any authentication agent in your " +                    "system.<br/>Make sure you have" +                    "<b>polkit-gnome-authentication-agent-1</b> running and" +                    "try again."), +            error=True) +        self.set_eipstatus_off() + +        self.aborted() + +    def _on_eip_no_pkexec_error(self): +        self.set_eip_status( +            self.tr("We could not find <b>pkexec</b> in your system."), +            error=True) +        self.set_eipstatus_off() + +        self.aborted() + +    def _on_eip_no_tun_kext_error(self): +        self.set_eip_status( +            self.tr("{0} cannot be started because the tuntap extension is " +                    "not installed properly in your " +                    "system.").format(self.eip_conductor.eip_name)) +        self.set_eipstatus_off() + +        self.aborted() + +    def _on_eip_connected(self): +        """ +        Reconnect the disconnecting signal when we are just connected, +        so that we restore the disconnecting -> stop behaviour. +        """ +        self.eip_conductor.reconnect_stop_signal() + +    @QtCore.Slot() +    def _on_eip_network_unreachable(self): +        """ +        TRIGGERS: +            self._eip_connection.qtsigs.network_unreachable + +        Displays a "network unreachable" error in the EIP status panel. +        """ +        self.set_eip_status(self.tr("Network is unreachable"), +                            error=True) +        self.set_eip_status_icon("error") + +    def set_eipstatus_off(self, error=True): +    # XXX this should be handled by the state machine. +        """ +        Sets eip status to off +        """ +        self.set_eip_status("", error=error) +        self.set_eip_status_icon("error") + +import eipstatus_rc +assert(eipstatus_rc) diff --git a/src/leap/bitmask/gui/loggerwindow.py b/src/leap/bitmask/gui/loggerwindow.py index f19b172f..3a8354b1 100644 --- a/src/leap/bitmask/gui/loggerwindow.py +++ b/src/leap/bitmask/gui/loggerwindow.py @@ -27,7 +27,7 @@ from twisted.internet import threads  from ui_loggerwindow import Ui_LoggerWindow  from leap.bitmask.util.constants import PASTEBIN_API_DEV_KEY -from leap.bitmask.util.leap_log_handler import LeapLogHandler +from leap.bitmask.logs.leap_log_handler import LeapLogHandler  from leap.bitmask.util import pastebin  from leap.common.check import leap_assert, leap_assert_type diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py index ac7ad878..f66e71d9 100644 --- a/src/leap/bitmask/gui/login.py +++ b/src/leap/bitmask/gui/login.py @@ -24,6 +24,7 @@ from ui_login import Ui_LoginWidget  from leap.bitmask.config import flags  from leap.bitmask.util import make_address +from leap.bitmask.util.credentials import USERNAME_REGEX  from leap.bitmask.util.keyring_helpers import has_keyring  from leap.bitmask.util.keyring_helpers import get_keyring  from leap.common.check import leap_assert_type @@ -48,8 +49,6 @@ class LoginWidget(QtGui.QWidget):      MAX_STATUS_WIDTH = 40 -    BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" -      # Keyring      KEYRING_KEY = "bitmask" @@ -87,7 +86,7 @@ class LoginWidget(QtGui.QWidget):          self.ui.btnLogout.clicked.connect(              self.logout) -        username_re = QtCore.QRegExp(self.BARE_USERNAME_REGEX) +        username_re = QtCore.QRegExp(USERNAME_REGEX)          self.ui.lnUser.setValidator(              QtGui.QRegExpValidator(username_re, self)) diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py index d3346780..5caef745 100644 --- a/src/leap/bitmask/gui/mail_status.py +++ b/src/leap/bitmask/gui/mail_status.py @@ -188,7 +188,7 @@ class MailStatusWidget(QtGui.QWidget):      def set_soledad_failed(self):          """          TRIGGERS: -            SoledadBootstrapper.soledad_failed +            Signaler.soledad_bootstrap_failed          This method is called whenever soledad has a failure.          """ diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index e3848c46..3ef994b1 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -19,22 +19,17 @@ Main window for Bitmask.  """  import logging  import socket -import time -from threading import Condition  from datetime import datetime  from PySide import QtCore, QtGui -from zope.proxy import ProxyBase, setProxiedObject  from twisted.internet import reactor, threads  from leap.bitmask import __version__ as VERSION  from leap.bitmask import __version_hash__ as VERSION_HASH  from leap.bitmask.config import flags  from leap.bitmask.config.leapsettings import LeapSettings -from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.gui import statemachines  from leap.bitmask.gui.advanced_key_management import AdvancedKeyManagement  from leap.bitmask.gui.eip_preferenceswindow import EIPPreferencesWindow  from leap.bitmask.gui.eip_status import EIPStatusWidget @@ -45,30 +40,24 @@ from leap.bitmask.gui.preferenceswindow import PreferencesWindow  from leap.bitmask.gui.systray import SysTray  from leap.bitmask.gui.wizard import Wizard -from leap.bitmask import provider  from leap.bitmask.platform_init import IS_WIN, IS_MAC, IS_LINUX  from leap.bitmask.platform_init.initializers import init_platform  from leap.bitmask import backend -from leap.bitmask.services import get_service_display_name - +from leap.bitmask.services.eip import conductor as eip_conductor  from leap.bitmask.services.mail import conductor as mail_conductor  from leap.bitmask.services import EIP_SERVICE, MX_SERVICE -from leap.bitmask.services.eip.connection import EIPConnection -from leap.bitmask.services.soledad.soledadbootstrapper import \ -    SoledadBootstrapper  from leap.bitmask.util import make_address  from leap.bitmask.util.keyring_helpers import has_keyring -from leap.bitmask.util.leap_log_handler import LeapLogHandler +from leap.bitmask.logs.leap_log_handler import LeapLogHandler  if IS_WIN:      from leap.bitmask.platform_init.locks import WindowsLock      from leap.bitmask.platform_init.locks import raise_window_ack -from leap.common.check import leap_assert  from leap.common.events import register  from leap.common.events import events_pb2 as proto @@ -76,6 +65,7 @@ from leap.mail.imap.service.imap import IMAP_PORT  from ui_mainwindow import Ui_MainWindow +QtDelayedCall = QtCore.QTimer.singleShot  logger = logging.getLogger(__name__) @@ -89,17 +79,17 @@ class MainWindow(QtGui.QMainWindow):      new_updates = QtCore.Signal(object)      raise_window = QtCore.Signal([])      soledad_ready = QtCore.Signal([]) -    mail_client_logged_in = QtCore.Signal([])      logout = QtCore.Signal([]) +    all_services_stopped = QtCore.Signal()      # We use this flag to detect abnormal terminations      user_stopped_eip = False      # We give EIP some time to come up before starting soledad anyway -    EIP_TIMEOUT = 60000  # in milliseconds +    EIP_START_TIMEOUT = 60000  # in milliseconds -    # We give each service some time to come to a halt before forcing quit -    SERVICE_STOP_TIMEOUT = 20 +    # We give the services some time to a halt before forcing quit. +    SERVICES_STOP_TIMEOUT = 20      def __init__(self, quit_callback, bypass_checks=False, start_hidden=False):          """ @@ -125,9 +115,6 @@ class MainWindow(QtGui.QMainWindow):          register(signal=proto.RAISE_WINDOW,                   callback=self._on_raise_window_event,                   reqcbk=lambda req, resp: None)  # make rpc call async -        register(signal=proto.IMAP_CLIENT_LOGIN, -                 callback=self._on_mail_client_logged_in, -                 reqcbk=lambda req, resp: None)  # make rpc call async          # end register leap events ####################################          self._quit_callback = quit_callback @@ -142,11 +129,16 @@ class MainWindow(QtGui.QMainWindow):          self._settings = LeapSettings() +        # Login Widget          self._login_widget = LoginWidget(              self._settings,              self)          self.ui.loginLayout.addWidget(self._login_widget) +        # Mail Widget +        self._mail_status = MailStatusWidget(self) +        self.ui.mailLayout.addWidget(self._mail_status) +          # Qt Signal Connections #####################################          # TODO separate logic from ui signals. @@ -155,67 +147,45 @@ class MainWindow(QtGui.QMainWindow):          self._login_widget.show_wizard.connect(self._launch_wizard)          self._login_widget.logout.connect(self._logout) -        self._eip_status = EIPStatusWidget(self) -        self.ui.eipLayout.addWidget(self._eip_status) -        self._login_widget.logged_in_signal.connect( -            self._eip_status.enable_eip_start) -        self._login_widget.logged_in_signal.connect( -            self._enable_eip_start_action) +        # EIP Control redux ######################################### +        self._eip_conductor = eip_conductor.EIPConductor( +            self._settings, self._backend) +        self._eip_status = EIPStatusWidget(self, self._eip_conductor) -        self._mail_status = MailStatusWidget(self) -        self.ui.mailLayout.addWidget(self._mail_status) - -        self._eip_connection = EIPConnection() - -        # XXX this should be handled by EIP Conductor -        self._eip_connection.qtsigs.connecting_signal.connect( -            self._start_EIP) -        self._eip_connection.qtsigs.disconnecting_signal.connect( -            self._stop_eip) +        self.ui.eipLayout.addWidget(self._eip_status) +        self._eip_conductor.add_eip_widget(self._eip_status) -        self._eip_status.eip_connection_connected.connect( +        self._eip_conductor.connect_signals() +        self._eip_conductor.qtsigs.connected_signal.connect(              self._on_eip_connection_connected) -        self._eip_status.eip_connection_connected.connect( +        self._eip_conductor.qtsigs.connected_signal.connect(              self._maybe_run_soledad_setup_checks) +          self.offline_mode_bypass_login.connect(              self._maybe_run_soledad_setup_checks)          self.eip_needs_login.connect(self._eip_status.disable_eip_start)          self.eip_needs_login.connect(self._disable_eip_start_action) +        self._already_started_eip = False          self._trying_to_start_eip = False -        # This is loaded only once, there's a bug when doing that more -        # than once -        # XXX HACK!! But we need it as long as we are using -        # provider_config in here -        self._provider_config = self._backend.get_provider_config() - -        # Used for automatic start of EIP -        self._provisional_provider_config = ProviderConfig() -          self._already_started_eip = False -        self._already_started_soledad = False +        self._soledad_started = False          # This is created once we have a valid provider config          self._srp_auth = None          self._logged_user = None          self._logged_in_offline = False -        self._backend_connected_signals = {} -        self._backend_connect() +        # Set used to track the services being stopped and need wait. +        self._services_being_stopped = {} -        self._soledad_bootstrapper = SoledadBootstrapper() -        self._soledad_bootstrapper.download_config.connect( -            self._soledad_intermediate_stage) -        self._soledad_bootstrapper.gen_key.connect( -            self._soledad_bootstrapped_stage) -        self._soledad_bootstrapper.local_only_ready.connect( -            self._soledad_bootstrapped_stage) -        self._soledad_bootstrapper.soledad_invalid_auth_token.connect( -            self._mail_status.set_soledad_invalid_auth_token) -        self._soledad_bootstrapper.soledad_failed.connect( -            self._mail_status.set_soledad_failed) +        # timeout object used to trigger quit +        self._quit_timeout_callater = None + +        self._backend_connected_signals = [] +        self._backend_connect()          self.ui.action_preferences.triggered.connect(self._show_preferences)          self.ui.action_eip_preferences.triggered.connect( @@ -241,8 +211,7 @@ class MainWindow(QtGui.QMainWindow):          self._systray = None -        # XXX separate actions into a different -        # module. +        # XXX separate actions into a different module.          self._action_mail_status = QtGui.QAction(self.tr("Mail is OFF"), self)          self._mail_status.set_action_mail_status(self._action_mail_status) @@ -260,6 +229,8 @@ class MainWindow(QtGui.QMainWindow):          self._ui_mx_visible = True          self._ui_eip_visible = True +        self._provider_details = None +          # last minute UI manipulations          self._center_window() @@ -280,10 +251,6 @@ class MainWindow(QtGui.QMainWindow):          # XXX should connect to mail_conductor.start_mail_service instead          self.soledad_ready.connect(self._start_smtp_bootstrapping)          self.soledad_ready.connect(self._start_imap_service) -        self.mail_client_logged_in.connect(self._fetch_incoming_mail) -        self.logout.connect(self._stop_imap_service) -        self.logout.connect(self._stop_smtp_service) -          ################################# end Qt Signals connection ########          init_platform() @@ -296,18 +263,11 @@ class MainWindow(QtGui.QMainWindow):          self._bypass_checks = bypass_checks          self._start_hidden = start_hidden -        # We initialize Soledad and Keymanager instances as -        # transparent proxies, so we can pass the reference freely -        # around. -        self._soledad = ProxyBase(None) -        self._keymanager = ProxyBase(None) - -        self._soledad_defer = None - -        self._mail_conductor = mail_conductor.MailConductor( -            self._soledad, self._keymanager) +        self._mail_conductor = mail_conductor.MailConductor(self._backend)          self._mail_conductor.connect_mail_signals(self._mail_status) +        self.logout.connect(self._mail_conductor.stop_mail_services) +          # Eip machine is a public attribute where the state machine for          # the eip connection will be available to the different components.          # Remember that this will not live in the  +1600LOC mainwindow for @@ -315,20 +275,19 @@ class MainWindow(QtGui.QMainWindow):          # the EIPConductor or some other clever component that we will          # instantiate from here. -        self.eip_machine = None          # start event machines -        self.start_eip_machine() +        # TODO should encapsulate all actions into one object +        self._eip_conductor.start_eip_machine( +            action=self._action_eip_startstop)          self._mail_conductor.start_mail_machine() -        self._eip_name = get_service_display_name(EIP_SERVICE) -          if self._first_run():              self._wizard_firstrun = True              self._disconnect_and_untrack()              self._wizard = Wizard(backend=self._backend,                                    bypass_checks=bypass_checks)              # Give this window time to finish init and then show the wizard -            QtCore.QTimer.singleShot(1, self._launch_wizard) +            QtDelayedCall(1, self._launch_wizard)              self._wizard.accepted.connect(self._finish_init)              self._wizard.rejected.connect(self._rejected_wizard)          else: @@ -357,7 +316,7 @@ class MainWindow(QtGui.QMainWindow):          :param method: the method to call when the signal is triggered.          :type method: callable, Slot or Signal          """ -        self._backend_connected_signals[signal] = method +        self._backend_connected_signals.append((signal, method))          signal.connect(method)      def _backend_bad_call(self, data): @@ -370,97 +329,102 @@ class MainWindow(QtGui.QMainWindow):          logger.error("Bad call to the backend:")          logger.error(data) -    def _backend_connect(self): +    def _backend_connect(self, only_tracked=False):          """ -        Helper to connect to backend signals -        """ -        sig = self._backend.signaler +        Connect to backend signals. -        sig.backend_bad_call.connect(self._backend_bad_call) +        We track some signals in order to disconnect them on demand. +        For instance, in the wizard we need to connect to some signals that are +        already connected in the mainwindow, so to avoid conflicts we do: +            - disconnect signals needed in wizard (`_disconnect_and_untrack`) +            - use wizard +            - reconnect disconnected signals (we use the `only_tracked` param) -        self._connect_and_track(sig.prov_name_resolution, -                                self._intermediate_stage) -        self._connect_and_track(sig.prov_https_connection, -                                self._intermediate_stage) -        self._connect_and_track(sig.prov_download_ca_cert, -                                self._intermediate_stage) +        :param only_tracked: whether or not we should connect only the signals +                             that we are tracking to disconnect later. +        :type only_tracked: bool +        """ +        sig = self._backend.signaler +        conntrack = self._connect_and_track +        auth_err = self._authentication_error -        self._connect_and_track(sig.prov_download_provider_info, -                                self._load_provider_config) -        self._connect_and_track(sig.prov_check_api_certificate, -                                self._provider_config_loaded) +        conntrack(sig.prov_name_resolution, self._intermediate_stage) +        conntrack(sig.prov_https_connection, self._intermediate_stage) +        conntrack(sig.prov_download_ca_cert, self._intermediate_stage) +        conntrack(sig.prov_download_provider_info, self._load_provider_config) +        conntrack(sig.prov_check_api_certificate, self._provider_config_loaded) +        conntrack(sig.prov_check_api_certificate, self._get_provider_details) -        self._connect_and_track(sig.prov_problem_with_provider, -                                self._login_problem_provider) +        conntrack(sig.prov_problem_with_provider, self._login_problem_provider) +        conntrack(sig.prov_cancelled_setup, self._set_login_cancelled) -        self._connect_and_track(sig.prov_cancelled_setup, -                                self._set_login_cancelled) +        conntrack(sig.prov_get_details, self._provider_get_details)          # Login signals -        self._connect_and_track(sig.srp_auth_ok, self._authentication_finished) +        conntrack(sig.srp_auth_ok, self._authentication_finished) -        auth_error = ( -            lambda: self._authentication_error(self.tr("Unknown error."))) -        self._connect_and_track(sig.srp_auth_error, auth_error) +        auth_error = lambda: auth_err(self.tr("Unknown error.")) +        conntrack(sig.srp_auth_error, auth_error) -        auth_server_error = ( -            lambda: self._authentication_error( -                self.tr("There was a server problem with authentication."))) -        self._connect_and_track(sig.srp_auth_server_error, auth_server_error) +        auth_server_error = lambda: auth_err(self.tr( +            "There was a server problem with authentication.")) +        conntrack(sig.srp_auth_server_error, auth_server_error) -        auth_connection_error = ( -            lambda: self._authentication_error( -                self.tr("Could not establish a connection."))) -        self._connect_and_track(sig.srp_auth_connection_error, -                                auth_connection_error) +        auth_connection_error = lambda: auth_err(self.tr( +            "Could not establish a connection.")) +        conntrack(sig.srp_auth_connection_error, auth_connection_error) -        auth_bad_user_or_password = ( -            lambda: self._authentication_error( -                self.tr("Invalid username or password."))) -        self._connect_and_track(sig.srp_auth_bad_user_or_password, -                                auth_bad_user_or_password) +        auth_bad_user_or_password = lambda: auth_err(self.tr( +            "Invalid username or password.")) +        conntrack(sig.srp_auth_bad_user_or_password, auth_bad_user_or_password)          # Logout signals -        self._connect_and_track(sig.srp_logout_ok, self._logout_ok) -        self._connect_and_track(sig.srp_logout_error, self._logout_error) - -        self._connect_and_track(sig.srp_not_logged_in_error, -                                self._not_logged_in_error) +        conntrack(sig.srp_logout_ok, self._logout_ok) +        conntrack(sig.srp_logout_error, self._logout_error) +        conntrack(sig.srp_not_logged_in_error, self._not_logged_in_error)          # EIP bootstrap signals -        self._connect_and_track(sig.eip_config_ready, -                                self._eip_intermediate_stage) -        self._connect_and_track(sig.eip_client_certificate_ready, -                                self._finish_eip_bootstrap) +        conntrack(sig.eip_config_ready, self._eip_intermediate_stage) +        conntrack(sig.eip_client_certificate_ready, self._finish_eip_bootstrap) + +        ################################################### +        # Add tracked signals above this, untracked below! +        ################################################### +        if only_tracked: +            return          # We don't want to disconnect some signals so don't track them: + +        sig.backend_bad_call.connect(self._backend_bad_call) +          sig.prov_unsupported_client.connect(self._needs_update)          sig.prov_unsupported_api.connect(self._incompatible_api) +        sig.prov_get_all_services.connect(self._provider_get_all_services) -        # EIP start signals -        sig.eip_openvpn_already_running.connect( -            self._on_eip_openvpn_already_running) -        sig.eip_alien_openvpn_already_running.connect( -            self._on_eip_alien_openvpn_already_running) -        sig.eip_openvpn_not_found_error.connect( -            self._on_eip_openvpn_not_found_error) -        sig.eip_vpn_launcher_exception.connect( -            self._on_eip_vpn_launcher_exception) -        sig.eip_no_polkit_agent_error.connect( -            self._on_eip_no_polkit_agent_error) -        sig.eip_no_pkexec_error.connect(self._on_eip_no_pkexec_error) -        sig.eip_no_tun_kext_error.connect(self._on_eip_no_tun_kext_error) - -        sig.eip_state_changed.connect(self._eip_status.update_vpn_state) -        sig.eip_status_changed.connect(self._eip_status.update_vpn_status) -        sig.eip_process_finished.connect(self._eip_finished) -        sig.eip_network_unreachable.connect(self._on_eip_network_unreachable) -        sig.eip_process_restart_tls.connect(self._do_eip_restart) -        sig.eip_process_restart_ping.connect(self._do_eip_restart) +        # EIP start signals ============================================== +        self._eip_conductor.connect_backend_signals()          sig.eip_can_start.connect(self._backend_can_start_eip)          sig.eip_cannot_start.connect(self._backend_cannot_start_eip) +        # ================================================================== + +        # Soledad signals +        # TODO delegate connection to soledad bootstrapper +        sig.soledad_bootstrap_failed.connect( +            self._mail_status.set_soledad_failed) +        sig.soledad_bootstrap_finished.connect(self._on_soledad_ready) + +        sig.soledad_offline_failed.connect( +            self._mail_status.set_soledad_failed) +        sig.soledad_offline_finished.connect(self._on_soledad_ready) + +        sig.soledad_invalid_auth_token.connect( +            self._mail_status.set_soledad_invalid_auth_token) + +        # TODO: connect this with something +        # sig.soledad_cancelled_bootstrap.connect() +      def _disconnect_and_untrack(self):          """          Helper to disconnect the tracked signals. @@ -468,13 +432,13 @@ class MainWindow(QtGui.QMainWindow):          Some signals are emitted from the wizard, and we want to          ignore those.          """ -        for signal, method in self._backend_connected_signals.items(): +        for signal, method in self._backend_connected_signals:              try:                  signal.disconnect(method)              except RuntimeError:                  pass  # Signal was not connected -        self._backend_connected_signals = {} +        self._backend_connected_signals = []      @QtCore.Slot()      def _rejected_wizard(self): @@ -497,7 +461,7 @@ class MainWindow(QtGui.QMainWindow):              # This happens if the user finishes the provider              # setup but does not register              self._wizard = None -            self._backend_connect() +            self._backend_connect(only_tracked=True)              if self._wizard_firstrun:                  self._finish_init() @@ -586,13 +550,14 @@ class MainWindow(QtGui.QMainWindow):          domain = self._login_widget.get_selected_provider()          logged_user = "{0}@{1}".format(self._logged_user, domain) -        has_mx = True -        if self._logged_user is not None: -            provider_config = self._get_best_provider_config() -            has_mx = provider_config.provides_mx() +        details = self._provider_details +        mx_provided = False +        if details is not None: +            mx_provided = MX_SERVICE in details.services -        akm = AdvancedKeyManagement( -            self, has_mx, logged_user, self._keymanager, self._soledad) +        # XXX: handle differently not logged in user? +        akm = AdvancedKeyManagement(self, mx_provided, logged_user, +                                    self._backend, self._soledad_started)          akm.show()      @QtCore.Slot() @@ -604,11 +569,13 @@ class MainWindow(QtGui.QMainWindow):          Displays the preferences window.          """ -        user = self._login_widget.get_user() -        prov = self._login_widget.get_selected_provider() -        preferences = PreferencesWindow( -            self, self._backend, self._provider_config, self._soledad, -            user, prov) +        user = self._logged_user +        domain = self._login_widget.get_selected_provider() +        mx_provided = False +        if self._provider_details is not None: +            mx_provided = MX_SERVICE in self._provider_details.services +        preferences = PreferencesWindow(self, user, domain, self._backend, +                                        self._soledad_started, mx_provided)          self.soledad_ready.connect(preferences.set_soledad_ready)          preferences.show() @@ -630,7 +597,7 @@ class MainWindow(QtGui.QMainWindow):          default_provider = settings.get_defaultprovider()          if default_provider is None: -            logger.warning("Trying toupdate eip enabled status but there's no" +            logger.warning("Trying to update eip enabled status but there's no"                             " default provider. Disabling EIP for the time"                             " being...")              self._backend_cannot_start_eip() @@ -642,7 +609,7 @@ class MainWindow(QtGui.QMainWindow):          # If we don't want to start eip, we leave everything          # initialized to quickly start it          if not self._trying_to_start_eip: -            self._backend.setup_eip(default_provider, skip_network=True) +            self._backend.eip_setup(default_provider, skip_network=True)      def _backend_can_start_eip(self):          """ @@ -670,7 +637,6 @@ class MainWindow(QtGui.QMainWindow):                  # so the user needs to log in first                  self._eip_status.disable_eip_start()          else: -            self._stop_eip()              self._eip_status.disable_eip_start()              self._eip_status.set_eip_status(self.tr("Disabled")) @@ -697,7 +663,6 @@ class MainWindow(QtGui.QMainWindow):              # so the user needs to log in first              self._eip_status.disable_eip_start()          else: -            self._stop_eip()              self._eip_status.disable_eip_start()              self._eip_status.set_eip_status(self.tr("Disabled")) @@ -817,7 +782,7 @@ class MainWindow(QtGui.QMainWindow):                  self.eip_needs_login.emit()              self._wizard = None -            self._backend_connect() +            self._backend_connect(only_tracked=True)          else:              self._update_eip_enabled_status() @@ -846,16 +811,9 @@ class MainWindow(QtGui.QMainWindow):          """          providers = self._settings.get_configured_providers() -        services = set() - -        for prov in providers: -            provider_config = ProviderConfig() -            loaded = provider_config.load( -                provider.get_provider_path(prov)) -            if loaded: -                for service in provider_config.get_services(): -                    services.add(service) +        self._backend.provider_get_all_services(providers) +    def _provider_get_all_services(self, services):          self._set_eip_visible(EIP_SERVICE in services)          self._set_mx_visible(MX_SERVICE in services) @@ -893,14 +851,11 @@ class MainWindow(QtGui.QMainWindow):          """          Set the login label to reflect offline status.          """ -        if self._logged_in_offline: -            provider = "" -        else: +        provider = "" +        if not self._logged_in_offline:              provider = self.ui.lblLoginProvider.text() -        self.ui.lblLoginProvider.setText( -            provider + -            self.tr(" (offline mode)")) +        self.ui.lblLoginProvider.setText(provider + self.tr(" (offline mode)"))      #      # systray @@ -923,7 +878,8 @@ class MainWindow(QtGui.QMainWindow):          systrayMenu.addAction(self._action_visible)          systrayMenu.addSeparator() -        eip_status_label = "{0}: {1}".format(self._eip_name, self.tr("OFF")) +        eip_status_label = "{0}: {1}".format( +            self._eip_conductor.eip_name, self.tr("OFF"))          self._eip_menu = eip_menu = systrayMenu.addMenu(eip_status_label)          eip_menu.addAction(self._action_eip_startstop)          self._eip_status.set_eip_status_menu(eip_menu) @@ -1005,7 +961,7 @@ class MainWindow(QtGui.QMainWindow):          # Wait a bit until the window visibility has changed so          # the menu is set with the correct value. -        QtCore.QTimer.singleShot(500, self._update_hideshow_menu) +        QtDelayedCall(500, self._update_hideshow_menu)      def _center_window(self):          """ @@ -1154,9 +1110,8 @@ class MainWindow(QtGui.QMainWindow):          provider configuration if it's not present, otherwise will          emit the corresponding signals inmediately          """ -        # XXX should rename this provider, name clash. -        provider = self._login_widget.get_selected_provider() -        self._backend.setup_provider(provider) +        domain = self._login_widget.get_selected_provider() +        self._backend.provider_setup(domain)      @QtCore.Slot(dict)      def _load_provider_config(self, data): @@ -1164,12 +1119,11 @@ class MainWindow(QtGui.QMainWindow):          TRIGGERS:              self._backend.signaler.prov_download_provider_info -        Once the provider config has been downloaded, this loads the -        self._provider_config instance with it and starts the second -        part of the bootstrapping sequence +        Once the provider config has been downloaded, start the second +        part of the bootstrapping sequence.          :param data: result from the last stage of the -                     run_provider_select_checks +                     backend.provider_setup()          :type data: dict          """          if data[self._backend.PASSED_KEY]: @@ -1211,7 +1165,6 @@ class MainWindow(QtGui.QMainWindow):              self._set_label_offline()              self.offline_mode_bypass_login.emit()          else: -            leap_assert(self._provider_config, "We need a provider config")              self.ui.action_create_new_account.setEnabled(False)              if self._login_widget.start_login():                  self._download_provider_config() @@ -1250,20 +1203,19 @@ class MainWindow(QtGui.QMainWindow):          Cancel the running defers to avoid app blocking.          """          # XXX: Should we stop all the backend defers? -        self._backend.cancel_setup_provider() -        self._backend.cancel_login() +        self._backend.provider_cancel_setup() +        self._backend.user_cancel_login() +        self._backend.soledad_cancel_bootstrap() +        self._backend.soledad_close() -        if self._soledad_defer is not None: -            logger.debug("Cancelling soledad defer.") -            self._soledad_defer.cancel() -            self._soledad_defer = None +        self._soledad_started = False      @QtCore.Slot()      def _set_login_cancelled(self):          """          TRIGGERS:              Signaler.prov_cancelled_setup fired by -            self._backend.cancel_setup_provider() +            self._backend.provider_cancel_setup()          This method re-enables the login widget and display a message for          the cancelled operation. @@ -1280,16 +1232,14 @@ class MainWindow(QtGui.QMainWindow):          Once the provider configuration is loaded, this starts the SRP          authentication          """ -        leap_assert(self._provider_config, "We need a provider config!") -          if data[self._backend.PASSED_KEY]:              username = self._login_widget.get_user()              password = self._login_widget.get_password()              self._show_hide_unsupported_services() -            domain = self._provider_config.get_domain() -            self._backend.login(domain, username, password) +            domain = self._login_widget.get_selected_provider() +            self._backend.user_login(domain, username, password)          else:              logger.error(data[self._backend.ERROR_KEY])              self._login_problem_provider() @@ -1307,7 +1257,7 @@ class MainWindow(QtGui.QMainWindow):          self._logged_user = self._login_widget.get_user()          user = self._logged_user -        domain = self._provider_config.get_domain() +        domain = self._login_widget.get_selected_provider()          full_user_id = make_address(user, domain)          self._mail_conductor.userid = full_user_id          self._start_eip_bootstrap() @@ -1317,11 +1267,11 @@ class MainWindow(QtGui.QMainWindow):          if MX_SERVICE in self._enabled_services:              btn_enabled = self._login_widget.set_logout_btn_enabled              btn_enabled(False) -            self.soledad_ready.connect(lambda: btn_enabled(True)) -            self._soledad_bootstrapper.soledad_failed.connect( -                lambda: btn_enabled(True)) +            sig = self._backend.signaler +            sig.soledad_bootstrap_failed.connect(lambda: btn_enabled(True)) +            sig.soledad_bootstrap_finished.connect(lambda: btn_enabled(True)) -        if not self._get_best_provider_config().provides_mx(): +        if not MX_SERVICE in self._provider_details.services:              self._set_mx_visible(False)      def _start_eip_bootstrap(self): @@ -1331,11 +1281,10 @@ class MainWindow(QtGui.QMainWindow):          """          self._login_widget.logged_in() -        provider = self._provider_config.get_domain() -        self.ui.lblLoginProvider.setText(provider) +        domain = self._login_widget.get_selected_provider() +        self.ui.lblLoginProvider.setText(domain) -        self._enabled_services = self._settings.get_enabled_services( -            self._provider_config.get_domain()) +        self._enabled_services = self._settings.get_enabled_services(domain)          # TODO separate UI from logic.          if self._provides_mx_and_enabled(): @@ -1345,6 +1294,30 @@ class MainWindow(QtGui.QMainWindow):          self._maybe_start_eip() +    @QtCore.Slot() +    def _get_provider_details(self): +        """ +        TRIGGERS: +            prov_check_api_certificate + +        Set the attributes to know if the EIP and MX services are supported +        and enabled. +        This is triggered right after the provider has been set up. +        """ +        domain = self._login_widget.get_selected_provider() +        lang = QtCore.QLocale.system().name() +        self._backend.provider_get_details(domain, lang) + +    @QtCore.Slot() +    def _provider_get_details(self, details): +        """ +        Set the details for the just downloaded provider. + +        :param details: the details of the provider. +        :type details: ProviderConfigLight +        """ +        self._provider_details = details +      def _provides_mx_and_enabled(self):          """          Defines if the current provider provides mx and if we have it enabled. @@ -1352,9 +1325,15 @@ class MainWindow(QtGui.QMainWindow):          :returns: True if provides and is enabled, False otherwise          :rtype: bool          """ -        provider_config = self._get_best_provider_config() -        return (provider_config.provides_mx() and -                MX_SERVICE in self._enabled_services) +        domain = self._login_widget.get_selected_provider() +        enabled_services = self._settings.get_enabled_services(domain) + +        mx_enabled = MX_SERVICE in enabled_services +        mx_provided = False +        if self._provider_details is not None: +            mx_provided = MX_SERVICE in self._provider_details.services + +        return mx_enabled and mx_provided      def _provides_eip_and_enabled(self):          """ @@ -1363,33 +1342,30 @@ class MainWindow(QtGui.QMainWindow):          :returns: True if provides and is enabled, False otherwise          :rtype: bool          """ -        provider_config = self._get_best_provider_config() -        return (provider_config.provides_eip() and -                EIP_SERVICE in self._enabled_services) +        domain = self._login_widget.get_selected_provider() +        enabled_services = self._settings.get_enabled_services(domain) + +        eip_enabled = EIP_SERVICE in enabled_services +        eip_provided = False +        if self._provider_details is not None: +            eip_provided = EIP_SERVICE in self._provider_details.services + +        return eip_enabled and eip_provided      def _maybe_run_soledad_setup_checks(self):          """          Conditionally start Soledad.          """          # TODO split. -        if self._already_started_soledad is True: -            return - -        if not self._provides_mx_and_enabled(): +        if not self._provides_mx_and_enabled() and not flags.OFFLINE: +            logger.debug("Provider does not offer MX, but it is enabled.")              return          username = self._login_widget.get_user()          password = unicode(self._login_widget.get_password())          provider_domain = self._login_widget.get_selected_provider() -        sb = self._soledad_bootstrapper -        if flags.OFFLINE is True: -            provider_domain = self._login_widget.get_selected_provider() -            sb._password = password - -            self._provisional_provider_config.load( -                provider.get_provider_path(provider_domain)) - +        if flags.OFFLINE:              full_user_id = make_address(username, provider_domain)              uuid = self._settings.get_uuid(full_user_id)              self._mail_conductor.userid = full_user_id @@ -1399,74 +1375,26 @@ class MainWindow(QtGui.QMainWindow):                  # this is mostly for internal use/debug for now.                  logger.warning("Sorry! Log-in at least one time.")                  return -            fun = sb.load_offline_soledad -            fun(full_user_id, password, uuid) +            self._backend.soledad_load_offline(full_user_id, password, uuid)          else: -            provider_config = self._provider_config -              if self._logged_user is not None: -                self._soledad_defer = sb.run_soledad_setup_checks( -                    provider_config, username, password, -                    download_if_needed=True) +                domain = self._login_widget.get_selected_provider() +                self._backend.soledad_bootstrap(username, domain, password)      ###################################################################      # Service control methods: soledad -    @QtCore.Slot(dict) -    def _soledad_intermediate_stage(self, data): -        # TODO missing param docstring -        """ -        TRIGGERS: -            self._soledad_bootstrapper.download_config - -        If there was a problem, displays it, otherwise it does nothing. -        This is used for intermediate bootstrapping stages, in case -        they fail. -        """ -        passed = data[self._soledad_bootstrapper.PASSED_KEY] -        if not passed: -            # TODO display in the GUI: -            # should pass signal to a slot in status_panel -            # that sets the global status -            logger.error("Soledad failed to start: %s" % -                         (data[self._soledad_bootstrapper.ERROR_KEY],)) - -    @QtCore.Slot(dict) -    def _soledad_bootstrapped_stage(self, data): +    @QtCore.Slot() +    def _on_soledad_ready(self):          """          TRIGGERS: -            self._soledad_bootstrapper.gen_key -            self._soledad_bootstrapper.local_only_ready - -        If there was a problem, displays it, otherwise it does nothing. -        This is used for intermediate bootstrapping stages, in case -        they fail. +            Signaler.soledad_bootstrap_finished -        :param data: result from the bootstrapping stage for Soledad -        :type data: dict +        Actions to take when Soledad is ready.          """ -        passed = data[self._soledad_bootstrapper.PASSED_KEY] -        if not passed: -            # TODO should actually *display* on the panel. -            logger.debug("ERROR on soledad bootstrapping:") -            logger.error("%r" % data[self._soledad_bootstrapper.ERROR_KEY]) -            return -          logger.debug("Done bootstrapping Soledad") -        # Update the proxy objects to point to -        # the initialized instances. -        setProxiedObject(self._soledad, -                         self._soledad_bootstrapper.soledad) -        setProxiedObject(self._keymanager, -                         self._soledad_bootstrapper.keymanager) - -        # Ok, now soledad is ready, so we can allow other things that -        # depend on soledad to start. -        self._soledad_defer = None - -        # this will trigger start_imap_service -        # and start_smtp_boostrapping +        self._soledad_started = True          self.soledad_ready.emit()      ################################################################### @@ -1483,19 +1411,7 @@ class MainWindow(QtGui.QMainWindow):              return          if self._provides_mx_and_enabled(): -            self._mail_conductor.start_smtp_service(self._provider_config, -                                                    download_if_needed=True) - -    # XXX --- should remove from here, and connecte directly to the state -    # machine. -    @QtCore.Slot() -    def _stop_smtp_service(self): -        """ -        TRIGGERS: -            self.logout -        """ -        # TODO call stop_mail_service -        self._mail_conductor.stop_smtp_service() +            self._mail_conductor.start_smtp_service(download_if_needed=True)      ###################################################################      # Service control methods: imap @@ -1509,69 +1425,14 @@ class MainWindow(QtGui.QMainWindow):          # TODO in the OFFLINE mode we should also modify the  rules          # in the mail state machine so it shows that imap is active          # (but not smtp since it's not yet ready for offline use) -        start_fun = self._mail_conductor.start_imap_service -        if flags.OFFLINE is True: -            provider_domain = self._login_widget.get_selected_provider() -            self._provider_config.load( -                provider.get_provider_path(provider_domain)) -        provides_mx = self._provider_config.provides_mx() - -        if flags.OFFLINE is True and provides_mx: -            start_fun() -            return - -        if self._provides_mx_and_enabled(): -            start_fun() - -    def _on_mail_client_logged_in(self, req): -        """ -        Triggers qt signal when client login event is received. -        """ -        self.mail_client_logged_in.emit() - -    @QtCore.Slot() -    def _fetch_incoming_mail(self): -        """ -        TRIGGERS: -            self.mail_client_logged_in -        """ -        # TODO connect signal directly!!! -        self._mail_conductor.fetch_incoming_mail() - -    @QtCore.Slot() -    def _stop_imap_service(self): -        """ -        TRIGGERS: -            self.logout -        """ -        cv = Condition() -        cv.acquire() -        # TODO call stop_mail_service -        threads.deferToThread(self._mail_conductor.stop_imap_service, cv) -        # and wait for it to be stopped -        logger.debug('Waiting for imap service to stop.') -        cv.wait(self.SERVICE_STOP_TIMEOUT) +        if self._provides_mx_and_enabled() or flags.OFFLINE: +            self._mail_conductor.start_imap_service()      # end service control methods (imap)      ###################################################################      # Service control methods: eip -    def start_eip_machine(self): -        """ -        Initializes and starts the EIP state machine -        """ -        button = self._eip_status.eip_button -        action = self._action_eip_startstop -        label = self._eip_status.eip_label -        builder = statemachines.ConnectionMachineBuilder(self._eip_connection) -        eip_machine = builder.make_machine(button=button, -                                           action=action, -                                           label=label) -        self.eip_machine = eip_machine -        self.eip_machine.start() -        logger.debug('eip machine started') -      @QtCore.Slot()      def _disable_eip_start_action(self):          """ @@ -1585,32 +1446,32 @@ class MainWindow(QtGui.QMainWindow):          Enables the EIP start action in the systray menu.          """          self._action_eip_startstop.setEnabled(True) +        self._eip_status.enable_eip_start()      @QtCore.Slot()      def _on_eip_connection_connected(self):          """          TRIGGERS: -            self._eip_status.eip_connection_connected - -        Emits the EIPConnection.qtsigs.connected_signal +            self._eip_conductor.qtsigs.connected_signal          This is a little workaround for connecting the vpn-connected          signal that currently is beeing processed under status_panel.          After the refactor to EIPConductor this should not be necessary.          """ -        self._eip_connection.qtsigs.connected_signal.emit() - -        provider_config = self._get_best_provider_config() -        domain = provider_config.get_domain() +        domain = self._login_widget.get_selected_provider()          self._eip_status.set_provider(domain)          self._settings.set_defaultprovider(domain)          self._already_started_eip = True          # check for connectivity +        # we might want to leave a little time here...          self._check_name_resolution(domain)      def _check_name_resolution(self, domain): +        # FIXME this has to be moved to backend !!! +        # Should move to netchecks module. +        # and separate qt from reactor...          """          Check if we can resolve the given domain name. @@ -1641,7 +1502,7 @@ class MainWindow(QtGui.QMainWindow):                  "missing some helper files that are needed to securely use "                  "DNS while {1} is active. To install these helper files, quit "                  "this application and start it again." -            ).format(domain, self._eip_name) +            ).format(domain, self._eip_conductor.eip_name)              show_err = lambda: QtGui.QMessageBox.critical(                  self, self.tr("Connection Error"), msg) @@ -1663,245 +1524,42 @@ class MainWindow(QtGui.QMainWindow):          self._enabled_services = settings.get_enabled_services(              default_provider) -        loaded = self._provisional_provider_config.load( -            provider.get_provider_path(default_provider)) -        if loaded and settings.get_autostart_eip(): -            # XXX I think we should not try to re-download config every time, -            # it adds some delay. -            # Maybe if it's the first run in a session, -            # or we can try only if it fails. -            self._maybe_start_eip() -        elif settings.get_autostart_eip(): -            # XXX: Display a proper message to the user -            self.eip_needs_login.emit() -            logger.error("Unable to load %s config, cannot autostart." % -                         (default_provider,)) - -    @QtCore.Slot() -    def _start_EIP(self): -        """ -        Starts EIP -        """ -        self._eip_status.eip_pre_up() -        self.user_stopped_eip = False - -        # Until we set an option in the preferences window, we'll assume that -        # by default we try to autostart. If we switch it off manually, it -        # won't try the next time. -        self._settings.set_autostart_eip(True) - -        self._backend.start_eip() - -    @QtCore.Slot() -    def _on_eip_connection_aborted(self): -        """ -        TRIGGERS: -            Signaler.eip_connection_aborted -        """ -        logger.error("Tried to start EIP but cannot find any " -                     "available provider!") - -        eip_status_label = self.tr("Could not load {0} configuration.") -        eip_status_label = eip_status_label.format(self._eip_name) -        self._eip_status.set_eip_status(eip_status_label, error=True) - -        # signal connection_aborted to state machine: -        qtsigs = self._eip_connection.qtsigs -        qtsigs.connection_aborted_signal.emit() - -    def _on_eip_openvpn_already_running(self): -        self._eip_status.set_eip_status( -            self.tr("Another openvpn instance is already running, and " -                    "could not be stopped."), -            error=True) -        self._set_eipstatus_off() - -    def _on_eip_alien_openvpn_already_running(self): -        self._eip_status.set_eip_status( -            self.tr("Another openvpn instance is already running, and " -                    "could not be stopped because it was not launched by " -                    "Bitmask. Please stop it and try again."), -            error=True) -        self._set_eipstatus_off() - -    def _on_eip_openvpn_not_found_error(self): -        self._eip_status.set_eip_status( -            self.tr("We could not find openvpn binary."), -            error=True) -        self._set_eipstatus_off() - -    def _on_eip_vpn_launcher_exception(self): -        # XXX We should implement again translatable exceptions so -        # we can pass a translatable string to the panel (usermessage attr) -        self._eip_status.set_eip_status("VPN Launcher error.", error=True) -        self._set_eipstatus_off() - -    def _on_eip_no_polkit_agent_error(self): -        self._eip_status.set_eip_status( -            # XXX this should change to polkit-kde where -            # applicable. -            self.tr("We could not find any authentication agent in your " -                    "system.<br/>Make sure you have" -                    "<b>polkit-gnome-authentication-agent-1</b> running and" -                    "try again."), -            error=True) -        self._set_eipstatus_off() - -    def _on_eip_no_pkexec_error(self): -        self._eip_status.set_eip_status( -            self.tr("We could not find <b>pkexec</b> in your system."), -            error=True) -        self._set_eipstatus_off() - -    def _on_eip_no_tun_kext_error(self): -        self._eip_status.set_eip_status( -            self.tr("{0} cannot be started because the tuntap extension is " -                    "not installed properly in your " -                    "system.").format(self._eip_name)) -        self._set_eipstatus_off() - -    @QtCore.Slot() -    def _stop_eip(self): -        """ -        TRIGGERS: -          self._eip_connection.qtsigs.do_disconnect_signal (via state machine) - -        Stops vpn process and makes gui adjustments to reflect -        the change of state. - -        :param abnormal: whether this was an abnormal termination. -        :type abnormal: bool -        """ -        self.user_stopped_eip = True -        self._backend.stop_eip() - -        self._set_eipstatus_off(False) -        self._already_started_eip = False - -        logger.debug('Setting autostart to: False') -        self._settings.set_autostart_eip(False) - -        if self._logged_user: -            self._eip_status.set_provider( -                make_address( -                    self._logged_user, -                    self._get_best_provider_config().get_domain())) -        self._eip_status.eip_stopped() - -    @QtCore.Slot() -    def _on_eip_network_unreachable(self): -        # XXX Should move to EIP Conductor -        """ -        TRIGGERS: -            self._eip_connection.qtsigs.network_unreachable - -        Displays a "network unreachable" error in the EIP status panel. -        """ -        self._eip_status.set_eip_status(self.tr("Network is unreachable"), -                                        error=True) -        self._eip_status.set_eip_status_icon("error") - -    @QtCore.Slot() -    def _do_eip_restart(self): -        # XXX Should move to EIP Conductor -        """ -        TRIGGERS: -            self._eip_connection.qtsigs.process_restart - -        Restart the connection. -        """ -        # for some reason, emitting the do_disconnect/do_connect -        # signals hangs the UI. -        self._stop_eip() -        QtCore.QTimer.singleShot(2000, self._start_EIP) - -    def _set_eipstatus_off(self, error=True): -        """ -        Sets eip status to off -        """ -        # XXX this should be handled by the state machine. -        self._eip_status.set_eip_status("", error=error) -        self._eip_status.set_eip_status_icon("error") - -    @QtCore.Slot(int) -    def _eip_finished(self, exitCode): -        """ -        TRIGGERS: -            Signaler.eip_process_finished - -        Triggered when the EIP/VPN process finishes to set the UI -        accordingly. - -        Ideally we would have the right exit code here, -        but the use of different wrappers (pkexec, cocoasudo) swallows -        the openvpn exit code so we get zero exit in some cases  where we -        shouldn't. As a workaround we just use a flag to indicate -        a purposeful switch off, and mark everything else as unexpected. - -        In the near future we should trigger a native notification from here, -        since the user really really wants to know she is unprotected asap. -        And the right thing to do will be to fail-close. - -        :param exitCode: the exit code of the eip process. -        :type exitCode: int -        """ -        # TODO move to EIPConductor. -        # TODO Add error catching to the openvpn log observer -        # so we can have a more precise idea of which type -        # of error did we have (server side, local problem, etc) - -        logger.info("VPN process finished with exitCode %s..." -                    % (exitCode,)) - -        qtsigs = self._eip_connection.qtsigs -        signal = qtsigs.disconnected_signal - -        # XXX check if these exitCodes are pkexec/cocoasudo specific -        if exitCode in (126, 127): -            eip_status_label = self.tr( -                "{0} could not be launched " -                "because you did not authenticate properly.") -            eip_status_label = eip_status_label.format(self._eip_name) -            self._eip_status.set_eip_status(eip_status_label, error=True) -            signal = qtsigs.connection_aborted_signal -            self._backend.terminate_eip() - -        elif exitCode != 0 or not self.user_stopped_eip: -            eip_status_label = self.tr("{0} finished in an unexpected manner!") -            eip_status_label = eip_status_label.format(self._eip_name) -            self._eip_status.eip_stopped() -            self._eip_status.set_eip_status_icon("error") -            self._eip_status.set_eip_status(eip_status_label, error=True) -            signal = qtsigs.connection_died_signal - -        if exitCode == 0 and IS_MAC: -            # XXX remove this warning after I fix cocoasudo. -            logger.warning("The above exit code MIGHT BE WRONG.") - -        # We emit signals to trigger transitions in the state machine: -        signal.emit() +        if settings.get_autostart_eip(): +            self._maybe_start_eip(autostart=True)      # eip boostrapping, config etc... -    def _maybe_start_eip(self): +    def _maybe_start_eip(self, autostart=False):          """          Start the EIP bootstrapping sequence if the client is configured to          do so. + +        :param autostart: we are autostarting EIP when this is True +        :type autostart: bool          """ -        if self._provides_eip_and_enabled() and not self._already_started_eip: +        # during autostart we assume that the provider provides EIP +        if autostart: +            should_start = EIP_SERVICE in self._enabled_services +        else: +            should_start = self._provides_eip_and_enabled() + +        if should_start and not self._already_started_eip: +            if self._eip_status.is_cold_start: +                self._backend.tear_fw_down()              # XXX this should be handled by the state machine. +            self._enable_eip_start_action()              self._eip_status.set_eip_status(                  self.tr("Starting...")) +            self._eip_status.eip_button.setEnabled(False)              domain = self._login_widget.get_selected_provider() -            self._backend.setup_eip(domain) +            self._backend.eip_setup(domain)              self._already_started_eip = True              # we want to start soledad anyway after a certain timeout if eip              # fails to come up -            QtCore.QTimer.singleShot( -                self.EIP_TIMEOUT, -                self._maybe_run_soledad_setup_checks) +            QtDelayedCall(self.EIP_START_TIMEOUT, +                          self._maybe_run_soledad_setup_checks)          else:              if not self._already_started_eip:                  if EIP_SERVICE in self._enabled_services: @@ -1920,8 +1578,8 @@ class MainWindow(QtGui.QMainWindow):          TRIGGERS:              self._backend.signaler.eip_client_certificate_ready -        Starts the VPN thread if the eip configuration is properly -        loaded +        Start the VPN thread if the eip configuration is properly +        loaded.          """          passed = data[self._backend.PASSED_KEY] @@ -1933,11 +1591,11 @@ class MainWindow(QtGui.QMainWindow):              return          # DO START EIP Connection! -        self._eip_connection.qtsigs.do_connect_signal.emit() +        self._eip_conductor.do_connect()      @QtCore.Slot(dict)      def _eip_intermediate_stage(self, data): -        # TODO missing param +        # TODO missing param documentation          """          TRIGGERS:              self._backend.signaler.eip_config_ready @@ -1952,33 +1610,10 @@ class MainWindow(QtGui.QMainWindow):                  self.tr("Unable to connect: Problem with provider"))              logger.error(data[self._backend.ERROR_KEY])              self._already_started_eip = False +            self._eip_status.aborted()      # end of EIP methods --------------------------------------------- -    def _get_best_provider_config(self): -        """ -        Returns the best ProviderConfig to use at a moment. We may -        have to use self._provider_config or -        self._provisional_provider_config depending on the start -        status. - -        :rtype: ProviderConfig -        """ -        # TODO move this out of gui. -        leap_assert(self._provider_config is not None or -                    self._provisional_provider_config is not None, -                    "We need a provider config") - -        provider_config = None -        if self._provider_config.loaded(): -            provider_config = self._provider_config -        elif self._provisional_provider_config.loaded(): -            provider_config = self._provisional_provider_config -        else: -            leap_assert(False, "We could not find any usable ProviderConfig.") - -        return provider_config -      @QtCore.Slot()      def _logout(self):          """ @@ -1987,16 +1622,11 @@ class MainWindow(QtGui.QMainWindow):          Starts the logout sequence          """ -        setProxiedObject(self._soledad, None) -          self._cancel_ongoing_defers() -        # reset soledad status flag -        self._already_started_soledad = False -          # XXX: If other defers are doing authenticated stuff, this          # might conflict with those. CHECK! -        self._backend.logout() +        self._backend.user_logout()          self.logout.emit()      @QtCore.Slot() @@ -2080,59 +1710,37 @@ class MainWindow(QtGui.QMainWindow):      # cleanup and quit methods      # -    def _cleanup_pidfiles(self): -        """ -        Removes lockfiles on a clean shutdown. - -        Triggered after aboutToQuit signal. +    def _stop_services(self):          """ -        if IS_WIN: -            WindowsLock.release_all_locks() - -    def _cleanup_and_quit(self): -        """ -        Call all the cleanup actions in a serialized way. -        Should be called from the quit function. +        Stop services and cancel ongoing actions (if any).          """ -        logger.debug('About to quit, doing cleanup...') - -        self._stop_imap_service() - -        if self._logged_user is not None: -            self._backend.logout() +        logger.debug('About to quit, doing cleanup.') -        if self._soledad_bootstrapper.soledad is not None: -            logger.debug("Closing soledad...") -            self._soledad_bootstrapper.soledad.close() -        else: -            logger.error("No instance of soledad was found.") +        self._cancel_ongoing_defers() -        logger.debug('Terminating vpn') -        self._backend.stop_eip(shutdown=True) +        self._services_being_stopped = {'imap', 'eip'} -        # We need to give some time to the ongoing signals for shutdown -        # to come into action. This needs to be solved using -        # back-communication from backend. -        QtCore.QTimer.singleShot(3000, self._shutdown) +        imap_stopped = lambda: self._remove_service('imap') +        self._backend.signaler.imap_stopped.connect(imap_stopped) -    def _shutdown(self): -        """ -        Actually shutdown. -        """ -        self._cancel_ongoing_defers() +        eip_stopped = lambda: self._remove_service('eip') +        self._backend.signaler.eip_stopped.connect(eip_stopped) -        # TODO missing any more cancels? +        logger.debug('Stopping mail services') +        self._backend.imap_stop_service() +        self._backend.smtp_stop_service() -        logger.debug('Cleaning pidfiles') -        self._cleanup_pidfiles() -        if self._quit_callback: -            self._quit_callback() +        if self._logged_user is not None: +            logger.debug("Doing logout") +            self._backend.user_logout() -        logger.debug('Bye.') +        logger.debug('Terminating vpn') +        self._backend.eip_stop(shutdown=True)      def quit(self):          """ -        Cleanup and tidely close the main window before quitting. +        Start the quit sequence and wait for services to finish. +        Cleanup and close the main window before quitting.          """          # TODO separate the shutting down of services from the          # UI stuff. @@ -2142,25 +1750,72 @@ class MainWindow(QtGui.QMainWindow):          if self._systray is not None:              self._systray.showMessage(                  self.tr('Quitting...'), -                self.tr('The app is quitting, please wait.')) +                self.tr('Bitmask is quitting, please wait.'))          # explicitly process events to display tooltip immediately -        QtCore.QCoreApplication.processEvents() +        QtCore.QCoreApplication.processEvents(0, 10) + +        # Close other windows if any. +        if self._wizard: +            self._wizard.close() + +        if self._logger_window: +            self._logger_window.close()          # Set this in case that the app is hidden          QtGui.QApplication.setQuitOnLastWindowClosed(True) -        self._cleanup_and_quit() +        self._stop_services() -        # We queue the call to stop since we need to wait until EIP is stopped. -        # Otherwise we may exit leaving an unmanaged openvpn process. -        reactor.callLater(0, self._backend.stop)          self._really_quit = True -        if self._wizard: -            self._wizard.close() +        # call final quit when all the services are stopped +        self.all_services_stopped.connect(self.final_quit) +        # or if we reach the timeout +        self._quit_timeout_callater = reactor.callLater( +            self.SERVICES_STOP_TIMEOUT, self.final_quit) -        if self._logger_window: -            self._logger_window.close() +    @QtCore.Slot() +    def _remove_service(self, service): +        """ +        Remove the given service from the waiting list and check if we have +        running services that we need to wait until we quit. +        Emit self.all_services_stopped signal if we don't need to keep waiting. +        :param service: the service that we want to remove +        :type service: str +        """ +        self._services_being_stopped.discard(service) + +        if not self._services_being_stopped: +            logger.debug("All services stopped.") +            self.all_services_stopped.emit() + +    @QtCore.Slot() +    def final_quit(self): +        """ +        Final steps to quit the app, starting from here we don't care about +        running services or user interaction, just quitting. +        """ +        logger.debug('Final quit...') + +        try: +            # disconnect signal if we get here due a timeout. +            self.all_services_stopped.disconnect(self.final_quit) +        except RuntimeError: +            pass  # Signal was not connected + +        # Cancel timeout to avoid being called if we reached here through the +        # signal +        if self._quit_timeout_callater.active(): +            self._quit_timeout_callater.cancel() + +        # Remove lockfiles on a clean shutdown. +        logger.debug('Cleaning pidfiles') +        if IS_WIN: +            WindowsLock.release_all_locks() + +        self._backend.stop()          self.close() + +        reactor.callLater(1, self._quit_callback) diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index 2947c5db..a3b81d38 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -23,15 +23,10 @@ import logging  from functools import partial  from PySide import QtCore, QtGui -from zope.proxy import sameProxiedObjects -from leap.bitmask.provider import get_provider_path  from leap.bitmask.config.leapsettings import LeapSettings  from leap.bitmask.gui.ui_preferences import Ui_Preferences -from leap.soledad.client import NoStorageSecret -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.util.credentials import password_checks  from leap.bitmask.services import get_service_display_name, MX_SERVICE  logger = logging.getLogger(__name__) @@ -43,32 +38,31 @@ class PreferencesWindow(QtGui.QDialog):      """      preferences_saved = QtCore.Signal() -    def __init__(self, parent, backend, provider_config, -                 soledad, username, domain): +    def __init__(self, parent, username, domain, backend, soledad_started, mx):          """          :param parent: parent object of the PreferencesWindow.          :parent type: QWidget -        :param backend: Backend being used -        :type backend: Backend -        :param provider_config: ProviderConfig object. -        :type provider_config: ProviderConfig -        :param soledad: Soledad instance -        :type soledad: Soledad          :param username: the user set in the login widget          :type username: unicode          :param domain: the selected domain in the login widget          :type domain: unicode +        :param backend: Backend being used +        :type backend: Backend +        :param soledad_started: whether soledad has started or not +        :type soledad_started: bool +        :param mx: whether the current provider provides mx or not. +        :type mx: bool          """          QtGui.QDialog.__init__(self, parent)          self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") -        self._backend = backend -        self._settings = LeapSettings() -        self._soledad = soledad -        self._provider_config = provider_config          self._username = username          self._domain = domain +        self._backend = backend +        self._soledad_started = soledad_started +        self._mx_provided = mx +        self._settings = LeapSettings()          self._backend_connect()          # Load UI @@ -89,50 +83,17 @@ class PreferencesWindow(QtGui.QDialog):          else:              self._add_configured_providers() -        self._backend.get_logged_in_status() +        if self._username is None: +            self._not_logged_in() +        else: +            self.ui.gbPasswordChange.setEnabled(True) +            if self._mx_provided: +                self._provides_mx()          self._select_provider_by_name(domain) -    @QtCore.Slot() -    def _is_logged_in(self): -        """ -        TRIGGERS: -            Signaler.srp_status_logged_in - -        Actions to perform is the user is logged in. -        """ -        settings = self._settings -        pw_enabled = True - -        # check if provider has 'mx' ... -        # TODO: we should move this to the backend. -        if self._provider_config.provides_mx(): -            enabled_services = settings.get_enabled_services(self._domain) -            mx_name = get_service_display_name(MX_SERVICE) - -            # ... and if the user have it enabled -            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) -                pw_enabled = False -            else: -                # check if Soledad is bootstrapped -                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) -                    pw_enabled = False - -        self.ui.gbPasswordChange.setEnabled(pw_enabled) - -    @QtCore.Slot()      def _not_logged_in(self):          """ -        TRIGGERS: -            Signaler.srp_status_not_logged_in -          Actions to perform if the user is not logged in.          """          msg = self.tr( @@ -140,6 +101,30 @@ class PreferencesWindow(QtGui.QDialog):          self._set_password_change_status(msg)          self.ui.gbPasswordChange.setEnabled(False) +    def _provides_mx(self): +        """ +        Actions to perform if the provider provides MX. +        """ +        pw_enabled = True +        enabled_services = self._settings.get_enabled_services(self._domain) +        mx_name = get_service_display_name(MX_SERVICE) + +        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) +            pw_enabled = False +        else: +            # check if Soledad is bootstrapped +            if not self._soledad_started: +                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) +                pw_enabled = False + +        self.ui.gbPasswordChange.setEnabled(pw_enabled) +      @QtCore.Slot()      def set_soledad_ready(self):          """ @@ -200,7 +185,7 @@ class PreferencesWindow(QtGui.QDialog):          new_password = self.ui.leNewPassword.text()          new_password2 = self.ui.leNewPassword2.text() -        ok, msg = basic_password_checks(username, new_password, new_password2) +        ok, msg = password_checks(username, new_password, new_password2)          if not ok:              self._set_changing_password(False) @@ -209,10 +194,10 @@ class PreferencesWindow(QtGui.QDialog):              return          self._set_changing_password(True) -        self._backend.change_password(current_password, new_password) +        self._backend.user_change_password(current_password, new_password)      @QtCore.Slot() -    def _change_password_ok(self): +    def _srp_change_password_ok(self):          """          TRIGGERS:              self._backend.signaler.srp_password_change_ok @@ -221,12 +206,44 @@ class PreferencesWindow(QtGui.QDialog):          """          new_password = self.ui.leNewPassword.text()          logger.debug("SRP password changed successfully.") -        try: -            self._soledad.change_passphrase(new_password) -            logger.debug("Soledad password changed successfully.") -        except NoStorageSecret: -            logger.debug( -                "No storage secret for password change in Soledad.") + +        if self._mx_provided: +            self._backend.soledad_change_password(new_password) +        else: +            self._change_password_success() + +    @QtCore.Slot(unicode) +    def _srp_change_password_problem(self, msg): +        """ +        TRIGGERS: +            self._backend.signaler.srp_password_change_error +            self._backend.signaler.srp_password_change_badpw + +        Callback used to display an error on changing password. + +        :param msg: the message to show to the user. +        :type msg: unicode +        """ +        logger.error("Error changing password") +        self._set_password_change_status(msg, error=True) +        self._set_changing_password(False) + +    @QtCore.Slot() +    def _soledad_change_password_ok(self): +        """ +        TRIGGERS: +            Signaler.soledad_password_change_ok + +        Soledad password change went OK. +        """ +        logger.debug("Soledad password changed successfully.") +        self._change_password_success() + +    def _change_password_success(self): +        """ +        Callback used to display a successfully changed password. +        """ +        logger.debug("Soledad password changed successfully.")          self._set_password_change_status(              self.tr("Password changed successfully."), success=True) @@ -234,18 +251,17 @@ class PreferencesWindow(QtGui.QDialog):          self._set_changing_password(False)      @QtCore.Slot(unicode) -    def _change_password_problem(self, msg): +    def _soledad_change_password_problem(self, msg):          """          TRIGGERS: -            self._backend.signaler.srp_password_change_error -            self._backend.signaler.srp_password_change_badpw +            Signaler.soledad_password_change_error          Callback used to display an error on changing password.          :param msg: the message to show to the user.          :type msg: unicode          """ -        logger.error("Error changing password") +        logger.error("Error changing soledad password")          self._set_password_change_status(msg, error=True)          self._set_changing_password(False) @@ -321,8 +337,7 @@ class PreferencesWindow(QtGui.QDialog):          TRIGGERS:              self.ui.cbProvidersServices.currentIndexChanged[unicode] -        Loads the services that the provider provides into the UI for -        the user to enable or disable. +        Fill the services list with the selected provider's services.          :param domain: the domain of the provider to load services from.          :type domain: str @@ -333,10 +348,6 @@ class PreferencesWindow(QtGui.QDialog):          if not domain:              return -        provider_config = self._get_provider_config(domain) -        if provider_config is None: -            return -          # set the proper connection for the 'save' button          try:              self.ui.pbSaveServices.clicked.disconnect() @@ -346,7 +357,21 @@ class PreferencesWindow(QtGui.QDialog):          save_services = partial(self._save_enabled_services, domain)          self.ui.pbSaveServices.clicked.connect(save_services) -        services = get_supported(provider_config.get_services()) +        self._backend.provider_get_supported_services(domain) + +    @QtCore.Slot(str) +    def _load_services(self, services): +        """ +        TRIGGERS: +            self.ui.cbProvidersServices.currentIndexChanged[unicode] + +        Loads the services that the provider provides into the UI for +        the user to enable or disable. + +        :param domain: the domain of the provider to load services from. +        :type domain: str +        """ +        domain = self.ui.cbProvidersServices.currentText()          services_conf = self._settings.get_enabled_services(domain)          # discard changes if other provider is selected @@ -394,36 +419,26 @@ class PreferencesWindow(QtGui.QDialog):          self._set_providers_services_status(msg, success=True)          self.preferences_saved.emit() -    def _get_provider_config(self, domain): -        """ -        Helper to return a valid Provider Config from the domain name. - -        :param domain: the domain name of the provider. -        :type domain: str - -        :rtype: ProviderConfig or None if there is a problem loading the config -        """ -        provider_config = ProviderConfig() -        if not provider_config.load(get_provider_path(domain)): -            provider_config = None - -        return provider_config -      def _backend_connect(self):          """          Helper to connect to backend signals          """          sig = self._backend.signaler -        sig.srp_status_logged_in.connect(self._is_logged_in) -        sig.srp_status_not_logged_in.connect(self._not_logged_in) +        sig.prov_get_supported_services.connect(self._load_services) -        sig.srp_password_change_ok.connect(self._change_password_ok) +        sig.srp_password_change_ok.connect(self._srp_change_password_ok) -        pwd_change_error = lambda: self._change_password_problem( +        pwd_change_error = lambda: self._srp_change_password_problem(              self.tr("There was a problem changing the password."))          sig.srp_password_change_error.connect(pwd_change_error) -        pwd_change_badpw = lambda: self._change_password_problem( +        pwd_change_badpw = lambda: self._srp_change_password_problem(              self.tr("You did not enter a correct current password."))          sig.srp_password_change_badpw.connect(pwd_change_badpw) + +        sig.soledad_password_change_ok.connect( +            self._soledad_change_password_ok) + +        sig.soledad_password_change_error.connect( +            self._soledad_change_password_problem) diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py index 31938a70..00a1387e 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -504,6 +504,11 @@ class ConnectionMachineBuilder(object):              conn.qtsigs.connection_died_signal,              states[_OFF]) +        # XXX adding this--------------------- +        states[_ON].addTransition( +            conn.qtsigs.do_disconnect_signal, +            states[_DIS]) +          # * If we receive the connection_aborted, we transition          #   from connecting to the off state          states[_CON].addTransition( @@ -551,7 +556,8 @@ class ConnectionMachineBuilder(object):          # TODO add tooltip          # OFF State ---------------------- -        off = QState() +        off = SignallingState( +            None, name=conn.name)          off_label = _tr("Turn {0}").format(              conn.Connected.short_label)          if button: @@ -559,11 +565,15 @@ class ConnectionMachineBuilder(object):                  button, 'text', off_label)              off.assignProperty(                  button, 'enabled', True) +            off.assignProperty( +                button, 'visible', True)          if action:              off.assignProperty(                  action, 'text', off_label)              off.assignProperty(                  action, 'enabled', True) +            off.assignProperty( +                action, 'visible', True)          off.setObjectName(_OFF)          states[_OFF] = off @@ -587,7 +597,10 @@ class ConnectionMachineBuilder(object):          states[_CON] = connecting          # ON State ------------------------ -        on = QState() +        on = SignallingState( +            None, name=conn.name) +        on_label = _tr("Turn {0}").format( +            conn.Disconnected.short_label)          if button:              on.assignProperty(                  button, 'text', on_label) diff --git a/src/leap/bitmask/gui/ui/eip_status.ui b/src/leap/bitmask/gui/ui/eip_status.ui index 64821ad6..7216bb0a 100644 --- a/src/leap/bitmask/gui/ui/eip_status.ui +++ b/src/leap/bitmask/gui/ui/eip_status.ui @@ -28,7 +28,7 @@       <property name="verticalSpacing">        <number>0</number>       </property> -     <item row="0" column="2"> +     <item row="0" column="4">        <widget class="QPushButton" name="btnEipStartStop">         <property name="text">          <string>Turn On</string> @@ -51,7 +51,7 @@         </property>        </widget>       </item> -     <item row="3" column="1"> +     <item row="3" column="2">        <widget class="QLabel" name="lblEIPStatus">         <property name="maximumSize">          <size> @@ -70,7 +70,7 @@         </property>        </widget>       </item> -     <item row="0" column="1"> +     <item row="0" column="2">        <widget class="QLabel" name="lblEIPMessage">         <property name="sizePolicy">          <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> @@ -86,7 +86,7 @@         </property>        </widget>       </item> -     <item row="0" column="3"> +     <item row="0" column="5">        <widget class="QLabel" name="lblVPNStatusIcon">         <property name="maximumSize">          <size> @@ -105,7 +105,7 @@         </property>        </widget>       </item> -     <item row="1" column="1"> +     <item row="1" column="2">        <spacer name="horizontalSpacer">         <property name="orientation">          <enum>Qt::Horizontal</enum> @@ -118,7 +118,7 @@         </property>        </spacer>       </item> -     <item row="2" column="1" colspan="3"> +     <item row="2" column="2" colspan="4">        <widget class="QWidget" name="eip_bandwidth" native="true">         <layout class="QHBoxLayout" name="horizontalLayout">          <property name="spacing"> @@ -161,12 +161,13 @@              <property name="text">               <string>0.0 KB/s</string>              </property> +            <property name="icon"> +             <iconset resource="../../../../../data/resources/mainwindow.qrc"> +              <normaloff>:/images/black/32/arrow-down.png</normaloff>:/images/black/32/arrow-down.png</iconset> +            </property>              <property name="flat">               <bool>true</bool>              </property> -            <property name="icon"> -             <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/down-arrow.png</pixmap> -            </property>             </widget>            </item>            <item> @@ -211,12 +212,13 @@              <property name="text">               <string>0.0 KB/s</string>              </property> +            <property name="icon"> +             <iconset resource="../../../../../data/resources/mainwindow.qrc"> +              <normaloff>:/images/black/32/arrow-up.png</normaloff>:/images/black/32/arrow-up.png</iconset> +            </property>              <property name="flat">               <bool>true</bool>              </property> -            <property name="icon"> -             <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/up-arrow.png</pixmap> -            </property>             </widget>            </item>            <item> @@ -237,6 +239,20 @@         </layout>        </widget>       </item> +     <item row="0" column="3"> +      <widget class="QPushButton" name="btnFwDown"> +       <property name="text"> +        <string>Turn Off</string> +       </property> +      </widget> +     </item> +     <item row="0" column="1"> +      <widget class="QLabel" name="lblGatewayCountryCode"> +       <property name="text"> +        <string/> +       </property> +      </widget> +     </item>      </layout>     </item>    </layout> diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index 020a58e2..4d774907 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -26,11 +26,10 @@ from PySide import QtCore, QtGui  from leap.bitmask.config import flags  from leap.bitmask.config.leapsettings import LeapSettings -from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.provider import get_provider_path  from leap.bitmask.services import get_service_display_name, get_supported +from leap.bitmask.util.credentials import password_checks, username_checks +from leap.bitmask.util.credentials import USERNAME_REGEX  from leap.bitmask.util.keyring_helpers import has_keyring -from leap.bitmask.util.password import basic_password_checks  from ui_wizard import Ui_Wizard @@ -49,8 +48,6 @@ class Wizard(QtGui.QWizard):      REGISTER_USER_PAGE = 4      SERVICES_PAGE = 5 -    BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" -      def __init__(self, backend, bypass_checks=False):          """          Constructor for the main Wizard. @@ -89,10 +86,9 @@ class Wizard(QtGui.QWizard):          self._backend_connect()          self._domain = None -        # HACK!! We need provider_config for the time being, it'll be -        # removed -        self._provider_config = ( -            self._backend._components["provider"]._provider_config) + +        # this details are set when the provider download is complete. +        self._provider_details = None          # We will store a reference to the defers for eventual use          # (eg, to cancel them) but not doing anything with them right now. @@ -118,7 +114,7 @@ class Wizard(QtGui.QWizard):          self.ui.rbExistingProvider.toggled.connect(self._skip_provider_checks) -        usernameRe = QtCore.QRegExp(self.BARE_USERNAME_REGEX) +        usernameRe = QtCore.QRegExp(USERNAME_REGEX)          self.ui.lblUser.setValidator(              QtGui.QRegExpValidator(usernameRe, self)) @@ -231,6 +227,12 @@ class Wizard(QtGui.QWizard):          if reset:              self._reset_provider_check() +    def _focus_username(self): +        """ +        Focus at the username lineedit for the registration page +        """ +        self.ui.lblUser.setFocus() +      def _focus_password(self):          """          Focuses at the password lineedit for the registration page @@ -253,16 +255,22 @@ class Wizard(QtGui.QWizard):          password = self.ui.lblPassword.text()          password2 = self.ui.lblPassword2.text() -        ok, msg = basic_password_checks(username, password, password2) -        if ok: +        user_ok, msg = username_checks(username) +        if user_ok: +            pass_ok, msg = password_checks(username, password, password2) + +        if user_ok and pass_ok:              self._set_register_status(self.tr("Starting registration...")) -            self._backend.register_user(self._domain, username, password) +            self._backend.user_register(self._domain, username, password)              self._username = username              self._password = password          else: +            if user_ok: +                self._focus_password() +            else: +                self._focus_username()              self._set_register_status(msg, error=True) -            self._focus_password()              self.ui.btnRegister.setEnabled(True)      def _set_registration_fields_visibility(self, visible): @@ -406,7 +414,7 @@ class Wizard(QtGui.QWizard):          self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON)          self._provider_select_defer = self._backend.\ -            setup_provider(self._domain) +            provider_setup(self._domain)      @QtCore.Slot(bool)      def _skip_provider_checks(self, skip): @@ -502,10 +510,12 @@ class Wizard(QtGui.QWizard):          check. Since this check is the last of this set, it also          completes the page if passed          """ -        if self._provider_config.load(get_provider_path(self._domain)): +        if data[self._backend.PASSED_KEY]:              self._complete_task(data, self.ui.lblProviderInfo,                                  True, self.SELECT_PROVIDER_PAGE)              self._provider_checks_ok = True +            lang = QtCore.QLocale.system().name() +            self._backend.provider_get_details(self._domain, lang)          else:              new_data = {                  self._backend.PASSED_KEY: False, @@ -527,6 +537,16 @@ class Wizard(QtGui.QWizard):          else:              self.ui.cbProviders.setEnabled(True) +    @QtCore.Slot() +    def _provider_get_details(self, details): +        """ +        Set the details for the just downloaded provider. + +        :param details: the details of the provider. +        :type details: ProviderConfigLight +        """ +        self._provider_details = details +      @QtCore.Slot(dict)      def _download_ca_cert(self, data):          """ @@ -594,11 +614,9 @@ class Wizard(QtGui.QWizard):          the user to enable or disable.          """          self.ui.grpServices.setTitle( -            self.tr("Services by %s") % -            (self._provider_config.get_name(),)) +            self.tr("Services by {0}").format(self._provider_details.name)) -        services = get_supported( -            self._provider_config.get_services()) +        services = get_supported(self._provider_details.services)          for service in services:              try: @@ -641,38 +659,31 @@ class Wizard(QtGui.QWizard):              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()) +                sub_title = sub_title.format(self._provider_details.name)                  self.page(pageId).setSubTitle(sub_title)                  self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON)                  self._provider_setup_defer = self._backend.\                      provider_bootstrap(self._domain)          if pageId == self.PRESENT_PROVIDER_PAGE: -            self.page(pageId).setSubTitle(self.tr("Description of services " -                                                  "offered by %s") % -                                          (self._provider_config -                                           .get_name(),)) - -            lang = QtCore.QLocale.system().name() -            self.ui.lblProviderName.setText( -                "<b>%s</b>" % -                (self._provider_config.get_name(lang=lang),)) -            self.ui.lblProviderURL.setText( -                "https://%s" % (self._provider_config.get_domain(),)) -            self.ui.lblProviderDesc.setText( -                "<i>%s</i>" % -                (self._provider_config.get_description(lang=lang),)) - -            self.ui.lblServicesOffered.setText(self._provider_config -                                               .get_services_string()) -            self.ui.lblProviderPolicy.setText(self._provider_config -                                              .get_enrollment_policy()) +            sub_title = self.tr("Description of services offered by {0}") +            sub_title = sub_title.format(self._provider_details.name) +            self.page(pageId).setSubTitle(sub_title) + +            details = self._provider_details +            name = "<b>{0}</b>".format(details.name) +            domain = "https://{0}".format(details.domain) +            description = "<i>{0}</i>".format(details.description) +            self.ui.lblProviderName.setText(name) +            self.ui.lblProviderURL.setText(domain) +            self.ui.lblProviderDesc.setText(description) +            self.ui.lblServicesOffered.setText(details.services_string) +            self.ui.lblProviderPolicy.setText(details.enrollment_policy)          if pageId == self.REGISTER_USER_PAGE: -            self.page(pageId).setSubTitle(self.tr("Register a new user with " -                                                  "%s") % -                                          (self._provider_config -                                           .get_name(),)) +            sub_title = self.tr("Register a new user with {0}") +            sub_title = sub_title.format(self._provider_details.name) +            self.page(pageId).setSubTitle(sub_title)              self.ui.chkRemember.setVisible(False)          if pageId == self.SERVICES_PAGE: @@ -695,8 +706,6 @@ class Wizard(QtGui.QWizard):          if self.currentPage() == self.page(self.SELECT_PROVIDER_PAGE):              if self._use_existing_provider:                  self._domain = self.ui.cbProviders.currentText() -                self._provider_config = ProviderConfig.get_provider_config( -                    self._domain)                  if self._show_register:                      return self.REGISTER_USER_PAGE                  else: @@ -721,6 +730,7 @@ class Wizard(QtGui.QWizard):          sig.prov_name_resolution.connect(self._name_resolution)          sig.prov_https_connection.connect(self._https_connection)          sig.prov_download_provider_info.connect(self._download_provider_info) +        sig.prov_get_details.connect(self._provider_get_details)          sig.prov_download_ca_cert.connect(self._download_ca_cert)          sig.prov_check_ca_fingerprint.connect(self._check_ca_fingerprint) diff --git a/src/leap/bitmask/logs/__init__.py b/src/leap/bitmask/logs/__init__.py new file mode 100644 index 00000000..0516b304 --- /dev/null +++ b/src/leap/bitmask/logs/__init__.py @@ -0,0 +1,3 @@ +# levelname length == 8, since 'CRITICAL' is the longest +LOG_FORMAT = ('%(asctime)s - %(levelname)-8s - ' +              'L#%(lineno)-4s : %(name)s:%(funcName)s() - %(message)s') diff --git a/src/leap/bitmask/util/leap_log_handler.py b/src/leap/bitmask/logs/leap_log_handler.py index 807e53d4..24141638 100644 --- a/src/leap/bitmask/util/leap_log_handler.py +++ b/src/leap/bitmask/logs/leap_log_handler.py @@ -21,7 +21,7 @@ import logging  from PySide import QtCore -from leap.bitmask.util import LOG_FORMAT +from leap.bitmask.logs import LOG_FORMAT  class LogHandler(logging.Handler): diff --git a/src/leap/bitmask/util/log_silencer.py b/src/leap/bitmask/logs/log_silencer.py index 56b290e4..56b290e4 100644 --- a/src/leap/bitmask/util/log_silencer.py +++ b/src/leap/bitmask/logs/log_silencer.py diff --git a/src/leap/bitmask/util/streamtologger.py b/src/leap/bitmask/logs/streamtologger.py index 25a06718..25a06718 100644 --- a/src/leap/bitmask/util/streamtologger.py +++ b/src/leap/bitmask/logs/streamtologger.py diff --git a/src/leap/bitmask/util/tests/test_leap_log_handler.py b/src/leap/bitmask/logs/tests/test_leap_log_handler.py index 518fd35b..20b09aef 100644 --- a/src/leap/bitmask/util/tests/test_leap_log_handler.py +++ b/src/leap/bitmask/logs/tests/test_leap_log_handler.py @@ -24,7 +24,7 @@ except ImportError:  import logging -from leap.bitmask.util.leap_log_handler import LeapLogHandler +from leap.bitmask.logs.leap_log_handler import LeapLogHandler  from leap.bitmask.util.pyside_tests_helper import BasicPySlotCase  from leap.common.testing.basetest import BaseLeapTest diff --git a/src/leap/bitmask/util/tests/test_streamtologger.py b/src/leap/bitmask/logs/tests/test_streamtologger.py index fc97b794..9bbadde8 100644 --- a/src/leap/bitmask/util/tests/test_streamtologger.py +++ b/src/leap/bitmask/logs/tests/test_streamtologger.py @@ -26,7 +26,7 @@ except ImportError:  import logging  import sys -from leap.bitmask.util.streamtologger import StreamToLogger +from leap.bitmask.logs.streamtologger import StreamToLogger  from leap.common.testing.basetest import BaseLeapTest diff --git a/src/leap/bitmask/logs/utils.py b/src/leap/bitmask/logs/utils.py new file mode 100644 index 00000000..06959c45 --- /dev/null +++ b/src/leap/bitmask/logs/utils.py @@ -0,0 +1,92 @@ +import logging +import sys + +from leap.bitmask.logs import LOG_FORMAT +from leap.bitmask.logs.log_silencer import SelectiveSilencerFilter +from leap.bitmask.logs.leap_log_handler import LeapLogHandler +from leap.bitmask.logs.streamtologger import StreamToLogger +from leap.bitmask.platform_init import IS_WIN + + +def get_logger(debug=False, logfile=None, replace_stdout=True): +    """ +    Create the logger and attach the handlers. + +    :param debug: the level of the messages that we should log +    :type debug: bool +    :param logfile: the file name of where we should to save the logs +    :type logfile: str +    :return: the new logger with the attached handlers. +    :rtype: logging.Logger +    """ +    # TODO: get severity from command line args +    if debug: +        level = logging.DEBUG +    else: +        level = logging.WARNING + +    # Create logger and formatter +    logger = logging.getLogger(name='leap') +    logger.setLevel(level) +    formatter = logging.Formatter(LOG_FORMAT) + +    # Console handler +    try: +        import coloredlogs +        console = coloredlogs.ColoredStreamHandler(level=level) +    except ImportError: +        console = logging.StreamHandler() +        console.setLevel(level) +        console.setFormatter(formatter) +        using_coloredlog = False +    else: +        using_coloredlog = True + +    if using_coloredlog: +        replace_stdout = False + +    silencer = SelectiveSilencerFilter() +    console.addFilter(silencer) +    logger.addHandler(console) +    logger.debug('Console handler plugged!') + +    # LEAP custom handler +    leap_handler = LeapLogHandler() +    leap_handler.setLevel(level) +    leap_handler.addFilter(silencer) +    logger.addHandler(leap_handler) +    logger.debug('Leap handler plugged!') + +    # File handler +    if logfile is not None: +        logger.debug('Setting logfile to %s ', logfile) +        fileh = logging.FileHandler(logfile) +        fileh.setLevel(logging.DEBUG) +        fileh.setFormatter(formatter) +        fileh.addFilter(silencer) +        logger.addHandler(fileh) +        logger.debug('File handler plugged!') + +    if replace_stdout: +        replace_stdout_stderr_with_logging(logger) + +    return logger + + +def replace_stdout_stderr_with_logging(logger): +    """ +    Replace: +        - the standard output +        - the standard error +        - the twisted log output +    with a custom one that writes to the logger. +    """ +    # Disabling this on windows since it breaks ALL THE THINGS +    # The issue for this is #4149 +    if not IS_WIN: +        sys.stdout = StreamToLogger(logger, logging.DEBUG) +        sys.stderr = StreamToLogger(logger, logging.ERROR) + +        # Replace twisted's logger to use our custom output. +        from twisted.python import log +        log.startLogging(sys.stdout) diff --git a/src/leap/bitmask/platform_init/initializers.py b/src/leap/bitmask/platform_init/initializers.py index f2710c58..b282a229 100644 --- a/src/leap/bitmask/platform_init/initializers.py +++ b/src/leap/bitmask/platform_init/initializers.py @@ -14,15 +14,14 @@  #  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -  """ -Platform dependant initializing code +Platform-dependant initialization code.  """ -  import logging  import os  import platform  import stat +import sys  import subprocess  import tempfile @@ -33,7 +32,6 @@ from leap.bitmask.services.eip import get_vpn_launcher  from leap.bitmask.services.eip.linuxvpnlauncher import LinuxVPNLauncher  from leap.bitmask.services.eip.darwinvpnlauncher import DarwinVPNLauncher  from leap.bitmask.util import first -from leap.bitmask.util import privilege_policies  logger = logging.getLogger(__name__) @@ -48,7 +46,7 @@ _system = platform.system()  def init_platform():      """ -    Returns the right initializer for the platform we are running in, or +    Return the right initializer for the platform we are running in, or      None if no proper initializer is found      """      initializer = None @@ -80,7 +78,7 @@ UPDOWN_BADEXEC_MSG = BADEXEC_MSG % (  def get_missing_updown_dialog():      """ -    Creates a dialog for notifying of missing updown scripts. +    Create a dialog for notifying of missing updown scripts.      Returns that dialog.      :rtype: QtGui.QMessageBox instance @@ -102,7 +100,7 @@ def get_missing_updown_dialog():  def check_missing():      """ -    Checks for the need of installing missing scripts, and +    Check for the need of installing missing scripts, and      raises a dialog to ask user for permission to do it.      """      config = LeapSettings() @@ -150,7 +148,7 @@ def check_missing():  def _windows_has_tap_device():      """ -    Loops over the windows registry trying to find if the tap0901 tap driver +    Loop over the windows registry trying to find if the tap0901 tap driver      has been installed on this machine.      """      import _winreg as reg @@ -176,7 +174,7 @@ def _windows_has_tap_device():  def WindowsInitializer():      """ -    Raises a dialog in case that the windows tap driver has not been found +    Raise a dialog in case that the windows tap driver has not been found      in the registry, asking the user for permission to install the driver      """      if not _windows_has_tap_device(): @@ -220,7 +218,7 @@ def WindowsInitializer():  def _darwin_has_tun_kext():      """ -    Returns True only if we found a directory under the system kext folder +    Return True only if we found a directory under the system kext folder      containing a kext named tun.kext, AND we found a startup item named 'tun'      """      # XXX we should be smarter here and use kextstats output. @@ -236,7 +234,7 @@ def _darwin_has_tun_kext():  def _darwin_install_missing_scripts(badexec, notfound):      """ -    Tries to install the missing up/down scripts. +    Try to install the missing up/down scripts.      :param badexec: error for notifying execution error during command.      :type badexec: str @@ -291,7 +289,7 @@ def _darwin_install_missing_scripts(badexec, notfound):  def DarwinInitializer():      """ -    Raises a dialog in case that the osx tuntap driver has not been found +    Raise a dialog in case that the osx tuntap driver has not been found      in the registry, asking the user for permission to install the driver      """      # XXX split this function into several @@ -345,9 +343,49 @@ def DarwinInitializer():  #  # Linux initializers  # + +def _get_missing_resolvconf_dialog(): +    """ +    Create a dialog for notifying about missing openresolv. + +    :rtype: QtGui.QMessageBox instance +    """ +    NO_RESOLVCONF = ( +        "Could not find <b>resolvconf</b> installed in your system.\n" +        "Do you want to quit Bitmask now?") + +    EXPLAIN = ( +        "Encrypted Internet needs resolvconf installed to work properly.\n" +        "Please use your package manager to install it.\n") + +    msg = QtGui.QMessageBox() +    msg.setWindowTitle(msg.tr("Missing resolvconf framework")) +    msg.setText(msg.tr(NO_RESOLVCONF)) +    # but maybe the user really deserve to know more +    msg.setInformativeText(msg.tr(EXPLAIN)) +    msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) +    msg.setDefaultButton(QtGui.QMessageBox.Yes) +    return msg + + +def _linux_check_resolvconf(): +    """ +    Raise a dialog warning about the lack of the resolvconf framework. +    """ +    RESOLVCONF_PATH = "/sbin/resolvconf" +    missing = not os.path.isfile(RESOLVCONF_PATH) + +    if missing: +        msg = _get_missing_resolvconf_dialog() +        ret = msg.exec_() + +        if ret == QtGui.QMessageBox.Yes: +            sys.exit() + +  def _linux_install_missing_scripts(badexec, notfound):      """ -    Tries to install the missing up/down scripts. +    Try to install the missing up/down scripts.      :param badexec: error for notifying execution error during command.      :type badexec: str @@ -398,7 +436,11 @@ def _linux_install_missing_scripts(badexec, notfound):  def LinuxInitializer():      """ -    Raises a dialog in case that either updown scripts or policykit file -    are missing or they have incorrect permissions. +    Raise a dialog if needed files are missing. + +    Missing files can be either system-wide resolvconf, bitmask-root, or +    policykit file. The dialog will also be raised if some of those files are +    found to have incorrect permissions.      """ +    _linux_check_resolvconf()      check_missing() diff --git a/src/leap/bitmask/services/eip/conductor.py b/src/leap/bitmask/services/eip/conductor.py new file mode 100644 index 00000000..a8821160 --- /dev/null +++ b/src/leap/bitmask/services/eip/conductor.py @@ -0,0 +1,321 @@ +# -*- coding: utf-8 -*- +# conductor.py +# Copyright (C) 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# 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/>. +""" +EIP Conductor module. +""" +import logging + +from PySide import QtCore + +from leap.bitmask.gui import statemachines +from leap.bitmask.services import EIP_SERVICE +from leap.bitmask.services import get_service_display_name +from leap.bitmask.services.eip.connection import EIPConnection +from leap.bitmask.platform_init import IS_MAC + +QtDelayedCall = QtCore.QTimer.singleShot +logger = logging.getLogger(__name__) + + +class EIPConductor(object): + +    def __init__(self, settings, backend, **kwargs): +        """ +        Initializes EIP Conductor. + +        :param settings: +        :type settings: + +        :param backend: +        :type backend: +        """ +        self.eip_connection = EIPConnection() +        self.eip_name = get_service_display_name(EIP_SERVICE) +        self._settings = settings +        self._backend = backend + +        self._eip_status = None + +    @property +    def qtsigs(self): +        return self.eip_connection.qtsigs + +    def add_eip_widget(self, widget): +        """ +        Keep a reference to the passed eip status widget. + +        :param widget: the EIP Status widget. +        :type widget: QWidget +        """ +        self._eip_status = widget + +    def connect_signals(self): +        """ +        Connect signals. +        """ +        self.qtsigs.connecting_signal.connect(self._start_eip) + +        self.qtsigs.disconnecting_signal.connect(self._stop_eip) +        self.qtsigs.disconnected_signal.connect(self._eip_status.eip_stopped) + +    def connect_backend_signals(self): +        """ +        Connect to backend signals. +        """ +        signaler = self._backend.signaler + +        # for conductor +        signaler.eip_process_restart_tls.connect(self._do_eip_restart) +        signaler.eip_process_restart_tls.connect(self._do_eip_failed) +        signaler.eip_process_restart_ping.connect(self._do_eip_restart) +        signaler.eip_process_finished.connect(self._eip_finished) + +        # for widget +        self._eip_status.connect_backend_signals() + +    def start_eip_machine(self, action): +        """ +        Initializes and starts the EIP state machine. +        Needs the reference to the eip_status widget not to be empty. + +        :action: QtAction +        """ +        action = action +        button = self._eip_status.eip_button +        label = self._eip_status.eip_label + +        builder = statemachines.ConnectionMachineBuilder(self.eip_connection) +        eip_machine = builder.make_machine(button=button, +                                           action=action, +                                           label=label) +        self.eip_machine = eip_machine +        self.eip_machine.start() +        logger.debug('eip machine started') + +    def do_connect(self): +        """ +        Start the connection procedure. +        Emits a signal that triggers the OFF -> Connecting sequence. +        This will call _start_eip via the state machine. +        """ +        self.qtsigs.do_connect_signal.emit() + +    def tear_fw_down(self): +        """ +        Tear the firewall down. +        """ +        self._backend.tear_fw_down() + +    @QtCore.Slot() +    def _start_eip(self): +        """ +        Starts EIP. +        """ +        st = self._eip_status +        is_restart = st and st.is_restart + +        def reconnect(): +            self.qtsigs.disconnecting_signal.connect(self._stop_eip) + +        if is_restart: +            QtDelayedCall(0, reconnect) +        else: +            self._eip_status.eip_pre_up() +        self.user_stopped_eip = False +        self._eip_status.hide_fw_down_button() + +        # Until we set an option in the preferences window, we'll assume that +        # by default we try to autostart. If we switch it off manually, it +        # won't try the next time. +        self._settings.set_autostart_eip(True) +        self._eip_status.is_restart = False + +        # DO the backend call! +        self._backend.eip_start(restart=is_restart) + +    def reconnect_stop_signal(self): +        """ +        Restore the original behaviour associated with the disconnecting +        signal, this is, trigger a normal stop, and not a restart one. +        """ + +        def do_stop(*args): +            self._stop_eip(restart=False) + +        self.qtsigs.disconnecting_signal.disconnect() +        self.qtsigs.disconnecting_signal.connect(do_stop) + +    @QtCore.Slot() +    def _stop_eip(self, restart=False, failed=False): +        """ +        TRIGGERS: +          self.qsigs.do_disconnect_signal (via state machine) + +        Stops vpn process and makes gui adjustments to reflect +        the change of state. + +        :param restart: whether this is part of a eip restart. +        :type restart: bool + +        :param failed: whether this is the final step of a retry sequence +        :type failed: bool +        """ +        self._eip_status.is_restart = restart +        self.user_stopped_eip = not restart and not failed + +        def on_disconnected_do_restart(): +            # hard restarts +            logger.debug("HARD RESTART") +            eip_status_label = self._eip_status.tr("{0} is restarting") +            eip_status_label = eip_status_label.format(self.eip_name) +            self._eip_status.eip_stopped(restart=True) +            self._eip_status.set_eip_status(eip_status_label, error=False) + +            QtDelayedCall(2000, self.do_connect) + +        def plug_restart_on_disconnected(): +            self.qtsigs.disconnected_signal.connect(on_disconnected_do_restart) + +        def reconnect_disconnected_signal(): +            self.qtsigs.disconnected_signal.disconnect( +                on_disconnected_do_restart) + +        def do_stop(*args): +            self._stop_eip(restart=False) + +        if restart: +            # we bypass the on_eip_disconnected here +            plug_restart_on_disconnected() +            self.qtsigs.disconnected_signal.emit() +            #QtDelayedCall(0, self.qtsigs.disconnected_signal.emit) +            # ...and reconnect the original signal again, after having used the +            # diversion +            QtDelayedCall(500, reconnect_disconnected_signal) + +        elif failed: +            self.qtsigs.disconnected_signal.emit() + +        else: +            logger.debug('Setting autostart to: False') +            self._settings.set_autostart_eip(False) + +        # Call to the backend. +        self._backend.eip_stop(restart=restart) + +        # ... and inform the status widget +        self._eip_status.set_eipstatus_off(False) +        self._eip_status.eip_stopped(restart=restart, failed=failed) + +        self._already_started_eip = False + +        # XXX needed? +        if restart: +            QtDelayedCall(2000, self.reconnect_stop_signal) + +    @QtCore.Slot() +    def _do_eip_restart(self): +        """ +        TRIGGERS: +            self._eip_connection.qtsigs.process_restart + +        Restart the connection. +        """ +        if self._eip_status is not None: +            self._eip_status.is_restart = True + +        def do_stop(*args): +            self._stop_eip(restart=True) + +        try: +            self.qtsigs.disconnecting_signal.disconnect() +        except Exception: +            logger.error("cannot disconnect signals") + +        self.qtsigs.disconnecting_signal.connect(do_stop) +        self.qtsigs.do_disconnect_signal.emit() + +    @QtCore.Slot() +    def _do_eip_failed(self): +        """ +        Stop EIP after a failure to start. + +        TRIGGERS +            signaler.eip_process_restart_tls +        """ +        logger.debug("TLS Error: eip_stop (failed)") +        self.qtsigs.connection_died_signal.emit() +        QtDelayedCall(1000, self._eip_status.eip_failed_to_connect) + +    @QtCore.Slot(int) +    def _eip_finished(self, exitCode): +        """ +        TRIGGERS: +            Signaler.eip_process_finished + +        Triggered when the EIP/VPN process finishes to set the UI +        accordingly. + +        Ideally we would have the right exit code here, +        but the use of different wrappers (pkexec, cocoasudo) swallows +        the openvpn exit code so we get zero exit in some cases  where we +        shouldn't. As a workaround we just use a flag to indicate +        a purposeful switch off, and mark everything else as unexpected. + +        :param exitCode: the exit code of the eip process. +        :type exitCode: int +        """ +        # TODO Add error catching to the openvpn log observer +        # so we can have a more precise idea of which type +        # of error did we have (server side, local problem, etc) + +        logger.info("VPN process finished with exitCode %s..." +                    % (exitCode,)) + +        signal = self.qtsigs.disconnected_signal + +        # XXX check if these exitCodes are pkexec/cocoasudo specific +        if exitCode in (126, 127): +            eip_status_label = self._eip_status.tr( +                "{0} could not be launched " +                "because you did not authenticate properly.") +            eip_status_label = eip_status_label.format(self.eip_name) +            self._eip_status.set_eip_status(eip_status_label, error=True) +            signal = self.qtsigs.connection_aborted_signal +            self._backend.eip_terminate() + +        # XXX FIXME --- check exitcode is != 0 really. +        # bitmask-root is masking the exitcode, so we might need +        # to fix it on that side. +        #if exitCode != 0 and not self.user_stopped_eip: +        if not self.user_stopped_eip: +            eip_status_label = self._eip_status.tr( +                "{0} finished in an unexpected manner!") +            eip_status_label = eip_status_label.format(self.eip_name) +            self._eip_status.eip_stopped() +            self._eip_status.set_eip_status_icon("error") +            self._eip_status.set_eip_status(eip_status_label, +                                            error=True) +            signal = self.qtsigs.connection_died_signal +            self._eip_status.show_fw_down_button() +            self._eip_status.eip_failed_to_connect() + +        if exitCode == 0 and IS_MAC: +            # XXX remove this warning after I fix cocoasudo. +            logger.warning("The above exit code MIGHT BE WRONG.") + +        # We emit signals to trigger transitions in the state machine: +        signal.emit() diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py index a03bfc44..41d75052 100644 --- a/src/leap/bitmask/services/eip/darwinvpnlauncher.py +++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py @@ -52,6 +52,8 @@ class DarwinVPNLauncher(VPNLauncher):      OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,)      OPENVPN_PATH_ESCAPED = "%s/Contents/Resources/openvpn" % (          INSTALL_PATH_ESCAPED,) +    OPENVPN_BIN_PATH = "%s/Contents/Resources/%s" % (INSTALL_PATH, +                                                     OPENVPN_BIN)      UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,)      DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,) diff --git a/src/leap/bitmask/services/eip/eipconfig.py b/src/leap/bitmask/services/eip/eipconfig.py index 09a3d257..e7419b22 100644 --- a/src/leap/bitmask/services/eip/eipconfig.py +++ b/src/leap/bitmask/services/eip/eipconfig.py @@ -110,7 +110,7 @@ class VPNGatewaySelector(object):      def get_gateways_list(self):          """ -        Returns the existing gateways, sorted by timezone proximity. +        Return the existing gateways, sorted by timezone proximity.          :rtype: list of tuples (location, ip)                  (str, IPv4Address or IPv6Address object) @@ -148,16 +148,36 @@ class VPNGatewaySelector(object):      def get_gateways(self):          """ -        Returns the 4 best gateways, sorted by timezone proximity. +        Return the 4 best gateways, sorted by timezone proximity.          :rtype: list of IPv4Address or IPv6Address object.          """          gateways = [ip for location, ip in self.get_gateways_list()][:4]          return gateways +    def get_gateways_country_code(self): +        """ +        Return a dict with ipaddress -> country code mapping. + +        :rtype: dict +        """ +        country_codes = {} + +        locations = self._eipconfig.get_locations() +        gateways = self._eipconfig.get_gateways() + +        for idx, gateway in enumerate(gateways): +            gateway_location = gateway.get('location') + +            ip = self._eipconfig.get_gateway_ip(idx) +            if gateway_location is not None: +                ccode = locations[gateway['location']]['country_code'] +                country_codes[ip] = ccode +        return country_codes +      def _get_timezone_distance(self, offset):          ''' -        Returns the distance between the local timezone and +        Return the distance between the local timezone and          the one with offset 'offset'.          :param offset: the distance of a timezone to GMT. @@ -179,7 +199,7 @@ class VPNGatewaySelector(object):      def _get_local_offset(self):          ''' -        Returns the distance between GMT and the local timezone. +        Return the distance between GMT and the local timezone.          :rtype: int          ''' diff --git a/src/leap/bitmask/services/eip/linuxvpnlauncher.py b/src/leap/bitmask/services/eip/linuxvpnlauncher.py index 1f0813e0..955768d1 100644 --- a/src/leap/bitmask/services/eip/linuxvpnlauncher.py +++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py @@ -63,14 +63,20 @@ def _is_auth_agent_running():      :return: True if it's running, False if it's not.      :rtype: boolean      """ +    # Note that gnome-shell does not uses a separate process for the +    # polkit-agent, it uses a polkit-agent within its own process so we can't +    # ps-grep a polkit process, we can ps-grep gnome-shell itself. +      # the [x] thing is to avoid grep match itself      polkit_options = [          'ps aux | grep "polkit-[g]nome-authentication-agent-1"',          'ps aux | grep "polkit-[k]de-authentication-agent-1"',          'ps aux | grep "polkit-[m]ate-authentication-agent-1"', -        'ps aux | grep "[l]xpolkit"' +        'ps aux | grep "[l]xpolkit"', +        'ps aux | grep "[g]nome-shell"',      ]      is_running = [commands.getoutput(cmd) for cmd in polkit_options] +      return any(is_running) diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py index dcb48e8a..9629afae 100644 --- a/src/leap/bitmask/services/eip/vpnlauncher.py +++ b/src/leap/bitmask/services/eip/vpnlauncher.py @@ -25,6 +25,7 @@ import stat  from abc import ABCMeta, abstractmethod  from functools import partial +from leap.bitmask.config import flags  from leap.bitmask.config.leapsettings import LeapSettings  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.platform_init import IS_LINUX @@ -122,9 +123,9 @@ class VPNLauncher(object):          leap_settings = LeapSettings()          domain = providerconfig.get_domain()          gateway_conf = leap_settings.get_selected_gateway(domain) +        gateway_selector = VPNGatewaySelector(eipconfig)          if gateway_conf == leap_settings.GATEWAY_AUTOMATIC: -            gateway_selector = VPNGatewaySelector(eipconfig)              gateways = gateway_selector.get_gateways()          else:              gateways = [gateway_conf] @@ -133,6 +134,12 @@ class VPNLauncher(object):              logger.error('No gateway was found!')              raise VPNLauncherException('No gateway was found!') +        # this only works for selecting the first gateway, as we're +        # currently doing. +        ccodes = gateway_selector.get_gateways_country_code() +        gateway_ccode = ccodes[gateways[0]] +        flags.CURRENT_VPN_COUNTRY = gateway_ccode +          logger.debug("Using gateways ips: {0}".format(', '.join(gateways)))          return gateways diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index 1559ea8b..f56d464e 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -17,6 +17,7 @@  """  VPN Manager, spawned in a custom processProtocol.  """ +import commands  import logging  import os  import shutil @@ -30,9 +31,11 @@ import psutil  try:      # psutil < 2.0.0      from psutil.error import AccessDenied as psutil_AccessDenied +    PSUTIL_2 = False  except ImportError:      # psutil >= 2.0.0      from psutil import AccessDenied as psutil_AccessDenied +    PSUTIL_2 = True  from leap.bitmask.config import flags  from leap.bitmask.config.providerconfig import ProviderConfig @@ -67,7 +70,7 @@ class VPNObserver(object):          'NETWORK_UNREACHABLE': (              'Network is unreachable (code=101)',),          'PROCESS_RESTART_TLS': ( -            "SIGUSR1[soft,tls-error]",), +            "SIGTERM[soft,tls-error]",),          'PROCESS_RESTART_PING': (              "SIGTERM[soft,ping-restart]",),          'INITIALIZATION_COMPLETED': ( @@ -113,10 +116,12 @@ class VPNObserver(object):          :returns: a Signaler signal or None          :rtype: str or None          """ +        sig = self._signaler          signals = { -            "network_unreachable": self._signaler.EIP_NETWORK_UNREACHABLE, -            "process_restart_tls": self._signaler.EIP_PROCESS_RESTART_TLS, -            "process_restart_ping": self._signaler.EIP_PROCESS_RESTART_PING, +            "network_unreachable": sig.EIP_NETWORK_UNREACHABLE, +            "process_restart_tls": sig.EIP_PROCESS_RESTART_TLS, +            "process_restart_ping": sig.EIP_PROCESS_RESTART_PING, +            "initialization_completed": sig.EIP_CONNECTED          }          return signals.get(event.lower()) @@ -178,6 +183,8 @@ class VPN(object):          kwargs['openvpn_verb'] = self._openvpn_verb          kwargs['signaler'] = self._signaler +        restart = kwargs.pop('restart', False) +          # start the main vpn subprocess          vpnproc = VPNProcess(*args, **kwargs) @@ -188,8 +195,9 @@ class VPN(object):          # we try to bring the firewall up          if IS_LINUX:              gateways = vpnproc.getGateways() -            firewall_up = self._launch_firewall(gateways) -            if not firewall_up: +            firewall_up = self._launch_firewall(gateways, +                                                restart=restart) +            if not restart and not firewall_up:                  logger.error("Could not bring firewall up, "                               "aborting openvpn launch.")                  return @@ -211,7 +219,7 @@ class VPN(object):          self._pollers.extend(poll_list)          self._start_pollers() -    def _launch_firewall(self, gateways): +    def _launch_firewall(self, gateways, restart=False):          """          Launch the firewall using the privileged wrapper. @@ -226,11 +234,24 @@ class VPN(object):          # XXX could check that the iptables rules are in place.          BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT -        exitCode = subprocess.call(["pkexec", -                                    BM_ROOT, "firewall", "start"] + gateways) +        cmd = ["pkexec", BM_ROOT, "firewall", "start"] +        if restart: +            cmd.append("restart") +        exitCode = subprocess.call(cmd + gateways)          return True if exitCode is 0 else False -    def _tear_down_firewall(self): +    def is_fw_down(self): +        """ +        Return whether the firewall is down or not. + +        :rtype: bool +        """ +        BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT +        fw_up_cmd = "pkexec {0} firewall isup".format(BM_ROOT) +        fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256 +        return fw_is_down() + +    def tear_down_firewall(self):          """          Tear the firewall down using the privileged wrapper.          """ @@ -254,7 +275,7 @@ class VPN(object):                  # we try to tear the firewall down                  if IS_LINUX and self._user_stopped: -                    firewall_down = self._tear_down_firewall() +                    firewall_down = self.tear_down_firewall()                      if firewall_down:                          logger.debug("Firewall down")                      else: @@ -286,22 +307,28 @@ class VPN(object):              self._vpnproc.aborted = True              self._vpnproc.killProcess() -    def terminate(self, shutdown=False): +    def terminate(self, shutdown=False, restart=False):          """          Stops the openvpn subprocess.          Attempts to send a SIGTERM first, and after a timeout          it sends a SIGKILL. + +        :param shutdown: whether this is the final shutdown +        :type shutdown: bool +        :param restart: whether this stop is part of a hard restart. +        :type restart: bool          """          from twisted.internet import reactor          self._stop_pollers() -        # We assume that the only valid shutodowns are initiated -        # by an user action. -        self._user_stopped = shutdown -          # First we try to be polite and send a SIGTERM... -        if self._vpnproc: +        if self._vpnproc is not None: +            # We assume that the only valid stops are initiated +            # by an user action, not hard restarts +            self._user_stopped = not restart +            self._vpnproc.is_restart = restart +              self._sentterm = True              self._vpnproc.terminate_openvpn(shutdown=shutdown) @@ -310,13 +337,12 @@ class VPN(object):              reactor.callLater(                  self.TERMINATE_WAIT, self._kill_if_left_alive) -            if shutdown: -                if IS_LINUX and self._user_stopped: -                    firewall_down = self._tear_down_firewall() -                    if firewall_down: -                        logger.debug("Firewall down") -                    else: -                        logger.warning("Could not tear firewall down") +            if IS_LINUX and self._user_stopped: +                firewall_down = self.tear_down_firewall() +                if firewall_down: +                    logger.debug("Firewall down") +                else: +                    logger.warning("Could not tear firewall down")      def _start_pollers(self):          """ @@ -676,7 +702,13 @@ class VPNManager(object):                  # we need to be able to filter out arguments in the form                  # --openvpn-foo, since otherwise we are shooting ourselves                  # in the feet. -                if any(map(lambda s: s.find("LEAPOPENVPN") != -1, p.cmdline)): + +                if PSUTIL_2: +                    cmdline = p.cmdline() +                else: +                    cmdline = p.cmdline +                if any(map(lambda s: s.find( +                        "LEAPOPENVPN") != -1, cmdline)):                      openvpn_process = p                      break              except psutil_AccessDenied: @@ -731,7 +763,7 @@ class VPNManager(object):                  # However, that should be a rare case right now.                  self._send_command("signal SIGTERM")                  self._close_management_socket(announce=True) -            except Exception as e: +            except (Exception, AssertionError) as e:                  logger.warning("Problem trying to terminate OpenVPN: %r"                                 % (e,))          else: @@ -800,6 +832,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):          self._openvpn_verb = openvpn_verb          self._vpn_observer = VPNObserver(signaler) +        self.is_restart = False      # processProtocol methods @@ -835,7 +868,8 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):          exit_code = reason.value.exitCode          if isinstance(exit_code, int):              logger.debug("processExited, status %d" % (exit_code,)) -        self._signaler.signal(self._signaler.EIP_PROCESS_FINISHED, exit_code) +        self._signaler.signal( +            self._signaler.EIP_PROCESS_FINISHED, exit_code)          self._alive = False      def processEnded(self, reason): diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py index 1766a39d..98b40929 100644 --- a/src/leap/bitmask/services/mail/conductor.py +++ b/src/leap/bitmask/services/mail/conductor.py @@ -19,15 +19,10 @@ Mail Services Conductor  """  import logging -from zope.proxy import sameProxiedObjects - +from leap.bitmask.config import flags  from leap.bitmask.gui import statemachines  from leap.bitmask.services.mail import connection as mail_connection -from leap.bitmask.services.mail import imap -from leap.bitmask.services.mail.smtpbootstrapper import SMTPBootstrapper -from leap.bitmask.services.mail.smtpconfig import SMTPConfig -from leap.common.check import leap_assert  from leap.common.events import events_pb2 as leap_events  from leap.common.events import register as leap_register @@ -44,9 +39,6 @@ class IMAPControl(object):          Initializes smtp variables.          """          self.imap_machine = None -        self.imap_service = None -        self.imap_port = None -        self.imap_factory = None          self.imap_connection = None          leap_register(signal=leap_events.IMAP_SERVICE_STARTED, @@ -55,10 +47,13 @@ class IMAPControl(object):          leap_register(signal=leap_events.IMAP_SERVICE_FAILED_TO_START,                        callback=self._handle_imap_events,                        reqcbk=lambda req, resp: None) +        leap_register(signal=leap_events.IMAP_CLIENT_LOGIN, +                      callback=self._handle_imap_events, +                      reqcbk=lambda req, resp: None)      def set_imap_connection(self, imap_connection):          """ -        Sets the imap connection to an initialized connection. +        Set the imap connection to an initialized connection.          :param imap_connection: an initialized imap connection          :type imap_connection: IMAPConnection instance. @@ -67,67 +62,18 @@ class IMAPControl(object):      def start_imap_service(self):          """ -        Starts imap service. +        Start imap service.          """ -        from leap.bitmask.config import flags - -        logger.debug('Starting imap service') -        leap_assert(sameProxiedObjects(self._soledad, None) -                    is not True, -                    "We need a non-null soledad for initializing imap service") -        leap_assert(sameProxiedObjects(self._keymanager, None) -                    is not True, -                    "We need a non-null keymanager for initializing imap " -                    "service") - -        offline = flags.OFFLINE -        self.imap_service, self.imap_port, \ -            self.imap_factory = imap.start_imap_service( -                self._soledad, -                self._keymanager, -                userid=self.userid, -                offline=offline) +        self._backend.imap_start_service(self.userid, flags.OFFLINE) -        if offline is False: -            logger.debug("Starting loop") -            self.imap_service.start_loop() - -    def stop_imap_service(self, cv): +    def stop_imap_service(self):          """ -        Stops imap service (fetcher, factory and port). - -        :param cv: A condition variable to which we can signal when imap -                   indeed stops. -        :type cv: threading.Condition +        Stop imap service.          """          self.imap_connection.qtsigs.disconnecting_signal.emit() -        # TODO We should homogenize both services. -        if self.imap_service is not None: -            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 -            self.imap_factory.theAccount.closed = True -            self.imap_factory.doStop(cv) -        else: -            # main window does not have to wait because there's no service to -            # be stopped, so we release the condition variable -            cv.acquire() -            cv.notify() -            cv.release() - -    def fetch_incoming_mail(self): -        """ -        Fetches incoming mail. -        """ -        if self.imap_service: -            logger.debug('Client connected, fetching mail...') -            self.imap_service.fetch() - -    # handle events +        logger.debug('Stopping imap service.') + +        self._backend.imap_stop_service()      def _handle_imap_events(self, req):          """ @@ -137,25 +83,31 @@ class IMAPControl(object):          :type req: leap.common.events.events_pb2.SignalRequest          """          if req.event == leap_events.IMAP_SERVICE_STARTED: -            self.on_imap_connected() +            self._on_imap_connected()          elif req.event == leap_events.IMAP_SERVICE_FAILED_TO_START: -            self.on_imap_failed() +            self._on_imap_failed() +        elif req.event == leap_events.IMAP_CLIENT_LOGIN: +            self._on_mail_client_logged_in() -    # emit connection signals +    def _on_mail_client_logged_in(self): +        """ +        On mail client logged in, fetch incoming mail. +        """ +        self._controller.imap_service_fetch() -    def on_imap_connecting(self): +    def _on_imap_connecting(self):          """          Callback for IMAP connecting state.          """          self.imap_connection.qtsigs.connecting_signal.emit() -    def on_imap_connected(self): +    def _on_imap_connected(self):          """          Callback for IMAP connected state.          """          self.imap_connection.qtsigs.connected_signal.emit() -    def on_imap_failed(self): +    def _on_imap_failed(self):          """          Callback for IMAP failed state.          """ @@ -167,12 +119,9 @@ class SMTPControl(object):          """          Initializes smtp variables.          """ -        self.smtp_config = SMTPConfig()          self.smtp_connection = None          self.smtp_machine = None -        self.smtp_bootstrapper = SMTPBootstrapper() -          leap_register(signal=leap_events.SMTP_SERVICE_STARTED,                        callback=self._handle_smtp_events,                        reqcbk=lambda req, resp: None) @@ -188,29 +137,23 @@ class SMTPControl(object):          """          self.smtp_connection = smtp_connection -    def start_smtp_service(self, provider_config, download_if_needed=False): +    def start_smtp_service(self, download_if_needed=False):          """          Starts the SMTP service. -        :param provider_config: Provider configuration -        :type provider_config: ProviderConfig          :param download_if_needed: True if it should check for mtime                                     for the file          :type download_if_needed: bool          """          self.smtp_connection.qtsigs.connecting_signal.emit() -        self.smtp_bootstrapper.start_smtp_service( -            provider_config, self.smtp_config, self._keymanager, -            self.userid, download_if_needed) +        self._backend.smtp_start_service(self.userid, download_if_needed)      def stop_smtp_service(self):          """          Stops the SMTP service.          """          self.smtp_connection.qtsigs.disconnecting_signal.emit() -        self.smtp_bootstrapper.stop_smtp_service() - -    # handle smtp events +        self._backend.smtp_stop_service()      def _handle_smtp_events(self, req):          """ @@ -224,8 +167,6 @@ class SMTPControl(object):          elif req.event == leap_events.SMTP_SERVICE_FAILED_TO_START:              self.on_smtp_failed() -    # emit connection signals -      def on_smtp_connecting(self):          """          Callback for SMTP connecting state. @@ -253,22 +194,17 @@ class MailConductor(IMAPControl, SMTPControl):      """      # XXX We could consider to use composition instead of inheritance here. -    def __init__(self, soledad, keymanager): +    def __init__(self, backend):          """          Initializes the mail conductor. -        :param soledad: a transparent proxy that eventually will point to a -                        Soledad Instance. -        :type soledad: zope.proxy.ProxyBase - -        :param keymanager: a transparent proxy that eventually will point to a -                           Keymanager Instance. -        :type keymanager: zope.proxy.ProxyBase +        :param backend: Backend being used +        :type backend: Backend          """          IMAPControl.__init__(self)          SMTPControl.__init__(self) -        self._soledad = soledad -        self._keymanager = keymanager + +        self._backend = backend          self._mail_machine = None          self._mail_connection = mail_connection.MailConnection() @@ -309,6 +245,13 @@ class MailConductor(IMAPControl, SMTPControl):          self._smtp_machine = smtp          self._smtp_machine.start() +    def stop_mail_services(self): +        """ +        Stop the IMAP and SMTP services. +        """ +        self.stop_imap_service() +        self.stop_smtp_service() +      def connect_mail_signals(self, widget):          """          Connects the mail signals to the mail_status widget slots. diff --git a/src/leap/bitmask/services/mail/imapcontroller.py b/src/leap/bitmask/services/mail/imapcontroller.py new file mode 100644 index 00000000..d0bf4c34 --- /dev/null +++ b/src/leap/bitmask/services/mail/imapcontroller.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# imapcontroller.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/>. +""" +IMAP service controller. +""" +import logging + +from leap.bitmask.services.mail import imap + + +logger = logging.getLogger(__name__) + + +class IMAPController(object): +    """ +    IMAP Controller. +    """ +    def __init__(self, soledad, keymanager): +        """ +        Initialize IMAP variables. + +        :param soledad: a transparent proxy that eventually will point to a +                        Soledad Instance. +        :type soledad: zope.proxy.ProxyBase +        :param keymanager: a transparent proxy that eventually will point to a +                           Keymanager Instance. +        :type keymanager: zope.proxy.ProxyBase +        """ +        self._soledad = soledad +        self._keymanager = keymanager + +        self.imap_service = None +        self.imap_port = None +        self.imap_factory = None + +    def start_imap_service(self, userid, offline=False): +        """ +        Start IMAP service. + +        :param userid: user id, in the form "user@provider" +        :type userid: str +        :param offline: whether imap should start in offline mode or not. +        :type offline: bool +        """ +        logger.debug('Starting imap service') + +        self.imap_service, self.imap_port, \ +            self.imap_factory = imap.start_imap_service( +                self._soledad, +                self._keymanager, +                userid=userid, +                offline=offline) + +        if offline is False: +            logger.debug("Starting loop") +            self.imap_service.start_loop() + +    def stop_imap_service(self, cv): +        """ +        Stop IMAP service (fetcher, factory and port). + +        :param cv: A condition variable to which we can signal when imap +                   indeed stops. +        :type cv: threading.Condition +        """ +        if self.imap_service is not None: +            # 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 +            self.imap_factory.theAccount.closed = True +            self.imap_factory.doStop(cv) +        else: +            # Release the condition variable so the caller doesn't have to wait +            cv.acquire() +            cv.notify() +            cv.release() + +    def fetch_incoming_mail(self): +        """ +        Fetch incoming mail. +        """ +        if self.imap_service: +            logger.debug('Client connected, fetching mail...') +            self.imap_service.fetch() diff --git a/src/leap/bitmask/services/mail/smtpbootstrapper.py b/src/leap/bitmask/services/mail/smtpbootstrapper.py index 7ecf8134..3ef755e8 100644 --- a/src/leap/bitmask/services/mail/smtpbootstrapper.py +++ b/src/leap/bitmask/services/mail/smtpbootstrapper.py @@ -28,7 +28,7 @@ from leap.bitmask.services.mail.smtpconfig import SMTPConfig  from leap.bitmask.util import is_file  from leap.common import certs as leap_certs -from leap.common.check import leap_assert, leap_assert_type +from leap.common.check import leap_assert  from leap.common.files import check_and_fix_urw_only  logger = logging.getLogger(__name__) @@ -38,6 +38,10 @@ class NoSMTPHosts(Exception):      """This is raised when there is no SMTP host to use.""" +class MalformedUserId(Exception): +    """This is raised when an userid does not have the form user@provider.""" + +  class SMTPBootstrapper(AbstractBootstrapper):      """      SMTP init procedure @@ -126,15 +130,10 @@ class SMTPBootstrapper(AbstractBootstrapper):              smtp_key=client_cert_path,              encrypted_only=False) -    def start_smtp_service(self, provider_config, smtp_config, keymanager, -                           userid, download_if_needed=False): +    def start_smtp_service(self, keymanager, userid, download_if_needed=False):          """          Starts the SMTP service. -        :param provider_config: Provider configuration -        :type provider_config: ProviderConfig -        :param smtp_config: SMTP configuration to populate -        :type smtp_config: SMTPConfig          :param keymanager: a transparent proxy that eventually will point to a                             Keymanager Instance.          :type keymanager: zope.proxy.ProxyBase @@ -144,13 +143,16 @@ class SMTPBootstrapper(AbstractBootstrapper):                                     for the file          :type download_if_needed: bool          """ -        leap_assert_type(provider_config, ProviderConfig) -        leap_assert_type(smtp_config, SMTPConfig) +        try: +            username, domain = userid.split('@') +        except ValueError: +            logger.critical("Malformed userid parameter!") +            raise MalformedUserId() -        self._provider_config = provider_config +        self._provider_config = ProviderConfig.get_provider_config(domain)          self._keymanager = keymanager -        self._smtp_config = smtp_config -        self._useid = userid +        self._smtp_config = SMTPConfig() +        self._userid = userid          self._download_if_needed = download_if_needed          try: diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index 6bb7c036..db12fd80 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -25,7 +25,6 @@ import sys  from ssl import SSLError  from sqlite3 import ProgrammingError as sqlite_ProgrammingError -from PySide import QtCore  from u1db import errors as u1db_errors  from twisted.internet import threads  from zope.proxy import sameProxiedObjects @@ -134,16 +133,11 @@ class SoledadBootstrapper(AbstractBootstrapper):      MAX_INIT_RETRIES = 10      MAX_SYNC_RETRIES = 10 -    # All dicts returned are of the form -    # {"passed": bool, "error": str} -    download_config = QtCore.Signal(dict) -    gen_key = QtCore.Signal(dict) -    local_only_ready = QtCore.Signal(dict) -    soledad_invalid_auth_token = QtCore.Signal() -    soledad_failed = QtCore.Signal() +    def __init__(self, signaler=None): +        AbstractBootstrapper.__init__(self, signaler) -    def __init__(self): -        AbstractBootstrapper.__init__(self) +        if signaler is not None: +            self._cancel_signal = signaler.SOLEDAD_CANCELLED_BOOTSTRAP          self._provider_config = None          self._soledad_config = None @@ -181,16 +175,23 @@ class SoledadBootstrapper(AbstractBootstrapper):          Instantiate Soledad for offline use.          :param username: full user id (user@provider) -        :type username: basestring +        :type username: str or unicode          :param password: the soledad passphrase          :type password: unicode          :param uuid: the user uuid -        :type uuid: basestring +        :type uuid: str or unicode          """          print "UUID ", uuid          self._address = username +        self._password = password          self._uuid = uuid -        return self.load_and_sync_soledad(uuid, offline=True) +        try: +            self.load_and_sync_soledad(uuid, offline=True) +            self._signaler.signal(self._signaler.SOLEDAD_OFFLINE_FINISHED) +        except Exception as e: +            # TODO: we should handle more specific exceptions in here +            logger.exception(e) +            self._signaler.signal(self._signaler.SOLEDAD_OFFLINE_FAILED)      def _get_soledad_local_params(self, uuid, offline=False):          """ @@ -245,7 +246,7 @@ class SoledadBootstrapper(AbstractBootstrapper):      def _do_soledad_init(self, uuid, secrets_path, local_db_path,                           server_url, cert_file, token):          """ -        Initialize soledad, retry if necessary and emit soledad_failed if we +        Initialize soledad, retry if necessary and raise an exception if we          can't succeed.          :param uuid: user identifier @@ -263,19 +264,22 @@ class SoledadBootstrapper(AbstractBootstrapper):          :param auth token: auth token          :type auth_token: str          """ -        init_tries = self.MAX_INIT_RETRIES -        while init_tries > 0: +        init_tries = 1 +        while init_tries <= self.MAX_INIT_RETRIES:              try: +                logger.debug("Trying to init soledad....")                  self._try_soledad_init(                      uuid, secrets_path, local_db_path,                      server_url, cert_file, token)                  logger.debug("Soledad has been initialized.")                  return              except Exception: -                init_tries -= 1 +                init_tries += 1 +                msg = "Init failed, retrying... (retry {0} of {1})".format( +                    init_tries, self.MAX_INIT_RETRIES) +                logger.warning(msg)                  continue -        self.soledad_failed.emit()          raise SoledadInitError()      def load_and_sync_soledad(self, uuid=None, offline=False): @@ -306,9 +310,8 @@ class SoledadBootstrapper(AbstractBootstrapper):          leap_assert(not sameProxiedObjects(self._soledad, None),                      "Null soledad, error while initializing") -        if flags.OFFLINE is True: +        if flags.OFFLINE:              self._init_keymanager(self._address, token) -            self.local_only_ready.emit({self.PASSED_KEY: True})          else:              try:                  address = make_address( @@ -353,9 +356,10 @@ class SoledadBootstrapper(AbstractBootstrapper):          Do several retries to get an initial soledad sync.          """          # and now, let's sync -        sync_tries = self.MAX_SYNC_RETRIES -        while sync_tries > 0: +        sync_tries = 1 +        while sync_tries <= self.MAX_SYNC_RETRIES:              try: +                logger.debug("Trying to sync soledad....")                  self._try_soledad_sync()                  logger.debug("Soledad has been synced.")                  # so long, and thanks for all the fish @@ -368,19 +372,20 @@ class SoledadBootstrapper(AbstractBootstrapper):                  # retry strategy can be pushed to u1db, or at least                  # it's something worthy to talk about with the                  # ubuntu folks. -                sync_tries -= 1 +                sync_tries += 1 +                msg = "Sync failed, retrying... (retry {0} of {1})".format( +                    sync_tries, self.MAX_SYNC_RETRIES) +                logger.warning(msg)                  continue              except InvalidAuthTokenError: -                self.soledad_invalid_auth_token.emit() +                self._signaler.signal( +                    self._signaler.SOLEDAD_INVALID_AUTH_TOKEN)                  raise              except Exception as e:                  logger.exception("Unhandled error while syncing "                                   "soledad: %r" % (e,))                  break -        # reached bottom, failed to sync -        # and there's nothing we can do... -        self.soledad_failed.emit()          raise SoledadSyncError()      def _try_soledad_init(self, uuid, secrets_path, local_db_path, @@ -443,7 +448,6 @@ class SoledadBootstrapper(AbstractBootstrapper):          Raises SoledadSyncError if not successful.          """          try: -            logger.debug("trying to sync soledad....")              self._soledad.sync()          except SSLError as exc:              logger.error("%r" % (exc,)) @@ -467,7 +471,6 @@ class SoledadBootstrapper(AbstractBootstrapper):          """          Download the Soledad config for the given provider          """ -          leap_assert(self._provider_config,                      "We need a provider configuration!")          logger.debug("Downloading Soledad config for %s" % @@ -480,14 +483,6 @@ class SoledadBootstrapper(AbstractBootstrapper):              self._session,              self._download_if_needed) -        # soledad config is ok, let's proceed to load and sync soledad -        # XXX but honestly, this is a pretty strange entry point for that. -        # it feels like it should be the other way around: -        # load_and_sync, and from there, if needed, call download_config - -        uuid = self.srpauth.get_uuid() -        self.load_and_sync_soledad(uuid) -      def _get_gpg_bin_path(self):          """          Return the path to gpg binary. @@ -574,7 +569,7 @@ class SoledadBootstrapper(AbstractBootstrapper):                  logger.exception(exc)                  # but we do not raise -    def _gen_key(self, _): +    def _gen_key(self):          """          Generates the key pair if needed, uploads it to the webapp and          nickserver @@ -613,10 +608,7 @@ class SoledadBootstrapper(AbstractBootstrapper):          logger.debug("Key generated successfully.") -    def run_soledad_setup_checks(self, -                                 provider_config, -                                 user, -                                 password, +    def run_soledad_setup_checks(self, provider_config, user, password,                                   download_if_needed=False):          """          Starts the checks needed for a new soledad setup @@ -640,9 +632,27 @@ class SoledadBootstrapper(AbstractBootstrapper):          self._user = user          self._password = password -        cb_chain = [ -            (self._download_config, self.download_config), -            (self._gen_key, self.gen_key) -        ] +        if flags.OFFLINE: +            signal_finished = self._signaler.SOLEDAD_OFFLINE_FINISHED +            signal_failed = self._signaler.SOLEDAD_OFFLINE_FAILED +        else: +            signal_finished = self._signaler.SOLEDAD_BOOTSTRAP_FINISHED +            signal_failed = self._signaler.SOLEDAD_BOOTSTRAP_FAILED -        return self.addCallbackChain(cb_chain) +        try: +            self._download_config() + +            # soledad config is ok, let's proceed to load and sync soledad +            uuid = self.srpauth.get_uuid() +            self.load_and_sync_soledad(uuid) + +            if not flags.OFFLINE: +                self._gen_key() + +            self._signaler.signal(signal_finished) +        except Exception as e: +            # TODO: we should handle more specific exceptions in here +            self._soledad = None +            self._keymanager = None +            logger.exception("Error while bootstrapping Soledad: %r" % (e, )) +            self._signaler.signal(signal_failed) diff --git a/src/leap/bitmask/util/__init__.py b/src/leap/bitmask/util/__init__.py index 2b2cd874..c35be99e 100644 --- a/src/leap/bitmask/util/__init__.py +++ b/src/leap/bitmask/util/__init__.py @@ -28,11 +28,6 @@ from leap.common.config import get_path_prefix as common_get_path_prefix  # We'll give your money back if it does not alleviate the eye strain, at least. -# levelname length == 8, since 'CRITICAL' is the longest -LOG_FORMAT = ('%(asctime)s - %(levelname)-8s - ' -              'L#%(lineno)-4s : %(name)s:%(funcName)s() - %(message)s') - -  def first(things):      """      Return the head of a collection. diff --git a/src/leap/bitmask/util/password.py b/src/leap/bitmask/util/credentials.py index 73659f0d..07ded17b 100644 --- a/src/leap/bitmask/util/password.py +++ b/src/leap/bitmask/util/credentials.py @@ -1,5 +1,5 @@  # -*- coding: utf-8 -*- -# password.py +# credentials.py  # Copyright (C) 2013 LEAP  #  # This program is free software: you can redistribute it and/or modify @@ -16,14 +16,34 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  """ -Password utilities +Credentials utilities  """ -from PySide import QtCore +from PySide import QtCore, QtGui  WEAK_PASSWORDS = ("123456", "qweasd", "qwerty", "password") +USERNAME_REGEX = r"^[A-Za-z][A-Za-z\d_\-\.]+[A-Za-z\d]$" +USERNAME_VALIDATOR = QtGui.QRegExpValidator(QtCore.QRegExp(USERNAME_REGEX)) -def basic_password_checks(username, password, password2): + +def username_checks(username): +    # translation helper +    _tr = QtCore.QObject().tr + +    message = None + +    if message is None and len(username) < 2: +        message = _tr("Username must have at least 2 characters") + +    valid = USERNAME_VALIDATOR.validate(username, 0) +    valid_username = valid[0] == QtGui.QValidator.State.Acceptable +    if message is None and not valid_username: +        message = _tr("Invalid username") + +    return message is None, message + + +def password_checks(username, password, password2):      """      Performs basic password checks to avoid really easy passwords. @@ -46,6 +66,9 @@ def basic_password_checks(username, password, password2):      if message is None and password != password2:          message = _tr("Passwords don't match") +    if message is None and not password: +        message = _tr("You can't use an empty password") +      if message is None and len(password) < 6:          message = _tr("Password too short") | 
