diff options
40 files changed, 3415 insertions, 1386 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1728f358..b429595b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,58 @@ History 2014 ==== +0.5.1 May 16 -- the "lil less leaky" release: ++++++++++++++++++++++++++++++++++++++++++++++ + +- Use non blocking dialog so the Pastebin result does not block the + app. Closes #5404. +- Handle provider setup problems and show an error to the user. Closes + #5424. +- Disable providers combo box during check and enable combo or line + edit depending on radio button. Closes #5495. +- Hide the bandwidth widget and update status icon if the openvpn + process is killed. Closes #5497. +- Change password doesn't work. Closes #5540. +- Hide services that the current logged in provider does not + have. Closes #5550. +- If we don't have a provider supporting that service we hide the + actions along with the widgets. Related to #5550. +- Client mistakenly says that traffic is routed in the clear. Closes + #5551. +- Avoid user getting errors if he does a 'ctrl-c' on the wizard during + the first run. Closes #5559. +- Download/upload rates were displayed backwards in the widget + rate. Closes #5563. +- Fix unable to login issue. Closes #5581. +- Hardcode paths for openvpn if STANDALONE=True. Related: #5592 +- Increase waiting time to wait for polkit agent to be up. Closes: + #5595 +- Use openvpn hard restart. Closes: #5669 +- Enable Turn ON button for EIP whenever possible (json and cert are + in place). Fixes #5665, #5666. +- Fix Logout button bottom margin. Fixes #4987. +- Properly finish the Qt app before stopping the reactor. +- Let OpenVPN run its course when a ping-restart happens. Fixes #5564. +- Refactor smtp logic into its bootstrapper. +- Add flag to allow the user to start the app hidden in the + tray. Closes #4990. +- Refactor: move SRPAuth to the backend. Closes #5347. +- Refactor: move EIP to backend. Closes #5349. +- Use PySide @Slot decorator instead of 'SLOT' docstring. Closes + #5506. +- Advanced key management: show a note to the user if the provider + does not support Encrypted Email. Closes #5513. +- Gracefully handle SIGTERM, with addSystemEventTrigger twisted + reactor's method. Closes #5672. +- Hide the main window on quit as first thing and show a tooltip to + inform that we are closing. +- Increase expiration life of a pastebin log from 1 week to 1 month. +- Use iptables firewall. Closes: #5588 +- Refactor Soledad initialization retries to SoledadBootstrapper. +- Refactor EIPBootstrapper to the backend. Closes #5348. +- Add flag to skip provider checks in wizard (only for testing). +- Add support for Mate's polkit agent. + 0.5.0 Apr 4 -- the "Long time no see" release: ++++++++++++++++++++++++++++++++++++++++++++++ - Fix logging out typo, closes #4815. diff --git a/pkg/linux/README b/pkg/linux/README deleted file mode 100644 index 7410789b..00000000 --- a/pkg/linux/README +++ /dev/null @@ -1,4 +0,0 @@ -= Files = -In GNU/Linux, we expect these files to be in place: - -resolv-update -> /etc/leap/resolv-update diff --git a/pkg/linux/README.rst b/pkg/linux/README.rst new file mode 100644 index 00000000..220565ff --- /dev/null +++ b/pkg/linux/README.rst @@ -0,0 +1,10 @@ +Files +===== + +In GNU/Linux, we expect these files to be in place:: + + update-resolv-conf -> /etc/leap/update-resolv-conf + resolv-update -> /etc/leap/resolv-update + + bitmask-root -> /usr/sbin/bitmask-root + polkit/se.leap.bitmask.policy -> /usr/share/polkit-1/actions/se.leap.bitmask.policy diff --git a/pkg/linux/bitmask-root b/pkg/linux/bitmask-root new file mode 100755 index 00000000..136fd6a4 --- /dev/null +++ b/pkg/linux/bitmask-root @@ -0,0 +1,829 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# 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/>. +# +""" +This is a privileged helper script for safely running certain commands as root. +It should only be called by the Bitmask application. + +USAGE: + bitmask-root firewall stop + bitmask-root firewall start 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. +""" +# TODO should be tested with python3, which can be the default on some distro. +from __future__ import print_function +import atexit +import os +import re +import signal +import socket +import subprocess +import sys +import time +import traceback + + +cmdcheck = subprocess.check_output + +## +## CONSTANTS +## + +SCRIPT = "bitmask-root" +NAMESERVER = "10.42.0.1" +BITMASK_CHAIN = "bitmask" + +IP = "/bin/ip" +IPTABLES = "/sbin/iptables" +IP6TABLES = "/sbin/ip6tables" + +RESOLVCONF_SYSTEM_BIN = "/sbin/resolvconf" +RESOLVCONF_LEAP_BIN = "/usr/local/sbin/leap-resolvconf" + +OPENVPN_USER = "nobody" +OPENVPN_GROUP = "nogroup" +LEAPOPENVPN = "LEAPOPENVPN" +OPENVPN_SYSTEM_BIN = "/usr/sbin/openvpn" # Debian location +OPENVPN_LEAP_BIN = "/usr/sbin/leap-openvpn" # installed by bundle + + +""" +The path to the script to update resolv.conf +""" +# XXX We have to check if we have a valid resolvconf, and use +# the old resolv-update if not. +LEAP_UPDATE_RESOLVCONF_FILE = "/etc/leap/update-resolv-conf" +LEAP_RESOLV_UPDATE = "/etc/leap/resolv-update" + +FIXED_FLAGS = [ + "--setenv", "LEAPOPENVPN", "1", + "--nobind", + "--client", + "--dev", "tun", + "--tls-client", + "--remote-cert-tls", "server", + "--management-signal", + "--script-security", "1", + "--user", "nobody", + "--group", "nogroup", + "--remap-usr1", "SIGTERM", +] + +ALLOWED_FLAGS = { + "--remote": ["IP", "NUMBER", "PROTO"], + "--tls-cipher": ["CIPHER"], + "--cipher": ["CIPHER"], + "--auth": ["CIPHER"], + "--management": ["DIR", "UNIXSOCKET"], + "--management-client-user": ["USER"], + "--cert": ["FILE"], + "--key": ["FILE"], + "--ca": ["FILE"] +} + +PARAM_FORMATS = { + "NUMBER": lambda s: re.match("^\d+$", s), + "PROTO": lambda s: re.match("^(tcp|udp)$", s), + "IP": lambda s: is_valid_address(s), + "CIPHER": lambda s: re.match("^[A-Z0-9-]+$", s), + "USER": lambda s: re.match( + "^[a-zA-Z0-9_\.\@][a-zA-Z0-9_\-\.\@]*\$?$", s), # IEEE Std 1003.1-2001 + "FILE": lambda s: os.path.isfile(s), + "DIR": lambda s: os.path.isdir(os.path.split(s)[0]), + "UNIXSOCKET": lambda s: s == "unix" +} + + +DEBUG = os.getenv("DEBUG") +TEST = os.getenv("TEST") + +if DEBUG: + import logging + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s") + ch = logging.StreamHandler() + ch.setLevel(logging.DEBUG) + ch.setFormatter(formatter) + logger = logging.getLogger(__name__) + logger.setLevel(logging.DEBUG) + logger.addHandler(ch) + +## +## UTILITY +## + + +def is_valid_address(value): + """ + Validate that the passed ip is a valid IP address. + + :param value: the value to be validated + :type value: str + :rtype: bool + """ + try: + socket.inet_aton(value) + return True + except Exception: + print("%s: ERROR: MALFORMED IP: %s!" % (SCRIPT, value)) + return False + + +def has_system_resolvconf(): + """ + Return True if resolvconf is found in the system. + + :rtype: bool + """ + return os.path.isfile(RESOLVCONF) + + +def has_valid_update_resolvconf(): + """ + Return True if a valid update-resolv-conf script is found in the system. + + :rtype: bool + """ + return os.path.isfile(LEAP_UPDATE_RESOLVCONF_FILE) + + +def has_valid_leap_resolv_update(): + """ + Return True if a valid resolv-update script is found in the system. + + :rtype: bool + """ + return os.path.isfile(LEAP_RESOLV_UPDATE) + + +def split_list(_list, regex): + """ + Split a list based on a regex: + e.g. split_list(["xx", "yy", "x1", "zz"], "^x") => [["xx", "yy"], ["x1", + "zz"]] + + :param _list: the list to be split. + :type _list: list + :param regex: the regex expression to filter with. + :type regex: str + + :rtype: list + """ + if not hasattr(regex, "match"): + regex = re.compile(regex) + result = [] + i = 0 + if not _list: + return result + while True: + if regex.match(_list[i]): + result.append([]) + while True: + result[-1].append(_list[i]) + i += 1 + if i >= len(_list) or regex.match(_list[i]): + break + else: + i += 1 + if i >= len(_list): + break + return result + + +def get_process_list(): + """ + Get a process list by reading `/proc` filesystem. + + :return: a list of tuples, each containing pid and command string. + :rtype: tuple if lists + """ + res = [] + pids = [pid for pid in os.listdir('/proc') if pid.isdigit()] + + for pid in pids: + try: + res.append((pid, open( + os.path.join( + '/proc', pid, 'cmdline'), 'rb').read())) + except IOError: # proc has already terminated + continue + return filter(None, res) + + +class Daemon(object): + """ + A generic daemon class. + """ + def __init__(self, pidfile, stdin='/dev/null', + stdout='/dev/null', stderr='/dev/null'): + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.pidfile = pidfile + + def daemonize(self): + """ + Do the UNIX double-fork magic, see Stevens' "Advanced + Programming in the UNIX Environment" for details (ISBN 0201563177) + http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + """ + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError, e: + sys.stderr.write( + "fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + # decouple from parent environment + os.chdir("/") + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + # exit from second parent + sys.exit(0) + except OSError, e: + sys.stderr.write( + "fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + # redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = file(self.stdin, 'r') + so = file(self.stdout, 'a+') + se = file(self.stderr, 'a+', 0) + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + # write pidfile + atexit.register(self.delpid) + pid = str(os.getpid()) + file(self.pidfile, 'w+').write("%s\n" % pid) + + def delpid(self): + """ + Delete the pidfile. + """ + os.remove(self.pidfile) + + def start(self, *args): + """ + Start the daemon. + """ + # Check for a pidfile to see if the daemon already runs + try: + pf = file(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if pid: + message = "pidfile %s already exist. Daemon already running?\n" + sys.stderr.write(message % self.pidfile) + sys.exit(1) + + # Start the daemon + self.daemonize() + self.run(args) + + def stop(self): + """ + Stop the daemon. + """ + # Get the pid from the pidfile + try: + pf = file(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if not pid: + message = "pidfile %s does not exist. Daemon not running?\n" + sys.stderr.write(message % self.pidfile) + return # not an error in a restart + + # Try killing the daemon process + try: + while 1: + os.kill(pid, signal.SIGTERM) + time.sleep(0.1) + except OSError, err: + err = str(err) + if err.find("No such process") > 0: + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + else: + print(str(err)) + sys.exit(1) + + def restart(self): + """ + Restart the daemon. + """ + self.stop() + self.start() + + def run(self): + """ + This should be overridden by derived classes. + """ + + +def run(command, *args, **options): + """ + Run an external command. + + Options: + + `check`: If True, check the command's output. bail if non-zero. (the + default is true unless detach or input is true) + `exitcode`: like `check`, but return exitcode instead of bailing. + `detach`: If True, run in detached process. + `input`: If True, open command for writing stream to, returning the Popen + object. + """ + parts = [command] + parts.extend(args) + if TEST or DEBUG: + print("%s run: %s " (SCRIPT, " ".join(parts))) + + _check = options.get("check", True) + _detach = options.get("detach", False) + _input = options.get("input", False) + _exitcode = options.get("exitcode", False) + + if not _check or _detach or _input: + if _input: + return subprocess.Popen(parts, stdin=subprocess.PIPE) + else: + # XXX ok with return None ?? + subprocess.Popen(parts) + else: + try: + devnull = open('/dev/null', 'w') + subprocess.check_call(parts, stdout=devnull, stderr=devnull) + return 0 + except subprocess.CalledProcessError as exc: + if DEBUG: + logger.exception(exc) + if _exitcode: + return exc.returncode + else: + bail("ERROR: Could not run %s: %s" % (exc.cmd, exc.output), + exception=exc) + + +def bail(msg=None, exception=None): + """ + Abnormal exit. + + :param msg: optional error message. + :type msg: str + """ + if msg is not None: + print("%s: %s" % (SCRIPT, msg)) + if exception is not None: + traceback.print_exc() + exit(1) + +## +## OPENVPN +## + + +def get_openvpn_bin(): + """ + Return the path for either the system openvpn or the one the + bundle has put there. + """ + if os.path.isfile(OPENVPN_SYSTEM_BIN): + return OPENVPN_SYSTEM_BIN + + # the bundle option should be removed from the debian package. + if os.path.isfile(OPENVPN_LEAP_BIN): + return OPENVPN_LEAP_BIN + + +def parse_openvpn_flags(args): + """ + Take argument list from the command line and parse it, only allowing some + configuration flags. + + :type args: list + """ + result = [] + try: + for flag in split_list(args, "^--"): + flag_name = flag[0] + if flag_name in ALLOWED_FLAGS: + result.append(flag_name) + required_params = ALLOWED_FLAGS[flag_name] + if required_params: + flag_params = flag[1:] + if len(flag_params) != len(required_params): + print("%s: ERROR: not enough params for %s" % + (SCRIPT, flag_name)) + return None + for param, param_type in zip(flag_params, required_params): + if PARAM_FORMATS[param_type](param): + result.append(param) + else: + print("%s: ERROR: Bad argument %s" % + (SCRIPT, param)) + return None + else: + print("WARNING: unrecognized openvpn flag %s" % flag_name) + return result + except Exception as exc: + print("%s: ERROR PARSING FLAGS: %s" % (SCRIPT, exc)) + if DEBUG: + logger.exception(exc) + return None + + +def openvpn_start(args): + """ + Launch openvpn, sanitizing input, and replacing the current process with + the openvpn process. + + :param args: arguments to be passed to openvpn + :type args: list + """ + openvpn_flags = parse_openvpn_flags(args) + if openvpn_flags: + OPENVPN = get_openvpn_bin() + flags = [OPENVPN] + FIXED_FLAGS + openvpn_flags + if DEBUG: + print("%s: running openvpn with flags:" % (SCRIPT,)) + print(flags) + # note: first argument to command is ignored, but customarily set to + # the command. + os.execv(OPENVPN, flags) + else: + bail('ERROR: could not parse openvpn options') + + +def openvpn_stop(args): + """ + Stop the openvpn that has likely been launched by bitmask. + + :param args: arguments to openvpn + :type args: list + """ + plist = get_process_list() + OPENVPN_BIN = get_openvpn_bin() + found_leap_openvpn = filter( + lambda (p, s): s.startswith(OPENVPN_BIN) and LEAPOPENVPN in s, + plist) + + if found_leap_openvpn: + pid = found_leap_openvpn[0][0] + os.kill(int(pid), signal.SIGTERM) + +## +## DNS +## + + +def get_resolvconf_bin(): + """ + Return the path for either the system resolvconf or the one the + bundle has put there. + """ + if os.path.isfile(RESOLVCONF_SYSTEM_BIN): + return RESOLVCONF_SYSTEM_BIN + + # the bundle option should be removed from the debian package. + if os.path.isfile(RESOLVCONF_LEAP_BIN): + return RESOLVCONF_LEAP_BIN + +RESOLVCONF = get_resolvconf_bin() + + +class NameserverSetter(Daemon): + """ + A daemon that will add leap nameserver inside the tunnel + to the system `resolv.conf` + """ + + def run(self, *args): + """ + Run when daemonized. + """ + if args: + ip_address = args[0] + self.set_dns_nameserver(ip_address) + + def set_dns_nameserver(self, ip_address): + """ + Add the tunnel DNS server to `resolv.conf` + + :param ip_address: the ip to add to `resolv.conf` + :type ip_address: str + """ + if os.path.isfile(RESOLVCONF): + process = run(RESOLVCONF, "-a", "bitmask", input=True) + process.communicate("nameserver %s\n" % ip_address) + else: + bail("ERROR: package openresolv or resolvconf not installed.") + +nameserver_setter = NameserverSetter('/tmp/leap-dns-up.pid') + + +class NameserverRestorer(Daemon): + """ + A daemon that will restore the previous nameservers. + """ + + def run(self): + """ + Run when daemonized. + """ + self.restore_dns_nameserver() + + def restore_dns_nameserver(self): + """ + Remove tunnel DNS server from `resolv.conf` + """ + if os.path.isfile(RESOLVCONF): + run(RESOLVCONF, "-d", "bitmask") + else: + print("%s: ERROR: package openresolv " + "or resolvconf not installed." % + (SCRIPT,)) + +nameserver_restorer = NameserverRestorer('/tmp/leap-dns-down.pid') + + +## +## FIREWALL +## + + +def get_gateways(gateways): + """ + Filter a passed sequence of gateways, returning only the valid ones. + + :param gateways: a sequence of gateways to filter. + :type gateways: iterable + :rtype: iterable + """ + result = filter(is_valid_address, gateways) + if not result: + bail("ERROR: No valid gateways specified") + else: + return result + + +def get_default_device(): + """ + Retrieve the current default network device. + + :rtype: str + """ + routes = subprocess.check_output([IP, "route", "show"]) + match = re.search("^default .*dev ([^\s]*) .*$", routes, flags=re.M) + if match.groups(): + return match.group(1) + else: + bail("Could not find default device") + + +def get_local_network_ipv4(device): + """ + Get the local ipv4 addres for a given device. + + :param device: + :type device: str + """ + addresses = cmdcheck([IP, "-o", "address", "show", "dev", device]) + match = re.search("^.*inet ([^ ]*) .*$", addresses, flags=re.M) + if match.groups(): + return match.group(1) + else: + return None + + +def get_local_network_ipv6(device): + """ + Get the local ipv6 addres for a given device. + + :param device: + :type device: str + """ + addresses = cmdcheck([IP, "-o", "address", "show", "dev", device]) + match = re.search("^.*inet6 ([^ ]*) .*$", addresses, flags=re.M) + if match.groups(): + return match.group(1) + else: + return None + + +def run_iptable_with_check(cmd, *args, **options): + """ + Run an iptables command checking to see if it should: + for --insert: run only if rule does not already exist. + for --delete: run only if rule does exist. + other commands are run normally. + """ + if "--insert" in args: + check_args = [arg.replace("--insert", "--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) + if check_code == 0: + run(cmd, *args, **options) + else: + run(cmd, *args, **options) + + +def iptables(*args, **options): + """ + Run iptables4 and iptables6. + """ + ip4tables(*args, **options) + ip6tables(*args, **options) + + +def ip4tables(*args, **options): + """ + Run iptables4 with checks. + """ + run_iptable_with_check(IPTABLES, *args, **options) + + +def ip6tables(*args, **options): + """ + Run iptables6 with checks. + """ + run_iptable_with_check(IP6TABLES, *args, **options) + + +def ipv4_chain_exists(table): + """ + Check if a given chain exists. + + :param table: the table to check against + :type table: str + :rtype: bool + """ + code = run(IPTABLES, "--list", table, "--numeric", exitcode=True) + return code == 0 + + +def ipv6_chain_exists(table): + """ + Check if a given chain exists. + + :param table: the table to check against + :type table: str + :rtype: bool + """ + code = run(IP6TABLES, "--list", table, "--numeric", exitcode=True) + return code == 0 + + +def firewall_start(args): + """ + Bring up the firewall. + + :param args: list of gateways, to be sanitized. + :type args: list + """ + default_device = get_default_device() + local_network_ipv4 = get_local_network_ipv4(default_device) + local_network_ipv6 = get_local_network_ipv6(default_device) + gateways = get_gateways(args) + + # add custom chain "bitmask" + 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 traffic to gateways + for gateway in gateways: + ip4tables("--insert", BITMASK_CHAIN, "--destination", gateway, + "-o", default_device, "--jump", "ACCEPT") + + # allow traffic to IPs on local network + if local_network_ipv4: + ip4tables("--insert", BITMASK_CHAIN, + "--destination", local_network_ipv4, "-o", default_device, + "--jump", "ACCEPT") + if local_network_ipv6: + ip6tables("--insert", BITMASK_CHAIN, + "--destination", local_network_ipv6, "-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") + + 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") + + +def firewall_stop(): + """ + Stop the firewall. + """ + iptables("--delete", "OUTPUT", "--jump", BITMASK_CHAIN) + if ipv4_chain_exists(BITMASK_CHAIN): + ip4tables("--flush", BITMASK_CHAIN) + ip4tables("--delete-chain", BITMASK_CHAIN) + if ipv6_chain_exists(BITMASK_CHAIN): + ip6tables("--flush", BITMASK_CHAIN) + ip6tables("--delete-chain", BITMASK_CHAIN) + +## +## MAIN +## + + +def main(): + if len(sys.argv) >= 3: + command = "_".join(sys.argv[1:3]) + args = sys.argv[3:] + + if command == "openvpn_start": + openvpn_start(args) + + elif command == "openvpn_stop": + openvpn_stop(args) + + elif command == "firewall_start": + try: + firewall_start(args) + nameserver_setter.start(NAMESERVER) + except Exception as ex: + nameserver_restorer.start() + firewall_stop() + bail("ERROR: could not start firewall", ex) + + elif command == "firewall_stop": + try: + firewall_stop() + nameserver_restorer.start() + except Exception as ex: + bail("ERROR: could not stop firewall", ex) + + elif command == "firewall_isup": + if ipv4_chain_exists(BITMASK_CHAIN): + print("%s: INFO: bitmask firewall is up" % (SCRIPT,)) + else: + bail("INFO: bitmask firewall is down") + + else: + bail("ERROR: No such command") + else: + bail("ERROR: No such command") + +if __name__ == "__main__": + if DEBUG: + logger.debug(" ".join(sys.argv)) + main() + print("%s: done" % (SCRIPT,)) + exit(0) diff --git a/pkg/linux/polkit/net.openvpn.gui.leap.policy b/pkg/linux/polkit/se.leap.bitmask.policy index 50f991a3..c66f4701 100644 --- a/pkg/linux/polkit/net.openvpn.gui.leap.policy +++ b/pkg/linux/polkit/se.leap.bitmask.policy @@ -7,17 +7,17 @@ <vendor>LEAP Project</vendor> <vendor_url>http://leap.se/</vendor_url> - <action id="net.openvpn,gui.leap.run-openvpn"> - <description>Runs the openvpn binary</description> - <description xml:lang="es">Ejecuta el binario openvpn</description> - <message>OpenVPN needs that you authenticate to start</message> - <message xml:lang="es">OpenVPN necesita autorizacion para comenzar</message> + <action id="se.leap.bitmask.policy"> + <description>Runs bitmask helper to launch firewall and openvpn</description> + <description xml:lang="es">Ejecuta el asistente de bitmask para lanzar el firewall y openvpn</description> + <message>Bitmask needs that you authenticate to start</message> + <message xml:lang="es">Bitmask necesita autorizacion para comenzar</message> <icon_name>package-x-generic</icon_name> <defaults> <allow_any>yes</allow_any> <allow_inactive>yes</allow_inactive> <allow_active>yes</allow_active> </defaults> - <annotate key="org.freedesktop.policykit.exec.path">/usr/sbin/openvpn</annotate> + <annotate key="org.freedesktop.policykit.exec.path">/usr/sbin/bitmask-root</annotate> </action> </policyconfig> diff --git a/pkg/linux/update-resolv-conf b/pkg/linux/update-resolv-conf new file mode 100755 index 00000000..76c69413 --- /dev/null +++ b/pkg/linux/update-resolv-conf @@ -0,0 +1,58 @@ +#!/bin/bash +# +# Parses DHCP options from openvpn to update resolv.conf +# To use set as 'up' and 'down' script in your openvpn *.conf: +# up /etc/leap/update-resolv-conf +# down /etc/leap/update-resolv-conf +# +# Used snippets of resolvconf script by Thomas Hood and Chris Hanson. +# Licensed under the GNU GPL. See /usr/share/common-licenses/GPL. +# +# Example envs set from openvpn: +# +# foreign_option_1='dhcp-option DNS 193.43.27.132' +# foreign_option_2='dhcp-option DNS 193.43.27.133' +# foreign_option_3='dhcp-option DOMAIN be.bnc.ch' +# + +[ -x /sbin/resolvconf ] || exit 0 +[ "$script_type" ] || exit 0 +[ "$dev" ] || exit 0 + +split_into_parts() +{ + part1="$1" + part2="$2" + part3="$3" +} + +case "$script_type" in + up) + NMSRVRS="" + SRCHS="" + for optionvarname in ${!foreign_option_*} ; do + option="${!optionvarname}" + echo "$option" + split_into_parts $option + if [ "$part1" = "dhcp-option" ] ; then + if [ "$part2" = "DNS" ] ; then + NMSRVRS="${NMSRVRS:+$NMSRVRS }$part3" + elif [ "$part2" = "DOMAIN" ] ; then + SRCHS="${SRCHS:+$SRCHS }$part3" + fi + fi + done + R="" + [ "$SRCHS" ] && R="search $SRCHS +" + for NS in $NMSRVRS ; do + R="${R}nameserver $NS +" + done + echo -n "$R" | /sbin/resolvconf -a "${dev}.openvpn" + ;; + down) + /sbin/resolvconf -d "${dev}.openvpn" + ;; +esac + diff --git a/pkg/requirements.pip b/pkg/requirements.pip index be4ea858..70427e63 100644 --- a/pkg/requirements.pip +++ b/pkg/requirements.pip @@ -10,16 +10,21 @@ requests>=1.1.0 srp>=1.0.2 pyopenssl python-dateutil -psutil==1.2.1 + +# 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 + ipaddr twisted python-daemon # this should not be needed for Windows. keyring zope.proxy -leap.common>=0.3.4 -leap.soledad.client>=0.4.2 -leap.keymanager>=0.3.6 +leap.common>=0.3.7 +leap.soledad.client>=0.5.0 +leap.keymanager>=0.3.8 leap.mail>=0.3.9 # Remove this when u1db fixes its dependency on oauth diff --git a/pkg/scripts/stats.sh b/pkg/scripts/stats.sh index 2b7a8b18..8ea41267 100755 --- a/pkg/scripts/stats.sh +++ b/pkg/scripts/stats.sh @@ -1,4 +1,55 @@ #!/bin/bash +###################################################################### +# boostrap_develop.sh +# 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/>. +###################################################################### + +# Script that gives a summary of changes between two annotated tags. +# Automatically detects the last annotated tag and the previous on. +# This is useful to give information during a release. + +# Example output: +# Changes summary between closest annotated tag to current annotated tag +# ====================================================================== +# +# ---------------------------------------------------------------------- +# Stats for: bitmask_client - 0.3.8..0.5.0 +# Stats: 65 files changed, 4384 insertions(+), 1384 deletions(-) +# Merges: 580 +# ---------------------------------------------------------------------- +# Stats for: leap_pycommon - 0.3.6..0.3.7 +# Stats: 9 files changed, 277 insertions(+), 18 deletions(-) +# Merges: 41 +# ---------------------------------------------------------------------- +# Stats for: soledad - 0.4.4..0.4.5 +# Stats: 61 files changed, 5361 insertions(+), 1377 deletions(-) +# Merges: 258 +# ---------------------------------------------------------------------- +# Stats for: keymanager - 0.3.7..0.3.8 +# Stats: 9 files changed, 118 insertions(+), 67 deletions(-) +# Merges: 83 +# ---------------------------------------------------------------------- +# Stats for: leap_mail - 0.3.8..0.3.9 +# Stats: 43 files changed, 9487 insertions(+), 2159 deletions(-) +# Merges: 419 +# ---------------------------------------------------------------------- +# +# TOTAL +# Stats: 187 files changed, 19627 insertions(+), 5005 deletions(-) +# Merges: 1381 REPOSITORIES="bitmask_client leap_pycommon soledad keymanager leap_mail" @@ -7,24 +58,32 @@ INSERTIONS=0 DELETIONS=0 MERGES_TOTAL=0 -echo "Changes summary between closest annotated tag to HEAD" -echo "=====================================================" +echo "Changes summary between closest annotated tag to current annotated tag" +echo "======================================================================" echo +echo "----------------------------------------------------------------------" for repo in $REPOSITORIES; do cd $repo - echo "Stats for: $repo" - # the 'describe' command gives the closest annotated tag - STATS=$(git diff --shortstat `git describe --abbrev=0`..HEAD) - MERGES=$(git log --merges `git describe --abbrev=0`..HEAD | wc -l) + + LAST_TWO_TAGS=(`git for-each-ref refs/tags --sort=-taggerdate --format='%(refname)' --count=2 | cut -d/ -f3`) + CURRENT=${LAST_TWO_TAGS[0]} + PREV=${LAST_TWO_TAGS[1]} + + echo "Stats for: $repo - $PREV..$CURRENT" + STATS=$(git diff --shortstat $PREV..$CURRENT) + MERGES=$(git log --merges $PREV..$CURRENT | wc -l) echo "Stats:$STATS" echo "Merges: $MERGES" - VALUES=(`echo $STATS | awk '{ print $1, $4, $6 }'`) # use array to store values + echo "----------------------------------------------------------------------" + + # Sum all the results for the grand total + VALUES=(`echo $STATS | awk '{ print $1, $4, $6 }'`) # use array to store/split values CHANGED=$(echo $CHANGED + ${VALUES[0]} | bc) INSERTIONS=$(echo $INSERTIONS + ${VALUES[1]} | bc) DELETIONS=$(echo $DELETIONS + ${VALUES[2]} | bc) MERGES_TOTAL=$(echo $MERGES_TOTAL + $MERGES | bc) - echo "----------------------------------------------------------------------" + cd .. done diff --git a/relnotes.txt b/relnotes.txt index 071b671e..a658a782 100644 --- a/relnotes.txt +++ b/relnotes.txt @@ -1,8 +1,8 @@ -ANNOUNCING Bitmask, the Internet Encryption Toolkit, release 0.5.0 +ANNOUNCING Bitmask, the Internet Encryption Toolkit, release 0.5.1 The LEAP team is pleased to announce the immediate availability of -version 0.5.0 of Bitmask, the Internet Encryption Toolkit, codename -"Long time no see". +version 0.5.1 of Bitmask, the Internet Encryption Toolkit, codename +"lil less leaky". https://downloads.leap.se/client/ @@ -17,6 +17,15 @@ The Encrypted Internet Proxy provides circumvention, location anonymization, and traffic encryption in a hassle-free, automatically self-configuring fashion. +WARNING (LINUX ONLY): If you ever run into the situation where you +cannot access internet, open the terminal and run the following +command: + +$ pkexec /usr/sbin/bitmask-root firewall stop + +If for some reason that doesn't work, you will need to reboot your +computer. + Encrypted Mail offers automatic encryption and decryption for both outgoing and incoming email, adding public key cryptography to your mail without you ever having to worry about key distribution or @@ -34,7 +43,7 @@ NOT trust your life to it. WHAT CAN THIS VERSION OF BITMASK DO FOR ME? -Bitmask 0.5.0 improves greatly its mail support and stability in +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. @@ -42,6 +51,10 @@ 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. +Encrypted Internet on Linux now helps you don't shoot yourself in the +foot by leaking traffic outside of the secure connection it +establishes. This will be added to other platforms in the future. + The Encrypted Mail services will run local SMTP and IMAP proxies that, once you configure the mail client of your choice, will automatically encrypt and decrypt your email using GPG encryption under the hood. @@ -87,7 +100,7 @@ our intensive bug-reeducation program. HACKING -You can find us in the #leap-dev channel on the freenode network. +You can find us in the #leap channel on the freenode network. If you are lucky enough, you can also spot us drinking mate, sleepless in night trains, rooftops, rainforests, lonely islands and, always, @@ -95,6 +108,6 @@ beyond any border. The LEAP team, -Apr 4, 2014 +May 16, 2014 Somewhere in the middle of the intertubes. EOF diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index 02e27123..e413ab4c 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -76,6 +76,16 @@ def sigint_handler(*args, **kwargs): mainwindow = args[0] mainwindow.quit() +def sigterm_handler(*args, **kwargs): + """ + Signal handler for SIGTERM. + This handler is actually passed to twisted reactor + """ + logger = kwargs.get('logger', None) + if logger: + logger.debug("SIGTERM catched. shutting down...") + mainwindow = args[0] + mainwindow.quit() def add_logger_handlers(debug=False, logfile=None, replace_stdout=True): """ @@ -200,7 +210,7 @@ def main(): debug = opts.debug logfile = opts.log_file mail_logfile = opts.mail_log_file - openvpn_verb = opts.openvpn_verb + start_hidden = opts.start_hidden ############################################################# # Given how paths and bundling works, we need to delay the imports @@ -213,6 +223,8 @@ def main(): flags.MAIL_LOGFILE = mail_logfile flags.APP_VERSION_CHECK = opts.app_version_check flags.API_VERSION_CHECK = opts.api_version_check + flags.OPENVPN_VERBOSITY = opts.openvpn_verb + flags.SKIP_WIZARD_CHECKS = opts.skip_wizard_checks flags.CA_CERT_FILE = opts.ca_cert_file @@ -306,12 +318,15 @@ def main(): window = MainWindow( lambda: twisted_main.quit(app), - openvpn_verb=openvpn_verb, - bypass_checks=bypass_checks) + bypass_checks=bypass_checks, + start_hidden=start_hidden) sigint_window = partial(sigint_handler, window, logger=logger) signal.signal(signal.SIGINT, sigint_window) + # callable used in addSystemEventTrigger to handle SIGTERM + sigterm_window = partial(sigterm_handler, window, logger=logger) + if IS_MAC: window.raise_() @@ -322,6 +337,12 @@ def main(): l = LoopingCall(QtCore.QCoreApplication.processEvents, 0, 10) l.start(0.01) + + # SIGTERM can't be handled the same way SIGINT is, since it's + # caught by twisted. See _handleSignals method in + # twisted/internet/base.py#L1150. So, addSystemEventTrigger + # reactor's method is used. + reactor.addSystemEventTrigger('before', 'shutdown', sigterm_window) reactor.run() if __name__ == "__main__": diff --git a/src/leap/bitmask/backend.py b/src/leap/bitmask/backend.py index 45ea451c..a2df465d 100644 --- a/src/leap/bitmask/backend.py +++ b/src/leap/bitmask/backend.py @@ -17,11 +17,15 @@ """ Backend for everything """ +import commands import logging +import os +import time from functools import partial from Queue import Queue, Empty +from twisted.internet import reactor from twisted.internet import threads, defer from twisted.internet.task import LoopingCall from twisted.python import log @@ -29,9 +33,19 @@ from twisted.python import log import zope.interface 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.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.common import certs as leap_certs # Frontend side from PySide import QtCore @@ -39,6 +53,26 @@ from PySide import QtCore logger = logging.getLogger(__name__) +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. + + :param config: a ProviderConfig object + :type conig: ProviderConfig + :param domain: the domain which config is required. + :type domain: unicode + + :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)) + + return config.loaded() + + class ILEAPComponent(zope.interface.Interface): """ Interface that every component for the backend should comply to @@ -54,7 +88,7 @@ class ILEAPService(ILEAPComponent): def start(self): """ - Starts the service. + Start the service. """ pass @@ -66,13 +100,13 @@ class ILEAPService(ILEAPComponent): def terminate(self): """ - Terminates the service, not necessarily in a nice way. + Terminate the service, not necessarily in a nice way. """ pass def status(self): """ - Returns a json object with the current status for the service. + Return a json object with the current status for the service. :rtype: object (list, str, dict) """ @@ -83,7 +117,7 @@ class ILEAPService(ILEAPComponent): def set_configs(self, keyval): """ - Sets the config parameters for this Service. + Set the config parameters for this Service. :param keyval: values to configure :type keyval: dict, {str: str} @@ -92,7 +126,7 @@ class ILEAPService(ILEAPComponent): def get_configs(self, keys): """ - Returns the configuration values for the list of keys. + Return the configuration values for the list of keys. :param keys: keys to retrieve :type keys: list of str @@ -109,8 +143,6 @@ class Provider(object): zope.interface.implements(ILEAPComponent) - PROBLEM_SIGNAL = "prov_problem_with_provider" - def __init__(self, signaler=None, bypass_checks=False): """ Constructor for the Provider component @@ -123,7 +155,6 @@ class Provider(object): certificates at bootstrap :type bypass_checks: bool """ - object.__init__(self) self.key = "provider" self._provider_bootstrapper = ProviderBootstrapper(signaler, bypass_checks) @@ -132,7 +163,7 @@ class Provider(object): def setup_provider(self, provider): """ - Initiates the setup for a provider + Initiate the setup for a provider :param provider: URL for the provider :type provider: unicode @@ -166,19 +197,15 @@ class Provider(object): """ d = None - # If there's no loaded provider or - # we want to connect to other provider... - if (not self._provider_config.loaded() or - self._provider_config.get_domain() != provider): - self._provider_config.load(get_provider_path(provider)) - - if self._provider_config.loaded(): + config = self._provider_config + if get_provider_config(config, provider): d = self._provider_bootstrapper.run_provider_setup_checks( self._provider_config, download_if_needed=True) else: if self._signaler is not None: - self._signaler.signal(self.PROBLEM_SIGNAL) + self._signaler.signal( + self._signaler.PROV_PROBLEM_WITH_PROVIDER_KEY) logger.error("Could not load provider configuration.") self._login_widget.set_enabled(True) @@ -202,10 +229,8 @@ class Register(object): back to the frontend :type signaler: Signaler """ - object.__init__(self) self.key = "register" self._signaler = signaler - self._provider_config = ProviderConfig() def register_user(self, domain, username, password): """ @@ -221,23 +246,406 @@ class Register(object): :returns: the defer for the operation running in a thread. :rtype: twisted.internet.defer.Deferred """ - # If there's no loaded provider or - # we want to connect to other provider... - if (not self._provider_config.loaded() or - self._provider_config.get_domain() != domain): - self._provider_config.load(get_provider_path(domain)) - - if self._provider_config.loaded(): + config = ProviderConfig() + if get_provider_config(config, domain): srpregister = SRPRegister(signaler=self._signaler, - provider_config=self._provider_config) + provider_config=config) return threads.deferToThread( partial(srpregister.register_user, username, password)) else: if self._signaler is not None: - self._signaler.signal(self._signaler.srp_registration_failed) + self._signaler.signal(self._signaler.SRP_REGISTRATION_FAILED) logger.error("Could not load provider configuration.") +class EIP(object): + """ + Interfaces with setup and launch of EIP + """ + + zope.interface.implements(ILEAPService) + + def __init__(self, signaler=None): + """ + Constructor for the EIP component + + :param signaler: Object in charge of handling communication + back to the frontend + :type signaler: Signaler + """ + self.key = "eip" + self._signaler = signaler + self._eip_bootstrapper = EIPBootstrapper(signaler) + self._eip_setup_defer = None + self._provider_config = ProviderConfig() + + self._vpn = vpnprocess.VPN(signaler=signaler) + + def setup_eip(self, domain, skip_network=False): + """ + Initiate the setup for a provider + + :param domain: URL for the provider + :type domain: unicode + :param skip_network: Whether checks that involve network should be done + or not + :type skip_network: bool + + :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): + if skip_network: + return defer.Deferred() + eb = self._eip_bootstrapper + d = eb.run_eip_setup_checks(self._provider_config, + download_if_needed=True) + self._eip_setup_defer = d + return d + else: + raise Exception("No provider setup loaded") + + def cancel_setup_eip(self): + """ + Cancel the ongoing setup eip defer (if any). + """ + d = self._eip_setup_defer + if d is not None: + d.cancel() + + def _start_eip(self): + """ + Start EIP + """ + provider_config = self._provider_config + eip_config = eipconfig.EIPConfig() + domain = provider_config.get_domain() + + loaded = eipconfig.load_eipconfig_if_needed( + provider_config, eip_config, domain) + + if not loaded: + if self._signaler is not None: + self._signaler.signal(self._signaler.EIP_CONNECTION_ABORTED) + logger.error("Tried to start EIP but cannot find any " + "available provider!") + return + + host, port = get_openvpn_management() + self._vpn.start(eipconfig=eip_config, + providerconfig=provider_config, + socket_host=host, socket_port=port) + + def start(self): + """ + Start the service. + """ + signaler = self._signaler + + if not self._provider_config.loaded(): + # This means that the user didn't call setup_eip first. + self._signaler.signal(signaler.BACKEND_BAD_CALL, "EIP.start(), " + "no provider loaded") + return + + try: + self._start_eip() + except vpnprocess.OpenVPNAlreadyRunning: + signaler.signal(signaler.EIP_OPENVPN_ALREADY_RUNNING) + except vpnprocess.AlienOpenVPNAlreadyRunning: + signaler.signal(signaler.EIP_ALIEN_OPENVPN_ALREADY_RUNNING) + except vpnlauncher.OpenVPNNotFoundException: + signaler.signal(signaler.EIP_OPENVPN_NOT_FOUND_ERROR) + except vpnlauncher.VPNLauncherException: + # TODO: this seems to be used for 'gateway not found' only. + # see vpnlauncher.py + signaler.signal(signaler.EIP_VPN_LAUNCHER_EXCEPTION) + except linuxvpnlauncher.EIPNoPolkitAuthAgentAvailable: + signaler.signal(signaler.EIP_NO_POLKIT_AGENT_ERROR) + except linuxvpnlauncher.EIPNoPkexecAvailable: + signaler.signal(signaler.EIP_NO_PKEXEC_ERROR) + except darwinvpnlauncher.EIPNoTunKextLoaded: + signaler.signal(signaler.EIP_NO_TUN_KEXT_ERROR) + except Exception as e: + logger.error("Unexpected problem: {0!r}".format(e)) + else: + # TODO: are we connected here? + signaler.signal(signaler.EIP_CONNECTED) + + def stop(self, shutdown=False): + """ + Stop the service. + """ + self._vpn.terminate(shutdown) + if IS_LINUX: + self._wait_for_firewall_down() + + def _wait_for_firewall_down(self): + """ + Wait for the firewall to come down. + """ + # Due to how we delay the resolvconf action in linux. + # XXX this *has* to wait for a reasonable lapse, since we have some + # delay in vpn.terminate. + # For a better solution it should be signaled from backend that + # everything is clear to proceed, or a timeout happened. + 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 + + while retry < MAX_FW_WAIT_RETRIES: + if fw_is_down(): + return + else: + time.sleep(FW_WAIT_STEP) + retry += 1 + logger.warning("After waiting, firewall is not down... " + "You might experience lack of connectivity") + + def terminate(self): + """ + Terminate the service, not necessarily in a nice way. + """ + self._vpn.killit() + + def status(self): + """ + Return a json object with the current status for the service. + + :rtype: object (list, str, dict) + """ + # XXX: Use a namedtuple or a specific object instead of a json + # object, since parsing it will be problematic otherwise. + # It has to be something easily serializable though. + pass + + def _provider_is_initialized(self, domain): + """ + Return whether the given domain is initialized or not. + + :param domain: the domain to check + :type domain: str + + :returns: True if is initialized, False otherwise. + :rtype: bool + """ + eipconfig_path = eipconfig.get_eipconfig_path(domain, relative=False) + if os.path.isfile(eipconfig_path): + return True + else: + return False + + def get_initialized_providers(self, domains): + """ + Signal a list of the given domains and if they are initialized or not. + + :param domains: the list of domains to check. + :type domain: list of str + + Signals: + eip_get_initialized_providers -> list of tuple(unicode, bool) + """ + filtered_domains = [] + for domain in domains: + is_initialized = self._provider_is_initialized(domain) + filtered_domains.append((domain, is_initialized)) + + if self._signaler is not None: + self._signaler.signal(self._signaler.EIP_GET_INITIALIZED_PROVIDERS, + filtered_domains) + + def get_gateways_list(self, domain): + """ + Signal a list of gateways for the given provider. + + :param domain: the domain to get the gateways. + :type domain: str + + Signals: + eip_get_gateways_list -> list of unicode + eip_get_gateways_list_error + eip_uninitialized_provider + """ + if not self._provider_is_initialized(domain): + if self._signaler is not None: + self._signaler.signal( + self._signaler.EIP_UNINITIALIZED_PROVIDER) + return + + 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)) + + # check for other problems + if not eip_loaded or provider_config is None: + if self._signaler is not None: + self._signaler.signal( + self._signaler.EIP_GET_GATEWAYS_LIST_ERROR) + return + + gateways = eipconfig.VPNGatewaySelector(eip_config).get_gateways_list() + + if self._signaler is not None: + self._signaler.signal( + self._signaler.EIP_GET_GATEWAYS_LIST, gateways) + + def can_start(self, domain): + """ + Signal whether it has everything that is needed to run EIP or not + + :param domain: the domain for the provider to check + :type domain: str + + Signals: + eip_can_start + eip_cannot_start + """ + try: + 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)) + + # check for other problems + if not eip_loaded or provider_config is None: + raise Exception("Cannot load provider and eip config, cannot " + "autostart") + + client_cert_path = eip_config.\ + get_client_cert_path(provider_config, about_to_download=False) + + if leap_certs.should_redownload(client_cert_path): + raise Exception("The client should redownload the certificate," + " cannot autostart") + + if not os.path.isfile(client_cert_path): + raise Exception("Can't find the certificate, cannot autostart") + + 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) + + +class Authenticate(object): + """ + Interfaces with setup and bootstrapping operations for a provider + """ + + zope.interface.implements(ILEAPComponent) + + def __init__(self, signaler=None): + """ + Constructor for the Authenticate component + + :param signaler: Object in charge of handling communication + back to the frontend + :type signaler: Signaler + """ + self.key = "authenticate" + self._signaler = signaler + self._srp_auth = SRPAuth(ProviderConfig(), self._signaler) + + def login(self, domain, username, password): + """ + Execute the whole authentication process for a user + + :param domain: the domain where we need to authenticate. + :type domain: unicode + :param username: username for this session + :type username: str + :param password: password for this user + :type password: str + + :returns: the defer for the operation running in a thread. + :rtype: twisted.internet.defer.Deferred + """ + config = ProviderConfig() + if get_provider_config(config, domain): + self._srp_auth = SRPAuth(config, self._signaler) + self._login_defer = self._srp_auth.authenticate(username, password) + return self._login_defer + else: + if self._signaler is not None: + self._signaler.signal(self._signaler.SRP_AUTH_ERROR) + logger.error("Could not load provider configuration.") + + def cancel_login(self): + """ + Cancel the ongoing login defer (if any). + """ + d = self._login_defer + if d is not None: + d.cancel() + + def change_password(self, current_password, new_password): + """ + Change the user's password. + + :param current_password: the current password of the user. + :type current_password: str + :param new_password: the new password for the user. + :type new_password: str + + :returns: a defer to interact with. + :rtype: twisted.internet.defer.Deferred + """ + if not self._is_logged_in(): + if self._signaler is not None: + self._signaler.signal(self._signaler.SRP_NOT_LOGGED_IN_ERROR) + return + + return self._srp_auth.change_password(current_password, new_password) + + def logout(self): + """ + Log out the current session. + Expects a session_id to exists, might raise AssertionError + """ + if not self._is_logged_in(): + if self._signaler is not None: + self._signaler.signal(self._signaler.SRP_NOT_LOGGED_IN_ERROR) + return + + self._srp_auth.logout() + + def _is_logged_in(self): + """ + Return whether the user is logged in or not. + + :rtype: bool + """ + return (self._srp_auth is not None and + self._srp_auth.is_authenticated()) + + def get_logged_in_status(self): + """ + Signal if the user is currently logged in or not. + """ + if self._signaler is None: + return + + signal = None + if self._is_logged_in(): + signal = self._signaler.SRP_STATUS_LOGGED_IN + else: + signal = self._signaler.SRP_STATUS_NOT_LOGGED_IN + + self._signaler.signal(signal) + + class Signaler(QtCore.QObject): """ Signaler object, handles converting string commands to Qt signals. @@ -269,6 +677,65 @@ class Signaler(QtCore.QObject): srp_registration_failed = QtCore.Signal(object) srp_registration_taken = QtCore.Signal(object) + # Signals for EIP bootstrapping + eip_config_ready = QtCore.Signal(object) + eip_client_certificate_ready = QtCore.Signal(object) + + eip_cancelled_setup = QtCore.Signal(object) + + # Signals for SRPAuth + srp_auth_ok = QtCore.Signal(object) + srp_auth_error = QtCore.Signal(object) + srp_auth_server_error = QtCore.Signal(object) + srp_auth_connection_error = QtCore.Signal(object) + srp_auth_bad_user_or_password = QtCore.Signal(object) + srp_logout_ok = QtCore.Signal(object) + srp_logout_error = QtCore.Signal(object) + srp_password_change_ok = QtCore.Signal(object) + srp_password_change_error = QtCore.Signal(object) + srp_password_change_badpw = QtCore.Signal(object) + srp_not_logged_in_error = QtCore.Signal(object) + srp_status_logged_in = QtCore.Signal(object) + srp_status_not_logged_in = QtCore.Signal(object) + + # Signals for EIP + eip_connected = QtCore.Signal(object) + eip_disconnected = QtCore.Signal(object) + eip_connection_died = QtCore.Signal(object) + eip_connection_aborted = QtCore.Signal(object) + + # EIP problems + eip_no_polkit_agent_error = QtCore.Signal(object) + eip_no_tun_kext_error = QtCore.Signal(object) + eip_no_pkexec_error = QtCore.Signal(object) + eip_openvpn_not_found_error = QtCore.Signal(object) + eip_openvpn_already_running = QtCore.Signal(object) + eip_alien_openvpn_already_running = QtCore.Signal(object) + eip_vpn_launcher_exception = QtCore.Signal(object) + + eip_get_gateways_list = QtCore.Signal(object) + eip_get_gateways_list_error = QtCore.Signal(object) + eip_uninitialized_provider = QtCore.Signal(object) + eip_get_initialized_providers = QtCore.Signal(object) + + # signals from parsing openvpn output + eip_network_unreachable = QtCore.Signal(object) + eip_process_restart_tls = QtCore.Signal(object) + eip_process_restart_ping = QtCore.Signal(object) + + # signals from vpnprocess.py + eip_state_changed = QtCore.Signal(dict) + eip_status_changed = QtCore.Signal(dict) + eip_process_finished = QtCore.Signal(int) + + # signals whether the needed files to start EIP exist or not + eip_can_start = QtCore.Signal(object) + eip_cannot_start = QtCore.Signal(object) + + # This signal is used to warn the backend user that is doing something + # wrong + backend_bad_call = QtCore.Signal(object) + #################### # These will exist both in the backend AND the front end. # The frontend might choose to not "interpret" all the signals @@ -288,6 +755,53 @@ class Signaler(QtCore.QObject): SRP_REGISTRATION_FINISHED = "srp_registration_finished" SRP_REGISTRATION_FAILED = "srp_registration_failed" SRP_REGISTRATION_TAKEN = "srp_registration_taken" + SRP_AUTH_OK = "srp_auth_ok" + SRP_AUTH_ERROR = "srp_auth_error" + SRP_AUTH_SERVER_ERROR = "srp_auth_server_error" + SRP_AUTH_CONNECTION_ERROR = "srp_auth_connection_error" + SRP_AUTH_BAD_USER_OR_PASSWORD = "srp_auth_bad_user_or_password" + SRP_LOGOUT_OK = "srp_logout_ok" + SRP_LOGOUT_ERROR = "srp_logout_error" + SRP_PASSWORD_CHANGE_OK = "srp_password_change_ok" + SRP_PASSWORD_CHANGE_ERROR = "srp_password_change_error" + SRP_PASSWORD_CHANGE_BADPW = "srp_password_change_badpw" + SRP_NOT_LOGGED_IN_ERROR = "srp_not_logged_in_error" + SRP_STATUS_LOGGED_IN = "srp_status_logged_in" + SRP_STATUS_NOT_LOGGED_IN = "srp_status_not_logged_in" + + EIP_CONFIG_READY = "eip_config_ready" + EIP_CLIENT_CERTIFICATE_READY = "eip_client_certificate_ready" + EIP_CANCELLED_SETUP = "eip_cancelled_setup" + + EIP_CONNECTED = "eip_connected" + EIP_DISCONNECTED = "eip_disconnected" + EIP_CONNECTION_DIED = "eip_connection_died" + EIP_CONNECTION_ABORTED = "eip_connection_aborted" + 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" + EIP_OPENVPN_NOT_FOUND_ERROR = "eip_openvpn_not_found_error" + EIP_OPENVPN_ALREADY_RUNNING = "eip_openvpn_already_running" + EIP_ALIEN_OPENVPN_ALREADY_RUNNING = "eip_alien_openvpn_already_running" + EIP_VPN_LAUNCHER_EXCEPTION = "eip_vpn_launcher_exception" + + EIP_GET_GATEWAYS_LIST = "eip_get_gateways_list" + EIP_GET_GATEWAYS_LIST_ERROR = "eip_get_gateways_list_error" + EIP_UNINITIALIZED_PROVIDER = "eip_uninitialized_provider" + EIP_GET_INITIALIZED_PROVIDERS = "eip_get_initialized_providers" + + EIP_NETWORK_UNREACHABLE = "eip_network_unreachable" + EIP_PROCESS_RESTART_TLS = "eip_process_restart_tls" + EIP_PROCESS_RESTART_PING = "eip_process_restart_ping" + + EIP_STATE_CHANGED = "eip_state_changed" + EIP_STATUS_CHANGED = "eip_status_changed" + EIP_PROCESS_FINISHED = "eip_process_finished" + + EIP_CAN_START = "eip_can_start" + EIP_CANNOT_START = "eip_cannot_start" + + BACKEND_BAD_CALL = "backend_bad_call" def __init__(self): """ @@ -311,6 +825,54 @@ class Signaler(QtCore.QObject): self.SRP_REGISTRATION_FINISHED, self.SRP_REGISTRATION_FAILED, self.SRP_REGISTRATION_TAKEN, + + self.EIP_CONFIG_READY, + self.EIP_CLIENT_CERTIFICATE_READY, + self.EIP_CANCELLED_SETUP, + + self.EIP_CONNECTED, + self.EIP_DISCONNECTED, + self.EIP_CONNECTION_DIED, + self.EIP_CONNECTION_ABORTED, + self.EIP_NO_POLKIT_AGENT_ERROR, + self.EIP_NO_TUN_KEXT_ERROR, + self.EIP_NO_PKEXEC_ERROR, + self.EIP_OPENVPN_NOT_FOUND_ERROR, + self.EIP_OPENVPN_ALREADY_RUNNING, + self.EIP_ALIEN_OPENVPN_ALREADY_RUNNING, + self.EIP_VPN_LAUNCHER_EXCEPTION, + + self.EIP_GET_GATEWAYS_LIST, + self.EIP_GET_GATEWAYS_LIST_ERROR, + self.EIP_UNINITIALIZED_PROVIDER, + self.EIP_GET_INITIALIZED_PROVIDERS, + + self.EIP_NETWORK_UNREACHABLE, + self.EIP_PROCESS_RESTART_TLS, + self.EIP_PROCESS_RESTART_PING, + + self.EIP_STATE_CHANGED, + self.EIP_STATUS_CHANGED, + self.EIP_PROCESS_FINISHED, + + self.EIP_CAN_START, + self.EIP_CANNOT_START, + + self.SRP_AUTH_OK, + self.SRP_AUTH_ERROR, + self.SRP_AUTH_SERVER_ERROR, + self.SRP_AUTH_CONNECTION_ERROR, + self.SRP_AUTH_BAD_USER_OR_PASSWORD, + self.SRP_LOGOUT_OK, + self.SRP_LOGOUT_ERROR, + self.SRP_PASSWORD_CHANGE_OK, + self.SRP_PASSWORD_CHANGE_ERROR, + self.SRP_PASSWORD_CHANGE_BADPW, + self.SRP_NOT_LOGGED_IN_ERROR, + self.SRP_STATUS_LOGGED_IN, + self.SRP_STATUS_NOT_LOGGED_IN, + + self.BACKEND_BAD_CALL, ] for sig in signals: @@ -332,7 +894,6 @@ class Signaler(QtCore.QObject): # Right now it emits Qt signals. The backend version of this # will do zmq.send_multipart, and the frontend version will be # similar to this - log.msg("Signaling %s :: %s" % (key, data)) # for some reason emitting 'None' gives a segmentation fault. if data is None: @@ -341,7 +902,7 @@ class Signaler(QtCore.QObject): try: self._signals[key].emit(data) except KeyError: - log.msg("Unknown key for signal %s!" % (key,)) + log.err("Unknown key for signal %s!" % (key,)) class Backend(object): @@ -356,8 +917,6 @@ class Backend(object): """ Constructor for the backend. """ - object.__init__(self) - # Components map for the commands received self._components = {} @@ -370,6 +929,8 @@ class Backend(object): # Component registration self._register(Provider(self._signaler, bypass_checks)) self._register(Register(self._signaler)) + self._register(Authenticate(self._signaler)) + self._register(EIP(self._signaler)) # We have a looping call on a thread executing all the # commands in queue. Right now this queue is an actual Queue @@ -398,9 +959,17 @@ class Backend(object): """ Stops the looping call and tries to cancel all the defers. """ + reactor.callLater(2, self._stop) + + def _stop(self): + """ + Delayed stopping of worker. Called from `stop`. + """ log.msg("Stopping worker...") if self._lc.running: self._lc.stop() + else: + logger.warning("Looping call is not running, cannot stop") while len(self._ongoing_defers) > 0: d = self._ongoing_defers.pop() d.cancel() @@ -476,14 +1045,241 @@ class Backend(object): # send_multipart and this backend class will be really simple. def setup_provider(self, provider): + """ + Initiate the setup for a provider. + + :param provider: URL for the provider + :type provider: unicode + + Signals: + prov_unsupported_client + prov_unsupported_api + prov_name_resolution -> { PASSED_KEY: bool, ERROR_KEY: str } + prov_https_connection -> { PASSED_KEY: bool, ERROR_KEY: str } + prov_download_provider_info -> { PASSED_KEY: bool, ERROR_KEY: str } + """ self._call_queue.put(("provider", "setup_provider", None, provider)) def cancel_setup_provider(self): + """ + Cancel the ongoing setup provider (if any). + """ self._call_queue.put(("provider", "cancel_setup_provider", None)) def provider_bootstrap(self, provider): + """ + Second stage of bootstrapping for a provider. + + :param provider: URL for the provider + :type provider: unicode + + Signals: + prov_problem_with_provider + prov_download_ca_cert -> {PASSED_KEY: bool, ERROR_KEY: str} + prov_check_ca_fingerprint -> {PASSED_KEY: bool, ERROR_KEY: str} + prov_check_api_certificate -> {PASSED_KEY: bool, ERROR_KEY: str} + """ self._call_queue.put(("provider", "bootstrap", None, provider)) def register_user(self, provider, username, password): + """ + Register a user using the domain and password given as parameters. + + :param domain: the domain we need to register the user. + :type domain: unicode + :param username: the user name + :type username: unicode + :param password: the password for the username + :type password: unicode + + Signals: + srp_registration_finished + srp_registration_taken + srp_registration_failed + """ self._call_queue.put(("register", "register_user", None, provider, username, password)) + + def setup_eip(self, provider, skip_network=False): + """ + Initiate the setup for a provider + + :param provider: URL for the provider + :type provider: unicode + :param skip_network: Whether checks that involve network should be done + or not + :type skip_network: bool + + Signals: + eip_config_ready -> {PASSED_KEY: bool, ERROR_KEY: str} + eip_client_certificate_ready -> {PASSED_KEY: bool, ERROR_KEY: str} + eip_cancelled_setup + """ + self._call_queue.put(("eip", "setup_eip", None, provider, + skip_network)) + + def cancel_setup_eip(self): + """ + Cancel the ongoing setup EIP (if any). + """ + self._call_queue.put(("eip", "cancel_setup_eip", None)) + + def start_eip(self): + """ + Start the EIP service. + + Signals: + backend_bad_call + eip_alien_openvpn_already_running + eip_connected + eip_connection_aborted + eip_network_unreachable + eip_no_pkexec_error + eip_no_polkit_agent_error + eip_no_tun_kext_error + eip_openvpn_already_running + eip_openvpn_not_found_error + eip_process_finished + eip_process_restart_ping + eip_process_restart_tls + eip_state_changed -> str + eip_status_changed -> tuple of str (download, upload) + eip_vpn_launcher_exception + """ + self._call_queue.put(("eip", "start", None)) + + def stop_eip(self, shutdown=False): + """ + Stop the EIP service. + + :param shutdown: + :type shutdown: bool + """ + self._call_queue.put(("eip", "stop", None, shutdown)) + + def terminate_eip(self): + """ + Terminate the EIP service, not necessarily in a nice way. + """ + self._call_queue.put(("eip", "terminate", None)) + + def eip_get_gateways_list(self, domain): + """ + Signal a list of gateways for the given provider. + + :param domain: the domain to get the gateways. + :type domain: str + + # TODO discuss how to document the expected result object received of + # the signal + :signal type: list of str + + Signals: + eip_get_gateways_list -> list of unicode + eip_get_gateways_list_error + eip_uninitialized_provider + """ + self._call_queue.put(("eip", "get_gateways_list", None, domain)) + + def eip_get_initialized_providers(self, domains): + """ + Signal a list of the given domains and if they are initialized or not. + + :param domains: the list of domains to check. + :type domain: list of str + + Signals: + eip_get_initialized_providers -> list of tuple(unicode, bool) + + """ + self._call_queue.put(("eip", "get_initialized_providers", + None, domains)) + + def eip_can_start(self, domain): + """ + Signal whether it has everything that is needed to run EIP or not + + :param domain: the domain for the provider to check + :type domain: str + + Signals: + eip_can_start + eip_cannot_start + """ + self._call_queue.put(("eip", "can_start", + None, domain)) + + def login(self, provider, username, password): + """ + Execute the whole authentication process for a user + + :param domain: the domain where we need to authenticate. + :type domain: unicode + :param username: username for this session + :type username: str + :param password: password for this user + :type password: str + + Signals: + srp_auth_error + srp_auth_ok + srp_auth_bad_user_or_password + srp_auth_server_error + srp_auth_connection_error + srp_auth_error + """ + self._call_queue.put(("authenticate", "login", None, provider, + username, password)) + + def logout(self): + """ + Log out the current session. + + Signals: + srp_logout_ok + srp_logout_error + srp_not_logged_in_error + """ + self._call_queue.put(("authenticate", "logout", None)) + + def 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): + """ + Change the user's password. + + :param current_password: the current password of the user. + :type current_password: str + :param new_password: the new password for the user. + :type new_password: str + + Signals: + srp_not_logged_in_error + srp_password_change_ok + srp_password_change_badpw + srp_password_change_error + """ + self._call_queue.put(("authenticate", "change_password", None, + current_password, new_password)) + + def get_logged_in_status(self): + """ + Signal if the user is currently logged in or not. + + Signals: + srp_status_logged_in + srp_status_not_logged_in + """ + 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 get_provider_config(self): + provider_config = self._components["provider"]._provider_config + return provider_config diff --git a/src/leap/bitmask/config/flags.py b/src/leap/bitmask/config/flags.py index 5d8bc9b3..6b70659d 100644 --- a/src/leap/bitmask/config/flags.py +++ b/src/leap/bitmask/config/flags.py @@ -46,7 +46,12 @@ API_VERSION_CHECK = True # Used for skipping soledad bootstrapping/syncs. OFFLINE = False - # CA cert path # used to allow self signed certs in requests that needs SSL CA_CERT_FILE = None + +# OpenVPN verbosity level +OPENVPN_VERBOSITY = 1 + +# Skip the checks in the wizard, use for testing purposes only! +SKIP_WIZARD_CHECKS = False diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index 7cf7e55a..192a9d5c 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -17,6 +17,7 @@ import binascii import logging +import threading import sys import requests @@ -28,8 +29,8 @@ from simplejson.decoder import JSONDecodeError from functools import partial from requests.adapters import HTTPAdapter -from PySide import QtCore from twisted.internet import threads +from twisted.internet.defer import CancelledError from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.util import request_helpers as reqhelper @@ -117,12 +118,12 @@ class SRPAuthNoSessionId(SRPAuthenticationError): pass -class SRPAuth(QtCore.QObject): +class SRPAuth(object): """ SRPAuth singleton """ - class __impl(QtCore.QObject): + class __impl(object): """ Implementation of the SRPAuth interface """ @@ -135,19 +136,21 @@ class SRPAuth(QtCore.QObject): USER_SALT_KEY = 'user[password_salt]' AUTHORIZATION_KEY = "Authorization" - def __init__(self, provider_config): + def __init__(self, provider_config, signaler=None): """ Constructor for SRPAuth implementation - :param server: Server to which we will authenticate - :type server: str + :param provider_config: ProviderConfig needed to authenticate. + :type provider_config: ProviderConfig + :param signaler: Signaler object used to receive notifications + from the backend + :type signaler: Signaler """ - QtCore.QObject.__init__(self) - leap_assert(provider_config, "We need a provider config to authenticate") self._provider_config = provider_config + self._signaler = signaler self._settings = LeapSettings() # **************************************************** # @@ -162,11 +165,11 @@ class SRPAuth(QtCore.QObject): self._reset_session() self._session_id = None - self._session_id_lock = QtCore.QMutex() + self._session_id_lock = threading.Lock() self._uuid = None - self._uuid_lock = QtCore.QMutex() + self._uuid_lock = threading.Lock() self._token = None - self._token_lock = QtCore.QMutex() + self._token_lock = threading.Lock() self._srp_user = None self._srp_a = None @@ -448,7 +451,7 @@ class SRPAuth(QtCore.QObject): def _threader(self, cb, res, *args, **kwargs): return threads.deferToThread(cb, res, *args, **kwargs) - def change_password(self, current_password, new_password): + def _change_password(self, current_password, new_password): """ Changes the password for the currently logged user if the current password match. @@ -499,6 +502,43 @@ class SRPAuth(QtCore.QObject): self._password = new_password + def change_password(self, current_password, new_password): + """ + Changes the password for the currently logged user if the current + password match. + It requires to be authenticated. + + :param current_password: the current password for the logged user. + :type current_password: str + :param new_password: the new password for the user + :type new_password: str + """ + d = threads.deferToThread( + self._change_password, current_password, new_password) + d.addCallback(self._change_password_ok) + d.addErrback(self._change_password_error) + + def _change_password_ok(self, _): + """ + Password change callback. + """ + if self._signaler is not None: + self._signaler.signal(self._signaler.SRP_PASSWORD_CHANGE_OK) + + def _change_password_error(self, failure): + """ + Password change errback. + """ + logger.debug( + "Error changing password. Failure: {0}".format(failure)) + if self._signaler is None: + return + + if failure.check(SRPAuthBadUserOrPassword): + self._signaler.signal(self._signaler.SRP_PASSWORD_CHANGE_BADPW) + else: + self._signaler.signal(self._signaler.SRP_PASSWORD_CHANGE_ERROR) + def authenticate(self, username, password): """ Executes the whole authentication process for a user @@ -539,8 +579,49 @@ class SRPAuth(QtCore.QObject): d.addCallback(partial(self._threader, self._verify_session)) + d.addCallback(self._authenticate_ok) + d.addErrback(self._authenticate_error) return d + def _authenticate_ok(self, _): + """ + Callback that notifies that the authentication was successful. + + :param _: IGNORED, output from the previous callback (None) + :type _: IGNORED + """ + logger.debug("Successful login!") + self._signaler.signal(self._signaler.SRP_AUTH_OK) + + def _authenticate_error(self, failure): + """ + Error handler for the srpauth.authenticate method. + + :param failure: failure object that Twisted generates + :type failure: twisted.python.failure.Failure + """ + logger.error("Error logging in, {0!r}".format(failure)) + + signal = None + if failure.check(CancelledError): + logger.debug("Defer cancelled.") + failure.trap(Exception) + return + + if self._signaler is None: + return + + if failure.check(SRPAuthBadUserOrPassword): + signal = self._signaler.SRP_AUTH_BAD_USER_OR_PASSWORD + elif failure.check(SRPAuthConnectionError): + signal = self._signaler.SRP_AUTH_CONNECTION_ERROR + elif failure.check(SRPAuthenticationError): + signal = self._signaler.SRP_AUTH_SERVER_ERROR + else: + signal = self._signaler.SRP_AUTH_ERROR + + self._signaler.signal(signal) + def logout(self): """ Logs out the current session. @@ -565,6 +646,8 @@ class SRPAuth(QtCore.QObject): except Exception as e: logger.warning("Something went wrong with the logout: %r" % (e,)) + if self._signaler is not None: + self._signaler.signal(self._signaler.SRP_LOGOUT_ERROR) raise else: self.set_session_id(None) @@ -573,51 +656,65 @@ class SRPAuth(QtCore.QObject): # Also reset the session self._session = self._fetcher.session() logger.debug("Successfully logged out.") + if self._signaler is not None: + self._signaler.signal(self._signaler.SRP_LOGOUT_OK) def set_session_id(self, session_id): - QtCore.QMutexLocker(self._session_id_lock) - self._session_id = session_id + with self._session_id_lock: + self._session_id = session_id def get_session_id(self): - QtCore.QMutexLocker(self._session_id_lock) - return self._session_id + with self._session_id_lock: + return self._session_id def set_uuid(self, uuid): - QtCore.QMutexLocker(self._uuid_lock) - full_uid = "%s@%s" % ( - self._username, self._provider_config.get_domain()) - if uuid is not None: # avoid removing the uuid from settings - self._settings.set_uuid(full_uid, uuid) - self._uuid = uuid + with self._uuid_lock: + full_uid = "%s@%s" % ( + self._username, self._provider_config.get_domain()) + if uuid is not None: # avoid removing the uuid from settings + self._settings.set_uuid(full_uid, uuid) + self._uuid = uuid def get_uuid(self): - QtCore.QMutexLocker(self._uuid_lock) - return self._uuid + with self._uuid_lock: + return self._uuid def set_token(self, token): - QtCore.QMutexLocker(self._token_lock) - self._token = token + with self._token_lock: + self._token = token def get_token(self): - QtCore.QMutexLocker(self._token_lock) - return self._token + with self._token_lock: + return self._token - __instance = None + def is_authenticated(self): + """ + Return whether the user is authenticated or not. - authentication_finished = QtCore.Signal() - logout_ok = QtCore.Signal() - logout_error = QtCore.Signal() + :rtype: bool + """ + user = self._srp_user + if user is not None: + return user.authenticated() - def __init__(self, provider_config): - """ - Creates a singleton instance if needed + return False + + __instance = None + + def __init__(self, provider_config, signaler=None): """ - QtCore.QObject.__init__(self) + Create a singleton instance if needed + :param provider_config: ProviderConfig needed to authenticate. + :type provider_config: ProviderConfig + :param signaler: Signaler object used to send notifications + from the backend + :type signaler: Signaler + """ # Check whether we already have an instance if SRPAuth.__instance is None: # Create and remember instance - SRPAuth.__instance = SRPAuth.__impl(provider_config) + SRPAuth.__instance = SRPAuth.__impl(provider_config, signaler) # Store instance reference as the only member in the handle self.__dict__['_SRPAuth__instance'] = SRPAuth.__instance @@ -642,9 +739,16 @@ class SRPAuth(QtCore.QObject): """ username = username.lower() d = self.__instance.authenticate(username, password) - d.addCallback(self._gui_notify) return d + def is_authenticated(self): + """ + Return whether the user is authenticated or not. + + :rtype: bool + """ + return self.__instance.is_authenticated() + def change_password(self, current_password, new_password): """ Changes the user's password. @@ -657,8 +761,7 @@ class SRPAuth(QtCore.QObject): :returns: a defer to interact with. :rtype: twisted.internet.defer.Deferred """ - d = threads.deferToThread( - self.__instance.change_password, current_password, new_password) + d = self.__instance.change_password(current_password, new_password) return d def get_username(self): @@ -672,16 +775,6 @@ class SRPAuth(QtCore.QObject): return None return self.__instance._username - def _gui_notify(self, _): - """ - Callback that notifies the UI with the proper signal. - - :param _: IGNORED, output from the previous callback (None) - :type _: IGNORED - """ - logger.debug("Successful login!") - self.authentication_finished.emit() - def get_session_id(self): return self.__instance.get_session_id() @@ -699,9 +792,7 @@ class SRPAuth(QtCore.QObject): try: self.__instance.logout() logger.debug("Logout success") - self.logout_ok.emit() return True except Exception as e: logger.debug("Logout error: {0!r}".format(e)) - self.logout_error.emit() return False diff --git a/src/leap/bitmask/crypto/srpregister.py b/src/leap/bitmask/crypto/srpregister.py index 4c52db42..f03dc469 100644 --- a/src/leap/bitmask/crypto/srpregister.py +++ b/src/leap/bitmask/crypto/srpregister.py @@ -46,8 +46,6 @@ class SRPRegister(QtCore.QObject): STATUS_TAKEN = 422 STATUS_ERROR = -999 # Custom error status - registration_finished = QtCore.Signal(bool, object) - def __init__(self, signaler=None, provider_config=None, register_path="users"): """ diff --git a/src/leap/bitmask/gui/advanced_key_management.py b/src/leap/bitmask/gui/advanced_key_management.py index cbc8c3e3..be6b4410 100644 --- a/src/leap/bitmask/gui/advanced_key_management.py +++ b/src/leap/bitmask/gui/advanced_key_management.py @@ -30,12 +30,17 @@ from ui_advanced_key_management import Ui_AdvancedKeyManagement logger = logging.getLogger(__name__) -class AdvancedKeyManagement(QtGui.QWidget): +class AdvancedKeyManagement(QtGui.QDialog): """ Advanced Key Management """ - def __init__(self, user, keymanager, soledad): + def __init__(self, parent, has_mx, user, keymanager, soledad): """ + :param parent: parent object of AdvancedKeyManagement. + :parent type: QWidget + :param has_mx: defines whether the current provider provides email or + not. + :type has_mx: bool :param user: the current logged in user. :type user: unicode :param keymanager: the existing keymanager instance @@ -43,7 +48,7 @@ class AdvancedKeyManagement(QtGui.QWidget): :param soledad: a loaded instance of Soledad :type soledad: Soledad """ - QtGui.QWidget.__init__(self) + QtGui.QDialog.__init__(self, parent) self.ui = Ui_AdvancedKeyManagement() self.ui.setupUi(self) @@ -52,13 +57,18 @@ class AdvancedKeyManagement(QtGui.QWidget): 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}.") + msg = msg.format(get_service_display_name(MX_SERVICE)) + self._disable_ui(msg) + return + + # if Soledad is not started yet if sameProxiedObjects(soledad, None): - self.ui.gbMyKeyPair.setEnabled(False) - self.ui.gbStoredPublicKeys.setEnabled(False) - msg = self.tr("<span style='color:#0000FF;'>NOTE</span>: " - "To use this, you need to enable/start {0}.") + msg = self.tr("To use this, you need to enable/start {0}.") msg = msg.format(get_service_display_name(MX_SERVICE)) - self.ui.lblStatus.setText(msg) + self._disable_ui(msg) return # XXX: since import is disabled this is no longer a dangerous feature. # else: @@ -90,6 +100,18 @@ class AdvancedKeyManagement(QtGui.QWidget): self._list_keys() + def _disable_ui(self, msg): + """ + Disable the UI and set a note in the status bar. + + :param msg: note to display in the status bar. + :type msg: unicode + """ + self.ui.gbMyKeyPair.setEnabled(False) + self.ui.gbStoredPublicKeys.setEnabled(False) + msg = self.tr("<span style='color:#0000FF;'>NOTE</span>: ") + msg + self.ui.lblStatus.setText(msg) + def _import_keys(self): """ Imports the user's key pair. diff --git a/src/leap/bitmask/gui/eip_preferenceswindow.py b/src/leap/bitmask/gui/eip_preferenceswindow.py index dcaa8b1e..530cd38d 100644 --- a/src/leap/bitmask/gui/eip_preferenceswindow.py +++ b/src/leap/bitmask/gui/eip_preferenceswindow.py @@ -18,17 +18,13 @@ """ EIP Preferences window """ -import os import logging from functools import partial from PySide import QtCore, QtGui from leap.bitmask.config.leapsettings import LeapSettings -from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.gui.ui_eippreferences import Ui_EIPPreferences -from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector -from leap.bitmask.services.eip.eipconfig import get_eipconfig_path logger = logging.getLogger(__name__) @@ -37,17 +33,20 @@ class EIPPreferencesWindow(QtGui.QDialog): """ Window that displays the EIP preferences. """ - def __init__(self, parent, domain): + def __init__(self, parent, domain, backend): """ :param parent: parent object of the EIPPreferencesWindow. :type parent: QWidget :param domain: the selected by default domain. :type domain: unicode + :param backend: Backend being used + :type backend: Backend """ QtGui.QDialog.__init__(self, parent) self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") self._settings = LeapSettings() + self._backend = backend # Load UI self.ui = Ui_EIPPreferences() @@ -61,7 +60,11 @@ class EIPPreferencesWindow(QtGui.QDialog): self.ui.cbGateways.currentIndexChanged[unicode].connect( lambda x: self.ui.lblProvidersGatewayStatus.setVisible(False)) - self._add_configured_providers(domain) + self._selected_domain = domain + self._configured_providers = [] + + self._backend_connect() + self._add_configured_providers() def _set_providers_gateway_status(self, status, success=False, error=False): @@ -85,35 +88,49 @@ class EIPPreferencesWindow(QtGui.QDialog): self.ui.lblProvidersGatewayStatus.setVisible(True) self.ui.lblProvidersGatewayStatus.setText(status) - def _add_configured_providers(self, domain=None): + def _add_configured_providers(self): """ Add the client's configured providers to the providers combo boxes. + """ + providers = self._settings.get_configured_providers() + if not providers: + return - :param domain: the domain to be selected by default. - :type domain: unicode + self._backend.eip_get_initialized_providers(providers) + + @QtCore.Slot(list) + def _load_providers_in_combo(self, providers): + """ + TRIGGERS: + Signaler.eip_get_initialized_providers + + Add the client's configured providers to the providers combo boxes. + + :param providers: the list of providers to add and whether each one is + initialized or not. + :type providers: list of tuples (str, bool) """ self.ui.cbProvidersGateway.clear() - providers = self._settings.get_configured_providers() if not providers: self.ui.gbGatewaySelector.setEnabled(False) return - for provider in providers: + for provider, is_initialized in providers: label = provider - eip_config_path = get_eipconfig_path(provider, relative=False) - if not os.path.isfile(eip_config_path): - label = provider + self.tr(" (uninitialized)") + if not is_initialized: + label += self.tr(" (uninitialized)") self.ui.cbProvidersGateway.addItem(label, userData=provider) # Select provider by name + domain = self._selected_domain if domain is not None: provider_index = self.ui.cbProvidersGateway.findText( domain, QtCore.Qt.MatchStartsWith) self.ui.cbProvidersGateway.setCurrentIndex(provider_index) + @QtCore.Slot(str) def _save_selected_gateway(self, provider): """ - SLOT TRIGGERS: self.ui.pbSaveGateway.clicked @@ -136,9 +153,9 @@ class EIPPreferencesWindow(QtGui.QDialog): "Gateway settings for provider '{0}' saved.").format(provider) self._set_providers_gateway_status(msg, success=True) + @QtCore.Slot(int) def _populate_gateways(self, domain_idx): """ - SLOT TRIGGERS: self.ui.cbProvidersGateway.currentIndexChanged[unicode] @@ -155,18 +172,27 @@ class EIPPreferencesWindow(QtGui.QDialog): return domain = self.ui.cbProvidersGateway.itemData(domain_idx) + self._selected_domain = domain - if not os.path.isfile(get_eipconfig_path(domain, relative=False)): - self._set_providers_gateway_status( - self.tr("This is an uninitialized provider, " - "please log in first."), - error=True) - self.ui.pbSaveGateway.setEnabled(False) - self.ui.cbGateways.setEnabled(False) - return - else: - self.ui.pbSaveGateway.setEnabled(True) - self.ui.cbGateways.setEnabled(True) + self._backend.eip_get_gateways_list(domain) + + @QtCore.Slot(list) + def _update_gateways_list(self, gateways): + """ + TRIGGERS: + Signaler.eip_get_gateways_list + + :param gateways: a list of gateways + :type gateways: list of unicode + + Add the available gateways and select the one stored in configuration + file. + """ + self.ui.pbSaveGateway.setEnabled(True) + self.ui.cbGateways.setEnabled(True) + + self.ui.cbGateways.clear() + self.ui.cbGateways.addItem(self.AUTOMATIC_GATEWAY_LABEL) try: # disconnect previously connected save method @@ -175,31 +201,13 @@ class EIPPreferencesWindow(QtGui.QDialog): pass # Signal was not connected # set the proper connection for the 'save' button + domain = self._selected_domain save_gateway = partial(self._save_selected_gateway, domain) self.ui.pbSaveGateway.clicked.connect(save_gateway) - eip_config = 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(get_eipconfig_path(domain)) - - if not eip_loaded or provider_config is None: - self._set_providers_gateway_status( - self.tr("There was a problem with configuration files."), - error=True) - return - - gateways = VPNGatewaySelector(eip_config).get_gateways_list() - logger.debug(gateways) - - self.ui.cbGateways.clear() - self.ui.cbGateways.addItem(self.AUTOMATIC_GATEWAY_LABEL) + selected_gateway = self._settings.get_selected_gateway( + self._selected_domain) - # Add the available gateways and - # select the one stored in configuration file. - selected_gateway = self._settings.get_selected_gateway(domain) index = 0 for idx, (gw_name, gw_ip) in enumerate(gateways): gateway = "{0} ({1})".format(gw_name, gw_ip) @@ -208,3 +216,42 @@ class EIPPreferencesWindow(QtGui.QDialog): index = idx + 1 self.ui.cbGateways.setCurrentIndex(index) + + @QtCore.Slot() + def _gateways_list_error(self): + """ + TRIGGERS: + Signaler.eip_get_gateways_list_error + + An error has occurred retrieving the gateway list so we inform the + user. + """ + self._set_providers_gateway_status( + self.tr("There was a problem with configuration files."), + error=True) + self.ui.pbSaveGateway.setEnabled(False) + self.ui.cbGateways.setEnabled(False) + + @QtCore.Slot() + def _gateways_list_uninitialized(self): + """ + TRIGGERS: + Signaler.eip_uninitialized_provider + + The requested provider in not initialized yet, so we give the user an + error msg. + """ + self._set_providers_gateway_status( + self.tr("This is an uninitialized provider, please log in first."), + error=True) + self.ui.pbSaveGateway.setEnabled(False) + self.ui.cbGateways.setEnabled(False) + + def _backend_connect(self): + sig = self._backend.signaler + sig.eip_get_gateways_list.connect(self._update_gateways_list) + sig.eip_get_gateways_list_error.connect(self._gateways_list_error) + sig.eip_uninitialized_provider.connect( + self._gateways_list_uninitialized) + sig.eip_get_initialized_providers.connect( + self._load_providers_in_combo) diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py index 19942d9d..ca28b8bf 100644 --- a/src/leap/bitmask/gui/eip_status.py +++ b/src/leap/bitmask/gui/eip_status.py @@ -25,7 +25,6 @@ from functools import partial from PySide import QtCore, QtGui from leap.bitmask.services.eip.connection import EIPConnection -from leap.bitmask.services.eip.vpnprocess import VPNManager from leap.bitmask.services import get_service_display_name, EIP_SERVICE from leap.bitmask.platform_init import IS_LINUX from leap.bitmask.util.averages import RateMovingAverage @@ -89,17 +88,18 @@ class EIPStatusWidget(QtGui.QWidget): self.ui.btnUpload.clicked.connect(onclicked) self.ui.btnDownload.clicked.connect(onclicked) + @QtCore.Slot() def _on_VPN_status_clicked(self): """ - SLOT - TRIGGER: self.ui.btnUpload.clicked - self.ui.btnDownload.clicked + TRIGGERS: + self.ui.btnUpload.clicked + self.ui.btnDownload.clicked Toggles between rate and total throughput display for vpn status figures. """ self.DISPLAY_TRAFFIC_RATES = not self.DISPLAY_TRAFFIC_RATES - self.update_vpn_status(None) # refresh + self.update_vpn_status() # refresh def _set_traffic_rates(self): """ @@ -117,7 +117,7 @@ class EIPStatusWidget(QtGui.QWidget): """ self._up_rate.reset() self._down_rate.reset() - self.update_vpn_status(None) + self.update_vpn_status() def _update_traffic_rates(self, up, down): """ @@ -260,11 +260,12 @@ class EIPStatusWidget(QtGui.QWidget): self._service_name, self.tr("disabled"))) # Replace EIP tray menu with an action that displays a "disabled" text - menu = self._systray.contextMenu() - menu.insertAction( - self._eip_status_menu.menuAction(), - self._eip_disabled_action) - self._eip_status_menu.menuAction().setVisible(False) + if self.isVisible(): + menu = self._systray.contextMenu() + menu.insertAction( + self._eip_status_menu.menuAction(), + self._eip_disabled_action) + self._eip_status_menu.menuAction().setVisible(False) @QtCore.Slot() def enable_eip_start(self): @@ -278,7 +279,8 @@ class EIPStatusWidget(QtGui.QWidget): # Restore the eip action menu menu = self._systray.contextMenu() menu.removeAction(self._eip_disabled_action) - self._eip_status_menu.menuAction().setVisible(True) + if self.isVisible(): + self._eip_status_menu.menuAction().setVisible(True) # XXX disable (later) -------------------------- def set_eip_status(self, status, error=False): @@ -348,23 +350,27 @@ class EIPStatusWidget(QtGui.QWidget): self.tr("Traffic is being routed in the clear")) self.ui.lblEIPStatus.show() - def update_vpn_status(self, data): + @QtCore.Slot(dict) + def update_vpn_status(self, data=None): """ - SLOT - TRIGGER: VPN.status_changed + TRIGGERS: + Signaler.eip_status_changed - Updates the download/upload labels based on the data provided - by the VPN thread. + Updates the download/upload labels based on the data provided by the + VPN thread. + If data is None, we just will refresh the display based on the previous + data. - :param data: a dictionary with the tcp/udp write and read totals. - If data is None, we just will refresh the display based - on the previous data. - :type data: dict + :param data: a tuple with download/upload totals (download, upload). + :type data: tuple """ - if data: - upload = float(data[VPNManager.TCPUDP_WRITE_KEY] or "0") - download = float(data[VPNManager.TCPUDP_READ_KEY] or "0") - self._update_traffic_rates(upload, download) + if data is not None: + try: + upload, download = map(float, data) + self._update_traffic_rates(upload, download) + except Exception: + # discard invalid data + return if self.DISPLAY_TRAFFIC_RATES: uprate, downrate = self._get_traffic_rates() @@ -379,39 +385,42 @@ class EIPStatusWidget(QtGui.QWidget): self.ui.btnUpload.setText(upload_str) self.ui.btnDownload.setText(download_str) - def update_vpn_state(self, data): + @QtCore.Slot(dict) + def update_vpn_state(self, vpn_state): """ - SLOT - TRIGGER: VPN.state_changed + TRIGGERS: + Signaler.eip_state_changed Updates the displayed VPN state based on the data provided by the VPN thread. + :param vpn_state: the state of the VPN + :type vpn_state: dict + Emits: - If the status is connected, we emit EIPConnection.qtsigs. + If the vpn_state is connected, we emit EIPConnection.qtsigs. connected_signal """ - status = data[VPNManager.STATUS_STEP_KEY] - self.set_eip_status_icon(status) - if status == "CONNECTED": + self.set_eip_status_icon(vpn_state) + if vpn_state == "CONNECTED": self.ui.eip_bandwidth.show() self.ui.lblEIPStatus.hide() # XXX should be handled by the state machine too. self.eip_connection_connected.emit() - # XXX should lookup status map in EIPConnection - elif status == "AUTH": + # XXX should lookup vpn_state map in EIPConnection + elif vpn_state == "AUTH": self.set_eip_status(self.tr("Authenticating...")) - elif status == "GET_CONFIG": + elif vpn_state == "GET_CONFIG": self.set_eip_status(self.tr("Retrieving configuration...")) - elif status == "WAIT": + elif vpn_state == "WAIT": self.set_eip_status(self.tr("Waiting to start...")) - elif status == "ASSIGN_IP": + elif vpn_state == "ASSIGN_IP": self.set_eip_status(self.tr("Assigning IP")) - elif status == "RECONNECTING": + elif vpn_state == "RECONNECTING": self.set_eip_status(self.tr("Reconnecting...")) - elif status == "ALREADYRUNNING": + elif vpn_state == "ALREADYRUNNING": # Put the following calls in Qt's event queue, otherwise # the UI won't update properly QtCore.QTimer.singleShot( @@ -419,7 +428,7 @@ class EIPStatusWidget(QtGui.QWidget): msg = self.tr("Unable to start VPN, it's already running.") QtCore.QTimer.singleShot(0, partial(self.set_eip_status, msg)) else: - self.set_eip_status(status) + self.set_eip_status(vpn_state) def set_eip_icon(self, icon): """ diff --git a/src/leap/bitmask/gui/loggerwindow.py b/src/leap/bitmask/gui/loggerwindow.py index 9f396574..f19b172f 100644 --- a/src/leap/bitmask/gui/loggerwindow.py +++ b/src/leap/bitmask/gui/loggerwindow.py @@ -21,14 +21,14 @@ History log window import logging import cgi -from PySide import QtGui +from PySide import QtCore, QtGui from twisted.internet import threads from ui_loggerwindow import Ui_LoggerWindow from leap.bitmask.util.constants import PASTEBIN_API_DEV_KEY from leap.bitmask.util.leap_log_handler import LeapLogHandler -from leap.bitmask.util.pastebin import PastebinAPI, PastebinError +from leap.bitmask.util import pastebin from leap.common.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) @@ -203,10 +203,10 @@ class LoggerWindow(QtGui.QDialog): Send content to pastebin and return the link. """ content = self._current_history - pb = PastebinAPI() + pb = pastebin.PastebinAPI() link = pb.paste(PASTEBIN_API_DEV_KEY, content, paste_name="Bitmask log", - paste_expire_date='1W') + paste_expire_date='1M') # convert to 'raw' link link = "http://pastebin.com/raw.php?i=" + link.split('/')[-1] @@ -220,12 +220,16 @@ class LoggerWindow(QtGui.QDialog): :param link: the recently created pastebin link. :type link: str """ + self._set_pastebin_sending(False) msg = self.tr("Your pastebin link <a href='{0}'>{0}</a>") msg = msg.format(link) - show_info = lambda: QtGui.QMessageBox.information( - self, self.tr("Pastebin OK"), msg) - self._set_pastebin_sending(False) - self.reactor.callLater(0, show_info) + + # We save the dialog in an instance member to avoid dialog being + # deleted right after we exit this method + self._msgBox = msgBox = QtGui.QMessageBox( + QtGui.QMessageBox.Information, self.tr("Pastebin OK"), msg) + msgBox.setWindowModality(QtCore.Qt.NonModal) + msgBox.show() def pastebin_err(failure): """ @@ -234,13 +238,19 @@ class LoggerWindow(QtGui.QDialog): :param failure: the failure that triggered the errback. :type failure: twisted.python.failure.Failure """ + self._set_pastebin_sending(False) logger.error(repr(failure)) + msg = self.tr("Sending logs to Pastebin failed!") - show_err = lambda: QtGui.QMessageBox.critical( - self, self.tr("Pastebin Error"), msg) - self._set_pastebin_sending(False) - self.reactor.callLater(0, show_err) - failure.trap(PastebinError) + if failure.check(pastebin.PostLimitError): + msg = self.tr('Maximum posts per day reached') + + # We save the dialog in an instance member to avoid dialog being + # deleted right after we exit this method + self._msgBox = msgBox = QtGui.QMessageBox( + QtGui.QMessageBox.Critical, self.tr("Pastebin Error"), msg) + msgBox.setWindowModality(QtCore.Qt.NonModal) + msgBox.show() self._set_pastebin_sending(True) d = threads.deferToThread(do_pastebin) diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py index 4a483c32..ac7ad878 100644 --- a/src/leap/bitmask/gui/login.py +++ b/src/leap/bitmask/gui/login.py @@ -259,12 +259,16 @@ class LoginWidget(QtGui.QWidget): """ self.ui.lnPassword.setFocus() - def _current_provider_changed(self, param): + @QtCore.Slot(int) + def _current_provider_changed(self, idx): """ - SLOT - TRIGGERS: self.ui.cmbProviders.currentIndexChanged + TRIGGERS: + self.ui.cmbProviders.currentIndexChanged + + :param idx: the index of the new selected item + :type idx: int """ - if param == (self.ui.cmbProviders.count() - 1): + if idx == (self.ui.cmbProviders.count() - 1): self.show_wizard.emit() # Leave the previously selected provider in the combobox prev_provider = 0 @@ -274,7 +278,7 @@ class LoginWidget(QtGui.QWidget): self.ui.cmbProviders.setCurrentIndex(prev_provider) self.ui.cmbProviders.blockSignals(False) else: - self._selected_provider_index = param + self._selected_provider_index = idx def start_login(self): """ diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py index 44a138e2..d3346780 100644 --- a/src/leap/bitmask/gui/mail_status.py +++ b/src/leap/bitmask/gui/mail_status.py @@ -184,10 +184,10 @@ class MailStatusWidget(QtGui.QWidget): leap_assert_type(action_mail_status, QtGui.QAction) self._action_mail_status = action_mail_status + @QtCore.Slot() def set_soledad_failed(self): """ - SLOT - TRIGGER: + TRIGGERS: SoledadBootstrapper.soledad_failed This method is called whenever soledad has a failure. @@ -195,10 +195,10 @@ class MailStatusWidget(QtGui.QWidget): msg = self.tr("There was an unexpected problem with Soledad.") self._set_mail_status(msg, ready=-1) + @QtCore.Slot() def set_soledad_invalid_auth_token(self): """ - SLOT - TRIGGER: + TRIGGERS: SoledadBootstrapper.soledad_invalid_token This method is called when the auth token is invalid @@ -250,10 +250,11 @@ class MailStatusWidget(QtGui.QWidget): """ self._soledad_event.emit(req) + @QtCore.Slot(object) def _mail_handle_soledad_events_slot(self, req): """ - SLOT - TRIGGER: _mail_handle_soledad_events + TRIGGERS: + _mail_handle_soledad_events Reacts to an Soledad event @@ -284,10 +285,11 @@ class MailStatusWidget(QtGui.QWidget): """ self._keymanager_event.emit(req) + @QtCore.Slot(object) def _mail_handle_keymanager_events_slot(self, req): """ - SLOT - TRIGGER: _mail_handle_keymanager_events + TRIGGERS: + _mail_handle_keymanager_events Reacts to an KeyManager event @@ -330,10 +332,11 @@ class MailStatusWidget(QtGui.QWidget): """ self._smtp_event.emit(req) + @QtCore.Slot(object) def _mail_handle_smtp_events_slot(self, req): """ - SLOT - TRIGGER: _mail_handle_smtp_events + TRIGGERS: + _mail_handle_smtp_events Reacts to an SMTP event @@ -364,10 +367,11 @@ class MailStatusWidget(QtGui.QWidget): """ self._imap_event.emit(req) + @QtCore.Slot(object) def _mail_handle_imap_events_slot(self, req): """ - SLOT - TRIGGER: _mail_handle_imap_events + TRIGGERS: + _mail_handle_imap_events Reacts to an IMAP event diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 5abfaa67..e3848c46 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -19,6 +19,7 @@ Main window for Bitmask. """ import logging import socket +import time from threading import Condition from datetime import datetime @@ -26,7 +27,6 @@ from datetime import datetime from PySide import QtCore, QtGui from zope.proxy import ProxyBase, setProxiedObject from twisted.internet import reactor, threads -from twisted.internet.defer import CancelledError from leap.bitmask import __version__ as VERSION from leap.bitmask import __version_hash__ as VERSION_HASH @@ -34,19 +34,16 @@ from leap.bitmask.config import flags from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.crypto import srpauth -from leap.bitmask.crypto.srpauth import SRPAuth - -from leap.bitmask.gui.loggerwindow import LoggerWindow +from leap.bitmask.gui import statemachines from leap.bitmask.gui.advanced_key_management import AdvancedKeyManagement -from leap.bitmask.gui.login import LoginWidget -from leap.bitmask.gui.preferenceswindow import PreferencesWindow from leap.bitmask.gui.eip_preferenceswindow import EIPPreferencesWindow -from leap.bitmask.gui import statemachines from leap.bitmask.gui.eip_status import EIPStatusWidget +from leap.bitmask.gui.loggerwindow import LoggerWindow +from leap.bitmask.gui.login import LoginWidget from leap.bitmask.gui.mail_status import MailStatusWidget -from leap.bitmask.gui.wizard import Wizard +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 @@ -59,20 +56,7 @@ from leap.bitmask.services import get_service_display_name from leap.bitmask.services.mail import conductor as mail_conductor from leap.bitmask.services import EIP_SERVICE, MX_SERVICE -from leap.bitmask.services.eip import eipconfig -from leap.bitmask.services.eip import get_openvpn_management -from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper from leap.bitmask.services.eip.connection import EIPConnection -from leap.bitmask.services.eip.vpnprocess import VPN -from leap.bitmask.services.eip.vpnprocess import OpenVPNAlreadyRunning -from leap.bitmask.services.eip.vpnprocess import AlienOpenVPNAlreadyRunning - -from leap.bitmask.services.eip.vpnlauncher import VPNLauncherException -from leap.bitmask.services.eip.vpnlauncher import OpenVPNNotFoundException -from leap.bitmask.services.eip.linuxvpnlauncher import EIPNoPkexecAvailable -from leap.bitmask.services.eip.linuxvpnlauncher import \ - EIPNoPolkitAuthAgentAvailable -from leap.bitmask.services.eip.darwinvpnlauncher import EIPNoTunKextLoaded from leap.bitmask.services.soledad.soledadbootstrapper import \ SoledadBootstrapper @@ -99,11 +83,6 @@ class MainWindow(QtGui.QMainWindow): """ Main window for login and presenting status updates to the user """ - - # StackedWidget indexes - LOGIN_INDEX = 0 - EIP_STATUS_INDEX = 1 - # Signals eip_needs_login = QtCore.Signal([]) offline_mode_bypass_login = QtCore.Signal([]) @@ -122,9 +101,7 @@ class MainWindow(QtGui.QMainWindow): # We give each service some time to come to a halt before forcing quit SERVICE_STOP_TIMEOUT = 20 - def __init__(self, quit_callback, - openvpn_verb=1, - bypass_checks=False): + def __init__(self, quit_callback, bypass_checks=False, start_hidden=False): """ Constructor for the client main window @@ -132,10 +109,12 @@ class MainWindow(QtGui.QMainWindow): the application. :type quit_callback: callable - :param bypass_checks: Set to true if the app should bypass - first round of checks for CA - certificates at bootstrap + :param bypass_checks: Set to true if the app should bypass first round + of checks for CA certificates at bootstrap :type bypass_checks: bool + :param start_hidden: Set to true if the app should not show the window + but just the tray. + :type start_hidden: bool """ QtGui.QMainWindow.__init__(self) @@ -190,31 +169,30 @@ class MainWindow(QtGui.QMainWindow): # XXX this should be handled by EIP Conductor self._eip_connection.qtsigs.connecting_signal.connect( - self._start_eip) + self._start_EIP) self._eip_connection.qtsigs.disconnecting_signal.connect( self._stop_eip) self._eip_status.eip_connection_connected.connect( - self._on_eip_connected) + self._on_eip_connection_connected) self._eip_status.eip_connection_connected.connect( self._maybe_run_soledad_setup_checks) self.offline_mode_bypass_login.connect( self._maybe_run_soledad_setup_checks) - self.eip_needs_login.connect( - self._eip_status.disable_eip_start) - self.eip_needs_login.connect( - self._disable_eip_start_action) + self.eip_needs_login.connect(self._eip_status.disable_eip_start) + self.eip_needs_login.connect(self._disable_eip_start_action) + + 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._components["provider"]._provider_config) + self._provider_config = self._backend.get_provider_config() + # Used for automatic start of EIP self._provisional_provider_config = ProviderConfig() - self._eip_config = eipconfig.EIPConfig() self._already_started_eip = False self._already_started_soledad = False @@ -224,35 +202,9 @@ class MainWindow(QtGui.QMainWindow): self._logged_user = None self._logged_in_offline = False + self._backend_connected_signals = {} self._backend_connect() - # This thread is similar to the provider bootstrapper - self._eip_bootstrapper = EIPBootstrapper() - - # EIP signals ---- move to eip conductor. - # TODO change the name of "download_config" signal to - # something less confusing (config_ready maybe) - self._eip_bootstrapper.download_config.connect( - self._eip_intermediate_stage) - self._eip_bootstrapper.download_client_certificate.connect( - self._finish_eip_bootstrap) - - self._vpn = VPN(openvpn_verb=openvpn_verb) - - # connect vpn process signals - self._vpn.qtsigs.state_changed.connect( - self._eip_status.update_vpn_state) - self._vpn.qtsigs.status_changed.connect( - self._eip_status.update_vpn_status) - self._vpn.qtsigs.process_finished.connect( - self._eip_finished) - self._vpn.qtsigs.network_unreachable.connect( - self._on_eip_network_unreachable) - self._vpn.qtsigs.process_restart_tls.connect( - self._do_eip_restart) - self._vpn.qtsigs.process_restart_ping.connect( - self._do_eip_restart) - self._soledad_bootstrapper = SoledadBootstrapper() self._soledad_bootstrapper.download_config.connect( self._soledad_intermediate_stage) @@ -260,8 +212,6 @@ class MainWindow(QtGui.QMainWindow): self._soledad_bootstrapped_stage) self._soledad_bootstrapper.local_only_ready.connect( self._soledad_bootstrapped_stage) - self._soledad_bootstrapper.soledad_timeout.connect( - self._retry_soledad_connection) self._soledad_bootstrapper.soledad_invalid_auth_token.connect( self._mail_status.set_soledad_invalid_auth_token) self._soledad_bootstrapper.soledad_failed.connect( @@ -307,6 +257,8 @@ class MainWindow(QtGui.QMainWindow): # self.ui.btnEIPPreferences.clicked.connect(self._show_eip_preferences) self._enabled_services = [] + self._ui_mx_visible = True + self._ui_eip_visible = True # last minute UI manipulations @@ -342,6 +294,7 @@ class MainWindow(QtGui.QMainWindow): self._logger_window = None 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 @@ -349,7 +302,6 @@ class MainWindow(QtGui.QMainWindow): self._soledad = ProxyBase(None) self._keymanager = ProxyBase(None) - self._login_defer = None self._soledad_defer = None self._mail_conductor = mail_conductor.MailConductor( @@ -372,7 +324,7 @@ class MainWindow(QtGui.QMainWindow): if self._first_run(): self._wizard_firstrun = True - self._backend_disconnect() + 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 @@ -384,46 +336,151 @@ class MainWindow(QtGui.QMainWindow): # so this has to be done after eip_machine is started self._finish_init() + def _not_logged_in_error(self): + """ + Handle the 'not logged in' backend error if we try to do an operation + that requires to be logged in. + """ + logger.critical("You are trying to do an operation that requires " + "log in first.") + QtGui.QMessageBox.critical( + self, self.tr("Application error"), + self.tr("You are trying to do an operation " + "that requires logging in first.")) + + def _connect_and_track(self, signal, method): + """ + Helper to connect signals and keep track of them. + + :param signal: the signal to connect to. + :type signal: QtCore.Signal + :param method: the method to call when the signal is triggered. + :type method: callable, Slot or Signal + """ + self._backend_connected_signals[signal] = method + signal.connect(method) + + def _backend_bad_call(self, data): + """ + Callback for debugging bad backend calls + + :param data: data from the backend about the problem + :type data: str + """ + logger.error("Bad call to the backend:") + logger.error(data) + def _backend_connect(self): """ Helper to connect to backend signals """ sig = self._backend.signaler - sig.prov_name_resolution.connect(self._intermediate_stage) - sig.prov_https_connection.connect(self._intermediate_stage) - sig.prov_download_ca_cert.connect(self._intermediate_stage) - sig.prov_download_provider_info.connect(self._load_provider_config) - sig.prov_check_api_certificate.connect(self._provider_config_loaded) + sig.backend_bad_call.connect(self._backend_bad_call) + + 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) + + 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) + + self._connect_and_track(sig.prov_problem_with_provider, + self._login_problem_provider) + + self._connect_and_track(sig.prov_cancelled_setup, + self._set_login_cancelled) + + # Login signals + self._connect_and_track(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) - # Only used at login, no need to disconnect this like we do - # with the other - sig.prov_problem_with_provider.connect(self._login_problem_provider) + 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_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_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) + + # 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) + + # 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) + + # We don't want to disconnect some signals so don't track them: sig.prov_unsupported_client.connect(self._needs_update) sig.prov_unsupported_api.connect(self._incompatible_api) - sig.prov_cancelled_setup.connect(self._set_login_cancelled) - - def _backend_disconnect(self): - """ - Helper to disconnect from backend signals. + # 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) + + sig.eip_can_start.connect(self._backend_can_start_eip) + sig.eip_cannot_start.connect(self._backend_cannot_start_eip) + + def _disconnect_and_untrack(self): + """ + Helper to disconnect the tracked signals. Some signals are emitted from the wizard, and we want to ignore those. """ - sig = self._backend.signaler - sig.prov_name_resolution.disconnect(self._intermediate_stage) - sig.prov_https_connection.disconnect(self._intermediate_stage) - sig.prov_download_ca_cert.disconnect(self._intermediate_stage) + for signal, method in self._backend_connected_signals.items(): + try: + signal.disconnect(method) + except RuntimeError: + pass # Signal was not connected - sig.prov_download_provider_info.disconnect(self._load_provider_config) - sig.prov_check_api_certificate.disconnect(self._provider_config_loaded) + self._backend_connected_signals = {} + @QtCore.Slot() def _rejected_wizard(self): """ - SLOT - TRIGGERS: self._wizard.rejected + TRIGGERS: + self._wizard.rejected Called if the wizard has been cancelled or closed before finishing. @@ -444,12 +501,12 @@ class MainWindow(QtGui.QMainWindow): if self._wizard_firstrun: self._finish_init() + @QtCore.Slot() def _launch_wizard(self): """ - SLOT TRIGGERS: - self._login_widget.show_wizard - self.ui.action_wizard.triggered + self._login_widget.show_wizard + self.ui.action_wizard.triggered Also called in first run. @@ -457,7 +514,7 @@ class MainWindow(QtGui.QMainWindow): there. """ if self._wizard is None: - self._backend_disconnect() + self._disconnect_and_untrack() self._wizard = Wizard(backend=self._backend, bypass_checks=self._bypass_checks) self._wizard.accepted.connect(self._finish_init) @@ -472,11 +529,11 @@ class MainWindow(QtGui.QMainWindow): self._wizard.finished.connect(self._wizard_finished) self._settings.set_skip_first_run(True) + @QtCore.Slot() def _wizard_finished(self): """ - SLOT - TRIGGERS - self._wizard.finished + TRIGGERS: + self._wizard.finished Called when the wizard has finished. """ @@ -497,11 +554,11 @@ class MainWindow(QtGui.QMainWindow): return h return None + @QtCore.Slot() def _show_logger_window(self): """ - SLOT TRIGGERS: - self.ui.action_show_logs.triggered + self.ui.action_show_logs.triggered Displays the window with the history of messages logged until now and displays the new ones on arrival. @@ -518,9 +575,9 @@ class MainWindow(QtGui.QMainWindow): else: self._logger_window.setVisible(not self._logger_window.isVisible()) + @QtCore.Slot() def _show_AKM(self): """ - SLOT TRIGGERS: self.ui.action_advanced_key_management.triggered @@ -528,30 +585,38 @@ class MainWindow(QtGui.QMainWindow): """ domain = self._login_widget.get_selected_provider() logged_user = "{0}@{1}".format(self._logged_user, domain) - self._akm = AdvancedKeyManagement( - logged_user, self._keymanager, self._soledad) - self._akm.show() + has_mx = True + if self._logged_user is not None: + provider_config = self._get_best_provider_config() + has_mx = provider_config.provides_mx() + + akm = AdvancedKeyManagement( + self, has_mx, logged_user, self._keymanager, self._soledad) + akm.show() + + @QtCore.Slot() def _show_preferences(self): """ - SLOT TRIGGERS: - self.ui.btnPreferences.clicked (disabled for now) - self.ui.action_preferences + self.ui.btnPreferences.clicked (disabled for now) + self.ui.action_preferences Displays the preferences window. """ + user = self._login_widget.get_user() + prov = self._login_widget.get_selected_provider() preferences = PreferencesWindow( - self, self._srp_auth, self._provider_config, self._soledad, - self._login_widget.get_selected_provider()) + self, self._backend, self._provider_config, self._soledad, + user, prov) self.soledad_ready.connect(preferences.set_soledad_ready) preferences.show() preferences.preferences_saved.connect(self._update_eip_enabled_status) + @QtCore.Slot() def _update_eip_enabled_status(self): """ - SLOT TRIGGER: PreferencesWindow.preferences_saved @@ -563,17 +628,43 @@ class MainWindow(QtGui.QMainWindow): """ settings = self._settings default_provider = settings.get_defaultprovider() + + if default_provider is None: + logger.warning("Trying toupdate eip enabled status but there's no" + " default provider. Disabling EIP for the time" + " being...") + self._backend_cannot_start_eip() + return + + self._trying_to_start_eip = settings.get_autostart_eip() + self._backend.eip_can_start(default_provider) + + # 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) + + def _backend_can_start_eip(self): + """ + TRIGGER: + self._backend.signaler.eip_can_start + + If EIP can be started right away, and the client is configured + to do so, start it. Otherwise it leaves everything in place + for the user to click Turn ON. + """ + settings = self._settings + default_provider = settings.get_defaultprovider() enabled_services = [] if default_provider is not None: enabled_services = settings.get_enabled_services(default_provider) eip_enabled = False if EIP_SERVICE in enabled_services: - should_autostart = settings.get_autostart_eip() - if should_autostart and default_provider is not None: + eip_enabled = True + if default_provider is not None: self._eip_status.enable_eip_start() self._eip_status.set_eip_status("") - eip_enabled = True else: # we don't have an usable provider # so the user needs to log in first @@ -583,19 +674,44 @@ class MainWindow(QtGui.QMainWindow): self._eip_status.disable_eip_start() self._eip_status.set_eip_status(self.tr("Disabled")) - return eip_enabled + if eip_enabled and self._trying_to_start_eip: + self._trying_to_start_eip = False + self._try_autostart_eip() + + def _backend_cannot_start_eip(self): + """ + TRIGGER: + self._backend.signaler.eip_cannot_start + If EIP can't be started right away, get the UI to what it + needs to look like and waits for a proper login/eip bootstrap. + """ + settings = self._settings + default_provider = settings.get_defaultprovider() + enabled_services = [] + if default_provider is not None: + enabled_services = settings.get_enabled_services(default_provider) + + if EIP_SERVICE in enabled_services: + # we don't have a usable provider + # so the user needs to log in first + self._eip_status.disable_eip_start() + else: + self._stop_eip() + self._eip_status.disable_eip_start() + self._eip_status.set_eip_status(self.tr("Disabled")) + + @QtCore.Slot() def _show_eip_preferences(self): """ - SLOT TRIGGERS: - self.ui.btnEIPPreferences.clicked - self.ui.action_eip_preferences (disabled for now) + self.ui.btnEIPPreferences.clicked + self.ui.action_eip_preferences (disabled for now) Displays the EIP preferences window. """ domain = self._login_widget.get_selected_provider() - EIPPreferencesWindow(self, domain).show() + EIPPreferencesWindow(self, domain, self._backend).show() # # updates @@ -610,22 +726,27 @@ class MainWindow(QtGui.QMainWindow): """ self.new_updates.emit(req) + @QtCore.Slot(object) def _react_to_new_updates(self, req): """ - SLOT - TRIGGER: self._new_updates_available + TRIGGERS: + self.new_updates Displays the new updates label and sets the updates_content + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest """ self.moveToThread(QtCore.QCoreApplication.instance().thread()) self.ui.lblNewUpdates.setVisible(True) self.ui.btnMore.setVisible(True) self._updates_content = req.content + @QtCore.Slot() def _updates_details(self): """ - SLOT - TRIGGER: self.ui.btnMore.clicked + TRIGGERS: + self.ui.btnMore.clicked Parses and displays the updates details """ @@ -649,11 +770,11 @@ class MainWindow(QtGui.QMainWindow): self.tr("Updates available"), msg) + @QtCore.Slot() def _finish_init(self): """ - SLOT TRIGGERS: - self._wizard.accepted + self._wizard.accepted Also called at the end of the constructor if not first run. @@ -666,11 +787,13 @@ class MainWindow(QtGui.QMainWindow): providers = self._settings.get_configured_providers() self._login_widget.set_providers(providers) self._show_systray() - self.show() - if IS_MAC: - self.raise_() - self._hide_unsupported_services() + if not self._start_hidden: + self.show() + if IS_MAC: + self.raise_() + + self._show_hide_unsupported_services() if self._wizard: possible_username = self._wizard.get_username() @@ -696,7 +819,7 @@ class MainWindow(QtGui.QMainWindow): self._wizard = None self._backend_connect() else: - self._try_autostart_eip() + self._update_eip_enabled_status() domain = self._settings.get_provider() if domain is not None: @@ -712,7 +835,7 @@ class MainWindow(QtGui.QMainWindow): if self._login_widget.load_user_from_keyring(saved_user): self._login() - def _hide_unsupported_services(self): + def _show_hide_unsupported_services(self): """ Given a set of configured providers, it creates a set of available services among all of them and displays the service @@ -733,8 +856,38 @@ class MainWindow(QtGui.QMainWindow): for service in provider_config.get_services(): services.add(service) - self.ui.eipWidget.setVisible(EIP_SERVICE in services) - self.ui.mailWidget.setVisible(MX_SERVICE in services) + self._set_eip_visible(EIP_SERVICE in services) + self._set_mx_visible(MX_SERVICE in services) + + def _set_mx_visible(self, visible): + """ + Change the visibility of MX_SERVICE related UI components. + + :param visible: whether the components should be visible or not. + :type visible: bool + """ + # only update visibility if it is something to change + if self._ui_mx_visible ^ visible: + self.ui.mailWidget.setVisible(visible) + self.ui.lineUnderEmail.setVisible(visible) + self._action_mail_status.setVisible(visible) + self._ui_mx_visible = visible + + def _set_eip_visible(self, visible): + """ + Change the visibility of EIP_SERVICE related UI components. + + :param visible: whether the components should be visible or not. + :type visible: bool + """ + # NOTE: we use xor to avoid the code being run if the visibility hasn't + # changed. This is meant to avoid the eip menu being displayed floating + # around at start because the systray isn't rendered yet. + if self._ui_eip_visible ^ visible: + self.ui.eipWidget.setVisible(visible) + self.ui.lineUnderEIP.setVisible(visible) + self._eip_menu.setVisible(visible) + self._ui_eip_visible = visible def _set_label_offline(self): """ @@ -771,7 +924,7 @@ class MainWindow(QtGui.QMainWindow): systrayMenu.addSeparator() eip_status_label = "{0}: {1}".format(self._eip_name, self.tr("OFF")) - eip_menu = systrayMenu.addMenu(eip_status_label) + 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) systrayMenu.addSeparator() @@ -787,10 +940,21 @@ class MainWindow(QtGui.QMainWindow): self._mail_status.set_systray(self._systray) self._eip_status.set_systray(self._systray) + if self._start_hidden: + hello = lambda: self._systray.showMessage( + self.tr('Hello!'), + self.tr('Bitmask has started in the tray.')) + # we wait for the systray to be ready + reactor.callLater(1, hello) + + @QtCore.Slot(int) def _tray_activated(self, reason=None): """ - SLOT - TRIGGER: self._systray.activated + TRIGGERS: + self._systray.activated + + :param reason: the reason why the tray got activated. + :type reason: int Displays the context menu from the tray icon """ @@ -817,10 +981,11 @@ class MainWindow(QtGui.QMainWindow): visible = self.isVisible() and self.isActiveWindow() self._action_visible.setText(get_action(visible)) + @QtCore.Slot() def _toggle_visible(self): """ - SLOT - TRIGGER: self._action_visible.triggered + TRIGGERS: + self._action_visible.triggered Toggles the window visibility """ @@ -864,10 +1029,11 @@ class MainWindow(QtGui.QMainWindow): if state is not None: self.restoreState(state) + @QtCore.Slot() def _about(self): """ - SLOT - TRIGGERS: self.ui.action_about_leap.triggered + TRIGGERS: + self.ui.action_about_leap.triggered Display the About Bitmask dialog """ @@ -891,10 +1057,11 @@ class MainWindow(QtGui.QMainWindow): "<a href='https://leap.se'>More about LEAP" "</a>") % (VERSION, VERSION_HASH[:10], greet)) + @QtCore.Slot() def _help(self): """ - SLOT - TRIGGERS: self.ui.action_help.triggered + TRIGGERS: + self.ui.action_help.triggered Display the Bitmask help dialog. """ @@ -991,10 +1158,11 @@ class MainWindow(QtGui.QMainWindow): provider = self._login_widget.get_selected_provider() self._backend.setup_provider(provider) + @QtCore.Slot(dict) def _load_provider_config(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_download_provider_info + 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 @@ -1009,8 +1177,9 @@ class MainWindow(QtGui.QMainWindow): self._backend.provider_bootstrap(selected_provider) else: logger.error(data[self._backend.ERROR_KEY]) - self._login_widget.set_enabled(True) + self._login_problem_provider() + @QtCore.Slot() def _login_problem_provider(self): """ Warns the user about a problem with the provider during login. @@ -1019,11 +1188,11 @@ class MainWindow(QtGui.QMainWindow): self.tr("Unable to login: Problem with provider")) self._login_widget.set_enabled(True) + @QtCore.Slot() def _login(self): """ - SLOT TRIGGERS: - self._login_widget.login + self._login_widget.login Starts the login sequence. Which involves bootstrapping the selected provider if the selection is valid (not empty), then @@ -1043,50 +1212,33 @@ class MainWindow(QtGui.QMainWindow): 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() - def _login_errback(self, failure): + @QtCore.Slot(unicode) + def _authentication_error(self, msg): """ - Error handler for the srpauth.authenticate method. + TRIGGERS: + Signaler.srp_auth_error + Signaler.srp_auth_server_error + Signaler.srp_auth_connection_error + Signaler.srp_auth_bad_user_or_password - :param failure: failure object that Twisted generates - :type failure: twisted.python.failure.Failure - """ - # NOTE: this behavior needs to be managed through the signaler, - # as we are doing with the prov_cancelled_setup signal. - # After we move srpauth to the backend, we need to update this. - logger.error("Error logging in, {0!r}".format(failure)) - - if failure.check(CancelledError): - logger.debug("Defer cancelled.") - failure.trap(Exception) - self._set_login_cancelled() - return - elif failure.check(srpauth.SRPAuthBadUserOrPassword): - msg = self.tr("Invalid username or password.") - elif failure.check(srpauth.SRPAuthBadStatusCode, - srpauth.SRPAuthenticationError, - srpauth.SRPAuthVerificationFailed, - srpauth.SRPAuthNoSessionId, - srpauth.SRPAuthNoSalt, srpauth.SRPAuthNoB, - srpauth.SRPAuthBadDataFromServer, - srpauth.SRPAuthJSONDecodeError): - msg = self.tr("There was a server problem with authentication.") - elif failure.check(srpauth.SRPAuthConnectionError): - msg = self.tr("Could not establish a connection.") - else: - # this shouldn't happen, but just in case. - msg = self.tr("Unknown error: {0!r}".format(failure.value)) + Handle the authentication errors. + :param msg: the message to show to the user. + :type msg: unicode + """ self._login_widget.set_status(msg) self._login_widget.set_enabled(True) + self.ui.action_create_new_account.setEnabled(True) + @QtCore.Slot() def _cancel_login(self): """ - SLOT TRIGGERS: - self._login_widget.cancel_login + self._login_widget.cancel_login Stops the login sequence. """ @@ -1097,21 +1249,18 @@ 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() - - if self._login_defer is not None: - logger.debug("Cancelling login defer.") - self._login_defer.cancel() - self._login_defer = None + self._backend.cancel_login() if self._soledad_defer is not None: logger.debug("Cancelling soledad defer.") self._soledad_defer.cancel() self._soledad_defer = None + @QtCore.Slot() def _set_login_cancelled(self): """ - SLOT TRIGGERS: Signaler.prov_cancelled_setup fired by self._backend.cancel_setup_provider() @@ -1122,10 +1271,11 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.set_status(self.tr("Log in cancelled by the user.")) self._login_widget.set_enabled(True) + @QtCore.Slot(dict) def _provider_config_loaded(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_check_api_certificate + TRIGGERS: + self._backend.signaler.prov_check_api_certificate Once the provider configuration is loaded, this starts the SRP authentication @@ -1136,27 +1286,19 @@ class MainWindow(QtGui.QMainWindow): username = self._login_widget.get_user() password = self._login_widget.get_password() - self._hide_unsupported_services() + self._show_hide_unsupported_services() - if self._srp_auth is None: - self._srp_auth = SRPAuth(self._provider_config) - self._srp_auth.authentication_finished.connect( - self._authentication_finished) - self._srp_auth.logout_ok.connect(self._logout_ok) - self._srp_auth.logout_error.connect(self._logout_error) - - self._login_defer = self._srp_auth.authenticate(username, password) - self._login_defer.addErrback(self._login_errback) + domain = self._provider_config.get_domain() + self._backend.login(domain, username, password) else: - self._login_widget.set_status( - "Unable to login: Problem with provider") logger.error(data[self._backend.ERROR_KEY]) - self._login_widget.set_enabled(True) + self._login_problem_provider() + @QtCore.Slot() def _authentication_finished(self): """ - SLOT - TRIGGER: self._srp_auth.authentication_finished + TRIGGERS: + self._srp_auth.authentication_finished Once the user is properly authenticated, try starting the EIP service @@ -1168,8 +1310,8 @@ class MainWindow(QtGui.QMainWindow): domain = self._provider_config.get_domain() full_user_id = make_address(user, domain) self._mail_conductor.userid = full_user_id - self._login_defer = None self._start_eip_bootstrap() + self.ui.action_create_new_account.setEnabled(True) # if soledad/mail is enabled: if MX_SERVICE in self._enabled_services: @@ -1179,6 +1321,9 @@ class MainWindow(QtGui.QMainWindow): self._soledad_bootstrapper.soledad_failed.connect( lambda: btn_enabled(True)) + if not self._get_best_provider_config().provides_mx(): + self._set_mx_visible(False) + def _start_eip_bootstrap(self): """ Changes the stackedWidget index to the EIP status one and @@ -1267,12 +1412,12 @@ class MainWindow(QtGui.QMainWindow): ################################################################### # Service control methods: soledad + @QtCore.Slot(dict) def _soledad_intermediate_stage(self, data): # TODO missing param docstring """ - SLOT TRIGGERS: - self._soledad_bootstrapper.download_config + 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 @@ -1285,29 +1430,13 @@ class MainWindow(QtGui.QMainWindow): # that sets the global status logger.error("Soledad failed to start: %s" % (data[self._soledad_bootstrapper.ERROR_KEY],)) - self._retry_soledad_connection() - - def _retry_soledad_connection(self): - """ - Retries soledad connection. - """ - # XXX should move logic to soledad boostrapper itself - logger.debug("Retrying soledad connection.") - if self._soledad_bootstrapper.should_retry_initialization(): - self._soledad_bootstrapper.increment_retries_count() - # XXX should cancel the existing socket --- this - # is avoiding a clean termination. - self._maybe_run_soledad_setup_checks() - else: - logger.warning("Max number of soledad initialization " - "retries reached.") + @QtCore.Slot(dict) def _soledad_bootstrapped_stage(self, data): """ - SLOT TRIGGERS: - self._soledad_bootstrapper.gen_key - self._soledad_bootstrapper.local_only_ready + 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 @@ -1346,7 +1475,6 @@ class MainWindow(QtGui.QMainWindow): @QtCore.Slot() def _start_smtp_bootstrapping(self): """ - SLOT TRIGGERS: self.soledad_ready """ @@ -1354,20 +1482,15 @@ class MainWindow(QtGui.QMainWindow): logger.debug("not starting smtp in offline mode") return - # TODO for simmetry, this should be called start_smtp_service - # (and delegate all the checks to the conductor) if self._provides_mx_and_enabled(): - self._mail_conductor.smtp_bootstrapper.run_smtp_setup_checks( - self._provider_config, - self._mail_conductor.smtp_config, - download_if_needed=True) + 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): """ - SLOT TRIGGERS: self.logout """ @@ -1380,7 +1503,6 @@ class MainWindow(QtGui.QMainWindow): @QtCore.Slot() def _start_imap_service(self): """ - SLOT TRIGGERS: self.soledad_ready """ @@ -1410,7 +1532,6 @@ class MainWindow(QtGui.QMainWindow): @QtCore.Slot() def _fetch_incoming_mail(self): """ - SLOT TRIGGERS: self.mail_client_logged_in """ @@ -1420,7 +1541,6 @@ class MainWindow(QtGui.QMainWindow): @QtCore.Slot() def _stop_imap_service(self): """ - SLOT TRIGGERS: self.logout """ @@ -1467,11 +1587,11 @@ class MainWindow(QtGui.QMainWindow): self._action_eip_startstop.setEnabled(True) @QtCore.Slot() - def _on_eip_connected(self): + def _on_eip_connection_connected(self): """ - SLOT TRIGGERS: self._eip_status.eip_connection_connected + Emits the EIPConnection.qtsigs.connected_signal This is a little workaround for connecting the vpn-connected @@ -1480,9 +1600,14 @@ class MainWindow(QtGui.QMainWindow): """ self._eip_connection.qtsigs.connected_signal.emit() - # check for connectivity provider_config = self._get_best_provider_config() domain = provider_config.get_domain() + + self._eip_status.set_provider(domain) + self._settings.set_defaultprovider(domain) + self._already_started_eip = True + + # check for connectivity self._check_name_resolution(domain) def _check_name_resolution(self, domain): @@ -1534,137 +1659,112 @@ class MainWindow(QtGui.QMainWindow): Tries to autostart EIP """ settings = self._settings - - if not self._update_eip_enabled_status(): - return - default_provider = settings.get_defaultprovider() self._enabled_services = settings.get_enabled_services( default_provider) loaded = self._provisional_provider_config.load( provider.get_provider_path(default_provider)) - if loaded: + 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() - else: + 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): + def _start_EIP(self): """ - SLOT - TRIGGERS: - self._eip_connection.qtsigs.do_connect_signal - (via state machine) - or called from _finish_eip_bootstrap - Starts EIP """ - provider_config = self._get_best_provider_config() - provider = provider_config.get_domain() 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. + # 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) - loaded = eipconfig.load_eipconfig_if_needed( - provider_config, self._eip_config, provider) + self._backend.start_eip() - if not loaded: - 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() - logger.error("Tried to start EIP but cannot find any " - "available provider!") - return + @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!") - try: - # XXX move this to EIPConductor - host, port = get_openvpn_management() - self._vpn.start(eipconfig=self._eip_config, - providerconfig=provider_config, - socket_host=host, - socket_port=port) - self._settings.set_defaultprovider(provider) - - # XXX move to the state machine too - self._eip_status.set_provider(provider) - - # TODO refactor exceptions so they provide translatable - # usef-facing messages. - except EIPNoPolkitAuthAgentAvailable: - 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() - except EIPNoTunKextLoaded: - 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() - except EIPNoPkexecAvailable: - self._eip_status.set_eip_status( - self.tr("We could not find <b>pkexec</b> " - "in your system."), - error=True) - self._set_eipstatus_off() - except OpenVPNNotFoundException: - self._eip_status.set_eip_status( - self.tr("We could not find openvpn binary."), - error=True) - self._set_eipstatus_off() - except OpenVPNAlreadyRunning as e: - 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() - except AlienOpenVPNAlreadyRunning as e: - 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() - except VPNLauncherException as e: - # 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("%s" % (e,), error=True) - self._set_eipstatus_off() - else: - self._already_started_eip = True + 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): """ - SLOT TRIGGERS: - self._eip_connection.qtsigs.do_disconnect_signal - (via state machine) - or called from _eip_finished + self._eip_connection.qtsigs.do_disconnect_signal (via state machine) Stops vpn process and makes gui adjustments to reflect the change of state. @@ -1673,7 +1773,7 @@ class MainWindow(QtGui.QMainWindow): :type abnormal: bool """ self.user_stopped_eip = True - self._vpn.terminate() + self._backend.stop_eip() self._set_eipstatus_off(False) self._already_started_eip = False @@ -1692,7 +1792,6 @@ class MainWindow(QtGui.QMainWindow): def _on_eip_network_unreachable(self): # XXX Should move to EIP Conductor """ - SLOT TRIGGERS: self._eip_connection.qtsigs.network_unreachable @@ -1706,7 +1805,7 @@ class MainWindow(QtGui.QMainWindow): def _do_eip_restart(self): # XXX Should move to EIP Conductor """ - SLOT + TRIGGERS: self._eip_connection.qtsigs.process_restart Restart the connection. @@ -1714,7 +1813,7 @@ class MainWindow(QtGui.QMainWindow): # for some reason, emitting the do_disconnect/do_connect # signals hangs the UI. self._stop_eip() - QtCore.QTimer.singleShot(2000, self._start_eip) + QtCore.QTimer.singleShot(2000, self._start_EIP) def _set_eipstatus_off(self, error=True): """ @@ -1724,11 +1823,11 @@ class MainWindow(QtGui.QMainWindow): self._eip_status.set_eip_status("", error=error) self._eip_status.set_eip_status_icon("error") + @QtCore.Slot(int) def _eip_finished(self, exitCode): """ - SLOT TRIGGERS: - self._vpn.process_finished + Signaler.eip_process_finished Triggered when the EIP/VPN process finishes to set the UI accordingly. @@ -1764,12 +1863,14 @@ class MainWindow(QtGui.QMainWindow): "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) - self._vpn.killit() 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 @@ -1787,17 +1888,14 @@ class MainWindow(QtGui.QMainWindow): Start the EIP bootstrapping sequence if the client is configured to do so. """ - leap_assert(self._eip_bootstrapper, "We need an eip bootstrapper!") - - provider_config = self._get_best_provider_config() - if self._provides_eip_and_enabled() and not self._already_started_eip: # XXX this should be handled by the state machine. self._eip_status.set_eip_status( self.tr("Starting...")) - self._eip_bootstrapper.run_eip_setup_checks( - provider_config, - download_if_needed=True) + + domain = self._login_widget.get_selected_provider() + self._backend.setup_eip(domain) + self._already_started_eip = True # we want to start soledad anyway after a certain timeout if eip # fails to come up @@ -1816,45 +1914,33 @@ class MainWindow(QtGui.QMainWindow): # eip will not start, so we start soledad anyway self._maybe_run_soledad_setup_checks() + @QtCore.Slot(dict) def _finish_eip_bootstrap(self, data): """ - SLOT - TRIGGER: self._eip_bootstrapper.download_client_certificate + TRIGGERS: + self._backend.signaler.eip_client_certificate_ready Starts the VPN thread if the eip configuration is properly loaded """ - leap_assert(self._eip_config, "We need an eip config!") - passed = data[self._eip_bootstrapper.PASSED_KEY] + passed = data[self._backend.PASSED_KEY] if not passed: error_msg = self.tr("There was a problem with the provider") self._eip_status.set_eip_status(error_msg, error=True) - logger.error(data[self._eip_bootstrapper.ERROR_KEY]) + logger.error(data[self._backend.ERROR_KEY]) self._already_started_eip = False return - provider_config = self._get_best_provider_config() - domain = provider_config.get_domain() - - # XXX move check to _start_eip ? - loaded = eipconfig.load_eipconfig_if_needed( - provider_config, self._eip_config, domain) - - if loaded: - # DO START EIP Connection! - self._eip_connection.qtsigs.do_connect_signal.emit() - else: - 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) + # DO START EIP Connection! + self._eip_connection.qtsigs.do_connect_signal.emit() + @QtCore.Slot(dict) def _eip_intermediate_stage(self, data): # TODO missing param """ - SLOT TRIGGERS: - self._eip_bootstrapper.download_config + self._backend.signaler.eip_config_ready If there was a problem, displays it, otherwise it does nothing. This is used for intermediate bootstrapping stages, in case @@ -1896,12 +1982,11 @@ class MainWindow(QtGui.QMainWindow): @QtCore.Slot() def _logout(self): """ - SLOT - TRIGGER: self._login_widget.logout + TRIGGERS: + self._login_widget.logout Starts the logout sequence """ - self._soledad_bootstrapper.cancel_bootstrap() setProxiedObject(self._soledad, None) self._cancel_ongoing_defers() @@ -1911,13 +1996,14 @@ class MainWindow(QtGui.QMainWindow): # XXX: If other defers are doing authenticated stuff, this # might conflict with those. CHECK! - threads.deferToThread(self._srp_auth.logout) + self._backend.logout() self.logout.emit() + @QtCore.Slot() def _logout_error(self): """ - SLOT - TRIGGER: self._srp_auth.logout_error + TRIGGER: + self._srp_auth.logout_error Inform the user about a logout error. """ @@ -1926,10 +2012,11 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.set_status( self.tr("Something went wrong with the logout.")) + @QtCore.Slot() def _logout_ok(self): """ - SLOT - TRIGGER: self._srp_auth.logout_ok + TRIGGER: + self._srp_auth.logout_ok Switches the stackedWidget back to the login stage after logging out @@ -1941,15 +2028,16 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.logged_out() self._mail_status.mail_state_disabled() + self._show_hide_unsupported_services() + + @QtCore.Slot(dict) def _intermediate_stage(self, data): # TODO this method name is confusing as hell. """ - SLOT TRIGGERS: - self._backend.signaler.prov_name_resolution - self._backend.signaler.prov_https_connection - self._backend.signaler.prov_download_ca_cert - self._eip_bootstrapper.download_config + self._backend.signaler.prov_name_resolution + self._backend.signaler.prov_https_connection + self._backend.signaler.prov_download_ca_cert If there was a problem, displays it, otherwise it does nothing. This is used for intermediate bootstrapping stages, in case @@ -1957,10 +2045,8 @@ class MainWindow(QtGui.QMainWindow): """ passed = data[self._backend.PASSED_KEY] if not passed: - msg = self.tr("Unable to connect: Problem with provider") - self._login_widget.set_status(msg) - self._login_widget.set_enabled(True) logger.error(data[self._backend.ERROR_KEY]) + self._login_problem_provider() # # window handling methods @@ -1974,9 +2060,9 @@ class MainWindow(QtGui.QMainWindow): raise_window_ack() self.raise_window.emit() + @QtCore.Slot() def _do_raise_mainwindow(self): """ - SLOT TRIGGERS: self._on_raise_window_event @@ -2012,11 +2098,8 @@ class MainWindow(QtGui.QMainWindow): self._stop_imap_service() - if self._srp_auth is not None: - if self._srp_auth.get_session_id() is not None or \ - self._srp_auth.get_token() is not None: - # XXX this can timeout after loong time: See #3368 - self._srp_auth.logout() + if self._logged_user is not None: + self._backend.logout() if self._soledad_bootstrapper.soledad is not None: logger.debug("Closing soledad...") @@ -2025,14 +2108,27 @@ class MainWindow(QtGui.QMainWindow): logger.error("No instance of soledad was found.") logger.debug('Terminating vpn') - self._vpn.terminate(shutdown=True) + self._backend.stop_eip(shutdown=True) + + # 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) + def _shutdown(self): + """ + Actually shutdown. + """ self._cancel_ongoing_defers() # TODO missing any more cancels? logger.debug('Cleaning pidfiles') self._cleanup_pidfiles() + if self._quit_callback: + self._quit_callback() + + logger.debug('Bye.') def quit(self): """ @@ -2041,11 +2137,24 @@ class MainWindow(QtGui.QMainWindow): # TODO separate the shutting down of services from the # UI stuff. + # first thing to do quitting, hide the mainwindow and show tooltip. + self.hide() + if self._systray is not None: + self._systray.showMessage( + self.tr('Quitting...'), + self.tr('The app is quitting, please wait.')) + + # explicitly process events to display tooltip immediately + QtCore.QCoreApplication.processEvents() + # Set this in case that the app is hidden QtGui.QApplication.setQuitOnLastWindowClosed(True) - self._backend.stop() self._cleanup_and_quit() + + # 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: @@ -2055,8 +2164,3 @@ class MainWindow(QtGui.QMainWindow): self._logger_window.close() self.close() - - if self._quit_callback: - self._quit_callback() - - logger.debug('Bye.') diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index b2cc2236..2947c5db 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -29,7 +29,6 @@ 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.crypto.srpauth import SRPAuthBadUserOrPassword from leap.bitmask.util.password import basic_password_checks from leap.bitmask.services import get_supported from leap.bitmask.config.providerconfig import ProviderConfig @@ -44,25 +43,33 @@ class PreferencesWindow(QtGui.QDialog): """ preferences_saved = QtCore.Signal() - def __init__(self, parent, srp_auth, provider_config, soledad, domain): + def __init__(self, parent, backend, provider_config, + soledad, username, domain): """ :param parent: parent object of the PreferencesWindow. :parent type: QWidget - :param srp_auth: SRPAuth object configured in the main app. - :type srp_auth: SRPAuth + :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 """ QtGui.QDialog.__init__(self, parent) self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") - self._srp_auth = srp_auth + self._backend = backend self._settings = LeapSettings() self._soledad = soledad + self._provider_config = provider_config + self._username = username + self._domain = domain + + self._backend_connect() # Load UI self.ui = Ui_Preferences() @@ -82,43 +89,60 @@ class PreferencesWindow(QtGui.QDialog): else: self._add_configured_providers() - pw_enabled = False - - # check if the user is logged in - if srp_auth is not None and srp_auth.get_token() is not None: - # check if provider has 'mx' ... - if provider_config.provides_mx(): - enabled_services = self._settings.get_enabled_services(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) - else: - if sameProxiedObjects(self._soledad, None): - msg = self.tr( - "You need to wait until {0} is ready in " - "order to change the password.".format(mx_name)) - self._set_password_change_status(msg) - else: - # Soledad is bootstrapped - pw_enabled = True - else: - pw_enabled = True - else: - msg = self.tr( - "In order to change your password you need to be logged in.") - self._set_password_change_status(msg) + self._backend.get_logged_in_status() 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( + "In order to change your password you need to be logged in.") + self._set_password_change_status(msg) + self.ui.gbPasswordChange.setEnabled(False) + + @QtCore.Slot() def set_soledad_ready(self): """ - SLOT TRIGGERS: parent.soledad_ready @@ -163,15 +187,15 @@ class PreferencesWindow(QtGui.QDialog): self.ui.leNewPassword2.setEnabled(not disable) self.ui.pbChangePassword.setEnabled(not disable) + @QtCore.Slot() def _change_password(self): """ - SLOT TRIGGERS: self.ui.pbChangePassword.clicked Changes the user's password if the inputboxes are correctly filled. """ - username = self._srp_auth.get_username() + username = self._username current_password = self.ui.leCurrentPassword.text() new_password = self.ui.leNewPassword.text() new_password2 = self.ui.leNewPassword2.text() @@ -185,19 +209,17 @@ class PreferencesWindow(QtGui.QDialog): return self._set_changing_password(True) - d = self._srp_auth.change_password(current_password, new_password) - d.addCallback(partial(self._change_password_success, new_password)) - d.addErrback(self._change_password_problem) + self._backend.change_password(current_password, new_password) - def _change_password_success(self, new_password, _): + @QtCore.Slot() + def _change_password_ok(self): """ - Callback used to display a successfully performed action. + TRIGGERS: + self._backend.signaler.srp_password_change_ok - :param new_password: the new password for the user. - :type new_password: str. - :param _: the returned data from self._srp_auth.change_password - Ignored + Callback used to display a successfully changed password. """ + new_password = self.ui.leNewPassword.text() logger.debug("SRP password changed successfully.") try: self._soledad.change_passphrase(new_password) @@ -211,24 +233,21 @@ class PreferencesWindow(QtGui.QDialog): self._clear_password_inputs() self._set_changing_password(False) - def _change_password_problem(self, failure): - """ - Errback called if there was a problem with the deferred. - Also is used to display an error message. - - :param failure: the cause of the method failed. - :type failure: twisted.python.Failure + @QtCore.Slot(unicode) + def _change_password_problem(self, msg): """ - logger.error("Error changing password: %s", (failure, )) - problem = self.tr("There was a problem changing the password.") - - if failure.check(SRPAuthBadUserOrPassword): - problem = self.tr("You did not enter a correct current password.") + TRIGGERS: + self._backend.signaler.srp_password_change_error + self._backend.signaler.srp_password_change_badpw - self._set_password_change_status(problem, error=True) + 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) - failure.trap(Exception) def _clear_password_inputs(self): """ @@ -272,10 +291,12 @@ class PreferencesWindow(QtGui.QDialog): provider_index = self.ui.cbProvidersServices.findText(name) self.ui.cbProvidersServices.setCurrentIndex(provider_index) + @QtCore.Slot(str, int) def _service_selection_changed(self, service, state): """ - SLOT - TRIGGER: service_checkbox.stateChanged + TRIGGERS: + service_checkbox.stateChanged + Adds the service to the state if the state is checked, removes it otherwise @@ -294,9 +315,9 @@ class PreferencesWindow(QtGui.QDialog): # We hide the maybe-visible status label after a change self.ui.lblProvidersServicesStatus.setVisible(False) + @QtCore.Slot(str) def _populate_services(self, domain): """ - SLOT TRIGGERS: self.ui.cbProvidersServices.currentIndexChanged[unicode] @@ -353,9 +374,9 @@ class PreferencesWindow(QtGui.QDialog): logger.error("Something went wrong while trying to " "load service %s" % (service,)) + @QtCore.Slot(str) def _save_enabled_services(self, provider): """ - SLOT TRIGGERS: self.ui.pbSaveServices.clicked @@ -387,3 +408,22 @@ class PreferencesWindow(QtGui.QDialog): 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.srp_password_change_ok.connect(self._change_password_ok) + + pwd_change_error = lambda: self._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( + self.tr("You did not enter a correct current password.")) + sig.srp_password_change_badpw.connect(pwd_change_badpw) diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py index 93731ce0..31938a70 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -562,6 +562,8 @@ class ConnectionMachineBuilder(object): if action: off.assignProperty( action, 'text', off_label) + off.assignProperty( + action, 'enabled', True) off.setObjectName(_OFF) states[_OFF] = off diff --git a/src/leap/bitmask/gui/twisted_main.py b/src/leap/bitmask/gui/twisted_main.py index 1e876c57..dfd69033 100644 --- a/src/leap/bitmask/gui/twisted_main.py +++ b/src/leap/bitmask/gui/twisted_main.py @@ -19,14 +19,23 @@ Main functions for integration of twisted reactor """ import logging -from twisted.internet import error - -# Resist the temptation of putting the import reactor here, -# it will raise an "reactor already imported" error. +from twisted.internet import error, reactor +from PySide import QtCore logger = logging.getLogger(__name__) +def stop(): + QtCore.QCoreApplication.sendPostedEvents() + QtCore.QCoreApplication.flush() + try: + reactor.stop() + logger.debug('Twisted reactor stopped') + except error.ReactorNotRunning: + logger.debug('Twisted reactor not running') + logger.debug("Done stopping all the things.") + + def quit(app): """ Stop the mainloop. @@ -34,9 +43,4 @@ def quit(app): :param app: the main qt QApplication instance. :type app: QtCore.QApplication """ - from twisted.internet import reactor - logger.debug('Stopping twisted reactor') - try: - reactor.callLater(0, reactor.stop) - except error.ReactorNotRunning: - logger.debug('Reactor not running') + reactor.callLater(0, stop) diff --git a/src/leap/bitmask/gui/ui/advanced_key_management.ui b/src/leap/bitmask/gui/ui/advanced_key_management.ui index 1112670f..3b567347 100644 --- a/src/leap/bitmask/gui/ui/advanced_key_management.ui +++ b/src/leap/bitmask/gui/ui/advanced_key_management.ui @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> <class>AdvancedKeyManagement</class> - <widget class="QWidget" name="AdvancedKeyManagement"> + <widget class="QDialog" name="AdvancedKeyManagement"> <property name="geometry"> <rect> <x>0</x> diff --git a/src/leap/bitmask/gui/ui/login.ui b/src/leap/bitmask/gui/ui/login.ui index f5725d5a..216eca9e 100644 --- a/src/leap/bitmask/gui/ui/login.ui +++ b/src/leap/bitmask/gui/ui/login.ui @@ -215,7 +215,7 @@ <number>0</number> </property> <property name="bottomMargin"> - <number>0</number> + <number>12</number> </property> <item row="0" column="0" colspan="2"> <widget class="QLabel" name="lblUser"> diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index e2c1a16e..020a58e2 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -24,6 +24,7 @@ from functools import partial 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 @@ -145,8 +146,7 @@ class Wizard(QtGui.QWizard): @QtCore.Slot() def _wizard_finished(self): """ - SLOT - TRIGGER: + TRIGGERS: self.finished This method is called when the wizard is accepted or rejected. @@ -210,15 +210,19 @@ class Wizard(QtGui.QWizard): def get_services(self): return self._selected_services - @QtCore.Slot() + @QtCore.Slot(unicode) def _enable_check(self, reset=True): """ - SLOT - TRIGGER: + TRIGGERS: self.ui.lnProvider.textChanged Enables/disables the 'check' button in the SELECT_PROVIDER_PAGE depending on the lnProvider content. + + :param reset: this contains the text of the line edit, and when is + called directly defines whether we want to reset the + checks. + :type reset: unicode or bool """ enabled = len(self.ui.lnProvider.text()) != 0 enabled = enabled or self.ui.rbExistingProvider.isChecked() @@ -282,11 +286,11 @@ class Wizard(QtGui.QWizard): # register button self.ui.btnRegister.setVisible(visible) + @QtCore.Slot() def _registration_finished(self): """ - SLOT TRIGGERS: - self._backend.signaler.srp_registration_finished + self._backend.signaler.srp_registration_finished The registration has finished successfully, so we do some final steps. """ @@ -308,11 +312,11 @@ class Wizard(QtGui.QWizard): self.page(self.REGISTER_USER_PAGE).set_completed() self.button(QtGui.QWizard.BackButton).setEnabled(False) + @QtCore.Slot() def _registration_failed(self): """ - SLOT TRIGGERS: - self._backend.signaler.srp_registration_failed + self._backend.signaler.srp_registration_failed The registration has failed, so we report the problem. """ @@ -322,11 +326,11 @@ class Wizard(QtGui.QWizard): self._set_register_status(error_msg, error=True) self.ui.btnRegister.setEnabled(True) + @QtCore.Slot() def _registration_taken(self): """ - SLOT TRIGGERS: - self._backend.signaler.srp_registration_taken + self._backend.signaler.srp_registration_taken The requested username is taken, warn the user about that. """ @@ -358,7 +362,8 @@ class Wizard(QtGui.QWizard): self.ui.lblProviderSelectStatus.setText("") self._domain = None self.button(QtGui.QWizard.NextButton).setEnabled(False) - self.page(self.SELECT_PROVIDER_PAGE).set_completed(False) + self.page(self.SELECT_PROVIDER_PAGE).set_completed( + flags.SKIP_WIZARD_CHECKS) def _reset_provider_setup(self): """ @@ -368,12 +373,12 @@ class Wizard(QtGui.QWizard): self.ui.lblCheckCaFpr.setPixmap(None) self.ui.lblCheckApiCert.setPixmap(None) + @QtCore.Slot() def _check_provider(self): """ - SLOT TRIGGERS: - self.ui.btnCheck.clicked - self.ui.lnProvider.returnPressed + self.ui.btnCheck.clicked + self.ui.lnProvider.returnPressed Starts the checks for a given provider """ @@ -390,17 +395,23 @@ class Wizard(QtGui.QWizard): self.ui.grpCheckProvider.setVisible(True) self.ui.btnCheck.setEnabled(False) - self.ui.lnProvider.setEnabled(False) + + # Disable provider widget + if self.ui.rbNewProvider.isChecked(): + self.ui.lnProvider.setEnabled(False) + else: + self.ui.cbProviders.setEnabled(False) + self.button(QtGui.QWizard.BackButton).clearFocus() self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) self._provider_select_defer = self._backend.\ setup_provider(self._domain) + @QtCore.Slot(bool) def _skip_provider_checks(self, skip): """ - SLOT - Triggered: + TRIGGERS: self.ui.rbExistingProvider.toggled Allows the user to move to the next page without make any checks, @@ -441,10 +452,11 @@ class Wizard(QtGui.QWizard): label.setPixmap(self.ERROR_ICON) logger.error(error) + @QtCore.Slot(dict) def _name_resolution(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_name_resolution + TRIGGERS: + self._backend.signaler.prov_name_resolution Sets the status for the name resolution check """ @@ -460,10 +472,11 @@ class Wizard(QtGui.QWizard): self.ui.btnCheck.setEnabled(not passed) self.ui.lnProvider.setEnabled(not passed) + @QtCore.Slot(dict) def _https_connection(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_https_connection + TRIGGERS: + self._backend.signaler.prov_https_connection Sets the status for the https connection check """ @@ -479,10 +492,11 @@ class Wizard(QtGui.QWizard): self.ui.btnCheck.setEnabled(not passed) self.ui.lnProvider.setEnabled(not passed) + @QtCore.Slot(dict) def _download_provider_info(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_download_provider_info + TRIGGERS: + self._backend.signaler.prov_download_provider_info Sets the status for the provider information download check. Since this check is the last of this set, it also @@ -506,12 +520,18 @@ class Wizard(QtGui.QWizard): "</b></font>") self.ui.lblProviderSelectStatus.setText(status) self.ui.btnCheck.setEnabled(True) - self.ui.lnProvider.setEnabled(True) + # Enable provider widget + if self.ui.rbNewProvider.isChecked(): + self.ui.lnProvider.setEnabled(True) + else: + self.ui.cbProviders.setEnabled(True) + + @QtCore.Slot(dict) def _download_ca_cert(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_download_ca_cert + TRIGGERS: + self._backend.signaler.prov_download_ca_cert Sets the status for the download of the CA certificate check """ @@ -520,10 +540,11 @@ class Wizard(QtGui.QWizard): if passed: self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON) + @QtCore.Slot(dict) def _check_ca_fingerprint(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_check_ca_fingerprint + TRIGGERS: + self._backend.signaler.prov_check_ca_fingerprint Sets the status for the CA fingerprint check """ @@ -532,10 +553,11 @@ class Wizard(QtGui.QWizard): if passed: self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON) + @QtCore.Slot(dict) def _check_api_certificate(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_check_api_certificate + TRIGGERS: + self._backend.signaler.prov_check_api_certificate Sets the status for the API certificate check. Also finishes the provider bootstrapper thread since it's not needed anymore @@ -545,10 +567,12 @@ class Wizard(QtGui.QWizard): True, self.SETUP_PROVIDER_PAGE) self._provider_setup_ok = True + @QtCore.Slot(str, int) def _service_selection_changed(self, service, state): """ - SLOT - TRIGGER: service_checkbox.stateChanged + TRIGGERS: + service_checkbox.stateChanged + Adds the service to the state if the state is checked, removes it otherwise @@ -593,12 +617,16 @@ class Wizard(QtGui.QWizard): self.tr("Something went wrong while trying to " "load service %s" % (service,))) + @QtCore.Slot(int) def _current_id_changed(self, pageId): """ - SLOT - TRIGGER: self.currentIdChanged + TRIGGERS: + self.currentIdChanged Prepares the pages when they appear + + :param pageId: the new current page id. + :type pageId: int """ if pageId == self.SELECT_PROVIDER_PAGE: self._clear_register_widgets() diff --git a/src/leap/bitmask/platform_init/initializers.py b/src/leap/bitmask/platform_init/initializers.py index d93efbc6..f2710c58 100644 --- a/src/leap/bitmask/platform_init/initializers.py +++ b/src/leap/bitmask/platform_init/initializers.py @@ -366,15 +366,8 @@ def _linux_install_missing_scripts(badexec, notfound): fd, tempscript = tempfile.mkstemp(prefix="leap_installer-") polfd, pol_tempfile = tempfile.mkstemp(prefix="leap_installer-") try: - path = launcher.OPENVPN_BIN_PATH - policy_contents = privilege_policies.get_policy_contents(path) - - with os.fdopen(polfd, 'w') as f: - f.write(policy_contents) - pkexec = first(launcher.maybe_pkexec()) - scriptlines = launcher.cmd_for_missing_scripts(installer_path, - pol_tempfile) + scriptlines = launcher.cmd_for_missing_scripts(installer_path) with os.fdopen(fd, 'w') as f: f.write(scriptlines) diff --git a/src/leap/bitmask/provider/providerbootstrapper.py b/src/leap/bitmask/provider/providerbootstrapper.py index 2a519206..6cdfe4f4 100644 --- a/src/leap/bitmask/provider/providerbootstrapper.py +++ b/src/leap/bitmask/provider/providerbootstrapper.py @@ -88,6 +88,8 @@ class ProviderBootstrapper(AbstractBootstrapper): self._domain = None self._provider_config = None self._download_if_needed = False + if signaler is not None: + self._cancel_signal = signaler.PROV_CANCELLED_SETUP @property def verify(self): diff --git a/src/leap/bitmask/services/abstractbootstrapper.py b/src/leap/bitmask/services/abstractbootstrapper.py index fc6bd3e9..77929b75 100644 --- a/src/leap/bitmask/services/abstractbootstrapper.py +++ b/src/leap/bitmask/services/abstractbootstrapper.py @@ -78,6 +78,7 @@ class AbstractBootstrapper(QtCore.QObject): self._signal_to_emit = None self._err_msg = None self._signaler = signaler + self._cancel_signal = None def _gui_errback(self, failure): """ @@ -95,7 +96,8 @@ class AbstractBootstrapper(QtCore.QObject): if failure.check(CancelledError): logger.debug("Defer cancelled.") failure.trap(Exception) - self._signaler.signal(self._signaler.PROV_CANCELLED_SETUP) + if self._signaler is not None and self._cancel_signal is not None: + self._signaler.signal(self._cancel_signal) return if self._signal_to_emit: diff --git a/src/leap/bitmask/services/eip/eipbootstrapper.py b/src/leap/bitmask/services/eip/eipbootstrapper.py index 5a238a1c..c77977ce 100644 --- a/src/leap/bitmask/services/eip/eipbootstrapper.py +++ b/src/leap/bitmask/services/eip/eipbootstrapper.py @@ -20,8 +20,6 @@ EIP bootstrapping import logging import os -from PySide import QtCore - from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.crypto.certs import download_client_cert from leap.bitmask.services import download_service_config @@ -41,17 +39,21 @@ class EIPBootstrapper(AbstractBootstrapper): If a check fails, the subsequent checks are not executed """ - # All dicts returned are of the form - # {"passed": bool, "error": str} - download_config = QtCore.Signal(dict) - download_client_certificate = QtCore.Signal(dict) + def __init__(self, signaler=None): + """ + Constructor for the EIP bootstrapper object - def __init__(self): - AbstractBootstrapper.__init__(self) + :param signaler: Signaler object used to receive notifications + from the backend + :type signaler: Signaler + """ + AbstractBootstrapper.__init__(self, signaler) self._provider_config = None self._eip_config = None self._download_if_needed = False + if signaler is not None: + self._cancel_signal = signaler.EIP_CANCELLED_SETUP def _download_config(self, *args): """ @@ -114,9 +116,9 @@ class EIPBootstrapper(AbstractBootstrapper): self._download_if_needed = download_if_needed cb_chain = [ - (self._download_config, self.download_config), + (self._download_config, self._signaler.EIP_CONFIG_READY), (self._download_client_certificates, - self.download_client_certificate) + self._signaler.EIP_CLIENT_CERTIFICATE_READY) ] return self.addCallbackChain(cb_chain) diff --git a/src/leap/bitmask/services/eip/linuxvpnlauncher.py b/src/leap/bitmask/services/eip/linuxvpnlauncher.py index d24e7ae7..1f0813e0 100644 --- a/src/leap/bitmask/services/eip/linuxvpnlauncher.py +++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py @@ -25,7 +25,6 @@ import sys import time from leap.bitmask.config import flags -from leap.bitmask.util import privilege_policies from leap.bitmask.util.privilege_policies import LinuxPolicyChecker from leap.common.files import which from leap.bitmask.services.eip.vpnlauncher import VPNLauncher @@ -36,6 +35,8 @@ from leap.bitmask.util import first logger = logging.getLogger(__name__) +COM = commands + class EIPNoPolkitAuthAgentAvailable(VPNLauncherException): pass @@ -64,9 +65,10 @@ def _is_auth_agent_running(): """ # 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 [l]xpolkit' + '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"' ] is_running = [commands.getoutput(cmd) for cmd in polkit_options] return any(is_running) @@ -84,35 +86,39 @@ def _try_to_launch_agent(): # will do "sh -c 'foo'", so if we do not quoute it we'll end # up with a invocation to the python interpreter. And that # is bad. + logger.debug("Trying to launch polkit agent") subprocess.call(["python -m leap.bitmask.util.polkit_agent"], shell=True, env=env) except Exception as exc: logger.exception(exc) +SYSTEM_CONFIG = "/etc/leap" +leapfile = lambda f: "%s/%s" % (SYSTEM_CONFIG, f) + + class LinuxVPNLauncher(VPNLauncher): PKEXEC_BIN = 'pkexec' - OPENVPN_BIN = 'openvpn' - OPENVPN_BIN_PATH = os.path.join( - get_path_prefix(), "..", "apps", "eip", OPENVPN_BIN) - - SYSTEM_CONFIG = "/etc/leap" - UP_DOWN_FILE = "resolv-update" - UP_DOWN_PATH = "%s/%s" % (SYSTEM_CONFIG, UP_DOWN_FILE) + BITMASK_ROOT = "/usr/sbin/bitmask-root" # We assume this is there by our openvpn dependency, and # we will put it there on the bundle too. - # TODO adapt to the bundle path. - OPENVPN_DOWN_ROOT_BASE = "/usr/lib/openvpn/" - OPENVPN_DOWN_ROOT_FILE = "openvpn-plugin-down-root.so" - OPENVPN_DOWN_ROOT_PATH = "%s/%s" % ( - OPENVPN_DOWN_ROOT_BASE, - OPENVPN_DOWN_ROOT_FILE) - - UP_SCRIPT = DOWN_SCRIPT = UP_DOWN_PATH - UPDOWN_FILES = (UP_DOWN_PATH,) + if flags.STANDALONE: + OPENVPN_BIN_PATH = "/usr/sbin/leap-openvpn" + else: + OPENVPN_BIN_PATH = "/usr/sbin/openvpn" + POLKIT_PATH = LinuxPolicyChecker.get_polkit_path() - OTHER_FILES = (POLKIT_PATH, ) + + if flags.STANDALONE: + RESOLVCONF_BIN_PATH = "/usr/local/sbin/leap-resolvconf" + else: + # this only will work with debian/ubuntu distros. + RESOLVCONF_BIN_PATH = "/sbin/resolvconf" + + # XXX openvpn binary TOO + OTHER_FILES = (POLKIT_PATH, BITMASK_ROOT, OPENVPN_BIN_PATH, + RESOLVCONF_BIN_PATH) @classmethod def maybe_pkexec(kls): @@ -130,7 +136,7 @@ class LinuxVPNLauncher(VPNLauncher): if _is_pkexec_in_system(): if not _is_auth_agent_running(): _try_to_launch_agent() - time.sleep(0.5) + time.sleep(2) if _is_auth_agent_running(): pkexec_possibilities = which(kls.PKEXEC_BIN) leap_assert(len(pkexec_possibilities) > 0, @@ -145,28 +151,6 @@ class LinuxVPNLauncher(VPNLauncher): raise EIPNoPkexecAvailable() @classmethod - def missing_other_files(kls): - """ - 'Extend' the VPNLauncher's missing_other_files to check if the polkit - files is outdated, in the case of an standalone bundle. - If the polkit file that is in OTHER_FILES exists but is not up to date, - it is added to the missing list. - - :returns: a list of missing files - :rtype: list of str - """ - # we use `super` in order to send the class to use - missing = super(LinuxVPNLauncher, kls).missing_other_files() - - if flags.STANDALONE: - polkit_file = LinuxPolicyChecker.get_polkit_path() - if polkit_file not in missing: - if privilege_policies.is_policy_outdated(kls.OPENVPN_BIN_PATH): - missing.append(polkit_file) - - return missing - - @classmethod def get_vpn_command(kls, eipconfig, providerconfig, socket_host, socket_port="unix", openvpn_verb=1): """ @@ -197,6 +181,10 @@ class LinuxVPNLauncher(VPNLauncher): command = super(LinuxVPNLauncher, kls).get_vpn_command( eipconfig, providerconfig, socket_host, socket_port, openvpn_verb) + command.insert(0, kls.BITMASK_ROOT) + command.insert(1, "openvpn") + command.insert(2, "start") + pkexec = kls.maybe_pkexec() if pkexec: command.insert(0, first(pkexec)) @@ -204,26 +192,44 @@ class LinuxVPNLauncher(VPNLauncher): return command @classmethod - def cmd_for_missing_scripts(kls, frompath, pol_file): + def cmd_for_missing_scripts(kls, frompath): """ Returns a sh script that can copy the missing files. - :param frompath: The path where the up/down scripts live + :param frompath: The path where the helper files live :type frompath: str - :param pol_file: The path where the dynamically generated - policy file lives - :type pol_file: str :rtype: str """ - to = kls.SYSTEM_CONFIG + # no system config for now + # sys_config = kls.SYSTEM_CONFIG + (polkit_file, openvpn_bin_file, + bitmask_root_file, resolvconf_bin_file) = map( + lambda p: os.path.split(p)[-1], + (kls.POLKIT_PATH, kls.OPENVPN_BIN_PATH, + kls.BITMASK_ROOT, kls.RESOLVCONF_BIN_PATH)) cmd = '#!/bin/sh\n' - cmd += 'mkdir -p "%s"\n' % (to, ) - cmd += 'cp "%s/%s" "%s"\n' % (frompath, kls.UP_DOWN_FILE, to) - cmd += 'cp "%s" "%s"\n' % (pol_file, kls.POLKIT_PATH) + cmd += 'mkdir -p /usr/local/sbin\n' + + cmd += 'cp "%s" "%s"\n' % (os.path.join(frompath, polkit_file), + kls.POLKIT_PATH) cmd += 'chmod 644 "%s"\n' % (kls.POLKIT_PATH, ) + cmd += 'cp "%s" "%s"\n' % (os.path.join(frompath, bitmask_root_file), + kls.BITMASK_ROOT) + cmd += 'chmod 744 "%s"\n' % (kls.BITMASK_ROOT, ) + + if flags.STANDALONE: + cmd += 'cp "%s" "%s"\n' % ( + os.path.join(frompath, openvpn_bin_file), + kls.OPENVPN_BIN_PATH) + cmd += 'chmod 744 "%s"\n' % (kls.POLKIT_PATH, ) + + cmd += 'cp "%s" "%s"\n' % ( + os.path.join(frompath, resolvconf_bin_file), + kls.RESOLVCONF_BIN_PATH) + cmd += 'chmod 744 "%s"\n' % (kls.POLKIT_PATH, ) return cmd @classmethod diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py index 99cae7f9..dcb48e8a 100644 --- a/src/leap/bitmask/services/eip/vpnlauncher.py +++ b/src/leap/bitmask/services/eip/vpnlauncher.py @@ -25,14 +25,12 @@ 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 from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector -from leap.bitmask.util import first -from leap.bitmask.util import get_path_prefix from leap.common.check import leap_assert, leap_assert_type -from leap.common.files import which + logger = logging.getLogger(__name__) @@ -107,10 +105,43 @@ class VPNLauncher(object): @classmethod @abstractmethod + def get_gateways(kls, eipconfig, providerconfig): + """ + Return the selected gateways for a given provider, looking at the EIP + config file. + + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :rtype: list + """ + gateways = [] + leap_settings = LeapSettings() + domain = providerconfig.get_domain() + gateway_conf = leap_settings.get_selected_gateway(domain) + + if gateway_conf == leap_settings.GATEWAY_AUTOMATIC: + gateway_selector = VPNGatewaySelector(eipconfig) + gateways = gateway_selector.get_gateways() + else: + gateways = [gateway_conf] + + if not gateways: + logger.error('No gateway was found!') + raise VPNLauncherException('No gateway was found!') + + logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) + return gateways + + @classmethod + @abstractmethod def get_vpn_command(kls, eipconfig, providerconfig, socket_host, socket_port, openvpn_verb=1): """ - Returns the platform dependant vpn launching command + Return the platform-dependant vpn command for launching openvpn. Might raise: OpenVPNNotFoundException, @@ -134,16 +165,19 @@ class VPNLauncher(object): leap_assert_type(eipconfig, EIPConfig) leap_assert_type(providerconfig, ProviderConfig) - kwargs = {} - if flags.STANDALONE: - kwargs['path_extension'] = os.path.join( - get_path_prefix(), "..", "apps", "eip") - - openvpn_possibilities = which(kls.OPENVPN_BIN, **kwargs) - if len(openvpn_possibilities) == 0: + # XXX this still has to be changed on osx and windows accordingly + #kwargs = {} + #openvpn_possibilities = which(kls.OPENVPN_BIN, **kwargs) + #if not openvpn_possibilities: + #raise OpenVPNNotFoundException() + #openvpn = first(openvpn_possibilities) + # ----------------------------------------- + if not os.path.isfile(kls.OPENVPN_BIN_PATH): + logger.warning("Could not find openvpn bin in path %s" % ( + kls.OPENVPN_BIN_PATH)) raise OpenVPNNotFoundException() - openvpn = first(openvpn_possibilities) + openvpn = kls.OPENVPN_BIN_PATH args = [] args += [ @@ -154,22 +188,7 @@ class VPNLauncher(object): if openvpn_verb is not None: args += ['--verb', '%d' % (openvpn_verb,)] - gateways = [] - leap_settings = LeapSettings() - domain = providerconfig.get_domain() - gateway_conf = leap_settings.get_selected_gateway(domain) - - if gateway_conf == leap_settings.GATEWAY_AUTOMATIC: - gateway_selector = VPNGatewaySelector(eipconfig) - gateways = gateway_selector.get_gateways() - else: - gateways = [gateway_conf] - - if not gateways: - logger.error('No gateway was found!') - raise VPNLauncherException('No gateway was found!') - - logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) + gateways = kls.get_gateways(eipconfig, providerconfig) for gw in gateways: args += ['--remote', gw, '1194', 'udp'] @@ -177,11 +196,6 @@ class VPNLauncher(object): args += [ '--client', '--dev', 'tun', - ############################################################## - # persist-tun makes ping-restart fail because it leaves a - # broken routing table - ############################################################## - # '--persist-tun', '--persist-key', '--tls-client', '--remote-cert-tls', @@ -194,15 +208,6 @@ class VPNLauncher(object): user = getpass.getuser() - ############################################################## - # The down-root plugin fails in some situations, so we don't - # drop privs for the time being - ############################################################## - # args += [ - # '--user', user, - # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name - # ] - if socket_port == "unix": # that's always the case for linux args += [ '--management-client-user', user @@ -226,20 +231,6 @@ class VPNLauncher(object): '--down', '\"%s\"' % (kls.DOWN_SCRIPT,) ] - ########################################################### - # For the time being we are disabling the usage of the - # down-root plugin, because it doesn't quite work as - # expected (i.e. it doesn't run route -del as root - # when finishing, so it fails to properly - # restart/quit) - ########################################################### - # if _has_updown_scripts(kls.OPENVPN_DOWN_PLUGIN): - # args += [ - # '--plugin', kls.OPENVPN_DOWN_ROOT, - # '\'%s\'' % kls.DOWN_SCRIPT # for OSX - # '\'script_type=down %s\'' % kls.DOWN_SCRIPT # for Linux - # ] - args += [ '--cert', eipconfig.get_client_cert_path(providerconfig), '--key', eipconfig.get_client_cert_path(providerconfig), @@ -271,13 +262,18 @@ class VPNLauncher(object): :rtype: list """ - leap_assert(kls.UPDOWN_FILES is not None, - "Need to define UPDOWN_FILES for this particular " - "launcher before calling this method") - file_exist = partial(_has_updown_scripts, warn=False) - zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) - missing = filter(lambda (path, exists): exists is False, zipped) - return [path for path, exists in missing] + # FIXME + # XXX remove method when we ditch UPDOWN in osx and win too + if IS_LINUX: + return [] + else: + leap_assert(kls.UPDOWN_FILES is not None, + "Need to define UPDOWN_FILES for this particular " + "launcher before calling this method") + file_exist = partial(_has_updown_scripts, warn=False) + zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) + missing = filter(lambda (path, exists): exists is False, zipped) + return [path for path, exists in missing] @classmethod def missing_other_files(kls): diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index 5c100036..1559ea8b 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -21,6 +21,7 @@ import logging import os import shutil import socket +import subprocess import sys from itertools import chain, repeat @@ -33,14 +34,14 @@ except ImportError: # psutil >= 2.0.0 from psutil import AccessDenied as psutil_AccessDenied -from PySide import QtCore - +from leap.bitmask.config import flags from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.services.eip import get_vpn_launcher +from leap.bitmask.services.eip import linuxvpnlauncher from leap.bitmask.services.eip.eipconfig import EIPConfig from leap.bitmask.services.eip.udstelnet import UDSTelnet from leap.bitmask.util import first -from leap.bitmask.platform_init import IS_MAC +from leap.bitmask.platform_init import IS_MAC, IS_LINUX from leap.common.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) @@ -52,28 +53,6 @@ from twisted.internet import error as internet_error from twisted.internet.task import LoopingCall -class VPNSignals(QtCore.QObject): - """ - These are the signals that we use to let the UI know - about the events we are polling. - They are instantiated in the VPN object and passed along - till the VPNProcess. - """ - # signals for the process - state_changed = QtCore.Signal(dict) - status_changed = QtCore.Signal(dict) - process_finished = QtCore.Signal(int) - - # signals that come from parsing - # openvpn output - network_unreachable = QtCore.Signal() - process_restart_tls = QtCore.Signal() - process_restart_ping = QtCore.Signal() - - def __init__(self): - QtCore.QObject.__init__(self) - - class VPNObserver(object): """ A class containing different patterns in the openvpn output that @@ -90,19 +69,13 @@ class VPNObserver(object): 'PROCESS_RESTART_TLS': ( "SIGUSR1[soft,tls-error]",), 'PROCESS_RESTART_PING': ( - "SIGUSR1[soft,ping-restart]",), + "SIGTERM[soft,ping-restart]",), 'INITIALIZATION_COMPLETED': ( "Initialization Sequence Completed",), } - def __init__(self, qtsigs): - """ - Initializer. Keeps a reference to the passed qtsigs object - :param qtsigs: an object containing the different qt signals to - be used to communicate with different parts of - the application (the EIP state machine, for instance). - """ - self._qtsigs = qtsigs + def __init__(self, signaler=None): + self._signaler = signaler def watch(self, line): """ @@ -123,27 +96,29 @@ class VPNObserver(object): return sig = self._get_signal(event) - if sig: - sig.emit() + if sig is not None: + self._signaler.signal(sig) return else: - logger.debug( - 'We got %s event from openvpn output but we ' - 'could not find a matching signal for it.' - % event) + logger.debug('We got %s event from openvpn output but we could ' + 'not find a matching signal for it.' % event) def _get_signal(self, event): """ Tries to get the matching signal from the eip signals objects based on the name of the passed event (in lowercase) - :param event: the name of the event that we want to get a signal - for + :param event: the name of the event that we want to get a signal for :type event: str - :returns: a QtSignal, or None - :rtype: QtSignal or None + :returns: a Signaler signal or None + :rtype: str or None """ - return getattr(self._qtsigs, event.lower(), None) + 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, + } + return signals.get(event.lower()) class OpenVPNAlreadyRunning(Exception): @@ -181,14 +156,11 @@ class VPN(object): self._vpnproc = None self._pollers = [] self._reactor = reactor - self._qtsigs = VPNSignals() - # XXX should get it from config.flags - self._openvpn_verb = kwargs.get(self.OPENVPN_VERB, None) + self._signaler = kwargs['signaler'] + self._openvpn_verb = flags.OPENVPN_VERBOSITY - @property - def qtsigs(self): - return self._qtsigs + self._user_stopped = False def start(self, *args, **kwargs): """ @@ -200,19 +172,28 @@ class VPN(object): :param kwargs: kwargs to be passed to the VPNProcess :type kwargs: dict """ + logger.debug('VPN: start') + self._user_stopped = False self._stop_pollers() - kwargs['qtsigs'] = self.qtsigs kwargs['openvpn_verb'] = self._openvpn_verb + kwargs['signaler'] = self._signaler # start the main vpn subprocess vpnproc = VPNProcess(*args, **kwargs) - #qtsigs=self.qtsigs, - #openvpn_verb=self._openvpn_verb) if vpnproc.get_openvpn_process(): logger.info("Another vpn process is running. Will try to stop it.") vpnproc.stop_if_already_running() + # we try to bring the firewall up + if IS_LINUX: + gateways = vpnproc.getGateways() + firewall_up = self._launch_firewall(gateways) + if not firewall_up: + logger.error("Could not bring firewall up, " + "aborting openvpn launch.") + return + cmd = vpnproc.getCommand() env = os.environ for key, val in vpnproc.vpn_env.items(): @@ -230,9 +211,37 @@ class VPN(object): self._pollers.extend(poll_list) self._start_pollers() + def _launch_firewall(self, gateways): + """ + Launch the firewall using the privileged wrapper. + + :param gateways: + :type gateways: list + + :returns: True if the exitcode of calling the root helper in a + subprocess is 0. + :rtype: bool + """ + # XXX could check for wrapper existence, check it's root owned etc. + # 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) + return True if exitCode is 0 else False + + def _tear_down_firewall(self): + """ + Tear the firewall down using the privileged wrapper. + """ + BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT + exitCode = subprocess.call(["pkexec", + BM_ROOT, "firewall", "stop"]) + return True if exitCode is 0 else False + def _kill_if_left_alive(self, tries=0): """ - Check if the process is still alive, and sends a + Check if the process is still alive, and send a SIGKILL after a timeout period. :param tries: counter of tries, used in recursion @@ -242,6 +251,15 @@ class VPN(object): while tries < self.TERMINATE_MAXTRIES: if self._vpnproc.transport.pid is None: logger.debug("Process has been happily terminated.") + + # we try to tear the 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") + return else: logger.debug("Process did not die, waiting...") @@ -262,8 +280,11 @@ class VPN(object): Sends a kill signal to the process. """ self._stop_pollers() - self._vpnproc.aborted = True - self._vpnproc.killProcess() + if self._vpnproc is None: + logger.debug("There's no vpn process running to kill.") + else: + self._vpnproc.aborted = True + self._vpnproc.killProcess() def terminate(self, shutdown=False): """ @@ -275,6 +296,10 @@ class VPN(object): 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: self._sentterm = True @@ -282,12 +307,17 @@ class VPN(object): # ...but we also trigger a countdown to be unpolite # if strictly needed. - - # XXX Watch out! This will fail NOW since we are running - # openvpn as root as a workaround for some connection issues. 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") + def _start_pollers(self): """ Iterate through the registered observers @@ -328,37 +358,21 @@ class VPNManager(object): POLL_TIME = 2.5 if IS_MAC else 1.0 CONNECTION_RETRY_TIME = 1 - TS_KEY = "ts" - STATUS_STEP_KEY = "status_step" - OK_KEY = "ok" - IP_KEY = "ip" - REMOTE_KEY = "remote" - - TUNTAP_READ_KEY = "tun_tap_read" - TUNTAP_WRITE_KEY = "tun_tap_write" - TCPUDP_READ_KEY = "tcp_udp_read" - TCPUDP_WRITE_KEY = "tcp_udp_write" - AUTH_READ_KEY = "auth_read" - - def __init__(self, qtsigs=None): + def __init__(self, signaler=None): """ Initializes the VPNManager. - :param qtsigs: a QObject containing the Qt signals used by the UI - to give feedback about state changes. - :type qtsigs: QObject + :param signaler: Signaler object used to send notifications to the + backend + :type signaler: backend.Signaler """ from twisted.internet import reactor self._reactor = reactor self._tn = None - self._qtsigs = qtsigs + self._signaler = signaler self._aborted = False @property - def qtsigs(self): - return self._qtsigs - - @property def aborted(self): return self._aborted @@ -552,17 +566,10 @@ class VPNManager(object): continue ts, status_step, ok, ip, remote = parts - state_dict = { - self.TS_KEY: ts, - self.STATUS_STEP_KEY: status_step, - self.OK_KEY: ok, - self.IP_KEY: ip, - self.REMOTE_KEY: remote - } - - if state_dict != self._last_state: - self.qtsigs.state_changed.emit(state_dict) - self._last_state = state_dict + state = status_step + if state != self._last_state: + self._signaler.signal(self._signaler.EIP_STATE_CHANGED, state) + self._last_state = state def _parse_status_and_notify(self, output): """ @@ -575,9 +582,7 @@ class VPNManager(object): """ tun_tap_read = "" tun_tap_write = "" - tcp_udp_read = "" - tcp_udp_write = "" - auth_read = "" + for line in output: stripped = line.strip() if stripped.endswith("STATISTICS") or stripped == "END": @@ -585,28 +590,24 @@ class VPNManager(object): parts = stripped.split(",") if len(parts) < 2: continue - if parts[0].strip() == "TUN/TAP read bytes": - tun_tap_read = parts[1] - elif parts[0].strip() == "TUN/TAP write bytes": - tun_tap_write = parts[1] - elif parts[0].strip() == "TCP/UDP read bytes": - tcp_udp_read = parts[1] - elif parts[0].strip() == "TCP/UDP write bytes": - tcp_udp_write = parts[1] - elif parts[0].strip() == "Auth read bytes": - auth_read = parts[1] - - status_dict = { - self.TUNTAP_READ_KEY: tun_tap_read, - self.TUNTAP_WRITE_KEY: tun_tap_write, - self.TCPUDP_READ_KEY: tcp_udp_read, - self.TCPUDP_WRITE_KEY: tcp_udp_write, - self.AUTH_READ_KEY: auth_read - } - if status_dict != self._last_status: - self.qtsigs.status_changed.emit(status_dict) - self._last_status = status_dict + text, value = parts + # text can be: + # "TUN/TAP read bytes" + # "TUN/TAP write bytes" + # "TCP/UDP read bytes" + # "TCP/UDP write bytes" + # "Auth read bytes" + + if text == "TUN/TAP read bytes": + tun_tap_read = value # download + elif text == "TUN/TAP write bytes": + tun_tap_write = value # upload + + status = (tun_tap_read, tun_tap_write) + if status != self._last_status: + self._signaler.signal(self._signaler.EIP_STATUS_CHANGED, status) + self._last_status = status def get_state(self): """ @@ -754,7 +755,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): """ def __init__(self, eipconfig, providerconfig, socket_host, socket_port, - qtsigs, openvpn_verb): + signaler, openvpn_verb): """ :param eipconfig: eip configuration object :type eipconfig: EIPConfig @@ -769,18 +770,17 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): socket, or port otherwise :type socket_port: str - :param qtsigs: a QObject containing the Qt signals used to notify the - UI. - :type qtsigs: QObject + :param signaler: Signaler object used to receive notifications to the + backend + :type signaler: backend.Signaler :param openvpn_verb: the desired level of verbosity in the openvpn invocation :type openvpn_verb: int """ - VPNManager.__init__(self, qtsigs=qtsigs) + VPNManager.__init__(self, signaler=signaler) leap_assert_type(eipconfig, EIPConfig) leap_assert_type(providerconfig, ProviderConfig) - leap_assert_type(qtsigs, QtCore.QObject) #leap_assert(not self.isRunning(), "Starting process more than once!") @@ -799,7 +799,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): # the parameter around. self._openvpn_verb = openvpn_verb - self._vpn_observer = VPNObserver(qtsigs) + self._vpn_observer = VPNObserver(signaler) # processProtocol methods @@ -835,7 +835,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): exit_code = reason.value.exitCode if isinstance(exit_code, int): logger.debug("processExited, status %d" % (exit_code,)) - self.qtsigs.process_finished.emit(exit_code) + self._signaler.signal(self._signaler.EIP_PROCESS_FINISHED, exit_code) self._alive = False def processEnded(self, reason): @@ -889,9 +889,20 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): if not isinstance(c, str): command[i] = c.encode(encoding) - logger.debug("Running VPN with command: {0}".format(command)) + logger.debug("Running VPN with command: ") + logger.debug("{0}".format(" ".join(command))) return command + def getGateways(self): + """ + Get the gateways from the appropiate launcher. + + :rtype: list + """ + gateways = self._launcher.get_gateways( + self._eipconfig, self._providerconfig) + return gateways + # shutdown def killProcess(self): diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py index 79f324dc..1766a39d 100644 --- a/src/leap/bitmask/services/mail/conductor.py +++ b/src/leap/bitmask/services/mail/conductor.py @@ -18,22 +18,18 @@ Mail Services Conductor """ import logging -import os -from PySide import QtCore from zope.proxy import sameProxiedObjects from leap.bitmask.gui import statemachines -from leap.bitmask.services.mail import imap 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.bitmask.util import is_file from leap.common.check import leap_assert - -from leap.common.events import register as leap_register from leap.common.events import events_pb2 as leap_events +from leap.common.events import register as leap_register logger = logging.getLogger(__name__) @@ -167,10 +163,6 @@ class IMAPControl(object): class SMTPControl(object): - - PORT_KEY = "port" - IP_KEY = "ip_address" - def __init__(self): """ Initializes smtp variables. @@ -178,12 +170,8 @@ class SMTPControl(object): self.smtp_config = SMTPConfig() self.smtp_connection = None self.smtp_machine = None - self._smtp_service = None - self._smtp_port = None self.smtp_bootstrapper = SMTPBootstrapper() - self.smtp_bootstrapper.download_config.connect( - self.smtp_bootstrapped_stage) leap_register(signal=leap_events.SMTP_SERVICE_STARTED, callback=self._handle_smtp_events, @@ -200,101 +188,27 @@ class SMTPControl(object): """ self.smtp_connection = smtp_connection - def start_smtp_service(self, host, port, cert): + def start_smtp_service(self, provider_config, download_if_needed=False): """ - Starts the smtp service. + Starts the SMTP service. - :param host: the hostname of the remove SMTP server. - :type host: str - :param port: the port of the remote SMTP server - :type port: str - :param cert: the client certificate for authentication - :type cert: str + :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 """ - # TODO Make the encrypted_only configurable - # TODO pick local smtp port in a better way - # TODO remove hard-coded port and let leap.mail set - # the specific default. self.smtp_connection.qtsigs.connecting_signal.emit() - from leap.mail.smtp import setup_smtp_gateway - self._smtp_service, self._smtp_port = setup_smtp_gateway( - port=2013, - userid=self.userid, - keymanager=self._keymanager, - smtp_host=host, - smtp_port=port, - smtp_cert=cert, - smtp_key=cert, - encrypted_only=False) + self.smtp_bootstrapper.start_smtp_service( + provider_config, self.smtp_config, self._keymanager, + self.userid, download_if_needed) def stop_smtp_service(self): """ - Stops the smtp service (port and factory). + Stops the SMTP service. """ self.smtp_connection.qtsigs.disconnecting_signal.emit() - # TODO We should homogenize both services. - if self._smtp_service is not None: - logger.debug('Stopping smtp service.') - self._smtp_port.stopListening() - self._smtp_service.doStop() - - @QtCore.Slot() - def smtp_bootstrapped_stage(self, data): - """ - SLOT - TRIGGERS: - self.smtp_bootstrapper.download_config - - If there was a problem, displays it, otherwise it does nothing. - This is used for intermediate bootstrapping stages, in case - they fail. - - :param data: result from the bootstrapping stage for Soledad - :type data: dict - """ - passed = data[self.smtp_bootstrapper.PASSED_KEY] - if not passed: - logger.error(data[self.smtp_bootstrapper.ERROR_KEY]) - return - logger.debug("Done bootstrapping SMTP") - self.check_smtp_config() - - def check_smtp_config(self): - """ - Checks smtp config and tries to download smtp client cert if needed. - Currently called when smtp_bootstrapped_stage has successfuly finished. - """ - logger.debug("Checking SMTP config...") - leap_assert(self.smtp_bootstrapper._provider_config, - "smtp bootstrapper does not have a provider_config") - - provider_config = self.smtp_bootstrapper._provider_config - smtp_config = self.smtp_config - hosts = smtp_config.get_hosts() - # TODO handle more than one host and define how to choose - if len(hosts) > 0: - hostname = hosts.keys()[0] - logger.debug("Using hostname %s for SMTP" % (hostname,)) - host = hosts[hostname][self.IP_KEY].encode("utf-8") - port = hosts[hostname][self.PORT_KEY] - - client_cert = smtp_config.get_client_cert_path( - provider_config, - about_to_download=True) - - # XXX change this logic! - # check_config should be called from within start_service, - # and not the other way around. - if not is_file(client_cert): - self.smtp_bootstrapper._download_client_certificates() - if os.path.isfile(client_cert): - self.start_smtp_service(host, port, client_cert) - else: - logger.warning("Tried to download email client " - "certificate, but could not find any") - - else: - logger.warning("No smtp hosts configured") + self.smtp_bootstrapper.stop_smtp_service() # handle smtp events @@ -349,7 +263,7 @@ class MailConductor(IMAPControl, SMTPControl): :param keymanager: a transparent proxy that eventually will point to a Keymanager Instance. - :type soledad: zope.proxy.ProxyBase + :type keymanager: zope.proxy.ProxyBase """ IMAPControl.__init__(self) SMTPControl.__init__(self) @@ -407,4 +321,5 @@ class MailConductor(IMAPControl, SMTPControl): qtsigs.connecting_signal.connect(widget.mail_state_connecting) qtsigs.disconnecting_signal.connect(widget.mail_state_disconnecting) qtsigs.disconnected_signal.connect(widget.mail_state_disconnected) - qtsigs.soledad_invalid_auth_token.connect(widget.soledad_invalid_auth_token) + qtsigs.soledad_invalid_auth_token.connect( + widget.soledad_invalid_auth_token) diff --git a/src/leap/bitmask/services/mail/smtpbootstrapper.py b/src/leap/bitmask/services/mail/smtpbootstrapper.py index 032d6357..7ecf8134 100644 --- a/src/leap/bitmask/services/mail/smtpbootstrapper.py +++ b/src/leap/bitmask/services/mail/smtpbootstrapper.py @@ -20,12 +20,13 @@ SMTP bootstrapping import logging import os -from PySide import QtCore - from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.crypto.certs import download_client_cert from leap.bitmask.services import download_service_config from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper +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.files import check_and_fix_urw_only @@ -33,27 +34,33 @@ from leap.common.files import check_and_fix_urw_only logger = logging.getLogger(__name__) +class NoSMTPHosts(Exception): + """This is raised when there is no SMTP host to use.""" + + class SMTPBootstrapper(AbstractBootstrapper): """ SMTP init procedure """ - # All dicts returned are of the form - # {"passed": bool, "error": str} - download_config = QtCore.Signal(dict) + PORT_KEY = "port" + IP_KEY = "ip_address" def __init__(self): AbstractBootstrapper.__init__(self) self._provider_config = None self._smtp_config = None + self._userid = None self._download_if_needed = False - def _download_config(self, *args): + self._smtp_service = None + self._smtp_port = None + + def _download_config_and_cert(self): """ - Downloads the SMTP config for the given provider + Downloads the SMTP config and cert for the given provider. """ - leap_assert(self._provider_config, "We need a provider configuration!") @@ -66,63 +73,101 @@ class SMTPBootstrapper(AbstractBootstrapper): self._session, self._download_if_needed) - def _download_client_certificates(self, *args): - """ - Downloads the SMTP client certificate for the given provider + hosts = self._smtp_config.get_hosts() - We actually are downloading the certificate for the same uri as - for the EIP config, but we duplicate these bits to allow mail - service to be working in a provider that does not offer EIP. - """ - # TODO factor out with eipboostrapper.download_client_certificates - # TODO this shouldn't be a private method, it's called from - # mainwindow. - leap_assert(self._provider_config, "We need a provider configuration!") - leap_assert(self._smtp_config, "We need an smtp configuration!") + if len(hosts) == 0: + raise NoSMTPHosts() - logger.debug("Downloading SMTP client certificate for %s" % - (self._provider_config.get_domain(),)) + # TODO handle more than one host and define how to choose + hostname = hosts.keys()[0] + logger.debug("Using hostname %s for SMTP" % (hostname,)) - client_cert_path = self._smtp_config.\ - get_client_cert_path(self._provider_config, - about_to_download=True) + client_cert_path = self._smtp_config.get_client_cert_path( + self._provider_config, about_to_download=True) - # For re-download if something is wrong with the cert - self._download_if_needed = self._download_if_needed and \ - not leap_certs.should_redownload(client_cert_path) + if not is_file(client_cert_path): + # For re-download if something is wrong with the cert + self._download_if_needed = ( + self._download_if_needed and + not leap_certs.should_redownload(client_cert_path)) - if self._download_if_needed and \ - os.path.isfile(client_cert_path): - check_and_fix_urw_only(client_cert_path) - return + if self._download_if_needed and os.path.isfile(client_cert_path): + check_and_fix_urw_only(client_cert_path) + return - download_client_cert(self._provider_config, - client_cert_path, - self._session) + download_client_cert(self._provider_config, + client_cert_path, + self._session) - def run_smtp_setup_checks(self, - provider_config, - smtp_config, - download_if_needed=False): + def _start_smtp_service(self): + """ + Start the smtp service using the downloaded configurations. + """ + # TODO Make the encrypted_only configurable + # TODO pick local smtp port in a better way + # TODO remove hard-coded port and let leap.mail set + # the specific default. + # TODO handle more than one host and define how to choose + hosts = self._smtp_config.get_hosts() + hostname = hosts.keys()[0] + host = hosts[hostname][self.IP_KEY].encode("utf-8") + port = hosts[hostname][self.PORT_KEY] + client_cert_path = self._smtp_config.get_client_cert_path( + self._provider_config, about_to_download=True) + + from leap.mail.smtp import setup_smtp_gateway + self._smtp_service, self._smtp_port = setup_smtp_gateway( + port=2013, + userid=self._userid, + keymanager=self._keymanager, + smtp_host=host, + smtp_port=port, + smtp_cert=client_cert_path, + smtp_key=client_cert_path, + encrypted_only=False) + + def start_smtp_service(self, provider_config, smtp_config, keymanager, + userid, download_if_needed=False): """ - Starts the checks needed for a new smtp setup + 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 + :param userid: the user id, in the form "user@provider" + :type userid: str :param download_if_needed: True if it should check for mtime for the file :type download_if_needed: bool """ leap_assert_type(provider_config, ProviderConfig) + leap_assert_type(smtp_config, SMTPConfig) self._provider_config = provider_config + self._keymanager = keymanager self._smtp_config = smtp_config + self._useid = userid self._download_if_needed = download_if_needed - cb_chain = [ - (self._download_config, self.download_config), - ] - - self.addCallbackChain(cb_chain) + try: + self._download_config_and_cert() + logger.debug("Starting SMTP service.") + self._start_smtp_service() + except NoSMTPHosts: + logger.warning("There is no SMTP host to use.") + except Exception as e: + # TODO: we should handle more specific exceptions in here + logger.exception("Error while bootstrapping SMTP: %r" % (e, )) + + def stop_smtp_service(self): + """ + Stops the smtp service (port and factory). + """ + if self._smtp_service is not None: + logger.debug('Stopping SMTP service.') + self._smtp_port.stopListening() + self._smtp_service.doStop() diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index ad5ee4d0..6bb7c036 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -139,7 +139,6 @@ class SoledadBootstrapper(AbstractBootstrapper): download_config = QtCore.Signal(dict) gen_key = QtCore.Signal(dict) local_only_ready = QtCore.Signal(dict) - soledad_timeout = QtCore.Signal() soledad_invalid_auth_token = QtCore.Signal() soledad_failed = QtCore.Signal() @@ -159,8 +158,6 @@ class SoledadBootstrapper(AbstractBootstrapper): self._srpauth = None self._soledad = None - self._soledad_retries = 0 - @property def keymanager(self): return self._keymanager @@ -177,26 +174,6 @@ class SoledadBootstrapper(AbstractBootstrapper): "We need a provider config") return SRPAuth(self._provider_config) - # retries - - def cancel_bootstrap(self): - self._soledad_retries = self.MAX_INIT_RETRIES - - def should_retry_initialization(self): - """ - Return True if we should retry the initialization. - """ - logger.debug("current retries: %s, max retries: %s" % ( - self._soledad_retries, - self.MAX_INIT_RETRIES)) - return self._soledad_retries < self.MAX_INIT_RETRIES - - def increment_retries_count(self): - """ - Increment the count of initialization retries. - """ - self._soledad_retries += 1 - # initialization def load_offline_soledad(self, username, password, uuid): @@ -265,6 +242,41 @@ class SoledadBootstrapper(AbstractBootstrapper): # in the case of an invalid token we have already turned off mail and # warned the user in _do_soledad_sync() + 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 + can't succeed. + + :param uuid: user identifier + :type uuid: str + :param secrets_path: path to secrets file + :type secrets_path: str + :param local_db_path: path to local db file + :type local_db_path: str + :param server_url: soledad server uri + :type server_url: str + :param cert_file: path to the certificate of the ca used + to validate the SSL certificate used by the remote + soledad server. + :type cert_file: str + :param auth token: auth token + :type auth_token: str + """ + init_tries = self.MAX_INIT_RETRIES + while init_tries > 0: + try: + 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 + continue + + self.soledad_failed.emit() + raise SoledadInitError() def load_and_sync_soledad(self, uuid=None, offline=False): """ @@ -283,10 +295,9 @@ class SoledadBootstrapper(AbstractBootstrapper): server_url, cert_file = remote_param try: - self._try_soledad_init( - uuid, secrets_path, local_db_path, - server_url, cert_file, token) - except Exception: + self._do_soledad_init(uuid, secrets_path, local_db_path, + server_url, cert_file, token) + except SoledadInitError: # re-raise the exceptions from try_init, # we're currently handling the retries from the # soledad-launcher in the gui. @@ -378,9 +389,13 @@ class SoledadBootstrapper(AbstractBootstrapper): Try to initialize soledad. :param uuid: user identifier + :type uuid: str :param secrets_path: path to secrets file + :type secrets_path: str :param local_db_path: path to local db file + :type local_db_path: str :param server_url: soledad server uri + :type server_url: str :param cert_file: path to the certificate of the ca used to validate the SSL certificate used by the remote soledad server. @@ -409,34 +424,17 @@ class SoledadBootstrapper(AbstractBootstrapper): # and return a subclass of SoledadInitializationFailed # recoverable, will guarantee retries - except socket.timeout: - logger.debug("SOLEDAD initialization TIMED OUT...") - self.soledad_timeout.emit() - raise - except socket.error as exc: - logger.warning("Socket error while initializing soledad") - self.soledad_timeout.emit() - raise - except BootstrapSequenceError as exc: - logger.warning("Error while initializing soledad") - self.soledad_timeout.emit() + except (socket.timeout, socket.error, BootstrapSequenceError): + logger.warning("Error while initializing Soledad") raise # unrecoverable - except u1db_errors.Unauthorized: - logger.error("Error while initializing soledad " - "(unauthorized).") - self.soledad_failed.emit() - raise - except u1db_errors.HTTPError as exc: - logger.exception("Error while initializing soledad " - "(HTTPError)") - self.soledad_failed.emit() + except (u1db_errors.Unauthorized, u1db_errors.HTTPError): + logger.error("Error while initializing Soledad (u1db error).") raise except Exception as exc: logger.exception("Unhandled error while initializating " - "soledad: %r" % (exc,)) - self.soledad_failed.emit() + "Soledad: %r" % (exc,)) raise def _try_soledad_sync(self): diff --git a/src/leap/bitmask/util/leap_argparse.py b/src/leap/bitmask/util/leap_argparse.py index 88267ff8..84af4e8d 100644 --- a/src/leap/bitmask/util/leap_argparse.py +++ b/src/leap/bitmask/util/leap_argparse.py @@ -59,6 +59,13 @@ def build_parser(): action="store_false", dest="api_version_check", help='Skip the api version compatibility check with ' 'the provider.') + parser.add_argument('-H', '--start-hidden', default=False, + action="store_true", dest="start_hidden", + help='Starts the application just in the taskbar.') + parser.add_argument('-S', '--skip-wizard-checks', default=False, + action="store_true", dest="skip_wizard_checks", + help='Skips the provider checks in the wizard (use ' + 'for testing purposes only).') # openvpn options parser.add_argument('--openvpn-verbosity', nargs='?', diff --git a/src/leap/bitmask/util/pastebin.py b/src/leap/bitmask/util/pastebin.py index 21b8a0b7..a3bdba02 100755 --- a/src/leap/bitmask/util/pastebin.py +++ b/src/leap/bitmask/util/pastebin.py @@ -27,8 +27,8 @@ __ALL__ = ['delete_paste', 'user_details', 'trending', 'pastes_by_user',
- 'generate_user_key', 'legacy_paste', 'paste', 'Pastebin',
- 'PastebinError']
+ 'generate_user_key', 'paste', 'Pastebin', 'PastebinError',
+ 'PostLimitError']
import urllib
@@ -40,6 +40,12 @@ class PastebinError(RuntimeError): exception message."""
+class PostLimitError(PastebinError):
+ """The user reached the limit of posts that can do in a day.
+ For more information look at: http://pastebin.com/faq#11a
+ """
+
+
class PastebinAPI(object):
"""Pastebin API interaction object.
@@ -67,6 +73,9 @@ class PastebinAPI(object): # String to determine bad API requests
_bad_request = 'Bad API request'
+ # String to determine if we reached the max post limit per day
+ _post_limit = 'Post limit, maximum pastes per 24h reached'
+
# Base domain name
_base_domain = 'pastebin.com'
@@ -708,95 +717,8 @@ class PastebinAPI(object): # errors we are likely to encounter
if response.startswith(self._bad_request):
raise PastebinError(response)
- elif not response.startswith(self._prefix_url):
- raise PastebinError(response)
-
- return response
-
- def legacy_paste(self, paste_code,
- paste_name=None, paste_private=None,
- paste_expire_date=None, paste_format=None):
- """Unofficial python interface to the Pastebin legacy API.
-
- Unlike the official API, this one doesn't require an API key, so it's
- virtually anonymous.
-
-
- Usage Example::
- from pastebin import PastebinAPI
- x = PastebinAPI()
- url = x.legacy_paste('Snippet of code to paste goes here',
- paste_name = 'title of paste',
- paste_private = 'unlisted',
- paste_expire_date = '10M',
- paste_format = 'python')
- print url
- http://pastebin.com/tawPUgqY
-
-
- @type paste_code: string
- @param paste_code: The file or string to paste to body of the
- U{http://pastebin.com} paste.
-
- @type paste_name: string
- @param paste_name: (Optional) Title of the paste.
- Default is to paste with no title.
-
- @type paste_private: string
- @param paste_private: (Optional) C{'public'} if the paste is public
- (visible by everyone), C{'unlisted'} if it's public but not
- searchable. C{'private'} if the paste is private and not
- searchable or indexed.
- The Pastebin FAQ (U{http://pastebin.com/faq}) claims
- private pastes are not indexed by search engines (aka Google).
-
- @type paste_expire_date: string
- @param paste_expire_date: (Optional) Expiration date for the paste.
- Once past this date the paste is deleted automatically. Valid
- values are found in the L{PastebinAPI.paste_expire_date} class
- member.
- If not provided, the paste never expires.
-
- @type paste_format: string
- @param paste_format: (Optional) Programming language of the code being
- pasted. This enables syntax highlighting when reading the code in
- U{http://pastebin.com}. Default is no syntax highlighting (text is
- just text and not source code).
-
- @rtype: string
- @return: Returns the URL to the newly created paste.
- """
-
- # Code snippet to submit
- argv = {'paste_code': str(paste_code)}
-
- # Name of the poster
- if paste_name is not None:
- argv['paste_name'] = str(paste_name)
-
- # Is the snippet private?
- if paste_private is not None:
- argv['paste_private'] = int(bool(int(paste_private)))
-
- # Expiration for the snippet
- if paste_expire_date is not None:
- paste_expire_date = str(paste_expire_date).strip().upper()
- argv['paste_expire_date'] = paste_expire_date
-
- # Syntax highlighting
- if paste_format is not None:
- paste_format = str(paste_format).strip().lower()
- argv['paste_format'] = paste_format
-
- # lets try to read the URL that we've just built.
- data = urllib.urlencode(argv)
- request_string = urllib.urlopen(self._legacy_api_url, data)
- response = request_string.read()
-
- # do some basic error checking here so we can gracefully handle any
- # errors we are likely to encounter
- if response.startswith(self._bad_request):
- raise PastebinError(response)
+ elif response.startswith(self._post_limit):
+ raise PostLimitError(response)
elif not response.startswith(self._prefix_url):
raise PastebinError(response)
@@ -810,5 +732,4 @@ user_details = PastebinAPI.user_details trending = PastebinAPI.trending
pastes_by_user = PastebinAPI.pastes_by_user
generate_user_key = PastebinAPI.generate_user_key
-legacy_paste = PastebinAPI.legacy_paste
paste = PastebinAPI.paste
diff --git a/src/leap/bitmask/util/privilege_policies.py b/src/leap/bitmask/util/privilege_policies.py index 72442553..9d1e2c9a 100644 --- a/src/leap/bitmask/util/privilege_policies.py +++ b/src/leap/bitmask/util/privilege_policies.py @@ -27,35 +27,6 @@ from abc import ABCMeta, abstractmethod logger = logging.getLogger(__name__) -POLICY_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE policyconfig PUBLIC - "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" - "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd"> -<policyconfig> - - <vendor>LEAP Project</vendor> - <vendor_url>https://leap.se/</vendor_url> - - <action id="net.openvpn.gui.leap.run-openvpn"> - <description>Runs the openvpn binary</description> - <description xml:lang="es">Ejecuta el binario openvpn</description> - <message>OpenVPN needs that you authenticate to start</message> - <message xml:lang="es"> - OpenVPN necesita autorizacion para comenzar - </message> - <icon_name>package-x-generic</icon_name> - <defaults> - <allow_any>yes</allow_any> - <allow_inactive>yes</allow_inactive> - <allow_active>yes</allow_active> - </defaults> - <annotate key="org.freedesktop.policykit.exec.path">{path}</annotate> - <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate> - </action> -</policyconfig> -""" - - def is_missing_policy_permissions(): """ Returns True if we do not have implemented a policy checker for this @@ -76,36 +47,6 @@ def is_missing_policy_permissions(): return policy_checker().is_missing_policy_permissions() -def get_policy_contents(openvpn_path): - """ - Returns the contents that the policy file should have. - - :param openvpn_path: the openvpn path to use in the polkit file - :type openvpn_path: str - :rtype: str - """ - return POLICY_TEMPLATE.format(path=openvpn_path) - - -def is_policy_outdated(path): - """ - Returns if the existing polkit file is outdated, comparing if the path - is correct. - - :param path: the path that should have the polkit file. - :type path: str. - :rtype: bool - """ - _system = platform.system() - platform_checker = _system + "PolicyChecker" - policy_checker = globals().get(platform_checker, None) - if policy_checker is None: - logger.debug("we could not find a policy checker implementation " - "for %s" % (_system,)) - return False - return policy_checker().is_outdated(path) - - class PolicyChecker: """ Abstract PolicyChecker class @@ -129,7 +70,7 @@ class LinuxPolicyChecker(PolicyChecker): PolicyChecker for Linux """ LINUX_POLKIT_FILE = ("/usr/share/polkit-1/actions/" - "net.openvpn.gui.leap.policy") + "se.leap.bitmask.policy") @classmethod def get_polkit_path(self): @@ -141,6 +82,8 @@ class LinuxPolicyChecker(PolicyChecker): return self.LINUX_POLKIT_FILE def is_missing_policy_permissions(self): + # FIXME this name is quite confusing, it does not have anything to do with + # file permissions. """ Returns True if we could not find the appropriate policykit file in place @@ -148,22 +91,3 @@ class LinuxPolicyChecker(PolicyChecker): :rtype: bool """ return not os.path.isfile(self.LINUX_POLKIT_FILE) - - def is_outdated(self, path): - """ - Returns if the existing polkit file is outdated, comparing if the path - is correct. - - :param path: the path that should have the polkit file. - :type path: str. - :rtype: bool - """ - polkit = None - try: - with open(self.LINUX_POLKIT_FILE) as f: - polkit = f.read() - except IOError, e: - logger.error("Error reading polkit file(%s): %r" % ( - self.LINUX_POLKIT_FILE, e)) - - return get_policy_contents(path) != polkit |