diff options
author | Tomás Touceda <chiiph@leap.se> | 2014-07-18 11:22:47 -0300 |
---|---|---|
committer | Tomás Touceda <chiiph@leap.se> | 2014-07-18 11:22:47 -0300 |
commit | 159dbe295148975bdfe9a50f871254aa9adf2328 (patch) | |
tree | 5b679b7f617c4bc09c25a4c369e77156f0ff3e2c | |
parent | 7858d83af4a09ab00f6ba33dd8dbcf07ade101ce (diff) | |
parent | 312746bc9b77f0f738ccf2192d81ab94fdf9d6ba (diff) |
Merge branch 'release-0.6.0'0.6.0
60 files changed, 2462 insertions, 1525 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 92da1573..e8d24c95 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,23 @@ History 2014 ==== +0.6.0 July 18 -- the "nothing to see here" release: ++++++++++++++++++++++++++++++++++++++++++++++++++++ + +- Initial sync message is confusing. Closes #5875. +- Use preferred provider on first run. Closes #5813. +- Add TUF init repository and release tools. Closes #5864. +- Add support for fingerprint-gui's polkit agent. Closes #5880. +- Reroute DNS packets instead of blocking them, eliminating need to + muck around with resolv.conf. Closes #4633, #5655, #5738, #4823 +- Use inline decrypting for initial soledad syncrhonization, to wait + for secrets. +- Add the ability to create an osx bundle with py2app. Closes #5845. +- Split frontend/backend in different files. Closes #5719. +- Implement ZMQ based messaging system. Closes #5733. +- Launch the backend in a different process than the app. Closes + #5734. + 0.5.3 June 27 -- the "encrypt ALL THE THINGS" release: ++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/docs/dev/environment.rst b/docs/dev/environment.rst index 0f6366ef..a3184b01 100644 --- a/docs/dev/environment.rst +++ b/docs/dev/environment.rst @@ -54,7 +54,7 @@ It is a tool to create isolated Python environments. The basic problem being addressed is one of dependencies and versions, and indirectly permissions. Imagine you have an application that needs version 1 of LibFoo, but another application requires version 2. How can you use both these applications? If you install everything into /usr/lib/python2.7/site-packages (or whatever your platform's standard location is), it's easy to end up in a situation where you unintentionally upgrade an application that shouldn't be upgraded. -Read more about it in the `project documentation page <http://pypi.python.org/pypi/virtualenv/>`_. +Read more about it in the `project documentation page <http://pypi.python.org/pypi/virtualenv/>`_. .. note:: this section could be completed with useful options that can be passed to the virtualenv command (e.g., to make portable paths, site-packages, ...). We also should document how to use virtualenvwrapper. @@ -73,7 +73,7 @@ You first create a virtualenv in any directory that you like:: Note the change in the prompt. -.. TODO use virtualenvwrapper + isis non-sudo recipe here +.. TODO use virtualenvwrapper + isis non-sudo recipe here .. _pysidevirtualenv: @@ -143,16 +143,6 @@ You need to repeat this step each time you change a ``.ui`` file. .. TODO need to make translations too? -.. _copyscriptfiles: - -Copy script files ------------------ - -The openvpn invocation expects some files to be in place. If you have not installed `bitmask` from a debian package, you must copy these files manually by now:: - - $ sudo mkdir -p /etc/leap - $ sudo cp pkg/linux/resolv-update /etc/leap - .. _policykit: Running openvpn without root privileges diff --git a/docs/dev/quickstart.rst b/docs/dev/quickstart.rst index a8be955e..d9c04a69 100644 --- a/docs/dev/quickstart.rst +++ b/docs/dev/quickstart.rst @@ -65,8 +65,6 @@ Compile the resource files:: Copy necessary files into system folders, with root privileges:: - (bitmask)$ sudo mkdir -p /etc/leap - (bitmask)$ sudo cp pkg/linux/resolv-update /etc/leap (bitmask)$ sudo cp pkg/linux/polkit/net.openvpn.gui.leap.policy /usr/share/polkit-1/actions/ diff --git a/docs/testers/howto.rst b/docs/testers/howto.rst index d9536632..a7a72b7f 100644 --- a/docs/testers/howto.rst +++ b/docs/testers/howto.rst @@ -67,7 +67,7 @@ Launch Bitmask in debug mode. Logs are way more verbose that way:: Get your hand on the logs. You can achieve that either by clicking on the "Show log" button, and saving to file, or directly by specifying the path to the logfile in the command line invocation:: - + bitmask --debug --logfile /tmp/bitmask.log Attach the logfile to your bug report. @@ -99,7 +99,7 @@ painlessly fetch the latest development code. We have put together a script to allow rapid testing in different platforms for the brave souls like you. It more or less does all the steps covered in the :ref:`Setting up a Work Enviroment <environment>` section, only that in a more -compact way suitable (ahem) also for non developers. +compact way suitable (ahem) also for non developers. .. note:: @@ -127,7 +127,7 @@ Download and source the following script in the parent folder where you want you .. code-block:: bash - cd /tmp + cd /tmp wget https://raw.github.com/leapcode/bitmask_client/develop/pkg/scripts/bitmask_bootstrap.sh source bitmask_bootstrap.sh @@ -142,7 +142,7 @@ Activating the virtualenv The above bootstrap script has fetched latest code inside a virtualenv, which is an isolated, *virtual* python local environment that avoids messing with your global paths. You will notice you are *inside* a virtualenv because you will see -a modified prompt reminding it to you (*bitmask-testbuild* in this case). +a modified prompt reminding it to you (*bitmask-testbuild* in this case). Thus, if you forget to *activate your virtualenv*, bitmask will not run from the local path, and it will be looking for something else in your global path. So, @@ -162,8 +162,6 @@ Copying config files If you have never installed ``bitmask`` globally, **you need to copy some files to its proper path before running it for the first time** (you only need to do this once). This, unless the virtualenv-based operations, will need root permissions. See :ref:`copy script files <copyscriptfiles>` and :ref:`running openvpn without root privileges <policykit>` sections for more info on this. In short:: $ sudo cp pkg/linux/polkit/net.openvpn.gui.leap.policy /usr/share/polkit-1/actions/ - $ sudo mkdir -p /etc/leap - $ sudo cp pkg/linux/resolv-update /etc/leap Local config files ^^^^^^^^^^^^^^^^^^^ diff --git a/pkg/linux/bitmask-root b/pkg/linux/bitmask-root index 5367a31c..58f9a103 100755 --- a/pkg/linux/bitmask-root +++ b/pkg/linux/bitmask-root @@ -47,37 +47,27 @@ import traceback cmdcheck = subprocess.check_output -## -## CONSTANTS -## +# +# CONSTANTS +# VERSION = "1" SCRIPT = "bitmask-root" NAMESERVER = "10.42.0.1" BITMASK_CHAIN = "bitmask" +BITMASK_CHAIN_NAT_OUT = "bitmask" +BITMASK_CHAIN_NAT_POST = "bitmask_postrouting" 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/local/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", @@ -133,9 +123,9 @@ if DEBUG: syslog.openlog(SCRIPT) -## -## UTILITY -## +# +# UTILITY +# def is_valid_address(value): @@ -150,37 +140,10 @@ def is_valid_address(value): socket.inet_aton(value) return True except Exception: - print("%s: ERROR: MALFORMED IP: %s!" % (SCRIPT, value)) + log("%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: @@ -235,135 +198,6 @@ def get_process_list(): 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. @@ -376,55 +210,85 @@ def run(command, *args, **options): `detach`: If True, run in detached process. `input`: If True, open command for writing stream to, returning the Popen object. + `throw`: If True, raise an exception if there is an error instead + of bailing. """ parts = [command] parts.extend(args) - if TEST or DEBUG: - print("%s run: %s " (SCRIPT, " ".join(parts))) + debug("%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) + _throw = options.get("throw", False) - if not _check or _detach or _input: + if not (_check or _throw) or _detach or _input: if _input: return subprocess.Popen(parts, stdin=subprocess.PIPE) else: - # XXX ok with return None ?? subprocess.Popen(parts) + return None 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: + if exc.returncode != 1: + # 0 or 1 is to be expected, but anything else + # should be logged. + debug("ERROR: Could not run %s: %s" % + (exc.cmd, exc.output), exception=exc) return exc.returncode + elif _throw: + raise exc else: bail("ERROR: Could not run %s: %s" % (exc.cmd, exc.output), exception=exc) -def bail(msg=None, exception=None): +def log(msg=None, exception=None, priority=syslog.LOG_INFO): """ - Abnormal exit. + print and log warning message or exception. :param msg: optional error message. :type msg: str + :param msg: optional exception. + :type msg: Exception + :param msg: syslog level + :type msg: one of LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR, + LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG """ if msg is not None: print("%s: %s" % (SCRIPT, msg)) - syslog.syslog(syslog.LOG_ERR, msg) + syslog.syslog(priority, msg) if exception is not None: - traceback.print_exc() + if TEST or DEBUG: + traceback.print_exc() + syslog.syslog(priority, traceback.format_exc()) + + +def debug(msg=None, exception=None): + """ + Just like log, but is skipped unless DEBUG. Use syslog.LOG_INFO + even for debug messages (we don't want to miss them). + """ + if TEST or DEBUG: + log(msg, exception) + + +def bail(msg=None, exception=None): + """ + abnormal exit. like log(), but exits with error status code. + """ + log(msg, exception) exit(1) -## -## OPENVPN -## +# +# OPENVPN +# def get_openvpn_bin(): @@ -457,21 +321,21 @@ def parse_openvpn_flags(args): 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)) + log("%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)) + log("%s: ERROR: Bad argument %s" % + (SCRIPT, param)) return None else: - print("WARNING: unrecognized openvpn flag %s" % flag_name) + log("WARNING: unrecognized openvpn flag %s" % flag_name) return result except Exception as exc: - print("%s: ERROR PARSING FLAGS: %s" % (SCRIPT, exc)) + log("%s: ERROR PARSING FLAGS: %s" % (SCRIPT, exc)) if DEBUG: logger.exception(exc) return None @@ -490,8 +354,8 @@ def openvpn_start(args): OPENVPN = get_openvpn_bin() flags = [OPENVPN] + FIXED_FLAGS + openvpn_flags if DEBUG: - print("%s: running openvpn with flags:" % (SCRIPT,)) - print(flags) + log("%s: running openvpn with flags:" % (SCRIPT,)) + log(flags) # note: first argument to command is ignored, but customarily set to # the command. os.execv(OPENVPN, flags) @@ -516,84 +380,9 @@ def openvpn_stop(args): 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, *args): - """ - 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 -## +# +# FIREWALL +# def get_gateways(gateways): @@ -703,29 +492,63 @@ def ip6tables(*args, **options): """ run_iptable_with_check(IP6TABLES, *args, **options) +# +# NOTE: these tests to see if a chain exists might incorrectly return false. +# This happens when there is an error in calling `iptables --list bitmask`. +# +# For this reason, when stopping the firewall, we do not trust the +# output of ipvx_chain_exists() but instead always attempt to delete +# the chain. +# + -def ipv4_chain_exists(table): +def ipv4_chain_exists(chain, table=None): """ - Check if a given chain exists. + Check if a given chain exists. Only returns true if it actually exists, + but might return false if it exists and iptables failed to run. - :param table: the table to check against - :type table: str + :param chain: the chain to check against + :type chain: str :rtype: bool """ - code = run(IPTABLES, "--list", table, "--numeric", exitcode=True) - return code == 0 + if table is not None: + code = run(IPTABLES, "-t", table, + "--list", chain, "--numeric", exitcode=True) + else: + code = run(IPTABLES, "--list", chain, "--numeric", exitcode=True) + if code == 0: + return True + elif code == 1: + return False + else: + log("ERROR: Could not determine state of iptable chain") + return False -def ipv6_chain_exists(table): +def ipv6_chain_exists(chain): """ - Check if a given chain exists. + see ipv4_chain_exists() - :param table: the table to check against - :type table: str + :param chain: the chain to check against + :type chain: str :rtype: bool """ - code = run(IP6TABLES, "--list", table, "--numeric", exitcode=True) - return code == 0 + code = run(IP6TABLES, "--list", chain, "--numeric", exitcode=True) + if code == 0: + return True + elif code == 1: + return False + else: + log("ERROR: Could not determine state of iptable chain") + return False + + +def enable_ip_forwarding(): + """ + ip_fowarding must be enabled for the firewall to work. + """ + with open('/proc/sys/net/ipv4/ip_forward', 'w') as f: + f.write('1\n') def firewall_start(args): @@ -740,29 +563,57 @@ def firewall_start(args): local_network_ipv6 = get_local_network_ipv6(default_device) gateways = get_gateways(args) - # add custom chain "bitmask" to front of OUTPUT chain + # add custom chain "bitmask" to front of OUTPUT chain for both + # the 'filter' and the 'nat' tables. if not ipv4_chain_exists(BITMASK_CHAIN): ip4tables("--new-chain", BITMASK_CHAIN) + if not ipv4_chain_exists(BITMASK_CHAIN_NAT_OUT, 'nat'): + ip4tables("--table", "nat", "--new-chain", BITMASK_CHAIN_NAT_OUT) + if not ipv4_chain_exists(BITMASK_CHAIN_NAT_POST, 'nat'): + ip4tables("--table", "nat", "--new-chain", BITMASK_CHAIN_NAT_POST) if not ipv6_chain_exists(BITMASK_CHAIN): ip6tables("--new-chain", BITMASK_CHAIN) + ip4tables("--table", "nat", "--insert", "OUTPUT", + "--jump", BITMASK_CHAIN_NAT_OUT) + ip4tables("--table", "nat", "--insert", "POSTROUTING", + "--jump", BITMASK_CHAIN_NAT_POST) iptables("--insert", "OUTPUT", "--jump", BITMASK_CHAIN) - # allow DNS over VPN - for allowed_dns in [NAMESERVER, "127.0.0.1", "127.0.1.1"]: - ip4tables("--append", BITMASK_CHAIN, "--protocol", "udp", - "--dport", "53", "--destination", allowed_dns, - "--jump", "ACCEPT") - - # block DNS requests to anyone but the service provider or localhost - # (when we actually route ipv6, we will need DNS rules for it too) - ip4tables("--append", BITMASK_CHAIN, "--protocol", "udp", "--dport", "53", - "--jump", "REJECT") - - # allow traffic to IPs on local network + # route all ipv4 DNS over VPN + # (note: NAT does not work with ipv6 until kernel 3.7) + enable_ip_forwarding() + # allow dns to localhost + ip4tables("-t", "nat", "--append", BITMASK_CHAIN, "--protocol", "udp", + "--dest", "127.0.1.1,127.0.0.1", "--dport", "53", + "--jump", "ACCEPT") + # rewrite all outgoing packets to use VPN DNS server + # (DNS does sometimes use TCP!) + ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_OUT, "-p", "udp", + "--dport", "53", "--jump", "DNAT", "--to", NAMESERVER+":53") + ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_OUT, "-p", "tcp", + "--dport", "53", "--jump", "DNAT", "--to", NAMESERVER+":53") + # enable masquerading, so that DNS packets rewritten by DNAT will + # have the correct source IPs + ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_POST, + "--protocol", "udp", "--dport", "53", "--jump", "MASQUERADE") + ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_POST, + "--protocol", "tcp", "--dport", "53", "--jump", "MASQUERADE") + + # allow local network traffic if local_network_ipv4: + # allow local network destinations ip4tables("--append", BITMASK_CHAIN, "--destination", local_network_ipv4, "-o", default_device, "--jump", "ACCEPT") + # allow local network sources for DNS + # (required to allow local network DNS that gets rewritten by NAT + # to get passed through so that MASQUERADE can set correct source IP) + ip4tables("--append", BITMASK_CHAIN, + "--source", local_network_ipv4, "-o", default_device, + "-p", "udp", "--dport", "53", "--jump", "ACCEPT") + ip4tables("--append", BITMASK_CHAIN, + "--source", local_network_ipv4, "-o", default_device, + "-p", "tcp", "--dport", "53", "--jump", "ACCEPT") # allow multicast Simple Service Discovery Protocol ip4tables("--append", BITMASK_CHAIN, "--protocol", "udp", @@ -800,8 +651,7 @@ def firewall_start(args): "--log-level", "7") # for now, ensure all other ipv6 packets get rejected (regardless of - # device) - # (not sure why, but "-p any" doesn't work) + # device). not sure why, but "-p any" doesn't work. ip6tables("--append", BITMASK_CHAIN, "-p", "tcp", "--jump", "REJECT") ip6tables("--append", BITMASK_CHAIN, "-p", "udp", "--jump", "REJECT") @@ -812,19 +662,89 @@ def firewall_start(args): def firewall_stop(): """ - Stop the firewall. + Stop the firewall. Because we really really always want the firewall to + be stopped if at all possible, this function is cautious and contains a + lot of trys and excepts. + + If there were any problems, we raise an exception at the end. This allows + the calling code to retry stopping the firewall. Stopping the firewall + can fail if iptables is being run by another process (only one iptables + command can be run at a time). """ - 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) + ok = True -## -## MAIN -## + # -t filter -D OUTPUT -j bitmask + try: + iptables("--delete", "OUTPUT", "--jump", BITMASK_CHAIN, throw=True) + except subprocess.CalledProcessError as exc: + debug("INFO: not able to remove bitmask firewall from OUTPUT chain " + "(maybe it is already removed?)", exc) + ok = False + + # -t nat -D OUTPUT -j bitmask + try: + ip4tables("-t", "nat", "--delete", "OUTPUT", + "--jump", BITMASK_CHAIN_NAT_OUT, throw=True) + except subprocess.CalledProcessError as exc: + debug("INFO: not able to remove bitmask firewall from OUTPUT chain " + "in 'nat' table (maybe it is already removed?)", exc) + ok = False + + # -t nat -D POSTROUTING -j bitmask_postrouting + try: + ip4tables("-t", "nat", "--delete", "POSTROUTING", + "--jump", BITMASK_CHAIN_NAT_POST, throw=True) + except subprocess.CalledProcessError as exc: + debug("INFO: not able to remove bitmask firewall from POSTROUTING " + "chain in 'nat' table (maybe it is already removed?)", exc) + ok = False + + # -t filter --delete-chain bitmask + try: + ip4tables("--flush", BITMASK_CHAIN, throw=True) + ip4tables("--delete-chain", BITMASK_CHAIN, throw=True) + except subprocess.CalledProcessError as exc: + debug("INFO: not able to flush and delete bitmask ipv4 firewall " + "chain (maybe it is already destroyed?)", exc) + ok = False + + # -t nat --delete-chain bitmask + try: + ip4tables("-t", "nat", "--flush", BITMASK_CHAIN_NAT_OUT, throw=True) + ip4tables("-t", "nat", "--delete-chain", + BITMASK_CHAIN_NAT_OUT, throw=True) + except subprocess.CalledProcessError as exc: + debug("INFO: not able to flush and delete bitmask ipv4 firewall " + "chain in 'nat' table (maybe it is already destroyed?)", exc) + ok = False + + # -t nat --delete-chain bitmask_postrouting + try: + ip4tables("-t", "nat", "--flush", BITMASK_CHAIN_NAT_POST, throw=True) + ip4tables("-t", "nat", "--delete-chain", + BITMASK_CHAIN_NAT_POST, throw=True) + except subprocess.CalledProcessError as exc: + debug("INFO: not able to flush and delete bitmask ipv4 firewall " + "chain in 'nat' table (maybe it is already destroyed?)", exc) + ok = False + + # -t filter --delete-chain bitmask (ipv6) + try: + ip6tables("--flush", BITMASK_CHAIN, throw=True) + ip6tables("--delete-chain", BITMASK_CHAIN, throw=True) + except subprocess.CalledProcessError as exc: + debug("INFO: not able to flush and delete bitmask ipv6 firewall " + "chain (maybe it is already destroyed?)", exc) + ok = False + + if not (ok or ipv4_chain_exists or ipv6_chain_exists): + raise Exception("firewall might still be left up. " + "Please try `firewall stop` again.") + + +# +# MAIN +# def main(): @@ -858,23 +778,20 @@ def main(): elif command == "firewall_start": try: firewall_start(args) - nameserver_setter.start(NAMESERVER) except Exception as ex: if not is_restart: - 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,)) + log("%s: INFO: bitmask firewall is up" % (SCRIPT,)) else: bail("INFO: bitmask firewall is down") @@ -884,8 +801,7 @@ def main(): bail("ERROR: No such command") if __name__ == "__main__": - if DEBUG: - logger.debug(" ".join(sys.argv)) + debug(" ".join(sys.argv)) main() - print("%s: done" % (SCRIPT,)) + log("done") exit(0) diff --git a/pkg/linux/resolv-update b/pkg/linux/resolv-update deleted file mode 100755 index c308b788..00000000 --- a/pkg/linux/resolv-update +++ /dev/null @@ -1,90 +0,0 @@ -#!/bin/bash -# -# Parses options from openvpn to update resolv.conf -# -# The only way to enforce that a linux system will not leak DNS -# queries is to replace /etc/resolv.conf with a file that only -# has the DNS resolver specified by the VPN. -# -# That is what this script does. This is what resolvconf is for, -# but sadly it does not always work. -# -# 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' -# - -function up() { - - comment=$( -cat <<SETVAR -# -# This is a temporary resolv.conf set by the Bitmask in order to -# strictly enforce that DNS lookups are secured by the VPN. -# -# When Bitmask quits or the VPN connection it manages is dropped, -# this file will be replace with the regularly scheduled /etc/resolv.conf -# -# If you want custom entries to appear in this file while Bitmask is running, -# put them in /etc/leap/resolv-head or /etc/leap/resolv-tail. These files -# should only be writable by root. -# - -SETVAR -) - - if [ -f /etc/leap/resolv-head ] ; then - custom_head=$(cat /etc/leap/resolv-head) - else - custom_head="" - fi - - if [ -f /etc/leap/resolv-tail ] ; then - custom_tail=$(cat /etc/leap/resolv-tail) - else - custom_tail="" - fi - - for optionname in ${!foreign_option_*} ; do - option="${!optionname}" - echo $option - part1=$(echo "$option" | cut -d " " -f 1) - if [ "$part1" == "dhcp-option" ] ; then - part2=$(echo "$option" | cut -d " " -f 2) - part3=$(echo "$option" | cut -d " " -f 3) - if [ "$part2" == "DNS" ] ; then - IF_DNS_NAMESERVERS="$IF_DNS_NAMESERVERS $part3" - fi - if [ "$part2" == "DOMAIN" ] ; then - IF_DNS_SEARCH="$IF_DNS_SEARCH $part3" - fi - fi - done - R="" - for SS in $IF_DNS_SEARCH ; do - R="${R}search $SS -" - done - for NS in $IF_DNS_NAMESERVERS ; do - R="${R}nameserver $NS -" - done - cp /etc/resolv.conf /etc/resolv.conf.bak - echo "$comment -$custom_head -$R -$custom_tail" > /etc/resolv.conf -} - -function down() { - if [ -f /etc/resolv.conf.bak ] ; then - cat /etc/resolv.conf.bak > /etc/resolv.conf - rm /etc/resolv.conf.bak - fi -} - -case $script_type in - up) up ;; - down) down ;; -esac diff --git a/pkg/linux/update-resolv-conf b/pkg/linux/update-resolv-conf deleted file mode 100755 index 76c69413..00000000 --- a/pkg/linux/update-resolv-conf +++ /dev/null @@ -1,58 +0,0 @@ -#!/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/osx/Info.plist b/pkg/osx/Info.plist index e90d920a..dc427c4a 100644 --- a/pkg/osx/Info.plist +++ b/pkg/osx/Info.plist @@ -2,21 +2,23 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> - <key>CFBundleDisplayName</key> - <string>leap-client</string> - <key>CFBundleExecutable</key> - <string>MacOS/app</string> - <key>CFBundleIconFile</key> - <string>icon-windowed.icns</string> - <key>CFBundleInfoDictionaryVersion</key> - <string>6.0</string> - <key>CFBundleName</key> - <string>leap-client</string> - <key>CFBundlePackageType</key> - <string>APPL</string> - <key>CFBundleShortVersionString</key> - <string>1</string> - <key>LSBackgroundOnly</key> - <false/> + <key>CFBundleDisplayName</key> + <string>Bitmask</string> + <key>CFBundleExecutable</key> + <string>app</string> + <key>CFBundleIconFile</key> + <string>bitmask.icns</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>Bitmask</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>1</string> + <key>LSBackgroundOnly</key> + <false/> + <key>CFBundleIdentifier</key> + <string>se.leap.bitmask</string> </dict> </plist> diff --git a/pkg/osx/bitmask.icns b/pkg/osx/bitmask.icns Binary files differnew file mode 100644 index 00000000..7cc3e752 --- /dev/null +++ b/pkg/osx/bitmask.icns diff --git a/pkg/requirements.pip b/pkg/requirements.pip index 3d6b33a3..bf05aa28 100644 --- a/pkg/requirements.pip +++ b/pkg/requirements.pip @@ -19,6 +19,8 @@ python-daemon # this should not be needed for Windows. keyring zope.proxy +pyzmq + leap.common>=0.3.7 leap.soledad.client>=0.5.0 leap.keymanager>=0.3.8 diff --git a/pkg/tuf/init.py b/pkg/tuf/init.py new file mode 100755 index 00000000..7300da0a --- /dev/null +++ b/pkg/tuf/init.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# init.py +# Copyright (C) 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +Tool to initialize a TUF repo. + +The keys can be generated with: + openssl genrsa -des3 -out private.pem 4096 +The public key can be exported with: + openssl rsa -in private.pem -outform PEM -pubout -out public.pem +""" + +import sys + +from tuf.repository_tool import create_new_repository +from tuf.repository_tool import import_rsa_privatekey_from_file +from tuf.repository_tool import import_rsa_publickey_from_file + + +def usage(): + print ("Usage: %s repo root_private_key root_pub_key targets_pub_key" + " timestamp_pub_key") % (sys.argv[0],) + + +def main(): + if len(sys.argv) < 6: + usage() + return + + repo_path = sys.argv[1] + root_priv_path = sys.argv[2] + root_pub_path = sys.argv[3] + targets_pub_path = sys.argv[4] + timestamp_pub_path = sys.argv[5] + repo = Repo(repo_path, root_priv_path) + repo.build(root_pub_path, targets_pub_path, timestamp_pub_path) + + print "%s/metadata.staged/root.json is ready" % (repo_path,) + + +class Repo(object): + """ + Repository builder class + """ + + def __init__(self, repo_path, key_path): + """ + Constructor + + :param repo_path: path where the repo lives + :type repo_path: str + :param key_path: path where the private root key lives + :type key_path: str + """ + self._repo_path = repo_path + self._key = import_rsa_privatekey_from_file(key_path) + + def build(self, root_pub_path, targets_pub_path, timestamp_pub_path): + """ + Create a new repo + + :param root_pub_path: path where the public root key lives + :type root_pub_path: str + :param targets_pub_path: path where the public targets key lives + :type targets_pub_path: str + :param timestamp_pub_path: path where the public timestamp key lives + :type timestamp_pub_path: str + """ + repository = create_new_repository(self._repo_path) + + pub_root_key = import_rsa_publickey_from_file(root_pub_path) + repository.root.add_verification_key(pub_root_key) + repository.root.load_signing_key(self._key) + + pub_target_key = import_rsa_publickey_from_file(targets_pub_path) + repository.targets.add_verification_key(pub_target_key) + repository.snapshot.add_verification_key(pub_target_key) + repository.targets.compressions = ["gz"] + repository.snapshot.compressions = ["gz"] + + pub_timestamp_key = import_rsa_publickey_from_file(timestamp_pub_path) + repository.timestamp.add_verification_key(pub_timestamp_key) + + repository.write_partial() + + +if __name__ == "__main__": + main() diff --git a/pkg/tuf/release.py b/pkg/tuf/release.py new file mode 100755 index 00000000..c4abcd0d --- /dev/null +++ b/pkg/tuf/release.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# release.py +# Copyright (C) 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +Tool to generate TUF related files after a release + +The 'repo' folder should contain two folders: + - 'metadata.staged' with all the jsons from the previows release + - 'targets' where the release targets are +""" + +import datetime +import os.path +import sys + +from tuf.repository_tool import load_repository +from tuf.repository_tool import import_rsa_privatekey_from_file +from tuf.repository_tool import import_rsa_publickey_from_file + +""" +Days until the expiration of targets.json and snapshot.json. After this ammount +of days the TUF client won't accept this files. +""" +EXPIRATION_DAYS = 90 + + +def usage(): + print "Usage: %s repo key" % (sys.argv[0],) + + +def main(): + if len(sys.argv) < 3: + usage() + return + + repo_path = sys.argv[1] + key_path = sys.argv[2] + targets = Targets(repo_path, key_path) + targets.build() + + print "%s/metadata.staged/(targets|snapshot).json[.gz] are ready" % \ + (repo_path,) + + +class Targets(object): + """ + Targets builder class + """ + + def __init__(self, repo_path, key_path): + """ + Constructor + + :param repo_path: path where the repo lives + :type repo_path: str + :param key_path: path where the private targets key lives + :type key_path: str + """ + self._repo_path = repo_path + self._key = import_rsa_privatekey_from_file(key_path) + + def build(self): + """ + Generate snapshot.json[.gz] and targets.json[.gz] + """ + self._repo = load_repository(self._repo_path) + self._load_targets() + + self._repo.targets.load_signing_key(self._key) + self._repo.snapshot.load_signing_key(self._key) + self._repo.targets.compressions = ["gz"] + self._repo.snapshot.compressions = ["gz"] + self._repo.snapshot.expiration = ( + datetime.datetime.now() + + datetime.timedelta(days=EXPIRATION_DAYS)) + self._repo.targets.expiration = ( + datetime.datetime.now() + + datetime.timedelta(days=EXPIRATION_DAYS)) + self._repo.write_partial() + + def _load_targets(self): + """ + Load a list of targets + """ + targets_path = os.path.join(self._repo_path, 'targets') + target_list = self._repo.get_filepaths_in_directory( + targets_path, + recursive_walk=True, + followlinks=True) + + for target in target_list: + octal_file_permissions = oct(os.stat(target).st_mode)[3:] + custom_file_permissions = { + 'file_permissions': octal_file_permissions + } + self._repo.targets.add_target(target, custom_file_permissions) + + +if __name__ == "__main__": + main() diff --git a/relnotes.txt b/relnotes.txt index 4fc33b39..925db87d 100644 --- a/relnotes.txt +++ b/relnotes.txt @@ -1,8 +1,8 @@ -ANNOUNCING Bitmask, the Internet Encryption Toolkit, release 0.5.3 +ANNOUNCING Bitmask, the Internet Encryption Toolkit, release 0.6.0 The LEAP team is pleased to announce the immediate availability of -version 0.5.3 of Bitmask, the Internet Encryption Toolkit, codename -"encrypt ALL THE THINGS". +version 0.6.0 of Bitmask, the Internet Encryption Toolkit, codename +"nothing to see here". https://downloads.leap.se/client/ @@ -21,7 +21,7 @@ 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 +$ pkexec /usr/local/sbin/bitmask-root firewall stop If for some reason that doesn't work, you will need to reboot your computer. @@ -43,9 +43,11 @@ NOT trust your life to it. WHAT CAN THIS VERSION OF BITMASK DO FOR ME? -Bitmask 0.5.3 improves greatly its Encrypted Internet support and -stability in general, among other various bug fixes. You can refer to -the CHANGELOG for the meat. +Bitmask 0.6.0 improves how email encryption/decryption works +internally to improve speed and CPU consumption. This release also +merges a big refactor we've been working on for some time, which will +give us room for a lot of improvements in the near future. You can +refer to the CHANGELOG for the meat. Encrypted Internet on Linux now helps you don't shoot yourself in the foot by leaking traffic outside of the secure connection it @@ -104,6 +106,6 @@ beyond any border. The LEAP team, -June 27, 2014 +July 18, 2014 Somewhere in the middle of the intertubes. EOF @@ -137,7 +137,7 @@ def get_versions(default={}, verbose=False): cmdclass["freeze_debianver"] = freeze_debianver parsed_reqs = utils.parse_requirements() -leap_launcher = 'bitmask=leap.bitmask.app:main' +leap_launcher = 'bitmask=leap.bitmask.app:start_app' from setuptools.command.develop import develop as _develop @@ -253,7 +253,8 @@ cmdclass["sdist"] = cmd_sdist import platform _system = platform.system() -IS_LINUX = True if _system == "Linux" else False +IS_LINUX = _system == "Linux" +IS_MAC = _system == "Darwin" data_files = [] @@ -267,6 +268,31 @@ if IS_LINUX: ["pkg/linux/bitmask-root"]), ] +extra_options = {} + +if IS_MAC: + extra_options["app"] = ['src/leap/bitmask/app.py'] + OPTIONS = { + 'argv_emulation': True, + 'plist': 'pkg/osx/Info.plist', + 'iconfile': 'pkg/osx/bitmask.icns', + } + extra_options["options"] = {'py2app': OPTIONS} + extra_options["setup_requires"] = ['py2app'] + + class jsonschema_recipe(object): + def check(self, dist, mf): + m = mf.findNode('jsonschema') + if m is None: + return None + + # Don't put jsonschema in the site-packages.zip file + return dict( + packages=['jsonschema'] + ) + + import py2app.recipes + py2app.recipes.jsonschema = jsonschema_recipe() setup( name="leap.bitmask", @@ -305,4 +331,5 @@ setup( entry_points={ 'console_scripts': [leap_launcher] }, + **extra_options ) diff --git a/src/leap/bitmask/__init__.py b/src/leap/bitmask/__init__.py index 0f733f26..9ec5aae7 100644 --- a/src/leap/bitmask/__init__.py +++ b/src/leap/bitmask/__init__.py @@ -25,6 +25,12 @@ from pkg_resources import parse_version from leap.bitmask.util import first +# HACK: This is a hack so that py2app copies _scrypt.so to the right +# place, it can't be technically imported, but that doesn't matter +# because the import is never executed +if False: + import _scrypt # noqa - skip 'not used' warning + def _is_release_version(version): """ @@ -60,16 +66,16 @@ try: IS_RELEASE_VERSION = _is_release_version(__version__) del get_versions except ImportError: - #running on a tree that has not run - #the setup.py setver + # running on a tree that has not run + # the setup.py setver pass __appname__ = "unknown" try: from leap.bitmask._appname import __appname__ except ImportError: - #running on a tree that has not run - #the setup.py setver + # running on a tree that has not run + # the setup.py setver pass __short_version__ = first(re.findall('\d+\.\d+\.\d+', __version__)) diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index 6a7d6ff1..88f6bc15 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -39,56 +39,49 @@ # M:::::::::::~NMMM7???7MMMM:::::::::::::::::::::::NMMMI??I7MMMM:::::::::::::M # M::::::::::::::7MMMMMMM+:::::::::::::::::::::::::::?MMMMMMMZ:::::::::::::::M # (thanks to: http://www.glassgiant.com/ascii/) -import signal -import sys +import multiprocessing import os +import sys -from functools import partial -from PySide import QtCore, QtGui +from leap.bitmask.backend.utils import generate_certificates from leap.bitmask import __version__ as VERSION from leap.bitmask.config import flags -from leap.bitmask.gui import locale_rc # noqa - silence pylint -from leap.bitmask.gui.mainwindow import MainWindow +from leap.bitmask.frontend_app import run_frontend +from leap.bitmask.backend_app import run_backend from leap.bitmask.logs.utils import create_logger from leap.bitmask.platform_init.locks import we_are_the_one_and_only from leap.bitmask.services.mail import plumber -from leap.bitmask.util import leap_argparse +from leap.bitmask.util import leap_argparse, flags_to_dict from leap.bitmask.util.requirement_checker import check_requirements from leap.common.events import server as event_server from leap.mail import __version__ as MAIL_VERSION -from twisted.internet import reactor -from twisted.internet.task import LoopingCall - import codecs codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None) - -def sigint_handler(*args, **kwargs): - """ - Signal handler for SIGINT - """ - logger = kwargs.get('logger', None) - if logger: - logger.debug("SIGINT catched. shutting down...") - mainwindow = args[0] - mainwindow.quit() +import psutil -def sigterm_handler(*args, **kwargs): +def kill_the_children(): """ - Signal handler for SIGTERM. - This handler is actually passed to twisted reactor + Make sure no lingering subprocesses are left in case of a bad termination. """ - logger = kwargs.get('logger', None) - if logger: - logger.debug("SIGTERM catched. shutting down...") - mainwindow = args[0] - mainwindow.quit() + me = os.getpid() + parent = psutil.Process(me) + print "Killing all the children processes..." + for child in parent.get_children(recursive=True): + try: + child.terminate() + except Exception as exc: + print exc + +# XXX This is currently broken, but we need to fix it to avoid +# orphaned processes in case of a crash. +# atexit.register(kill_the_children) def do_display_version(opts): @@ -116,16 +109,22 @@ def do_mail_plumbing(opts): # XXX catch when import is used w/o acct -def main(): +def start_app(): """ Starts the main event loop and launches the main window. """ + # Ignore the signals since we handle them in the subprocesses + # signal.signal(signal.SIGINT, signal.SIG_IGN) + # Parse arguments and store them opts = leap_argparse.get_options() do_display_version(opts) - bypass_checks = opts.danger - start_hidden = opts.start_hidden + options = { + 'start_hidden': opts.start_hidden, + 'debug': opts.debug, + 'log_file': opts.log_file, + } flags.STANDALONE = opts.standalone flags.OFFLINE = opts.offline @@ -177,59 +176,17 @@ def main(): logger.info('Starting app') - # We force the style if on KDE so that it doesn't load all the kde - # libs, which causes a compatibility issue in some systems. - # For more info, see issue #3194 - if flags.STANDALONE and os.environ.get("KDE_SESSION_UID") is not None: - sys.argv.append("-style") - sys.argv.append("Cleanlooks") - - app = QtGui.QApplication(sys.argv) - - # To test: - # $ LANG=es ./app.py - locale = QtCore.QLocale.system().name() - qtTranslator = QtCore.QTranslator() - if qtTranslator.load("qt_%s" % locale, ":/translations"): - app.installTranslator(qtTranslator) - appTranslator = QtCore.QTranslator() - if appTranslator.load("%s.qm" % locale[:2], ":/translations"): - app.installTranslator(appTranslator) - - # Needed for initializing qsettings it will write - # .config/leap/leap.conf top level app settings in a platform - # independent way - app.setOrganizationName("leap") - app.setApplicationName("leap") - app.setOrganizationDomain("leap.se") - - # XXX --------------------------------------------------------- - # In quarantine, looks like we don't need it anymore. - # This dummy timer ensures that control is given to the outside - # loop, so we can hook our sigint handler. - #timer = QtCore.QTimer() - #timer.start(500) - #timer.timeout.connect(lambda: None) - # XXX --------------------------------------------------------- - - window = MainWindow(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) - - 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() + generate_certificates() + + flags_dict = flags_to_dict() + + backend = lambda: run_backend(opts.danger, flags_dict) + backend_process = multiprocessing.Process(target=backend, name='Backend') + backend_process.daemon = True + backend_process.start() + + run_frontend(options, flags_dict) + if __name__ == "__main__": - main() + start_app() diff --git a/src/leap/bitmask/backend/api.py b/src/leap/bitmask/backend/api.py new file mode 100644 index 00000000..b8533f36 --- /dev/null +++ b/src/leap/bitmask/backend/api.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# api.py +# Copyright (C) 2013, 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# 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/>. +""" +Backend available API and SIGNALS definition. +""" +STOP_REQUEST = "stop" + +API = ( + STOP_REQUEST, # this method needs to be defined in order to support the + # backend stop action + + "eip_can_start", + "eip_cancel_setup", + "eip_check_dns", + "eip_get_gateway_country_code", + "eip_get_gateways_list", + "eip_get_initialized_providers", + "eip_setup", + "eip_start", + "eip_stop", + "eip_terminate", + "imap_start_service", + "imap_stop_service", + "keymanager_export_keys", + "keymanager_get_key_details", + "keymanager_list_keys", + "provider_bootstrap", + "provider_cancel_setup", + "provider_get_all_services", + "provider_get_details", + "provider_get_pinned_providers", + "provider_get_supported_services", + "provider_setup", + "settings_set_selected_gateway", + "smtp_start_service", + "smtp_stop_service", + "soledad_bootstrap", + "soledad_cancel_bootstrap", + "soledad_change_password", + "soledad_close", + "soledad_load_offline", + "tear_fw_down", + "user_cancel_login", + "user_change_password", + "user_get_logged_in_status", + "user_login", + "user_logout", + "user_register", +) + + +SIGNALS = ( + "backend_bad_call", + "eip_alien_openvpn_already_running", + "eip_can_start", + "eip_cancelled_setup", + "eip_cannot_start", + "eip_client_certificate_ready", + "eip_config_ready", + "eip_connected", + "eip_connection_aborted", + "eip_connection_died", + "eip_disconnected", + "eip_dns_error", + "eip_dns_ok", + "eip_get_gateway_country_code", + "eip_get_gateways_list", + "eip_get_gateways_list_error", + "eip_get_initialized_providers", + "eip_network_unreachable", + "eip_no_gateway", + "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", + "eip_status_changed", + "eip_stopped", + "eip_tear_fw_down", + "eip_uninitialized_provider", + "eip_vpn_launcher_exception", + "imap_stopped", + "keymanager_export_error", + "keymanager_export_ok", + "keymanager_import_addressmismatch", + "keymanager_import_datamismatch", + "keymanager_import_ioerror", + "keymanager_import_missingkey", + "keymanager_import_ok", + "keymanager_key_details", + "keymanager_keys_list", + "prov_cancelled_setup", + "prov_check_api_certificate", + "prov_check_ca_fingerprint", + "prov_download_ca_cert", + "prov_download_provider_info", + "prov_get_all_services", + "prov_get_details", + "prov_get_pinned_providers", + "prov_get_supported_services", + "prov_https_connection", + "prov_name_resolution", + "prov_problem_with_provider", + "prov_unsupported_api", + "prov_unsupported_client", + "soledad_bootstrap_failed", + "soledad_bootstrap_finished", + "soledad_cancelled_bootstrap", + "soledad_invalid_auth_token", + "soledad_offline_failed", + "soledad_offline_finished", + "soledad_password_change_error", + "soledad_password_change_ok", + "srp_auth_bad_user_or_password", + "srp_auth_connection_error", + "srp_auth_error", + "srp_auth_ok", + "srp_auth_server_error", + "srp_logout_error", + "srp_logout_ok", + "srp_not_logged_in_error", + "srp_password_change_badpw", + "srp_password_change_error", + "srp_password_change_ok", + "srp_registration_failed", + "srp_registration_finished", + "srp_registration_taken", + "srp_status_logged_in", + "srp_status_not_logged_in", +) diff --git a/src/leap/bitmask/backend/backend.py b/src/leap/bitmask/backend/backend.py new file mode 100644 index 00000000..833f4368 --- /dev/null +++ b/src/leap/bitmask/backend/backend.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +# backend.py +# Copyright (C) 2013, 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# 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/>. +import json +import threading +import time + +from twisted.internet import defer, reactor, threads + +import zmq +from zmq.auth.thread import ThreadAuthenticator + +from leap.bitmask.backend.api import API +from leap.bitmask.backend.utils import get_backend_certificates +from leap.bitmask.backend.signaler import Signaler + +import logging +logger = logging.getLogger(__name__) + + +class Backend(object): + """ + Backend server. + Receives signals from backend_proxy and emit signals if needed. + """ + PORT = '5556' + BIND_ADDR = "tcp://127.0.0.1:%s" % PORT + + def __init__(self): + """ + Backend constructor, create needed instances. + """ + self._signaler = Signaler() + + self._do_work = threading.Event() # used to stop the worker thread. + self._zmq_socket = None + + self._ongoing_defers = [] + self._init_zmq() + + def _init_zmq(self): + """ + Configure the zmq components and connection. + """ + context = zmq.Context() + socket = context.socket(zmq.REP) + + # Start an authenticator for this context. + auth = ThreadAuthenticator(context) + auth.start() + auth.allow('127.0.0.1') + + # Tell authenticator to use the certificate in a directory + auth.configure_curve(domain='*', location=zmq.auth.CURVE_ALLOW_ANY) + public, secret = get_backend_certificates() + socket.curve_publickey = public + socket.curve_secretkey = secret + socket.curve_server = True # must come before bind + + socket.bind(self.BIND_ADDR) + + self._zmq_socket = socket + + def _worker(self): + """ + Receive requests and send it to process. + + Note: we use a simple while since is less resource consuming than a + Twisted's LoopingCall. + """ + while self._do_work.is_set(): + # Wait for next request from client + try: + request = self._zmq_socket.recv(zmq.NOBLOCK) + self._zmq_socket.send("OK") + # logger.debug("Received request: '{0}'".format(request)) + self._process_request(request) + except zmq.ZMQError as e: + if e.errno != zmq.EAGAIN: + raise + time.sleep(0.01) + + def _stop_reactor(self): + """ + Stop the Twisted reactor, but first wait a little for some threads to + complete their work. + + Note: this method needs to be run in a different thread so the + time.sleep() does not block and other threads can finish. + i.e.: + use threads.deferToThread(this_method) instead of this_method() + """ + wait_max = 5 # seconds + wait_step = 0.5 + wait = 0 + while self._ongoing_defers and wait < wait_max: + time.sleep(wait_step) + wait += wait_step + msg = "Waiting for running threads to finish... {0}/{1}" + msg = msg.format(wait, wait_max) + logger.debug(msg) + + # after a timeout we shut down the existing threads. + for d in self._ongoing_defers: + d.cancel() + + reactor.stop() + logger.debug("Twisted reactor stopped.") + + def run(self): + """ + Start the ZMQ server and run the loop to handle requests. + """ + self._signaler.start() + self._do_work.set() + threads.deferToThread(self._worker) + reactor.run() + + def stop(self): + """ + Stop the server and the zmq request parse loop. + """ + logger.debug("STOP received.") + self._signaler.stop() + self._do_work.clear() + threads.deferToThread(self._stop_reactor) + + def _process_request(self, request_json): + """ + Process a request and call the according method with the given + parameters. + + :param request_json: a json specification of a request. + :type request_json: str + """ + try: + # request = zmq.utils.jsonapi.loads(request_json) + # We use stdlib's json to ensure that we get unicode strings + request = json.loads(request_json) + api_method = request['api_method'] + kwargs = request['arguments'] or None + except Exception as e: + msg = "Malformed JSON data in Backend request '{0}'. Exc: {1!r}" + msg = msg.format(request_json, e) + msg = msg.format(request_json) + logger.critical(msg) + raise + + if api_method not in API: + logger.error("Invalid API call '{0}'".format(api_method)) + return + + self._run_in_thread(api_method, kwargs) + + def _run_in_thread(self, api_method, kwargs): + """ + Run the method name in a thread with the given arguments. + + :param api_method: the callable name to run in a thread. + :type api_method: str + :param kwargs: the arguments dict that will be sent to the callable. + :type kwargs: tuple + """ + func = getattr(self, api_method) + + method = func + if kwargs is not None: + method = lambda: func(**kwargs) + + # logger.debug("Running method: '{0}' " + # "with args: '{1}' in a thread".format(api_method, kwargs)) + + # run the action in a thread and keep track of it + d = threads.deferToThread(method) + d.addCallback(self._done_action, d) + d.addErrback(self._done_action, d) + self._ongoing_defers.append(d) + + def _done_action(self, failure, d): + """ + Remove the defer from the ongoing list. + + :param failure: the failure that triggered the errback. + None if no error. + :type failure: twisted.python.failure.Failure + :param d: defer to remove + :type d: twisted.internet.defer.Deferred + """ + if failure is not None: + if failure.check(defer.CancelledError): + logger.debug("A defer was cancelled.") + else: + logger.error("There was a failure - {0!r}".format(failure)) + logger.error(failure.getTraceback()) + + if d in self._ongoing_defers: + self._ongoing_defers.remove(d) diff --git a/src/leap/bitmask/backend/backend_proxy.py b/src/leap/bitmask/backend/backend_proxy.py new file mode 100644 index 00000000..f683e465 --- /dev/null +++ b/src/leap/bitmask/backend/backend_proxy.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +# backend_proxy.py +# Copyright (C) 2013, 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# 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/>. +""" +The BackendProxy handles calls from the GUI and forwards (through ZMQ) +to the backend. +""" +import functools +import Queue +import threading +import time + +import zmq + +from leap.bitmask.backend.api import API, STOP_REQUEST +from leap.bitmask.backend.utils import get_backend_certificates + +import logging +logger = logging.getLogger(__name__) + + +class BackendProxy(object): + """ + The BackendProxy handles calls from the GUI and forwards (through ZMQ) + to the backend. + """ + PORT = '5556' + SERVER = "tcp://localhost:%s" % PORT + + def __init__(self): + self._socket = None + + # initialize ZMQ stuff: + context = zmq.Context() + logger.debug("Connecting to server...") + socket = context.socket(zmq.REQ) + + # public, secret = zmq.curve_keypair() + client_keys = zmq.curve_keypair() + socket.curve_publickey = client_keys[0] + socket.curve_secretkey = client_keys[1] + + # The client must know the server's public key to make a CURVE + # connection. + public, _ = get_backend_certificates() + socket.curve_serverkey = public + + socket.setsockopt(zmq.RCVTIMEO, 1000) + socket.connect(self.SERVER) + self._socket = socket + + self._call_queue = Queue.Queue() + self._worker_caller = threading.Thread(target=self._worker) + self._worker_caller.start() + + def _worker(self): + """ + Worker loop that processes the Queue of pending requests to do. + """ + while True: + try: + request = self._call_queue.get(block=False) + # break the loop after sending the 'stop' action to the + # backend. + if request == STOP_REQUEST: + break + + self._send_request(request) + except Queue.Empty: + pass + time.sleep(0.01) + + logger.debug("BackendProxy worker stopped.") + + def _api_call(self, *args, **kwargs): + """ + Call the `api_method` method in backend (through zmq). + + :param kwargs: named arguments to forward to the backend api method. + :type kwargs: dict + + Note: is mandatory to have the kwarg 'api_method' defined. + """ + if args: + # Use a custom message to be more clear about using kwargs *only* + raise Exception("All arguments need to be kwargs!") + + api_method = kwargs.pop('api_method', None) + if api_method is None: + raise Exception("Missing argument, no method name specified.") + + request = { + 'api_method': api_method, + 'arguments': kwargs, + } + + try: + request_json = zmq.utils.jsonapi.dumps(request) + except Exception as e: + msg = ("Error serializing request into JSON.\n" + "Exception: {0} Data: {1}") + msg = msg.format(e, request) + logger.critical(msg) + raise + + # queue the call in order to handle the request in a thread safe way. + self._call_queue.put(request_json) + + if api_method == STOP_REQUEST: + self._call_queue.put(STOP_REQUEST) + + def _send_request(self, request): + """ + Send the given request to the server. + This is used from a thread safe loop in order to avoid sending a + request without receiving a response from a previous one. + + :param request: the request to send. + :type request: str + """ + # logger.debug("Sending request to backend: {0}".format(request)) + self._socket.send(request) + + try: + # Get the reply. + self._socket.recv() + # response = self._socket.recv() + # msg = "Received reply for '{0}' -> '{1}'" + # msg = msg.format(request, response) + # logger.debug(msg) + except zmq.error.Again as e: + msg = "Timeout error contacting backend. {0!r}".format(e) + logger.critical(msg) + + def __getattribute__(self, name): + """ + This allows the user to do: + bp = BackendProxy() + bp.some_method() + + Just by having defined 'some_method' in the API + + :param name: the attribute name that is requested. + :type name: str + """ + if name in API: + return functools.partial(self._api_call, api_method=name) + else: + return object.__getattribute__(self, name) diff --git a/src/leap/bitmask/backend/components.py b/src/leap/bitmask/backend/components.py index 19fcf283..b372db89 100644 --- a/src/leap/bitmask/backend/components.py +++ b/src/leap/bitmask/backend/components.py @@ -31,6 +31,7 @@ from twisted.python import log import zope.interface import zope.proxy +from leap.bitmask.backend.settings import Settings, GATEWAY_AUTOMATIC from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.crypto.srpauth import SRPAuth from leap.bitmask.crypto.srpregister import SRPRegister @@ -197,7 +198,7 @@ class Provider(object): else: if self._signaler is not None: self._signaler.signal( - self._signaler.PROV_PROBLEM_WITH_PROVIDER_KEY) + self._signaler.prov_problem_with_provider) logger.error("Could not load provider configuration.") self._login_widget.set_enabled(True) @@ -234,7 +235,7 @@ class Provider(object): services = get_supported(self._get_services(domain)) self._signaler.signal( - self._signaler.PROV_GET_SUPPORTED_SERVICES, services) + self._signaler.prov_get_supported_services, services) def get_all_services(self, providers): """ @@ -253,7 +254,7 @@ class Provider(object): services_all = services_all.union(set(services)) self._signaler.signal( - self._signaler.PROV_GET_ALL_SERVICES, services_all) + self._signaler.prov_get_all_services, list(services_all)) def get_details(self, domain, lang=None): """ @@ -268,7 +269,7 @@ class Provider(object): prov_get_details -> dict """ self._signaler.signal( - self._signaler.PROV_GET_DETAILS, + self._signaler.prov_get_details, self._provider_config.get_light_config(domain, lang)) def get_pinned_providers(self): @@ -279,7 +280,7 @@ class Provider(object): prov_get_pinned_providers -> list of provider domains """ self._signaler.signal( - self._signaler.PROV_GET_PINNED_PROVIDERS, + self._signaler.prov_get_pinned_providers, PinnedProviders.domains()) @@ -324,7 +325,7 @@ class Register(object): 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.") @@ -401,12 +402,12 @@ class EIP(object): if not self._can_start(domain): if self._signaler is not None: - self._signaler.signal(self._signaler.EIP_CONNECTION_ABORTED) + self._signaler.signal(self._signaler.eip_connection_aborted) return if not loaded: if self._signaler is not None: - self._signaler.signal(self._signaler.EIP_CONNECTION_ABORTED) + self._signaler.signal(self._signaler.eip_connection_aborted) logger.error("Tried to start EIP but cannot find any " "available provider!") return @@ -425,28 +426,28 @@ class EIP(object): 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(), " + self._signaler.signal(signaler.backend_bad_call, "EIP.start(), " "no provider loaded") return try: self._start_eip(*args, **kwargs) except vpnprocess.OpenVPNAlreadyRunning: - signaler.signal(signaler.EIP_OPENVPN_ALREADY_RUNNING) + signaler.signal(signaler.eip_openvpn_already_running) except vpnprocess.AlienOpenVPNAlreadyRunning: - signaler.signal(signaler.EIP_ALIEN_OPENVPN_ALREADY_RUNNING) + signaler.signal(signaler.eip_alien_openvpn_already_running) except vpnlauncher.OpenVPNNotFoundException: - signaler.signal(signaler.EIP_OPENVPN_NOT_FOUND_ERROR) + 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) + signaler.signal(signaler.eip_vpn_launcher_exception) except linuxvpnlauncher.EIPNoPolkitAuthAgentAvailable: - signaler.signal(signaler.EIP_NO_POLKIT_AGENT_ERROR) + signaler.signal(signaler.eip_no_polkit_agent_error) except linuxvpnlauncher.EIPNoPkexecAvailable: - signaler.signal(signaler.EIP_NO_PKEXEC_ERROR) + signaler.signal(signaler.eip_no_pkexec_error) except darwinvpnlauncher.EIPNoTunKextLoaded: - signaler.signal(signaler.EIP_NO_TUN_KEXT_ERROR) + signaler.signal(signaler.eip_no_tun_kext_error) except Exception as e: logger.error("Unexpected problem: {0!r}".format(e)) else: @@ -482,12 +483,12 @@ class EIP(object): while retry <= MAX_FW_WAIT_RETRIES: if self._vpn.is_fw_down(): - self._signaler.signal(self._signaler.EIP_STOPPED) + self._signaler.signal(self._signaler.eip_stopped) return else: - #msg = "Firewall is not down yet, waiting... {0} of {1}" - #msg = msg.format(retry, MAX_FW_WAIT_RETRIES) - #logger.debug(msg) + # msg = "Firewall is not down yet, waiting... {0} of {1}" + # msg = msg.format(retry, MAX_FW_WAIT_RETRIES) + # logger.debug(msg) time.sleep(FW_WAIT_STEP) retry += 1 logger.warning("After waiting, firewall is not down... " @@ -542,7 +543,7 @@ class EIP(object): filtered_domains.append((domain, is_initialized)) if self._signaler is not None: - self._signaler.signal(self._signaler.EIP_GET_INITIALIZED_PROVIDERS, + self._signaler.signal(self._signaler.eip_get_initialized_providers, filtered_domains) def tear_fw_down(self): @@ -566,7 +567,7 @@ class EIP(object): if not self._provider_is_initialized(domain): if self._signaler is not None: self._signaler.signal( - self._signaler.EIP_UNINITIALIZED_PROVIDER) + self._signaler.eip_uninitialized_provider) return eip_config = eipconfig.EIPConfig() @@ -580,14 +581,55 @@ class EIP(object): 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) + 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) + self._signaler.eip_get_gateways_list, gateways) + + def get_gateway_country_code(self, domain): + """ + Signal the country code for the currently used gateway for the given + provider. + + :param domain: the domain to get country code. + :type domain: str + + Signals: + eip_get_gateway_country_code -> str + eip_no_gateway + """ + settings = Settings() + + 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_config.load(eipconfig.get_eipconfig_path(domain)) + + gateway_selector = eipconfig.VPNGatewaySelector(eip_config) + gateway_conf = settings.get_selected_gateway(domain) + + if gateway_conf == GATEWAY_AUTOMATIC: + gateways = gateway_selector.get_gateways() + else: + gateways = [gateway_conf] + + if not gateways: + self._signaler.signal(self._signaler.eip_no_gateway) + return + + # this only works for selecting the first gateway, as we're + # currently doing. + ccodes = gateway_selector.get_gateways_country_code() + gateway_ccode = ccodes[gateways[0]] + + self._signaler.signal(self._signaler.eip_get_gateway_country_code, + gateway_ccode) def _can_start(self, domain): """ @@ -607,7 +649,8 @@ class EIP(object): launcher = get_vpn_launcher() ovpn_path = force_eval(launcher.OPENVPN_BIN_PATH) if not os.path.isfile(ovpn_path): - logger.error("Cannot start OpenVPN, binary not found") + logger.error("Cannot start OpenVPN, binary not found: %s" % + (ovpn_path,)) return False # check for other problems @@ -643,10 +686,10 @@ class EIP(object): """ if self._can_start(domain): if self._signaler is not None: - self._signaler.signal(self._signaler.EIP_CAN_START) + self._signaler.signal(self._signaler.eip_can_start) else: if self._signaler is not None: - self._signaler.signal(self._signaler.EIP_CANNOT_START) + self._signaler.signal(self._signaler.eip_cannot_start) def check_dns(self, domain): """ @@ -665,7 +708,7 @@ class EIP(object): """ Callback handler for `do_check`. """ - self._signaler.signal(self._signaler.EIP_DNS_OK) + self._signaler.signal(self._signaler.eip_dns_ok) logger.debug("DNS check OK") def check_err(failure): @@ -677,7 +720,7 @@ class EIP(object): """ logger.debug("Can't resolve hostname. {0!r}".format(failure)) - self._signaler.signal(self._signaler.EIP_DNS_ERROR) + self._signaler.signal(self._signaler.eip_dns_error) # python 2.7.4 raises socket.error # python 2.7.5 raises socket.gaierror @@ -737,7 +780,7 @@ class Soledad(object): self._soledad_defer.addCallback(self._set_proxies_cb) else: if self._signaler is not None: - self._signaler.signal(self._signaler.SOLEDAD_BOOTSTRAP_FAILED) + self._signaler.signal(self._signaler.soledad_bootstrap_failed) logger.error("Could not load provider configuration.") return self._soledad_defer @@ -793,7 +836,7 @@ class Soledad(object): Password change callback. """ if self._signaler is not None: - self._signaler.signal(self._signaler.SOLEDAD_PASSWORD_CHANGE_OK) + self._signaler.signal(self._signaler.soledad_password_change_ok) def _change_password_error(self, failure): """ @@ -808,7 +851,7 @@ class Soledad(object): logger.error("Passphrase too short.") if self._signaler is not None: - self._signaler.signal(self._signaler.SOLEDAD_PASSWORD_CHANGE_ERROR) + self._signaler.signal(self._signaler.soledad_password_change_error) def change_password(self, new_password): """ @@ -866,7 +909,7 @@ class Keymanager(object): new_key = keys_file.read() except IOError as e: logger.error("IOError importing key. {0!r}".format(e)) - signal = self._signaler.KEYMANAGER_IMPORT_IOERROR + signal = self._signaler.keymanager_import_ioerror self._signaler.signal(signal) return @@ -876,19 +919,19 @@ class Keymanager(object): new_key) except (KeyAddressMismatch, KeyFingerprintMismatch) as e: logger.error(repr(e)) - signal = self._signaler.KEYMANAGER_IMPORT_DATAMISMATCH + signal = self._signaler.keymanager_import_datamismatch self._signaler.signal(signal) return if public_key is None or private_key is None: - signal = self._signaler.KEYMANAGER_IMPORT_MISSINGKEY + signal = self._signaler.keymanager_import_missingkey self._signaler.signal(signal) return current_public_key = keymanager.get_key(username, openpgp.OpenPGPKey) if public_key.address != current_public_key.address: logger.error("The key does not match the ID") - signal = self._signaler.KEYMANAGER_IMPORT_ADDRESSMISMATCH + signal = self._signaler.keymanager_import_addressmismatch self._signaler.signal(signal) return @@ -899,7 +942,7 @@ class Keymanager(object): keymanager.send_key(openpgp.OpenPGPKey) logger.debug('Import ok') - signal = self._signaler.KEYMANAGER_IMPORT_OK + signal = self._signaler.keymanager_import_ok self._signaler.signal(signal) @@ -923,17 +966,17 @@ class Keymanager(object): keys_file.write(private_key.key_data) logger.debug('Export ok') - self._signaler.signal(self._signaler.KEYMANAGER_EXPORT_OK) + self._signaler.signal(self._signaler.keymanager_export_ok) except IOError as e: logger.error("IOError exporting key. {0!r}".format(e)) - self._signaler.signal(self._signaler.KEYMANAGER_EXPORT_ERROR) + self._signaler.signal(self._signaler.keymanager_export_error) def list_keys(self): """ List all the keys stored in the local DB. """ keys = self._keymanager_proxy.get_all_keys_in_local_db() - self._signaler.signal(self._signaler.KEYMANAGER_KEYS_LIST, keys) + self._signaler.signal(self._signaler.keymanager_keys_list, keys) def get_key_details(self, username): """ @@ -942,7 +985,7 @@ class Keymanager(object): public_key = self._keymanager_proxy.get_key(username, openpgp.OpenPGPKey) details = (public_key.key_id, public_key.fingerprint) - self._signaler.signal(self._signaler.KEYMANAGER_KEY_DETAILS, details) + self._signaler.signal(self._signaler.keymanager_key_details, details) class Mail(object): @@ -1027,7 +1070,7 @@ class Mail(object): logger.debug('Waiting for imap service to stop.') cv.wait(self.SERVICE_STOP_TIMEOUT) logger.debug('IMAP stopped') - self._signaler.signal(self._signaler.IMAP_STOPPED) + self._signaler.signal(self._signaler.imap_stopped) def stop_imap_service(self): """ @@ -1080,7 +1123,7 @@ class Authenticate(object): return self._login_defer else: if self._signaler is not None: - self._signaler.signal(self._signaler.SRP_AUTH_ERROR) + self._signaler.signal(self._signaler.srp_auth_error) logger.error("Could not load provider configuration.") def cancel_login(self): @@ -1105,7 +1148,7 @@ class Authenticate(object): """ if not self._is_logged_in(): if self._signaler is not None: - self._signaler.signal(self._signaler.SRP_NOT_LOGGED_IN_ERROR) + self._signaler.signal(self._signaler.srp_not_logged_in_error) return return self._srp_auth.change_password(current_password, new_password) @@ -1117,7 +1160,7 @@ class Authenticate(object): """ if not self._is_logged_in(): if self._signaler is not None: - self._signaler.signal(self._signaler.SRP_NOT_LOGGED_IN_ERROR) + self._signaler.signal(self._signaler.srp_not_logged_in_error) return self._srp_auth.logout() @@ -1140,8 +1183,8 @@ class Authenticate(object): signal = None if self._is_logged_in(): - signal = self._signaler.SRP_STATUS_LOGGED_IN + signal = self._signaler.srp_status_logged_in else: - signal = self._signaler.SRP_STATUS_NOT_LOGGED_IN + signal = self._signaler.srp_status_not_logged_in self._signaler.signal(signal) diff --git a/src/leap/bitmask/backend/leapbackend.py b/src/leap/bitmask/backend/leapbackend.py index 3c5222f4..d3c4fcda 100644 --- a/src/leap/bitmask/backend/leapbackend.py +++ b/src/leap/bitmask/backend/leapbackend.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # leapbackend.py -# Copyright (C) 2013 LEAP +# Copyright (C) 2013, 2014 LEAP # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -15,178 +15,65 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. """ -Backend for GUI/Logic communication. +Backend for everything """ import logging -from Queue import Queue, Empty - -from twisted.internet import reactor -from twisted.internet import threads, defer -from twisted.internet.task import LoopingCall - import zope.interface import zope.proxy -from leap.bitmask.backend.leapsignaler import Signaler from leap.bitmask.backend import components +from leap.bitmask.backend.backend import Backend +from leap.bitmask.backend.settings import Settings logger = logging.getLogger(__name__) +ERROR_KEY = "error" +PASSED_KEY = "passed" + -class Backend(object): +class LeapBackend(Backend): """ - Backend for everything, the UI should only use this class. + Backend server subclass, used to implement the API methods. """ - - PASSED_KEY = "passed" - ERROR_KEY = "error" - def __init__(self, bypass_checks=False): """ Constructor for the backend. """ - # Components map for the commands received - self._components = {} - - # Ongoing defers that will be cancelled at stop time - self._ongoing_defers = [] + Backend.__init__(self) - # Signaler object to translate commands into Qt signals - self._signaler = Signaler() + self._settings = Settings() # Objects needed by several components, so we make a proxy and pass # them around self._soledad_proxy = zope.proxy.ProxyBase(None) self._keymanager_proxy = zope.proxy.ProxyBase(None) - # Component registration - self._register(components.Provider(self._signaler, bypass_checks)) - self._register(components.Register(self._signaler)) - self._register(components.Authenticate(self._signaler)) - self._register(components.EIP(self._signaler)) - self._register(components.Soledad(self._soledad_proxy, - self._keymanager_proxy, - self._signaler)) - self._register(components.Keymanager(self._keymanager_proxy, - self._signaler)) - self._register(components.Mail(self._soledad_proxy, - self._keymanager_proxy, - self._signaler)) - - # We have a looping call on a thread executing all the - # commands in queue. Right now this queue is an actual Queue - # object, but it'll become the zmq recv_multipart queue - self._lc = LoopingCall(threads.deferToThread, self._worker) - - # Temporal call_queue for worker, will be replaced with - # recv_multipart os something equivalent in the loopingcall - self._call_queue = Queue() - - @property - def signaler(self): - """ - Public signaler access to let the UI connect to its signals. - """ - return self._signaler - - def start(self): - """ - Starts the looping call - """ - logger.debug("Starting worker...") - self._lc.start(0.01) - - def stop(self): - """ - 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`. - """ - logger.debug("Stopping worker...") - if self._lc.running: - self._lc.stop() - else: - logger.warning("Looping call is not running, cannot stop") - - logger.debug("Cancelling ongoing defers...") - while len(self._ongoing_defers) > 0: - d = self._ongoing_defers.pop() - d.cancel() - logger.debug("Defers cancelled.") - - def _register(self, component): - """ - Registers a component in this backend - - :param component: Component to register - :type component: any object that implements ILEAPComponent - """ - # TODO: assert that the component implements the interfaces - # expected - try: - self._components[component.key] = component - except Exception: - logger.error("There was a problem registering %s" % (component,)) - - def _signal_back(self, _, signal): - """ - Helper method to signal back (callback like behavior) to the - UI that an operation finished. - - :param signal: signal name - :type signal: str - """ - self._signaler.signal(signal) - - def _worker(self): - """ - Worker method, called from a different thread and as a part of - a looping call - """ - try: - # this'll become recv_multipart - cmd = self._call_queue.get(block=False) - - # cmd is: component, method, signalback, *args - func = getattr(self._components[cmd[0]], cmd[1]) - d = func(*cmd[3:]) - if d is not None: # d may be None if a defer chain is cancelled. - # A call might not have a callback signal, but if it does, - # we add it to the chain - if cmd[2] is not None: - d.addCallbacks(self._signal_back, logger.error, cmd[2]) - d.addCallbacks(self._done_action, logger.error, - callbackKeywords={"d": d}) - d.addErrback(logger.error) - self._ongoing_defers.append(d) - except Empty: - # If it's just empty we don't have anything to do. - pass - except defer.CancelledError: - logger.debug("defer cancelled somewhere (CancelledError).") - except Exception as e: - # But we log the rest - logger.exception("Unexpected exception: {0!r}".format(e)) + # Component instances creation + self._provider = components.Provider(self._signaler, bypass_checks) + self._register = components.Register(self._signaler) + self._authenticate = components.Authenticate(self._signaler) + self._eip = components.EIP(self._signaler) + self._soledad = components.Soledad(self._soledad_proxy, + self._keymanager_proxy, + self._signaler) + self._keymanager = components.Keymanager(self._keymanager_proxy, + self._signaler) + self._mail = components.Mail(self._soledad_proxy, + self._keymanager_proxy, + self._signaler) - def _done_action(self, _, d): + def _check_type(self, obj, expected_type): """ - Remover of the defer once it's done + Check the type of a parameter. - :param d: defer to remove - :type d: twisted.internet.defer.Deferred + :param obj: object to check its type. + :type obj: any type + :param expected_type: the expected type of the object. + :type expected_type: type """ - if d in self._ongoing_defers: - self._ongoing_defers.remove(d) - - # XXX: Temporal interface until we migrate to zmq - # We simulate the calls to zmq.send_multipart. Once we separate - # this in two processes, the methods bellow can be changed to - # send_multipart and this backend class will be really simple. + if not isinstance(obj, expected_type): + raise TypeError("The parameter type is incorrect.") def provider_setup(self, provider): """ @@ -202,13 +89,13 @@ class Backend(object): 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)) + self._provider.setup_provider(provider) def provider_cancel_setup(self): """ Cancel the ongoing setup provider (if any). """ - self._call_queue.put(("provider", "cancel_setup_provider", None)) + self._provider.cancel_setup_provider() def provider_bootstrap(self, provider): """ @@ -223,7 +110,7 @@ class Backend(object): 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)) + self._provider.bootstrap(provider) def provider_get_supported_services(self, domain): """ @@ -235,8 +122,7 @@ class Backend(object): Signals: prov_get_supported_services -> list of unicode """ - self._call_queue.put(("provider", "get_supported_services", None, - domain)) + self._provider.get_supported_services(domain) def provider_get_all_services(self, providers): """ @@ -248,13 +134,11 @@ class Backend(object): Signals: prov_get_all_services -> list of unicode """ - self._call_queue.put(("provider", "get_all_services", None, - providers)) + self._provider.get_all_services(providers) def provider_get_details(self, domain, lang): """ - Signal a ProviderConfigLight object with the current ProviderConfig - settings. + Signal a dict with the current ProviderConfig settings. :param domain: the domain name of the provider. :type domain: str @@ -262,9 +146,9 @@ class Backend(object): :type lang: str Signals: - prov_get_details -> ProviderConfigLight + prov_get_details -> dict """ - self._call_queue.put(("provider", "get_details", None, domain, lang)) + self._provider.get_details(domain, lang) def provider_get_pinned_providers(self): """ @@ -273,7 +157,7 @@ class Backend(object): Signals: prov_get_pinned_providers -> list of provider domains """ - self._call_queue.put(("provider", "get_pinned_providers", None)) + self._provider.get_pinned_providers() def user_register(self, provider, username, password): """ @@ -291,8 +175,7 @@ class Backend(object): srp_registration_taken srp_registration_failed """ - self._call_queue.put(("register", "register_user", None, provider, - username, password)) + self._register.register_user(provider, username, password) def eip_setup(self, provider, skip_network=False): """ @@ -309,14 +192,13 @@ class Backend(object): eip_client_certificate_ready -> {PASSED_KEY: bool, ERROR_KEY: str} eip_cancelled_setup """ - self._call_queue.put(("eip", "setup_eip", None, provider, - skip_network)) + self._eip.setup_eip(provider, skip_network) def eip_cancel_setup(self): """ Cancel the ongoing setup EIP (if any). """ - self._call_queue.put(("eip", "cancel_setup_eip", None)) + self._eip.cancel_setup_eip() def eip_start(self, restart=False): """ @@ -343,7 +225,7 @@ class Backend(object): :param restart: whether is is a restart. :type restart: bool """ - self._call_queue.put(("eip", "start", None, restart)) + self._eip.start(restart) def eip_stop(self, shutdown=False, restart=False, failed=False): """ @@ -355,13 +237,13 @@ class Backend(object): :param restart: whether this is part of a restart. :type restart: bool """ - self._call_queue.put(("eip", "stop", None, shutdown, restart)) + self._eip.stop(shutdown, restart) def eip_terminate(self): """ Terminate the EIP service, not necessarily in a nice way. """ - self._call_queue.put(("eip", "terminate", None)) + self._eip.terminate() def eip_get_gateways_list(self, domain): """ @@ -370,16 +252,25 @@ class Backend(object): :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)) + self._eip.get_gateways_list(domain) + + def eip_get_gateway_country_code(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 -> str + eip_no_gateway + """ + self._eip.get_gateway_country_code(domain) def eip_get_initialized_providers(self, domains): """ @@ -392,8 +283,7 @@ class Backend(object): eip_get_initialized_providers -> list of tuple(unicode, bool) """ - self._call_queue.put(("eip", "get_initialized_providers", - None, domains)) + self._eip.get_initialized_providers(domains) def eip_can_start(self, domain): """ @@ -406,8 +296,7 @@ class Backend(object): eip_can_start eip_cannot_start """ - self._call_queue.put(("eip", "can_start", - None, domain)) + self._eip.can_start(domain) def eip_check_dns(self, domain): """ @@ -420,13 +309,13 @@ class Backend(object): eip_dns_ok eip_dns_error """ - self._call_queue.put(("eip", "check_dns", None, domain)) + self._eip.check_dns(domain) def tear_fw_down(self): """ Signal the need to tear the fw down. """ - self._call_queue.put(("eip", "tear_fw_down", None)) + self._eip.tear_fw_down() def user_login(self, provider, username, password): """ @@ -447,8 +336,7 @@ class Backend(object): srp_auth_connection_error srp_auth_error """ - self._call_queue.put(("authenticate", "login", None, provider, - username, password)) + self._authenticate.login(provider, username, password) def user_logout(self): """ @@ -459,13 +347,13 @@ class Backend(object): srp_logout_error srp_not_logged_in_error """ - self._call_queue.put(("authenticate", "logout", None)) + self._authenticate.logout() def user_cancel_login(self): """ Cancel the ongoing login (if any). """ - self._call_queue.put(("authenticate", "cancel_login", None)) + self._authenticate.cancel_login() def user_change_password(self, current_password, new_password): """ @@ -482,8 +370,7 @@ class Backend(object): srp_password_change_badpw srp_password_change_error """ - self._call_queue.put(("authenticate", "change_password", None, - current_password, new_password)) + self._authenticate.change_password(current_password, new_password) def soledad_change_password(self, new_password): """ @@ -498,8 +385,7 @@ class Backend(object): srp_password_change_badpw srp_password_change_error """ - self._call_queue.put(("soledad", "change_password", None, - new_password)) + self._soledad.change_password(new_password) def user_get_logged_in_status(self): """ @@ -509,7 +395,7 @@ class Backend(object): srp_status_logged_in srp_status_not_logged_in """ - self._call_queue.put(("authenticate", "get_logged_in_status", None)) + self._authenticate.get_logged_in_status() def soledad_bootstrap(self, username, domain, password): """ @@ -527,8 +413,10 @@ class Backend(object): soledad_bootstrap_failed soledad_invalid_auth_token """ - self._call_queue.put(("soledad", "bootstrap", None, - username, domain, password)) + self._check_type(username, unicode) + self._check_type(domain, unicode) + self._check_type(password, unicode) + self._soledad.bootstrap(username, domain, password) def soledad_load_offline(self, username, password, uuid): """ @@ -543,20 +431,19 @@ class Backend(object): Signals: """ - self._call_queue.put(("soledad", "load_offline", None, - username, password, uuid)) + self._soledad.load_offline(username, password, uuid) def soledad_cancel_bootstrap(self): """ Cancel the ongoing soledad bootstrapping process (if any). """ - self._call_queue.put(("soledad", "cancel_bootstrap", None)) + self._soledad.cancel_bootstrap() def soledad_close(self): """ Close soledad database. """ - self._call_queue.put(("soledad", "close", None)) + self._soledad.close() def keymanager_list_keys(self): """ @@ -565,7 +452,7 @@ class Backend(object): Signals: keymanager_keys_list -> list """ - self._call_queue.put(("keymanager", "list_keys", None)) + self._keymanager.list_keys() def keymanager_export_keys(self, username, filename): """ @@ -580,8 +467,7 @@ class Backend(object): keymanager_export_ok keymanager_export_error """ - self._call_queue.put(("keymanager", "export_keys", None, - username, filename)) + self._keymanager.export_keys(username, filename) def keymanager_get_key_details(self, username): """ @@ -593,7 +479,7 @@ class Backend(object): Signals: keymanager_key_details """ - self._call_queue.put(("keymanager", "get_key_details", None, username)) + self._keymanager.get_key_details(username) def smtp_start_service(self, full_user_id, download_if_needed=False): """ @@ -605,8 +491,7 @@ class Backend(object): for the file :type download_if_needed: bool """ - self._call_queue.put(("mail", "start_smtp_service", None, - full_user_id, download_if_needed)) + self._mail.start_smtp_service(full_user_id, download_if_needed) def imap_start_service(self, full_user_id, offline=False): """ @@ -617,14 +502,13 @@ class Backend(object): :param offline: whether imap should start in offline mode or not. :type offline: bool """ - self._call_queue.put(("mail", "start_imap_service", None, - full_user_id, offline)) + self._mail.start_imap_service(full_user_id, offline) def smtp_stop_service(self): """ Stop the SMTP service. """ - self._call_queue.put(("mail", "stop_smtp_service", None)) + self._mail.stop_smtp_service() def imap_stop_service(self): """ @@ -633,4 +517,15 @@ class Backend(object): Signals: imap_stopped """ - self._call_queue.put(("mail", "stop_imap_service", None)) + self._mail.stop_imap_service() + + def settings_set_selected_gateway(self, provider, gateway): + """ + Set the selected gateway for a given provider. + + :param provider: provider domain + :type provider: str + :param gateway: gateway to use as default + :type gateway: str + """ + self._settings.set_selected_gateway(provider, gateway) diff --git a/src/leap/bitmask/backend/leapsignaler.py b/src/leap/bitmask/backend/leapsignaler.py index da8908fd..a36e6fdc 100644 --- a/src/leap/bitmask/backend/leapsignaler.py +++ b/src/leap/bitmask/backend/leapsignaler.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# components.py +# leapsignaler.py # Copyright (C) 2013 LEAP # # This program is free software: you can redistribute it and/or modify @@ -15,371 +15,101 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. """ -Signaler for Backend/Frontend communication. +Signaling server, used to define the API signals. """ -import logging - from PySide import QtCore -logger = logging.getLogger(__name__) +from leap.bitmask.backend.signaler_qt import SignalerQt -class Signaler(QtCore.QObject): +class LeapSignaler(SignalerQt): """ - Signaler object, handles converting string commands to Qt signals. - - This is intended for the separation in frontend/backend, this will - live in the frontend. + Signaling server subclass, used to define the API signals. """ + backend_bad_call = QtCore.Signal(object) - #################### - # These will only exist in the frontend - # Signals for the ProviderBootstrapper - prov_name_resolution = QtCore.Signal(object) - prov_https_connection = QtCore.Signal(object) - prov_download_provider_info = QtCore.Signal(object) - - prov_download_ca_cert = QtCore.Signal(object) - prov_check_ca_fingerprint = QtCore.Signal(object) - prov_check_api_certificate = QtCore.Signal(object) - - prov_problem_with_provider = QtCore.Signal(object) - - prov_unsupported_client = QtCore.Signal(object) - prov_unsupported_api = QtCore.Signal(object) - - prov_get_all_services = QtCore.Signal(object) - prov_get_supported_services = QtCore.Signal(object) - prov_get_details = QtCore.Signal(object) - prov_get_pinned_providers = QtCore.Signal(object) - - prov_cancelled_setup = QtCore.Signal(object) - - # Signals for SRPRegister - srp_registration_finished = QtCore.Signal(object) - srp_registration_failed = QtCore.Signal(object) - srp_registration_taken = QtCore.Signal(object) - - # Signals for EIP bootstrapping - eip_config_ready = QtCore.Signal(object) + eip_alien_openvpn_already_running = QtCore.Signal() + eip_can_start = QtCore.Signal() + eip_cancelled_setup = QtCore.Signal() + eip_cannot_start = QtCore.Signal() 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_config_ready = QtCore.Signal(object) + eip_connected = QtCore.Signal() + eip_connection_aborted = QtCore.Signal() eip_connection_died = QtCore.Signal(object) - eip_connection_aborted = QtCore.Signal(object) - eip_stopped = QtCore.Signal(object) - - eip_dns_ok = QtCore.Signal(object) - eip_dns_error = 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_disconnected = QtCore.Signal(object) + eip_dns_error = QtCore.Signal() + eip_dns_ok = QtCore.Signal() + eip_get_gateway_country_code = 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_gateways_list_error = QtCore.Signal() 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_network_unreachable = QtCore.Signal() + eip_no_gateway = QtCore.Signal() + eip_no_pkexec_error = QtCore.Signal() + eip_no_polkit_agent_error = QtCore.Signal() + eip_no_tun_kext_error = QtCore.Signal() + eip_openvpn_already_running = QtCore.Signal() + eip_openvpn_not_found_error = QtCore.Signal() + eip_process_finished = QtCore.Signal(int) + eip_process_restart_ping = QtCore.Signal() + eip_process_restart_tls = QtCore.Signal() eip_state_changed = QtCore.Signal(dict) eip_status_changed = QtCore.Signal(dict) - eip_process_finished = QtCore.Signal(int) + eip_stopped = QtCore.Signal() eip_tear_fw_down = QtCore.Signal(object) - - # signals whether the needed files to start EIP exist or not - eip_can_start = QtCore.Signal(object) - eip_cannot_start = QtCore.Signal(object) - - # Signals for Soledad - soledad_bootstrap_failed = QtCore.Signal(object) - soledad_bootstrap_finished = QtCore.Signal(object) - soledad_offline_failed = QtCore.Signal(object) - soledad_offline_finished = QtCore.Signal(object) - soledad_invalid_auth_token = QtCore.Signal(object) - soledad_cancelled_bootstrap = QtCore.Signal(object) - soledad_password_change_ok = QtCore.Signal(object) - soledad_password_change_error = QtCore.Signal(object) - - # Keymanager signals - keymanager_export_ok = QtCore.Signal(object) - keymanager_export_error = QtCore.Signal(object) - keymanager_keys_list = QtCore.Signal(object) - - keymanager_import_ioerror = QtCore.Signal(object) - keymanager_import_datamismatch = QtCore.Signal(object) - keymanager_import_missingkey = QtCore.Signal(object) - keymanager_import_addressmismatch = QtCore.Signal(object) - keymanager_import_ok = QtCore.Signal(object) - + eip_uninitialized_provider = QtCore.Signal() + eip_vpn_launcher_exception = QtCore.Signal() + + imap_stopped = QtCore.Signal() + + keymanager_export_error = QtCore.Signal() + keymanager_export_ok = QtCore.Signal() + keymanager_import_addressmismatch = QtCore.Signal() + keymanager_import_datamismatch = QtCore.Signal() + keymanager_import_ioerror = QtCore.Signal() + keymanager_import_missingkey = QtCore.Signal() + keymanager_import_ok = QtCore.Signal() keymanager_key_details = QtCore.Signal(object) + keymanager_keys_list = QtCore.Signal(object) - # mail related signals - imap_stopped = QtCore.Signal(object) - - # This signal is used to warn the backend user that is doing something - # wrong - backend_bad_call = QtCore.Signal(object) - - #################### - # These will exist both in the backend AND the front end. - # The frontend might choose to not "interpret" all the signals - # from the backend, but the backend needs to have all the signals - # it's going to emit defined here - PROV_NAME_RESOLUTION_KEY = "prov_name_resolution" - PROV_HTTPS_CONNECTION_KEY = "prov_https_connection" - PROV_DOWNLOAD_PROVIDER_INFO_KEY = "prov_download_provider_info" - PROV_DOWNLOAD_CA_CERT_KEY = "prov_download_ca_cert" - PROV_CHECK_CA_FINGERPRINT_KEY = "prov_check_ca_fingerprint" - PROV_CHECK_API_CERTIFICATE_KEY = "prov_check_api_certificate" - PROV_PROBLEM_WITH_PROVIDER_KEY = "prov_problem_with_provider" - PROV_UNSUPPORTED_CLIENT = "prov_unsupported_client" - PROV_UNSUPPORTED_API = "prov_unsupported_api" - PROV_CANCELLED_SETUP = "prov_cancelled_setup" - PROV_GET_ALL_SERVICES = "prov_get_all_services" - PROV_GET_SUPPORTED_SERVICES = "prov_get_supported_services" - PROV_GET_DETAILS = "prov_get_details" - PROV_GET_PINNED_PROVIDERS = "prov_get_pinned_providers" - - 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_STOPPED = "eip_stopped" - - EIP_NO_POLKIT_AGENT_ERROR = "eip_no_polkit_agent_error" - EIP_NO_TUN_KEXT_ERROR = "eip_no_tun_kext_error" - EIP_NO_PKEXEC_ERROR = "eip_no_pkexec_error" - 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_TEAR_FW_DOWN = "eip_tear_fw_down" - - EIP_CAN_START = "eip_can_start" - EIP_CANNOT_START = "eip_cannot_start" - - EIP_DNS_OK = "eip_dns_ok" - EIP_DNS_ERROR = "eip_dns_error" - - SOLEDAD_BOOTSTRAP_FAILED = "soledad_bootstrap_failed" - SOLEDAD_BOOTSTRAP_FINISHED = "soledad_bootstrap_finished" - SOLEDAD_OFFLINE_FAILED = "soledad_offline_failed" - SOLEDAD_OFFLINE_FINISHED = "soledad_offline_finished" - SOLEDAD_INVALID_AUTH_TOKEN = "soledad_invalid_auth_token" - - SOLEDAD_PASSWORD_CHANGE_OK = "soledad_password_change_ok" - SOLEDAD_PASSWORD_CHANGE_ERROR = "soledad_password_change_error" - - SOLEDAD_CANCELLED_BOOTSTRAP = "soledad_cancelled_bootstrap" - - KEYMANAGER_EXPORT_OK = "keymanager_export_ok" - KEYMANAGER_EXPORT_ERROR = "keymanager_export_error" - KEYMANAGER_KEYS_LIST = "keymanager_keys_list" - - KEYMANAGER_IMPORT_IOERROR = "keymanager_import_ioerror" - KEYMANAGER_IMPORT_DATAMISMATCH = "keymanager_import_datamismatch" - KEYMANAGER_IMPORT_MISSINGKEY = "keymanager_import_missingkey" - KEYMANAGER_IMPORT_ADDRESSMISMATCH = "keymanager_import_addressmismatch" - KEYMANAGER_IMPORT_OK = "keymanager_import_ok" - KEYMANAGER_KEY_DETAILS = "keymanager_key_details" - - IMAP_STOPPED = "imap_stopped" - - BACKEND_BAD_CALL = "backend_bad_call" - - def __init__(self): - """ - Constructor for the Signaler - """ - QtCore.QObject.__init__(self) - self._signals = {} - - signals = [ - self.PROV_NAME_RESOLUTION_KEY, - self.PROV_HTTPS_CONNECTION_KEY, - self.PROV_DOWNLOAD_PROVIDER_INFO_KEY, - self.PROV_DOWNLOAD_CA_CERT_KEY, - self.PROV_CHECK_CA_FINGERPRINT_KEY, - self.PROV_CHECK_API_CERTIFICATE_KEY, - self.PROV_PROBLEM_WITH_PROVIDER_KEY, - self.PROV_UNSUPPORTED_CLIENT, - self.PROV_UNSUPPORTED_API, - self.PROV_CANCELLED_SETUP, - self.PROV_GET_ALL_SERVICES, - self.PROV_GET_SUPPORTED_SERVICES, - self.PROV_GET_DETAILS, - self.PROV_GET_PINNED_PROVIDERS, - - 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_STOPPED, - - 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.EIP_DNS_OK, - self.EIP_DNS_ERROR, - - 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.SOLEDAD_BOOTSTRAP_FAILED, - self.SOLEDAD_BOOTSTRAP_FINISHED, - self.SOLEDAD_OFFLINE_FAILED, - self.SOLEDAD_OFFLINE_FINISHED, - self.SOLEDAD_INVALID_AUTH_TOKEN, - self.SOLEDAD_CANCELLED_BOOTSTRAP, - - self.SOLEDAD_PASSWORD_CHANGE_OK, - self.SOLEDAD_PASSWORD_CHANGE_ERROR, - - self.KEYMANAGER_EXPORT_OK, - self.KEYMANAGER_EXPORT_ERROR, - self.KEYMANAGER_KEYS_LIST, - - self.KEYMANAGER_IMPORT_IOERROR, - self.KEYMANAGER_IMPORT_DATAMISMATCH, - self.KEYMANAGER_IMPORT_MISSINGKEY, - self.KEYMANAGER_IMPORT_ADDRESSMISMATCH, - self.KEYMANAGER_IMPORT_OK, - self.KEYMANAGER_KEY_DETAILS, - - self.IMAP_STOPPED, - - self.BACKEND_BAD_CALL, - ] - - for sig in signals: - self._signals[sig] = getattr(self, sig) - - def signal(self, key, data=None): - """ - Emits a Qt signal based on the key provided, with the data if provided. - - :param key: string identifying the signal to emit - :type key: str - :param data: object to send with the data - :type data: object - - NOTE: The data object will be a serialized str in the backend, - and an unserialized object in the frontend, but for now we - just care about objects. - """ - # 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 - - # for some reason emitting 'None' gives a segmentation fault. - if data is None: - data = '' - - try: - self._signals[key].emit(data) - except KeyError: - logger.error("Unknown key for signal %s!" % (key,)) + prov_cancelled_setup = QtCore.Signal() + prov_check_api_certificate = QtCore.Signal(object) + prov_check_ca_fingerprint = QtCore.Signal(object) + prov_download_ca_cert = QtCore.Signal(object) + prov_download_provider_info = QtCore.Signal(object) + prov_get_all_services = QtCore.Signal(object) + prov_get_details = QtCore.Signal(object) + prov_get_pinned_providers = QtCore.Signal(object) + prov_get_supported_services = QtCore.Signal(object) + prov_https_connection = QtCore.Signal(object) + prov_name_resolution = QtCore.Signal(object) + prov_problem_with_provider = QtCore.Signal() + prov_unsupported_api = QtCore.Signal() + prov_unsupported_client = QtCore.Signal() + + soledad_bootstrap_failed = QtCore.Signal() + soledad_bootstrap_finished = QtCore.Signal() + soledad_cancelled_bootstrap = QtCore.Signal() + soledad_invalid_auth_token = QtCore.Signal() + soledad_offline_failed = QtCore.Signal() + soledad_offline_finished = QtCore.Signal() + soledad_password_change_error = QtCore.Signal() + soledad_password_change_ok = QtCore.Signal() + + srp_auth_bad_user_or_password = QtCore.Signal() + srp_auth_connection_error = QtCore.Signal() + srp_auth_error = QtCore.Signal() + srp_auth_ok = QtCore.Signal() + srp_auth_server_error = QtCore.Signal() + srp_logout_error = QtCore.Signal() + srp_logout_ok = QtCore.Signal() + srp_not_logged_in_error = QtCore.Signal() + srp_password_change_badpw = QtCore.Signal() + srp_password_change_error = QtCore.Signal() + srp_password_change_ok = QtCore.Signal() + srp_registration_failed = QtCore.Signal() + srp_registration_finished = QtCore.Signal() + srp_registration_taken = QtCore.Signal() + srp_status_logged_in = QtCore.Signal() + srp_status_not_logged_in = QtCore.Signal() diff --git a/src/leap/bitmask/backend/settings.py b/src/leap/bitmask/backend/settings.py new file mode 100644 index 00000000..5cb4c616 --- /dev/null +++ b/src/leap/bitmask/backend/settings.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# settings.py +# Copyright (C) 2013, 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# 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/>. +""" +Backend settings +""" +import ConfigParser +import logging +import os + +from leap.bitmask.util import get_path_prefix +from leap.common.check import leap_assert, leap_assert_type + +logger = logging.getLogger(__name__) + +# We need this one available for the default decorator +GATEWAY_AUTOMATIC = "Automatic" +GENERAL_SECTION = "General" + + +class Settings(object): + """ + Leap backend settings hanler. + """ + CONFIG_NAME = "leap-backend.conf" + + # keys + GATEWAY_KEY = "Gateway" + + def __init__(self): + """ + Create the ConfigParser object and read it. + """ + self._settings_path = os.path.join(get_path_prefix(), + "leap", self.CONFIG_NAME) + + self._settings = ConfigParser.ConfigParser() + self._settings.read(self._settings_path) + + self._add_section(GENERAL_SECTION) + + def _add_section(self, section): + """ + Add `section` to the config file and don't fail if already exists. + + :param section: the section to add. + :type section: str + """ + self._settings.read(self._settings_path) + try: + self._settings.add_section(section) + except ConfigParser.DuplicateSectionError: + pass + + def _save(self): + """ + Save the current state to the config file. + """ + with open(self._settings_path, 'wb') as f: + self._settings.write(f) + + def _get_value(self, section, key, default): + """ + Return the value for the fiven `key` in `section`. + If there's no such section/key, `default` is returned. + + :param section: the section to get the value from. + :type section: str + :param key: the key which value we want to get. + :type key: str + :param default: the value to return if there is no section/key. + :type default: object + + :rtype: object + """ + try: + return self._settings.get(section, key) + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): + return default + + def get_selected_gateway(self, provider): + """ + Return the configured gateway for the given `provider`. + + :param provider: provider domain + :type provider: str + + :rtype: str + """ + leap_assert(len(provider) > 0, "We need a nonempty provider") + return self._get_value(provider, self.GATEWAY_KEY, GATEWAY_AUTOMATIC) + + def set_selected_gateway(self, provider, gateway): + """ + Saves the configured gateway for the given provider + + :param provider: provider domain + :type provider: str + + :param gateway: gateway to use as default + :type gateway: str + """ + + leap_assert(len(provider) > 0, "We need a nonempty provider") + leap_assert_type(gateway, (str, unicode)) + + self._add_section(provider) + + self._settings.set(provider, self.GATEWAY_KEY, gateway) + self._save() + + def get_uuid(self, username): + """ + Gets the uuid for a given username. + + :param username: the full user identifier in the form user@provider + :type username: basestring + """ + leap_assert("@" in username, + "Expected username in the form user@provider") + user, provider = username.split('@') + + return self._get_value(provider, username, "") + + def set_uuid(self, username, value): + """ + Sets the uuid for a given username. + + :param username: the full user identifier in the form user@provider + :type username: str or unicode + :param value: the uuid to save or None to remove it + :type value: str or unicode or None + """ + leap_assert("@" in username, + "Expected username in the form user@provider") + user, provider = username.split('@') + + if value is None: + self._settings.remove_option(provider, username) + else: + leap_assert(len(value) > 0, "We cannot save an empty uuid") + self._add_section(provider) + self._settings.set(provider, username, value) + + self._save() diff --git a/src/leap/bitmask/backend/signaler.py b/src/leap/bitmask/backend/signaler.py new file mode 100644 index 00000000..574bfa71 --- /dev/null +++ b/src/leap/bitmask/backend/signaler.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# signaler.py +# Copyright (C) 2013, 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# 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/>. +""" +Signaler client. +Receives signals from the backend and sends to the signaling server. +""" +import Queue +import threading +import time + +import zmq + +from leap.bitmask.backend.api import SIGNALS +from leap.bitmask.backend.utils import get_frontend_certificates + +import logging +logger = logging.getLogger(__name__) + + +class Signaler(object): + """ + Signaler client. + Receives signals from the backend and sends to the signaling server. + """ + PORT = "5667" + SERVER = "tcp://localhost:%s" % PORT + POLL_TIMEOUT = 2000 # ms + POLL_TRIES = 500 + + def __init__(self): + """ + Initialize the ZMQ socket to talk to the signaling server. + """ + context = zmq.Context() + logger.debug("Connecting to signaling server...") + socket = context.socket(zmq.REQ) + + # public, secret = zmq.curve_keypair() + client_keys = zmq.curve_keypair() + socket.curve_publickey = client_keys[0] + socket.curve_secretkey = client_keys[1] + + # The client must know the server's public key to make a CURVE + # connection. + public, _ = get_frontend_certificates() + socket.curve_serverkey = public + + socket.setsockopt(zmq.RCVTIMEO, 1000) + socket.connect(self.SERVER) + self._socket = socket + + self._signal_queue = Queue.Queue() + + self._do_work = threading.Event() # used to stop the worker thread. + self._worker_signaler = threading.Thread(target=self._worker) + + def __getattribute__(self, name): + """ + This allows the user to do: + S = Signaler() + S.SOME_SIGNAL + + Just by having defined 'some_signal' in _SIGNALS + + :param name: the attribute name that is requested. + :type name: str + """ + if name in SIGNALS: + return name + else: + return object.__getattribute__(self, name) + + def signal(self, signal, data=None): + """ + Sends a signal to the signaling server. + + :param signal: the signal to send. + :type signal: str + """ + if signal not in SIGNALS: + raise Exception("Unknown signal: '{0}'".format(signal)) + + request = { + 'signal': signal, + 'data': data, + } + + try: + request_json = zmq.utils.jsonapi.dumps(request) + except Exception as e: + msg = ("Error serializing request into JSON.\n" + "Exception: {0} Data: {1}") + msg = msg.format(e, request) + logger.critical(msg) + raise + + # queue the call in order to handle the request in a thread safe way. + self._signal_queue.put(request_json) + + def _worker(self): + """ + Worker loop that processes the Queue of pending requests to do. + """ + while self._do_work.is_set(): + try: + request = self._signal_queue.get(block=False) + self._send_request(request) + except Queue.Empty: + pass + time.sleep(0.01) + + logger.debug("Signaler thread stopped.") + + def start(self): + """ + Start the Signaler worker. + """ + self._do_work.set() + self._worker_signaler.start() + + def stop(self): + """ + Stop the Signaler worker. + """ + self._do_work.clear() + + def _send_request(self, request): + """ + Send the given request to the server. + This is used from a thread safe loop in order to avoid sending a + request without receiving a response from a previous one. + + :param request: the request to send. + :type request: str + """ + # logger.debug("Signaling '{0}'".format(request)) + self._socket.send(request) + + poll = zmq.Poller() + poll.register(self._socket, zmq.POLLIN) + + reply = None + tries = 0 + + while True: + socks = dict(poll.poll(self.POLL_TIMEOUT)) + if socks.get(self._socket) == zmq.POLLIN: + reply = self._socket.recv() + break + + tries += 1 + if tries < self.POLL_TRIES: + logger.warning('Retrying receive... {0}/{1}'.format( + tries, self.POLL_TRIES)) + else: + break + + if reply is None: + msg = "Timeout error contacting backend." + logger.critical(msg) + # else: + # msg = "Received reply for '{0}' -> '{1}'".format(request, reply) + # logger.debug(msg) diff --git a/src/leap/bitmask/backend/signaler_qt.py b/src/leap/bitmask/backend/signaler_qt.py new file mode 100644 index 00000000..433f18ed --- /dev/null +++ b/src/leap/bitmask/backend/signaler_qt.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# signaler_qt.py +# Copyright (C) 2013, 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# 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/>. +""" +Signaling server. +Receives signals from the signaling client and emit Qt signals for the GUI. +""" +import threading +import time + +from PySide import QtCore + +import zmq +from zmq.auth.thread import ThreadAuthenticator + +from leap.bitmask.backend.api import SIGNALS +from leap.bitmask.backend.utils import get_frontend_certificates + +import logging +logger = logging.getLogger(__name__) + + +class SignalerQt(QtCore.QObject): + """ + Signaling server. + Receives signals from the signaling client and emit Qt signals for the GUI. + """ + PORT = "5667" + BIND_ADDR = "tcp://127.0.0.1:%s" % PORT + + def __init__(self): + QtCore.QObject.__init__(self) + + # Note: we use a plain thread instead of a QThread since works better. + # The signaler was not responding on OSX if the worker loop was run in + # a QThread. + # Possibly, ZMQ was not getting cycles to do work because Qt not + # receiving focus or something. + self._worker_thread = threading.Thread(target=self._run) + self._do_work = threading.Event() + + def start(self): + """ + Start the worker thread for the signaler server. + """ + self._do_work.set() + self._worker_thread.start() + + def _run(self): + """ + Start a loop to process the ZMQ requests from the signaler client. + """ + logger.debug("Running SignalerQt loop") + context = zmq.Context() + socket = context.socket(zmq.REP) + + # Start an authenticator for this context. + auth = ThreadAuthenticator(context) + auth.start() + auth.allow('127.0.0.1') + + # Tell authenticator to use the certificate in a directory + auth.configure_curve(domain='*', location=zmq.auth.CURVE_ALLOW_ANY) + public, secret = get_frontend_certificates() + socket.curve_publickey = public + socket.curve_secretkey = secret + socket.curve_server = True # must come before bind + + socket.bind(self.BIND_ADDR) + + while self._do_work.is_set(): + # Wait for next request from client + try: + request = socket.recv(zmq.NOBLOCK) + # logger.debug("Received request: '{0}'".format(request)) + socket.send("OK") + self._process_request(request) + except zmq.ZMQError as e: + if e.errno != zmq.EAGAIN: + raise + time.sleep(0.01) + + logger.debug("SignalerQt thread stopped.") + + def stop(self): + """ + Stop the SignalerQt blocking loop. + """ + self._do_work.clear() + + def _process_request(self, request_json): + """ + Process a request and call the according method with the given + parameters. + + :param request_json: a json specification of a request. + :type request_json: str + """ + try: + request = zmq.utils.jsonapi.loads(request_json) + signal = request['signal'] + data = request['data'] + except Exception as e: + msg = "Malformed JSON data in Signaler request '{0}'. Exc: {1!r}" + msg = msg.format(request_json, e) + logger.critical(msg) + raise + + if signal not in SIGNALS: + logger.error("Unknown signal received, '{0}'".format(signal)) + return + + try: + qt_signal = getattr(self, signal) + except Exception: + logger.warning("Signal not implemented, '{0}'".format(signal)) + return + + # logger.debug("Emitting '{0}'".format(signal)) + if data is None: + qt_signal.emit() + else: + qt_signal.emit(data) diff --git a/src/leap/bitmask/backend/utils.py b/src/leap/bitmask/backend/utils.py new file mode 100644 index 00000000..54a16fd7 --- /dev/null +++ b/src/leap/bitmask/backend/utils.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# utils.py +# Copyright (C) 2013, 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# 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/>. +""" +Backend utilities to handle ZMQ certificates. +""" +import os +import shutil + +import zmq.auth + +from leap.bitmask.util import get_path_prefix +from leap.common.files import mkdir_p + +KEYS_DIR = os.path.join(get_path_prefix(), 'leap', 'zmq_certificates') + + +def generate_certificates(): + """ + Generate client and server CURVE certificate files. + """ + # Create directory for certificates, remove old content if necessary + if os.path.exists(KEYS_DIR): + shutil.rmtree(KEYS_DIR) + mkdir_p(KEYS_DIR) + + # create new keys in certificates dir + # public_file, secret_file = create_certificates(...) + zmq.auth.create_certificates(KEYS_DIR, "frontend") + zmq.auth.create_certificates(KEYS_DIR, "backend") + + +def get_frontend_certificates(): + """ + Return the frontend's public and secret certificates. + """ + frontend_secret_file = os.path.join(KEYS_DIR, "frontend.key_secret") + public, secret = zmq.auth.load_certificate(frontend_secret_file) + return public, secret + + +def get_backend_certificates(base_dir='.'): + """ + Return the backend's public and secret certificates. + """ + backend_secret_file = os.path.join(KEYS_DIR, "backend.key_secret") + public, secret = zmq.auth.load_certificate(backend_secret_file) + return public, secret diff --git a/src/leap/bitmask/backend_app.py b/src/leap/bitmask/backend_app.py index e69de29b..5c0e4803 100644 --- a/src/leap/bitmask/backend_app.py +++ b/src/leap/bitmask/backend_app.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# backend_app.py +# Copyright (C) 2013, 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# 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/>. +""" +Start point for the Backend. +""" +import logging +import multiprocessing +import signal + +from leap.bitmask.backend.leapbackend import LeapBackend +from leap.bitmask.util import dict_to_flags + +logger = logging.getLogger(__name__) + + +def signal_handler(signum, frame): + """ + Signal handler that quits the running app cleanly. + + :param signum: number of the signal received (e.g. SIGINT -> 2) + :type signum: int + :param frame: current stack frame + :type frame: frame or None + """ + # Note: we don't stop the backend in here since the frontend signal handler + # will take care of that. + # In the future we may need to do the stop in here when the frontend and + # the backend are run separately (without multiprocessing) + pname = multiprocessing.current_process().name + logger.debug("{0}: SIGNAL #{1} catched.".format(pname, signum)) + + +def run_backend(bypass_checks, flags_dict): + """ + Run the backend for the application. + + :param bypass_checks: whether we should bypass the checks or not + :type bypass_checks: bool + :param flags_dict: a dict containing the flag values set on app start. + :type flags_dict: dict + """ + # ignore SIGINT since app.py takes care of signaling SIGTERM to us. + signal.signal(signal.SIGINT, signal.SIG_IGN) + signal.signal(signal.SIGTERM, signal_handler) + + dict_to_flags(flags_dict) + + backend = LeapBackend(bypass_checks=bypass_checks) + backend.run() diff --git a/src/leap/bitmask/config/flags.py b/src/leap/bitmask/config/flags.py index 2f3fdde4..6b70659d 100644 --- a/src/leap/bitmask/config/flags.py +++ b/src/leap/bitmask/config/flags.py @@ -55,5 +55,3 @@ OPENVPN_VERBOSITY = 1 # Skip the checks in the wizard, use for testing purposes only! SKIP_WIZARD_CHECKS = False - -CURRENT_VPN_COUNTRY = None diff --git a/src/leap/bitmask/config/provider_spec.py b/src/leap/bitmask/config/provider_spec.py index cf942c7b..a1d91b90 100644 --- a/src/leap/bitmask/config/provider_spec.py +++ b/src/leap/bitmask/config/provider_spec.py @@ -37,7 +37,7 @@ leap_provider_spec = { 'default': {u'en': u'Test Provider'} }, 'description': { - #'type': LEAPTranslatable, + # 'type': LEAPTranslatable, 'type': dict, 'format': 'translatable', 'default': {u'en': u'Test provider'} diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index 192a9d5c..d59b3c31 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -24,7 +24,7 @@ import requests import srp import json -#this error is raised from requests +# this error is raised from requests from simplejson.decoder import JSONDecodeError from functools import partial from requests.adapters import HTTPAdapter @@ -32,7 +32,7 @@ from requests.adapters import HTTPAdapter from twisted.internet import threads from twisted.internet.defer import CancelledError -from leap.bitmask.config.leapsettings import LeapSettings +from leap.bitmask.backend.settings import Settings from leap.bitmask.util import request_helpers as reqhelper from leap.bitmask.util.compat import requests_has_max_retries from leap.bitmask.util.constants import REQUEST_TIMEOUT @@ -151,7 +151,7 @@ class SRPAuth(object): self._provider_config = provider_config self._signaler = signaler - self._settings = LeapSettings() + self._settings = Settings() # **************************************************** # # Dependency injection helpers, override this for more @@ -523,7 +523,7 @@ class SRPAuth(object): Password change callback. """ if self._signaler is not None: - self._signaler.signal(self._signaler.SRP_PASSWORD_CHANGE_OK) + self._signaler.signal(self._signaler.srp_password_change_ok) def _change_password_error(self, failure): """ @@ -535,9 +535,9 @@ class SRPAuth(object): return if failure.check(SRPAuthBadUserOrPassword): - self._signaler.signal(self._signaler.SRP_PASSWORD_CHANGE_BADPW) + self._signaler.signal(self._signaler.srp_password_change_badpw) else: - self._signaler.signal(self._signaler.SRP_PASSWORD_CHANGE_ERROR) + self._signaler.signal(self._signaler.srp_password_change_error) def authenticate(self, username, password): """ @@ -591,7 +591,7 @@ class SRPAuth(object): :type _: IGNORED """ logger.debug("Successful login!") - self._signaler.signal(self._signaler.SRP_AUTH_OK) + self._signaler.signal(self._signaler.srp_auth_ok) def _authenticate_error(self, failure): """ @@ -612,13 +612,13 @@ class SRPAuth(object): return if failure.check(SRPAuthBadUserOrPassword): - signal = self._signaler.SRP_AUTH_BAD_USER_OR_PASSWORD + signal = self._signaler.srp_auth_bad_user_or_password elif failure.check(SRPAuthConnectionError): - signal = self._signaler.SRP_AUTH_CONNECTION_ERROR + signal = self._signaler.srp_auth_connection_error elif failure.check(SRPAuthenticationError): - signal = self._signaler.SRP_AUTH_SERVER_ERROR + signal = self._signaler.srp_auth_server_error else: - signal = self._signaler.SRP_AUTH_ERROR + signal = self._signaler.srp_auth_error self._signaler.signal(signal) @@ -647,7 +647,7 @@ class SRPAuth(object): logger.warning("Something went wrong with the logout: %r" % (e,)) if self._signaler is not None: - self._signaler.signal(self._signaler.SRP_LOGOUT_ERROR) + self._signaler.signal(self._signaler.srp_logout_error) raise else: self.set_session_id(None) @@ -657,7 +657,7 @@ class SRPAuth(object): self._session = self._fetcher.session() logger.debug("Successfully logged out.") if self._signaler is not None: - self._signaler.signal(self._signaler.SRP_LOGOUT_OK) + self._signaler.signal(self._signaler.srp_logout_ok) def set_session_id(self, session_id): with self._session_id_lock: diff --git a/src/leap/bitmask/crypto/srpregister.py b/src/leap/bitmask/crypto/srpregister.py index f03dc469..86510de1 100644 --- a/src/leap/bitmask/crypto/srpregister.py +++ b/src/leap/bitmask/crypto/srpregister.py @@ -179,11 +179,11 @@ class SRPRegister(QtCore.QObject): return if status_code in self.STATUS_OK: - self._signaler.signal(self._signaler.SRP_REGISTRATION_FINISHED) + self._signaler.signal(self._signaler.srp_registration_finished) elif status_code == self.STATUS_TAKEN: - self._signaler.signal(self._signaler.SRP_REGISTRATION_TAKEN) + self._signaler.signal(self._signaler.srp_registration_taken) else: - self._signaler.signal(self._signaler.SRP_REGISTRATION_FAILED) + self._signaler.signal(self._signaler.srp_registration_failed) if __name__ == "__main__": diff --git a/src/leap/bitmask/crypto/tests/fake_provider.py b/src/leap/bitmask/crypto/tests/fake_provider.py index b8cdbb12..60a3ef0a 100755 --- a/src/leap/bitmask/crypto/tests/fake_provider.py +++ b/src/leap/bitmask/crypto/tests/fake_provider.py @@ -156,7 +156,7 @@ def getSession(self, sessionInterface=None): put the right cookie name in place """ if not self.session: - #cookiename = b"_".join([b'TWISTED_SESSION'] + self.sitepath) + # cookiename = b"_".join([b'TWISTED_SESSION'] + self.sitepath) cookiename = b"_".join([b'_session_id'] + self.sitepath) sessionCookie = self.getCookie(cookiename) if sessionCookie: @@ -321,7 +321,7 @@ class OpenSSLServerContextFactory(object): Create an SSL context. """ ctx = SSL.Context(SSL.SSLv23_METHOD) - #ctx = SSL.Context(SSL.TLSv1_METHOD) + # ctx = SSL.Context(SSL.TLSv1_METHOD) ctx.use_certificate_file(where('leaptestscert.pem')) ctx.use_privatekey_file(where('leaptestskey.pem')) diff --git a/src/leap/bitmask/frontend_app.py b/src/leap/bitmask/frontend_app.py index e69de29b..51607d0b 100644 --- a/src/leap/bitmask/frontend_app.py +++ b/src/leap/bitmask/frontend_app.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# frontend_app.py +# Copyright (C) 2013, 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# 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/>. +""" +Start point for the Frontend. +""" +import multiprocessing +import signal +import sys +import os + +from functools import partial + +from PySide import QtCore, QtGui + +from leap.bitmask.config import flags +from leap.bitmask.gui.mainwindow import MainWindow +from leap.bitmask.util import dict_to_flags + +import logging +logger = logging.getLogger(__name__) + + +def signal_handler(window, pid, signum, frame): + """ + Signal handler that quits the running app cleanly. + + :param window: a window with a `quit` callable + :type window: MainWindow + :param pid: process id of the main process. + :type pid: int + :param signum: number of the signal received (e.g. SIGINT -> 2) + :type signum: int + :param frame: current stack frame + :type frame: frame or None + """ + my_pid = os.getpid() + if pid == my_pid: + pname = multiprocessing.current_process().name + logger.debug("{0}: SIGNAL #{1} catched.".format(pname, signum)) + window.quit() + + +def run_frontend(options, flags_dict): + """ + Run the GUI for the application. + + :param options: a dict of options parsed from the command line. + :type options: dict + :param flags_dict: a dict containing the flag values set on app start. + :type flags_dict: dict + """ + dict_to_flags(flags_dict) + + start_hidden = options["start_hidden"] + + # We force the style if on KDE so that it doesn't load all the kde + # libs, which causes a compatibility issue in some systems. + # For more info, see issue #3194 + if flags.STANDALONE and os.environ.get("KDE_SESSION_UID") is not None: + sys.argv.append("-style") + sys.argv.append("Cleanlooks") + + qApp = QtGui.QApplication(sys.argv) + + # To test: + # $ LANG=es ./app.py + locale = QtCore.QLocale.system().name() + qtTranslator = QtCore.QTranslator() + if qtTranslator.load("qt_%s" % locale, ":/translations"): + qApp.installTranslator(qtTranslator) + appTranslator = QtCore.QTranslator() + if appTranslator.load("%s.qm" % locale[:2], ":/translations"): + qApp.installTranslator(appTranslator) + + # Needed for initializing qsettings it will write + # .config/leap/leap.conf top level app settings in a platform + # independent way + qApp.setOrganizationName("leap") + qApp.setApplicationName("leap") + qApp.setOrganizationDomain("leap.se") + + # HACK: + # We need to do some 'python work' once in a while, otherwise, no python + # code will be called and the Qt event loop will prevent the signal + # handlers for SIGINT/SIGTERM to be called. + # see: http://stackoverflow.com/a/4939113/687989 + timer = QtCore.QTimer() + timer.start(500) # You may change this if you wish. + timer.timeout.connect(lambda: None) # Let the interpreter run each 500 ms. + + window = MainWindow(start_hidden=start_hidden) + + my_pid = os.getpid() + sig_handler = partial(signal_handler, window, my_pid) + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + sys.exit(qApp.exec_()) + + +if __name__ == '__main__': + run_frontend() diff --git a/src/leap/bitmask/gui/__init__.py b/src/leap/bitmask/gui/__init__.py index 4b289442..bba02061 100644 --- a/src/leap/bitmask/gui/__init__.py +++ b/src/leap/bitmask/gui/__init__.py @@ -17,5 +17,8 @@ """ init file for leap.gui """ -app = __import__("app", globals(), locals(), [], 2) -__all__ = [app] +# This was added for coverage and testing, but when doing the osx +# bundle with py2app it fails because of this, so commenting for now +# Also creates conflicts with the new frontend/backend separation. +# app = __import__("app", globals(), locals(), [], 2) +# __all__ = [app] diff --git a/src/leap/bitmask/gui/eip_preferenceswindow.py b/src/leap/bitmask/gui/eip_preferenceswindow.py index 530cd38d..0f63972f 100644 --- a/src/leap/bitmask/gui/eip_preferenceswindow.py +++ b/src/leap/bitmask/gui/eip_preferenceswindow.py @@ -33,7 +33,7 @@ class EIPPreferencesWindow(QtGui.QDialog): """ Window that displays the EIP preferences. """ - def __init__(self, parent, domain, backend): + def __init__(self, parent, domain, backend, leap_signaler): """ :param parent: parent object of the EIPPreferencesWindow. :type parent: QWidget @@ -46,6 +46,7 @@ class EIPPreferencesWindow(QtGui.QDialog): self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") self._settings = LeapSettings() + self._leap_signaler = leap_signaler self._backend = backend # Load UI @@ -96,7 +97,7 @@ class EIPPreferencesWindow(QtGui.QDialog): if not providers: return - self._backend.eip_get_initialized_providers(providers) + self._backend.eip_get_initialized_providers(domains=providers) @QtCore.Slot(list) def _load_providers_in_combo(self, providers): @@ -148,6 +149,8 @@ class EIPPreferencesWindow(QtGui.QDialog): gateway = self.ui.cbGateways.itemData(idx) self._settings.set_selected_gateway(provider, gateway) + self._backend.settings_set_selected_gateway(provider=provider, + gateway=gateway) msg = self.tr( "Gateway settings for provider '{0}' saved.").format(provider) @@ -174,7 +177,7 @@ class EIPPreferencesWindow(QtGui.QDialog): domain = self.ui.cbProvidersGateway.itemData(domain_idx) self._selected_domain = domain - self._backend.eip_get_gateways_list(domain) + self._backend.eip_get_gateways_list(domain=domain) @QtCore.Slot(list) def _update_gateways_list(self, gateways): @@ -248,7 +251,7 @@ class EIPPreferencesWindow(QtGui.QDialog): self.ui.cbGateways.setEnabled(False) def _backend_connect(self): - sig = self._backend.signaler + sig = self._leap_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( diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py index bd569343..a707050a 100644 --- a/src/leap/bitmask/gui/eip_status.py +++ b/src/leap/bitmask/gui/eip_status.py @@ -24,7 +24,6 @@ from functools import partial from PySide import QtCore, QtGui -from leap.bitmask.config import flags from leap.bitmask.services import get_service_display_name, EIP_SERVICE from leap.bitmask.platform_init import IS_LINUX from leap.bitmask.util.averages import RateMovingAverage @@ -44,7 +43,7 @@ class EIPStatusWidget(QtGui.QWidget): RATE_STR = "%1.2f KB/s" TOTAL_STR = "%1.2f Kb" - def __init__(self, parent=None, eip_conductor=None): + def __init__(self, parent, eip_conductor, leap_signaler): """ :param parent: the parent of the widget. :type parent: QObject @@ -60,6 +59,8 @@ class EIPStatusWidget(QtGui.QWidget): self.ui = Ui_EIPStatus() self.ui.setupUi(self) + self._leap_signaler = leap_signaler + self.eip_conductor = eip_conductor self.eipconnection = eip_conductor.eip_connection @@ -98,7 +99,7 @@ class EIPStatusWidget(QtGui.QWidget): """ Connect backend signals. """ - signaler = self.eip_conductor._backend.signaler + signaler = self._leap_signaler signaler.eip_openvpn_already_running.connect( self._on_eip_openvpn_already_running) @@ -121,8 +122,8 @@ class EIPStatusWidget(QtGui.QWidget): # XXX we cannot connect this signal now because # it interferes with the proper notifications during restarts # without available network. - #signaler.eip_network_unreachable.connect( - #self._on_eip_network_unreachable) + # signaler.eip_network_unreachable.connect( + # self._on_eip_network_unreachable) def _make_status_clickable(self): """ @@ -324,7 +325,7 @@ class EIPStatusWidget(QtGui.QWidget): Triggered after a successful login. Enables the start button. """ - #logger.debug('Showing EIP start button') + # logger.debug('Showing EIP start button') self.eip_button.show() # Restore the eip action menu @@ -543,7 +544,7 @@ class EIPStatusWidget(QtGui.QWidget): elif vpn_state == "ALREADYRUNNING": # Put the following calls in Qt's event queue, otherwise # the UI won't update properly - #self.send_disconnect_signal() + # self.send_disconnect_signal() QtDelayedCall( 0, self.eipconnection.qtsigns.do_disconnect_signal.emit) msg = self.tr("Unable to start VPN, it's already running.") @@ -587,16 +588,23 @@ class EIPStatusWidget(QtGui.QWidget): self._systray.setIcon(QtGui.QIcon(selected_pixmap_tray)) self._eip_status_menu.setTitle(tray_message) - def set_provider(self, provider): + def set_provider(self, provider, country_code): + """ + Set the provider used right now, name and flag (if available). + + :param provider: the provider in use. + :type provider: str + :param country_code: the country code of the gateway in use. + :type country_code: str + """ self._provider = provider self.ui.lblEIPMessage.setText( self.tr("Routing traffic through: <b>{0}</b>").format( provider)) - ccode = flags.CURRENT_VPN_COUNTRY - if ccode is not None: - self.set_country_code(ccode) + if country_code is not None: + self.set_country_code(country_code) def set_country_code(self, code): """ @@ -729,7 +737,7 @@ class EIPStatusWidget(QtGui.QWidget): self.set_eip_status_icon("error") def set_eipstatus_off(self, error=True): - # XXX this should be handled by the state machine. + # XXX this should be handled by the state machine. """ Sets eip status to off """ diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py index 5caef745..bb755b5c 100644 --- a/src/leap/bitmask/gui/mail_status.py +++ b/src/leap/bitmask/gui/mail_status.py @@ -304,7 +304,7 @@ class MailStatusWidget(QtGui.QWidget): ext_status = "" if req.event == proto.KEYMANAGER_LOOKING_FOR_KEY: - ext_status = self.tr("Looking for key for this user") + ext_status = self.tr("Initial sync in progress, please wait...") elif req.event == proto.KEYMANAGER_KEY_FOUND: ext_status = self.tr("Found key! Starting mail...") # elif req.event == proto.KEYMANAGER_KEY_NOT_FOUND: diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 53a7d95a..6959650b 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -18,6 +18,7 @@ Main window for Bitmask. """ import logging +import time from datetime import datetime @@ -25,6 +26,11 @@ from PySide import QtCore, QtGui from leap.bitmask import __version__ as VERSION from leap.bitmask import __version_hash__ as VERSION_HASH + +# TODO: we should use a more granular signaling instead of passing error/ok as +# a result. +from leap.bitmask.backend.leapbackend import ERROR_KEY, PASSED_KEY + from leap.bitmask.config import flags from leap.bitmask.config.leapsettings import LeapSettings @@ -37,13 +43,13 @@ from leap.bitmask.gui.mail_status import MailStatusWidget from leap.bitmask.gui.preferenceswindow import PreferencesWindow from leap.bitmask.gui.systray import SysTray from leap.bitmask.gui.wizard import Wizard -from leap.bitmask.gui import twisted_main from leap.bitmask.platform_init import IS_WIN, IS_MAC, IS_LINUX from leap.bitmask.platform_init.initializers import init_platform from leap.bitmask.platform_init.initializers import init_signals -from leap.bitmask.backend import leapbackend +from leap.bitmask.backend.backend_proxy import BackendProxy +from leap.bitmask.backend.leapsignaler import LeapSignaler from leap.bitmask.services.eip import conductor as eip_conductor from leap.bitmask.services.mail import conductor as mail_conductor @@ -91,13 +97,10 @@ class MainWindow(QtGui.QMainWindow): # We give the services some time to a halt before forcing quit. SERVICES_STOP_TIMEOUT = 20000 # in milliseconds - def __init__(self, bypass_checks=False, start_hidden=False): + def __init__(self, start_hidden=False): """ Constructor for the client main window - :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 @@ -119,15 +122,18 @@ class MainWindow(QtGui.QMainWindow): self.ui = Ui_MainWindow() self.ui.setupUi(self) self.menuBar().setNativeMenuBar(not IS_LINUX) - self._backend = leapbackend.Backend(bypass_checks) - self._backend.start() + + self._backend = BackendProxy() + + self._leap_signaler = LeapSignaler() + self._leap_signaler.start() self._settings = LeapSettings() + # gateway = self._settings.get_selected_gateway(provider) + # self._backend.settings_set_selected_gateway(provider, gateway) # Login Widget - self._login_widget = LoginWidget( - self._settings, - self) + self._login_widget = LoginWidget(self._settings, self) self.ui.loginLayout.addWidget(self._login_widget) # Mail Widget @@ -144,8 +150,9 @@ class MainWindow(QtGui.QMainWindow): # EIP Control redux ######################################### self._eip_conductor = eip_conductor.EIPConductor( - self._settings, self._backend) - self._eip_status = EIPStatusWidget(self, self._eip_conductor) + self._settings, self._backend, self._leap_signaler) + self._eip_status = EIPStatusWidget(self, self._eip_conductor, + self._leap_signaler) init_signals.eip_missing_helpers.connect( self._disable_eip_missing_helpers) @@ -180,6 +187,7 @@ class MainWindow(QtGui.QMainWindow): self._services_being_stopped = {} # used to know if we are in the final steps of quitting + self._quitting = False self._finally_quitting = False self._backend_connected_signals = [] @@ -249,7 +257,7 @@ class MainWindow(QtGui.QMainWindow): # XXX should connect to mail_conductor.start_mail_service instead self.soledad_ready.connect(self._start_smtp_bootstrapping) self.soledad_ready.connect(self._start_imap_service) - ################################# end Qt Signals connection ######## + # ################################ end Qt Signals connection ######## init_platform() @@ -258,7 +266,6 @@ class MainWindow(QtGui.QMainWindow): self._logger_window = None - self._bypass_checks = bypass_checks self._start_hidden = start_hidden self._mail_conductor = mail_conductor.MailConductor(self._backend) @@ -283,7 +290,7 @@ class MainWindow(QtGui.QMainWindow): self._wizard_firstrun = True self._disconnect_and_untrack() self._wizard = Wizard(backend=self._backend, - bypass_checks=bypass_checks) + leap_signaler=self._leap_signaler) # Give this window time to finish init and then show the wizard QtDelayedCall(1, self._launch_wizard) self._wizard.accepted.connect(self._finish_init) @@ -342,7 +349,7 @@ class MainWindow(QtGui.QMainWindow): that we are tracking to disconnect later. :type only_tracked: bool """ - sig = self._backend.signaler + sig = self._leap_signaler conntrack = self._connect_and_track auth_err = self._authentication_error @@ -407,6 +414,9 @@ class MainWindow(QtGui.QMainWindow): sig.eip_dns_error.connect(self._eip_dns_error) + sig.eip_get_gateway_country_code.connect(self._set_eip_provider) + sig.eip_no_gateway.connect(self._set_eip_provider) + # ================================================================== # Soledad signals @@ -480,7 +490,7 @@ class MainWindow(QtGui.QMainWindow): if self._wizard is None: self._disconnect_and_untrack() self._wizard = Wizard(backend=self._backend, - bypass_checks=self._bypass_checks) + leap_signaler=self._leap_signaler) self._wizard.accepted.connect(self._finish_init) self._wizard.rejected.connect(self._rejected_wizard) @@ -575,7 +585,8 @@ class MainWindow(QtGui.QMainWindow): if self._provider_details is not None: mx_provided = MX_SERVICE in self._provider_details['services'] preferences = PreferencesWindow(self, user, domain, self._backend, - self._soledad_started, mx_provided) + self._soledad_started, mx_provided, + self._leap_signaler) self.soledad_ready.connect(preferences.set_soledad_ready) preferences.show() @@ -604,12 +615,13 @@ class MainWindow(QtGui.QMainWindow): return self._trying_to_start_eip = settings.get_autostart_eip() - self._backend.eip_can_start(default_provider) + self._backend.eip_can_start(domain=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.eip_setup(default_provider, skip_network=True) + self._backend.eip_setup(provider=default_provider, + skip_network=True) def _backend_can_start_eip(self): """ @@ -686,7 +698,9 @@ class MainWindow(QtGui.QMainWindow): Displays the EIP preferences window. """ domain = self._login_widget.get_selected_provider() - EIPPreferencesWindow(self, domain, self._backend).show() + pref = EIPPreferencesWindow(self, domain, + self._backend, self._leap_signaler) + pref.show() # # updates @@ -821,7 +835,7 @@ class MainWindow(QtGui.QMainWindow): """ providers = self._settings.get_configured_providers() - self._backend.provider_get_all_services(providers) + self._backend.provider_get_all_services(providers=providers) def _provider_get_all_services(self, services): self._set_eip_visible(EIP_SERVICE in services) @@ -1121,7 +1135,7 @@ class MainWindow(QtGui.QMainWindow): emit the corresponding signals inmediately """ domain = self._login_widget.get_selected_provider() - self._backend.provider_setup(domain) + self._backend.provider_setup(provider=domain) @QtCore.Slot(dict) def _load_provider_config(self, data): @@ -1136,11 +1150,11 @@ class MainWindow(QtGui.QMainWindow): backend.provider_setup() :type data: dict """ - if data[self._backend.PASSED_KEY]: + if data[PASSED_KEY]: selected_provider = self._login_widget.get_selected_provider() - self._backend.provider_bootstrap(selected_provider) + self._backend.provider_bootstrap(provider=selected_provider) else: - logger.error(data[self._backend.ERROR_KEY]) + logger.error(data[ERROR_KEY]) self._login_problem_provider() @QtCore.Slot() @@ -1242,16 +1256,17 @@ class MainWindow(QtGui.QMainWindow): Once the provider configuration is loaded, this starts the SRP authentication """ - if data[self._backend.PASSED_KEY]: + if data[PASSED_KEY]: username = self._login_widget.get_user() password = self._login_widget.get_password() self._show_hide_unsupported_services() domain = self._login_widget.get_selected_provider() - self._backend.user_login(domain, username, password) + self._backend.user_login(provider=domain, + username=username, password=password) else: - logger.error(data[self._backend.ERROR_KEY]) + logger.error(data[ERROR_KEY]) self._login_problem_provider() @QtCore.Slot() @@ -1277,11 +1292,11 @@ class MainWindow(QtGui.QMainWindow): if MX_SERVICE in self._enabled_services: btn_enabled = self._login_widget.set_logout_btn_enabled btn_enabled(False) - sig = self._backend.signaler + sig = self._leap_signaler sig.soledad_bootstrap_failed.connect(lambda: btn_enabled(True)) sig.soledad_bootstrap_finished.connect(lambda: btn_enabled(True)) - if not MX_SERVICE in self._provider_details['services']: + if MX_SERVICE not in self._provider_details['services']: self._set_mx_visible(False) def _start_eip_bootstrap(self): @@ -1316,7 +1331,7 @@ class MainWindow(QtGui.QMainWindow): """ domain = self._login_widget.get_selected_provider() lang = QtCore.QLocale.system().name() - self._backend.provider_get_details(domain, lang) + self._backend.provider_get_details(domain=domain, lang=lang) @QtCore.Slot() def _provider_get_details(self, details): @@ -1385,11 +1400,14 @@ class MainWindow(QtGui.QMainWindow): # this is mostly for internal use/debug for now. logger.warning("Sorry! Log-in at least one time.") return - self._backend.soledad_load_offline(full_user_id, password, uuid) + self._backend.soledad_load_offline(username=full_user_id, + password=password, uuid=uuid) else: if self._logged_user is not None: domain = self._login_widget.get_selected_provider() - self._backend.soledad_bootstrap(username, domain, password) + self._backend.soledad_bootstrap(username=username, + domain=domain, + password=password) ################################################################### # Service control methods: soledad @@ -1468,14 +1486,27 @@ class MainWindow(QtGui.QMainWindow): signal that currently is beeing processed under status_panel. After the refactor to EIPConductor this should not be necessary. """ - domain = self._login_widget.get_selected_provider() + self._already_started_eip = True - self._eip_status.set_provider(domain) + domain = self._login_widget.get_selected_provider() self._settings.set_defaultprovider(domain) - self._already_started_eip = True + + self._backend.eip_get_gateway_country_code(domain=domain) # check for connectivity - self._backend.eip_check_dns(domain) + self._backend.eip_check_dns(domain=domain) + + @QtCore.Slot() + def _set_eip_provider(self, country_code=None): + """ + TRIGGERS: + Signaler.eip_get_gateway_country_code + Signaler.eip_no_gateway + + Set the current provider and country code in the eip status widget. + """ + domain = self._login_widget.get_selected_provider() + self._eip_status.set_provider(domain, country_code) @QtCore.Slot() def _eip_dns_error(self): @@ -1539,7 +1570,7 @@ class MainWindow(QtGui.QMainWindow): self._eip_status.eip_button.setEnabled(False) domain = self._login_widget.get_selected_provider() - self._backend.eip_setup(domain) + self._backend.eip_setup(provider=domain) self._already_started_eip = True # we want to start soledad anyway after a certain timeout if eip @@ -1571,12 +1602,12 @@ class MainWindow(QtGui.QMainWindow): Start the VPN thread if the eip configuration is properly loaded. """ - passed = data[self._backend.PASSED_KEY] + passed = data[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._backend.ERROR_KEY]) + logger.error(data[ERROR_KEY]) self._already_started_eip = False return @@ -1594,11 +1625,11 @@ class MainWindow(QtGui.QMainWindow): This is used for intermediate bootstrapping stages, in case they fail. """ - passed = data[self._backend.PASSED_KEY] + passed = data[PASSED_KEY] if not passed: self._login_widget.set_status( self.tr("Unable to connect: Problem with provider")) - logger.error(data[self._backend.ERROR_KEY]) + logger.error(data[ERROR_KEY]) self._already_started_eip = False self._eip_status.aborted() @@ -1663,9 +1694,9 @@ class MainWindow(QtGui.QMainWindow): This is used for intermediate bootstrapping stages, in case they fail. """ - passed = data[self._backend.PASSED_KEY] + passed = data[PASSED_KEY] if not passed: - logger.error(data[self._backend.ERROR_KEY]) + logger.error(data[ERROR_KEY]) self._login_problem_provider() # @@ -1708,13 +1739,13 @@ class MainWindow(QtGui.QMainWindow): self._cancel_ongoing_defers() - self._services_being_stopped = {'imap', 'eip'} + self._services_being_stopped = set(('imap', 'eip')) imap_stopped = lambda: self._remove_service('imap') - self._backend.signaler.imap_stopped.connect(imap_stopped) + self._leap_signaler.imap_stopped.connect(imap_stopped) eip_stopped = lambda: self._remove_service('eip') - self._backend.signaler.eip_stopped.connect(eip_stopped) + self._leap_signaler.eip_stopped.connect(eip_stopped) logger.debug('Stopping mail services') self._backend.imap_stop_service() @@ -1732,8 +1763,10 @@ class MainWindow(QtGui.QMainWindow): Start the quit sequence and wait for services to finish. Cleanup and close the main window before quitting. """ - # TODO separate the shutting down of services from the - # UI stuff. + if self._quitting: + return + + self._quitting = True # first thing to do quitting, hide the mainwindow and show tooltip. self.hide() @@ -1786,8 +1819,6 @@ class MainWindow(QtGui.QMainWindow): Final steps to quit the app, starting from here we don't care about running services or user interaction, just quitting. """ - logger.debug('Final quit...') - # We can reach here because all the services are stopped or because a # timeout was triggered. Since we want to run this only once, we exit # if this is called twice. @@ -1796,12 +1827,17 @@ class MainWindow(QtGui.QMainWindow): self._finally_quitting = True + logger.debug('Closing soledad...') + self._backend.soledad_close() + logger.debug('Final quit...') + + self._leap_signaler.stop() + self._backend.stop() + time.sleep(0.05) # give the thread a little time to finish. + # Remove lockfiles on a clean shutdown. logger.debug('Cleaning pidfiles') if IS_WIN: WindowsLock.release_all_locks() - self._backend.stop() self.close() - - QtDelayedCall(100, twisted_main.quit) diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index a3b81d38..3c9cd5d0 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -38,7 +38,8 @@ class PreferencesWindow(QtGui.QDialog): """ preferences_saved = QtCore.Signal() - def __init__(self, parent, username, domain, backend, soledad_started, mx): + def __init__(self, parent, username, domain, backend, soledad_started, mx, + leap_signaler): """ :param parent: parent object of the PreferencesWindow. :parent type: QWidget @@ -58,6 +59,7 @@ class PreferencesWindow(QtGui.QDialog): self._username = username self._domain = domain + self._leap_signaler = leap_signaler self._backend = backend self._soledad_started = soledad_started self._mx_provided = mx @@ -194,7 +196,8 @@ class PreferencesWindow(QtGui.QDialog): return self._set_changing_password(True) - self._backend.user_change_password(current_password, new_password) + self._backend.user_change_password(current_password=current_password, + new_password=new_password) @QtCore.Slot() def _srp_change_password_ok(self): @@ -208,7 +211,7 @@ class PreferencesWindow(QtGui.QDialog): logger.debug("SRP password changed successfully.") if self._mx_provided: - self._backend.soledad_change_password(new_password) + self._backend.soledad_change_password(new_password=new_password) else: self._change_password_success() @@ -357,7 +360,7 @@ class PreferencesWindow(QtGui.QDialog): save_services = partial(self._save_enabled_services, domain) self.ui.pbSaveServices.clicked.connect(save_services) - self._backend.provider_get_supported_services(domain) + self._backend.provider_get_supported_services(domain=domain) @QtCore.Slot(str) def _load_services(self, services): @@ -423,7 +426,7 @@ class PreferencesWindow(QtGui.QDialog): """ Helper to connect to backend signals """ - sig = self._backend.signaler + sig = self._leap_signaler sig.prov_get_supported_services.connect(self._load_services) diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py index 00a1387e..91f1f605 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -240,9 +240,9 @@ class CompositeMachine(QStateMachine): c2.qtsigs.disconnected_signal.connect(self.off_ev2_slot) # XXX why is this getting deletec in c++? - #Traceback (most recent call last): - #self.postEvent(self.events.on_ev2) - #RuntimeError: Internal C++ object (ConnectedEvent2) already deleted. + # Traceback (most recent call last): + # self.postEvent(self.events.on_ev2) + # RuntimeError: Internal C++ object (ConnectedEvent2) already deleted. # XXX trying the following workaround, since # I cannot find why in the world this is getting deleted :( # XXX refactor! @@ -318,9 +318,8 @@ class ConnectionMachineBuilder(object): components = self._conn.components if components is None: - # simple case: connection definition inherits directly from - # the abstract connection. - + # simple case: connection definition inherits directly from + # the abstract connection. leap_assert_type(self._conn, connections.AbstractLEAPConnection) return self._make_simple_machine(self._conn, **kwargs) diff --git a/src/leap/bitmask/gui/twisted_main.py b/src/leap/bitmask/gui/twisted_main.py deleted file mode 100644 index b1ce0ead..00000000 --- a/src/leap/bitmask/gui/twisted_main.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# twisted_main.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -""" -Main functions for integration of twisted reactor -""" -import logging - -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(): - """ - Stop the mainloop. - """ - reactor.callLater(0, stop) diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index f66c553d..be5bde52 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -24,6 +24,10 @@ from functools import partial from PySide import QtCore, QtGui +# TODO: we should use a more granular signaling instead of passing error/ok as +# a result. +from leap.bitmask.backend.leapbackend import ERROR_KEY, PASSED_KEY + from leap.bitmask.config import flags from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.services import get_service_display_name, get_supported @@ -49,16 +53,12 @@ class Wizard(QtGui.QWizard): REGISTER_USER_PAGE = 4 SERVICES_PAGE = 5 - def __init__(self, backend, bypass_checks=False): + def __init__(self, backend, leap_signaler): """ Constructor for the main Wizard. :param backend: Backend being used :type backend: Backend - :param bypass_checks: Set to true if the app should bypass - first round of checks for CA - certificates at bootstrap - :type bypass_checks: bool """ QtGui.QWizard.__init__(self) @@ -83,7 +83,10 @@ class Wizard(QtGui.QWizard): self.ui.grpCheckProvider.setVisible(False) self._connect_and_track(self.ui.btnCheck.clicked, self._check_provider) - self._connect_and_track(self.ui.lnProvider.returnPressed, self._check_provider) + self._connect_and_track(self.ui.lnProvider.returnPressed, + self._check_provider) + + self._leap_signaler = leap_signaler self._backend = backend self._backend_connect() @@ -98,22 +101,24 @@ class Wizard(QtGui.QWizard): self._provider_select_defer = None self._provider_setup_defer = None - self._connect_and_track(self.currentIdChanged, self._current_id_changed) + self._connect_and_track(self.currentIdChanged, + self._current_id_changed) - self._connect_and_track(self.ui.lnProvider.textChanged, self._enable_check) + self._connect_and_track(self.ui.lnProvider.textChanged, + self._enable_check) self._connect_and_track(self.ui.rbNewProvider.toggled, - lambda x: self._enable_check()) + lambda x: self._enable_check()) self._connect_and_track(self.ui.cbProviders.currentIndexChanged[int], - self._reset_provider_check) + self._reset_provider_check) self._connect_and_track(self.ui.lblUser.returnPressed, - self._focus_password) + self._focus_password) self._connect_and_track(self.ui.lblPassword.returnPressed, - self._focus_second_password) + self._focus_second_password) self._connect_and_track(self.ui.lblPassword2.returnPressed, - self._register) + self._register) self._connect_and_track(self.ui.btnRegister.clicked, - self._register) + self._register) self._connect_and_track(self.ui.rbExistingProvider.toggled, self._skip_provider_checks) @@ -184,21 +189,55 @@ class Wizard(QtGui.QWizard): :param pinned: list of pinned providers :type pinned: list of str + + + How the combobox items are arranged: + ----------------------------------- + + First run: + + demo.bitmask.net + -- + pinned2.org + pinned1.org + pinned3.org + + After some usage: + + added-by-user.org + pinned-but-then-used.org + --- + demo.bitmask.net + pinned1.org + pinned3.org + pinned2.org + + In other words: + * There are two sections. + * Section one consists of all the providers that the user has used. + If this is empty, than use demo.bitmask.net for this section. + This list is sorted alphabetically. + * Section two consists of all the pinned or 'pre seeded' providers, + minus any providers that are now in section one. This last list + is in random order. """ ls = LeapSettings() - providers = ls.get_configured_providers() - if not providers and not pinned: + user_added = ls.get_configured_providers() + if not user_added and not pinned: self.ui.rbExistingProvider.setEnabled(False) self.ui.label_8.setEnabled(False) # 'https://' label self.ui.cbProviders.setEnabled(False) return - user_added = [] + user_added.sort() + + if not user_added: + user_added = [pinned.pop(0)] - # separate pinned providers from user added ones - for p in providers: - if p not in pinned: - user_added.append(p) + # separate unused pinned providers from user added ones + for p in user_added: + if p in pinned: + pinned.remove(p) if user_added: self.ui.cbProviders.addItems(user_added) @@ -288,7 +327,8 @@ class Wizard(QtGui.QWizard): if user_ok and pass_ok: self._set_register_status(self.tr("Starting registration...")) - self._backend.user_register(self._domain, username, password) + self._backend.user_register(provider=self._domain, + username=username, password=password) self._username = username self._password = password else: @@ -440,7 +480,7 @@ class Wizard(QtGui.QWizard): self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) self._provider_select_defer = self._backend.\ - provider_setup(self._domain) + provider_setup(provider=self._domain) @QtCore.Slot(bool) def _skip_provider_checks(self, skip): @@ -475,8 +515,8 @@ class Wizard(QtGui.QWizard): :param complete_page: page id to complete :type complete_page: int """ - passed = data[self._backend.PASSED_KEY] - error = data[self._backend.ERROR_KEY] + passed = data[PASSED_KEY] + error = data[ERROR_KEY] if passed: label.setPixmap(self.OK_ICON) if complete: @@ -496,7 +536,7 @@ class Wizard(QtGui.QWizard): """ self._complete_task(data, self.ui.lblNameResolution) status = "" - passed = data[self._backend.PASSED_KEY] + passed = data[PASSED_KEY] if not passed: status = self.tr("<font color='red'><b>Non-existent " "provider</b></font>") @@ -516,10 +556,10 @@ class Wizard(QtGui.QWizard): """ self._complete_task(data, self.ui.lblHTTPS) status = "" - passed = data[self._backend.PASSED_KEY] + passed = data[PASSED_KEY] if not passed: status = self.tr("<font color='red'><b>%s</b></font>") \ - % (data[self._backend.ERROR_KEY]) + % (data[ERROR_KEY]) self.ui.lblProviderSelectStatus.setText(status) else: self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) @@ -536,22 +576,21 @@ class Wizard(QtGui.QWizard): check. Since this check is the last of this set, it also completes the page if passed """ - if data[self._backend.PASSED_KEY]: + if data[PASSED_KEY]: self._complete_task(data, self.ui.lblProviderInfo, True, self.SELECT_PROVIDER_PAGE) self._provider_checks_ok = True lang = QtCore.QLocale.system().name() - self._backend.provider_get_details(self._domain, lang) + self._backend.provider_get_details(domain=self._domain, lang=lang) else: new_data = { - self._backend.PASSED_KEY: False, - self._backend.ERROR_KEY: - self.tr("Unable to load provider configuration") + PASSED_KEY: False, + ERROR_KEY: self.tr("Unable to load provider configuration") } self._complete_task(new_data, self.ui.lblProviderInfo) status = "" - if not data[self._backend.PASSED_KEY]: + if not data[PASSED_KEY]: status = self.tr("<font color='red'><b>Not a valid provider" "</b></font>") self.ui.lblProviderSelectStatus.setText(status) @@ -582,7 +621,7 @@ class Wizard(QtGui.QWizard): Sets the status for the download of the CA certificate check """ self._complete_task(data, self.ui.lblDownloadCaCert) - passed = data[self._backend.PASSED_KEY] + passed = data[PASSED_KEY] if passed: self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON) @@ -595,7 +634,7 @@ class Wizard(QtGui.QWizard): Sets the status for the CA fingerprint check """ self._complete_task(data, self.ui.lblCheckCaFpr) - passed = data[self._backend.PASSED_KEY] + passed = data[PASSED_KEY] if passed: self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON) @@ -689,7 +728,7 @@ class Wizard(QtGui.QWizard): self.page(pageId).setSubTitle(sub_title) self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON) self._provider_setup_defer = self._backend.\ - provider_bootstrap(self._domain) + provider_bootstrap(provider=self._domain) if pageId == self.PRESENT_PROVIDER_PAGE: sub_title = self.tr("Description of services offered by {0}") @@ -752,7 +791,7 @@ class Wizard(QtGui.QWizard): """ Connects all the backend signals with the wizard. """ - sig = self._backend.signaler + sig = self._leap_signaler conntrack = self._connect_and_track conntrack(sig.prov_name_resolution, self._name_resolution) conntrack(sig.prov_https_connection, self._https_connection) diff --git a/src/leap/bitmask/platform_init/initializers.py b/src/leap/bitmask/platform_init/initializers.py index 384e1ec1..e4d6f9b3 100644 --- a/src/leap/bitmask/platform_init/initializers.py +++ b/src/leap/bitmask/platform_init/initializers.py @@ -21,7 +21,6 @@ import logging import os import platform import stat -import sys import subprocess import tempfile @@ -103,7 +102,7 @@ def get_missing_helpers_dialog(): msg.setWindowTitle(msg.tr("Missing helper files")) msg.setText(msg.tr(WE_NEED_POWERS)) # but maybe the user really deserve to know more - #msg.setInformativeText(msg.tr(BECAUSE)) + # msg.setInformativeText(msg.tr(BECAUSE)) msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) msg.addButton("No, don't ask again", QtGui.QMessageBox.RejectRole) msg.setDefaultButton(QtGui.QMessageBox.Yes) @@ -373,30 +372,6 @@ def DarwinInitializer(): # Linux initializers # -def _get_missing_resolvconf_dialog(): - """ - Create a dialog for notifying about missing openresolv. - - :rtype: QtGui.QMessageBox instance - """ - msgstr = QtCore.QObject() - msgstr.NO_RESOLVCONF = msgstr.tr( - "Could not find <b>resolvconf</b> installed in your system.\n" - "Do you want to quit Bitmask now?") - - msgstr.EXPLAIN = msgstr.tr( - "Encrypted Internet needs resolvconf installed to work properly.\n" - "Please use your package manager to install it.\n") - - msg = QtGui.QMessageBox() - msg.setWindowTitle(msg.tr("Missing resolvconf framework")) - msg.setText(msgstr.NO_RESOLVCONF) - # but maybe the user really deserve to know more - msg.setInformativeText(msgstr.EXPLAIN) - msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) - msg.setDefaultButton(QtGui.QMessageBox.Yes) - return msg - def _get_missing_complain_dialog(stuff): """ @@ -445,21 +420,6 @@ def _get_missing_complain_dialog(stuff): return msg -def _linux_check_resolvconf(): - """ - Raise a dialog warning about the lack of the resolvconf framework. - """ - RESOLVCONF_PATH = "/sbin/resolvconf" - missing = not os.path.isfile(RESOLVCONF_PATH) - - if missing: - msg = _get_missing_resolvconf_dialog() - ret = msg.exec_() - - if ret == QtGui.QMessageBox.Yes: - sys.exit() - - def _linux_install_missing_scripts(badexec, notfound): """ Try to install the missing helper files. @@ -509,9 +469,8 @@ def LinuxInitializer(): """ Raise a dialog if needed files are missing. - Missing files can be either system-wide resolvconf, bitmask-root, or - policykit file. The dialog will also be raised if some of those files are + Missing files can be either bitmask-root policykit file. + The dialog will also be raised if some of those files are found to have incorrect permissions. """ - _linux_check_resolvconf() check_missing() diff --git a/src/leap/bitmask/provider/pinned.py b/src/leap/bitmask/provider/pinned.py index 38851621..6fd2fa70 100644 --- a/src/leap/bitmask/provider/pinned.py +++ b/src/leap/bitmask/provider/pinned.py @@ -32,6 +32,7 @@ class PinnedProviders(object): CONFIG_KEY = "config" CACERT_KEY = "cacert" + PREFERRED_PROVIDER = pinned_demobitmask.DOMAIN PROVIDERS = { pinned_demobitmask.DOMAIN: { @@ -50,11 +51,16 @@ class PinnedProviders(object): @classmethod def domains(self): """ - Return the domains that are pinned in here + Return the domains that are pinned in here. + The first domain in the list is the preferred one. :rtype: list of str """ - return self.PROVIDERS.keys() + domains = self.PROVIDERS.keys() + domains.remove(self.PREFERRED_PROVIDER) + domains.insert(0, self.PREFERRED_PROVIDER) + + return domains @classmethod def save_hardcoded(self, domain, provider_path, cacert_path): diff --git a/src/leap/bitmask/provider/providerbootstrapper.py b/src/leap/bitmask/provider/providerbootstrapper.py index 8c96a8b5..71edbb87 100644 --- a/src/leap/bitmask/provider/providerbootstrapper.py +++ b/src/leap/bitmask/provider/providerbootstrapper.py @@ -90,7 +90,7 @@ class ProviderBootstrapper(AbstractBootstrapper): self._provider_config = None self._download_if_needed = False if signaler is not None: - self._cancel_signal = signaler.PROV_CANCELLED_SETUP + self._cancel_signal = signaler.prov_cancelled_setup @property def verify(self): @@ -194,8 +194,8 @@ class ProviderBootstrapper(AbstractBootstrapper): verify = self.verify if mtime: # the provider.json exists - # So, we're getting it from the api.* and checking against - # the provider ca. + # So, we're getting it from the api.* and checking against + # the provider ca. try: provider_config = ProviderConfig() provider_config.load(provider_json) @@ -228,7 +228,7 @@ class ProviderBootstrapper(AbstractBootstrapper): # TODO split if not provider.supports_client(min_client_version): self._signaler.signal( - self._signaler.PROV_UNSUPPORTED_CLIENT) + self._signaler.prov_unsupported_client) raise UnsupportedClientVersionError() provider_definition, mtime = get_content(res) @@ -250,7 +250,7 @@ class ProviderBootstrapper(AbstractBootstrapper): 'Found: {1}.').format(api_supported, api_version) logger.error(error) - self._signaler.signal(self._signaler.PROV_UNSUPPORTED_API) + self._signaler.signal(self._signaler.prov_unsupported_api) raise UnsupportedProviderAPI(error) def run_provider_select_checks(self, domain, download_if_needed=False): @@ -271,10 +271,10 @@ class ProviderBootstrapper(AbstractBootstrapper): cb_chain = [ (self._check_name_resolution, - self._signaler.PROV_NAME_RESOLUTION_KEY), - (self._check_https, self._signaler.PROV_HTTPS_CONNECTION_KEY), + self._signaler.prov_name_resolution), + (self._check_https, self._signaler.prov_https_connection), (self._download_provider_info, - self._signaler.PROV_DOWNLOAD_PROVIDER_INFO_KEY) + self._signaler.prov_download_provider_info) ] return self.addCallbackChain(cb_chain) @@ -401,11 +401,11 @@ class ProviderBootstrapper(AbstractBootstrapper): self._download_if_needed = download_if_needed cb_chain = [ - (self._download_ca_cert, self._signaler.PROV_DOWNLOAD_CA_CERT_KEY), + (self._download_ca_cert, self._signaler.prov_download_ca_cert), (self._check_ca_fingerprint, - self._signaler.PROV_CHECK_CA_FINGERPRINT_KEY), + self._signaler.prov_check_ca_fingerprint), (self._check_api_certificate, - self._signaler.PROV_CHECK_API_CERTIFICATE_KEY) + self._signaler.prov_check_api_certificate) ] return self.addCallbackChain(cb_chain) diff --git a/src/leap/bitmask/services/eip/conductor.py b/src/leap/bitmask/services/eip/conductor.py index a8821160..bb07809a 100644 --- a/src/leap/bitmask/services/eip/conductor.py +++ b/src/leap/bitmask/services/eip/conductor.py @@ -33,7 +33,7 @@ logger = logging.getLogger(__name__) class EIPConductor(object): - def __init__(self, settings, backend, **kwargs): + def __init__(self, settings, backend, leap_signaler, **kwargs): """ Initializes EIP Conductor. @@ -46,6 +46,7 @@ class EIPConductor(object): self.eip_connection = EIPConnection() self.eip_name = get_service_display_name(EIP_SERVICE) self._settings = settings + self._leap_signaler = leap_signaler self._backend = backend self._eip_status = None @@ -76,7 +77,7 @@ class EIPConductor(object): """ Connect to backend signals. """ - signaler = self._backend.signaler + signaler = self._leap_signaler # for conductor signaler.eip_process_restart_tls.connect(self._do_eip_restart) @@ -201,7 +202,7 @@ class EIPConductor(object): # we bypass the on_eip_disconnected here plug_restart_on_disconnected() self.qtsigs.disconnected_signal.emit() - #QtDelayedCall(0, self.qtsigs.disconnected_signal.emit) + # QtDelayedCall(0, self.qtsigs.disconnected_signal.emit) # ...and reconnect the original signal again, after having used the # diversion QtDelayedCall(500, reconnect_disconnected_signal) @@ -300,7 +301,7 @@ class EIPConductor(object): # XXX FIXME --- check exitcode is != 0 really. # bitmask-root is masking the exitcode, so we might need # to fix it on that side. - #if exitCode != 0 and not self.user_stopped_eip: + # if exitCode != 0 and not self.user_stopped_eip: if not self.user_stopped_eip: eip_status_label = self._eip_status.tr( "{0} finished in an unexpected manner!") diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py index 41d75052..f83e0170 100644 --- a/src/leap/bitmask/services/eip/darwinvpnlauncher.py +++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py @@ -46,7 +46,9 @@ class DarwinVPNLauncher(VPNLauncher): INSTALL_MSG = ("\"Bitmask needs administrative privileges to install " "missing scripts and fix permissions.\"") - INSTALL_PATH = os.path.realpath(os.getcwd() + "/../../") + # Hardcode the installation path for OSX for security, openvpn is + # run as root + INSTALL_PATH = "/Applications/Bitmask.app/" INSTALL_PATH_ESCAPED = os.path.realpath(os.getcwd() + "/../../") OPENVPN_BIN = 'openvpn.leap' OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,) diff --git a/src/leap/bitmask/services/eip/eipbootstrapper.py b/src/leap/bitmask/services/eip/eipbootstrapper.py index c77977ce..264eac2e 100644 --- a/src/leap/bitmask/services/eip/eipbootstrapper.py +++ b/src/leap/bitmask/services/eip/eipbootstrapper.py @@ -53,7 +53,7 @@ class EIPBootstrapper(AbstractBootstrapper): self._eip_config = None self._download_if_needed = False if signaler is not None: - self._cancel_signal = signaler.EIP_CANCELLED_SETUP + self._cancel_signal = signaler.eip_cancelled_setup def _download_config(self, *args): """ @@ -116,9 +116,9 @@ class EIPBootstrapper(AbstractBootstrapper): self._download_if_needed = download_if_needed cb_chain = [ - (self._download_config, self._signaler.EIP_CONFIG_READY), + (self._download_config, self._signaler.eip_config_ready), (self._download_client_certificates, - self._signaler.EIP_CLIENT_CERTIFICATE_READY) + 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 8ec0c050..b6e47f25 100644 --- a/src/leap/bitmask/services/eip/linuxvpnlauncher.py +++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py @@ -74,6 +74,7 @@ def _is_auth_agent_running(): 'ps aux | grep "polkit-[m]ate-authentication-agent-1"', 'ps aux | grep "[l]xpolkit"', 'ps aux | grep "[g]nome-shell"', + 'ps aux | grep "[f]ingerprint-polkit-agent"', ] is_running = [commands.getoutput(cmd) for cmd in polkit_options] @@ -126,12 +127,6 @@ class LinuxVPNLauncher(VPNLauncher): # LinuxPolicyChecker will give us the right path if standalone. return LinuxPolicyChecker.get_polkit_path() - class RESOLVCONF_BIN_PATH(object): - def __call__(self): - return ("/usr/local/sbin/leap-resolvconf" if flags.STANDALONE else - "/sbin/resolvconf") - # this only will work with debian/ubuntu distros. - OTHER_FILES = (POLKIT_PATH, BITMASK_ROOT, OPENVPN_BIN_PATH) @classmethod diff --git a/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py b/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py index 6640a860..1888f2c9 100644 --- a/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py +++ b/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py @@ -30,7 +30,7 @@ import time try: import unittest2 as unittest except ImportError: - import unittest + import unittest # noqa - skip 'unused import' warning from nose.twistedtools import deferred, reactor from twisted.internet import threads diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py index 0731bee3..72e19413 100644 --- a/src/leap/bitmask/services/eip/vpnlauncher.py +++ b/src/leap/bitmask/services/eip/vpnlauncher.py @@ -27,7 +27,7 @@ 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.backend.settings import Settings, GATEWAY_AUTOMATIC from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.platform_init import IS_LINUX from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector @@ -122,12 +122,12 @@ class VPNLauncher(object): :rtype: list """ gateways = [] - leap_settings = LeapSettings() + settings = Settings() domain = providerconfig.get_domain() - gateway_conf = leap_settings.get_selected_gateway(domain) + gateway_conf = settings.get_selected_gateway(domain) gateway_selector = VPNGatewaySelector(eipconfig) - if gateway_conf == leap_settings.GATEWAY_AUTOMATIC: + if gateway_conf == GATEWAY_AUTOMATIC: gateways = gateway_selector.get_gateways() else: gateways = [gateway_conf] @@ -136,12 +136,6 @@ class VPNLauncher(object): logger.error('No gateway was found!') raise VPNLauncherException('No gateway was found!') - # this only works for selecting the first gateway, as we're - # currently doing. - ccodes = gateway_selector.get_gateways_country_code() - gateway_ccode = ccodes[gateways[0]] - flags.CURRENT_VPN_COUNTRY = gateway_ccode - logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) return gateways @@ -175,11 +169,11 @@ class VPNLauncher(object): leap_assert_type(providerconfig, ProviderConfig) # 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) + # kwargs = {} + # openvpn_possibilities = which(kls.OPENVPN_BIN, **kwargs) + # if not openvpn_possibilities: + # raise OpenVPNNotFoundException() + # openvpn = first(openvpn_possibilities) # ----------------------------------------- openvpn_path = force_eval(kls.OPENVPN_BIN_PATH) diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index b54f2925..d1a3fdaa 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -118,10 +118,10 @@ class VPNObserver(object): """ sig = self._signaler signals = { - "network_unreachable": sig.EIP_NETWORK_UNREACHABLE, - "process_restart_tls": sig.EIP_PROCESS_RESTART_TLS, - "process_restart_ping": sig.EIP_PROCESS_RESTART_PING, - "initialization_completed": sig.EIP_CONNECTED + "network_unreachable": sig.eip_network_unreachable, + "process_restart_tls": sig.eip_process_restart_tls, + "process_restart_ping": sig.eip_process_restart_ping, + "initialization_completed": sig.eip_connected } return signals.get(event.lower()) @@ -255,6 +255,9 @@ class VPN(object): """ Tear the firewall down using the privileged wrapper. """ + if IS_MAC: + # We don't support Mac so far + return True BM_ROOT = force_eval(linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT) exitCode = subprocess.call(["pkexec", BM_ROOT, "firewall", "stop"]) @@ -594,7 +597,7 @@ class VPNManager(object): state = status_step if state != self._last_state: - self._signaler.signal(self._signaler.EIP_STATE_CHANGED, state) + self._signaler.signal(self._signaler.eip_state_changed, state) self._last_state = state def _parse_status_and_notify(self, output): @@ -632,7 +635,7 @@ class VPNManager(object): status = (tun_tap_read, tun_tap_write) if status != self._last_status: - self._signaler.signal(self._signaler.EIP_STATUS_CHANGED, status) + self._signaler.signal(self._signaler.eip_status_changed, status) self._last_status = status def get_state(self): @@ -814,7 +817,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): leap_assert_type(eipconfig, EIPConfig) leap_assert_type(providerconfig, ProviderConfig) - #leap_assert(not self.isRunning(), "Starting process more than once!") + # leap_assert(not self.isRunning(), "Starting process more than once!") self._eipconfig = eipconfig self._providerconfig = providerconfig @@ -869,7 +872,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): if isinstance(exit_code, int): logger.debug("processExited, status %d" % (exit_code,)) self._signaler.signal( - self._signaler.EIP_PROCESS_FINISHED, exit_code) + self._signaler.eip_process_finished, exit_code) self._alive = False def processEnded(self, reason): diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py index 98b40929..5e85368f 100644 --- a/src/leap/bitmask/services/mail/conductor.py +++ b/src/leap/bitmask/services/mail/conductor.py @@ -64,7 +64,8 @@ class IMAPControl(object): """ Start imap service. """ - self._backend.imap_start_service(self.userid, flags.OFFLINE) + self._backend.imap_start_service(full_user_id=self.userid, + offline=flags.OFFLINE) def stop_imap_service(self): """ @@ -146,7 +147,8 @@ class SMTPControl(object): :type download_if_needed: bool """ self.smtp_connection.qtsigs.connecting_signal.emit() - self._backend.smtp_start_service(self.userid, download_if_needed) + self._backend.smtp_start_service(full_user_id=self.userid, + download_if_needed=download_if_needed) def stop_smtp_service(self): """ diff --git a/src/leap/bitmask/services/mail/plumber.py b/src/leap/bitmask/services/mail/plumber.py index c16a1fed..1af65c5d 100644 --- a/src/leap/bitmask/services/mail/plumber.py +++ b/src/leap/bitmask/services/mail/plumber.py @@ -26,7 +26,7 @@ from functools import partial from twisted.internet import defer -from leap.bitmask.config.leapsettings import LeapSettings +from leap.bitmask.backend.settings import Settings from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.provider import get_provider_path from leap.bitmask.services.soledad.soledadbootstrapper import get_db_paths @@ -83,7 +83,8 @@ def initialize_soledad(uuid, email, passwd, secrets, localdb, server_url, - cert_file) + cert_file, + defer_encryption=True) return soledad @@ -113,7 +114,7 @@ class MBOXPlumber(object): self.user = user self.mdir = mdir self.sol = None - self._settings = LeapSettings() + self._settings = Settings() provider_config_path = os.path.join(get_path_prefix(), get_provider_path(provider)) @@ -231,8 +232,8 @@ class MBOXPlumber(object): with open(mail_filename) as f: mail_string = f.read() - #uid = self._mbox.getUIDNext() - #print "saving with UID: %s" % uid + # uid = self._mbox.getUIDNext() + # print "saving with UID: %s" % uid d = self._mbox.messages.add_msg( mail_string, notify_on_disk=True) return d diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index db12fd80..c4e43bfe 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -21,6 +21,7 @@ import logging import os import socket import sys +import time from ssl import SSLError from sqlite3 import ProgrammingError as sqlite_ProgrammingError @@ -132,12 +133,15 @@ class SoledadBootstrapper(AbstractBootstrapper): MAX_INIT_RETRIES = 10 MAX_SYNC_RETRIES = 10 + WAIT_MAX_SECONDS = 600 + # WAIT_STEP_SECONDS = 1 + WAIT_STEP_SECONDS = 5 def __init__(self, signaler=None): AbstractBootstrapper.__init__(self, signaler) if signaler is not None: - self._cancel_signal = signaler.SOLEDAD_CANCELLED_BOOTSTRAP + self._cancel_signal = signaler.soledad_cancelled_bootstrap self._provider_config = None self._soledad_config = None @@ -181,17 +185,16 @@ class SoledadBootstrapper(AbstractBootstrapper): :param uuid: the user uuid :type uuid: str or unicode """ - print "UUID ", uuid self._address = username self._password = password self._uuid = uuid try: self.load_and_sync_soledad(uuid, offline=True) - self._signaler.signal(self._signaler.SOLEDAD_OFFLINE_FINISHED) + self._signaler.signal(self._signaler.soledad_offline_finished) except Exception as e: # TODO: we should handle more specific exceptions in here logger.exception(e) - self._signaler.signal(self._signaler.SOLEDAD_OFFLINE_FAILED) + self._signaler.signal(self._signaler.soledad_offline_failed) def _get_soledad_local_params(self, uuid, offline=False): """ @@ -356,12 +359,20 @@ class SoledadBootstrapper(AbstractBootstrapper): Do several retries to get an initial soledad sync. """ # and now, let's sync - sync_tries = 1 - while sync_tries <= self.MAX_SYNC_RETRIES: + sync_tries = self.MAX_SYNC_RETRIES + step = self.WAIT_STEP_SECONDS + max_wait = self.WAIT_MAX_SECONDS + while sync_tries > 0: + wait = 0 try: logger.debug("Trying to sync soledad....") self._try_soledad_sync() - logger.debug("Soledad has been synced.") + while self.soledad.syncing: + time.sleep(step) + wait += step + if wait >= max_wait: + raise SoledadSyncError("timeout!") + logger.debug("Soledad has been synced!") # so long, and thanks for all the fish return except SoledadSyncError: @@ -379,9 +390,10 @@ class SoledadBootstrapper(AbstractBootstrapper): continue except InvalidAuthTokenError: self._signaler.signal( - self._signaler.SOLEDAD_INVALID_AUTH_TOKEN) + self._signaler.soledad_invalid_auth_token) raise except Exception as e: + # XXX release syncing lock logger.exception("Unhandled error while syncing " "soledad: %r" % (e,)) break @@ -423,7 +435,8 @@ class SoledadBootstrapper(AbstractBootstrapper): local_db_path=local_db_path.encode(encoding), server_url=server_url, cert_file=cert_file.encode(encoding), - auth_token=auth_token) + auth_token=auth_token, + defer_encryption=True) # XXX All these errors should be handled by soledad itself, # and return a subclass of SoledadInitializationFailed @@ -448,7 +461,10 @@ class SoledadBootstrapper(AbstractBootstrapper): Raises SoledadSyncError if not successful. """ try: - self._soledad.sync() + logger.debug("BOOTSTRAPPER: trying to sync Soledad....") + # pass defer_decryption=False to get inline decryption + # for debugging. + self._soledad.sync(defer_decryption=True) except SSLError as exc: logger.error("%r" % (exc,)) raise SoledadSyncError("Failed to sync soledad") @@ -633,11 +649,11 @@ class SoledadBootstrapper(AbstractBootstrapper): self._password = password if flags.OFFLINE: - signal_finished = self._signaler.SOLEDAD_OFFLINE_FINISHED - signal_failed = self._signaler.SOLEDAD_OFFLINE_FAILED + signal_finished = self._signaler.soledad_offline_finished + signal_failed = self._signaler.soledad_offline_failed else: - signal_finished = self._signaler.SOLEDAD_BOOTSTRAP_FINISHED - signal_failed = self._signaler.SOLEDAD_BOOTSTRAP_FAILED + signal_finished = self._signaler.soledad_bootstrap_finished + signal_failed = self._signaler.soledad_bootstrap_failed try: self._download_config() diff --git a/src/leap/bitmask/services/tests/test_abstractbootstrapper.py b/src/leap/bitmask/services/tests/test_abstractbootstrapper.py index 3ac126ac..c3fda9e1 100644 --- a/src/leap/bitmask/services/tests/test_abstractbootstrapper.py +++ b/src/leap/bitmask/services/tests/test_abstractbootstrapper.py @@ -1,4 +1,4 @@ -## -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- # test_abstrctbootstrapper.py # Copyright (C) 2013 LEAP # diff --git a/src/leap/bitmask/util/__init__.py b/src/leap/bitmask/util/__init__.py index 25b86874..caa94ec7 100644 --- a/src/leap/bitmask/util/__init__.py +++ b/src/leap/bitmask/util/__init__.py @@ -129,3 +129,28 @@ def force_eval(items): return map(do_eval, items) else: return do_eval(items) + + +def dict_to_flags(values): + """ + Set the flags values given in the values dict. + If a value isn't provided then use the already existing one. + + :param values: the values to set. + :type values: dict. + """ + for k, v in values.items(): + setattr(flags, k, v) + + +def flags_to_dict(): + """ + Get the flags values in a dict. + + :return: the values of flags into a dict. + :rtype: dict. + """ + items = [i for i in dir(flags) if i[0] != '_'] + values = {i: getattr(flags, i) for i in items} + + return values diff --git a/src/leap/bitmask/util/leap_argparse.py b/src/leap/bitmask/util/leap_argparse.py index 0717aea5..cbd6d8a5 100644 --- a/src/leap/bitmask/util/leap_argparse.py +++ b/src/leap/bitmask/util/leap_argparse.py @@ -107,19 +107,19 @@ def build_parser(): 'against domains.') # Not in use, we might want to reintroduce them. - #parser.add_argument('-i', '--no-provider-checks', - #action="store_true", default=False, - #help="skips download of provider config files. gets " - #"config from local files only. Will fail if cannot " - #"find any") - #parser.add_argument('-k', '--no-ca-verify', - #action="store_true", default=False, - #help="(insecure). Skips verification of the server " - #"certificate used in TLS handshake.") - #parser.add_argument('-c', '--config', metavar="CONFIG FILE", nargs='?', - #action="store", dest="config_file", - #type=argparse.FileType('r'), - #help='optional config file') + # parser.add_argument('-i', '--no-provider-checks', + # action="store_true", default=False, + # help="skips download of provider config files. gets " + # "config from local files only. Will fail if cannot " + # "find any") + # parser.add_argument('-k', '--no-ca-verify', + # action="store_true", default=False, + # help="(insecure). Skips verification of the server " + # "certificate used in TLS handshake.") + # parser.add_argument('-c', '--config', metavar="CONFIG FILE", nargs='?', + # action="store", dest="config_file", + # type=argparse.FileType('r'), + # help='optional config file') return parser @@ -132,4 +132,9 @@ def get_options(): """ parser = build_parser() opts, unknown = parser.parse_known_args() + + # we add this option manually since it's not defined for 'release version' + if IS_RELEASE_VERSION: + opts.danger = False + return opts diff --git a/src/leap/bitmask/util/polkit_agent.py b/src/leap/bitmask/util/polkit_agent.py index 6fda2f88..7764f571 100644 --- a/src/leap/bitmask/util/polkit_agent.py +++ b/src/leap/bitmask/util/polkit_agent.py @@ -39,9 +39,9 @@ def _launch_agent(): logger.error('Exception while running polkit authentication agent ' '%s' % (exc,)) # XXX fix KDE launch. See: #3755 - #try: - #subprocess.call(KDE_PATH) - #except Exception as exc: + # try: + # subprocess.call(KDE_PATH) + # except Exception as exc: def launch(): diff --git a/src/leap/bitmask/util/privilege_policies.py b/src/leap/bitmask/util/privilege_policies.py index adc3503f..f894d73b 100644 --- a/src/leap/bitmask/util/privilege_policies.py +++ b/src/leap/bitmask/util/privilege_policies.py @@ -87,8 +87,8 @@ class LinuxPolicyChecker(PolicyChecker): else 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. + # 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 |