From 6da8d09846db4d2eed01e488bc6a6f5ba48b959f Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 12 Aug 2013 13:25:44 +0200 Subject: move everything into bitmask namespace --- src/leap/_version.py | 201 --- src/leap/app.py | 224 --- src/leap/bitmask/__init__.py | 6 + src/leap/bitmask/_version.py | 201 +++ src/leap/bitmask/app.py | 224 +++ src/leap/bitmask/config/__init__.py | 0 src/leap/bitmask/config/leapsettings.py | 253 ++++ src/leap/bitmask/config/provider_spec.py | 105 ++ src/leap/bitmask/config/providerconfig.py | 218 +++ .../bitmask/config/tests/test_providerconfig.py | 279 ++++ src/leap/bitmask/crypto/__init__.py | 0 src/leap/bitmask/crypto/srpauth.py | 606 ++++++++ src/leap/bitmask/crypto/srpregister.py | 168 +++ src/leap/bitmask/crypto/tests/__init__.py | 16 + src/leap/bitmask/crypto/tests/eip-service.json | 43 + src/leap/bitmask/crypto/tests/fake_provider.py | 376 +++++ src/leap/bitmask/crypto/tests/openvpn.pem | 33 + src/leap/bitmask/crypto/tests/test_provider.json | 15 + src/leap/bitmask/crypto/tests/test_srpauth.py | 790 ++++++++++ src/leap/bitmask/crypto/tests/test_srpregister.py | 201 +++ src/leap/bitmask/crypto/tests/wrongcert.pem | 33 + src/leap/bitmask/gui/__init__.py | 21 + src/leap/bitmask/gui/loggerwindow.py | 137 ++ src/leap/bitmask/gui/login.py | 245 ++++ src/leap/bitmask/gui/mainwindow.py | 1537 ++++++++++++++++++++ src/leap/bitmask/gui/statuspanel.py | 459 ++++++ src/leap/bitmask/gui/twisted_main.py | 60 + src/leap/bitmask/gui/ui/loggerwindow.ui | 155 ++ src/leap/bitmask/gui/ui/login.ui | 132 ++ src/leap/bitmask/gui/ui/mainwindow.ui | 315 ++++ src/leap/bitmask/gui/ui/statuspanel.ui | 289 ++++ src/leap/bitmask/gui/ui/wizard.ui | 846 +++++++++++ src/leap/bitmask/gui/wizard.py | 626 ++++++++ src/leap/bitmask/gui/wizardpage.py | 40 + src/leap/bitmask/platform_init/__init__.py | 28 + src/leap/bitmask/platform_init/initializers.py | 409 ++++++ src/leap/bitmask/platform_init/locks.py | 405 ++++++ src/leap/bitmask/provider/__init__.py | 0 src/leap/bitmask/provider/supportedapis.py | 38 + src/leap/bitmask/services/__init__.py | 33 + src/leap/bitmask/services/abstractbootstrapper.py | 163 +++ src/leap/bitmask/services/eip/__init__.py | 0 src/leap/bitmask/services/eip/eipbootstrapper.py | 183 +++ src/leap/bitmask/services/eip/eipconfig.py | 263 ++++ src/leap/bitmask/services/eip/eipspec.py | 85 ++ .../bitmask/services/eip/providerbootstrapper.py | 340 +++++ src/leap/bitmask/services/eip/tests/__init__.py | 0 .../services/eip/tests/test_eipbootstrapper.py | 347 +++++ .../bitmask/services/eip/tests/test_eipconfig.py | 324 +++++ .../eip/tests/test_providerbootstrapper.py | 531 +++++++ .../services/eip/tests/test_vpngatewayselector.py | 131 ++ src/leap/bitmask/services/eip/tests/wrongcert.pem | 33 + src/leap/bitmask/services/eip/udstelnet.py | 60 + src/leap/bitmask/services/eip/vpnlaunchers.py | 927 ++++++++++++ src/leap/bitmask/services/eip/vpnprocess.py | 791 ++++++++++ src/leap/bitmask/services/mail/__init__.py | 0 src/leap/bitmask/services/mail/imap.py | 42 + src/leap/bitmask/services/mail/smtpbootstrapper.py | 139 ++ src/leap/bitmask/services/mail/smtpconfig.py | 49 + src/leap/bitmask/services/mail/smtpspec.py | 70 + src/leap/bitmask/services/soledad/__init__.py | 0 .../services/soledad/soledadbootstrapper.py | 265 ++++ src/leap/bitmask/services/soledad/soledadconfig.py | 49 + src/leap/bitmask/services/soledad/soledadspec.py | 76 + src/leap/bitmask/services/tests/__init__.py | 0 .../services/tests/test_abstractbootstrapper.py | 196 +++ src/leap/bitmask/services/tx.py | 46 + src/leap/bitmask/util/__init__.py | 105 ++ src/leap/bitmask/util/constants.py | 19 + src/leap/bitmask/util/keyring_helpers.py | 37 + src/leap/bitmask/util/leap_argparse.py | 72 + src/leap/bitmask/util/leap_log_handler.py | 134 ++ src/leap/bitmask/util/privilege_policies.py | 169 +++ src/leap/bitmask/util/pyside_tests_helper.py | 136 ++ src/leap/bitmask/util/reqs.txt | 14 + src/leap/bitmask/util/request_helpers.py | 58 + src/leap/bitmask/util/requirement_checker.py | 101 ++ src/leap/bitmask/util/streamtologger.py | 59 + src/leap/bitmask/util/tests/__init__.py | 0 .../bitmask/util/tests/test_is_release_version.py | 57 + .../bitmask/util/tests/test_leap_log_handler.py | 118 ++ src/leap/bitmask/util/tests/test_streamtologger.py | 122 ++ src/leap/config/__init__.py | 0 src/leap/config/leapsettings.py | 253 ---- src/leap/config/provider_spec.py | 105 -- src/leap/config/providerconfig.py | 218 --- src/leap/config/tests/test_providerconfig.py | 279 ---- src/leap/crypto/__init__.py | 0 src/leap/crypto/srpauth.py | 606 -------- src/leap/crypto/srpregister.py | 168 --- src/leap/crypto/tests/__init__.py | 16 - src/leap/crypto/tests/eip-service.json | 43 - src/leap/crypto/tests/fake_provider.py | 376 ----- src/leap/crypto/tests/openvpn.pem | 33 - src/leap/crypto/tests/test_provider.json | 15 - src/leap/crypto/tests/test_srpauth.py | 790 ---------- src/leap/crypto/tests/test_srpregister.py | 201 --- src/leap/crypto/tests/wrongcert.pem | 33 - src/leap/gui/__init__.py | 21 - src/leap/gui/loggerwindow.py | 137 -- src/leap/gui/login.py | 245 ---- src/leap/gui/mainwindow.py | 1537 -------------------- src/leap/gui/statuspanel.py | 459 ------ src/leap/gui/twisted_main.py | 60 - src/leap/gui/ui/loggerwindow.ui | 155 -- src/leap/gui/ui/login.ui | 132 -- src/leap/gui/ui/mainwindow.ui | 315 ---- src/leap/gui/ui/statuspanel.ui | 289 ---- src/leap/gui/ui/wizard.ui | 846 ----------- src/leap/gui/wizard.py | 626 -------- src/leap/gui/wizardpage.py | 40 - src/leap/platform_init/__init__.py | 28 - src/leap/platform_init/initializers.py | 409 ------ src/leap/platform_init/locks.py | 405 ------ src/leap/provider/__init__.py | 0 src/leap/provider/supportedapis.py | 38 - src/leap/services/__init__.py | 33 - src/leap/services/abstractbootstrapper.py | 163 --- src/leap/services/eip/__init__.py | 0 src/leap/services/eip/eipbootstrapper.py | 183 --- src/leap/services/eip/eipconfig.py | 263 ---- src/leap/services/eip/eipspec.py | 85 -- src/leap/services/eip/providerbootstrapper.py | 340 ----- src/leap/services/eip/tests/__init__.py | 0 .../services/eip/tests/test_eipbootstrapper.py | 347 ----- src/leap/services/eip/tests/test_eipconfig.py | 324 ----- .../eip/tests/test_providerbootstrapper.py | 531 ------- .../services/eip/tests/test_vpngatewayselector.py | 131 -- src/leap/services/eip/tests/wrongcert.pem | 33 - src/leap/services/eip/udstelnet.py | 60 - src/leap/services/eip/vpnlaunchers.py | 927 ------------ src/leap/services/eip/vpnprocess.py | 791 ---------- src/leap/services/mail/__init__.py | 0 src/leap/services/mail/imap.py | 42 - src/leap/services/mail/smtpbootstrapper.py | 139 -- src/leap/services/mail/smtpconfig.py | 49 - src/leap/services/mail/smtpspec.py | 70 - src/leap/services/soledad/__init__.py | 0 src/leap/services/soledad/soledadbootstrapper.py | 265 ---- src/leap/services/soledad/soledadconfig.py | 49 - src/leap/services/soledad/soledadspec.py | 76 - src/leap/services/tests/__init__.py | 0 .../services/tests/test_abstractbootstrapper.py | 196 --- src/leap/services/tx.py | 46 - src/leap/util/__init__.py | 105 -- src/leap/util/constants.py | 19 - src/leap/util/keyring_helpers.py | 37 - src/leap/util/leap_argparse.py | 72 - src/leap/util/leap_log_handler.py | 134 -- src/leap/util/privilege_policies.py | 169 --- src/leap/util/pyside_tests_helper.py | 136 -- src/leap/util/request_helpers.py | 58 - src/leap/util/requirement_checker.py | 101 -- src/leap/util/streamtologger.py | 59 - src/leap/util/tests/__init__.py | 0 src/leap/util/tests/test_is_release_version.py | 57 - src/leap/util/tests/test_leap_log_handler.py | 118 -- src/leap/util/tests/test_streamtologger.py | 122 -- 158 files changed, 15653 insertions(+), 15633 deletions(-) delete mode 100644 src/leap/_version.py delete mode 100644 src/leap/app.py create mode 100644 src/leap/bitmask/__init__.py create mode 100644 src/leap/bitmask/_version.py create mode 100644 src/leap/bitmask/app.py create mode 100644 src/leap/bitmask/config/__init__.py create mode 100644 src/leap/bitmask/config/leapsettings.py create mode 100644 src/leap/bitmask/config/provider_spec.py create mode 100644 src/leap/bitmask/config/providerconfig.py create mode 100644 src/leap/bitmask/config/tests/test_providerconfig.py create mode 100644 src/leap/bitmask/crypto/__init__.py create mode 100644 src/leap/bitmask/crypto/srpauth.py create mode 100644 src/leap/bitmask/crypto/srpregister.py create mode 100644 src/leap/bitmask/crypto/tests/__init__.py create mode 100644 src/leap/bitmask/crypto/tests/eip-service.json create mode 100755 src/leap/bitmask/crypto/tests/fake_provider.py create mode 100644 src/leap/bitmask/crypto/tests/openvpn.pem create mode 100644 src/leap/bitmask/crypto/tests/test_provider.json create mode 100644 src/leap/bitmask/crypto/tests/test_srpauth.py create mode 100644 src/leap/bitmask/crypto/tests/test_srpregister.py create mode 100644 src/leap/bitmask/crypto/tests/wrongcert.pem create mode 100644 src/leap/bitmask/gui/__init__.py create mode 100644 src/leap/bitmask/gui/loggerwindow.py create mode 100644 src/leap/bitmask/gui/login.py create mode 100644 src/leap/bitmask/gui/mainwindow.py create mode 100644 src/leap/bitmask/gui/statuspanel.py create mode 100644 src/leap/bitmask/gui/twisted_main.py create mode 100644 src/leap/bitmask/gui/ui/loggerwindow.ui create mode 100644 src/leap/bitmask/gui/ui/login.ui create mode 100644 src/leap/bitmask/gui/ui/mainwindow.ui create mode 100644 src/leap/bitmask/gui/ui/statuspanel.ui create mode 100644 src/leap/bitmask/gui/ui/wizard.ui create mode 100644 src/leap/bitmask/gui/wizard.py create mode 100644 src/leap/bitmask/gui/wizardpage.py create mode 100644 src/leap/bitmask/platform_init/__init__.py create mode 100644 src/leap/bitmask/platform_init/initializers.py create mode 100644 src/leap/bitmask/platform_init/locks.py create mode 100644 src/leap/bitmask/provider/__init__.py create mode 100644 src/leap/bitmask/provider/supportedapis.py create mode 100644 src/leap/bitmask/services/__init__.py create mode 100644 src/leap/bitmask/services/abstractbootstrapper.py create mode 100644 src/leap/bitmask/services/eip/__init__.py create mode 100644 src/leap/bitmask/services/eip/eipbootstrapper.py create mode 100644 src/leap/bitmask/services/eip/eipconfig.py create mode 100644 src/leap/bitmask/services/eip/eipspec.py create mode 100644 src/leap/bitmask/services/eip/providerbootstrapper.py create mode 100644 src/leap/bitmask/services/eip/tests/__init__.py create mode 100644 src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py create mode 100644 src/leap/bitmask/services/eip/tests/test_eipconfig.py create mode 100644 src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py create mode 100644 src/leap/bitmask/services/eip/tests/test_vpngatewayselector.py create mode 100644 src/leap/bitmask/services/eip/tests/wrongcert.pem create mode 100644 src/leap/bitmask/services/eip/udstelnet.py create mode 100644 src/leap/bitmask/services/eip/vpnlaunchers.py create mode 100644 src/leap/bitmask/services/eip/vpnprocess.py create mode 100644 src/leap/bitmask/services/mail/__init__.py create mode 100644 src/leap/bitmask/services/mail/imap.py create mode 100644 src/leap/bitmask/services/mail/smtpbootstrapper.py create mode 100644 src/leap/bitmask/services/mail/smtpconfig.py create mode 100644 src/leap/bitmask/services/mail/smtpspec.py create mode 100644 src/leap/bitmask/services/soledad/__init__.py create mode 100644 src/leap/bitmask/services/soledad/soledadbootstrapper.py create mode 100644 src/leap/bitmask/services/soledad/soledadconfig.py create mode 100644 src/leap/bitmask/services/soledad/soledadspec.py create mode 100644 src/leap/bitmask/services/tests/__init__.py create mode 100644 src/leap/bitmask/services/tests/test_abstractbootstrapper.py create mode 100644 src/leap/bitmask/services/tx.py create mode 100644 src/leap/bitmask/util/__init__.py create mode 100644 src/leap/bitmask/util/constants.py create mode 100644 src/leap/bitmask/util/keyring_helpers.py create mode 100644 src/leap/bitmask/util/leap_argparse.py create mode 100644 src/leap/bitmask/util/leap_log_handler.py create mode 100644 src/leap/bitmask/util/privilege_policies.py create mode 100644 src/leap/bitmask/util/pyside_tests_helper.py create mode 100644 src/leap/bitmask/util/reqs.txt create mode 100644 src/leap/bitmask/util/request_helpers.py create mode 100644 src/leap/bitmask/util/requirement_checker.py create mode 100644 src/leap/bitmask/util/streamtologger.py create mode 100644 src/leap/bitmask/util/tests/__init__.py create mode 100644 src/leap/bitmask/util/tests/test_is_release_version.py create mode 100644 src/leap/bitmask/util/tests/test_leap_log_handler.py create mode 100644 src/leap/bitmask/util/tests/test_streamtologger.py delete mode 100644 src/leap/config/__init__.py delete mode 100644 src/leap/config/leapsettings.py delete mode 100644 src/leap/config/provider_spec.py delete mode 100644 src/leap/config/providerconfig.py delete mode 100644 src/leap/config/tests/test_providerconfig.py delete mode 100644 src/leap/crypto/__init__.py delete mode 100644 src/leap/crypto/srpauth.py delete mode 100644 src/leap/crypto/srpregister.py delete mode 100644 src/leap/crypto/tests/__init__.py delete mode 100644 src/leap/crypto/tests/eip-service.json delete mode 100755 src/leap/crypto/tests/fake_provider.py delete mode 100644 src/leap/crypto/tests/openvpn.pem delete mode 100644 src/leap/crypto/tests/test_provider.json delete mode 100644 src/leap/crypto/tests/test_srpauth.py delete mode 100644 src/leap/crypto/tests/test_srpregister.py delete mode 100644 src/leap/crypto/tests/wrongcert.pem delete mode 100644 src/leap/gui/__init__.py delete mode 100644 src/leap/gui/loggerwindow.py delete mode 100644 src/leap/gui/login.py delete mode 100644 src/leap/gui/mainwindow.py delete mode 100644 src/leap/gui/statuspanel.py delete mode 100644 src/leap/gui/twisted_main.py delete mode 100644 src/leap/gui/ui/loggerwindow.ui delete mode 100644 src/leap/gui/ui/login.ui delete mode 100644 src/leap/gui/ui/mainwindow.ui delete mode 100644 src/leap/gui/ui/statuspanel.ui delete mode 100644 src/leap/gui/ui/wizard.ui delete mode 100644 src/leap/gui/wizard.py delete mode 100644 src/leap/gui/wizardpage.py delete mode 100644 src/leap/platform_init/__init__.py delete mode 100644 src/leap/platform_init/initializers.py delete mode 100644 src/leap/platform_init/locks.py delete mode 100644 src/leap/provider/__init__.py delete mode 100644 src/leap/provider/supportedapis.py delete mode 100644 src/leap/services/__init__.py delete mode 100644 src/leap/services/abstractbootstrapper.py delete mode 100644 src/leap/services/eip/__init__.py delete mode 100644 src/leap/services/eip/eipbootstrapper.py delete mode 100644 src/leap/services/eip/eipconfig.py delete mode 100644 src/leap/services/eip/eipspec.py delete mode 100644 src/leap/services/eip/providerbootstrapper.py delete mode 100644 src/leap/services/eip/tests/__init__.py delete mode 100644 src/leap/services/eip/tests/test_eipbootstrapper.py delete mode 100644 src/leap/services/eip/tests/test_eipconfig.py delete mode 100644 src/leap/services/eip/tests/test_providerbootstrapper.py delete mode 100644 src/leap/services/eip/tests/test_vpngatewayselector.py delete mode 100644 src/leap/services/eip/tests/wrongcert.pem delete mode 100644 src/leap/services/eip/udstelnet.py delete mode 100644 src/leap/services/eip/vpnlaunchers.py delete mode 100644 src/leap/services/eip/vpnprocess.py delete mode 100644 src/leap/services/mail/__init__.py delete mode 100644 src/leap/services/mail/imap.py delete mode 100644 src/leap/services/mail/smtpbootstrapper.py delete mode 100644 src/leap/services/mail/smtpconfig.py delete mode 100644 src/leap/services/mail/smtpspec.py delete mode 100644 src/leap/services/soledad/__init__.py delete mode 100644 src/leap/services/soledad/soledadbootstrapper.py delete mode 100644 src/leap/services/soledad/soledadconfig.py delete mode 100644 src/leap/services/soledad/soledadspec.py delete mode 100644 src/leap/services/tests/__init__.py delete mode 100644 src/leap/services/tests/test_abstractbootstrapper.py delete mode 100644 src/leap/services/tx.py delete mode 100644 src/leap/util/__init__.py delete mode 100644 src/leap/util/constants.py delete mode 100644 src/leap/util/keyring_helpers.py delete mode 100644 src/leap/util/leap_argparse.py delete mode 100644 src/leap/util/leap_log_handler.py delete mode 100644 src/leap/util/privilege_policies.py delete mode 100644 src/leap/util/pyside_tests_helper.py delete mode 100644 src/leap/util/request_helpers.py delete mode 100644 src/leap/util/requirement_checker.py delete mode 100644 src/leap/util/streamtologger.py delete mode 100644 src/leap/util/tests/__init__.py delete mode 100644 src/leap/util/tests/test_is_release_version.py delete mode 100644 src/leap/util/tests/test_leap_log_handler.py delete mode 100644 src/leap/util/tests/test_streamtologger.py (limited to 'src/leap') diff --git a/src/leap/_version.py b/src/leap/_version.py deleted file mode 100644 index 05542975..00000000 --- a/src/leap/_version.py +++ /dev/null @@ -1,201 +0,0 @@ - -IN_LONG_VERSION_PY = True -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (build by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. Generated by -# versioneer-0.7+ (https://github.com/warner/python-versioneer) - -# these strings will be replaced by git during git-archive -git_refnames = "$Format:%d$" -git_full = "$Format:%H$" - - -import subprocess -import sys -import re -import os.path - - -def run_command(args, cwd=None, verbose=False): - try: - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) - except EnvironmentError: - e = sys.exc_info()[1] - if verbose: - print("unable to run %s" % args[0]) - print(e) - return None - stdout = p.communicate()[0].strip() - if sys.version >= '3': - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %s (error)" % args[0]) - return None - return stdout - - -def get_expanded_variables(versionfile_source): - # the code embedded in _version.py can just fetch the value of these - # variables. When used from setup.py, we don't want to import - # _version.py, so we do it with a regexp instead. This function is not - # used from _version.py. - variables = {} - try: - for line in open(versionfile_source, "r").readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - variables["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - variables["full"] = mo.group(1) - except EnvironmentError: - pass - return variables - - -def versions_from_expanded_variables(variables, tag_prefix, verbose=False): - refnames = variables["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("variables are unexpanded, not using") - return {} # unexpanded, so not in an unpacked git-archive tarball - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - for ref in list(refs): - if not re.search(r'\d', ref): - if verbose: - print("discarding '%s', no digits" % ref) - refs.discard(ref) - # Assume all version tags have a digit. git's %d expansion - # behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us - # distinguish between branches and tags. By ignoring refnames - # without digits, we filter out many common branch names like - # "release" and "stabilization", as well as "HEAD" and "master". - if verbose: - print("remaining refs: %s" % ",".join(sorted(refs))) - for ref in sorted(refs): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - if verbose: - print("picking %s" % r) - return {"version": r, - "full": variables["full"].strip()} - # no suitable tags, so we use the full revision id - if verbose: - print("no suitable tags, using full revision id") - return {"version": variables["full"].strip(), - "full": variables["full"].strip()} - - -def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): - # this runs 'git' from the root of the source tree. That either means - # someone ran a setup.py command (and this code is in versioneer.py, so - # IN_LONG_VERSION_PY=False, thus the containing directory is the root of - # the source tree), or someone ran a project-specific entry point (and - # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the - # containing directory is somewhere deeper in the source tree). This only - # gets called if the git-archive 'subst' variables were *not* expanded, - # and _version.py hasn't already been rewritten with a short version - # string, meaning we're inside a checked out source tree. - - try: - here = os.path.abspath(__file__) - except NameError: - # some py2exe/bbfreeze/non-CPython implementations don't do __file__ - return {} # not always correct - - # versionfile_source is the relative path from the top of the source tree - # (where the .git directory might live) to this file. Invert this to find - # the root from __file__. - root = here - if IN_LONG_VERSION_PY: - for i in range(len(versionfile_source.split("/"))): - root = os.path.dirname(root) - else: - root = os.path.dirname(here) - if not os.path.exists(os.path.join(root, ".git")): - if verbose: - print("no .git in %s" % root) - return {} - - GIT = "git" - if sys.platform == "win32": - GIT = "git.cmd" - stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], - cwd=root) - if stdout is None: - return {} - if not stdout.startswith(tag_prefix): - if verbose: - print("tag '%s' doesn't start with prefix '%s'" % ( - stdout, tag_prefix)) - return {} - tag = stdout[len(tag_prefix):] - stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root) - if stdout is None: - return {} - full = stdout.strip() - if tag.endswith("-dirty"): - full += "-dirty" - return {"version": tag, "full": full} - - -def versions_from_parentdir(parentdir_prefix, versionfile_source, - verbose=False): - if IN_LONG_VERSION_PY: - # We're running from _version.py. If it's from a source tree - # (execute-in-place), we can work upwards to find the root of the - # tree, and then check the parent directory for a version string. If - # it's in an installed application, there's no hope. - try: - here = os.path.abspath(__file__) - except NameError: - # py2exe/bbfreeze/non-CPython don't have __file__ - return {} # without __file__, we have no hope - # versionfile_source is the relative path from the top of the source - # tree to _version.py. Invert this to find the root from __file__. - root = here - for i in range(len(versionfile_source.split("/"))): - root = os.path.dirname(root) - else: - # we're running from versioneer.py, which means we're running from - # the setup.py in a source tree. sys.argv[0] is setup.py in the root. - here = os.path.abspath(sys.argv[0]) - root = os.path.dirname(here) - - # Source tarballs conventionally unpack into a directory that includes - # both the project name and a version string. - dirname = os.path.basename(root) - if not dirname.startswith(parentdir_prefix): - if verbose: - print("guessing rootdir is '%s', but '%s' " - "doesn't start with prefix '%s'" % - (root, dirname, parentdir_prefix)) - return None - return {"version": dirname[len(parentdir_prefix):], "full": ""} - -tag_prefix = "" -parentdir_prefix = "bitmask-" -versionfile_source = "src/leap/_version.py" - - -def get_versions(default={"version": "unknown", "full": ""}, verbose=False): - variables = {"refnames": git_refnames, "full": git_full} - ver = versions_from_expanded_variables(variables, tag_prefix, verbose) - if not ver: - ver = versions_from_vcs(tag_prefix, versionfile_source, verbose) - if not ver: - ver = versions_from_parentdir(parentdir_prefix, versionfile_source, - verbose) - if not ver: - ver = default - return ver diff --git a/src/leap/app.py b/src/leap/app.py deleted file mode 100644 index 22340d4d..00000000 --- a/src/leap/app.py +++ /dev/null @@ -1,224 +0,0 @@ -# -*- coding: utf-8 -*- -# app.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 . - -import logging -import signal -import sys -import os - -from functools import partial - -from PySide import QtCore, QtGui - -from leap.common.events import server as event_server -from leap.util import __version__ as VERSION -from leap.util import leap_argparse -from leap.util.leap_log_handler import LeapLogHandler -from leap.util.streamtologger import StreamToLogger -from leap.util.requirement_checker import check_requirements -from leap.gui import locale_rc -from leap.gui import twisted_main -from leap.gui.mainwindow import MainWindow -from leap.platform_init import IS_MAC -from leap.platform_init.locks import we_are_the_one_and_only -from leap.services.tx import leap_services - - -import codecs -codecs.register(lambda name: codecs.lookup('utf-8') - if name == 'cp65001' else None) - -# pylint: avoid unused import -assert(locale_rc) - - -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() - - -def install_qtreactor(logger): - import qt4reactor - qt4reactor.install() - logger.debug("Qt4 reactor installed") - - -def add_logger_handlers(debug=False, logfile=None): - """ - Create the logger and attach the handlers. - - :param debug: the level of the messages that we should log - :type debug: bool - :param logfile: the file name of where we should to save the logs - :type logfile: str - :return: the new logger with the attached handlers. - :rtype: logging.Logger - """ - # TODO: get severity from command line args - if debug: - level = logging.DEBUG - else: - level = logging.WARNING - - # Create logger and formatter - logger = logging.getLogger(name='leap') - logger.setLevel(level) - log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' - formatter = logging.Formatter(log_format) - - # Console handler - console = logging.StreamHandler() - console.setLevel(level) - console.setFormatter(formatter) - logger.addHandler(console) - logger.debug('Console handler plugged!') - - # LEAP custom handler - leap_handler = LeapLogHandler() - leap_handler.setLevel(level) - logger.addHandler(leap_handler) - logger.debug('Leap handler plugged!') - - # File handler - if logfile is not None: - logger.debug('Setting logfile to %s ', logfile) - fileh = logging.FileHandler(logfile) - fileh.setLevel(logging.DEBUG) - fileh.setFormatter(formatter) - logger.addHandler(fileh) - logger.debug('File handler plugged!') - - return logger - - -def replace_stdout_stderr_with_logging(logger): - """ - Replace: - - the standard output - - the standard error - - the twisted log output - with a custom one that writes to the logger. - """ - sys.stdout = StreamToLogger(logger, logging.DEBUG) - sys.stderr = StreamToLogger(logger, logging.ERROR) - - # Replace twisted's logger to use our custom output. - from twisted.python import log - log.startLogging(sys.stdout) - - -def main(): - """ - Starts the main event loop and launches the main window. - """ - try: - event_server.ensure_server(event_server.SERVER_PORT) - except Exception as e: - # We don't even have logger configured in here - print "Could not ensure server: %r" % (e,) - - _, opts = leap_argparse.init_leapc_args() - standalone = opts.standalone - bypass_checks = getattr(opts, 'danger', False) - debug = opts.debug - logfile = opts.log_file - openvpn_verb = opts.openvpn_verb - - logger = add_logger_handlers(debug, logfile) - replace_stdout_stderr_with_logging(logger) - - if not we_are_the_one_and_only(): - # Bitmask is already running - logger.warning("Tried to launch more than one instance " - "of Bitmask. Raising the existing " - "one instead.") - sys.exit(1) - - check_requirements() - - logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') - logger.info('Bitmask version %s', VERSION) - logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') - - 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 os.environ.get("KDE_SESSION_UID") is not None: - sys.argv.append("-style") - sys.argv.append("Cleanlooks") - - app = QtGui.QApplication(sys.argv) - - # install the qt4reactor. - install_qtreactor(logger) - - # 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( - lambda: twisted_main.quit(app), - standalone=standalone, - openvpn_verb=openvpn_verb, - bypass_checks=bypass_checks) - - sigint_window = partial(sigint_handler, window, logger=logger) - signal.signal(signal.SIGINT, sigint_window) - - if IS_MAC: - window.raise_() - - # This was a good idea, but for this to work as intended we - # should centralize the start of all services in there. - #tx_app = leap_services() - #assert(tx_app) - - # Run main loop - twisted_main.start(app) - -if __name__ == "__main__": - main() diff --git a/src/leap/bitmask/__init__.py b/src/leap/bitmask/__init__.py new file mode 100644 index 00000000..f48ad105 --- /dev/null +++ b/src/leap/bitmask/__init__.py @@ -0,0 +1,6 @@ +# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages +try: + __import__('pkg_resources').declare_namespace(__name__) +except ImportError: + from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) diff --git a/src/leap/bitmask/_version.py b/src/leap/bitmask/_version.py new file mode 100644 index 00000000..05542975 --- /dev/null +++ b/src/leap/bitmask/_version.py @@ -0,0 +1,201 @@ + +IN_LONG_VERSION_PY = True +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (build by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.7+ (https://github.com/warner/python-versioneer) + +# these strings will be replaced by git during git-archive +git_refnames = "$Format:%d$" +git_full = "$Format:%H$" + + +import subprocess +import sys +import re +import os.path + + +def run_command(args, cwd=None, verbose=False): + try: + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) + except EnvironmentError: + e = sys.exc_info()[1] + if verbose: + print("unable to run %s" % args[0]) + print(e) + return None + stdout = p.communicate()[0].strip() + if sys.version >= '3': + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % args[0]) + return None + return stdout + + +def get_expanded_variables(versionfile_source): + # the code embedded in _version.py can just fetch the value of these + # variables. When used from setup.py, we don't want to import + # _version.py, so we do it with a regexp instead. This function is not + # used from _version.py. + variables = {} + try: + for line in open(versionfile_source, "r").readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["full"] = mo.group(1) + except EnvironmentError: + pass + return variables + + +def versions_from_expanded_variables(variables, tag_prefix, verbose=False): + refnames = variables["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("variables are unexpanded, not using") + return {} # unexpanded, so not in an unpacked git-archive tarball + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + for ref in list(refs): + if not re.search(r'\d', ref): + if verbose: + print("discarding '%s', no digits" % ref) + refs.discard(ref) + # Assume all version tags have a digit. git's %d expansion + # behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us + # distinguish between branches and tags. By ignoring refnames + # without digits, we filter out many common branch names like + # "release" and "stabilization", as well as "HEAD" and "master". + if verbose: + print("remaining refs: %s" % ",".join(sorted(refs))) + for ref in sorted(refs): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return {"version": r, + "full": variables["full"].strip()} + # no suitable tags, so we use the full revision id + if verbose: + print("no suitable tags, using full revision id") + return {"version": variables["full"].strip(), + "full": variables["full"].strip()} + + +def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): + # this runs 'git' from the root of the source tree. That either means + # someone ran a setup.py command (and this code is in versioneer.py, so + # IN_LONG_VERSION_PY=False, thus the containing directory is the root of + # the source tree), or someone ran a project-specific entry point (and + # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the + # containing directory is somewhere deeper in the source tree). This only + # gets called if the git-archive 'subst' variables were *not* expanded, + # and _version.py hasn't already been rewritten with a short version + # string, meaning we're inside a checked out source tree. + + try: + here = os.path.abspath(__file__) + except NameError: + # some py2exe/bbfreeze/non-CPython implementations don't do __file__ + return {} # not always correct + + # versionfile_source is the relative path from the top of the source tree + # (where the .git directory might live) to this file. Invert this to find + # the root from __file__. + root = here + if IN_LONG_VERSION_PY: + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + root = os.path.dirname(here) + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %s" % root) + return {} + + GIT = "git" + if sys.platform == "win32": + GIT = "git.cmd" + stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], + cwd=root) + if stdout is None: + return {} + if not stdout.startswith(tag_prefix): + if verbose: + print("tag '%s' doesn't start with prefix '%s'" % ( + stdout, tag_prefix)) + return {} + tag = stdout[len(tag_prefix):] + stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root) + if stdout is None: + return {} + full = stdout.strip() + if tag.endswith("-dirty"): + full += "-dirty" + return {"version": tag, "full": full} + + +def versions_from_parentdir(parentdir_prefix, versionfile_source, + verbose=False): + if IN_LONG_VERSION_PY: + # We're running from _version.py. If it's from a source tree + # (execute-in-place), we can work upwards to find the root of the + # tree, and then check the parent directory for a version string. If + # it's in an installed application, there's no hope. + try: + here = os.path.abspath(__file__) + except NameError: + # py2exe/bbfreeze/non-CPython don't have __file__ + return {} # without __file__, we have no hope + # versionfile_source is the relative path from the top of the source + # tree to _version.py. Invert this to find the root from __file__. + root = here + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + # we're running from versioneer.py, which means we're running from + # the setup.py in a source tree. sys.argv[0] is setup.py in the root. + here = os.path.abspath(sys.argv[0]) + root = os.path.dirname(here) + + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%s', but '%s' " + "doesn't start with prefix '%s'" % + (root, dirname, parentdir_prefix)) + return None + return {"version": dirname[len(parentdir_prefix):], "full": ""} + +tag_prefix = "" +parentdir_prefix = "bitmask-" +versionfile_source = "src/leap/_version.py" + + +def get_versions(default={"version": "unknown", "full": ""}, verbose=False): + variables = {"refnames": git_refnames, "full": git_full} + ver = versions_from_expanded_variables(variables, tag_prefix, verbose) + if not ver: + ver = versions_from_vcs(tag_prefix, versionfile_source, verbose) + if not ver: + ver = versions_from_parentdir(parentdir_prefix, versionfile_source, + verbose) + if not ver: + ver = default + return ver diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py new file mode 100644 index 00000000..22340d4d --- /dev/null +++ b/src/leap/bitmask/app.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +# app.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 . + +import logging +import signal +import sys +import os + +from functools import partial + +from PySide import QtCore, QtGui + +from leap.common.events import server as event_server +from leap.util import __version__ as VERSION +from leap.util import leap_argparse +from leap.util.leap_log_handler import LeapLogHandler +from leap.util.streamtologger import StreamToLogger +from leap.util.requirement_checker import check_requirements +from leap.gui import locale_rc +from leap.gui import twisted_main +from leap.gui.mainwindow import MainWindow +from leap.platform_init import IS_MAC +from leap.platform_init.locks import we_are_the_one_and_only +from leap.services.tx import leap_services + + +import codecs +codecs.register(lambda name: codecs.lookup('utf-8') + if name == 'cp65001' else None) + +# pylint: avoid unused import +assert(locale_rc) + + +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() + + +def install_qtreactor(logger): + import qt4reactor + qt4reactor.install() + logger.debug("Qt4 reactor installed") + + +def add_logger_handlers(debug=False, logfile=None): + """ + Create the logger and attach the handlers. + + :param debug: the level of the messages that we should log + :type debug: bool + :param logfile: the file name of where we should to save the logs + :type logfile: str + :return: the new logger with the attached handlers. + :rtype: logging.Logger + """ + # TODO: get severity from command line args + if debug: + level = logging.DEBUG + else: + level = logging.WARNING + + # Create logger and formatter + logger = logging.getLogger(name='leap') + logger.setLevel(level) + log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + formatter = logging.Formatter(log_format) + + # Console handler + console = logging.StreamHandler() + console.setLevel(level) + console.setFormatter(formatter) + logger.addHandler(console) + logger.debug('Console handler plugged!') + + # LEAP custom handler + leap_handler = LeapLogHandler() + leap_handler.setLevel(level) + logger.addHandler(leap_handler) + logger.debug('Leap handler plugged!') + + # File handler + if logfile is not None: + logger.debug('Setting logfile to %s ', logfile) + fileh = logging.FileHandler(logfile) + fileh.setLevel(logging.DEBUG) + fileh.setFormatter(formatter) + logger.addHandler(fileh) + logger.debug('File handler plugged!') + + return logger + + +def replace_stdout_stderr_with_logging(logger): + """ + Replace: + - the standard output + - the standard error + - the twisted log output + with a custom one that writes to the logger. + """ + sys.stdout = StreamToLogger(logger, logging.DEBUG) + sys.stderr = StreamToLogger(logger, logging.ERROR) + + # Replace twisted's logger to use our custom output. + from twisted.python import log + log.startLogging(sys.stdout) + + +def main(): + """ + Starts the main event loop and launches the main window. + """ + try: + event_server.ensure_server(event_server.SERVER_PORT) + except Exception as e: + # We don't even have logger configured in here + print "Could not ensure server: %r" % (e,) + + _, opts = leap_argparse.init_leapc_args() + standalone = opts.standalone + bypass_checks = getattr(opts, 'danger', False) + debug = opts.debug + logfile = opts.log_file + openvpn_verb = opts.openvpn_verb + + logger = add_logger_handlers(debug, logfile) + replace_stdout_stderr_with_logging(logger) + + if not we_are_the_one_and_only(): + # Bitmask is already running + logger.warning("Tried to launch more than one instance " + "of Bitmask. Raising the existing " + "one instead.") + sys.exit(1) + + check_requirements() + + logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') + logger.info('Bitmask version %s', VERSION) + logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') + + 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 os.environ.get("KDE_SESSION_UID") is not None: + sys.argv.append("-style") + sys.argv.append("Cleanlooks") + + app = QtGui.QApplication(sys.argv) + + # install the qt4reactor. + install_qtreactor(logger) + + # 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( + lambda: twisted_main.quit(app), + standalone=standalone, + openvpn_verb=openvpn_verb, + bypass_checks=bypass_checks) + + sigint_window = partial(sigint_handler, window, logger=logger) + signal.signal(signal.SIGINT, sigint_window) + + if IS_MAC: + window.raise_() + + # This was a good idea, but for this to work as intended we + # should centralize the start of all services in there. + #tx_app = leap_services() + #assert(tx_app) + + # Run main loop + twisted_main.start(app) + +if __name__ == "__main__": + main() diff --git a/src/leap/bitmask/config/__init__.py b/src/leap/bitmask/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/config/leapsettings.py b/src/leap/bitmask/config/leapsettings.py new file mode 100644 index 00000000..35010280 --- /dev/null +++ b/src/leap/bitmask/config/leapsettings.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- +# leapsettings.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 . + +""" +QSettings abstraction +""" +import os +import logging + +from PySide import QtCore + +from leap.common.check import leap_assert, leap_assert_type +from leap.common.config.prefixers import get_platform_prefixer + +logger = logging.getLogger(__name__) + + +def to_bool(val): + """ + Returns the boolean value corresponding to val. Will return False + in case val is not a string or something that behaves like one. + + :param val: value to cast + :type val: either bool already or str + + :rtype: bool + """ + if isinstance(val, bool): + return val + + bool_val = False + try: + bool_val = val.lower() == "true" + except: + pass + + return bool_val + + +class LeapSettings(object): + """ + Leap client QSettings wrapper + """ + + CONFIG_NAME = "leap.conf" + + # keys + GEOMETRY_KEY = "Geometry" + WINDOWSTATE_KEY = "WindowState" + USER_KEY = "User" + PROPERPROVIDER_KEY = "ProperProvider" + REMEMBER_KEY = "RememberUserAndPass" + DEFAULTPROVIDER_KEY = "DefaultProvider" + ALERTMISSING_KEY = "AlertMissingScripts" + + def __init__(self, standalone=False): + """ + Constructor + + :param standalone: parameter used to define the location of + the config + :type standalone: bool + """ + + settings_path = os.path.join(get_platform_prefixer() + .get_path_prefix(standalone=standalone), + "leap", + self.CONFIG_NAME) + self._settings = QtCore.QSettings(settings_path, + QtCore.QSettings.IniFormat) + + def get_geometry(self): + """ + Returns the saved geometry or None if it wasn't saved + + :rtype: bytearray or None + """ + return self._settings.value(self.GEOMETRY_KEY, None) + + def set_geometry(self, geometry): + """ + Saves the geometry to the settings + + :param geometry: bytearray representing the geometry + :type geometry: bytearray + """ + leap_assert(geometry, "We need a geometry") + self._settings.setValue(self.GEOMETRY_KEY, geometry) + + def get_windowstate(self): + """ + Returns the window state or None if it wasn't saved + + :rtype: bytearray or None + """ + return self._settings.value(self.WINDOWSTATE_KEY, None) + + def set_windowstate(self, windowstate): + """ + Saves the window state to the settings + + :param windowstate: bytearray representing the window state + :type windowstate: bytearray + """ + leap_assert(windowstate, "We need a window state") + self._settings.setValue(self.WINDOWSTATE_KEY, windowstate) + + def get_enabled_services(self, provider): + """ + Returns a list of enabled services for the given provider + + :param provider: provider domain + :type provider: str + + :rtype: list of str + """ + + leap_assert(len(provider) > 0, "We need a nonempty provider") + enabled_services = self._settings.value("%s/Services" % (provider,), + []) + if isinstance(enabled_services, (str, unicode)): + enabled_services = enabled_services.split(",") + + return enabled_services + + def set_enabled_services(self, provider, services): + """ + Saves the list of enabled services for the given provider + + :param provider: provider domain + :type provider: str + + :param services: list of services to save + :type services: list of str + """ + + leap_assert(len(provider) > 0, "We need a nonempty provider") + leap_assert_type(services, list) + + self._settings.setValue("%s/Services" % (provider,), + services) + + def get_user(self): + """ + Returns the configured user to remember, None if there isn't one + + :rtype: str or None + """ + return self._settings.value(self.USER_KEY, None) + + def set_user(self, user): + """ + Saves the user to remember + + :param user: user name to remember + :type user: str + """ + leap_assert(len(user) > 0, "We cannot save an empty user") + self._settings.setValue(self.USER_KEY, user) + + def get_remember(self): + """ + Returns the value of the remember selection. + + :rtype: bool + """ + return to_bool(self._settings.value(self.REMEMBER_KEY, False)) + + def set_remember(self, remember): + """ + Sets wheter the app should remember username and password + + :param remember: True if the app should remember username and + password, False otherwise + :rtype: bool + """ + leap_assert_type(remember, bool) + self._settings.setValue(self.REMEMBER_KEY, remember) + + # TODO: make this scale with multiple providers, we are assuming + # just one for now + def get_properprovider(self): + """ + Returns True if there is a properly configured provider. + + .. note:: this assumes only one provider for now. + + :rtype: bool + """ + return to_bool(self._settings.value(self.PROPERPROVIDER_KEY, False)) + + def set_properprovider(self, properprovider): + """ + Sets whether the app should automatically login. + + :param properprovider: True if the provider is properly configured, + False otherwise. + :type properprovider: bool + """ + leap_assert_type(properprovider, bool) + self._settings.setValue(self.PROPERPROVIDER_KEY, properprovider) + + def get_defaultprovider(self): + """ + Returns the default provider to be used for autostarting EIP + + :rtype: str or None + """ + return self._settings.value(self.DEFAULTPROVIDER_KEY, None) + + def set_defaultprovider(self, provider): + """ + Sets the default provider to be used for autostarting EIP + + :param provider: provider to use + :type provider: str or None + """ + if provider is None: + self._settings.remove(self.DEFAULTPROVIDER_KEY) + else: + self._settings.setValue(self.DEFAULTPROVIDER_KEY, provider) + + def get_alert_missing_scripts(self): + """ + Returns the setting for alerting of missing up/down scripts. + + :rtype: bool + """ + return to_bool(self._settings.value(self.ALERTMISSING_KEY, True)) + + def set_alert_missing_scripts(self, value): + """ + Sets the setting for alerting of missing up/down scripts. + + :param value: the value to set + :type value: bool + """ + leap_assert_type(value, bool) + self._settings.setValue(self.ALERTMISSING_KEY, value) diff --git a/src/leap/bitmask/config/provider_spec.py b/src/leap/bitmask/config/provider_spec.py new file mode 100644 index 00000000..cf942c7b --- /dev/null +++ b/src/leap/bitmask/config/provider_spec.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# provider_spec.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 . + +leap_provider_spec = { + 'description': 'provider definition', + 'type': 'object', + 'properties': { + 'version': { + 'type': unicode, + 'default': '0.1.0' + }, + "default_language": { + 'type': unicode, + 'default': 'en' + }, + 'domain': { + 'type': unicode, # XXX define uri type + 'default': 'testprovider.example.org' + }, + 'name': { + 'type': dict, + 'format': 'translatable', + 'default': {u'en': u'Test Provider'} + }, + 'description': { + #'type': LEAPTranslatable, + 'type': dict, + 'format': 'translatable', + 'default': {u'en': u'Test provider'} + }, + 'enrollment_policy': { + 'type': unicode, # oneof ?? + 'default': 'open' + }, + 'services': { + 'type': list, # oneof ?? + 'default': ['eip'] + }, + 'api_version': { + 'type': unicode, + 'default': '0.1.0' # version regexp + }, + 'api_uri': { + 'type': unicode # uri + }, + 'public_key': { + 'type': unicode # fingerprint + }, + 'ca_cert_fingerprint': { + 'type': unicode, + }, + 'ca_cert_uri': { + 'type': unicode, + 'format': 'https-uri' + }, + 'languages': { + 'type': list, + 'default': ['en'] + }, + 'service': { + 'levels': { + 'type': list + }, + 'default_service_level': { + 'type': int, + 'default': 1 + }, + 'allow_free': { + 'type': unicode + }, + 'allow_paid': { + 'type': unicode + }, + 'allow_anonymous': { + 'type': unicode + }, + 'allow_registration': { + 'type': unicode + }, + 'bandwidth_limit': { + 'type': int + }, + 'allow_limited_bandwidth': { + 'type': unicode + }, + 'allow_unlimited_bandwidth': { + 'type': unicode + } + } + } +} diff --git a/src/leap/bitmask/config/providerconfig.py b/src/leap/bitmask/config/providerconfig.py new file mode 100644 index 00000000..f899b17c --- /dev/null +++ b/src/leap/bitmask/config/providerconfig.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +# providerconfig.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 . + +""" +Provider configuration +""" +import logging +import os + +from leap.common.check import leap_check +from leap.common.config.baseconfig import BaseConfig, LocalizedKey +from leap.config.provider_spec import leap_provider_spec + +logger = logging.getLogger(__name__) + + +class MissingCACert(Exception): + """ + Raised when a CA certificate is needed but not found. + """ + pass + + +class ProviderConfig(BaseConfig): + """ + Provider configuration abstraction class + """ + def __init__(self): + BaseConfig.__init__(self) + + def _get_schema(self): + """ + Returns the schema corresponding to the version given. + + :rtype: dict or None if the version is not supported. + """ + return leap_provider_spec + + def _get_spec(self): + """ + Returns the spec object for the specific configuration. + + Override the BaseConfig one because we do not support multiple schemas + for the provider yet. + + :rtype: dict or None if the version is not supported. + """ + return self._get_schema() + + def get_api_uri(self): + return self._safe_get_value("api_uri") + + def get_api_version(self): + return self._safe_get_value("api_version") + + def get_ca_cert_fingerprint(self): + return self._safe_get_value("ca_cert_fingerprint") + + def get_ca_cert_uri(self): + return self._safe_get_value("ca_cert_uri") + + def get_default_language(self): + return self._safe_get_value("default_language") + + @LocalizedKey + def get_description(self): + return self._safe_get_value("description") + + @classmethod + def sanitize_path_component(cls, component): + """ + If the provider tries to instrument the component of a path + that is controlled by them, this will take care of + removing/escaping all the necessary elements. + + :param component: Path component to process + :type component: unicode or str + + :returns: The path component properly escaped + :rtype: unicode or str + """ + # TODO: Fix for windows, names like "aux" or "con" aren't + # allowed. + return component.replace(os.path.sep, "") + + def get_domain(self): + return ProviderConfig.sanitize_path_component( + self._safe_get_value("domain")) + + def get_enrollment_policy(self): + """ + Returns the enrollment policy + + :rtype: string + """ + return self._safe_get_value("enrollment_policy") + + def get_languages(self): + return self._safe_get_value("languages") + + @LocalizedKey + def get_name(self): + return self._safe_get_value("name") + + def get_services(self): + """ + Returns a list with the available services in the current provider. + + :rtype: list + """ + services = self._safe_get_value("services") + return services + + def get_services_string(self): + """ + Returns a string with the available services in the current + provider, ready to be shown to the user. + """ + services_str = ", ".join(self.get_services()) + services_str = services_str.replace( + "openvpn", "Encrypted Internet") + return services_str + + def get_ca_cert_path(self, about_to_download=False): + """ + Returns the path to the certificate for the current provider. + It may raise MissingCACert if + the certificate does not exists and not about_to_download + + :param about_to_download: defines wether we want the path to + download the cert or not. This helps avoid + checking if the cert exists because we + are about to write it. + :type about_to_download: bool + """ + + cert_path = os.path.join(self.get_path_prefix(), + "leap", + "providers", + self.get_domain(), + "keys", + "ca", + "cacert.pem") + + if not about_to_download: + cert_exists = os.path.exists(cert_path) + error_msg = "You need to download the certificate first" + leap_check(cert_exists, error_msg, MissingCACert) + logger.debug("Going to verify SSL against %s" % (cert_path,)) + + return cert_path + + def provides_eip(self): + """ + Returns True if this particular provider has the EIP service, + False otherwise. + + :rtype: bool + """ + return "openvpn" in self.get_services() + + def provides_mx(self): + """ + Returns True if this particular provider has the MX service, + False otherwise. + + :rtype: bool + """ + return "mx" in self.get_services() + + +if __name__ == "__main__": + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + provider = ProviderConfig() + + try: + provider.get_api_version() + except Exception as e: + assert isinstance(e, AssertionError), "Expected an assert" + print "Safe value getting is working" + + # standalone minitest + #if provider.load("provider_bad.json"): + if provider.load("leap/providers/bitmask.net/provider.json"): + print provider.get_api_version() + print provider.get_ca_cert_fingerprint() + print provider.get_ca_cert_uri() + print provider.get_default_language() + print provider.get_description() + print provider.get_description(lang="asd") + print provider.get_domain() + print provider.get_enrollment_policy() + print provider.get_languages() + print provider.get_name() + print provider.get_services() diff --git a/src/leap/bitmask/config/tests/test_providerconfig.py b/src/leap/bitmask/config/tests/test_providerconfig.py new file mode 100644 index 00000000..ff2828e6 --- /dev/null +++ b/src/leap/bitmask/config/tests/test_providerconfig.py @@ -0,0 +1,279 @@ +# -*- coding: utf-8 -*- +# test_providerconfig.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 . +""" +Tests for providerconfig +""" + +try: + import unittest2 as unittest +except ImportError: + import unittest + +import os +import json +import copy + +from leap.common.testing.basetest import BaseLeapTest +from leap.config.providerconfig import ProviderConfig, MissingCACert +from leap.services import get_supported + +from mock import Mock + + +sample_config = { + "api_uri": "https://api.test.bitmask.net:4430", + "api_version": "1", + "ca_cert_fingerprint": + "SHA256: 0f17c033115f6b76ff67871872303ff65034efe7dd1b910062ca323eb4da5c7e", + "ca_cert_uri": "https://test.bitmask.net/ca.crt", + "default_language": "en", + "description": { + "en": "Test description for provider", + "es": "Descripcion de prueba para el proveedor" + }, + "domain": "test.bitmask.net", + "enrollment_policy": "open", + "languages": [ + "en", + "es" + ], + "name": { + "en": "Bitmask testing environment", + "es": "Entorno de pruebas de Bitmask" + }, + "service": { + "allow_anonymous": True, + "allow_free": True, + "allow_limited_bandwidth": True, + "allow_paid": False, + "allow_registration": True, + "allow_unlimited_bandwidth": False, + "bandwidth_limit": 400000, + "default_service_level": 1, + "levels": [ + { + "bandwidth": "limited", + "id": 1, + "name": "anonymous" + }, + { + "bandwidth": "limited", + "id": 2, + "name": "free", + "storage": 50 + } + ] + }, + "services": [ + "openvpn" + ] +} + + +class ProviderConfigTest(BaseLeapTest): + """Tests for ProviderConfig""" + + def setUp(self): + self._provider_config = ProviderConfig() + json_string = json.dumps(sample_config) + self._provider_config.load(data=json_string) + + # At certain points we are going to be replacing these method + # to avoid creating a file. + # We need to save the old implementation and restore it in + # tearDown so we are sure everything is as expected for each + # test. If we do it inside each specific test, a failure in + # the test will leave the implementation with the mock. + self._old_ospath_exists = os.path.exists + + def tearDown(self): + os.path.exists = self._old_ospath_exists + + def test_configs_ok(self): + """ + Test if the configs loads ok + """ + # TODO: this test should go to the BaseConfig tests + pc = self._provider_config + self.assertEqual(pc.get_api_uri(), sample_config['api_uri']) + self.assertEqual(pc.get_api_version(), sample_config['api_version']) + self.assertEqual(pc.get_ca_cert_fingerprint(), + sample_config['ca_cert_fingerprint']) + self.assertEqual(pc.get_ca_cert_uri(), sample_config['ca_cert_uri']) + self.assertEqual(pc.get_default_language(), + sample_config['default_language']) + + self.assertEqual(pc.get_domain(), sample_config['domain']) + self.assertEqual(pc.get_enrollment_policy(), + sample_config['enrollment_policy']) + self.assertEqual(pc.get_languages(), sample_config['languages']) + + def test_localizations(self): + pc = self._provider_config + + self.assertEqual(pc.get_description(lang='en'), + sample_config['description']['en']) + self.assertEqual(pc.get_description(lang='es'), + sample_config['description']['es']) + + self.assertEqual(pc.get_name(lang='en'), sample_config['name']['en']) + self.assertEqual(pc.get_name(lang='es'), sample_config['name']['es']) + + def _localize(self, lang): + """ + Helper to change default language of the provider config. + """ + pc = self._provider_config + config = copy.deepcopy(sample_config) + config['default_language'] = lang + json_string = json.dumps(config) + pc.load(data=json_string) + + return config + + def test_default_localization1(self): + pc = self._provider_config + config = self._localize(sample_config['languages'][0]) + + default_language = config['default_language'] + default_description = config['description'][default_language] + default_name = config['name'][default_language] + + self.assertEqual(pc.get_description(lang='xx'), default_description) + self.assertEqual(pc.get_description(), default_description) + + self.assertEqual(pc.get_name(lang='xx'), default_name) + self.assertEqual(pc.get_name(), default_name) + + def test_default_localization2(self): + pc = self._provider_config + config = self._localize(sample_config['languages'][1]) + + default_language = config['default_language'] + default_description = config['description'][default_language] + default_name = config['name'][default_language] + + self.assertEqual(pc.get_description(lang='xx'), default_description) + self.assertEqual(pc.get_description(), default_description) + + self.assertEqual(pc.get_name(lang='xx'), default_name) + self.assertEqual(pc.get_name(), default_name) + + def test_get_ca_cert_path_as_expected(self): + pc = self._provider_config + pc.get_path_prefix = Mock(return_value='test') + + provider_domain = sample_config['domain'] + expected_path = os.path.join('test', 'leap', 'providers', + provider_domain, 'keys', 'ca', + 'cacert.pem') + + # mock 'os.path.exists' so we don't get an error for unexisting file + os.path.exists = Mock(return_value=True) + cert_path = pc.get_ca_cert_path() + + self.assertEqual(cert_path, expected_path) + + def test_get_ca_cert_path_about_to_download(self): + pc = self._provider_config + pc.get_path_prefix = Mock(return_value='test') + + provider_domain = sample_config['domain'] + expected_path = os.path.join('test', 'leap', 'providers', + provider_domain, 'keys', 'ca', + 'cacert.pem') + + cert_path = pc.get_ca_cert_path(about_to_download=True) + + self.assertEqual(cert_path, expected_path) + + def test_get_ca_cert_path_fails(self): + pc = self._provider_config + pc.get_path_prefix = Mock(return_value='test') + + # mock 'get_domain' so we don't need to load a config + provider_domain = 'test.provider.com' + pc.get_domain = Mock(return_value=provider_domain) + + with self.assertRaises(MissingCACert): + pc.get_ca_cert_path() + + def test_provides_eip(self): + pc = self._provider_config + config = copy.deepcopy(sample_config) + + # It provides + config['services'] = ['openvpn', 'test_service'] + json_string = json.dumps(config) + pc.load(data=json_string) + self.assertTrue(pc.provides_eip()) + + # It does not provides + config['services'] = ['test_service', 'other_service'] + json_string = json.dumps(config) + pc.load(data=json_string) + self.assertFalse(pc.provides_eip()) + + def test_provides_mx(self): + pc = self._provider_config + config = copy.deepcopy(sample_config) + + # It provides + config['services'] = ['mx', 'other_service'] + json_string = json.dumps(config) + pc.load(data=json_string) + self.assertTrue(pc.provides_mx()) + + # It does not provides + config['services'] = ['test_service', 'other_service'] + json_string = json.dumps(config) + pc.load(data=json_string) + self.assertFalse(pc.provides_mx()) + + def test_supports_unknown_service(self): + pc = self._provider_config + config = copy.deepcopy(sample_config) + + config['services'] = ['unknown'] + json_string = json.dumps(config) + pc.load(data=json_string) + self.assertFalse('unknown' in get_supported(pc.get_services())) + + def test_provides_unknown_service(self): + pc = self._provider_config + config = copy.deepcopy(sample_config) + + config['services'] = ['unknown'] + json_string = json.dumps(config) + pc.load(data=json_string) + self.assertTrue('unknown' in pc.get_services()) + + def test_get_services_string(self): + pc = self._provider_config + config = copy.deepcopy(sample_config) + config['services'] = [ + 'openvpn', 'asdf', 'openvpn', 'not_supported_service'] + json_string = json.dumps(config) + pc.load(data=json_string) + + self.assertEqual(pc.get_services_string(), + "Encrypted Internet, asdf, Encrypted Internet," + " not_supported_service") + + +if __name__ == "__main__": + unittest.main() diff --git a/src/leap/bitmask/crypto/__init__.py b/src/leap/bitmask/crypto/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py new file mode 100644 index 00000000..fc0533fc --- /dev/null +++ b/src/leap/bitmask/crypto/srpauth.py @@ -0,0 +1,606 @@ +# -*- coding: utf-8 -*- +# srpauth.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 . + +import binascii +import logging + +import requests +import srp +import json + +#this error is raised from requests +from simplejson.decoder import JSONDecodeError +from functools import partial + +from PySide import QtCore +from twisted.internet import threads + +from leap.common.check import leap_assert +from leap.util.constants import REQUEST_TIMEOUT +from leap.util import request_helpers as reqhelper +from leap.common.events import signal as events_signal +from leap.common.events import events_pb2 as proto + +logger = logging.getLogger(__name__) + + +class SRPAuthenticationError(Exception): + """ + Exception raised for authentication errors + """ + pass + + +class SRPAuthConnectionError(SRPAuthenticationError): + """ + Exception raised when there's a connection error + """ + pass + + +class SRPAuthUnknownUser(SRPAuthenticationError): + """ + Exception raised when trying to authenticate an unknown user + """ + pass + + +class SRPAuthBadStatusCode(SRPAuthenticationError): + """ + Exception raised when we received an unknown bad status code + """ + pass + + +class SRPAuthNoSalt(SRPAuthenticationError): + """ + Exception raised when we don't receive the salt param at a + specific point in the auth process + """ + pass + + +class SRPAuthNoB(SRPAuthenticationError): + """ + Exception raised when we don't receive the B param at a specific + point in the auth process + """ + pass + + +class SRPAuthBadDataFromServer(SRPAuthenticationError): + """ + Generic exception when we receive bad data from the server. + """ + pass + + +class SRPAuthJSONDecodeError(SRPAuthenticationError): + """ + Exception raised when there's a problem decoding the JSON content + parsed as received from th e server. + """ + pass + + +class SRPAuthBadPassword(SRPAuthenticationError): + """ + Exception raised when the user provided a bad password to auth. + """ + pass + + +class SRPAuthVerificationFailed(SRPAuthenticationError): + """ + Exception raised when we can't verify the SRP data received from + the server. + """ + pass + + +class SRPAuthNoSessionId(SRPAuthenticationError): + """ + Exception raised when we don't receive a session id from the + server. + """ + pass + + +class SRPAuth(QtCore.QObject): + """ + SRPAuth singleton + """ + + class __impl(QtCore.QObject): + """ + Implementation of the SRPAuth interface + """ + + LOGIN_KEY = "login" + A_KEY = "A" + CLIENT_AUTH_KEY = "client_auth" + SESSION_ID_KEY = "_session_id" + + def __init__(self, provider_config): + """ + Constructor for SRPAuth implementation + + :param server: Server to which we will authenticate + :type server: str + """ + QtCore.QObject.__init__(self) + + leap_assert(provider_config, + "We need a provider config to authenticate") + + self._provider_config = provider_config + + # **************************************************** # + # Dependency injection helpers, override this for more + # granular testing + self._fetcher = requests + self._srp = srp + self._hashfun = self._srp.SHA256 + self._ng = self._srp.NG_1024 + # **************************************************** # + + self._session = self._fetcher.session() + self._session_id = None + self._session_id_lock = QtCore.QMutex() + self._uid = None + self._uid_lock = QtCore.QMutex() + self._token = None + self._token_lock = QtCore.QMutex() + + self._srp_user = None + self._srp_a = None + + def _safe_unhexlify(self, val): + """ + Rounds the val to a multiple of 2 and returns the + unhexlified value + + :param val: hexlified value + :type val: str + + :rtype: binary hex data + :return: unhexlified val + """ + return binascii.unhexlify(val) \ + if (len(val) % 2 == 0) else binascii.unhexlify('0' + val) + + def _authentication_preprocessing(self, username, password): + """ + Generates the SRP.User to get the A SRP parameter + + :param username: username to login + :type username: str + :param password: password for the username + :type password: str + """ + logger.debug("Authentication preprocessing...") + self._srp_user = self._srp.User(username, + password, + self._hashfun, + self._ng) + _, A = self._srp_user.start_authentication() + + self._srp_a = A + + def _start_authentication(self, _, username): + """ + Sends the first request for authentication to retrieve the + salt and B parameter + + Might raise all SRPAuthenticationError based: + SRPAuthenticationError + SRPAuthConnectionError + SRPAuthUnknownUser + SRPAuthBadStatusCode + SRPAuthNoSalt + SRPAuthNoB + + :param _: IGNORED, output from the previous callback (None) + :type _: IGNORED + :param username: username to login + :type username: str + + :return: salt and B parameters + :rtype: tuple + """ + logger.debug("Starting authentication process...") + try: + auth_data = { + self.LOGIN_KEY: username, + self.A_KEY: binascii.hexlify(self._srp_a) + } + sessions_url = "%s/%s/%s/" % \ + (self._provider_config.get_api_uri(), + self._provider_config.get_api_version(), + "sessions") + init_session = self._session.post(sessions_url, + data=auth_data, + verify=self._provider_config. + get_ca_cert_path(), + timeout=REQUEST_TIMEOUT) + # Clean up A value, we don't need it anymore + self._srp_a = None + except requests.exceptions.ConnectionError as e: + logger.error("No connection made (salt): %r" % + (e,)) + raise SRPAuthConnectionError("Could not establish a " + "connection") + except Exception as e: + logger.error("Unknown error: %r" % (e,)) + raise SRPAuthenticationError("Unknown error: %r" % + (e,)) + + content, mtime = reqhelper.get_content(init_session) + + if init_session.status_code not in (200,): + logger.error("No valid response (salt): " + "Status code = %r. Content: %r" % + (init_session.status_code, content)) + if init_session.status_code == 422: + raise SRPAuthUnknownUser(self.tr("Unknown user")) + + raise SRPAuthBadStatusCode(self.tr("There was a problem with" + " authentication")) + + json_content = json.loads(content) + salt = json_content.get("salt", None) + B = json_content.get("B", None) + + if salt is None: + logger.error("No salt parameter sent") + raise SRPAuthNoSalt(self.tr("The server did not send " + "the salt parameter")) + if B is None: + logger.error("No B parameter sent") + raise SRPAuthNoB(self.tr("The server did not send " + "the B parameter")) + + return salt, B + + def _process_challenge(self, salt_B, username): + """ + Given the salt and B processes the auth challenge and + generates the M2 parameter + + Might raise SRPAuthenticationError based: + SRPAuthenticationError + SRPAuthBadDataFromServer + SRPAuthConnectionError + SRPAuthJSONDecodeError + SRPAuthBadPassword + + :param salt_B: salt and B parameters for the username + :type salt_B: tuple + :param username: username for this session + :type username: str + + :return: the M2 SRP parameter + :rtype: str + """ + logger.debug("Processing challenge...") + try: + salt, B = salt_B + unhex_salt = self._safe_unhexlify(salt) + unhex_B = self._safe_unhexlify(B) + except (TypeError, ValueError) as e: + logger.error("Bad data from server: %r" % (e,)) + raise SRPAuthBadDataFromServer( + self.tr("The data sent from the server had errors")) + M = self._srp_user.process_challenge(unhex_salt, unhex_B) + + auth_url = "%s/%s/%s/%s" % (self._provider_config.get_api_uri(), + self._provider_config. + get_api_version(), + "sessions", + username) + + auth_data = { + self.CLIENT_AUTH_KEY: binascii.hexlify(M) + } + + try: + auth_result = self._session.put(auth_url, + data=auth_data, + verify=self._provider_config. + get_ca_cert_path(), + timeout=REQUEST_TIMEOUT) + except requests.exceptions.ConnectionError as e: + logger.error("No connection made (HAMK): %r" % (e,)) + raise SRPAuthConnectionError(self.tr("Could not connect to " + "the server")) + + try: + content, mtime = reqhelper.get_content(auth_result) + except JSONDecodeError: + raise SRPAuthJSONDecodeError("Bad JSON content in auth result") + + if auth_result.status_code == 422: + error = "" + try: + error = json.loads(content).get("errors", "") + except ValueError: + logger.error("Problem parsing the received response: %s" + % (content,)) + except AttributeError: + logger.error("Expecting a dict but something else was " + "received: %s", (content,)) + logger.error("[%s] Wrong password (HAMK): [%s]" % + (auth_result.status_code, error)) + raise SRPAuthBadPassword(self.tr("Wrong password")) + + if auth_result.status_code not in (200,): + logger.error("No valid response (HAMK): " + "Status code = %s. Content = %r" % + (auth_result.status_code, content)) + raise SRPAuthBadStatusCode(self.tr("Unknown error (%s)") % + (auth_result.status_code,)) + + return json.loads(content) + + def _extract_data(self, json_content): + """ + Extracts the necessary parameters from json_content (M2, + id, token) + + Might raise SRPAuthenticationError based: + SRPBadDataFromServer + + :param json_content: Data received from the server + :type json_content: dict + """ + try: + M2 = json_content.get("M2", None) + uid = json_content.get("id", None) + token = json_content.get("token", None) + except Exception as e: + logger.error(e) + raise SRPAuthBadDataFromServer("Something went wrong with the " + "login") + + self.set_uid(uid) + self.set_token(token) + + if M2 is None or self.get_uid() is None: + logger.error("Something went wrong. Content = %r" % + (json_content,)) + raise SRPAuthBadDataFromServer(self.tr("Problem getting data " + "from server")) + + events_signal( + proto.CLIENT_UID, content=uid, + reqcbk=lambda req, res: None) # make the rpc call async + + return M2 + + def _verify_session(self, M2): + """ + Verifies the session based on the M2 parameter. If the + verification succeeds, it sets the session_id for this + session + + Might raise SRPAuthenticationError based: + SRPAuthBadDataFromServer + SRPAuthVerificationFailed + + :param M2: M2 SRP parameter + :type M2: str + """ + logger.debug("Verifying session...") + try: + unhex_M2 = self._safe_unhexlify(M2) + except TypeError: + logger.error("Bad data from server (HAWK)") + raise SRPAuthBadDataFromServer(self.tr("Bad data from server")) + + self._srp_user.verify_session(unhex_M2) + + if not self._srp_user.authenticated(): + logger.error("Auth verification failed") + raise SRPAuthVerificationFailed(self.tr("Auth verification " + "failed")) + logger.debug("Session verified.") + + session_id = self._session.cookies.get(self.SESSION_ID_KEY, None) + if not session_id: + logger.error("Bad cookie from server (missing _session_id)") + raise SRPAuthNoSessionId(self.tr("Session cookie " + "verification " + "failed")) + + events_signal( + proto.CLIENT_SESSION_ID, content=session_id, + reqcbk=lambda req, res: None) # make the rpc call async + + self.set_session_id(session_id) + + def _threader(self, cb, res, *args, **kwargs): + return threads.deferToThread(cb, res, *args, **kwargs) + + def authenticate(self, username, password): + """ + Executes the whole authentication process for a user + + Might raise SRPAuthenticationError + + :param username: username for this session + :type username: str + :param password: password for this user + :type password: str + + :returns: A defer on a different thread + :rtype: twisted.internet.defer.Deferred + """ + leap_assert(self.get_session_id() is None, "Already logged in") + + d = threads.deferToThread(self._authentication_preprocessing, + username=username, + password=password) + + d.addCallback( + partial(self._threader, + self._start_authentication), + username=username) + d.addCallback( + partial(self._threader, + self._process_challenge), + username=username) + d.addCallback( + partial(self._threader, + self._extract_data)) + d.addCallback(partial(self._threader, + self._verify_session)) + + return d + + def logout(self): + """ + Logs out the current session. + Expects a session_id to exists, might raise AssertionError + """ + logger.debug("Starting logout...") + + leap_assert(self.get_session_id(), + "Cannot logout an unexisting session") + + logout_url = "%s/%s/%s/" % (self._provider_config.get_api_uri(), + self._provider_config. + get_api_version(), + "sessions") + try: + self._session.delete(logout_url, + data=self.get_session_id(), + verify=self._provider_config. + get_ca_cert_path(), + timeout=REQUEST_TIMEOUT) + except Exception as e: + logger.warning("Something went wrong with the logout: %r" % + (e,)) + + self.set_session_id(None) + self.set_uid(None) + # Also reset the session + self._session = self._fetcher.session() + logger.debug("Successfully logged out.") + + def set_session_id(self, session_id): + QtCore.QMutexLocker(self._session_id_lock) + self._session_id = session_id + + def get_session_id(self): + QtCore.QMutexLocker(self._session_id_lock) + return self._session_id + + def set_uid(self, uid): + QtCore.QMutexLocker(self._uid_lock) + self._uid = uid + + def get_uid(self): + QtCore.QMutexLocker(self._uid_lock) + return self._uid + + def set_token(self, token): + QtCore.QMutexLocker(self._token_lock) + self._token = token + + def get_token(self): + QtCore.QMutexLocker(self._token_lock) + return self._token + + __instance = None + + authentication_finished = QtCore.Signal(bool, str) + logout_finished = QtCore.Signal(bool, str) + + def __init__(self, provider_config): + """ + Creates a singleton instance if needed + """ + QtCore.QObject.__init__(self) + + # Check whether we already have an instance + if SRPAuth.__instance is None: + # Create and remember instance + SRPAuth.__instance = SRPAuth.__impl(provider_config) + + # Store instance reference as the only member in the handle + self.__dict__['_SRPAuth__instance'] = SRPAuth.__instance + + def authenticate(self, username, password): + """ + Executes the whole authentication process for a user + + Might raise SRPAuthenticationError based + + :param username: username for this session + :type username: str + :param password: password for this user + :type password: str + """ + + d = self.__instance.authenticate(username, password) + d.addCallback(self._gui_notify) + d.addErrback(self._errback) + return d + + def _gui_notify(self, _): + """ + Callback that notifies the UI with the proper signal. + + :param _: IGNORED, output from the previous callback (None) + :type _: IGNORED + """ + logger.debug("Successful login!") + self.authentication_finished.emit(True, self.tr("Succeeded")) + + def _errback(self, failure): + """ + General errback for the whole login process. Will notify the + UI with the proper signal. + + :param failure: Failure object captured from a callback. + :type failure: twisted.python.failure.Failure + """ + logger.error("Error logging in %s" % (failure,)) + self.authentication_finished.emit(False, "%s" % (failure.value,)) + failure.trap(Exception) + + def get_session_id(self): + return self.__instance.get_session_id() + + def get_uid(self): + return self.__instance.get_uid() + + def get_token(self): + return self.__instance.get_token() + + def logout(self): + """ + Logs out the current session. + Expects a session_id to exists, might raise AssertionError + """ + try: + self.__instance.logout() + self.logout_finished.emit(True, self.tr("Succeeded")) + return True + except Exception as e: + self.logout_finished.emit(False, "%s" % (e,)) + return False diff --git a/src/leap/bitmask/crypto/srpregister.py b/src/leap/bitmask/crypto/srpregister.py new file mode 100644 index 00000000..de1978b5 --- /dev/null +++ b/src/leap/bitmask/crypto/srpregister.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +# srpregister.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 . + +import binascii +import logging + +import requests +import srp + +from PySide import QtCore +from urlparse import urlparse + +from leap.config.providerconfig import ProviderConfig +from leap.util.constants import SIGNUP_TIMEOUT +from leap.common.check import leap_assert, leap_assert_type + +logger = logging.getLogger(__name__) + + +class SRPRegister(QtCore.QObject): + """ + Registers a user to a specific provider using SRP + """ + + USER_LOGIN_KEY = 'user[login]' + USER_VERIFIER_KEY = 'user[password_verifier]' + USER_SALT_KEY = 'user[password_salt]' + + registration_finished = QtCore.Signal(bool, object) + + def __init__(self, + provider_config=None, + register_path="users"): + """ + Constructor + + :param provider_config: provider configuration instance, + properly loaded + :type privider_config: ProviderConfig + :param register_path: webapp path for registering users + :type register_path; str + """ + QtCore.QObject.__init__(self) + leap_assert(provider_config, "Please provide a provider") + leap_assert_type(provider_config, ProviderConfig) + + self._provider_config = provider_config + + # **************************************************** # + # Dependency injection helpers, override this for more + # granular testing + self._fetcher = requests + self._srp = srp + self._hashfun = self._srp.SHA256 + self._ng = self._srp.NG_1024 + # **************************************************** # + + parsed_url = urlparse(provider_config.get_api_uri()) + self._provider = parsed_url.hostname + self._port = parsed_url.port + if self._port is None: + self._port = "443" + + self._register_path = register_path + + self._session = self._fetcher.session() + + def _get_registration_uri(self): + """ + Returns the URI where the register request should be made for + the provider + + :rtype: str + """ + + uri = "https://%s:%s/%s/%s" % ( + self._provider, + self._port, + self._provider_config.get_api_version(), + self._register_path) + + return uri + + def register_user(self, username, password): + """ + Registers a user with the validator based on the password provider + + :param username: username to register + :type username: str + :param password: password for this username + :type password: str + + :rtype: tuple + :rparam: (ok, request) + """ + salt, verifier = self._srp.create_salted_verification_key( + username, + password, + self._hashfun, + self._ng) + + user_data = { + self.USER_LOGIN_KEY: username, + self.USER_VERIFIER_KEY: binascii.hexlify(verifier), + self.USER_SALT_KEY: binascii.hexlify(salt) + } + + uri = self._get_registration_uri() + + logger.debug('Post to uri: %s' % uri) + logger.debug("Will try to register user = %s" % (username,)) + + ok = False + # This should be None, but we don't like when PySide segfaults, + # so it something else. + # To reproduce it, just do: + # self.registration_finished.emit(False, None) + req = [] + try: + req = self._session.post(uri, + data=user_data, + timeout=SIGNUP_TIMEOUT, + verify=self._provider_config. + get_ca_cert_path()) + + except (requests.exceptions.SSLError, + requests.exceptions.ConnectionError) as exc: + logger.error(exc.message) + ok = False + else: + ok = req.ok + + self.registration_finished.emit(ok, req) + return ok + + +if __name__ == "__main__": + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + provider = ProviderConfig() + + if provider.load("leap/providers/bitmask.net/provider.json"): + register = SRPRegister(provider_config=provider) + print "Registering user..." + print register.register_user("test1", "sarasaaaa") + print register.register_user("test2", "sarasaaaa") diff --git a/src/leap/bitmask/crypto/tests/__init__.py b/src/leap/bitmask/crypto/tests/__init__.py new file mode 100644 index 00000000..7f118735 --- /dev/null +++ b/src/leap/bitmask/crypto/tests/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# __init__.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 . diff --git a/src/leap/bitmask/crypto/tests/eip-service.json b/src/leap/bitmask/crypto/tests/eip-service.json new file mode 100644 index 00000000..24df42a2 --- /dev/null +++ b/src/leap/bitmask/crypto/tests/eip-service.json @@ -0,0 +1,43 @@ +{ + "gateways": [ + { + "capabilities": { + "adblock": false, + "filter_dns": false, + "limited": true, + "ports": [ + "1194", + "443", + "53", + "80" + ], + "protocols": [ + "tcp", + "udp" + ], + "transport": [ + "openvpn" + ], + "user_ips": false + }, + "host": "harrier.cdev.bitmask.net", + "ip_address": "199.254.238.50", + "location": "seattle__wa" + } + ], + "locations": { + "seattle__wa": { + "country_code": "US", + "hemisphere": "N", + "name": "Seattle, WA", + "timezone": "-7" + } + }, + "openvpn_configuration": { + "auth": "SHA1", + "cipher": "AES-128-CBC", + "tls-cipher": "DHE-RSA-AES128-SHA" + }, + "serial": 1, + "version": 1 +} \ No newline at end of file diff --git a/src/leap/bitmask/crypto/tests/fake_provider.py b/src/leap/bitmask/crypto/tests/fake_provider.py new file mode 100755 index 00000000..54af485d --- /dev/null +++ b/src/leap/bitmask/crypto/tests/fake_provider.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# fake_provider.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 . +"""A server faking some of the provider resources and apis, +used for testing Leap Client requests + +It needs that you create a subfolder named 'certs', +and that you place the following files: + +XXX check if in use + +[ ] test-openvpn.pem +[ ] test-provider.json +[ ] test-eip-service.json +""" +import binascii +import json +import os +import sys +import time + +import srp + +from OpenSSL import SSL + +from zope.interface import Interface, Attribute, implements + +from twisted.web.server import Site, Request +from twisted.web.static import File, Data +from twisted.web.resource import Resource +from twisted.internet import reactor + +from leap.common.testing.https_server import where + +# See +# http://twistedmatrix.com/documents/current/web/howto/web-in-60/index.html +# for more examples + +""" +Testing the FAKE_API: +##################### + + 1) register an user + >> curl -d "user[login]=me" -d "user[password_salt]=foo" \ + -d "user[password_verifier]=beef" http://localhost:8000/1/users + << {"errors": null} + + 2) check that if you try to register again, it will fail: + >> curl -d "user[login]=me" -d "user[password_salt]=foo" \ + -d "user[password_verifier]=beef" http://localhost:8000/1/users + << {"errors": {"login": "already taken!"}} + +""" + +# Globals to mock user/sessiondb + +_USERDB = {} +_SESSIONDB = {} + +_here = os.path.split(__file__)[0] + + +safe_unhexlify = lambda x: binascii.unhexlify(x) \ + if (len(x) % 2 == 0) else binascii.unhexlify('0' + x) + + +class IUser(Interface): + """ + Defines the User Interface + """ + login = Attribute("User login.") + salt = Attribute("Password salt.") + verifier = Attribute("Password verifier.") + session = Attribute("Session.") + svr = Attribute("Server verifier.") + + +class User(object): + """ + User object. + We store it in our simple session mocks + """ + + implements(IUser) + + def __init__(self, login, salt, verifier): + self.login = login + self.salt = salt + self.verifier = verifier + self.session = None + self.svr = None + + def set_server_verifier(self, svr): + """ + Adds a svr verifier object to this + User instance + """ + self.svr = svr + + def set_session(self, session): + """ + Adds this instance of User to the + global session dict + """ + _SESSIONDB[session] = self + self.session = session + + +class FakeUsers(Resource): + """ + Resource that handles user registration. + """ + + def __init__(self, name): + self.name = name + + def render_POST(self, request): + """ + Handles POST to the users api resource + Simulates a login. + """ + args = request.args + + login = args['user[login]'][0] + salt = args['user[password_salt]'][0] + verifier = args['user[password_verifier]'][0] + + if login in _USERDB: + request.setResponseCode(422) + return "%s\n" % json.dumps( + {'errors': {'login': 'already taken!'}}) + + print '[server]', login, verifier, salt + user = User(login, salt, verifier) + _USERDB[login] = user + return json.dumps({'errors': None}) + + +def getSession(self, sessionInterface=None): + """ + we overwrite twisted.web.server.Request.getSession method to + put the right cookie name in place + """ + if not self.session: + #cookiename = b"_".join([b'TWISTED_SESSION'] + self.sitepath) + cookiename = b"_".join([b'_session_id'] + self.sitepath) + sessionCookie = self.getCookie(cookiename) + if sessionCookie: + try: + self.session = self.site.getSession(sessionCookie) + except KeyError: + pass + # if it still hasn't been set, fix it up. + if not self.session: + self.session = self.site.makeSession() + self.addCookie(cookiename, self.session.uid, path=b'/') + self.session.touch() + if sessionInterface: + return self.session.getComponent(sessionInterface) + return self.session + + +def get_user(request): + """ + Returns user from the session dict + """ + login = request.args.get('login') + if login: + user = _USERDB.get(login[0], None) + if user: + return user + + request.getSession = getSession.__get__(request, Request) + session = request.getSession() + + user = _SESSIONDB.get(session, None) + return user + + +class FakeSession(Resource): + def __init__(self, name): + """ + Initializes session + """ + self.name = name + + def render_GET(self, request): + """ + Handles GET requests. + """ + return "%s\n" % json.dumps({'errors': None}) + + def render_POST(self, request): + """ + Handles POST requests. + """ + user = get_user(request) + + if not user: + # XXX get real error from demo provider + return json.dumps({'errors': 'no such user'}) + + A = request.args['A'][0] + + _A = safe_unhexlify(A) + _salt = safe_unhexlify(user.salt) + _verifier = safe_unhexlify(user.verifier) + + svr = srp.Verifier( + user.login, + _salt, + _verifier, + _A, + hash_alg=srp.SHA256, + ng_type=srp.NG_1024) + + s, B = svr.get_challenge() + + _B = binascii.hexlify(B) + + print '[server] login = %s' % user.login + print '[server] salt = %s' % user.salt + print '[server] len(_salt) = %s' % len(_salt) + print '[server] vkey = %s' % user.verifier + print '[server] len(vkey) = %s' % len(_verifier) + print '[server] s = %s' % binascii.hexlify(s) + print '[server] B = %s' % _B + print '[server] len(B) = %s' % len(_B) + + # override Request.getSession + request.getSession = getSession.__get__(request, Request) + session = request.getSession() + + user.set_session(session) + user.set_server_verifier(svr) + + # yep, this is tricky. + # some things are *already* unhexlified. + data = { + 'salt': user.salt, + 'B': _B, + 'errors': None} + + return json.dumps(data) + + def render_PUT(self, request): + """ + Handles PUT requests. + """ + # XXX check session??? + user = get_user(request) + + if not user: + print '[server] NO USER' + return json.dumps({'errors': 'no such user'}) + + data = request.content.read() + auth = data.split("client_auth=") + M = auth[1] if len(auth) > 1 else None + # if not H, return + if not M: + return json.dumps({'errors': 'no M proof passed by client'}) + + svr = user.svr + HAMK = svr.verify_session(binascii.unhexlify(M)) + if HAMK is None: + print '[server] verification failed!!!' + raise Exception("Authentication failed!") + #import ipdb;ipdb.set_trace() + + assert svr.authenticated() + print "***" + print '[server] User successfully authenticated using SRP!' + print "***" + + return json.dumps( + {'M2': binascii.hexlify(HAMK), + 'id': '9c943eb9d96a6ff1b7a7030bdeadbeef', + 'errors': None}) + + +class API_Sessions(Resource): + """ + Top resource for the API v1 + """ + def getChild(self, name, request): + return FakeSession(name) + + +class FileModified(File): + def render_GET(self, request): + since = request.getHeader('if-modified-since') + if since: + tsince = time.strptime(since.replace(" GMT", "")) + tfrom = time.strptime(time.ctime(os.path.getmtime(self.path))) + if tfrom > tsince: + return File.render_GET(self, request) + else: + request.setResponseCode(304) + return "" + return File.render_GET(self, request) + + +class OpenSSLServerContextFactory(object): + + def getContext(self): + """ + Create an SSL context. + """ + ctx = SSL.Context(SSL.SSLv23_METHOD) + #ctx = SSL.Context(SSL.TLSv1_METHOD) + ctx.use_certificate_file(where('leaptestscert.pem')) + ctx.use_privatekey_file(where('leaptestskey.pem')) + + return ctx + + +def get_provider_factory(): + """ + Instantiates a Site that serves the resources + that we expect from a valid provider. + Listens on: + * port 8000 for http connections + * port 8443 for https connections + + :rparam: factory for a site + :rtype: Site instance + """ + root = Data("", "") + root.putChild("", root) + root.putChild("provider.json", FileModified( + os.path.join(_here, + "test_provider.json"))) + config = Resource() + config.putChild( + "eip-service.json", + FileModified( + os.path.join(_here, "eip-service.json"))) + apiv1 = Resource() + apiv1.putChild("config", config) + apiv1.putChild("sessions", API_Sessions()) + apiv1.putChild("users", FakeUsers(None)) + apiv1.putChild("cert", FileModified( + os.path.join(_here, + 'openvpn.pem'))) + root.putChild("1", apiv1) + + factory = Site(root) + return factory + + +if __name__ == "__main__": + + from twisted.python import log + log.startLogging(sys.stdout) + + factory = get_provider_factory() + + # regular http (for debugging with curl) + reactor.listenTCP(8000, factory) + reactor.listenSSL(8443, factory, OpenSSLServerContextFactory()) + reactor.run() diff --git a/src/leap/bitmask/crypto/tests/openvpn.pem b/src/leap/bitmask/crypto/tests/openvpn.pem new file mode 100644 index 00000000..a95e9370 --- /dev/null +++ b/src/leap/bitmask/crypto/tests/openvpn.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAIGJ8Dg+DtemMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTMwNjI2MjAyMDIyWhcNMTgwNjI2MjAyMDIyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAxJaN0lWjFu+3j48c0WG8BvmPUf026Xli5d5NE4EjGsirwfre0oTeWZT9 +WRxqLGd2wDh6Mc9r6UqH6dwqLZKbsgwB5zI2lag7UWFttJF1U1c6AJynhaLMoy73 +sL9USTmQ57iYRFrVP/nGj9/L6I1XnV6midPi7a5aZreH9q8dWaAhmc9eFDU+Y4vS +sTFS6aomajLrI6YWo5toKqLq8IMryD03IM78a7gJtLgfWs+pYZRUBlM5JaYX98eX +mVPAYYH9krWxLVN3hTt1ngECzK+epo275zQJh960/2fNCfVJSXqSXcficLs+bR7t +FEkNuOP1hFV6LuoLL+k5Su+hp5kXMYZTvYYDpW4nPJoBdSG1w5O5IxO6zh+9VLB7 +oLrlgoyWvBoou5coCBpZVU6UyWcOx58kuZF8wNr0GgdvWAFwOGVuVG5jmcVdhaKC +0C8NxHrxlhcrcp0zwtDaOxfmZfcxiXs35iwUip5vS18Nv+XBK8ad9T79Ox8nSzP3 +RGPVDpExz7gPbZglqSe47XBIk0ZuIzgOgYpJj4JrpoewoIYb+OmUgI7UZjoGsMrV ++B2BqOKs7kF0HW3i5bR9YAi0ZYvnhQgjBtwCKm4zvLqwuPZHz9VWgIk6uezgStCP +WyzQ8IcopK49fOjcKa6JT5JRU+27paIZf1BkQsTkJy/Nti4TvwMCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUEgXSd3Yl3xAzbkWa7xeNe27d99cwdQYDVR0jBG4wbIAUEgXS +d3Yl3xAzbkWa7xeNe27d99ehSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCB +ifA4Pg7XpjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQA6Vl9Ve4Qe +ewzXAxr0BabFRhtIuF7DV+/niT46qJhW2KgYe6rwZqdAhEbgH3kTPJ5JmmcUnAEH +nmrfoku/YAb5ObfdHUACsHy4cvSvFwBUQ9vXP6+oOFJhrGW4uzRI2pHGvnqB3lQ0 +JEPmPwduBCI5reRYauPbd4Wl4VhLGrjELb4JQZL24Q5ehXMnv415m7+aMkLzT2IA +p6B2xgRR+JAeUdyCNOV1f5AqJWyAUJPWGR0e1OTKNfc49+2skK0NmzrpGsoktSHa +uN6vGBCVGiZh7BTYblWMG5q9Am7idcdmC2fdpIf5yj7CKzV7WIPxPs0I7TuRcr41 +pUBLCAElcyCPB89lySol2BDs4gk4wZs4y2shUs3o0+mIpw/6o8tQF/9IL8ALkLqr +q9SuND7O1RXcg74o3HeVmRKtoI/KdgaVhJ0rFvcq83ftfu3KMyWB6SOKOu6ZYON8 +AcSjsDDpnDrwGFvjAYHiTkS9NaaJC1/g7Y6jjhxmbTkXPA6V8MvLKQiOvqk/9gCh +85FHsFkElIYnH6fbHIRxg20cnqmddTd+H5HgBIlhiKWuydtuoQFwzR/D3ypgLBaB +OWLcBP7I+RYhKlJFIWnfiyB0xbyI4W/UfL8p8jQI8TE9oIlm3WqxJXfebDEDEstj +8nS4Fb3G5Wr4pZMjfbtmBSAgHeWH6B90jg== +-----END CERTIFICATE----- diff --git a/src/leap/bitmask/crypto/tests/test_provider.json b/src/leap/bitmask/crypto/tests/test_provider.json new file mode 100644 index 00000000..c37bef8f --- /dev/null +++ b/src/leap/bitmask/crypto/tests/test_provider.json @@ -0,0 +1,15 @@ +{ + "api_uri": "https://localhost:8443", + "api_version": "1", + "ca_cert_fingerprint": "SHA256: 0f17c033115f6b76ff67871872303ff65034efe7dd1b910062ca323eb4da5c7e", + "ca_cert_uri": "https://bitmask.net/ca.crt", + "default_language": "en", + "domain": "example.com", + "enrollment_policy": "open", + "name": { + "en": "Bitmask" + }, + "services": [ + "openvpn" + ] +} diff --git a/src/leap/bitmask/crypto/tests/test_srpauth.py b/src/leap/bitmask/crypto/tests/test_srpauth.py new file mode 100644 index 00000000..e3258fd3 --- /dev/null +++ b/src/leap/bitmask/crypto/tests/test_srpauth.py @@ -0,0 +1,790 @@ +# -*- coding: utf-8 -*- +# test_srpauth.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 . +""" +Tests for: + * leap/crypto/srpauth.py +""" +try: + import unittest2 as unittest +except ImportError: + import unittest +import os +import sys +import binascii +import requests +import mock + +from mock import MagicMock +from nose.twistedtools import reactor, deferred +from twisted.python import log +from twisted.internet import threads +from functools import partial +from requests.models import Response +from simplejson.decoder import JSONDecodeError + +from leap.common.testing.https_server import where +from leap.config.providerconfig import ProviderConfig +from leap.crypto import srpregister, srpauth +from leap.crypto.tests import fake_provider +from leap.util.request_helpers import get_content + +log.startLogging(sys.stdout) + + +def _get_capath(): + return where("cacert.pem") + +_here = os.path.split(__file__)[0] + + +class ImproperlyConfiguredError(Exception): + """ + Raised if the test provider is missing configuration + """ + + +class SRPAuthTestCase(unittest.TestCase): + """ + Tests for the SRPAuth class + """ + __name__ = "SRPAuth tests" + + def setUp(self): + """ + Sets up this TestCase with a simple and faked provider instance: + + * runs a threaded reactor + * loads a mocked ProviderConfig that points to the certs in the + leap.common.testing module. + """ + factory = fake_provider.get_provider_factory() + http = reactor.listenTCP(0, factory) + https = reactor.listenSSL( + 0, factory, + fake_provider.OpenSSLServerContextFactory()) + get_port = lambda p: p.getHost().port + self.http_port = get_port(http) + self.https_port = get_port(https) + + provider = ProviderConfig() + provider.get_ca_cert_path = mock.create_autospec( + provider.get_ca_cert_path) + provider.get_ca_cert_path.return_value = _get_capath() + + provider.get_api_uri = mock.create_autospec( + provider.get_api_uri) + provider.get_api_uri.return_value = self._get_https_uri() + + loaded = provider.load(path=os.path.join( + _here, "test_provider.json")) + if not loaded: + raise ImproperlyConfiguredError( + "Could not load test provider config") + self.register = srpregister.SRPRegister(provider_config=provider) + self.provider = provider + self.TEST_USER = "register_test_auth" + self.TEST_PASS = "pass" + + # Reset the singleton + srpauth.SRPAuth._SRPAuth__instance = None + self.auth = srpauth.SRPAuth(self.provider) + self.auth_backend = self.auth._SRPAuth__instance + + self.old_post = self.auth_backend._session.post + self.old_put = self.auth_backend._session.put + self.old_delete = self.auth_backend._session.delete + + self.old_start_auth = self.auth_backend._start_authentication + self.old_proc_challenge = self.auth_backend._process_challenge + self.old_extract_data = self.auth_backend._extract_data + self.old_verify_session = self.auth_backend._verify_session + self.old_auth_preproc = self.auth_backend._authentication_preprocessing + self.old_get_sid = self.auth_backend.get_session_id + self.old_cookie_get = self.auth_backend._session.cookies.get + self.old_auth = self.auth_backend.authenticate + + def tearDown(self): + self.auth_backend._session.post = self.old_post + self.auth_backend._session.put = self.old_put + self.auth_backend._session.delete = self.old_delete + + self.auth_backend._start_authentication = self.old_start_auth + self.auth_backend._process_challenge = self.old_proc_challenge + self.auth_backend._extract_data = self.old_extract_data + self.auth_backend._verify_session = self.old_verify_session + self.auth_backend._authentication_preprocessing = self.old_auth_preproc + self.auth_backend.get_session_id = self.old_get_sid + self.auth_backend._session.cookies.get = self.old_cookie_get + self.auth_backend.authenticate = self.old_auth + + # helper methods + + def _get_https_uri(self): + """ + Returns a https uri with the right https port initialized + """ + return "https://localhost:%s" % (self.https_port,) + + # Auth tests + + def _prepare_auth_test(self, code=200, side_effect=None): + """ + Creates the needed defers to test several test situations. It + adds up to the auth preprocessing step. + + :param code: status code for the response of POST in requests + :type code: int + :param side_effect: side effect triggered by the POST method + in requests + :type side_effect: some kind of Exception + + :returns: the defer that is created + :rtype: defer.Deferred + """ + res = Response() + res.status_code = code + self.auth_backend._session.post = mock.create_autospec( + self.auth_backend._session.post, + return_value=res, + side_effect=side_effect) + + d = threads.deferToThread(self.register.register_user, + self.TEST_USER, + self.TEST_PASS) + + def wrapper_preproc(*args): + return threads.deferToThread( + self.auth_backend._authentication_preprocessing, + self.TEST_USER, self.TEST_PASS) + + d.addCallback(wrapper_preproc) + + return d + + def test_safe_unhexlify(self): + input_value = "somestring" + test_value = binascii.hexlify(input_value) + self.assertEqual( + self.auth_backend._safe_unhexlify(test_value), + input_value) + + def test_safe_unhexlify_not_raises(self): + input_value = "somestring" + test_value = binascii.hexlify(input_value)[:-1] + + with self.assertRaises(TypeError): + binascii.unhexlify(test_value) + + self.auth_backend._safe_unhexlify(test_value) + + def test_preprocessing_loads_a(self): + self.assertEqual(self.auth_backend._srp_a, None) + self.auth_backend._authentication_preprocessing("user", "pass") + self.assertIsNotNone(self.auth_backend._srp_a) + self.assertTrue(len(self.auth_backend._srp_a) > 0) + + @deferred() + def test_start_authentication(self): + d = threads.deferToThread(self.register.register_user, self.TEST_USER, + self.TEST_PASS) + + def wrapper_preproc(*args): + return threads.deferToThread( + self.auth_backend._authentication_preprocessing, + self.TEST_USER, self.TEST_PASS) + + d.addCallback(wrapper_preproc) + + def wrapper(_): + return threads.deferToThread( + self.auth_backend._start_authentication, + None, self.TEST_USER) + + d.addCallback(wrapper) + return d + + @deferred() + def test_start_authentication_fails_connerror(self): + d = self._prepare_auth_test( + side_effect=requests.exceptions.ConnectionError()) + + def wrapper(_): + with self.assertRaises(srpauth.SRPAuthConnectionError): + self.auth_backend._start_authentication(None, self.TEST_USER) + + d.addCallback(partial(threads.deferToThread, wrapper)) + return d + + @deferred() + def test_start_authentication_fails_any_error(self): + d = self._prepare_auth_test(side_effect=Exception()) + + def wrapper(_): + with self.assertRaises(srpauth.SRPAuthenticationError): + self.auth_backend._start_authentication(None, self.TEST_USER) + + d.addCallback(partial(threads.deferToThread, wrapper)) + return d + + @deferred() + def test_start_authentication_fails_unknown_user(self): + d = self._prepare_auth_test(422) + + def wrapper(_): + with self.assertRaises(srpauth.SRPAuthUnknownUser): + with mock.patch('leap.util.request_helpers.get_content', + new=mock.create_autospec(get_content)) as \ + content: + content.return_value = ("{}", 0) + + self.auth_backend._start_authentication( + None, self.TEST_USER) + + d.addCallback(partial(threads.deferToThread, wrapper)) + return d + + @deferred() + def test_start_authentication_fails_errorcode(self): + d = self._prepare_auth_test(302) + + def wrapper(_): + with self.assertRaises(srpauth.SRPAuthBadStatusCode): + with mock.patch('leap.util.request_helpers.get_content', + new=mock.create_autospec(get_content)) as \ + content: + content.return_value = ("{}", 0) + + self.auth_backend._start_authentication(None, + self.TEST_USER) + + d.addCallback(partial(threads.deferToThread, wrapper)) + return d + + @deferred() + def test_start_authentication_fails_no_salt(self): + d = self._prepare_auth_test(200) + + def wrapper(_): + with self.assertRaises(srpauth.SRPAuthNoSalt): + with mock.patch('leap.util.request_helpers.get_content', + new=mock.create_autospec(get_content)) as \ + content: + content.return_value = ("{}", 0) + + self.auth_backend._start_authentication(None, + self.TEST_USER) + + d.addCallback(partial(threads.deferToThread, wrapper)) + return d + + @deferred() + def test_start_authentication_fails_no_B(self): + d = self._prepare_auth_test(200) + + def wrapper(_): + with self.assertRaises(srpauth.SRPAuthNoB): + with mock.patch('leap.util.request_helpers.get_content', + new=mock.create_autospec(get_content)) as \ + content: + content.return_value = ('{"salt": ""}', 0) + + self.auth_backend._start_authentication(None, + self.TEST_USER) + + d.addCallback(partial(threads.deferToThread, wrapper)) + return d + + @deferred() + def test_start_authentication_correct_saltb(self): + d = self._prepare_auth_test(200) + + test_salt = "12345" + test_B = "67890" + + def wrapper(_): + with mock.patch('leap.util.request_helpers.get_content', + new=mock.create_autospec(get_content)) as \ + content: + content.return_value = ('{"salt":"%s", "B":"%s"}' % (test_salt, + test_B), + 0) + + salt, B = self.auth_backend._start_authentication( + None, + self.TEST_USER) + self.assertEqual(salt, test_salt) + self.assertEqual(B, test_B) + + d.addCallback(partial(threads.deferToThread, wrapper)) + return d + + def _prepare_auth_challenge(self): + """ + Creates the needed defers to test several test situations. It + adds up to the start authentication step. + + :returns: the defer that is created + :rtype: defer.Deferred + """ + d = threads.deferToThread(self.register.register_user, + self.TEST_USER, + self.TEST_PASS) + + def wrapper_preproc(*args): + return threads.deferToThread( + self.auth_backend._authentication_preprocessing, + self.TEST_USER, self.TEST_PASS) + + d.addCallback(wrapper_preproc) + + def wrapper_start(*args): + return threads.deferToThread( + self.auth_backend._start_authentication, + None, self.TEST_USER) + + d.addCallback(wrapper_start) + + return d + + @deferred() + def test_process_challenge_wrong_saltb(self): + d = self._prepare_auth_challenge() + + def wrapper(salt_B): + with self.assertRaises(srpauth.SRPAuthBadDataFromServer): + self.auth_backend._process_challenge("", + username=self.TEST_USER) + + d.addCallback(partial(threads.deferToThread, wrapper)) + return d + + @deferred() + def test_process_challenge_requests_problem_raises(self): + d = self._prepare_auth_challenge() + + self.auth_backend._session.put = mock.create_autospec( + self.auth_backend._session.put, + side_effect=requests.exceptions.ConnectionError()) + + def wrapper(salt_B): + with self.assertRaises(srpauth.SRPAuthConnectionError): + self.auth_backend._process_challenge(salt_B, + username=self.TEST_USER) + + d.addCallback(partial(threads.deferToThread, wrapper)) + + return d + + @deferred() + def test_process_challenge_json_decode_error(self): + d = self._prepare_auth_challenge() + + def wrapper(salt_B): + with mock.patch('leap.util.request_helpers.get_content', + new=mock.create_autospec(get_content)) as \ + content: + content.return_value = ("{", 0) + content.side_effect = JSONDecodeError("", "", 0) + + with self.assertRaises(srpauth.SRPAuthJSONDecodeError): + self.auth_backend._process_challenge( + salt_B, + username=self.TEST_USER) + + d.addCallback(partial(threads.deferToThread, wrapper)) + + return d + + @deferred() + def test_process_challenge_bad_password(self): + d = self._prepare_auth_challenge() + + res = Response() + res.status_code = 422 + self.auth_backend._session.put = mock.create_autospec( + self.auth_backend._session.put, + return_value=res) + + def wrapper(salt_B): + with mock.patch('leap.util.request_helpers.get_content', + new=mock.create_autospec(get_content)) as \ + content: + content.return_value = ("", 0) + with self.assertRaises(srpauth.SRPAuthBadPassword): + self.auth_backend._process_challenge( + salt_B, + username=self.TEST_USER) + + d.addCallback(partial(threads.deferToThread, wrapper)) + + return d + + @deferred() + def test_process_challenge_bad_password2(self): + d = self._prepare_auth_challenge() + + res = Response() + res.status_code = 422 + self.auth_backend._session.put = mock.create_autospec( + self.auth_backend._session.put, + return_value=res) + + def wrapper(salt_B): + with mock.patch('leap.util.request_helpers.get_content', + new=mock.create_autospec(get_content)) as \ + content: + content.return_value = ("[]", 0) + with self.assertRaises(srpauth.SRPAuthBadPassword): + self.auth_backend._process_challenge( + salt_B, + username=self.TEST_USER) + + d.addCallback(partial(threads.deferToThread, wrapper)) + + return d + + @deferred() + def test_process_challenge_other_error_code(self): + d = self._prepare_auth_challenge() + + res = Response() + res.status_code = 300 + self.auth_backend._session.put = mock.create_autospec( + self.auth_backend._session.put, + return_value=res) + + def wrapper(salt_B): + with mock.patch('leap.util.request_helpers.get_content', + new=mock.create_autospec(get_content)) as \ + content: + content.return_value = ("{}", 0) + with self.assertRaises(srpauth.SRPAuthBadStatusCode): + self.auth_backend._process_challenge( + salt_B, + username=self.TEST_USER) + + d.addCallback(partial(threads.deferToThread, wrapper)) + + return d + + @deferred() + def test_process_challenge(self): + d = self._prepare_auth_challenge() + + def wrapper(salt_B): + self.auth_backend._process_challenge(salt_B, + username=self.TEST_USER) + + d.addCallback(partial(threads.deferToThread, wrapper)) + + return d + + def test_extract_data_wrong_data(self): + with self.assertRaises(srpauth.SRPAuthBadDataFromServer): + self.auth_backend._extract_data(None) + + with self.assertRaises(srpauth.SRPAuthBadDataFromServer): + self.auth_backend._extract_data("") + + def test_extract_data_fails_on_wrong_data_from_server(self): + with self.assertRaises(srpauth.SRPAuthBadDataFromServer): + self.auth_backend._extract_data({}) + + with self.assertRaises(srpauth.SRPAuthBadDataFromServer): + self.auth_backend._extract_data({"M2": ""}) + + def test_extract_data_sets_uidtoken(self): + test_uid = "someuid" + test_m2 = "somem2" + test_token = "sometoken" + test_data = { + "M2": test_m2, + "id": test_uid, + "token": test_token + } + m2 = self.auth_backend._extract_data(test_data) + + self.assertEqual(m2, test_m2) + self.assertEqual(self.auth_backend.get_uid(), test_uid) + self.assertEqual(self.auth_backend.get_uid(), + self.auth.get_uid()) + self.assertEqual(self.auth_backend.get_token(), test_token) + self.assertEqual(self.auth_backend.get_token(), + self.auth.get_token()) + + def _prepare_verify_session(self): + """ + Prepares the tests for verify session with needed steps + before. It adds up to the extract_data step. + + :returns: The defer to chain to + :rtype: defer.Deferred + """ + d = self._prepare_auth_challenge() + + def wrapper_proc_challenge(salt_B): + return self.auth_backend._process_challenge( + salt_B, + username=self.TEST_USER) + + def wrapper_extract_data(data): + return self.auth_backend._extract_data(data) + + d.addCallback(partial(threads.deferToThread, wrapper_proc_challenge)) + d.addCallback(partial(threads.deferToThread, wrapper_extract_data)) + + return d + + @deferred() + def test_verify_session_unhexlifiable_m2(self): + d = self._prepare_verify_session() + + def wrapper(M2): + with self.assertRaises(srpauth.SRPAuthBadDataFromServer): + self.auth_backend._verify_session("za") # unhexlifiable value + + d.addCallback(wrapper) + + return d + + @deferred() + def test_verify_session_unverifiable_m2(self): + d = self._prepare_verify_session() + + def wrapper(M2): + with self.assertRaises(srpauth.SRPAuthVerificationFailed): + # Correctly unhelifiable value, but not for verifying the + # session + self.auth_backend._verify_session("abc12") + + d.addCallback(wrapper) + + return d + + @deferred() + def test_verify_session_fails_on_no_session_id(self): + d = self._prepare_verify_session() + + def wrapper(M2): + self.auth_backend._session.cookies.get = mock.create_autospec( + self.auth_backend._session.cookies.get, + return_value=None) + with self.assertRaises(srpauth.SRPAuthNoSessionId): + self.auth_backend._verify_session(M2) + + d.addCallback(wrapper) + + return d + + @deferred() + def test_verify_session_session_id(self): + d = self._prepare_verify_session() + + test_session_id = "12345" + + def wrapper(M2): + self.auth_backend._session.cookies.get = mock.create_autospec( + self.auth_backend._session.cookies.get, + return_value=test_session_id) + self.auth_backend._verify_session(M2) + self.assertEqual(self.auth_backend.get_session_id(), + test_session_id) + self.assertEqual(self.auth_backend.get_session_id(), + self.auth.get_session_id()) + + d.addCallback(wrapper) + + return d + + @deferred() + def test_verify_session(self): + d = self._prepare_verify_session() + + def wrapper(M2): + self.auth_backend._verify_session(M2) + + d.addCallback(wrapper) + + return d + + @deferred() + def test_authenticate(self): + self.auth_backend._authentication_preprocessing = mock.create_autospec( + self.auth_backend._authentication_preprocessing, + return_value=None) + self.auth_backend._start_authentication = mock.create_autospec( + self.auth_backend._start_authentication, + return_value=None) + self.auth_backend._process_challenge = mock.create_autospec( + self.auth_backend._process_challenge, + return_value=None) + self.auth_backend._extract_data = mock.create_autospec( + self.auth_backend._extract_data, + return_value=None) + self.auth_backend._verify_session = mock.create_autospec( + self.auth_backend._verify_session, + return_value=None) + + d = self.auth_backend.authenticate(self.TEST_USER, self.TEST_PASS) + + def check(*args): + self.auth_backend._authentication_preprocessing.\ + assert_called_once_with( + username=self.TEST_USER, + password=self.TEST_PASS + ) + self.auth_backend._start_authentication.assert_called_once_with( + None, + username=self.TEST_USER) + self.auth_backend._process_challenge.assert_called_once_with( + None, + username=self.TEST_USER) + self.auth_backend._extract_data.assert_called_once_with( + None) + self.auth_backend._verify_session.assert_called_once_with(None) + + d.addCallback(check) + + return d + + @deferred() + def test_logout_fails_if_not_logged_in(self): + + def wrapper(*args): + with self.assertRaises(AssertionError): + self.auth_backend.logout() + + d = threads.deferToThread(wrapper) + return d + + @deferred() + def test_logout_traps_delete(self): + self.auth_backend.get_session_id = mock.create_autospec( + self.auth_backend.get_session_id, + return_value="1234") + self.auth_backend._session.delete = mock.create_autospec( + self.auth_backend._session.delete, + side_effect=Exception()) + + def wrapper(*args): + self.auth_backend.logout() + + d = threads.deferToThread(wrapper) + return d + + @deferred() + def test_logout_clears(self): + self.auth_backend._session_id = "1234" + + def wrapper(*args): + old_session = self.auth_backend._session + self.auth_backend.logout() + self.assertIsNone(self.auth_backend.get_session_id()) + self.assertIsNone(self.auth_backend.get_uid()) + self.assertNotEqual(old_session, self.auth_backend._session) + + d = threads.deferToThread(wrapper) + return d + + +class SRPAuthSingletonTestCase(unittest.TestCase): + def setUp(self): + self.old_auth = srpauth.SRPAuth._SRPAuth__impl.authenticate + + def tearDown(self): + srpauth.SRPAuth._SRPAuth__impl.authenticate = self.old_auth + + def test_singleton(self): + obj1 = srpauth.SRPAuth(ProviderConfig()) + obj2 = srpauth.SRPAuth(ProviderConfig()) + self.assertEqual(obj1._SRPAuth__instance, obj2._SRPAuth__instance) + + @deferred() + def test_authenticate_notifies_gui(self): + auth = srpauth.SRPAuth(ProviderConfig()) + auth._SRPAuth__instance.authenticate = mock.create_autospec( + auth._SRPAuth__instance.authenticate, + return_value=threads.deferToThread(lambda: None)) + auth._gui_notify = mock.create_autospec( + auth._gui_notify) + + d = auth.authenticate("", "") + + def check(*args): + auth._gui_notify.assert_called_once_with(None) + + d.addCallback(check) + return d + + @deferred() + def test_authenticate_errsback(self): + auth = srpauth.SRPAuth(ProviderConfig()) + auth._SRPAuth__instance.authenticate = mock.create_autospec( + auth._SRPAuth__instance.authenticate, + return_value=threads.deferToThread(MagicMock( + side_effect=Exception()))) + auth._gui_notify = mock.create_autospec( + auth._gui_notify) + auth._errback = mock.create_autospec( + auth._errback) + + d = auth.authenticate("", "") + + def check(*args): + self.assertFalse(auth._gui_notify.called) + self.assertEqual(auth._errback.call_count, 1) + + d.addCallback(check) + return d + + @deferred() + def test_authenticate_runs_cleanly_when_raises(self): + auth = srpauth.SRPAuth(ProviderConfig()) + auth._SRPAuth__instance.authenticate = mock.create_autospec( + auth._SRPAuth__instance.authenticate, + return_value=threads.deferToThread(MagicMock( + side_effect=Exception()))) + + d = auth.authenticate("", "") + + return d + + @deferred() + def test_authenticate_runs_cleanly(self): + auth = srpauth.SRPAuth(ProviderConfig()) + auth._SRPAuth__instance.authenticate = mock.create_autospec( + auth._SRPAuth__instance.authenticate, + return_value=threads.deferToThread(MagicMock())) + + d = auth.authenticate("", "") + + return d + + def test_logout(self): + auth = srpauth.SRPAuth(ProviderConfig()) + auth._SRPAuth__instance.logout = mock.create_autospec( + auth._SRPAuth__instance.logout) + + self.assertTrue(auth.logout()) + + def test_logout_rets_false_when_raises(self): + auth = srpauth.SRPAuth(ProviderConfig()) + auth._SRPAuth__instance.logout = mock.create_autospec( + auth._SRPAuth__instance.logout, + side_effect=Exception()) + + self.assertFalse(auth.logout()) diff --git a/src/leap/bitmask/crypto/tests/test_srpregister.py b/src/leap/bitmask/crypto/tests/test_srpregister.py new file mode 100644 index 00000000..66b815f2 --- /dev/null +++ b/src/leap/bitmask/crypto/tests/test_srpregister.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +# test_srpregister.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 . +""" +Tests for: + * leap/crypto/srpregister.py +""" +try: + import unittest2 as unittest +except ImportError: + import unittest +import os +import sys + +from mock import MagicMock +from nose.twistedtools import reactor, deferred +from twisted.python import log +from twisted.internet import threads + +from leap.common.testing.https_server import where +from leap.config.providerconfig import ProviderConfig +from leap.crypto import srpregister, srpauth +from leap.crypto.tests import fake_provider + +log.startLogging(sys.stdout) + + +def _get_capath(): + return where("cacert.pem") + +_here = os.path.split(__file__)[0] + + +class ImproperlyConfiguredError(Exception): + """ + Raised if the test provider is missing configuration + """ + + +class SRPTestCase(unittest.TestCase): + """ + Tests for the SRPRegister class + """ + __name__ = "SRPRegister tests" + + @classmethod + def setUpClass(cls): + """ + Sets up this TestCase with a simple and faked provider instance: + + * runs a threaded reactor + * loads a mocked ProviderConfig that points to the certs in the + leap.common.testing module. + """ + factory = fake_provider.get_provider_factory() + http = reactor.listenTCP(8001, factory) + https = reactor.listenSSL( + 0, factory, + fake_provider.OpenSSLServerContextFactory()) + get_port = lambda p: p.getHost().port + cls.http_port = get_port(http) + cls.https_port = get_port(https) + + provider = ProviderConfig() + provider.get_ca_cert_path = MagicMock() + provider.get_ca_cert_path.return_value = _get_capath() + + provider.get_api_uri = MagicMock() + provider.get_api_uri.return_value = cls._get_https_uri() + + loaded = provider.load(path=os.path.join( + _here, "test_provider.json")) + if not loaded: + raise ImproperlyConfiguredError( + "Could not load test provider config") + cls.register = srpregister.SRPRegister(provider_config=provider) + + cls.auth = srpauth.SRPAuth(provider) + + # helper methods + + @classmethod + def _get_https_uri(cls): + """ + Returns a https uri with the right https port initialized + """ + return "https://localhost:%s" % (cls.https_port,) + + # Register tests + + def test_none_port(self): + provider = ProviderConfig() + provider.get_api_uri = MagicMock() + provider.get_api_uri.return_value = "http://localhost/" + loaded = provider.load(path=os.path.join( + _here, "test_provider.json")) + if not loaded: + raise ImproperlyConfiguredError( + "Could not load test provider config") + + register = srpregister.SRPRegister(provider_config=provider) + self.assertEquals(register._port, "443") + + @deferred() + def test_wrong_cert(self): + provider = ProviderConfig() + loaded = provider.load(path=os.path.join( + _here, "test_provider.json")) + provider.get_ca_cert_path = MagicMock() + provider.get_ca_cert_path.return_value = os.path.join( + _here, + "wrongcert.pem") + provider.get_api_uri = MagicMock() + provider.get_api_uri.return_value = self._get_https_uri() + if not loaded: + raise ImproperlyConfiguredError( + "Could not load test provider config") + + register = srpregister.SRPRegister(provider_config=provider) + d = threads.deferToThread(register.register_user, "foouser_firsttime", + "barpass") + d.addCallback(self.assertFalse) + return d + + @deferred() + def test_register_user(self): + """ + Checks if the registration of an unused name works as expected when + it is the first time that we attempt to register that user, as well as + when we request a user that is taken. + """ + # pristine registration + d = threads.deferToThread(self.register.register_user, + "foouser_firsttime", + "barpass") + d.addCallback(self.assertTrue) + return d + + @deferred() + def test_second_register_user(self): + # second registration attempt with the same user should return errors + d = threads.deferToThread(self.register.register_user, + "foouser_second", + "barpass") + d.addCallback(self.assertTrue) + + # FIXME currently we are catching this in an upper layer, + # we could bring the error validation to the SRPRegister class + def register_wrapper(_): + return threads.deferToThread(self.register.register_user, + "foouser_second", + "barpass") + d.addCallback(register_wrapper) + d.addCallback(self.assertFalse) + return d + + @deferred() + def test_correct_http_uri(self): + """ + Checks that registration autocorrect http uris to https ones. + """ + HTTP_URI = "http://localhost:%s" % (self.https_port, ) + HTTPS_URI = "https://localhost:%s/1/users" % (self.https_port, ) + provider = ProviderConfig() + provider.get_ca_cert_path = MagicMock() + provider.get_ca_cert_path.return_value = _get_capath() + provider.get_api_uri = MagicMock() + + # we introduce a http uri in the config file... + provider.get_api_uri.return_value = HTTP_URI + loaded = provider.load(path=os.path.join( + _here, "test_provider.json")) + if not loaded: + raise ImproperlyConfiguredError( + "Could not load test provider config") + + register = srpregister.SRPRegister(provider_config=provider) + + # ... and we check that we're correctly taking the HTTPS protocol + # instead + reg_uri = register._get_registration_uri() + self.assertEquals(reg_uri, HTTPS_URI) + register._get_registration_uri = MagicMock(return_value=HTTPS_URI) + d = threads.deferToThread(register.register_user, "test_failhttp", + "barpass") + d.addCallback(self.assertTrue) + + return d diff --git a/src/leap/bitmask/crypto/tests/wrongcert.pem b/src/leap/bitmask/crypto/tests/wrongcert.pem new file mode 100644 index 00000000..e6cff38a --- /dev/null +++ b/src/leap/bitmask/crypto/tests/wrongcert.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAIWZus5EIXNtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTMwNjI1MTc0NjExWhcNMTgwNjI1MTc0NjExWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEA2ObM7ESjyuxFZYD/Y68qOPQgjgggW+cdXfBpU2p4n7clsrUeMhWdW40Y +77Phzor9VOeqs3ZpHuyLzsYVp/kFDm8tKyo2ah5fJwzL0VCSLYaZkUQQ7GNUmTCk +furaxl8cQx/fg395V7/EngsS9B3/y5iHbctbA4MnH3jaotO5EGeo6hw7/eyCotQ9 +KbBV9GJMcY94FsXBCmUB+XypKklWTLhSaS6Cu4Fo8YLW6WmcnsyEOGS2F7WVf5at +7CBWFQZHaSgIBLmc818/mDYCnYmCVMFn/6Ndx7V2NTlz+HctWrQn0dmIOnCUeCwS +wXq9PnBR1rSx/WxwyF/WpyjOFkcIo7vm72kS70pfrYsXcZD4BQqkXYj3FyKnPt3O +ibLKtCxL8/83wOtErPcYpG6LgFkgAAlHQ9MkUi5dbmjCJtpqQmlZeK1RALdDPiB3 +K1KZimrGsmcE624dJxUIOJJpuwJDy21F8kh5ZAsAtE1prWETrQYNElNFjQxM83rS +ZR1Ql2MPSB4usEZT57+KvpEzlOnAT3elgCg21XrjSFGi14hCEao4g2OEZH5GAwm5 +frf6UlSRZ/g3tLTfI8Hv1prw15W2qO+7q7SBAplTODCRk+Yb0YoA2mMM/QXBUcXs +vKEDLSSxzNIBi3T62l39RB/ml+gPKo87ZMDivex1ZhrcJc3Yu3sCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUPjE+4pun+8FreIdpoR8v6N7xKtUwdQYDVR0jBG4wbIAUPjE+ +4pun+8FreIdpoR8v6N7xKtWhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCF +mbrORCFzbTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCpvCPdtvXJ +muTj379TZuCJs7/l0FhA7AHa1WAlHjsXHaA7N0+3ZWAbdtXDsowal6S+ldgU/kfV +Lq7NrRq+amJWC7SYj6cvVwhrSwSvu01fe/TWuOzHrRv1uTfJ/VXLonVufMDd9opo +bhqYxMaxLdIx6t/MYmZH4Wpiq0yfZuv//M8i7BBl/qvaWbLhg0yVAKRwjFvf59h6 +6tRFCLddELOIhLDQtk8zMbioPEbfAlKdwwP8kYGtDGj6/9/YTd/oTKRdgHuwyup3 +m0L20Y6LddC+tb0WpK5EyrNbCbEqj1L4/U7r6f/FKNA3bx6nfdXbscaMfYonKAKg +1cRrRg45sErmCz0QyTnWzXyvbjR4oQRzyW3kJ1JZudZ+AwOi00J5FYa3NiLuxl1u +gIGKWSrASQWhEdpa1nlCgX7PhdaQgYjEMpQvA0GCA0OF5JDu8en1yZqsOt1hCLIN +lkz/5jKPqrclY5hV99bE3hgCHRmIPNHCZG3wbZv2yJKxJX1YLMmQwAmSh2N7YwGG +yXRvCxQs5ChPHyRairuf/5MZCZnSVb45ppTVuNUijsbflKRUgfj/XvfqQ22f+C9N +Om2dmNvAiS2TOIfuP47CF2OUa5q4plUwmr+nyXQGM0SIoHNCj+MBdFfb3oxxAtI+ +SLhbnzQv5e84Doqz3YF0XW8jyR7q8GFLNA== +-----END CERTIFICATE----- diff --git a/src/leap/bitmask/gui/__init__.py b/src/leap/bitmask/gui/__init__.py new file mode 100644 index 00000000..4b289442 --- /dev/null +++ b/src/leap/bitmask/gui/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# __init__.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 . +""" +init file for leap.gui +""" +app = __import__("app", globals(), locals(), [], 2) +__all__ = [app] diff --git a/src/leap/bitmask/gui/loggerwindow.py b/src/leap/bitmask/gui/loggerwindow.py new file mode 100644 index 00000000..fcbdbf19 --- /dev/null +++ b/src/leap/bitmask/gui/loggerwindow.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +# loggerwindow.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 . + +""" +History log window +""" +import logging + +from PySide import QtGui +from ui_loggerwindow import Ui_LoggerWindow +from leap.common.check import leap_assert, leap_assert_type +from leap.util.leap_log_handler import LeapLogHandler + +logger = logging.getLogger(__name__) + + +class LoggerWindow(QtGui.QDialog): + """ + Window that displays a history of the logged messages in the app. + """ + def __init__(self, handler): + """ + Initialize the widget with the custom handler. + + :param handler: Custom handler that supports history and signal. + :type handler: LeapLogHandler. + """ + QtGui.QDialog.__init__(self) + leap_assert(handler, "We need a handler for the logger window") + leap_assert_type(handler, LeapLogHandler) + + # Load UI + self.ui = Ui_LoggerWindow() + self.ui.setupUi(self) + + # Make connections + self.ui.btnSave.clicked.connect(self._save_log_to_file) + self.ui.btnDebug.toggled.connect(self._load_history), + self.ui.btnInfo.toggled.connect(self._load_history), + self.ui.btnWarning.toggled.connect(self._load_history), + self.ui.btnError.toggled.connect(self._load_history), + self.ui.btnCritical.toggled.connect(self._load_history) + + # Load logging history and connect logger with the widget + self._logging_handler = handler + self._connect_to_handler() + self._load_history() + + def _connect_to_handler(self): + """ + This method connects the loggerwindow with the handler through a + signal communicate the logger events. + """ + self._logging_handler.new_log.connect(self._add_log_line) + + def _add_log_line(self, log): + """ + Adds a line to the history, only if it's in the desired levels to show. + + :param log: a log record to be inserted in the widget + :type log: a dict with RECORD_KEY and MESSAGE_KEY. + the record contains the LogRecord of the logging module, + the message contains the formatted message for the log. + """ + html_style = { + logging.DEBUG: "background: #CDFFFF;", + logging.INFO: "background: white;", + logging.WARNING: "background: #FFFF66;", + logging.ERROR: "background: red; color: white;", + logging.CRITICAL: "background: red; color: white; font: bold;" + } + level = log[LeapLogHandler.RECORD_KEY].levelno + message = log[LeapLogHandler.MESSAGE_KEY] + message = message.replace('\n', '
\n') + + if self._logs_to_display[level]: + open_tag = "" + open_tag += "" + close_tag = "" + message = open_tag + message + close_tag + + self.ui.txtLogHistory.append(message) + + def _load_history(self): + """ + Load the previous logged messages in the widget. + They are stored in the custom handler. + """ + self._set_logs_to_display() + self.ui.txtLogHistory.clear() + history = self._logging_handler.log_history + for line in history: + self._add_log_line(line) + + def _set_logs_to_display(self): + """ + Sets the logs_to_display dict getting the toggled options from the ui + """ + self._logs_to_display = { + logging.DEBUG: self.ui.btnDebug.isChecked(), + logging.INFO: self.ui.btnInfo.isChecked(), + logging.WARNING: self.ui.btnWarning.isChecked(), + logging.ERROR: self.ui.btnError.isChecked(), + logging.CRITICAL: self.ui.btnCritical.isChecked() + } + + def _save_log_to_file(self): + """ + Lets the user save the current log to a file + """ + fileName, filtr = QtGui.QFileDialog.getSaveFileName( + self, self.tr("Save As")) + + if fileName: + try: + with open(fileName, 'w') as output: + output.write(self.ui.txtLogHistory.toPlainText()) + output.write('\n') + logger.debug('Log saved in %s' % (fileName, )) + except IOError, e: + logger.error("Error saving log file: %r" % (e, )) + else: + logger.debug('Log not saved!') diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py new file mode 100644 index 00000000..de0b2d50 --- /dev/null +++ b/src/leap/bitmask/gui/login.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- +# login.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 . + +""" +Login widget implementation +""" +import logging + +from PySide import QtCore, QtGui +from ui_login import Ui_LoginWidget + +from leap.util.keyring_helpers import has_keyring + +logger = logging.getLogger(__name__) + + +class LoginWidget(QtGui.QWidget): + """ + Login widget that emits signals to display the wizard or to + perform login. + """ + + # Emitted when the login button is clicked + login = QtCore.Signal() + cancel_login = QtCore.Signal() + + # Emitted when the user selects "Other..." in the provider + # combobox or click "Create Account" + show_wizard = QtCore.Signal() + + MAX_STATUS_WIDTH = 40 + + BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" + + def __init__(self, settings, parent=None): + """ + Constructs the LoginWidget. + + :param settings: client wide settings + :type settings: LeapSettings + :param parent: The parent widget for this widget + :type parent: QWidget or None + """ + QtGui.QWidget.__init__(self, parent) + + self._settings = settings + self._selected_provider_index = -1 + + self.ui = Ui_LoginWidget() + self.ui.setupUi(self) + + self.ui.chkRemember.stateChanged.connect( + self._remember_state_changed) + self.ui.chkRemember.setEnabled(has_keyring()) + + self.ui.lnPassword.setEchoMode(QtGui.QLineEdit.Password) + + self.ui.btnLogin.clicked.connect(self.login) + self.ui.lnPassword.returnPressed.connect(self.login) + + self.ui.lnUser.returnPressed.connect(self._focus_password) + + self.ui.cmbProviders.currentIndexChanged.connect( + self._current_provider_changed) + self.ui.btnCreateAccount.clicked.connect( + self.show_wizard) + + username_re = QtCore.QRegExp(self.BARE_USERNAME_REGEX) + self.ui.lnUser.setValidator( + QtGui.QRegExpValidator(username_re, self)) + + def _remember_state_changed(self, state): + """ + Saves the remember state in the LeapSettings + + :param state: possible stats can be Checked, Unchecked and + PartiallyChecked + :type state: QtCore.Qt.CheckState + """ + enable = True if state == QtCore.Qt.Checked else False + self._settings.set_remember(enable) + + def set_providers(self, provider_list): + """ + Set the provider list to provider_list plus an "Other..." item + that triggers the wizard + + :param provider_list: list of providers + :type provider_list: list of str + """ + self.ui.cmbProviders.blockSignals(True) + self.ui.cmbProviders.clear() + self.ui.cmbProviders.addItems(provider_list + [self.tr("Other...")]) + self.ui.cmbProviders.blockSignals(False) + + def select_provider_by_name(self, name): + """ + Given a provider name/domain, it selects it in the combobox + + :param name: name or domain for the provider + :type name: str + """ + provider_index = self.ui.cmbProviders.findText(name) + self.ui.cmbProviders.setCurrentIndex(provider_index) + + def get_selected_provider(self): + """ + Returns the selected provider in the combobox + """ + return self.ui.cmbProviders.currentText() + + def set_remember(self, value): + """ + Checks the remember user and password checkbox + + :param value: True to mark it checked, False otherwise + :type value: bool + """ + self.ui.chkRemember.setChecked(value) + + def get_remember(self): + """ + Returns the remember checkbox state + + :rtype: bool + """ + return self.ui.chkRemember.isChecked() + + def set_user(self, user): + """ + Sets the user and focuses on the next field, password. + + :param user: user to set the field to + :type user: str + """ + self.ui.lnUser.setText(user) + self._focus_password() + + def get_user(self): + """ + Returns the user that appears in the widget. + + :rtype: str + """ + return self.ui.lnUser.text() + + def set_password(self, password): + """ + Sets the password for the widget + + :param password: password to set + :type password: str + """ + self.ui.lnPassword.setText(password) + + def get_password(self): + """ + Returns the password that appears in the widget + + :rtype: str + """ + return self.ui.lnPassword.text() + + def set_status(self, status, error=True): + """ + Sets the status label at the login stage to status + + :param status: status message + :type status: str + """ + if len(status) > self.MAX_STATUS_WIDTH: + status = status[:self.MAX_STATUS_WIDTH] + "..." + if error: + status = "%s" % (status,) + self.ui.lblStatus.setText(status) + + def set_enabled(self, enabled=False): + """ + Enables or disables all the login widgets + + :param enabled: wether they should be enabled or not + :type enabled: bool + """ + self.ui.lnUser.setEnabled(enabled) + self.ui.lnPassword.setEnabled(enabled) + self.ui.chkRemember.setEnabled(enabled) + self.ui.cmbProviders.setEnabled(enabled) + + self._set_cancel(not enabled) + + def _set_cancel(self, enabled=False): + """ + Enables or disables the cancel action in the "log in" process. + + :param enabled: wether it should be enabled or not + :type enabled: bool + """ + text = self.tr("Cancel") + login_or_cancel = self.cancel_login + + if not enabled: + text = self.tr("Log In") + login_or_cancel = self.login + + self.ui.btnLogin.setText(text) + + self.ui.btnLogin.clicked.disconnect() + self.ui.btnLogin.clicked.connect(login_or_cancel) + + def _focus_password(self): + """ + Focuses in the password lineedit + """ + self.ui.lnPassword.setFocus() + + def _current_provider_changed(self, param): + """ + SLOT + TRIGGERS: self.ui.cmbProviders.currentIndexChanged + """ + if param == (self.ui.cmbProviders.count() - 1): + self.show_wizard.emit() + # Leave the previously selected provider in the combobox + prev_provider = 0 + if self._selected_provider_index != -1: + prev_provider = self._selected_provider_index + self.ui.cmbProviders.blockSignals(True) + self.ui.cmbProviders.setCurrentIndex(prev_provider) + self.ui.cmbProviders.blockSignals(False) + else: + self._selected_provider_index = param diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py new file mode 100644 index 00000000..5ace1043 --- /dev/null +++ b/src/leap/bitmask/gui/mainwindow.py @@ -0,0 +1,1537 @@ +# -*- coding: utf-8 -*- +# mainwindow.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 . + +""" +Main window for the leap client +""" +import logging +import os +import platform +import tempfile +from functools import partial + +import keyring + +from PySide import QtCore, QtGui +from twisted.internet import threads + +from leap.common.check import leap_assert +from leap.common.events import register +from leap.common.events import events_pb2 as proto +from leap.config.leapsettings import LeapSettings +from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpauth import SRPAuth +from leap.gui.loggerwindow import LoggerWindow +from leap.gui.wizard import Wizard +from leap.gui.login import LoginWidget +from leap.gui.statuspanel import StatusPanelWidget +from leap.services.eip.eipbootstrapper import EIPBootstrapper +from leap.services.eip.eipconfig import EIPConfig +from leap.services.eip.providerbootstrapper import ProviderBootstrapper +# XXX: Soledad might not work out of the box in Windows, issue #2932 +from leap.services.soledad.soledadbootstrapper import SoledadBootstrapper +from leap.services.mail.smtpbootstrapper import SMTPBootstrapper +from leap.services.mail import imap +from leap.platform_init import IS_WIN, IS_MAC +from leap.platform_init.initializers import init_platform + +from leap.services.eip.vpnprocess import VPN +from leap.services.eip.vpnprocess import OpenVPNAlreadyRunning +from leap.services.eip.vpnprocess import AlienOpenVPNAlreadyRunning + +from leap.services.eip.vpnlaunchers import VPNLauncherException +from leap.services.eip.vpnlaunchers import OpenVPNNotFoundException +from leap.services.eip.vpnlaunchers import EIPNoPkexecAvailable +from leap.services.eip.vpnlaunchers import EIPNoPolkitAuthAgentAvailable +from leap.services.eip.vpnlaunchers import EIPNoTunKextLoaded + +from leap.util import __version__ as VERSION +from leap.util.keyring_helpers import has_keyring + +from leap.services.mail.smtpconfig import SMTPConfig + +if IS_WIN: + from leap.platform_init.locks import WindowsLock + from leap.platform_init.locks import raise_window_ack + +from ui_mainwindow import Ui_MainWindow + +logger = logging.getLogger(__name__) + + +class MainWindow(QtGui.QMainWindow): + """ + Main window for login and presenting status updates to the user + """ + + # StackedWidget indexes + LOGIN_INDEX = 0 + EIP_STATUS_INDEX = 1 + + # Keyring + KEYRING_KEY = "bitmask" + + # SMTP + PORT_KEY = "port" + IP_KEY = "ip_address" + + OPENVPN_SERVICE = "openvpn" + MX_SERVICE = "mx" + + # Signals + new_updates = QtCore.Signal(object) + raise_window = QtCore.Signal([]) + soledad_ready = QtCore.Signal([]) + + # We use this flag to detect abnormal terminations + user_stopped_eip = False + + def __init__(self, quit_callback, + standalone=False, + openvpn_verb=1, + bypass_checks=False): + """ + Constructor for the client main window + + :param quit_callback: Function to be called when closing + the application. + :type quit_callback: callable + + :param standalone: Set to true if the app should use configs + inside its pwd + :type standalone: bool + + :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.QMainWindow.__init__(self) + + # register leap events + register(signal=proto.UPDATER_NEW_UPDATES, + callback=self._new_updates_available, + reqcbk=lambda req, resp: None) # make rpc call async + register(signal=proto.RAISE_WINDOW, + callback=self._on_raise_window_event, + reqcbk=lambda req, resp: None) # make rpc call async + + self._quit_callback = quit_callback + + self._updates_content = "" + + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + + self._settings = LeapSettings(standalone) + + self._login_widget = LoginWidget( + self._settings, + self.ui.stackedWidget.widget(self.LOGIN_INDEX)) + self.ui.loginLayout.addWidget(self._login_widget) + + # Signals + # TODO separate logic from ui signals. + + self._login_widget.login.connect(self._login) + self._login_widget.cancel_login.connect(self._cancel_login) + self._login_widget.show_wizard.connect( + self._launch_wizard) + + self.ui.btnShowLog.clicked.connect(self._show_logger_window) + + self._status_panel = StatusPanelWidget( + self.ui.stackedWidget.widget(self.EIP_STATUS_INDEX)) + self.ui.statusLayout.addWidget(self._status_panel) + + self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) + + self._status_panel.start_eip.connect(self._start_eip) + self._status_panel.stop_eip.connect(self._stop_eip) + + # This is loaded only once, there's a bug when doing that more + # than once + ProviderConfig.standalone = standalone + EIPConfig.standalone = standalone + self._standalone = standalone + self._provider_config = ProviderConfig() + # Used for automatic start of EIP + self._provisional_provider_config = ProviderConfig() + self._eip_config = EIPConfig() + + self._already_started_eip = False + + # This is created once we have a valid provider config + self._srp_auth = None + self._logged_user = None + + # This thread is always running, although it's quite + # lightweight when it's done setting up provider + # configuration and certificate. + self._provider_bootstrapper = ProviderBootstrapper(bypass_checks) + + # Intermediate stages, only do something if there was an error + self._provider_bootstrapper.name_resolution.connect( + self._intermediate_stage) + self._provider_bootstrapper.https_connection.connect( + self._intermediate_stage) + self._provider_bootstrapper.download_ca_cert.connect( + self._intermediate_stage) + + # Important stages, loads the provider config and checks + # certificates + self._provider_bootstrapper.download_provider_info.connect( + self._load_provider_config) + self._provider_bootstrapper.check_api_certificate.connect( + self._provider_config_loaded) + + # This thread is similar to the provider bootstrapper + self._eip_bootstrapper = EIPBootstrapper() + + self._eip_bootstrapper.download_config.connect( + self._eip_intermediate_stage) + self._eip_bootstrapper.download_client_certificate.connect( + self._finish_eip_bootstrap) + + self._soledad_bootstrapper = SoledadBootstrapper() + self._soledad_bootstrapper.download_config.connect( + self._soledad_intermediate_stage) + self._soledad_bootstrapper.gen_key.connect( + self._soledad_bootstrapped_stage) + + self._smtp_bootstrapper = SMTPBootstrapper() + self._smtp_bootstrapper.download_config.connect( + self._smtp_bootstrapped_stage) + + self._vpn = VPN(openvpn_verb=openvpn_verb) + self._vpn.qtsigs.state_changed.connect( + self._status_panel.update_vpn_state) + self._vpn.qtsigs.status_changed.connect( + self._status_panel.update_vpn_status) + self._vpn.qtsigs.process_finished.connect( + self._eip_finished) + + self.ui.action_log_out.setEnabled(False) + self.ui.action_log_out.triggered.connect(self._logout) + self.ui.action_about_leap.triggered.connect(self._about) + self.ui.action_quit.triggered.connect(self.quit) + self.ui.action_wizard.triggered.connect(self._launch_wizard) + self.ui.action_show_logs.triggered.connect(self._show_logger_window) + self.raise_window.connect(self._do_raise_mainwindow) + + # Used to differentiate between real quits and close to tray + self._really_quit = False + + self._systray = None + + self._action_eip_provider = QtGui.QAction( + self.tr("No default provider"), self) + self._action_eip_provider.setEnabled(False) + self._action_eip_status = QtGui.QAction( + self.tr("Encrypted internet is OFF"), + self) + self._action_eip_status.setEnabled(False) + + self._status_panel.set_action_eip_status( + self._action_eip_status) + + self._action_eip_startstop = QtGui.QAction( + self.tr("Turn OFF"), self) + self._action_eip_startstop.triggered.connect( + self._stop_eip) + self._action_eip_startstop.setEnabled(False) + self._status_panel.set_action_eip_startstop( + self._action_eip_startstop) + + self._action_visible = QtGui.QAction(self.tr("Hide Main Window"), self) + self._action_visible.triggered.connect(self._toggle_visible) + + self._enabled_services = [] + + self._center_window() + + self.ui.lblNewUpdates.setVisible(False) + self.ui.btnMore.setVisible(False) + self.ui.btnMore.clicked.connect(self._updates_details) + + self.new_updates.connect(self._react_to_new_updates) + self.soledad_ready.connect(self._start_imap_service) + + init_platform() + + self._wizard = None + self._wizard_firstrun = False + + self._logger_window = None + + self._bypass_checks = bypass_checks + + self._soledad = None + self._keymanager = None + self._imap_service = None + + self._login_defer = None + self._download_provider_defer = None + + self._smtp_config = SMTPConfig() + + if self._first_run(): + self._wizard_firstrun = True + self._wizard = Wizard(standalone=standalone, + bypass_checks=bypass_checks) + # Give this window time to finish init and then show the wizard + QtCore.QTimer.singleShot(1, self._launch_wizard) + self._wizard.accepted.connect(self._finish_init) + self._wizard.rejected.connect(self._rejected_wizard) + else: + self._finish_init() + + def _rejected_wizard(self): + """ + SLOT + TRIGGERS: self._wizard.rejected + + Called if the wizard has been cancelled or closed before + finishing. + """ + if self._wizard_firstrun: + self._settings.set_properprovider(False) + self.quit() + else: + self._finish_init() + + def _launch_wizard(self): + """ + SLOT + TRIGGERS: + self._login_widget.show_wizard + self.ui.action_wizard.triggered + + Also called in first run. + + Launches the wizard, creating the object itself if not already + there. + """ + if self._wizard is None: + self._wizard = Wizard(bypass_checks=self._bypass_checks) + self._wizard.accepted.connect(self._finish_init) + self._wizard.rejected.connect(self._wizard.close) + + self.setVisible(False) + # Do NOT use exec_, it will use a child event loop! + # Refer to http://www.themacaque.com/?p=1067 for funny details. + self._wizard.show() + if IS_MAC: + self._wizard.raise_() + self._wizard.finished.connect(self._wizard_finished) + + def _wizard_finished(self): + """ + SLOT + TRIGGERS + self._wizard.finished + + Called when the wizard has finished. + """ + self.setVisible(True) + + def _get_leap_logging_handler(self): + """ + Gets the leap handler from the top level logger + + :return: a logging handler or None + :rtype: LeapLogHandler or None + """ + from leap.util.leap_log_handler import LeapLogHandler + leap_logger = logging.getLogger('leap') + for h in leap_logger.handlers: + if isinstance(h, LeapLogHandler): + return h + return None + + def _show_logger_window(self): + """ + SLOT + TRIGGERS: + self.ui.action_show_logs.triggered + self.ui.btnShowLog.clicked + + Displays the window with the history of messages logged until now + and displays the new ones on arrival. + """ + if self._logger_window is None: + leap_log_handler = self._get_leap_logging_handler() + if leap_log_handler is None: + logger.error('Leap logger handler not found') + else: + self._logger_window = LoggerWindow(handler=leap_log_handler) + self._logger_window.setVisible( + not self._logger_window.isVisible()) + self.ui.btnShowLog.setChecked(self._logger_window.isVisible()) + else: + self._logger_window.setVisible(not self._logger_window.isVisible()) + self.ui.btnShowLog.setChecked(self._logger_window.isVisible()) + + self._logger_window.finished.connect(self._uncheck_logger_button) + + def _uncheck_logger_button(self): + """ + SLOT + Sets the checked state of the loggerwindow button to false. + """ + self.ui.btnShowLog.setChecked(False) + + def _new_updates_available(self, req): + """ + Callback for the new updates event + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + self.new_updates.emit(req) + + def _react_to_new_updates(self, req): + """ + SLOT + TRIGGER: self._new_updates_available + + Displays the new updates label and sets the updates_content + """ + self.moveToThread(QtCore.QCoreApplication.instance().thread()) + self.ui.lblNewUpdates.setVisible(True) + self.ui.btnMore.setVisible(True) + self._updates_content = req.content + + def _updates_details(self): + """ + SLOT + TRIGGER: self.ui.btnMore.clicked + + Parses and displays the updates details + """ + msg = self.tr("The Bitmask app is ready to update, please" + " restart the application.") + + # We assume that if there is nothing in the contents, then + # the Bitmask bundle is what needs updating. + if len(self._updates_content) > 0: + files = self._updates_content.split(", ") + files_str = "" + for f in files: + final_name = f.replace("/data/", "") + final_name = final_name.replace(".thp", "") + files_str += final_name + files_str += "\n" + msg += self.tr(" The following components will be updated:\n%s") \ + % (files_str,) + + QtGui.QMessageBox.information(self, + self.tr("Updates available"), + msg) + + def _finish_init(self): + """ + SLOT + TRIGGERS: + self._wizard.accepted + + Also called at the end of the constructor if not first run, + and after _rejected_wizard if not first run. + + Implements the behavior after either constructing the + mainwindow object, loading the saved user/password, or after + the wizard has been executed. + """ + # XXX: May be this can be divided into two methods? + + self._login_widget.set_providers(self._configured_providers()) + self._show_systray() + self.show() + if IS_MAC: + self.raise_() + + if self._wizard: + possible_username = self._wizard.get_username() + possible_password = self._wizard.get_password() + + # select the configured provider in the combo box + domain = self._wizard.get_domain() + self._login_widget.select_provider_by_name(domain) + + self._login_widget.set_remember(self._wizard.get_remember()) + self._enabled_services = list(self._wizard.get_services()) + self._settings.set_enabled_services( + self._login_widget.get_selected_provider(), + self._enabled_services) + if possible_username is not None: + self._login_widget.set_user(possible_username) + if possible_password is not None: + self._login_widget.set_password(possible_password) + self._login() + self._wizard = None + self._settings.set_properprovider(True) + else: + self._try_autostart_eip() + if not self._settings.get_remember(): + # nothing to do here + return + + saved_user = self._settings.get_user() + + try: + username, domain = saved_user.split('@') + except (ValueError, AttributeError) as e: + # if the saved_user does not contain an '@' or its None + logger.error('Username@provider malformed. %r' % (e, )) + saved_user = None + + if saved_user is not None and has_keyring(): + # fill the username + self._login_widget.set_user(username) + + # select the configured provider in the combo box + self._login_widget.select_provider_by_name(domain) + + self._login_widget.set_remember(True) + + saved_password = None + try: + saved_password = keyring.get_password(self.KEYRING_KEY, + saved_user + .encode("utf8")) + except ValueError, e: + logger.debug("Incorrect Password. %r." % (e,)) + + if saved_password is not None: + self._login_widget.set_password( + saved_password.decode("utf8")) + self._login() + + def _try_autostart_eip(self): + """ + Tries to autostart EIP + """ + default_provider = self._settings.get_defaultprovider() + + if default_provider is None: + logger.info("Cannot autostart Encrypted Internet because there is " + "no default provider configured") + return + + self._action_eip_provider.setText(default_provider) + + self._enabled_services = self._settings.get_enabled_services( + default_provider) + + if self._provisional_provider_config.load( + os.path.join("leap", + "providers", + default_provider, + "provider.json")): + self._download_eip_config() + else: + # XXX: Display a proper message to the user + logger.error("Unable to load %s config, cannot autostart." % + (default_provider,)) + + def _show_systray(self): + """ + Sets up the systray icon + """ + if self._systray is not None: + self._systray.setVisible(True) + return + + # Placeholder actions + # They are temporary to display the tray as designed + preferences_action = QtGui.QAction(self.tr("Preferences"), self) + preferences_action.setEnabled(False) + help_action = QtGui.QAction(self.tr("Help"), self) + help_action.setEnabled(False) + + systrayMenu = QtGui.QMenu(self) + systrayMenu.addAction(self._action_visible) + systrayMenu.addSeparator() + systrayMenu.addAction(self._action_eip_provider) + systrayMenu.addAction(self._action_eip_status) + systrayMenu.addAction(self._action_eip_startstop) + systrayMenu.addSeparator() + systrayMenu.addAction(preferences_action) + systrayMenu.addAction(help_action) + systrayMenu.addSeparator() + systrayMenu.addAction(self.ui.action_log_out) + systrayMenu.addAction(self.ui.action_quit) + self._systray = QtGui.QSystemTrayIcon(self) + self._systray.setContextMenu(systrayMenu) + self._systray.setIcon(self._status_panel.ERROR_ICON_TRAY) + self._systray.setVisible(True) + self._systray.activated.connect(self._tray_activated) + + self._status_panel.set_systray(self._systray) + + def _tray_activated(self, reason=None): + """ + SLOT + TRIGGER: self._systray.activated + + Displays the context menu from the tray icon + """ + self._update_hideshow_menu() + + context_menu = self._systray.contextMenu() + if not IS_MAC: + # for some reason, context_menu.show() + # is failing in a way beyond my understanding. + # (not working the first time it's clicked). + # this works however. + context_menu.exec_(self._systray.geometry().center()) + + def _update_hideshow_menu(self): + """ + Updates the Hide/Show main window menu text based on the + visibility of the window. + """ + get_action = lambda visible: ( + self.tr("Show Main Window"), + self.tr("Hide Main Window"))[int(visible)] + + # set labels + visible = self.isVisible() + self._action_visible.setText(get_action(visible)) + + def _toggle_visible(self): + """ + SLOT + TRIGGER: self._action_visible.triggered + + Toggles the window visibility + """ + if not self.isVisible(): + self.show() + self.raise_() + else: + self.hide() + + self._update_hideshow_menu() + + def _center_window(self): + """ + Centers the mainwindow based on the desktop geometry + """ + geometry = self._settings.get_geometry() + state = self._settings.get_windowstate() + + if geometry is None: + app = QtGui.QApplication.instance() + width = app.desktop().width() + height = app.desktop().height() + window_width = self.size().width() + window_height = self.size().height() + x = (width / 2.0) - (window_width / 2.0) + y = (height / 2.0) - (window_height / 2.0) + self.move(x, y) + else: + self.restoreGeometry(geometry) + + if state is not None: + self.restoreState(state) + + def _about(self): + """ + SLOT + TRIGGERS: self.ui.action_about_leap.triggered + + Display the About Bitmask dialog + """ + QtGui.QMessageBox.about( + self, self.tr("About Bitmask - %s") % (VERSION,), + self.tr("Version: %s
" + "
" + "Bitmask is the Desktop client application for " + "the LEAP platform, supporting encrypted internet " + "proxy, secure email, and secure chat (coming soon).
" + "
" + "LEAP is a non-profit dedicated to giving " + "all internet users access to secure " + "communication. Our focus is on adapting " + "encryption technology to make it easy to use " + "and widely available.
" + "
" + "More about LEAP" + "") % (VERSION,)) + + def changeEvent(self, e): + """ + Reimplements the changeEvent method to minimize to tray + """ + if QtGui.QSystemTrayIcon.isSystemTrayAvailable() and \ + e.type() == QtCore.QEvent.WindowStateChange and \ + self.isMinimized(): + self._toggle_visible() + e.accept() + return + QtGui.QMainWindow.changeEvent(self, e) + + def closeEvent(self, e): + """ + Reimplementation of closeEvent to close to tray + """ + if QtGui.QSystemTrayIcon.isSystemTrayAvailable() and \ + not self._really_quit: + self._toggle_visible() + e.ignore() + return + + self._settings.set_geometry(self.saveGeometry()) + self._settings.set_windowstate(self.saveState()) + + QtGui.QMainWindow.closeEvent(self, e) + + def _configured_providers(self): + """ + Returns the available providers based on the file structure + + :rtype: list + """ + + # TODO: check which providers have a valid certificate among + # other things, not just the directories + providers = [] + try: + providers = os.listdir( + os.path.join(self._provider_config.get_path_prefix(), + "leap", + "providers")) + except Exception as e: + logger.debug("Error listing providers, assume there are none. %r" + % (e,)) + + return providers + + def _first_run(self): + """ + Returns True if there are no configured providers. False otherwise + + :rtype: bool + """ + has_provider_on_disk = len(self._configured_providers()) != 0 + is_proper_provider = self._settings.get_properprovider() + return not (has_provider_on_disk and is_proper_provider) + + def _download_provider_config(self): + """ + Starts the bootstrapping sequence. It will download the + provider configuration if it's not present, otherwise will + emit the corresponding signals inmediately + """ + provider = self._login_widget.get_selected_provider() + + pb = self._provider_bootstrapper + d = pb.run_provider_select_checks(provider, download_if_needed=True) + self._download_provider_defer = d + + def _load_provider_config(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.download_provider_info + + Once the provider config has been downloaded, this loads the + self._provider_config instance with it and starts the second + part of the bootstrapping sequence + + :param data: result from the last stage of the + run_provider_select_checks + :type data: dict + """ + if data[self._provider_bootstrapper.PASSED_KEY]: + provider = self._login_widget.get_selected_provider() + + # If there's no loaded provider or + # we want to connect to other provider... + if (not self._provider_config.loaded() or + self._provider_config.get_domain() != provider): + self._provider_config.load( + os.path.join("leap", "providers", + provider, "provider.json")) + + if self._provider_config.loaded(): + self._provider_bootstrapper.run_provider_setup_checks( + self._provider_config, + download_if_needed=True) + else: + self._login_widget.set_status( + self.tr("Unable to login: Problem with provider")) + logger.error("Could not load provider configuration.") + self._login_widget.set_enabled(True) + else: + self._login_widget.set_status( + self.tr("Unable to login: Problem with provider")) + logger.error(data[self._provider_bootstrapper.ERROR_KEY]) + self._login_widget.set_enabled(True) + + def _login(self): + """ + SLOT + TRIGGERS: + self._login_widget.login + + Starts the login sequence. Which involves bootstrapping the + selected provider if the selection is valid (not empty), then + start the SRP authentication, and as the last step + bootstrapping the EIP service + """ + leap_assert(self._provider_config, "We need a provider config") + + username = self._login_widget.get_user() + password = self._login_widget.get_password() + provider = self._login_widget.get_selected_provider() + + self._enabled_services = self._settings.get_enabled_services( + self._login_widget.get_selected_provider()) + + if len(provider) == 0: + self._login_widget.set_status( + self.tr("Please select a valid provider")) + return + + if len(username) == 0: + self._login_widget.set_status( + self.tr("Please provide a valid username")) + return + + if len(password) == 0: + self._login_widget.set_status( + self.tr("Please provide a valid Password")) + return + + self._login_widget.set_status(self.tr("Logging in..."), error=False) + self._login_widget.set_enabled(False) + + if self._login_widget.get_remember() and has_keyring(): + # in the keyring and in the settings + # we store the value 'usename@provider' + username_domain = (username + '@' + provider).encode("utf8") + try: + keyring.set_password(self.KEYRING_KEY, + username_domain, + password.encode("utf8")) + # Only save the username if it was saved correctly in + # the keyring + self._settings.set_user(username_domain) + except Exception as e: + logger.error("Problem saving data to keyring. %r" + % (e,)) + + self._download_provider_config() + + def _cancel_login(self): + """ + SLOT + TRIGGERS: + self._login_widget.cancel_login + + Stops the login sequence. + """ + logger.debug("Cancelling log in.") + + if self._download_provider_defer: + logger.debug("Cancelling download provider defer.") + self._download_provider_defer.cancel() + + if self._login_defer: + logger.debug("Cancelling login defer.") + self._login_defer.cancel() + + def _provider_config_loaded(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.check_api_certificate + + Once the provider configuration is loaded, this starts the SRP + authentication + """ + leap_assert(self._provider_config, "We need a provider config!") + + if data[self._provider_bootstrapper.PASSED_KEY]: + username = self._login_widget.get_user().encode("utf8") + password = self._login_widget.get_password().encode("utf8") + + if self._srp_auth is None: + self._srp_auth = SRPAuth(self._provider_config) + self._srp_auth.authentication_finished.connect( + self._authentication_finished) + self._srp_auth.logout_finished.connect( + self._done_logging_out) + + # TODO: Add errback! + self._login_defer = self._srp_auth.authenticate(username, password) + else: + self._login_widget.set_status( + "Unable to login: Problem with provider") + logger.error(data[self._provider_bootstrapper.ERROR_KEY]) + self._login_widget.set_enabled(True) + + def _authentication_finished(self, ok, message): + """ + SLOT + TRIGGER: self._srp_auth.authentication_finished + + Once the user is properly authenticated, try starting the EIP + service + """ + + # In general we want to "filter" likely complicated error + # messages, but in this case, the messages make more sense as + # they come. Since they are "Unknown user" or "Unknown + # password" + self._login_widget.set_status(message, error=not ok) + + if ok: + self._logged_user = self._login_widget.get_user() + self.ui.action_log_out.setEnabled(True) + # We leave a bit of room for the user to see the + # "Succeeded" message and then we switch to the EIP status + # panel + QtCore.QTimer.singleShot(1000, self._switch_to_status) + self._login_defer = None + else: + self._login_widget.set_enabled(True) + + def _switch_to_status(self): + """ + Changes the stackedWidget index to the EIP status one and + triggers the eip bootstrapping + """ + if not self._already_started_eip: + self._status_panel.set_provider( + "%s@%s" % (self._login_widget.get_user(), + self._get_best_provider_config().get_domain())) + + self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX) + + self._soledad_bootstrapper.run_soledad_setup_checks( + self._provider_config, + self._login_widget.get_user(), + self._login_widget.get_password(), + download_if_needed=True, + standalone=self._standalone) + + self._download_eip_config() + + def _soledad_intermediate_stage(self, data): + """ + SLOT + TRIGGERS: + self._soledad_bootstrapper.download_config + + If there was a problem, displays it, otherwise it does nothing. + This is used for intermediate bootstrapping stages, in case + they fail. + """ + passed = data[self._soledad_bootstrapper.PASSED_KEY] + if not passed: + # TODO: display in the GUI: + # should pass signal to a slot in status_panel + # that sets the global status + logger.warning("Soledad failed to start: %s" % + (data[self._soledad_bootstrapper.ERROR_KEY],)) + + def _soledad_bootstrapped_stage(self, data): + """ + SLOT + TRIGGERS: + self._soledad_bootstrapper.gen_key + + If there was a problem, displays it, otherwise it does nothing. + This is used for intermediate bootstrapping stages, in case + they fail. + + :param data: result from the bootstrapping stage for Soledad + :type data: dict + """ + passed = data[self._soledad_bootstrapper.PASSED_KEY] + if not passed: + logger.error(data[self._soledad_bootstrapper.ERROR_KEY]) + return + + logger.debug("Done bootstrapping Soledad") + + self._soledad = self._soledad_bootstrapper.soledad + self._keymanager = self._soledad_bootstrapper.keymanager + + # Ok, now soledad is ready, so we can allow other things that + # depend on soledad to start. + + # this will trigger start_imap_service + self.soledad_ready.emit() + + # TODO connect all these activations to the soledad_ready + # signal so the logic is clearer to follow. + + if self._provider_config.provides_mx() and \ + self._enabled_services.count(self.MX_SERVICE) > 0: + self._smtp_bootstrapper.run_smtp_setup_checks( + self._provider_config, + self._smtp_config, + True) + else: + if self._enabled_services.count(self.MX_SERVICE) > 0: + pass # TODO: show MX status + #self._status_panel.set_eip_status( + # self.tr("%s does not support MX") % + # (self._provider_config.get_domain(),), + # error=True) + else: + pass # TODO: show MX status + #self._status_panel.set_eip_status( + # self.tr("MX is disabled")) + + # Service control methods: smtp + + def _smtp_bootstrapped_stage(self, data): + """ + SLOT + TRIGGERS: + self._smtp_bootstrapper.download_config + + If there was a problem, displays it, otherwise it does nothing. + This is used for intermediate bootstrapping stages, in case + they fail. + + :param data: result from the bootstrapping stage for Soledad + :type data: dict + """ + passed = data[self._smtp_bootstrapper.PASSED_KEY] + if not passed: + logger.error(data[self._smtp_bootstrapper.ERROR_KEY]) + return + logger.debug("Done bootstrapping SMTP") + + hosts = self._smtp_config.get_hosts() + # TODO: handle more than one host and define how to choose + if len(hosts) > 0: + hostname = hosts.keys()[0] + logger.debug("Using hostname %s for SMTP" % (hostname,)) + host = hosts[hostname][self.IP_KEY].encode("utf-8") + port = hosts[hostname][self.PORT_KEY] + # TODO: pick local smtp port in a better way + # TODO: Make the encrypted_only configurable + + from leap.mail.smtp import setup_smtp_relay + client_cert = self._eip_config.get_client_cert_path( + self._provider_config) + setup_smtp_relay(port=2013, + keymanager=self._keymanager, + smtp_host=host, + smtp_port=port, + smtp_cert=client_cert, + smtp_key=client_cert, + encrypted_only=False) + + def _start_imap_service(self): + """ + SLOT + TRIGGERS: + soledad_ready + """ + logger.debug('Starting imap service') + + self._imap_service = imap.start_imap_service( + self._soledad, + self._keymanager) + + def _get_socket_host(self): + """ + Returns the socket and port to be used for VPN + + :rtype: tuple (str, str) (host, port) + """ + + # TODO: make this properly multiplatform + + if platform.system() == "Windows": + host = "localhost" + port = "9876" + else: + host = os.path.join(tempfile.mkdtemp(prefix="leap-tmp"), + 'openvpn.socket') + port = "unix" + + return host, port + + def _start_eip(self): + """ + SLOT + TRIGGERS: + self._status_panel.start_eip + self._action_eip_startstop.triggered + or called from _finish_eip_bootstrap + + Starts EIP + """ + self._status_panel.eip_pre_up() + self.user_stopped_eip = False + provider_config = self._get_best_provider_config() + + try: + host, port = self._get_socket_host() + self._vpn.start(eipconfig=self._eip_config, + providerconfig=provider_config, + socket_host=host, + socket_port=port) + + self._settings.set_defaultprovider( + provider_config.get_domain()) + + provider = provider_config.get_domain() + if self._logged_user is not None: + provider = "%s@%s" % (self._logged_user, provider) + + self._status_panel.set_provider(provider) + + self._action_eip_provider.setText(provider_config.get_domain()) + + self._status_panel.eip_started() + + # XXX refactor into status_panel method? + self._action_eip_startstop.setText(self.tr("Turn OFF")) + self._action_eip_startstop.disconnect(self) + self._action_eip_startstop.triggered.connect( + self._stop_eip) + except EIPNoPolkitAuthAgentAvailable: + self._status_panel.set_global_status( + # XXX this should change to polkit-kde where + # applicable. + self.tr("We could not find any " + "authentication " + "agent in your system.
" + "Make sure you have " + "polkit-gnome-authentication-" + "agent-1 " + "running and try again."), + error=True) + self._set_eipstatus_off() + except EIPNoTunKextLoaded: + self._status_panel.set_global_status( + self.tr("Encrypted Internet cannot be started because " + "the tuntap extension is not installed properly " + "in your system.")) + self._set_eipstatus_off() + except EIPNoPkexecAvailable: + self._status_panel.set_global_status( + self.tr("We could not find pkexec " + "in your system."), + error=True) + self._set_eipstatus_off() + except OpenVPNNotFoundException: + self._status_panel.set_global_status( + self.tr("We could not find openvpn binary."), + error=True) + self._set_eipstatus_off() + except OpenVPNAlreadyRunning as e: + self._status_panel.set_global_status( + self.tr("Another openvpn instance is already running, and " + "could not be stopped."), + error=True) + self._set_eipstatus_off() + except AlienOpenVPNAlreadyRunning as e: + self._status_panel.set_global_status( + self.tr("Another openvpn instance is already running, and " + "could not be stopped because it was not launched by " + "Bitmask. Please stop it and try again."), + error=True) + self._set_eipstatus_off() + except VPNLauncherException as e: + # XXX We should implement again translatable exceptions so + # we can pass a translatable string to the panel (usermessage attr) + self._status_panel.set_global_status("%s" % (e,), error=True) + self._set_eipstatus_off() + else: + self._already_started_eip = True + + def _set_eipstatus_off(self): + """ + Sets eip status to off + """ + self._status_panel.set_eip_status(self.tr("OFF"), error=True) + self._status_panel.set_eip_status_icon("error") + self._status_panel.set_startstop_enabled(True) + self._status_panel.eip_stopped() + + self._set_action_eipstart_off() + + def _set_action_eipstart_off(self): + """ + Sets eip startstop action to OFF status. + """ + self._action_eip_startstop.setText(self.tr("Turn ON")) + self._action_eip_startstop.disconnect(self) + self._action_eip_startstop.triggered.connect( + self._start_eip) + + def _stop_eip(self, abnormal=False): + """ + SLOT + TRIGGERS: + self._status_panel.stop_eip + self._action_eip_startstop.triggered + or called from _eip_finished + + Stops vpn process and makes gui adjustments to reflect + the change of state. + + :param abnormal: whether this was an abnormal termination. + :type abnormal: bool + """ + if abnormal: + logger.warning("Abnormal EIP termination.") + + self.user_stopped_eip = True + self._vpn.terminate() + + self._set_eipstatus_off() + + self._already_started_eip = False + self._settings.set_defaultprovider(None) + if self._logged_user: + self._status_panel.set_provider( + "%s@%s" % (self._logged_user, + self._get_best_provider_config().get_domain())) + + def _get_best_provider_config(self): + """ + Returns the best ProviderConfig to use at a moment. We may + have to use self._provider_config or + self._provisional_provider_config depending on the start + status. + + :rtype: ProviderConfig + """ + leap_assert(self._provider_config is not None or + self._provisional_provider_config is not None, + "We need a provider config") + + provider_config = None + if self._provider_config.loaded(): + provider_config = self._provider_config + elif self._provisional_provider_config.loaded(): + provider_config = self._provisional_provider_config + else: + leap_assert(False, "We could not find any usable ProviderConfig.") + + return provider_config + + def _download_eip_config(self): + """ + Starts the EIP bootstrapping sequence + """ + leap_assert(self._eip_bootstrapper, "We need an eip bootstrapper!") + + provider_config = self._get_best_provider_config() + + if provider_config.provides_eip() and \ + self._enabled_services.count(self.OPENVPN_SERVICE) > 0 and \ + not self._already_started_eip: + + self._status_panel.set_eip_status( + self.tr("Starting...")) + self._eip_bootstrapper.run_eip_setup_checks( + provider_config, + download_if_needed=True) + self._already_started_eip = True + elif not self._already_started_eip: + if self._enabled_services.count(self.OPENVPN_SERVICE) > 0: + self._status_panel.set_eip_status( + self.tr("Not supported"), + error=True) + else: + self._status_panel.set_eip_status(self.tr("Disabled")) + self._status_panel.set_startstop_enabled(False) + + def _finish_eip_bootstrap(self, data): + """ + SLOT + TRIGGER: self._eip_bootstrapper.download_client_certificate + + Starts the VPN thread if the eip configuration is properly + loaded + """ + leap_assert(self._eip_config, "We need an eip config!") + passed = data[self._eip_bootstrapper.PASSED_KEY] + + if not passed: + error_msg = self.tr("There was a problem with the provider") + self._status_panel.set_eip_status(error_msg, error=True) + logger.error(data[self._eip_bootstrapper.ERROR_KEY]) + self._already_started_eip = False + return + + provider_config = self._get_best_provider_config() + + domain = provider_config.get_domain() + + loaded = self._eip_config.loaded() + if not loaded: + eip_config_path = os.path.join("leap", "providers", + domain, "eip-service.json") + api_version = provider_config.get_api_version() + self._eip_config.set_api_version(api_version) + loaded = self._eip_config.load(eip_config_path) + + if loaded: + self._start_eip() + else: + self._status_panel.set_eip_status( + self.tr("Could not load Encrypted Internet " + "Configuration."), + error=True) + + def _logout(self): + """ + SLOT + TRIGGER: self.ui.action_log_out.triggered + + Starts the logout sequence + """ + # XXX: If other defers are doing authenticated stuff, this + # might conflict with those. CHECK! + threads.deferToThread(self._srp_auth.logout) + + def _done_logging_out(self, ok, message): + """ + SLOT + TRIGGER: self._srp_auth.logout_finished + + Switches the stackedWidget back to the login stage after + logging out + """ + self._logged_user = None + self.ui.action_log_out.setEnabled(False) + self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) + self._login_widget.set_password("") + self._login_widget.set_enabled(True) + self._login_widget.set_status("") + + def _intermediate_stage(self, data): + """ + SLOT + TRIGGERS: + self._provider_bootstrapper.name_resolution + self._provider_bootstrapper.https_connection + self._provider_bootstrapper.download_ca_cert + self._eip_bootstrapper.download_config + + If there was a problem, displays it, otherwise it does nothing. + This is used for intermediate bootstrapping stages, in case + they fail. + """ + passed = data[self._provider_bootstrapper.PASSED_KEY] + if not passed: + self._login_widget.set_enabled(True) + self._login_widget.set_status( + self.tr("Unable to connect: Problem with provider")) + logger.error(data[self._provider_bootstrapper.ERROR_KEY]) + + def _eip_intermediate_stage(self, data): + """ + SLOT + TRIGGERS: + self._eip_bootstrapper.download_config + + If there was a problem, displays it, otherwise it does nothing. + This is used for intermediate bootstrapping stages, in case + they fail. + """ + passed = data[self._provider_bootstrapper.PASSED_KEY] + if not passed: + self._login_widget.set_status( + self.tr("Unable to connect: Problem with provider")) + logger.error(data[self._provider_bootstrapper.ERROR_KEY]) + self._already_started_eip = False + + def _eip_finished(self, exitCode): + """ + SLOT + TRIGGERS: + self._vpn.process_finished + + Triggered when the EIP/VPN process finishes to set the UI + accordingly. + """ + logger.info("VPN process finished with exitCode %s..." + % (exitCode,)) + + # Ideally we would have the right exit code here, + # but the use of different wrappers (pkexec, cocoasudo) swallows + # the openvpn exit code so we get zero exit in some cases where we + # shouldn't. As a workaround we just use a flag to indicate + # a purposeful switch off, and mark everything else as unexpected. + + # In the near future we should trigger a native notification from here, + # since the user really really wants to know she is unprotected asap. + # And the right thing to do will be to fail-close. + + # TODO we should have a way of parsing the latest lines in the vpn + # log buffer so we can have a more precise idea of which type + # of error did we have (server side, local problem, etc) + abnormal = True + + # XXX check if these exitCodes are pkexec/cocoasudo specific + if exitCode in (126, 127): + self._status_panel.set_global_status( + self.tr("Encrypted Internet could not be launched " + "because you did not authenticate properly."), + error=True) + self._vpn.killit() + elif exitCode != 0 or not self.user_stopped_eip: + self._status_panel.set_global_status( + self.tr("Encrypted Internet finished in an " + "unexpected manner!"), error=True) + else: + abnormal = False + if exitCode == 0 and IS_MAC: + # XXX remove this warning after I fix cocoasudo. + logger.warning("The above exit code MIGHT BE WRONG.") + self._stop_eip(abnormal) + + def _on_raise_window_event(self, req): + """ + Callback for the raise window event + """ + if IS_WIN: + raise_window_ack() + self.raise_window.emit() + + def _do_raise_mainwindow(self): + """ + SLOT + TRIGGERS: + self._on_raise_window_event + + Triggered when we receive a RAISE_WINDOW event. + """ + TOPFLAG = QtCore.Qt.WindowStaysOnTopHint + self.setWindowFlags(self.windowFlags() | TOPFLAG) + self.show() + self.setWindowFlags(self.windowFlags() & ~TOPFLAG) + self.show() + if IS_MAC: + self.raise_() + + def _cleanup_pidfiles(self): + """ + Removes lockfiles on a clean shutdown. + + Triggered after aboutToQuit signal. + """ + if IS_WIN: + WindowsLock.release_all_locks() + + def _cleanup_and_quit(self): + """ + Call all the cleanup actions in a serialized way. + Should be called from the quit function. + """ + logger.debug('About to quit, doing cleanup...') + + if self._imap_service is not None: + self._imap_service.stop() + + if self._srp_auth is not None: + if self._srp_auth.get_session_id() is not None or \ + self._srp_auth.get_token() is not None: + # XXX this can timeout after loong time: See #3368 + self._srp_auth.logout() + + if self._soledad: + logger.debug("Closing soledad...") + self._soledad.close() + else: + logger.error("No instance of soledad was found.") + + logger.debug('Terminating vpn') + self._vpn.terminate(shutdown=True) + + if self._login_defer: + logger.debug("Cancelling login defer.") + self._login_defer.cancel() + + if self._download_provider_defer: + logger.debug("Cancelling download provider defer.") + self._download_provider_defer.cancel() + + # TODO missing any more cancels? + + logger.debug('Cleaning pidfiles') + self._cleanup_pidfiles() + + def quit(self): + """ + Cleanup and tidely close the main window before quitting. + """ + # TODO: separate the shutting down of services from the + # UI stuff. + self._cleanup_and_quit() + + self._really_quit = True + + if self._wizard: + self._wizard.close() + + if self._logger_window: + self._logger_window.close() + + self.close() + + if self._quit_callback: + self._quit_callback() + + logger.debug('Bye.') + + +if __name__ == "__main__": + import signal + + def sigint_handler(*args, **kwargs): + logger.debug('SIGINT catched. shutting down...') + mainwindow = args[0] + mainwindow.quit() + + import sys + + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + app = QtGui.QApplication(sys.argv) + mainwindow = MainWindow() + mainwindow.show() + + timer = QtCore.QTimer() + timer.start(500) + timer.timeout.connect(lambda: None) + + sigint = partial(sigint_handler, mainwindow) + signal.signal(signal.SIGINT, sigint) + + sys.exit(app.exec_()) diff --git a/src/leap/bitmask/gui/statuspanel.py b/src/leap/bitmask/gui/statuspanel.py new file mode 100644 index 00000000..f3424c7c --- /dev/null +++ b/src/leap/bitmask/gui/statuspanel.py @@ -0,0 +1,459 @@ +# -*- coding: utf-8 -*- +# statuspanel.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 . + +""" +Status Panel widget implementation +""" +import logging + +from datetime import datetime +from functools import partial +from PySide import QtCore, QtGui + +from ui_statuspanel import Ui_StatusPanel + +from leap.common.check import leap_assert_type +from leap.services.eip.vpnprocess import VPNManager +from leap.platform_init import IS_WIN, IS_LINUX +from leap.util import first + +logger = logging.getLogger(__name__) + + +class RateMovingAverage(object): + """ + Moving window average for calculating + upload and download rates. + """ + SAMPLE_SIZE = 5 + + def __init__(self): + """ + Initializes an empty array of fixed size + """ + self.reset() + + def reset(self): + self._data = [None for i in xrange(self.SAMPLE_SIZE)] + + def append(self, x): + """ + Appends a new data point to the collection. + + :param x: A tuple containing timestamp and traffic points + in the form (timestamp, traffic) + :type x: tuple + """ + self._data.pop(0) + self._data.append(x) + + def get(self): + """ + Gets the collection. + """ + return self._data + + def get_average(self): + """ + Gets the moving average. + """ + data = filter(None, self.get()) + traff = [traffic for (ts, traffic) in data] + times = [ts for (ts, traffic) in data] + + try: + deltatraffic = traff[-1] - first(traff) + deltat = (times[-1] - first(times)).seconds + except IndexError: + deltatraffic = 0 + deltat = 0 + + try: + rate = float(deltatraffic) / float(deltat) / 1024 + except ZeroDivisionError: + rate = 0 + + # In some cases we get negative rates + if rate < 0: + rate = 0 + + return rate + + def get_total(self): + """ + Gets the total accumulated throughput. + """ + try: + return self._data[-1][1] / 1024 + except TypeError: + return 0 + + +class StatusPanelWidget(QtGui.QWidget): + """ + Status widget that displays the current state of the LEAP services + """ + + start_eip = QtCore.Signal() + stop_eip = QtCore.Signal() + + DISPLAY_TRAFFIC_RATES = True + RATE_STR = "%14.2f KB/s" + TOTAL_STR = "%14.2f Kb" + + def __init__(self, parent=None): + QtGui.QWidget.__init__(self, parent) + + self._systray = None + self._action_eip_status = None + + self.ui = Ui_StatusPanel() + self.ui.setupUi(self) + + self.ui.btnEipStartStop.setEnabled(False) + self.ui.btnEipStartStop.clicked.connect( + self.start_eip) + + self.hide_status_box() + + # Set the EIP status icons + self.CONNECTING_ICON = None + self.CONNECTED_ICON = None + self.ERROR_ICON = None + self.CONNECTING_ICON_TRAY = None + self.CONNECTED_ICON_TRAY = None + self.ERROR_ICON_TRAY = None + self._set_eip_icons() + + self._set_traffic_rates() + self._make_status_clickable() + + def _make_status_clickable(self): + """ + Makes upload and download figures clickable. + """ + onclicked = self._on_VPN_status_clicked + self.ui.btnUpload.clicked.connect(onclicked) + self.ui.btnDownload.clicked.connect(onclicked) + + def _on_VPN_status_clicked(self): + """ + SLOT + TRIGGER: self.ui.btnUpload.clicked + self.ui.btnDownload.clicked + + Toggles between rate and total throughput display for vpn + status figures. + """ + self.DISPLAY_TRAFFIC_RATES = not self.DISPLAY_TRAFFIC_RATES + self.update_vpn_status(None) # refresh + + def _set_traffic_rates(self): + """ + Initializes up and download rates. + """ + self._up_rate = RateMovingAverage() + self._down_rate = RateMovingAverage() + + self.ui.btnUpload.setText(self.RATE_STR % (0,)) + self.ui.btnDownload.setText(self.RATE_STR % (0,)) + + def _reset_traffic_rates(self): + """ + Resets up and download rates, and cleans up the labels. + """ + self._up_rate.reset() + self._down_rate.reset() + self.update_vpn_status(None) + + def _update_traffic_rates(self, up, down): + """ + Updates up and download rates. + + :param up: upload total. + :type up: int + :param down: download total. + :type down: int + """ + ts = datetime.now() + self._up_rate.append((ts, up)) + self._down_rate.append((ts, down)) + + def _get_traffic_rates(self): + """ + Gets the traffic rates (in KB/s). + + :returns: a tuple with the (up, down) rates + :rtype: tuple + """ + up = self._up_rate + down = self._down_rate + + return (up.get_average(), down.get_average()) + + def _get_traffic_totals(self): + """ + Gets the traffic total throughput (in Kb). + + :returns: a tuple with the (up, down) totals + :rtype: tuple + """ + up = self._up_rate + down = self._down_rate + + return (up.get_total(), down.get_total()) + + def _set_eip_icons(self): + """ + Sets the EIP status icons for the main window and for the tray + + MAC : dark icons + LINUX : dark icons in window, light icons in tray + WIN : light icons + """ + EIP_ICONS = EIP_ICONS_TRAY = ( + ":/images/conn_connecting-light.png", + ":/images/conn_connected-light.png", + ":/images/conn_error-light.png") + + if IS_LINUX: + EIP_ICONS_TRAY = ( + ":/images/conn_connecting.png", + ":/images/conn_connected.png", + ":/images/conn_error.png") + elif IS_WIN: + EIP_ICONS = EIP_ICONS_TRAY = ( + ":/images/conn_connecting.png", + ":/images/conn_connected.png", + ":/images/conn_error.png") + + self.CONNECTING_ICON = QtGui.QPixmap(EIP_ICONS[0]) + self.CONNECTED_ICON = QtGui.QPixmap(EIP_ICONS[1]) + self.ERROR_ICON = QtGui.QPixmap(EIP_ICONS[2]) + + self.CONNECTING_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[0]) + self.CONNECTED_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[1]) + self.ERROR_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[2]) + + def set_systray(self, systray): + """ + Sets the systray object to use. + + :param systray: Systray object + :type systray: QtGui.QSystemTrayIcon + """ + leap_assert_type(systray, QtGui.QSystemTrayIcon) + self._systray = systray + + def set_action_eip_startstop(self, action_eip_startstop): + """ + Sets the action_eip_startstop to use. + + :param action_eip_startstop: action_eip_status to be used + :type action_eip_startstop: QtGui.QAction + """ + self._action_eip_startstop = action_eip_startstop + + def set_action_eip_status(self, action_eip_status): + """ + Sets the action_eip_status to use. + + :param action_eip_status: action_eip_status to be used + :type action_eip_status: QtGui.QAction + """ + leap_assert_type(action_eip_status, QtGui.QAction) + self._action_eip_status = action_eip_status + + def set_global_status(self, status, error=False): + """ + Sets the global status label. + + :param status: status message + :type status: str or unicode + :param error: if the status is an erroneous one, then set this + to True + :type error: bool + """ + leap_assert_type(error, bool) + if error: + status = "%s" % (status,) + self.ui.lblGlobalStatus.setText(status) + self.ui.globalStatusBox.show() + + def hide_status_box(self): + """ + Hide global status box. + """ + self.ui.globalStatusBox.hide() + + def set_eip_status(self, status, error=False): + """ + Sets the status label at the VPN stage to status + + :param status: status message + :type status: str or unicode + :param error: if the status is an erroneous one, then set this + to True + :type error: bool + """ + leap_assert_type(error, bool) + + self._systray.setToolTip(status) + if error: + status = "%s" % (status,) + self.ui.lblEIPStatus.setText(status) + + def set_startstop_enabled(self, value): + """ + Enable or disable btnEipStartStop and _action_eip_startstop + based on value + + :param value: True for enabled, False otherwise + :type value: bool + """ + leap_assert_type(value, bool) + self.ui.btnEipStartStop.setEnabled(value) + self._action_eip_startstop.setEnabled(value) + + def eip_pre_up(self): + """ + Triggered when the app activates eip. + Hides the status box and disables the start/stop button. + """ + self.hide_status_box() + self.set_startstop_enabled(False) + + def eip_started(self): + """ + Sets the state of the widget to how it should look after EIP + has started + """ + self.ui.btnEipStartStop.setText(self.tr("Turn OFF")) + self.ui.btnEipStartStop.disconnect(self) + self.ui.btnEipStartStop.clicked.connect( + self.stop_eip) + + def eip_stopped(self): + """ + Sets the state of the widget to how it should look after EIP + has stopped + """ + self._reset_traffic_rates() + self.ui.btnEipStartStop.setText(self.tr("Turn ON")) + self.ui.btnEipStartStop.disconnect(self) + self.ui.btnEipStartStop.clicked.connect( + self.start_eip) + + def set_icon(self, icon): + """ + Sets the icon to display for EIP + + :param icon: icon to display + :type icon: QPixmap + """ + self.ui.lblVPNStatusIcon.setPixmap(icon) + + def update_vpn_status(self, data): + """ + SLOT + TRIGGER: VPN.status_changed + + Updates the download/upload labels based on the data provided + by the VPN thread. + + :param data: a dictionary with the tcp/udp write and read totals. + If data is None, we just will refresh the display based + on the previous data. + :type data: dict + """ + if data: + upload = float(data[VPNManager.TCPUDP_WRITE_KEY] or "0") + download = float(data[VPNManager.TCPUDP_READ_KEY] or "0") + self._update_traffic_rates(upload, download) + + if self.DISPLAY_TRAFFIC_RATES: + uprate, downrate = self._get_traffic_rates() + upload_str = self.RATE_STR % (uprate,) + download_str = self.RATE_STR % (downrate,) + + else: # display total throughput + uptotal, downtotal = self._get_traffic_totals() + upload_str = self.TOTAL_STR % (uptotal,) + download_str = self.TOTAL_STR % (downtotal,) + + self.ui.btnUpload.setText(upload_str) + self.ui.btnDownload.setText(download_str) + + def update_vpn_state(self, data): + """ + SLOT + TRIGGER: VPN.state_changed + + Updates the displayed VPN state based on the data provided by + the VPN thread + """ + status = data[VPNManager.STATUS_STEP_KEY] + self.set_eip_status_icon(status) + if status == "CONNECTED": + self.set_eip_status(self.tr("ON")) + # Only now we can properly enable the button. + self.set_startstop_enabled(True) + elif status == "AUTH": + self.set_eip_status(self.tr("Authenticating...")) + elif status == "GET_CONFIG": + self.set_eip_status(self.tr("Retrieving configuration...")) + elif status == "WAIT": + self.set_eip_status(self.tr("Waiting to start...")) + elif status == "ASSIGN_IP": + self.set_eip_status(self.tr("Assigning IP")) + elif status == "ALREADYRUNNING": + # Put the following calls in Qt's event queue, otherwise + # the UI won't update properly + QtCore.QTimer.singleShot(0, self.stop_eip) + QtCore.QTimer.singleShot(0, partial(self.set_global_status, + self.tr("Unable to start VPN, " + "it's already " + "running."))) + else: + self.set_eip_status(status) + + def set_eip_status_icon(self, status): + """ + Given a status step from the VPN thread, set the icon properly + + :param status: status step + :type status: str + """ + selected_pixmap = self.ERROR_ICON + selected_pixmap_tray = self.ERROR_ICON_TRAY + tray_message = self.tr("Encryption is OFF") + if status in ("WAIT", "AUTH", "GET_CONFIG", + "RECONNECTING", "ASSIGN_IP"): + selected_pixmap = self.CONNECTING_ICON + selected_pixmap_tray = self.CONNECTING_ICON_TRAY + tray_message = self.tr("Turning ON") + elif status in ("CONNECTED"): + tray_message = self.tr("Encryption is ON") + selected_pixmap = self.CONNECTED_ICON + selected_pixmap_tray = self.CONNECTED_ICON_TRAY + + self.set_icon(selected_pixmap) + self._systray.setIcon(QtGui.QIcon(selected_pixmap_tray)) + self._action_eip_status.setText(tray_message) + + def set_provider(self, provider): + self.ui.lblProvider.setText(provider) diff --git a/src/leap/bitmask/gui/twisted_main.py b/src/leap/bitmask/gui/twisted_main.py new file mode 100644 index 00000000..c7add3ee --- /dev/null +++ b/src/leap/bitmask/gui/twisted_main.py @@ -0,0 +1,60 @@ +# -*- 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 . +""" +Main functions for integration of twisted reactor +""" +import logging + +from twisted.internet import error + +# Resist the temptation of putting the import reactor here, +# it will raise an "reactor already imported" error. + +logger = logging.getLogger(__name__) + + +def start(app): + """ + Start the mainloop. + + :param app: the main qt QApplication instance. + :type app: QtCore.QApplication + """ + from twisted.internet import reactor + logger.debug('starting twisted reactor') + + # this seems to be troublesome under some + # unidentified settings. + #reactor.run() + + reactor.runReturn() + app.exec_() + + +def quit(app): + """ + Stop the mainloop. + + :param app: the main qt QApplication instance. + :type app: QtCore.QApplication + """ + from twisted.internet import reactor + logger.debug('stopping twisted reactor') + try: + reactor.stop() + except error.ReactorNotRunning: + logger.debug('reactor not running') diff --git a/src/leap/bitmask/gui/ui/loggerwindow.ui b/src/leap/bitmask/gui/ui/loggerwindow.ui new file mode 100644 index 00000000..b08428a9 --- /dev/null +++ b/src/leap/bitmask/gui/ui/loggerwindow.ui @@ -0,0 +1,155 @@ + + + LoggerWindow + + + + 0 + 0 + 648 + 469 + + + + Logs + + + + :/images/mask-icon.png:/images/mask-icon.png + + + + + + + + + + + Debug + + + + :/images/oxygen-icons/script-error.png:/images/oxygen-icons/script-error.png + + + true + + + true + + + true + + + + + + + Info + + + + :/images/oxygen-icons/dialog-information.png:/images/oxygen-icons/dialog-information.png + + + true + + + true + + + true + + + + + + + Warning + + + + :/images/oxygen-icons/dialog-warning.png:/images/oxygen-icons/dialog-warning.png + + + true + + + true + + + true + + + + + + + Error + + + + :/images/oxygen-icons/dialog-error.png:/images/oxygen-icons/dialog-error.png + + + true + + + true + + + true + + + + + + + Critical + + + + :/images/oxygen-icons/edit-bomb.png:/images/oxygen-icons/edit-bomb.png + + + true + + + true + + + true + + + + + + + Save to file + + + + :/images/oxygen-icons/document-save-as.png:/images/oxygen-icons/document-save-as.png + + + + + + + + + btnDebug + btnInfo + btnWarning + btnError + btnCritical + btnSave + txtLogHistory + + + + + + + diff --git a/src/leap/bitmask/gui/ui/login.ui b/src/leap/bitmask/gui/ui/login.ui new file mode 100644 index 00000000..42a6897a --- /dev/null +++ b/src/leap/bitmask/gui/ui/login.ui @@ -0,0 +1,132 @@ + + + LoginWidget + + + + 0 + 0 + 356 + 223 + + + + Form + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Create a new account + + + + + + + <b>Provider:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + + Remember username and password + + + + + + + <b>Username:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + <b>Password:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Log In + + + + + + + + + + Qt::AlignCenter + + + true + + + + + + + cmbProviders + lnUser + lnPassword + chkRemember + btnLogin + btnCreateAccount + + + + diff --git a/src/leap/bitmask/gui/ui/mainwindow.ui b/src/leap/bitmask/gui/ui/mainwindow.ui new file mode 100644 index 00000000..ecd3cbe9 --- /dev/null +++ b/src/leap/bitmask/gui/ui/mainwindow.ui @@ -0,0 +1,315 @@ + + + MainWindow + + + + 0 + 0 + 429 + 579 + + + + Bitmask + + + + :/images/mask-icon.png:/images/mask-icon.png + + + Qt::ImhHiddenText + + + + 128 + 128 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + There are new updates available, please restart. + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + More... + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + 1 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + false + + + + + + :/images/mask-launcher.png + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Show Log + + + true + + + false + + + true + + + + + + + + + + + 0 + 0 + 429 + 21 + + + + + &Session + + + + + + + + Help + + + + + + + + + + + + Log &out + + + + + &Quit + + + + + About &Bitmask + + + + + &Help + + + + + &Wizard + + + + + Show &logs + + + + + + + + + diff --git a/src/leap/bitmask/gui/ui/statuspanel.ui b/src/leap/bitmask/gui/ui/statuspanel.ui new file mode 100644 index 00000000..3482ac7c --- /dev/null +++ b/src/leap/bitmask/gui/ui/statuspanel.ui @@ -0,0 +1,289 @@ + + + StatusPanel + + + + 0 + 0 + 384 + 477 + + + + Form + + + + + + font: bold; + + + user@domain.org + + + true + + + + + + + true + + + + + + + + + + + Encrypted Internet: + + + + + + + font: bold; + + + Off + + + Qt::AutoText + + + Qt::AlignCenter + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Turn On + + + + + + + + + Qt::Vertical + + + QSizePolicy::Preferred + + + + 0 + 11 + + + + + + + + + 64 + 64 + + + + + + + :/images/light/64/network-eip-down.png + + + Qt::AlignCenter + + + + + + + 4 + + + QLayout::SetDefaultConstraint + + + + + + + + :/images/light/16/down-arrow.png + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 120 + 16777215 + + + + PointingHandCursor + + + 0.0 KB/s + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + + + :/images/light/16/up-arrow.png + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 120 + 16777215 + + + + PointingHandCursor + + + 0.0 KB/s + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + false + + + + + + false + + + ... + + + true + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + diff --git a/src/leap/bitmask/gui/ui/wizard.ui b/src/leap/bitmask/gui/ui/wizard.ui new file mode 100644 index 00000000..a8f66bbc --- /dev/null +++ b/src/leap/bitmask/gui/ui/wizard.ui @@ -0,0 +1,846 @@ + + + Wizard + + + + 0 + 0 + 536 + 452 + + + + Bitmask first run + + + + :/images/mask-icon.png:/images/mask-icon.png + + + true + + + QWizard::ModernStyle + + + QWizard::IndependentPages + + + + Welcome + + + This is the Bitmask first run wizard + + + 0 + + + + + + Log In with my credentials + + + + + + + <html><head/><body><p>Now we will guide you through some configuration that is needed before you can connect for the first time.</p><p>If you ever need to modify these options again, you can find the wizard in the <span style=" font-style:italic;">'Settings'</span> menu from the main window.</p><p>Do you want to <span style=" font-weight:600;">sign up</span> for a new account, or <span style=" font-weight:600;">log in</span> with an already existing username?</p></body></html> + + + Qt::RichText + + + true + + + + + + + Sign up for a new account + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Provider selection + + + Please enter the domain of the provider you want to use for your connection + + + 1 + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 60 + + + + + + + + + + + Check + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + https:// + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Checking for a valid provider + + + + + + Getting provider information + + + + + + + Can we stablish a secure connection? + + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + + + + :/images/Emblem-question.png + + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + + + + :/images/Emblem-question.png + + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + + + + :/images/Emblem-question.png + + + + + + + Can we reach this provider? + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + + + + + + + + + + + + Provider Information + + + Description of services offered by this provider + + + 2 + + + + + + Name + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + Desc + + + true + + + + + + + <b>Services offered:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + services + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + <b>Enrollment policy:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + policy + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <b>URL:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + URL + + + + + + + <b>Description:</b> + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + + Provider setup + + + Gathering configuration options for this provider + + + 3 + + + + + + Qt::Vertical + + + + 20 + 60 + + + + + + + + We are downloading some bits that we need to establish a secure connection with the provider for the first time. + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Setting up provider + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + + + + :/images/Emblem-question.png + + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + + + + :/images/Emblem-question.png + + + + + + + Getting info from the Certificate Authority + + + + + + + Do we trust this Certificate Authority? + + + + + + + Establishing a trust relationship with this provider + + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + + + + :/images/Emblem-question.png + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Register new user + + + Register a new user with provider + + + 4 + + + + QLayout::SetDefaultConstraint + + + 4 + + + + + <b>Password:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + <b>Re-enter password:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Register + + + + + + + Qt::Vertical + + + + 20 + 60 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <b>Username:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + false + + + Remember my username and password + + + + + + + + + + Qt::AutoText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + Service selection + + + Please select the services you would like to have + + + 5 + + + + + + Services by PROVIDER + + + + + + + + + + + + + Congratulations! + + + You have successfully configured Bitmask. + + + 6 + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + :/images/leap-color-big.png + + + + + + + + 0 + 0 + + + + + + + :/images/Globe.png + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + WizardPage + QWizardPage +
wizardpage.h
+ 1 +
+
+ + lblUser + lblPassword + lblPassword2 + btnRegister + rdoRegister + rdoLogin + lnProvider + btnCheck + + + + + + +
diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py new file mode 100644 index 00000000..2b48fc81 --- /dev/null +++ b/src/leap/bitmask/gui/wizard.py @@ -0,0 +1,626 @@ +# -*- coding: utf-8 -*- +# wizard.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 . + +""" +First run wizard +""" +import os +import logging +import json + +from PySide import QtCore, QtGui +from functools import partial +from twisted.internet import threads + +from ui_wizard import Ui_Wizard +from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpregister import SRPRegister +from leap.util.privilege_policies import is_missing_policy_permissions +from leap.util.request_helpers import get_content +from leap.util.keyring_helpers import has_keyring +from leap.services.eip.providerbootstrapper import ProviderBootstrapper +from leap.services import get_supported + +logger = logging.getLogger(__name__) + + +class Wizard(QtGui.QWizard): + """ + First run wizard to register a user and setup a provider + """ + + INTRO_PAGE = 0 + SELECT_PROVIDER_PAGE = 1 + PRESENT_PROVIDER_PAGE = 2 + SETUP_PROVIDER_PAGE = 3 + REGISTER_USER_PAGE = 4 + SERVICES_PAGE = 5 + FINISH_PAGE = 6 + + WEAK_PASSWORDS = ("123456", "qweasd", "qwerty", + "password") + + BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" + + def __init__(self, standalone=False, bypass_checks=False): + """ + Constructor for the main Wizard. + + :param standalone: If True, the application is running as standalone + and the wizard should display some messages according to this. + :type standalone: bool + :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) + + self.standalone = standalone + + self.ui = Ui_Wizard() + self.ui.setupUi(self) + + self.setPixmap(QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(":/images/leap-color-small.png")) + + self.QUESTION_ICON = QtGui.QPixmap(":/images/Emblem-question.png") + self.ERROR_ICON = QtGui.QPixmap(":/images/Dialog-error.png") + self.OK_ICON = QtGui.QPixmap(":/images/Dialog-accept.png") + + # Correspondence for services and their name to display + EIP_LABEL = self.tr("Encrypted Internet") + MX_LABEL = self.tr("Encrypted Mail") + + if self._is_need_eip_password_warning(): + EIP_LABEL += " " + self.tr( + "(will need admin password to start)") + + self.SERVICE_DISPLAY = [ + EIP_LABEL, + MX_LABEL + ] + self.SERVICE_CONFIG = [ + "openvpn", + "mx" + ] + + self._selected_services = set() + self._shown_services = set() + + self._show_register = False + + self.ui.grpCheckProvider.setVisible(False) + self.ui.btnCheck.clicked.connect(self._check_provider) + self.ui.lnProvider.returnPressed.connect(self._check_provider) + + self._provider_bootstrapper = ProviderBootstrapper(bypass_checks) + self._provider_bootstrapper.name_resolution.connect( + self._name_resolution) + self._provider_bootstrapper.https_connection.connect( + self._https_connection) + self._provider_bootstrapper.download_provider_info.connect( + self._download_provider_info) + + self._provider_bootstrapper.download_ca_cert.connect( + self._download_ca_cert) + self._provider_bootstrapper.check_ca_fingerprint.connect( + self._check_ca_fingerprint) + self._provider_bootstrapper.check_api_certificate.connect( + self._check_api_certificate) + + self._domain = None + self._provider_config = ProviderConfig() + + # We will store a reference to the defers for eventual use + # (eg, to cancel them) but not doing anything with them right now. + self._provider_select_defer = None + self._provider_setup_defer = None + + self.currentIdChanged.connect(self._current_id_changed) + + self.ui.lblPassword.setEchoMode(QtGui.QLineEdit.Password) + self.ui.lblPassword2.setEchoMode(QtGui.QLineEdit.Password) + + self.ui.lnProvider.textChanged.connect( + self._enable_check) + + self.ui.lblUser.returnPressed.connect( + self._focus_password) + self.ui.lblPassword.returnPressed.connect( + self._focus_second_password) + self.ui.lblPassword2.returnPressed.connect( + self._register) + self.ui.btnRegister.clicked.connect( + self._register) + + usernameRe = QtCore.QRegExp(self.BARE_USERNAME_REGEX) + self.ui.lblUser.setValidator( + QtGui.QRegExpValidator(usernameRe, self)) + + self.page(self.REGISTER_USER_PAGE).setCommitPage(True) + + self._username = None + self._password = None + + self.page(self.REGISTER_USER_PAGE).setButtonText( + QtGui.QWizard.CommitButton, self.tr("&Next >")) + self.page(self.FINISH_PAGE).setButtonText( + QtGui.QWizard.FinishButton, self.tr("Connect")) + + # XXX: Temporary removal for enrollment policy + # https://leap.se/code/issues/2922 + self.ui.label_12.setVisible(False) + self.ui.lblProviderPolicy.setVisible(False) + + def get_domain(self): + return self._domain + + def get_username(self): + return self._username + + def get_password(self): + return self._password + + def get_remember(self): + return has_keyring() and self.ui.chkRemember.isChecked() + + def get_services(self): + return self._selected_services + + def _enable_check(self, text): + self.ui.btnCheck.setEnabled(len(self.ui.lnProvider.text()) != 0) + self._reset_provider_check() + + def _focus_password(self): + """ + Focuses at the password lineedit for the registration page + """ + self.ui.lblPassword.setFocus() + + def _focus_second_password(self): + """ + Focuses at the second password lineedit for the registration page + """ + self.ui.lblPassword2.setFocus() + + def _basic_password_checks(self, username, password, password2): + """ + Performs basic password checks to avoid really easy passwords. + + :param username: username provided at the registrarion form + :type username: str + :param password: password from the registration form + :type password: str + :param password2: second password from the registration form + :type password: str + + :return: returns True if all the checks pass, False otherwise + :rtype: bool + """ + message = None + + if message is None and password != password2: + message = self.tr("Passwords don't match") + + if message is None and len(password) < 6: + message = self.tr("Password too short") + + if message is None and password in self.WEAK_PASSWORDS: + message = self.tr("Password too easy") + + if message is None and username == password: + message = self.tr("Password equal to username") + + if message is not None: + self._set_register_status(message, error=True) + self._focus_password() + return False + + return True + + def _register(self): + """ + Performs the registration based on the values provided in the form + """ + self.ui.btnRegister.setEnabled(False) + + username = self.ui.lblUser.text() + password = self.ui.lblPassword.text() + password2 = self.ui.lblPassword2.text() + + if self._basic_password_checks(username, password, password2): + register = SRPRegister(provider_config=self._provider_config) + register.registration_finished.connect( + self._registration_finished) + + threads.deferToThread( + partial(register.register_user, + username.encode("utf8"), + password.encode("utf8"))) + + self._username = username + self._password = password + self._set_register_status(self.tr("Starting registration...")) + else: + self.ui.btnRegister.setEnabled(True) + + def _set_registration_fields_visibility(self, visible): + """ + This method hides the username and password labels and inputboxes. + + :param visible: sets the visibility of the widgets + True: widgets are visible or False: are not + :type visible: bool + """ + # username and password inputs + self.ui.lblUser.setVisible(visible) + self.ui.lblPassword.setVisible(visible) + self.ui.lblPassword2.setVisible(visible) + + # username and password labels + self.ui.label_15.setVisible(visible) + self.ui.label_16.setVisible(visible) + self.ui.label_17.setVisible(visible) + + # register button + self.ui.btnRegister.setVisible(visible) + + def _registration_finished(self, ok, req): + if ok: + user_domain = self._username + "@" + self._domain + message = "

" + message += self.tr("User %s successfully registered.") % ( + user_domain, ) + message += "

" + self._set_register_status(message) + + self.ui.lblPassword2.clearFocus() + self._set_registration_fields_visibility(False) + + # Allow the user to remember his password + if has_keyring(): + self.ui.chkRemember.setVisible(True) + self.ui.chkRemember.setEnabled(True) + + self.page(self.REGISTER_USER_PAGE).set_completed() + self.button(QtGui.QWizard.BackButton).setEnabled(False) + else: + old_username = self._username + self._username = None + self._password = None + error_msg = self.tr("Unknown error") + try: + content, _ = get_content(req) + json_content = json.loads(content) + error_msg = json_content.get("errors").get("login")[0] + if not error_msg.istitle(): + error_msg = "%s %s" % (old_username, error_msg) + except Exception as e: + logger.error("Unknown error: %r" % (e,)) + + self._set_register_status(error_msg, error=True) + self.ui.btnRegister.setEnabled(True) + + def _set_register_status(self, status, error=False): + """ + Sets the status label in the registration page to status + + :param status: status message to display, can be HTML + :type status: str + """ + if error: + status = "%s" % (status,) + self.ui.lblRegisterStatus.setText(status) + + def _reset_provider_check(self): + """ + Resets the UI for checking a provider. Also resets the domain + in this object. + """ + self.ui.lblNameResolution.setPixmap(None) + self.ui.lblHTTPS.setPixmap(None) + self.ui.lblProviderInfo.setPixmap(None) + self.ui.lblProviderSelectStatus.setText("") + self._domain = None + self.button(QtGui.QWizard.NextButton).setEnabled(False) + self.page(self.SELECT_PROVIDER_PAGE).set_completed(False) + + def _reset_provider_setup(self): + """ + Resets the UI for setting up a provider. + """ + self.ui.lblDownloadCaCert.setPixmap(None) + self.ui.lblCheckCaFpr.setPixmap(None) + self.ui.lblCheckApiCert.setPixmap(None) + + def _check_provider(self): + """ + SLOT + TRIGGERS: + self.ui.btnCheck.clicked + self.ui.lnProvider.returnPressed + + Starts the checks for a given provider + """ + if len(self.ui.lnProvider.text()) == 0: + return + + self.ui.grpCheckProvider.setVisible(True) + self.ui.btnCheck.setEnabled(False) + self.ui.lnProvider.setEnabled(False) + self.button(QtGui.QWizard.BackButton).clearFocus() + self._domain = self.ui.lnProvider.text() + + self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) + self._provider_select_defer = self._provider_bootstrapper.\ + run_provider_select_checks(self._domain) + + def _complete_task(self, data, label, complete=False, complete_page=-1): + """ + Checks a task and completes a page if specified + + :param data: data as it comes from the bootstrapper thread for + a specific check + :type data: dict + :param label: label that displays the status icon for a + specific check that corresponds to the data + :type label: QtGui.QLabel + :param complete: if True, it completes the page specified, + which must be of type WizardPage + :type complete: bool + :param complete_page: page id to complete + :type complete_page: int + """ + passed = data[self._provider_bootstrapper.PASSED_KEY] + error = data[self._provider_bootstrapper.ERROR_KEY] + if passed: + label.setPixmap(self.OK_ICON) + if complete: + self.page(complete_page).set_completed() + self.button(QtGui.QWizard.NextButton).setFocus() + else: + label.setPixmap(self.ERROR_ICON) + logger.error(error) + + def _name_resolution(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.name_resolution + + Sets the status for the name resolution check + """ + self._complete_task(data, self.ui.lblNameResolution) + status = "" + passed = data[self._provider_bootstrapper.PASSED_KEY] + if not passed: + status = self.tr("Non-existent " + "provider") + else: + self.ui.lblHTTPS.setPixmap(self.QUESTION_ICON) + self.ui.lblProviderSelectStatus.setText(status) + self.ui.btnCheck.setEnabled(not passed) + self.ui.lnProvider.setEnabled(not passed) + + def _https_connection(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.https_connection + + Sets the status for the https connection check + """ + self._complete_task(data, self.ui.lblHTTPS) + status = "" + passed = data[self._provider_bootstrapper.PASSED_KEY] + if not passed: + status = self.tr("%s") \ + % (data[self._provider_bootstrapper.ERROR_KEY]) + self.ui.lblProviderSelectStatus.setText(status) + else: + self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) + self.ui.btnCheck.setEnabled(not passed) + self.ui.lnProvider.setEnabled(not passed) + + def _download_provider_info(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.download_provider_info + + Sets the status for the provider information download + check. Since this check is the last of this set, it also + completes the page if passed + """ + if self._provider_config.load(os.path.join("leap", + "providers", + self._domain, + "provider.json")): + self._complete_task(data, self.ui.lblProviderInfo, + True, self.SELECT_PROVIDER_PAGE) + else: + new_data = { + self._provider_bootstrapper.PASSED_KEY: False, + self._provider_bootstrapper.ERROR_KEY: + self.tr("Unable to load provider configuration") + } + self._complete_task(new_data, self.ui.lblProviderInfo) + + status = "" + if not data[self._provider_bootstrapper.PASSED_KEY]: + status = self.tr("Not a valid provider" + "") + self.ui.lblProviderSelectStatus.setText(status) + self.ui.btnCheck.setEnabled(True) + self.ui.lnProvider.setEnabled(True) + + def _download_ca_cert(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.download_ca_cert + + Sets the status for the download of the CA certificate check + """ + self._complete_task(data, self.ui.lblDownloadCaCert) + passed = data[self._provider_bootstrapper.PASSED_KEY] + if passed: + self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON) + + def _check_ca_fingerprint(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.check_ca_fingerprint + + Sets the status for the CA fingerprint check + """ + self._complete_task(data, self.ui.lblCheckCaFpr) + passed = data[self._provider_bootstrapper.PASSED_KEY] + if passed: + self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON) + + def _check_api_certificate(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.check_api_certificate + + Sets the status for the API certificate check. Also finishes + the provider bootstrapper thread since it's not needed anymore + from this point on, unless the whole check chain is restarted + """ + self._complete_task(data, self.ui.lblCheckApiCert, + True, self.SETUP_PROVIDER_PAGE) + + def _service_selection_changed(self, service, state): + """ + SLOT + TRIGGER: service_checkbox.stateChanged + Adds the service to the state if the state is checked, removes + it otherwise + + :param service: service to handle + :type service: str + :param state: state of the checkbox + :type state: int + """ + if state == QtCore.Qt.Checked: + self._selected_services = \ + self._selected_services.union(set([service])) + else: + self._selected_services = \ + self._selected_services.difference(set([service])) + + def _populate_services(self): + """ + Loads the services that the provider provides into the UI for + the user to enable or disable. + """ + self.ui.grpServices.setTitle( + self.tr("Services by %s") % + (self._provider_config.get_name(),)) + + services = get_supported( + self._provider_config.get_services()) + + for service in services: + try: + if service not in self._shown_services: + checkbox = QtGui.QCheckBox(self) + service_index = self.SERVICE_CONFIG.index(service) + checkbox.setText(self.SERVICE_DISPLAY[service_index]) + self.ui.serviceListLayout.addWidget(checkbox) + checkbox.stateChanged.connect( + partial(self._service_selection_changed, service)) + checkbox.setChecked(True) + self._shown_services.add(service) + except ValueError: + logger.error( + self.tr("Something went wrong while trying to " + "load service %s" % (service,))) + + def _current_id_changed(self, pageId): + """ + SLOT + TRIGGER: self.currentIdChanged + + Prepares the pages when they appear + """ + if pageId == self.SELECT_PROVIDER_PAGE: + self._reset_provider_check() + self._enable_check("") + + if pageId == self.SETUP_PROVIDER_PAGE: + self._reset_provider_setup() + self.page(pageId).setSubTitle(self.tr("Gathering configuration " + "options for %s") % + (self._provider_config + .get_name(),)) + self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON) + self._provider_setup_defer = self._provider_bootstrapper.\ + run_provider_setup_checks(self._provider_config) + + if pageId == self.PRESENT_PROVIDER_PAGE: + self.page(pageId).setSubTitle(self.tr("Description of services " + "offered by %s") % + (self._provider_config + .get_name(),)) + + lang = QtCore.QLocale.system().name() + self.ui.lblProviderName.setText( + "%s" % + (self._provider_config.get_name(lang=lang),)) + self.ui.lblProviderURL.setText( + "https://%s" % (self._provider_config.get_domain(),)) + self.ui.lblProviderDesc.setText( + "%s" % + (self._provider_config.get_description(lang=lang),)) + + self.ui.lblServicesOffered.setText(self._provider_config + .get_services_string()) + self.ui.lblProviderPolicy.setText(self._provider_config + .get_enrollment_policy()) + + if pageId == self.REGISTER_USER_PAGE: + self.page(pageId).setSubTitle(self.tr("Register a new user with " + "%s") % + (self._provider_config + .get_name(),)) + self.ui.chkRemember.setVisible(False) + + if pageId == self.SERVICES_PAGE: + self._populate_services() + + def _is_need_eip_password_warning(self): + """ + Returns True if we need to add a warning about eip needing + administrative permissions to start. That can be either + because we are running in standalone mode, or because we could + not find the needed privilege escalation mechanisms being operative. + """ + return self.standalone or is_missing_policy_permissions() + + def nextId(self): + """ + Sets the next page id for the wizard based on wether the user + wants to register a new identity or uses an existing one + """ + if self.currentPage() == self.page(self.INTRO_PAGE): + self._show_register = self.ui.rdoRegister.isChecked() + + if self.currentPage() == self.page(self.SETUP_PROVIDER_PAGE): + if self._show_register: + return self.REGISTER_USER_PAGE + else: + return self.SERVICES_PAGE + + return QtGui.QWizard.nextId(self) diff --git a/src/leap/bitmask/gui/wizardpage.py b/src/leap/bitmask/gui/wizardpage.py new file mode 100644 index 00000000..b2a00028 --- /dev/null +++ b/src/leap/bitmask/gui/wizardpage.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# wizardpage.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 . + +from PySide import QtGui + + +class WizardPage(QtGui.QWizardPage): + """ + Simple wizard page helper + """ + + def __init__(self): + QtGui.QWizardPage.__init__(self) + self._completed = False + + def set_completed(self, val=True): + self._completed = val + if val: + self.completeChanged.emit() + + def isComplete(self): + return self._completed + + def cleanupPage(self): + self._completed = False + QtGui.QWizardPage.cleanupPage(self) diff --git a/src/leap/bitmask/platform_init/__init__.py b/src/leap/bitmask/platform_init/__init__.py new file mode 100644 index 00000000..2a262a30 --- /dev/null +++ b/src/leap/bitmask/platform_init/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# __init__.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 . + +""" +System constants +""" +import platform + +_system = platform.system() + +IS_WIN = True if _system == "Windows" else False +IS_MAC = True if _system == "Darwin" else False +IS_LINUX = True if _system == "Linux" else False +IS_UNIX = IS_MAC or IS_LINUX diff --git a/src/leap/bitmask/platform_init/initializers.py b/src/leap/bitmask/platform_init/initializers.py new file mode 100644 index 00000000..46488250 --- /dev/null +++ b/src/leap/bitmask/platform_init/initializers.py @@ -0,0 +1,409 @@ +# -*- coding: utf-8 -*- +# initializers.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 . + +""" +Platform dependant initializing code +""" + +import logging +import os +import platform +import stat +import subprocess +import tempfile + +from PySide import QtGui + +from leap.config.leapsettings import LeapSettings +from leap.services.eip import vpnlaunchers +from leap.util import first +from leap.util import privilege_policies + + +logger = logging.getLogger(__name__) + +# NOTE we could use a deferToThread here, but should +# be aware of this bug: http://www.themacaque.com/?p=1067 + +__all__ = ["init_platform"] + +_system = platform.system() + + +def init_platform(): + """ + Returns the right initializer for the platform we are running in, or + None if no proper initializer is found + """ + initializer = None + try: + initializer = globals()[_system + "Initializer"] + except: + pass + if initializer: + logger.debug("Running initializer for %s" % (platform.system(),)) + initializer() + else: + logger.debug("Initializer not found for %s" % (platform.system(),)) + + +# +# common utils +# + +NOTFOUND_MSG = ("Tried to install %s, but %s " + "not found inside this bundle.") +BADEXEC_MSG = ("Tried to install %s, but %s " + "failed to %s.") + +UPDOWN_NOTFOUND_MSG = NOTFOUND_MSG % ( + "updown scripts", "those were") +UPDOWN_BADEXEC_MSG = BADEXEC_MSG % ( + "updown scripts", "they", "be copied") + + +def get_missing_updown_dialog(): + """ + Creates a dialog for notifying of missing updown scripts. + Returns that dialog. + + :rtype: QtGui.QMessageBox instance + """ + WE_NEED_POWERS = ("To better protect your privacy, " + "Bitmask needs administrative privileges " + "to install helper files. " + "Do you want to proceed?") + msg = QtGui.QMessageBox() + msg.setWindowTitle(msg.tr("Missing up/down scripts")) + msg.setText(msg.tr(WE_NEED_POWERS)) + # but maybe the user really deserve to know more + #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) + return msg + + +def check_missing(): + """ + Checks for the need of installing missing scripts, and + raises a dialog to ask user for permission to do it. + """ + config = LeapSettings() + alert_missing = config.get_alert_missing_scripts() + + launcher = vpnlaunchers.get_platform_launcher() + missing_scripts = launcher.missing_updown_scripts + missing_other = launcher.missing_other_files + + if alert_missing and (missing_scripts() or missing_other()): + msg = get_missing_updown_dialog() + ret = msg.exec_() + + if ret == QtGui.QMessageBox.Yes: + install_missing_fun = globals().get( + "_%s_install_missing_scripts" % (_system.lower(),), + None) + if not install_missing_fun: + logger.warning( + "Installer not found for platform %s." % (_system,)) + return + + # XXX maybe move constants to fun + ok = install_missing_fun(UPDOWN_BADEXEC_MSG, UPDOWN_NOTFOUND_MSG) + if not ok: + msg = QtGui.QMessageBox() + msg.setWindowTitle(msg.tr("Problem installing files")) + msg.setText(msg.tr('Some of the files could not be copied.')) + msg.setIcon(QtGui.QMessageBox.Warning) + msg.exec_() + + elif ret == QtGui.QMessageBox.No: + logger.debug("Not installing missing scripts, " + "user decided to ignore our warning.") + + elif ret == QtGui.QMessageBox.Rejected: + logger.debug( + "Setting alert_missing_scripts to False, we will not " + "ask again") + config.set_alert_missing_scripts(False) +# +# windows initializers +# + + +def _windows_has_tap_device(): + """ + Loops over the windows registry trying to find if the tap0901 tap driver + has been installed on this machine. + """ + import _winreg as reg + + adapter_key = 'SYSTEM\CurrentControlSet\Control\Class' \ + '\{4D36E972-E325-11CE-BFC1-08002BE10318}' + with reg.OpenKey(reg.HKEY_LOCAL_MACHINE, adapter_key) as adapters: + try: + for i in xrange(10000): + key_name = reg.EnumKey(adapters, i) + with reg.OpenKey(adapters, key_name) as adapter: + try: + component_id = reg.QueryValueEx(adapter, + 'ComponentId')[0] + if component_id.startswith("tap0901"): + return True + except WindowsError: + pass + except WindowsError: + pass + return False + + +def WindowsInitializer(): + """ + Raises a dialog in case that the windows tap driver has not been found + in the registry, asking the user for permission to install the driver + """ + if not _windows_has_tap_device(): + msg = QtGui.QMessageBox() + msg.setWindowTitle(msg.tr("TAP Driver")) + msg.setText(msg.tr("Bitmask needs to install the necessary drivers " + "for Encrypted Internet to work. Would you like to " + "proceed?")) + msg.setInformativeText(msg.tr("Encrypted Internet uses VPN, which " + "needs a TAP device installed and none " + "has been found. This will ask for " + "administrative privileges.")) + msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) + msg.setDefaultButton(QtGui.QMessageBox.Yes) + ret = msg.exec_() + + if ret == QtGui.QMessageBox.Yes: + # XXX should do this only if executed inside bundle. + # Let's assume it's the only way it's gonna be executed under win + # by now. + driver_path = os.path.join(os.getcwd(), + "apps", + "eip", + "tap_driver") + dev_installer = os.path.join(driver_path, + "devcon.exe") + if os.path.isfile(dev_installer) and \ + stat.S_IXUSR & os.stat(dev_installer)[stat.ST_MODE] != 0: + inf_path = os.path.join(driver_path, + "OemWin2k.inf") + cmd = [dev_installer, "install", inf_path, "tap0901"] + ret = subprocess.call(cmd, stdout=subprocess.PIPE, shell=True) + else: + logger.error("Tried to install TAP driver, but the installer " + "is not found or not executable") + +# +# Darwin initializer functions +# + + +def _darwin_has_tun_kext(): + """ + Returns True only if we found a directory under the system kext folder + containing a kext named tun.kext, AND we found a startup item named 'tun' + """ + # XXX we should be smarter here and use kextstats output. + + has_kext = os.path.isdir("/System/Library/Extensions/tun.kext") + has_startup = os.path.isdir("/System/Library/StartupItems/tun") + has_tun_and_startup = has_kext and has_startup + logger.debug( + 'platform initializer check: has tun_and_startup = %s' % + (has_tun_and_startup,)) + return has_tun_and_startup + + +def _darwin_install_missing_scripts(badexec, notfound): + """ + Tries to install the missing up/down scripts. + + :param badexec: error for notifying execution error during command. + :type badexec: str + :param notfound: error for notifying missing path. + :type notfound: str + :returns: True if the files could be copied successfully. + :rtype: bool + """ + # We expect to execute this from some way of bundle, since + # the up/down scripts should be put in place by the installer. + success = False + installer_path = os.path.join( + os.getcwd(), + "..", + "Resources", + "openvpn") + launcher = vpnlaunchers.DarwinVPNLauncher + + if os.path.isdir(installer_path): + fd, tempscript = tempfile.mkstemp(prefix="leap_installer-") + try: + scriptlines = launcher.cmd_for_missing_scripts(installer_path) + with os.fdopen(fd, 'w') as f: + f.write(scriptlines) + st = os.stat(tempscript) + os.chmod(tempscript, st.st_mode | stat.S_IEXEC | stat.S_IXUSR | + stat.S_IXGRP | stat.S_IXOTH) + + cmd, args = launcher().get_cocoasudo_installmissing_cmd() + args.append(tempscript) + cmdline = " ".join([cmd] + args) + ret = subprocess.call( + cmdline, stdout=subprocess.PIPE, + shell=True) + success = ret == 0 + if not success: + logger.error("Install missing scripts failed.") + except Exception as exc: + logger.error(badexec) + logger.error("Error was: %r" % (exc,)) + finally: + try: + os.remove(tempscript) + except OSError as exc: + logger.error("%r" % (exc,)) + else: + logger.error(notfound) + logger.debug('path searched: %s' % (installer_path,)) + + return success + + +def DarwinInitializer(): + """ + Raises a dialog in case that the osx tuntap driver has not been found + in the registry, asking the user for permission to install the driver + """ + # XXX split this function into several + + TUNTAP_NOTFOUND_MSG = NOTFOUND_MSG % ( + "tuntaposx kext", "the installer") + TUNTAP_BADEXEC_MSG = BADEXEC_MSG % ( + "tuntaposx kext", "the installer", "be launched") + + # TODO DRY this with other cases, and + # factor out to _should_install() function. + # Leave the dialog as a more generic thing. + + if not _darwin_has_tun_kext(): + msg = QtGui.QMessageBox() + msg.setWindowTitle(msg.tr("TUN Driver")) + msg.setText(msg.tr("Bitmask needs to install the necessary drivers " + "for Encrypted Internet to work. Would you like to " + "proceed?")) + msg.setInformativeText(msg.tr("Encrypted Internet uses VPN, which " + "needs a kernel extension for a TUN " + "device installed, and none " + "has been found. This will ask for " + "administrative privileges.")) + msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) + msg.setDefaultButton(QtGui.QMessageBox.Yes) + ret = msg.exec_() + + if ret == QtGui.QMessageBox.Yes: + installer_path = os.path.abspath( + os.path.join( + os.getcwd(), + "..", + "Resources", + "tuntap-installer.app")) + if os.path.isdir(installer_path): + cmd = ["open '%s'" % (installer_path,)] + try: + ret = subprocess.call( + cmd, stdout=subprocess.PIPE, + shell=True) + except: + logger.error(TUNTAP_BADEXEC_MSG) + else: + logger.error(TUNTAP_NOTFOUND_MSG) + + # Second check, for missing scripts. + check_missing() + + +# +# Linux initializers +# +def _linux_install_missing_scripts(badexec, notfound): + """ + Tries to install the missing up/down scripts. + + :param badexec: error for notifying execution error during command. + :type badexec: str + :param notfound: error for notifying missing path. + :type notfound: str + :returns: True if the files could be copied successfully. + :rtype: bool + """ + success = False + installer_path = os.path.join(os.getcwd(), "apps", "eip", "files") + launcher = vpnlaunchers.LinuxVPNLauncher + + # XXX refactor with darwin, same block. + + if os.path.isdir(installer_path): + fd, tempscript = tempfile.mkstemp(prefix="leap_installer-") + polfd, pol_tempfile = tempfile.mkstemp(prefix="leap_installer-") + try: + path = launcher.OPENVPN_BIN_PATH + policy_contents = privilege_policies.get_policy_contents(path) + + with os.fdopen(polfd, 'w') as f: + f.write(policy_contents) + + pkexec = first(launcher.maybe_pkexec()) + scriptlines = launcher.cmd_for_missing_scripts(installer_path, + pol_tempfile) + with os.fdopen(fd, 'w') as f: + f.write(scriptlines) + + st = os.stat(tempscript) + os.chmod(tempscript, st.st_mode | stat.S_IEXEC | stat.S_IXUSR | + stat.S_IXGRP | stat.S_IXOTH) + cmdline = ["%s %s" % (pkexec, tempscript)] + ret = subprocess.call( + cmdline, stdout=subprocess.PIPE, + shell=True) + success = ret == 0 + if not success: + logger.error("Install missing scripts failed.") + except Exception as exc: + logger.error(badexec) + logger.error("Error was: %r" % (exc,)) + finally: + try: + os.remove(tempscript) + except OSError as exc: + logger.error("%r" % (exc,)) + else: + logger.error(notfound) + logger.debug('path searched: %s' % (installer_path,)) + + return success + + +def LinuxInitializer(): + """ + Raises a dialog in case that either updown scripts or policykit file + are missing or they have incorrect permissions. + """ + check_missing() diff --git a/src/leap/bitmask/platform_init/locks.py b/src/leap/bitmask/platform_init/locks.py new file mode 100644 index 00000000..39b18648 --- /dev/null +++ b/src/leap/bitmask/platform_init/locks.py @@ -0,0 +1,405 @@ +# -*- coding: utf-8 -*- +# locks.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 . +""" +Utilities for handling multi-platform file locking mechanisms +""" +import logging +import errno +import os +import platform + +from leap.common.events import signal as signal_event +from leap.common.events import events_pb2 as proto +from leap import platform_init + +if platform_init.IS_UNIX: + from fcntl import flock, LOCK_EX, LOCK_NB +else: # WINDOWS + import datetime + import glob + import shutil + import time + + from tempfile import gettempdir + + from leap.util import get_modification_ts, update_modification_ts + +logger = logging.getLogger(__name__) + +if platform_init.IS_UNIX: + + class UnixLock(object): + """ + Uses flock to get an exclusive lock over a file. + See man 2 flock + """ + + def __init__(self, path): + """ + iniializes t he UnixLock with the path of the + desired lockfile + """ + + self._fd = None + self.path = path + + def get_lock(self): + """ + Tries to get a lock, and writes the running pid there if successful + """ + gotit, pid = self._get_lock_and_pid() + return gotit + + def get_pid(self): + """ + Returns the pid of the locking process + """ + gotit, pid = self._get_lock_and_pid() + return pid + + def _get_lock(self): + """ + Tries to get a lock, returning True if successful + + :rtype: bool + """ + self._fd = os.open(self.path, os.O_CREAT | os.O_RDWR) + + try: + flock(self._fd, LOCK_EX | LOCK_NB) + except IOError as exc: + # could not get the lock + #import ipdb; ipdb.set_trace() + + if exc.args[0] in (errno.EDEADLK, errno.EAGAIN): + # errno 11 or 35 + # Resource temporarily unavailable + return False + else: + raise + return True + + @property + def locked_by_us(self): + """ + Returns True if the pid in the pidfile + is ours. + + :rtype: bool + """ + gotit, pid = self._get_lock_and_pid() + return pid == os.getpid() + + def _get_lock_and_pid(self): + """ + Tries to get a lock over the file. + Returns (locked, pid) tuple. + + :rtype: tuple + """ + + if self._get_lock(): + self._write_to_pidfile() + return True, None + + return False, self._read_from_pidfile() + + def _read_from_pidfile(self): + """ + Tries to read pid from the pidfile, + returns False if no content found. + """ + + pidfile = os.read( + self._fd, 16) + if not pidfile: + return False + + try: + return int(pidfile.strip()) + except Exception as exc: + exc.args += (pidfile, self.lock_file) + raise + + def _write_to_pidfile(self): + """ + Writes the pid of the running process + to the pidfile + """ + fd = self._fd + os.ftruncate(fd, 0) + os.write(fd, '%d\n' % os.getpid()) + os.fsync(fd) + + +if platform_init.IS_WIN: + + # Time to wait (in secs) before assuming a raise window signal has not been + # ack-ed. + + RAISE_WINDOW_TIMEOUT = 2 + + # How many steps to do while checking lockfile ts update. + + RAISE_WINDOW_WAIT_STEPS = 10 + + def _release_lock(name): + """ + Tries to remove a folder path. + + :param name: folder lock to remove + :type name: str + """ + try: + shutil.rmtree(name) + return True + except WindowsError as exc: + if exc.errno in (errno.EPIPE, errno.ENOENT, + errno.ESRCH, errno.EACCES): + logger.warning( + 'exception while trying to remove the lockfile dir') + logger.warning('errno %s: %s' % (exc.errno, exc.args[1])) + # path does not exist + return False + else: + logger.debug('errno = %s' % (exc.errno,)) + # we did not foresee this error, better add it explicitely + raise + + class WindowsLock(object): + """ + Creates a lock based on the atomic nature of mkdir on Windows + system calls. + """ + LOCKBASE = os.path.join(gettempdir(), "leap-client-lock") + + def __init__(self): + """ + Initializes the lock. + Sets the lock name to basename plus the process pid. + """ + self._fd = None + pid = os.getpid() + self.name = "%s-%s" % (self.LOCKBASE, pid) + self.pid = pid + + def get_lock(self): + """ + Tries to get a lock, and writes the running pid there if successful + """ + gotit = self._get_lock() + return gotit + + def _get_lock(self): + """ + Tries to write to a file with the current pid as part of the name + """ + try: + self._fd = os.makedirs(self.name) + except OSError as exc: + # could not create the dir + if exc.args[0] == 183: + logger.debug('cannot create dir') + # cannot create dir with existing name + return False + else: + raise + return self._is_one_pidfile()[0] + + def _is_one_pidfile(self): + """ + Returns True, pid if there is only one pidfile with the expected + base path + + :rtype: tuple + """ + pidfiles = glob.glob(self.LOCKBASE + '-*') + if len(pidfiles) == 1: + pid = pidfiles[0].split('-')[-1] + return True, int(pid) + else: + return False, None + + def get_pid(self): + """ + Returns the pid of the locking process. + + :rtype: int + """ + # XXX assert there is only one? + _, pid = self._is_one_pidfile() + return pid + + def get_locking_path(self): + """ + Returns the pid path of the locking process. + + :rtype: str + """ + pid = self.get_pid() + if pid: + return "%s-%s" % (self.LOCKBASE, pid) + + def release_lock(self, name=None): + """ + Releases the pidfile dir for this process, by removing it. + """ + if not name: + name = self.name + _release_lock(name) + + @classmethod + def release_all_locks(self): + """ + Releases all locks. Used for clean shutdown. + """ + for lockdir in glob.glob("%s-%s" % (self.LOCKBASE, '*')): + _release_lock(lockdir) + + @property + def locked_by_us(self): + """ + Returns True if the pid in the pidfile + is ours. + + :rtype: bool + """ + _, pid = self._is_one_pidfile() + return pid == self.pid + + def update_ts(self): + """ + Updates the timestamp of the lock. + """ + if self.locked_by_us: + update_modification_ts(self.name) + + def write_port(self, port): + """ + Writes the port for windows control to the pidfile folder + Returns True if successful. + + :rtype: bool + """ + if not self.locked_by_us: + logger.warning("Tried to write control port to a " + "non-unique pidfile folder") + return False + port_file = os.path.join(self.name, "port") + with open(port_file, 'w') as f: + f.write("%s" % port) + return True + + def get_control_port(self): + """ + Reads control port of the main instance from the port file + in the pidfile dir + + :rtype: int + """ + pid = self.get_pid() + port_file = os.path.join(self.LOCKBASE + "-%s" % pid, "port") + port = None + try: + with open(port_file) as f: + port_str = f.read() + port = int(port_str.strip()) + except IOError as exc: + if exc.errno == errno.ENOENT: + logger.error("Tried to read port from non-existent file") + else: + # we did not know explicitely about this error + raise + return port + + def raise_window_ack(): + """ + This function is called from the windows callback that is registered + with the raise_window event. It just updates the modification time + of the lock file so we can signal an ack to the instance that tried + to raise the window. + """ + lock = WindowsLock() + lock.update_ts() + + +def we_are_the_one_and_only(): + """ + Returns True if we are the only instance running, False otherwise. + If we came later, send a raise signal to the main instance of the + application. + + Under windows we are not using flock magic, so we wait during + RAISE_WINDOW_TIMEOUT time, if not ack is + received, we assume it was a stalled lock, so we remove it and continue + with initialization. + + :rtype: bool + """ + _sys = platform.system() + + if _sys in ("Linux", "Darwin"): + locker = UnixLock('/tmp/leap-client.lock') + locker.get_lock() + we_are_the_one = locker.locked_by_us + if not we_are_the_one: + signal_event(proto.RAISE_WINDOW) + return we_are_the_one + + elif _sys == "Windows": + locker = WindowsLock() + locker.get_lock() + we_are_the_one = locker.locked_by_us + + if not we_are_the_one: + locker.release_lock() + lock_path = locker.get_locking_path() + ts = get_modification_ts(lock_path) + + nowfun = datetime.datetime.now + t0 = nowfun() + pause = RAISE_WINDOW_TIMEOUT / float(RAISE_WINDOW_WAIT_STEPS) + timeout_delta = datetime.timedelta(0, RAISE_WINDOW_TIMEOUT) + check_interval = lambda: nowfun() - t0 < timeout_delta + + # let's assume it's a stalled lock + we_are_the_one = True + signal_event(proto.RAISE_WINDOW) + + while check_interval(): + if get_modification_ts(lock_path) > ts: + # yay! someone claimed their control over the lock. + # so the lock is alive + logger.debug('Raise window ACK-ed') + we_are_the_one = False + break + else: + time.sleep(pause) + + if we_are_the_one: + # ok, it really was a stalled lock. let's remove all + # that is left, and put only ours there. + WindowsLock.release_all_locks() + WindowsLock().get_lock() + + return we_are_the_one + + else: + logger.warning("Multi-instance checker " + "not implemented for %s" % (_sys)) + # lies, lies, lies... + return True diff --git a/src/leap/bitmask/provider/__init__.py b/src/leap/bitmask/provider/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/provider/supportedapis.py b/src/leap/bitmask/provider/supportedapis.py new file mode 100644 index 00000000..3e650ba2 --- /dev/null +++ b/src/leap/bitmask/provider/supportedapis.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# supportedapis.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 . + +""" +API Support check. +""" + + +class SupportedAPIs(object): + """ + Class responsible of checking for API compatibility. + """ + SUPPORTED_APIS = ["1"] + + @classmethod + def supports(self, api_version): + """ + :param api_version: the version number of the api that we need to check + :type api_version: str + + :returns: if that version is supported or not. + :return type: bool + """ + return api_version in self.SUPPORTED_APIS diff --git a/src/leap/bitmask/services/__init__.py b/src/leap/bitmask/services/__init__.py new file mode 100644 index 00000000..253359cd --- /dev/null +++ b/src/leap/bitmask/services/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# __init__.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 . +""" +Services module. +""" +DEPLOYED = ["openvpn", "mx"] + + +def get_supported(services): + """ + Returns a list of the available services. + + :param services: a list containing the services to be filtered. + :type services: list of str + + :returns: a list of the available services + :rtype: list of str + """ + return filter(lambda s: s in DEPLOYED, services) diff --git a/src/leap/bitmask/services/abstractbootstrapper.py b/src/leap/bitmask/services/abstractbootstrapper.py new file mode 100644 index 00000000..9e50948c --- /dev/null +++ b/src/leap/bitmask/services/abstractbootstrapper.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# abstractbootstrapper.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 . + +""" +Abstract bootstrapper implementation +""" +import logging + +import requests + +from functools import partial + +from PySide import QtCore +from twisted.internet import threads +from leap.common.check import leap_assert, leap_assert_type + +logger = logging.getLogger(__name__) + + +class AbstractBootstrapper(QtCore.QObject): + """ + Abstract Bootstrapper that implements the needed deferred callbacks + """ + + PASSED_KEY = "passed" + ERROR_KEY = "error" + + def __init__(self, bypass_checks=False): + """ + Constructor for the abstract bootstrapper + + :param bypass_checks: Set to true if the app should bypass + first round of checks for CA + certificates at bootstrap + :type bypass_checks: bool + """ + QtCore.QObject.__init__(self) + + leap_assert(self._gui_errback.im_func == + AbstractBootstrapper._gui_errback.im_func, + "Cannot redefine _gui_errback") + leap_assert(self._errback.im_func == + AbstractBootstrapper._errback.im_func, + "Cannot redefine _errback") + leap_assert(self._gui_notify.im_func == + AbstractBootstrapper._gui_notify.im_func, + "Cannot redefine _gui_notify") + + # **************************************************** # + # Dependency injection helpers, override this for more + # granular testing + self._fetcher = requests + # **************************************************** # + + self._session = self._fetcher.session() + self._bypass_checks = bypass_checks + self._signal_to_emit = None + self._err_msg = None + + def _gui_errback(self, failure): + """ + Errback used to notify the GUI of a problem, it should be used + as the last errback of the whole chain. + + Traps all exceptions if a signal is defined, otherwise it just + lets it continue. + + NOTE: This method is final, it should not be redefined. + + :param failure: failure object that Twisted generates + :type failure: twisted.python.failure.Failure + """ + if self._signal_to_emit: + err_msg = self._err_msg \ + if self._err_msg is not None \ + else str(failure.value) + self._signal_to_emit.emit({ + self.PASSED_KEY: False, + self.ERROR_KEY: err_msg + }) + failure.trap(Exception) + + def _errback(self, failure, signal=None): + """ + Regular errback used for the middle of the chain. If it's + executed, the first one will set the signal to emit as + failure. + + NOTE: This method is final, it should not be redefined. + + :param failure: failure object that Twisted generates + :type failure: twisted.python.failure.Failure + :param signal: Signal to emit if it fails here first + :type signal: QtCore.SignalInstance + + :returns: failure object that Twisted generates + :rtype: twisted.python.failure.Failure + """ + if self._signal_to_emit is None: + self._signal_to_emit = signal + return failure + + def _gui_notify(self, _, signal=None): + """ + Callback used to notify the GUI of a success. Will emit signal + if specified + + NOTE: This method is final, it should not be redefined. + + :param _: IGNORED. Returned from the previous callback + :type _: IGNORED + :param signal: Signal to emit if it fails here first + :type signal: QtCore.SignalInstance + """ + if signal: + logger.debug("Emitting %s" % (signal,)) + signal.emit({self.PASSED_KEY: True, self.ERROR_KEY: ""}) + + def _callback_threader(self, cb, res, *args, **kwargs): + return threads.deferToThread(cb, res, *args, **kwargs) + + def addCallbackChain(self, callbacks): + """ + Creates a callback/errback chain on another thread using + deferToThread and adds the _gui_errback to the end to notify + the GUI on an error. + + :param callbacks: List of tuples of callbacks and the signal + associated to that callback + :type callbacks: list(tuple(func, func)) + + :returns: the defer with the callback chain + :rtype: deferred + """ + leap_assert_type(callbacks, list) + + self._signal_to_emit = None + self._err_msg = None + + d = None + for cb, sig in callbacks: + if d is None: + d = threads.deferToThread(cb) + else: + d.addCallback(partial(self._callback_threader, cb)) + d.addErrback(self._errback, signal=sig) + d.addCallback(self._gui_notify, signal=sig) + d.addErrback(self._gui_errback) + return d diff --git a/src/leap/bitmask/services/eip/__init__.py b/src/leap/bitmask/services/eip/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/services/eip/eipbootstrapper.py b/src/leap/bitmask/services/eip/eipbootstrapper.py new file mode 100644 index 00000000..1d7bc342 --- /dev/null +++ b/src/leap/bitmask/services/eip/eipbootstrapper.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +# eipbootstrapper.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 . + +""" +EIP bootstrapping +""" + +import logging +import os + +from PySide import QtCore + +from leap.common.check import leap_assert, leap_assert_type +from leap.common import certs +from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p +from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpauth import SRPAuth +from leap.services.eip.eipconfig import EIPConfig +from leap.util.request_helpers import get_content +from leap.util.constants import REQUEST_TIMEOUT +from leap.services.abstractbootstrapper import AbstractBootstrapper + +logger = logging.getLogger(__name__) + + +class EIPBootstrapper(AbstractBootstrapper): + """ + Sets up EIP for a provider a series of checks and emits signals + after they are passed. + If a check fails, the subsequent checks are not executed + """ + + # All dicts returned are of the form + # {"passed": bool, "error": str} + download_config = QtCore.Signal(dict) + download_client_certificate = QtCore.Signal(dict) + + def __init__(self): + AbstractBootstrapper.__init__(self) + + self._provider_config = None + self._eip_config = None + self._download_if_needed = False + + def _download_config(self, *args): + """ + Downloads the EIP config for the given provider + """ + + leap_assert(self._provider_config, + "We need a provider configuration!") + + logger.debug("Downloading EIP config for %s" % + (self._provider_config.get_domain(),)) + + api_version = self._provider_config.get_api_version() + self._eip_config = EIPConfig() + self._eip_config.set_api_version(api_version) + + headers = {} + mtime = get_mtime(os.path.join(self._eip_config + .get_path_prefix(), + "leap", + "providers", + self._provider_config.get_domain(), + "eip-service.json")) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + # there is some confusion with this uri, + # it's in 1/config/eip, config/eip and config/1/eip... + config_uri = "%s/%s/config/eip-service.json" % ( + self._provider_config.get_api_uri(), + api_version) + logger.debug('Downloading eip config from: %s' % config_uri) + + res = self._session.get(config_uri, + verify=self._provider_config + .get_ca_cert_path(), + headers=headers, + timeout=REQUEST_TIMEOUT) + res.raise_for_status() + + # Not modified + if res.status_code == 304: + logger.debug("EIP definition has not been modified") + else: + eip_definition, mtime = get_content(res) + + self._eip_config.load(data=eip_definition, mtime=mtime) + self._eip_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "eip-service.json"]) + + def _download_client_certificates(self, *args): + """ + Downloads the EIP client certificate for the given provider + """ + leap_assert(self._provider_config, "We need a provider configuration!") + leap_assert(self._eip_config, "We need an eip configuration!") + + logger.debug("Downloading EIP client certificate for %s" % + (self._provider_config.get_domain(),)) + + client_cert_path = self._eip_config.\ + get_client_cert_path(self._provider_config, + about_to_download=True) + + # For re-download if something is wrong with the cert + self._download_if_needed = self._download_if_needed and \ + not certs.should_redownload(client_cert_path) + + if self._download_if_needed and \ + os.path.exists(client_cert_path): + check_and_fix_urw_only(client_cert_path) + return + + srp_auth = SRPAuth(self._provider_config) + session_id = srp_auth.get_session_id() + cookies = None + if session_id: + cookies = {"_session_id": session_id} + cert_uri = "%s/%s/cert" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('getting cert from uri: %s' % cert_uri) + res = self._session.get(cert_uri, + verify=self._provider_config + .get_ca_cert_path(), + cookies=cookies, + timeout=REQUEST_TIMEOUT) + res.raise_for_status() + client_cert = res.content + + if not certs.is_valid_pemfile(client_cert): + raise Exception(self.tr("The downloaded certificate is not a " + "valid PEM file")) + + mkdir_p(os.path.dirname(client_cert_path)) + + with open(client_cert_path, "w") as f: + f.write(client_cert) + + check_and_fix_urw_only(client_cert_path) + + def run_eip_setup_checks(self, + provider_config, + download_if_needed=False): + """ + Starts the checks needed for a new eip setup + + :param provider_config: Provider configuration + :type provider_config: ProviderConfig + """ + leap_assert(provider_config, "We need a provider config!") + leap_assert_type(provider_config, ProviderConfig) + + self._provider_config = provider_config + self._download_if_needed = download_if_needed + + cb_chain = [ + (self._download_config, self.download_config), + (self._download_client_certificates, + self.download_client_certificate) + ] + + return self.addCallbackChain(cb_chain) diff --git a/src/leap/bitmask/services/eip/eipconfig.py b/src/leap/bitmask/services/eip/eipconfig.py new file mode 100644 index 00000000..d69e1fd8 --- /dev/null +++ b/src/leap/bitmask/services/eip/eipconfig.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- +# eipconfig.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 . + +""" +Provider configuration +""" +import logging +import os +import re +import time + +import ipaddr + +from leap.common.check import leap_assert, leap_assert_type +from leap.common.config.baseconfig import BaseConfig +from leap.config.providerconfig import ProviderConfig +from leap.services.eip.eipspec import get_schema + +logger = logging.getLogger(__name__) + + +class VPNGatewaySelector(object): + """ + VPN Gateway selector. + """ + # http://www.timeanddate.com/time/map/ + equivalent_timezones = {13: -11, 14: -10} + + def __init__(self, eipconfig, tz_offset=None): + ''' + Constructor for VPNGatewaySelector. + + :param eipconfig: a valid EIP Configuration. + :type eipconfig: EIPConfig + :param tz_offset: use this offset as a local distance to GMT. + :type tz_offset: int + ''' + leap_assert_type(eipconfig, EIPConfig) + + self._local_offset = tz_offset + if tz_offset is None: + tz_offset = self._get_local_offset() + + if tz_offset in self.equivalent_timezones: + tz_offset = self.equivalent_timezones[tz_offset] + + self._local_offset = tz_offset + + self._eipconfig = eipconfig + + def get_gateways(self): + """ + Returns the 4 best gateways, sorted by timezone proximity. + + :rtype: list of IPv4Address or IPv6Address object. + """ + gateways_timezones = [] + locations = self._eipconfig.get_locations() + gateways = self._eipconfig.get_gateways() + + for idx, gateway in enumerate(gateways): + gateway_location = gateway.get('location') + gateway_distance = 99 # if hasn't location -> should go last + + if gateway_location is not None: + gw_offset = int(locations[gateway['location']]['timezone']) + if gw_offset in self.equivalent_timezones: + gw_offset = self.equivalent_timezones[gw_offset] + + gateway_distance = self._get_timezone_distance(gw_offset) + + ip = self._eipconfig.get_gateway_ip(idx) + gateways_timezones.append((ip, gateway_distance)) + + gateways_timezones = sorted(gateways_timezones, + key=lambda gw: gw[1])[:4] + + gateways = [ip for ip, dist in gateways_timezones] + return gateways + + def _get_timezone_distance(self, offset): + ''' + Returns the distance between the local timezone and + the one with offset 'offset'. + + :param offset: the distance of a timezone to GMT. + :type offset: int + :returns: distance between local offset and param offset. + :rtype: int + ''' + timezones = range(-11, 13) + tz1 = offset + tz2 = self._local_offset + distance = abs(timezones.index(tz1) - timezones.index(tz2)) + if distance > 12: + if tz1 < 0: + distance = timezones.index(tz1) + timezones[::-1].index(tz2) + else: + distance = timezones[::-1].index(tz1) + timezones.index(tz2) + + return distance + + def _get_local_offset(self): + ''' + Returns the distance between GMT and the local timezone. + + :rtype: int + ''' + local_offset = time.timezone + if time.daylight: + local_offset = time.altzone + + return local_offset / 3600 + + +class EIPConfig(BaseConfig): + """ + Provider configuration abstraction class + """ + OPENVPN_ALLOWED_KEYS = ("auth", "cipher", "tls-cipher") + OPENVPN_CIPHERS_REGEX = re.compile("[A-Z0-9\-]+") + + def __init__(self): + BaseConfig.__init__(self) + self._api_version = None + + def _get_schema(self): + """ + Returns the schema corresponding to the version given. + + :rtype: dict or None if the version is not supported. + """ + return get_schema(self._api_version) + + def get_clusters(self): + # TODO: create an abstraction for clusters + return self._safe_get_value("clusters") + + def get_gateways(self): + # TODO: create an abstraction for gateways + return self._safe_get_value("gateways") + + def get_locations(self): + ''' + Returns a list of locations + + :rtype: dict + ''' + return self._safe_get_value("locations") + + def get_openvpn_configuration(self): + """ + Returns a dictionary containing the openvpn configuration + parameters. + + These are sanitized with alphanumeric whitelist. + + :returns: openvpn configuration dict + :rtype: C{dict} + """ + ovpncfg = self._safe_get_value("openvpn_configuration") + config = {} + for key, value in ovpncfg.items(): + if key in self.OPENVPN_ALLOWED_KEYS and value is not None: + sanitized_val = self.OPENVPN_CIPHERS_REGEX.findall(value) + if len(sanitized_val) != 0: + _val = sanitized_val[0] + config[str(key)] = str(_val) + return config + + def get_serial(self): + return self._safe_get_value("serial") + + def get_version(self): + return self._safe_get_value("version") + + def get_gateway_ip(self, index=0): + """ + Returns the ip of the gateway. + + :rtype: An IPv4Address or IPv6Address object. + """ + gateways = self.get_gateways() + leap_assert(len(gateways) > 0, "We don't have any gateway!") + if index > len(gateways): + index = 0 + logger.warning("Provided an unknown gateway index %s, " + + "defaulting to 0") + ip_addr_str = gateways[index]["ip_address"] + + try: + ipaddr.IPAddress(ip_addr_str) + return ip_addr_str + except ValueError: + logger.error("Invalid ip address in config: %s" % (ip_addr_str,)) + return None + + def get_client_cert_path(self, + providerconfig=None, + about_to_download=False): + """ + Returns the path to the certificate used by openvpn + """ + + leap_assert(providerconfig, "We need a provider") + leap_assert_type(providerconfig, ProviderConfig) + + cert_path = os.path.join(self.get_path_prefix(), + "leap", + "providers", + providerconfig.get_domain(), + "keys", + "client", + "openvpn.pem") + + if not about_to_download: + leap_assert(os.path.exists(cert_path), + "You need to download the certificate first") + logger.debug("Using OpenVPN cert %s" % (cert_path,)) + + return cert_path + + +if __name__ == "__main__": + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + eipconfig = EIPConfig('1') + + try: + eipconfig.get_clusters() + except Exception as e: + assert isinstance(e, AssertionError), "Expected an assert" + print "Safe value getting is working" + + if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): + print eipconfig.get_clusters() + print eipconfig.get_gateways() + print eipconfig.get_locations() + print eipconfig.get_openvpn_configuration() + print eipconfig.get_serial() + print eipconfig.get_version() diff --git a/src/leap/bitmask/services/eip/eipspec.py b/src/leap/bitmask/services/eip/eipspec.py new file mode 100644 index 00000000..9cc56be3 --- /dev/null +++ b/src/leap/bitmask/services/eip/eipspec.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# eipspec.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 . + + +# Schemas dict +# To add a schema for a version you should follow the form: +# { '1': schema_v1, '2': schema_v2, ... etc } +# so for instance, to add the '2' version, you should do: +# eipservice_config_spec['2'] = schema_v2 +eipservice_config_spec = {} + +eipservice_config_spec['1'] = { + 'description': 'sample eip service config', + 'type': 'object', + 'properties': { + 'serial': { + 'type': int, + 'default': 1, + 'required': ["True"] + }, + 'version': { + 'type': int, + 'default': 1, + 'required': ["True"] + }, + 'clusters': { + 'type': list, + 'default': [ + {"label": { + "en": "Location Unknown"}, + "name": "location_unknown"}] + }, + 'gateways': { + 'type': list, + 'default': [ + {"capabilities": { + "adblock": True, + "filter_dns": True, + "ports": ["80", "53", "443", "1194"], + "protocols": ["udp", "tcp"], + "transport": ["openvpn"], + "user_ips": False}, + "cluster": "location_unknown", + "host": "location.example.org", + "ip_address": "127.0.0.1"}] + }, + 'locations': { + 'type': dict, + 'default': {} + }, + 'openvpn_configuration': { + 'type': dict, + 'default': { + "auth": None, + "cipher": None, + "tls-cipher": None} + } + } +} + + +def get_schema(version): + """ + Returns the schema corresponding to the version given. + + :param version: the version of the schema to get. + :type version: str + :rtype: dict or None if the version is not supported. + """ + schema = eipservice_config_spec.get(version, None) + return schema diff --git a/src/leap/bitmask/services/eip/providerbootstrapper.py b/src/leap/bitmask/services/eip/providerbootstrapper.py new file mode 100644 index 00000000..bf5938dc --- /dev/null +++ b/src/leap/bitmask/services/eip/providerbootstrapper.py @@ -0,0 +1,340 @@ +# -*- coding: utf-8 -*- +# providerbootstrapper.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 . + +""" +Provider bootstrapping +""" +import logging +import socket +import os + +import requests + +from PySide import QtCore + +from leap.common.certs import get_digest +from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p +from leap.common.check import leap_assert, leap_assert_type, leap_check +from leap.config.providerconfig import ProviderConfig, MissingCACert +from leap.util.request_helpers import get_content +from leap.util.constants import REQUEST_TIMEOUT +from leap.services.abstractbootstrapper import AbstractBootstrapper +from leap.provider.supportedapis import SupportedAPIs + + +logger = logging.getLogger(__name__) + + +class UnsupportedProviderAPI(Exception): + """ + Raised when attempting to use a provider with an incompatible API. + """ + pass + + +class WrongFingerprint(Exception): + """ + Raised when a fingerprint comparison does not match. + """ + pass + + +class ProviderBootstrapper(AbstractBootstrapper): + """ + Given a provider URL performs a series of checks and emits signals + after they are passed. + If a check fails, the subsequent checks are not executed + """ + + # All dicts returned are of the form + # {"passed": bool, "error": str} + name_resolution = QtCore.Signal(dict) + https_connection = QtCore.Signal(dict) + download_provider_info = QtCore.Signal(dict) + + download_ca_cert = QtCore.Signal(dict) + check_ca_fingerprint = QtCore.Signal(dict) + check_api_certificate = QtCore.Signal(dict) + + def __init__(self, bypass_checks=False): + """ + Constructor for provider bootstrapper object + + :param bypass_checks: Set to true if the app should bypass + first round of checks for CA certificates at bootstrap + :type bypass_checks: bool + """ + AbstractBootstrapper.__init__(self, bypass_checks) + + self._domain = None + self._provider_config = None + self._download_if_needed = False + + def _check_name_resolution(self): + """ + Checks that the name resolution for the provider name works + """ + leap_assert(self._domain, "Cannot check DNS without a domain") + + logger.debug("Checking name resolution for %s" % (self._domain)) + + # We don't skip this check, since it's basic for the whole + # system to work + socket.gethostbyname(self._domain) + + def _check_https(self, *args): + """ + Checks that https is working and that the provided certificate + checks out + """ + + leap_assert(self._domain, "Cannot check HTTPS without a domain") + + logger.debug("Checking https for %s" % (self._domain)) + + # We don't skip this check, since it's basic for the whole + # system to work + + try: + res = self._session.get("https://%s" % (self._domain,), + verify=not self._bypass_checks, + timeout=REQUEST_TIMEOUT) + res.raise_for_status() + except requests.exceptions.SSLError: + self._err_msg = self.tr("Provider certificate could " + "not be verified") + raise + except Exception: + self._err_msg = self.tr("Provider does not support HTTPS") + raise + + def _download_provider_info(self, *args): + """ + Downloads the provider.json defition + """ + leap_assert(self._domain, + "Cannot download provider info without a domain") + + logger.debug("Downloading provider info for %s" % (self._domain)) + + headers = {} + + provider_json = os.path.join( + ProviderConfig().get_path_prefix(), "leap", "providers", + self._domain, "provider.json") + mtime = get_mtime(provider_json) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + uri = "https://%s/%s" % (self._domain, "provider.json") + verify = not self._bypass_checks + + if mtime: # the provider.json exists + provider_config = ProviderConfig() + provider_config.load(provider_json) + try: + verify = provider_config.get_ca_cert_path() + uri = provider_config.get_api_uri() + '/provider.json' + except MissingCACert: + # get_ca_cert_path fails if the certificate does not exists. + pass + + logger.debug("Requesting for provider.json... " + "uri: {0}, verify: {1}, headers: {2}".format( + uri, verify, headers)) + res = self._session.get(uri, verify=verify, + headers=headers, timeout=REQUEST_TIMEOUT) + res.raise_for_status() + logger.debug("Request status code: {0}".format(res.status_code)) + + # Not modified + if res.status_code == 304: + logger.debug("Provider definition has not been modified") + else: + provider_definition, mtime = get_content(res) + + provider_config = ProviderConfig() + provider_config.load(data=provider_definition, mtime=mtime) + provider_config.save(["leap", + "providers", + self._domain, + "provider.json"]) + + api_version = provider_config.get_api_version() + if SupportedAPIs.supports(api_version): + logger.debug("Provider definition has been modified") + else: + api_supported = ', '.join(SupportedAPIs.SUPPORTED_APIS) + error = ('Unsupported provider API version. ' + 'Supported versions are: {}. ' + 'Found: {}.').format(api_supported, api_version) + + logger.error(error) + raise UnsupportedProviderAPI(error) + + def run_provider_select_checks(self, domain, download_if_needed=False): + """ + Populates the check queue. + + :param domain: domain to check + :type domain: str + + :param download_if_needed: if True, makes the checks do not + overwrite already downloaded data + :type download_if_needed: bool + """ + leap_assert(domain and len(domain) > 0, "We need a domain!") + + self._domain = ProviderConfig.sanitize_path_component(domain) + self._download_if_needed = download_if_needed + + cb_chain = [ + (self._check_name_resolution, self.name_resolution), + (self._check_https, self.https_connection), + (self._download_provider_info, self.download_provider_info) + ] + + return self.addCallbackChain(cb_chain) + + def _should_proceed_cert(self): + """ + Returns False if the certificate already exists for the given + provider. True otherwise + + :rtype: bool + """ + leap_assert(self._provider_config, "We need a provider config!") + + if not self._download_if_needed: + return True + + return not os.path.exists(self._provider_config + .get_ca_cert_path(about_to_download=True)) + + def _download_ca_cert(self, *args): + """ + Downloads the CA cert that is going to be used for the api URL + """ + + leap_assert(self._provider_config, "Cannot download the ca cert " + "without a provider config!") + + logger.debug("Downloading ca cert for %s at %s" % + (self._domain, self._provider_config.get_ca_cert_uri())) + + if not self._should_proceed_cert(): + check_and_fix_urw_only( + self._provider_config + .get_ca_cert_path(about_to_download=True)) + return + + res = self._session.get(self._provider_config.get_ca_cert_uri(), + verify=not self._bypass_checks, + timeout=REQUEST_TIMEOUT) + res.raise_for_status() + + cert_path = self._provider_config.get_ca_cert_path( + about_to_download=True) + cert_dir = os.path.dirname(cert_path) + mkdir_p(cert_dir) + with open(cert_path, "w") as f: + f.write(res.content) + + check_and_fix_urw_only(cert_path) + + def _check_ca_fingerprint(self, *args): + """ + Checks the CA cert fingerprint against the one provided in the + json definition + """ + leap_assert(self._provider_config, "Cannot check the ca cert " + "without a provider config!") + + logger.debug("Checking ca fingerprint for %s and cert %s" % + (self._domain, + self._provider_config.get_ca_cert_path())) + + if not self._should_proceed_cert(): + return + + parts = self._provider_config.get_ca_cert_fingerprint().split(":") + + error_msg = "Wrong fingerprint format" + leap_check(len(parts) == 2, error_msg, WrongFingerprint) + + method = parts[0].strip() + fingerprint = parts[1].strip() + cert_data = None + with open(self._provider_config.get_ca_cert_path()) as f: + cert_data = f.read() + + leap_assert(len(cert_data) > 0, "Could not read certificate data") + digest = get_digest(cert_data, method) + + error_msg = "Downloaded certificate has a different fingerprint!" + leap_check(digest == fingerprint, error_msg, WrongFingerprint) + + def _check_api_certificate(self, *args): + """ + Tries to make an API call with the downloaded cert and checks + if it validates against it + """ + leap_assert(self._provider_config, "Cannot check the ca cert " + "without a provider config!") + + logger.debug("Checking api certificate for %s and cert %s" % + (self._provider_config.get_api_uri(), + self._provider_config.get_ca_cert_path())) + + if not self._should_proceed_cert(): + return + + test_uri = "%s/%s/cert" % (self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + res = self._session.get(test_uri, + verify=self._provider_config + .get_ca_cert_path(), + timeout=REQUEST_TIMEOUT) + res.raise_for_status() + + def run_provider_setup_checks(self, + provider_config, + download_if_needed=False): + """ + Starts the checks needed for a new provider setup. + + :param provider_config: Provider configuration + :type provider_config: ProviderConfig + + :param download_if_needed: if True, makes the checks do not + overwrite already downloaded data. + :type download_if_needed: bool + """ + leap_assert(provider_config, "We need a provider config!") + leap_assert_type(provider_config, ProviderConfig) + + self._provider_config = provider_config + self._download_if_needed = download_if_needed + + cb_chain = [ + (self._download_ca_cert, self.download_ca_cert), + (self._check_ca_fingerprint, self.check_ca_fingerprint), + (self._check_api_certificate, self.check_api_certificate) + ] + + return self.addCallbackChain(cb_chain) diff --git a/src/leap/bitmask/services/eip/tests/__init__.py b/src/leap/bitmask/services/eip/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py b/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py new file mode 100644 index 00000000..f2331eca --- /dev/null +++ b/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py @@ -0,0 +1,347 @@ +# -*- coding: utf-8 -*- +# test_eipbootstrapper.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 . + + +""" +Tests for the EIP Boostrapper checks + +These will be whitebox tests since we want to make sure the private +implementation is checking what we expect. +""" + +import os +import mock +import tempfile +import time +try: + import unittest2 as unittest +except ImportError: + import unittest + +from nose.twistedtools import deferred, reactor +from twisted.internet import threads +from requests.models import Response + +from leap.common.testing.basetest import BaseLeapTest +from leap.services.eip.eipbootstrapper import EIPBootstrapper +from leap.services.eip.eipconfig import EIPConfig +from leap.config.providerconfig import ProviderConfig +from leap.crypto.tests import fake_provider +from leap.common.files import mkdir_p +from leap.crypto.srpauth import SRPAuth + + +class EIPBootstrapperActiveTest(BaseLeapTest): + @classmethod + def setUpClass(cls): + BaseLeapTest.setUpClass() + factory = fake_provider.get_provider_factory() + http = reactor.listenTCP(0, factory) + https = reactor.listenSSL( + 0, factory, + fake_provider.OpenSSLServerContextFactory()) + get_port = lambda p: p.getHost().port + cls.http_port = get_port(http) + cls.https_port = get_port(https) + + def setUp(self): + self.eb = EIPBootstrapper() + self.old_pp = EIPConfig.get_path_prefix + self.old_save = EIPConfig.save + self.old_load = EIPConfig.load + self.old_si = SRPAuth.get_session_id + + def tearDown(self): + EIPConfig.get_path_prefix = self.old_pp + EIPConfig.save = self.old_save + EIPConfig.load = self.old_load + SRPAuth.get_session_id = self.old_si + + def _download_config_test_template(self, ifneeded, new): + """ + All download config tests have the same structure, so this is + a parametrized test for that. + + :param ifneeded: sets _download_if_needed + :type ifneeded: bool + :param new: if True uses time.time() as mtime for the mocked + eip-service file, otherwise it uses 100 (a really + old mtime) + :type new: float or int (will be coersed) + """ + pc = ProviderConfig() + pc.get_domain = mock.MagicMock( + return_value="localhost:%s" % (self.https_port)) + self.eb._provider_config = pc + + pc.get_api_uri = mock.MagicMock( + return_value="https://%s" % (pc.get_domain())) + pc.get_api_version = mock.MagicMock(return_value="1") + + # This is to ignore https checking, since it's not the point + # of this test + pc.get_ca_cert_path = mock.MagicMock(return_value=False) + + path_prefix = tempfile.mkdtemp() + EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix) + EIPConfig.save = mock.MagicMock() + EIPConfig.load = mock.MagicMock() + + self.eb._download_if_needed = ifneeded + + provider_dir = os.path.join(EIPConfig.get_path_prefix(), + "leap", + "providers", + pc.get_domain()) + mkdir_p(provider_dir) + eip_config_path = os.path.join(provider_dir, + "eip-service.json") + + with open(eip_config_path, "w") as ec: + ec.write("A") + + # set mtime to something really new + if new: + os.utime(eip_config_path, (-1, time.time())) + else: + os.utime(eip_config_path, (-1, 100)) + + @deferred() + def test_download_config_not_modified(self): + self._download_config_test_template(True, True) + + d = threads.deferToThread(self.eb._download_config) + + def check(*args): + self.assertFalse(self.eb._eip_config.save.called) + d.addCallback(check) + return d + + @deferred() + def test_download_config_modified(self): + self._download_config_test_template(True, False) + + d = threads.deferToThread(self.eb._download_config) + + def check(*args): + self.assertTrue(self.eb._eip_config.save.called) + d.addCallback(check) + return d + + @deferred() + def test_download_config_ignores_mtime(self): + self._download_config_test_template(False, True) + + d = threads.deferToThread(self.eb._download_config) + + def check(*args): + self.eb._eip_config.save.assert_called_once_with( + ["leap", + "providers", + self.eb._provider_config.get_domain(), + "eip-service.json"]) + d.addCallback(check) + return d + + def _download_certificate_test_template(self, ifneeded, createcert): + """ + All download client certificate tests have the same structure, + so this is a parametrized test for that. + + :param ifneeded: sets _download_if_needed + :type ifneeded: bool + :param createcert: if True it creates a dummy file to play the + part of a downloaded certificate + :type createcert: bool + + :returns: the temp eip cert path and the dummy cert contents + :rtype: tuple of str, str + """ + pc = ProviderConfig() + ec = EIPConfig() + self.eb._provider_config = pc + self.eb._eip_config = ec + + pc.get_domain = mock.MagicMock( + return_value="localhost:%s" % (self.https_port)) + pc.get_api_uri = mock.MagicMock( + return_value="https://%s" % (pc.get_domain())) + pc.get_api_version = mock.MagicMock(return_value="1") + pc.get_ca_cert_path = mock.MagicMock(return_value=False) + + path_prefix = tempfile.mkdtemp() + EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix) + EIPConfig.save = mock.MagicMock() + EIPConfig.load = mock.MagicMock() + + self.eb._download_if_needed = ifneeded + + provider_dir = os.path.join(EIPConfig.get_path_prefix(), + "leap", + "providers", + "somedomain") + mkdir_p(provider_dir) + eip_cert_path = os.path.join(provider_dir, + "cert") + + ec.get_client_cert_path = mock.MagicMock( + return_value=eip_cert_path) + + cert_content = "A" + if createcert: + with open(eip_cert_path, "w") as ec: + ec.write(cert_content) + + return eip_cert_path, cert_content + + def test_download_client_certificate_not_modified(self): + cert_path, old_cert_content = self._download_certificate_test_template( + True, True) + + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=False): + self.eb._download_client_certificates() + with open(cert_path, "r") as c: + self.assertEqual(c.read(), old_cert_content) + + @deferred() + def test_download_client_certificate_old_cert(self): + cert_path, old_cert_content = self._download_certificate_test_template( + True, True) + + def wrapper(*args): + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=True): + with mock.patch('leap.common.certs.is_valid_pemfile', + new_callable=mock.MagicMock, + return_value=True): + self.eb._download_client_certificates() + + def check(*args): + with open(cert_path, "r") as c: + self.assertNotEqual(c.read(), old_cert_content) + d = threads.deferToThread(wrapper) + d.addCallback(check) + + return d + + @deferred() + def test_download_client_certificate_no_cert(self): + cert_path, _ = self._download_certificate_test_template( + True, False) + + def wrapper(*args): + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=False): + with mock.patch('leap.common.certs.is_valid_pemfile', + new_callable=mock.MagicMock, + return_value=True): + self.eb._download_client_certificates() + + def check(*args): + self.assertTrue(os.path.exists(cert_path)) + d = threads.deferToThread(wrapper) + d.addCallback(check) + + return d + + @deferred() + def test_download_client_certificate_force_not_valid(self): + cert_path, old_cert_content = self._download_certificate_test_template( + True, True) + + def wrapper(*args): + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=True): + with mock.patch('leap.common.certs.is_valid_pemfile', + new_callable=mock.MagicMock, + return_value=True): + self.eb._download_client_certificates() + + def check(*args): + with open(cert_path, "r") as c: + self.assertNotEqual(c.read(), old_cert_content) + d = threads.deferToThread(wrapper) + d.addCallback(check) + + return d + + @deferred() + def test_download_client_certificate_invalid_download(self): + cert_path, _ = self._download_certificate_test_template( + False, False) + + def wrapper(*args): + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=True): + with mock.patch('leap.common.certs.is_valid_pemfile', + new_callable=mock.MagicMock, + return_value=False): + with self.assertRaises(Exception): + self.eb._download_client_certificates() + d = threads.deferToThread(wrapper) + + return d + + @deferred() + def test_download_client_certificate_uses_session_id(self): + _, _ = self._download_certificate_test_template( + False, False) + + SRPAuth.get_session_id = mock.MagicMock(return_value="1") + + def check_cookie(*args, **kwargs): + cookies = kwargs.get("cookies", None) + self.assertEqual(cookies, {'_session_id': '1'}) + return Response() + + def wrapper(*args): + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=False): + with mock.patch('leap.common.certs.is_valid_pemfile', + new_callable=mock.MagicMock, + return_value=True): + with mock.patch('requests.sessions.Session.get', + new_callable=mock.MagicMock, + side_effect=check_cookie): + with mock.patch('requests.models.Response.content', + new_callable=mock.PropertyMock, + return_value="A"): + self.eb._download_client_certificates() + + d = threads.deferToThread(wrapper) + + return d + + @deferred() + def test_run_eip_setup_checks(self): + self.eb._download_config = mock.MagicMock() + self.eb._download_client_certificates = mock.MagicMock() + + d = self.eb.run_eip_setup_checks(ProviderConfig()) + + def check(*args): + self.eb._download_config.assert_called_once_with() + self.eb._download_client_certificates.assert_called_once_with(None) + d.addCallback(check) + return d diff --git a/src/leap/bitmask/services/eip/tests/test_eipconfig.py b/src/leap/bitmask/services/eip/tests/test_eipconfig.py new file mode 100644 index 00000000..87ce04c2 --- /dev/null +++ b/src/leap/bitmask/services/eip/tests/test_eipconfig.py @@ -0,0 +1,324 @@ +# -*- coding: utf-8 -*- +# test_eipconfig.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 . +""" +Tests for eipconfig +""" +import copy +import json +import os +import unittest + +from leap.common.testing.basetest import BaseLeapTest +from leap.services.eip.eipconfig import EIPConfig +from leap.config.providerconfig import ProviderConfig + +from mock import Mock + + +sample_config = { + "gateways": [ + { + "capabilities": { + "adblock": False, + "filter_dns": True, + "limited": True, + "ports": [ + "1194", + "443", + "53", + "80"], + "protocols": [ + "tcp", + "udp"], + "transport": ["openvpn"], + "user_ips": False}, + "host": "host.dev.example.org", + "ip_address": "11.22.33.44", + "location": "cyberspace" + }, { + "capabilities": { + "adblock": False, + "filter_dns": True, + "limited": True, + "ports": [ + "1194", + "443", + "53", + "80"], + "protocols": [ + "tcp", + "udp"], + "transport": ["openvpn"], + "user_ips": False}, + "host": "host2.dev.example.org", + "ip_address": "22.33.44.55", + "location": "cyberspace" + } + ], + "locations": { + "ankara": { + "country_code": "XX", + "hemisphere": "S", + "name": "Antarctica", + "timezone": "+2" + }, + "cyberspace": { + "country_code": "XX", + "hemisphere": "X", + "name": "outer space", + "timezone": "" + } + }, + "openvpn_configuration": { + "auth": "SHA1", + "cipher": "AES-128-CBC", + "tls-cipher": "DHE-RSA-AES128-SHA" + }, + "serial": 1, + "version": 1 +} + + +class EIPConfigTest(BaseLeapTest): + + __name__ = "eip_config_tests" + + maxDiff = None + + def setUp(self): + self._old_ospath_exists = os.path.exists + + def tearDown(self): + os.path.exists = self._old_ospath_exists + + def _write_config(self, data): + """ + Helper to write some data to a temp config file. + + :param data: data to be used to save in the config file. + :data type: dict (valid json) + """ + self.configfile = os.path.join(self.tempdir, "eipconfig.json") + conf = open(self.configfile, "w") + conf.write(json.dumps(data)) + conf.close() + + def _get_eipconfig(self, fromfile=True, data=sample_config, api_ver='1'): + """ + Helper that returns an EIPConfig object using the data parameter + or a sample data. + + :param fromfile: sets if we should use a file or a string + :type fromfile: bool + :param data: sets the data to be used to load in the EIPConfig object + :type data: dict (valid json) + :param api_ver: the api_version schema to use. + :type api_ver: str + :rtype: EIPConfig + """ + config = EIPConfig() + config.set_api_version(api_ver) + + loaded = False + if fromfile: + self._write_config(data) + loaded = config.load(self.configfile, relative=False) + else: + json_string = json.dumps(data) + loaded = config.load(data=json_string) + + if not loaded: + return None + + return config + + def test_loads_from_file(self): + config = self._get_eipconfig() + self.assertIsNotNone(config) + + def test_loads_from_data(self): + config = self._get_eipconfig(fromfile=False) + self.assertIsNotNone(config) + + def test_load_valid_config_from_file(self): + config = self._get_eipconfig() + self.assertIsNotNone(config) + + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + + sample_ip = sample_config["gateways"][0]["ip_address"] + self.assertEqual( + config.get_gateway_ip(), + sample_ip) + self.assertEqual(config.get_version(), sample_config["version"]) + self.assertEqual(config.get_serial(), sample_config["serial"]) + self.assertEqual(config.get_gateways(), sample_config["gateways"]) + self.assertEqual(config.get_locations(), sample_config["locations"]) + self.assertEqual(config.get_clusters(), None) + + def test_load_valid_config_from_data(self): + config = self._get_eipconfig(fromfile=False) + self.assertIsNotNone(config) + + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + + sample_ip = sample_config["gateways"][0]["ip_address"] + self.assertEqual( + config.get_gateway_ip(), + sample_ip) + + self.assertEqual(config.get_version(), sample_config["version"]) + self.assertEqual(config.get_serial(), sample_config["serial"]) + self.assertEqual(config.get_gateways(), sample_config["gateways"]) + self.assertEqual(config.get_locations(), sample_config["locations"]) + self.assertEqual(config.get_clusters(), None) + + def test_sanitize_extra_parameters(self): + data = copy.deepcopy(sample_config) + data['openvpn_configuration']["extra_param"] = "FOO" + config = self._get_eipconfig(data=data) + + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + + def test_sanitize_non_allowed_chars(self): + data = copy.deepcopy(sample_config) + data['openvpn_configuration']["auth"] = "SHA1;" + config = self._get_eipconfig(data=data) + + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + + data = copy.deepcopy(sample_config) + data['openvpn_configuration']["auth"] = "SHA1>`&|" + config = self._get_eipconfig(data=data) + + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + + def test_sanitize_lowercase(self): + data = copy.deepcopy(sample_config) + data['openvpn_configuration']["auth"] = "shaSHA1" + config = self._get_eipconfig(data=data) + + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + + def test_all_characters_invalid(self): + data = copy.deepcopy(sample_config) + data['openvpn_configuration']["auth"] = "sha&*!@#;" + config = self._get_eipconfig(data=data) + + self.assertEqual( + config.get_openvpn_configuration(), + {'cipher': 'AES-128-CBC', + 'tls-cipher': 'DHE-RSA-AES128-SHA'}) + + def test_sanitize_bad_ip(self): + data = copy.deepcopy(sample_config) + data['gateways'][0]["ip_address"] = "11.22.33.44;" + config = self._get_eipconfig(data=data) + + self.assertEqual(config.get_gateway_ip(), None) + + data = copy.deepcopy(sample_config) + data['gateways'][0]["ip_address"] = "11.22.33.44`" + config = self._get_eipconfig(data=data) + + self.assertEqual(config.get_gateway_ip(), None) + + def test_default_gateway_on_unknown_index(self): + config = self._get_eipconfig() + sample_ip = sample_config["gateways"][0]["ip_address"] + self.assertEqual(config.get_gateway_ip(999), sample_ip) + + def test_get_gateway_by_index(self): + config = self._get_eipconfig() + sample_ip_0 = sample_config["gateways"][0]["ip_address"] + sample_ip_1 = sample_config["gateways"][1]["ip_address"] + self.assertEqual(config.get_gateway_ip(0), sample_ip_0) + self.assertEqual(config.get_gateway_ip(1), sample_ip_1) + + def test_get_client_cert_path_as_expected(self): + config = self._get_eipconfig() + config.get_path_prefix = Mock(return_value='test') + + provider_config = ProviderConfig() + + # mock 'get_domain' so we don't need to load a config + provider_domain = 'test.provider.com' + provider_config.get_domain = Mock(return_value=provider_domain) + + expected_path = os.path.join('test', 'leap', 'providers', + provider_domain, 'keys', 'client', + 'openvpn.pem') + + # mock 'os.path.exists' so we don't get an error for unexisting file + os.path.exists = Mock(return_value=True) + cert_path = config.get_client_cert_path(provider_config) + + self.assertEqual(cert_path, expected_path) + + def test_get_client_cert_path_about_to_download(self): + config = self._get_eipconfig() + config.get_path_prefix = Mock(return_value='test') + + provider_config = ProviderConfig() + + # mock 'get_domain' so we don't need to load a config + provider_domain = 'test.provider.com' + provider_config.get_domain = Mock(return_value=provider_domain) + + expected_path = os.path.join('test', 'leap', 'providers', + provider_domain, 'keys', 'client', + 'openvpn.pem') + + cert_path = config.get_client_cert_path( + provider_config, about_to_download=True) + + self.assertEqual(cert_path, expected_path) + + def test_get_client_cert_path_fails(self): + config = self._get_eipconfig() + provider_config = ProviderConfig() + + # mock 'get_domain' so we don't need to load a config + provider_domain = 'test.provider.com' + provider_config.get_domain = Mock(return_value=provider_domain) + + with self.assertRaises(AssertionError): + config.get_client_cert_path(provider_config) + + def test_fails_without_api_set(self): + config = EIPConfig() + with self.assertRaises(AssertionError): + config.load('non-relevant-path') + + def test_fails_with_api_without_schema(self): + with self.assertRaises(AssertionError): + self._get_eipconfig(api_ver='123') + +if __name__ == "__main__": + unittest.main() diff --git a/src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py b/src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py new file mode 100644 index 00000000..b24334a2 --- /dev/null +++ b/src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py @@ -0,0 +1,531 @@ +# -*- coding: utf-8 -*- +# test_providerbootstrapper.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 . + + +""" +Tests for the Provider Boostrapper checks + +These will be whitebox tests since we want to make sure the private +implementation is checking what we expect. +""" + +import os +import mock +import socket +import stat +import tempfile +import time +import requests +try: + import unittest2 as unittest +except ImportError: + import unittest + +from nose.twistedtools import deferred, reactor +from twisted.internet import threads +from requests.models import Response + +from leap.common.testing.https_server import where +from leap.common.testing.basetest import BaseLeapTest +from leap.services.eip.providerbootstrapper import ProviderBootstrapper +from leap.services.eip.providerbootstrapper import UnsupportedProviderAPI +from leap.services.eip.providerbootstrapper import WrongFingerprint +from leap.provider.supportedapis import SupportedAPIs +from leap.config.providerconfig import ProviderConfig +from leap.crypto.tests import fake_provider +from leap.common.files import mkdir_p + + +class ProviderBootstrapperTest(BaseLeapTest): + def setUp(self): + self.pb = ProviderBootstrapper() + + def tearDown(self): + pass + + def test_name_resolution_check(self): + # Something highly likely to success + self.pb._domain = "google.com" + self.pb._check_name_resolution() + # Something highly likely to fail + self.pb._domain = "uquhqweuihowquie.abc.def" + with self.assertRaises(socket.gaierror): + self.pb._check_name_resolution() + + @deferred() + def test_run_provider_select_checks(self): + self.pb._check_name_resolution = mock.MagicMock() + self.pb._check_https = mock.MagicMock() + self.pb._download_provider_info = mock.MagicMock() + + d = self.pb.run_provider_select_checks("somedomain") + + def check(*args): + self.pb._check_name_resolution.assert_called_once_with() + self.pb._check_https.assert_called_once_with(None) + self.pb._download_provider_info.assert_called_once_with(None) + d.addCallback(check) + return d + + @deferred() + def test_run_provider_setup_checks(self): + self.pb._download_ca_cert = mock.MagicMock() + self.pb._check_ca_fingerprint = mock.MagicMock() + self.pb._check_api_certificate = mock.MagicMock() + + d = self.pb.run_provider_setup_checks(ProviderConfig()) + + def check(*args): + self.pb._download_ca_cert.assert_called_once_with() + self.pb._check_ca_fingerprint.assert_called_once_with(None) + self.pb._check_api_certificate.assert_called_once_with(None) + d.addCallback(check) + return d + + def test_should_proceed_cert(self): + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=where("cacert.pem")) + + self.pb._download_if_needed = False + self.assertTrue(self.pb._should_proceed_cert()) + + self.pb._download_if_needed = True + self.assertFalse(self.pb._should_proceed_cert()) + + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=where("somefilethatdoesntexist.pem")) + self.assertTrue(self.pb._should_proceed_cert()) + + def _check_download_ca_cert(self, should_proceed): + """ + Helper to check different paths easily for the download ca + cert check + + :param should_proceed: sets the _should_proceed_cert in the + provider bootstrapper being tested + :type should_proceed: bool + + :returns: The contents of the certificate, the expected + content depending on should_proceed, and the mode of + the file to be checked by the caller + :rtype: tuple of str, str, int + """ + old_content = "NOT THE NEW CERT" + new_content = "NEW CERT" + new_cert_path = os.path.join(tempfile.mkdtemp(), + "mynewcert.pem") + + with open(new_cert_path, "w") as c: + c.write(old_content) + + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=new_cert_path) + self.pb._domain = "somedomain" + + self.pb._should_proceed_cert = mock.MagicMock( + return_value=should_proceed) + + read = None + content_to_check = None + mode = None + + with mock.patch('requests.models.Response.content', + new_callable=mock.PropertyMock) as \ + content: + content.return_value = new_content + response_obj = Response() + response_obj.raise_for_status = mock.MagicMock() + + self.pb._session.get = mock.MagicMock(return_value=response_obj) + self.pb._download_ca_cert() + with open(new_cert_path, "r") as nc: + read = nc.read() + if should_proceed: + content_to_check = new_content + else: + content_to_check = old_content + mode = stat.S_IMODE(os.stat(new_cert_path).st_mode) + + os.unlink(new_cert_path) + return read, content_to_check, mode + + def test_download_ca_cert_no_saving(self): + read, expected_read, mode = self._check_download_ca_cert(False) + self.assertEqual(read, expected_read) + self.assertEqual(mode, int("600", 8)) + + def test_download_ca_cert_saving(self): + read, expected_read, mode = self._check_download_ca_cert(True) + self.assertEqual(read, expected_read) + self.assertEqual(mode, int("600", 8)) + + def test_check_ca_fingerprint_skips(self): + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( + return_value="") + self.pb._domain = "somedomain" + + self.pb._should_proceed_cert = mock.MagicMock(return_value=False) + + self.pb._check_ca_fingerprint() + self.assertFalse(self.pb._provider_config. + get_ca_cert_fingerprint.called) + + def test_check_ca_cert_fingerprint_raises_bad_format(self): + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( + return_value="wrongfprformat!!") + self.pb._domain = "somedomain" + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + with self.assertRaises(WrongFingerprint): + self.pb._check_ca_fingerprint() + + # This two hashes different in the last byte, but that's good enough + # for the tests + KNOWN_BAD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034efe" \ + "7dd1b910062ca323eb4da5c7f" + KNOWN_GOOD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034ef" \ + "e7dd1b910062ca323eb4da5c7e" + KNOWN_GOOD_CERT = """ +-----BEGIN CERTIFICATE----- +MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt +YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v +Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw +FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV +BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai +dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB +7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84 +CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+ +znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4 +MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4 +lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0 +bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl +DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB +lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy +YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw +XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE +MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w +DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl +cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY +k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj +RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG +htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX +EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J +aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l +mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK +G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co +Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d +69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e +yV8e +-----END CERTIFICATE----- +""" + + def _prepare_provider_config_with(self, cert_path, cert_hash): + """ + Mocks the provider config to give the cert_path and cert_hash + specified + + :param cert_path: path for the certificate + :type cert_path: str + :param cert_hash: hash for the certificate as it would appear + in the provider config json + :type cert_hash: str + """ + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( + return_value=cert_hash) + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=cert_path) + self.pb._domain = "somedomain" + + def test_check_ca_fingerprint_checksout(self): + cert_path = os.path.join(tempfile.mkdtemp(), + "mynewcert.pem") + + with open(cert_path, "w") as c: + c.write(self.KNOWN_GOOD_CERT) + + self._prepare_provider_config_with(cert_path, self.KNOWN_GOOD_HASH) + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + self.pb._check_ca_fingerprint() + + os.unlink(cert_path) + + def test_check_ca_fingerprint_fails(self): + cert_path = os.path.join(tempfile.mkdtemp(), + "mynewcert.pem") + + with open(cert_path, "w") as c: + c.write(self.KNOWN_GOOD_CERT) + + self._prepare_provider_config_with(cert_path, self.KNOWN_BAD_HASH) + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + with self.assertRaises(WrongFingerprint): + self.pb._check_ca_fingerprint() + + os.unlink(cert_path) + + +############################################################################### +# Tests with a fake provider # +############################################################################### + +class ProviderBootstrapperActiveTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + factory = fake_provider.get_provider_factory() + http = reactor.listenTCP(8002, factory) + https = reactor.listenSSL( + 0, factory, + fake_provider.OpenSSLServerContextFactory()) + get_port = lambda p: p.getHost().port + cls.http_port = get_port(http) + cls.https_port = get_port(https) + + def setUp(self): + self.pb = ProviderBootstrapper() + + # At certain points we are going to be replacing these methods + # directly in ProviderConfig to be able to catch calls from + # new ProviderConfig objects inside the methods tested. We + # need to save the old implementation and restore it in + # tearDown so we are sure everything is as expected for each + # test. If we do it inside each specific test, a failure in + # the test will leave the implementation with the mock. + self.old_gpp = ProviderConfig.get_path_prefix + self.old_load = ProviderConfig.load + self.old_save = ProviderConfig.save + self.old_api_version = ProviderConfig.get_api_version + + def tearDown(self): + ProviderConfig.get_path_prefix = self.old_gpp + ProviderConfig.load = self.old_load + ProviderConfig.save = self.old_save + ProviderConfig.get_api_version = self.old_api_version + + def test_check_https_succeeds(self): + # XXX: Need a proper CA signed cert to test this + pass + + @deferred() + def test_check_https_fails(self): + self.pb._domain = "localhost:%s" % (self.https_port,) + + def check(*args): + with self.assertRaises(requests.exceptions.SSLError): + self.pb._check_https() + return threads.deferToThread(check) + + @deferred() + def test_second_check_https_fails(self): + self.pb._domain = "localhost:1234" + + def check(*args): + with self.assertRaises(Exception): + self.pb._check_https() + return threads.deferToThread(check) + + @deferred() + def test_check_https_succeeds_if_danger(self): + self.pb._domain = "localhost:%s" % (self.https_port,) + self.pb._bypass_checks = True + + def check(*args): + self.pb._check_https() + + return threads.deferToThread(check) + + def _setup_provider_config_with(self, api, path_prefix): + """ + Sets up the ProviderConfig with mocks for the path prefix, the + api returned and load/save methods. + It modifies ProviderConfig directly instead of an object + because the object used is created in the method itself and we + cannot control that. + + :param api: API to return + :type api: str + :param path_prefix: path prefix to be used when calculating + paths + :type path_prefix: str + """ + ProviderConfig.get_path_prefix = mock.MagicMock( + return_value=path_prefix) + ProviderConfig.get_api_version = mock.MagicMock( + return_value=api) + ProviderConfig.load = mock.MagicMock() + ProviderConfig.save = mock.MagicMock() + + def _setup_providerbootstrapper(self, ifneeded): + """ + Sets the provider bootstrapper's domain to + localhost:https_port, sets it to bypass https checks and sets + the download if needed based on the ifneeded value. + + :param ifneeded: Value for _download_if_needed + :type ifneeded: bool + """ + self.pb._domain = "localhost:%s" % (self.https_port,) + self.pb._bypass_checks = True + self.pb._download_if_needed = ifneeded + + def _produce_dummy_provider_json(self): + """ + Creates a dummy provider json on disk in order to test + behaviour around it (download if newer online, etc) + + :returns: the provider.json path used + :rtype: str + """ + provider_dir = os.path.join(ProviderConfig() + .get_path_prefix(), + "leap", + "providers", + self.pb._domain) + mkdir_p(provider_dir) + provider_path = os.path.join(provider_dir, + "provider.json") + + with open(provider_path, "w") as p: + p.write("A") + return provider_path + + def test_download_provider_info_new_provider(self): + self._setup_provider_config_with("1", tempfile.mkdtemp()) + self._setup_providerbootstrapper(True) + + self.pb._download_provider_info() + self.assertTrue(ProviderConfig.save.called) + + @mock.patch('leap.config.providerconfig.ProviderConfig.get_ca_cert_path', + lambda x: where('cacert.pem')) + def test_download_provider_info_not_modified(self): + self._setup_provider_config_with("1", tempfile.mkdtemp()) + self._setup_providerbootstrapper(True) + provider_path = self._produce_dummy_provider_json() + + # set mtime to something really new + os.utime(provider_path, (-1, time.time())) + + with mock.patch.object( + ProviderConfig, 'get_api_uri', + return_value="https://localhost:%s" % (self.https_port,)): + self.pb._download_provider_info() + # we check that it doesn't save the provider + # config, because it's new enough + self.assertFalse(ProviderConfig.save.called) + + @mock.patch('leap.config.providerconfig.ProviderConfig.get_ca_cert_path', + lambda x: where('cacert.pem')) + def test_download_provider_info_modified(self): + self._setup_provider_config_with("1", tempfile.mkdtemp()) + self._setup_providerbootstrapper(True) + provider_path = self._produce_dummy_provider_json() + + # set mtime to something really old + os.utime(provider_path, (-1, 100)) + + with mock.patch.object( + ProviderConfig, 'get_api_uri', + return_value="https://localhost:%s" % (self.https_port,)): + self.pb._download_provider_info() + self.assertTrue(ProviderConfig.load.called) + self.assertTrue(ProviderConfig.save.called) + + @mock.patch('leap.config.providerconfig.ProviderConfig.get_ca_cert_path', + lambda x: where('cacert.pem')) + def test_download_provider_info_unsupported_api_raises(self): + self._setup_provider_config_with("9999999", tempfile.mkdtemp()) + self._setup_providerbootstrapper(False) + self._produce_dummy_provider_json() + + with mock.patch.object( + ProviderConfig, 'get_api_uri', + return_value="https://localhost:%s" % (self.https_port,)): + with self.assertRaises(UnsupportedProviderAPI): + self.pb._download_provider_info() + + @mock.patch('leap.config.providerconfig.ProviderConfig.get_ca_cert_path', + lambda x: where('cacert.pem')) + def test_download_provider_info_unsupported_api(self): + self._setup_provider_config_with(SupportedAPIs.SUPPORTED_APIS[0], + tempfile.mkdtemp()) + self._setup_providerbootstrapper(False) + self._produce_dummy_provider_json() + + with mock.patch.object( + ProviderConfig, 'get_api_uri', + return_value="https://localhost:%s" % (self.https_port,)): + self.pb._download_provider_info() + + @mock.patch('leap.config.providerconfig.ProviderConfig.get_api_uri', + lambda x: 'api.uri') + @mock.patch('leap.config.providerconfig.ProviderConfig.get_ca_cert_path', + lambda x: '/cert/path') + def test_check_api_certificate_skips(self): + self.pb._provider_config = ProviderConfig() + self.pb._session.get = mock.MagicMock(return_value=Response()) + + self.pb._should_proceed_cert = mock.MagicMock(return_value=False) + self.pb._check_api_certificate() + self.assertFalse(self.pb._session.get.called) + + @deferred() + def test_check_api_certificate_fails(self): + self.pb._provider_config = ProviderConfig() + self.pb._provider_config.get_api_uri = mock.MagicMock( + return_value="https://localhost:%s" % (self.https_port,)) + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=os.path.join( + os.path.split(__file__)[0], + "wrongcert.pem")) + self.pb._provider_config.get_api_version = mock.MagicMock( + return_value="1") + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + def check(*args): + with self.assertRaises(requests.exceptions.SSLError): + self.pb._check_api_certificate() + d = threads.deferToThread(check) + return d + + @deferred() + def test_check_api_certificate_succeeds(self): + self.pb._provider_config = ProviderConfig() + self.pb._provider_config.get_api_uri = mock.MagicMock( + return_value="https://localhost:%s" % (self.https_port,)) + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=where('cacert.pem')) + self.pb._provider_config.get_api_version = mock.MagicMock( + return_value="1") + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + def check(*args): + self.pb._check_api_certificate() + d = threads.deferToThread(check) + return d diff --git a/src/leap/bitmask/services/eip/tests/test_vpngatewayselector.py b/src/leap/bitmask/services/eip/tests/test_vpngatewayselector.py new file mode 100644 index 00000000..c90681d7 --- /dev/null +++ b/src/leap/bitmask/services/eip/tests/test_vpngatewayselector.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# test_vpngatewayselector.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 . +""" +tests for vpngatewayselector +""" + +import unittest + +from leap.services.eip.eipconfig import EIPConfig, VPNGatewaySelector +from leap.common.testing.basetest import BaseLeapTest +from mock import Mock + + +sample_gateways = [ + {u'host': u'gateway1.com', + u'ip_address': u'1.2.3.4', + u'location': u'location1'}, + {u'host': u'gateway2.com', + u'ip_address': u'2.3.4.5', + u'location': u'location2'}, + {u'host': u'gateway3.com', + u'ip_address': u'3.4.5.6', + u'location': u'location3'}, + {u'host': u'gateway4.com', + u'ip_address': u'4.5.6.7', + u'location': u'location4'} +] + +sample_gateways_no_location = [ + {u'host': u'gateway1.com', + u'ip_address': u'1.2.3.4'}, + {u'host': u'gateway2.com', + u'ip_address': u'2.3.4.5'}, + {u'host': u'gateway3.com', + u'ip_address': u'3.4.5.6'} +] + +sample_locations = { + u'location1': {u'timezone': u'2'}, + u'location2': {u'timezone': u'-7'}, + u'location3': {u'timezone': u'-4'}, + u'location4': {u'timezone': u'+13'} +} + +# 0 is not used, only for indexing from 1 in tests +ips = (0, u'1.2.3.4', u'2.3.4.5', u'3.4.5.6', u'4.5.6.7') + + +class VPNGatewaySelectorTest(BaseLeapTest): + """ + VPNGatewaySelector's tests. + """ + def setUp(self): + self.eipconfig = EIPConfig() + self.eipconfig.get_gateways = Mock(return_value=sample_gateways) + self.eipconfig.get_locations = Mock(return_value=sample_locations) + + def tearDown(self): + pass + + def test_get_no_gateways(self): + gateway_selector = VPNGatewaySelector(self.eipconfig) + self.eipconfig.get_gateways = Mock(return_value=[]) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, []) + + def test_get_gateway_with_no_locations(self): + gateway_selector = VPNGatewaySelector(self.eipconfig) + self.eipconfig.get_gateways = Mock( + return_value=sample_gateways_no_location) + self.eipconfig.get_locations = Mock(return_value=[]) + gateways = gateway_selector.get_gateways() + gateways_default_order = [ + sample_gateways[0]['ip_address'], + sample_gateways[1]['ip_address'], + sample_gateways[2]['ip_address'] + ] + self.assertEqual(gateways, gateways_default_order) + + def test_correct_order_gmt(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, 0) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [ips[1], ips[3], ips[2], ips[4]]) + + def test_correct_order_gmt_minus_3(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, -3) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [ips[3], ips[2], ips[1], ips[4]]) + + def test_correct_order_gmt_minus_7(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, -7) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [ips[2], ips[3], ips[4], ips[1]]) + + def test_correct_order_gmt_plus_5(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, 5) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [ips[1], ips[4], ips[3], ips[2]]) + + def test_correct_order_gmt_plus_12(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, 12) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [ips[4], ips[2], ips[3], ips[1]]) + + def test_correct_order_gmt_minus_11(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, -11) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [ips[4], ips[2], ips[3], ips[1]]) + + def test_correct_order_gmt_plus_14(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, 14) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [ips[4], ips[2], ips[3], ips[1]]) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/leap/bitmask/services/eip/tests/wrongcert.pem b/src/leap/bitmask/services/eip/tests/wrongcert.pem new file mode 100644 index 00000000..e6cff38a --- /dev/null +++ b/src/leap/bitmask/services/eip/tests/wrongcert.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAIWZus5EIXNtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTMwNjI1MTc0NjExWhcNMTgwNjI1MTc0NjExWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEA2ObM7ESjyuxFZYD/Y68qOPQgjgggW+cdXfBpU2p4n7clsrUeMhWdW40Y +77Phzor9VOeqs3ZpHuyLzsYVp/kFDm8tKyo2ah5fJwzL0VCSLYaZkUQQ7GNUmTCk +furaxl8cQx/fg395V7/EngsS9B3/y5iHbctbA4MnH3jaotO5EGeo6hw7/eyCotQ9 +KbBV9GJMcY94FsXBCmUB+XypKklWTLhSaS6Cu4Fo8YLW6WmcnsyEOGS2F7WVf5at +7CBWFQZHaSgIBLmc818/mDYCnYmCVMFn/6Ndx7V2NTlz+HctWrQn0dmIOnCUeCwS +wXq9PnBR1rSx/WxwyF/WpyjOFkcIo7vm72kS70pfrYsXcZD4BQqkXYj3FyKnPt3O +ibLKtCxL8/83wOtErPcYpG6LgFkgAAlHQ9MkUi5dbmjCJtpqQmlZeK1RALdDPiB3 +K1KZimrGsmcE624dJxUIOJJpuwJDy21F8kh5ZAsAtE1prWETrQYNElNFjQxM83rS +ZR1Ql2MPSB4usEZT57+KvpEzlOnAT3elgCg21XrjSFGi14hCEao4g2OEZH5GAwm5 +frf6UlSRZ/g3tLTfI8Hv1prw15W2qO+7q7SBAplTODCRk+Yb0YoA2mMM/QXBUcXs +vKEDLSSxzNIBi3T62l39RB/ml+gPKo87ZMDivex1ZhrcJc3Yu3sCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUPjE+4pun+8FreIdpoR8v6N7xKtUwdQYDVR0jBG4wbIAUPjE+ +4pun+8FreIdpoR8v6N7xKtWhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCF +mbrORCFzbTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCpvCPdtvXJ +muTj379TZuCJs7/l0FhA7AHa1WAlHjsXHaA7N0+3ZWAbdtXDsowal6S+ldgU/kfV +Lq7NrRq+amJWC7SYj6cvVwhrSwSvu01fe/TWuOzHrRv1uTfJ/VXLonVufMDd9opo +bhqYxMaxLdIx6t/MYmZH4Wpiq0yfZuv//M8i7BBl/qvaWbLhg0yVAKRwjFvf59h6 +6tRFCLddELOIhLDQtk8zMbioPEbfAlKdwwP8kYGtDGj6/9/YTd/oTKRdgHuwyup3 +m0L20Y6LddC+tb0WpK5EyrNbCbEqj1L4/U7r6f/FKNA3bx6nfdXbscaMfYonKAKg +1cRrRg45sErmCz0QyTnWzXyvbjR4oQRzyW3kJ1JZudZ+AwOi00J5FYa3NiLuxl1u +gIGKWSrASQWhEdpa1nlCgX7PhdaQgYjEMpQvA0GCA0OF5JDu8en1yZqsOt1hCLIN +lkz/5jKPqrclY5hV99bE3hgCHRmIPNHCZG3wbZv2yJKxJX1YLMmQwAmSh2N7YwGG +yXRvCxQs5ChPHyRairuf/5MZCZnSVb45ppTVuNUijsbflKRUgfj/XvfqQ22f+C9N +Om2dmNvAiS2TOIfuP47CF2OUa5q4plUwmr+nyXQGM0SIoHNCj+MBdFfb3oxxAtI+ +SLhbnzQv5e84Doqz3YF0XW8jyR7q8GFLNA== +-----END CERTIFICATE----- diff --git a/src/leap/bitmask/services/eip/udstelnet.py b/src/leap/bitmask/services/eip/udstelnet.py new file mode 100644 index 00000000..e6c82350 --- /dev/null +++ b/src/leap/bitmask/services/eip/udstelnet.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# udstelnet.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 . + +import os +import socket +import telnetlib + + +class ConnectionRefusedError(Exception): + pass + + +class MissingSocketError(Exception): + pass + + +class UDSTelnet(telnetlib.Telnet): + """ + A telnet-alike class, that can listen on unix domain sockets + """ + + def open(self, host, port=23, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): + """ + Connect to a host. If port is 'unix', it will open a + connection over unix docmain sockets. + + The optional second argument is the port number, which + defaults to the standard telnet port (23). + Don't try to reopen an already connected instance. + """ + self.eof = 0 + self.host = host + self.port = port + self.timeout = timeout + + if self.port == "unix": + # unix sockets spoken + if not os.path.exists(self.host): + raise MissingSocketError() + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + self.sock.connect(self.host) + except socket.error: + raise ConnectionRefusedError() + else: + self.sock = socket.create_connection((host, port), timeout) diff --git a/src/leap/bitmask/services/eip/vpnlaunchers.py b/src/leap/bitmask/services/eip/vpnlaunchers.py new file mode 100644 index 00000000..17950a25 --- /dev/null +++ b/src/leap/bitmask/services/eip/vpnlaunchers.py @@ -0,0 +1,927 @@ +# -*- coding: utf-8 -*- +# vpnlaunchers.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 . + +""" +Platform dependant VPN launchers +""" +import commands +import logging +import getpass +import os +import platform +import subprocess +import stat +try: + import grp +except ImportError: + pass # ignore, probably windows + +from abc import ABCMeta, abstractmethod +from functools import partial + +from leap.common.check import leap_assert, leap_assert_type +from leap.common.files import which +from leap.config.providerconfig import ProviderConfig +from leap.services.eip.eipconfig import EIPConfig, VPNGatewaySelector +from leap.util import first +from leap.util.privilege_policies import LinuxPolicyChecker +from leap.util import privilege_policies + +logger = logging.getLogger(__name__) + + +class VPNLauncherException(Exception): + pass + + +class OpenVPNNotFoundException(VPNLauncherException): + pass + + +class EIPNoPolkitAuthAgentAvailable(VPNLauncherException): + pass + + +class EIPNoPkexecAvailable(VPNLauncherException): + pass + + +class EIPNoTunKextLoaded(VPNLauncherException): + pass + + +class VPNLauncher(object): + """ + Abstract launcher class + """ + __metaclass__ = ABCMeta + + UPDOWN_FILES = None + OTHER_FILES = None + + @abstractmethod + def get_vpn_command(self, eipconfig=None, providerconfig=None, + socket_host=None, socket_port=None): + """ + Returns the platform dependant vpn launching command + + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + :param socket_port: either string "unix" if it's a unix + socket, or port otherwise + :type socket_port: str + + :return: A VPN command ready to be launched + :rtype: list + """ + return [] + + @abstractmethod + def get_vpn_env(self, providerconfig): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :rtype: dict + """ + return {} + + @classmethod + def missing_updown_scripts(kls): + """ + Returns what updown scripts are missing. + :rtype: list + """ + leap_assert(kls.UPDOWN_FILES is not None, + "Need to define UPDOWN_FILES for this particular " + "auncher before calling this method") + file_exist = partial(_has_updown_scripts, warn=False) + zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) + missing = filter(lambda (path, exists): exists is False, zipped) + return [path for path, exists in missing] + + @classmethod + def missing_other_files(kls): + """ + Returns what other important files are missing during startup. + Same as missing_updown_scripts but does not check for exec bit. + :rtype: list + """ + leap_assert(kls.UPDOWN_FILES is not None, + "Need to define OTHER_FILES for this particular " + "auncher before calling this method") + file_exist = partial(_has_other_files, warn=False) + zipped = zip(kls.OTHER_FILES, map(file_exist, kls.OTHER_FILES)) + missing = filter(lambda (path, exists): exists is False, zipped) + return [path for path, exists in missing] + + +def get_platform_launcher(): + launcher = globals()[platform.system() + "VPNLauncher"] + leap_assert(launcher, "Unimplemented platform launcher: %s" % + (platform.system(),)) + return launcher() + + +def _is_pkexec_in_system(): + """ + Checks the existence of the pkexec binary in system. + """ + pkexec_path = which('pkexec') + if len(pkexec_path) == 0: + return False + return True + + +def _has_updown_scripts(path, warn=True): + """ + Checks the existence of the up/down scripts and its + exec bit if applicable. + + :param path: the path to be checked + :type path: str + + :param warn: whether we should log the absence + :type warn: bool + + :rtype: bool + """ + is_file = os.path.isfile(path) + if warn and not is_file: + logger.error("Could not find up/down script %s. " + "Might produce DNS leaks." % (path,)) + + # XXX check if applies in win + is_exe = False + try: + is_exe = (stat.S_IXUSR & os.stat(path)[stat.ST_MODE] != 0) + except OSError as e: + logger.warn("%s" % (e,)) + if warn and not is_exe: + logger.error("Up/down script %s is not executable. " + "Might produce DNS leaks." % (path,)) + return is_file and is_exe + + +def _has_other_files(path, warn=True): + """ + Checks the existence of other important files. + + :param path: the path to be checked + :type path: str + + :param warn: whether we should log the absence + :type warn: bool + + :rtype: bool + """ + is_file = os.path.isfile(path) + if warn and not is_file: + logger.warning("Could not find file during checks: %s. " % ( + path,)) + return is_file + + +def _is_auth_agent_running(): + """ + Checks if a polkit daemon is running. + + :return: True if it's running, False if it's not. + :rtype: boolean + """ + ps = 'ps aux | grep polkit-%s-authentication-agent-1' + opts = (ps % case for case in ['[g]nome', '[k]de']) + is_running = map(lambda l: commands.getoutput(l), opts) + return any(is_running) + + +def _try_to_launch_agent(): + """ + Tries to launch a polkit daemon. + """ + opts = [ + "/usr/lib/policykit-1-gnome/polkit-gnome-authentication-agent-1", + # XXX add kde thing here + ] + for cmd in opts: + try: + subprocess.Popen([cmd], shell=True) + except: + pass + + +class LinuxVPNLauncher(VPNLauncher): + """ + VPN launcher for the Linux platform + """ + + PKEXEC_BIN = 'pkexec' + OPENVPN_BIN = 'openvpn' + OPENVPN_BIN_PATH = os.path.join( + ProviderConfig().get_path_prefix(), + "..", "apps", "eip", OPENVPN_BIN) + + SYSTEM_CONFIG = "/etc/leap" + UP_DOWN_FILE = "resolv-update" + UP_DOWN_PATH = "%s/%s" % (SYSTEM_CONFIG, UP_DOWN_FILE) + + # We assume this is there by our openvpn dependency, and + # we will put it there on the bundle too. + # TODO adapt to the bundle path. + OPENVPN_DOWN_ROOT_BASE = "/usr/lib/openvpn/" + OPENVPN_DOWN_ROOT_FILE = "openvpn-plugin-down-root.so" + OPENVPN_DOWN_ROOT_PATH = "%s/%s" % ( + OPENVPN_DOWN_ROOT_BASE, + OPENVPN_DOWN_ROOT_FILE) + + UPDOWN_FILES = (UP_DOWN_PATH,) + POLKIT_PATH = LinuxPolicyChecker.get_polkit_path() + OTHER_FILES = (POLKIT_PATH, ) + + def missing_other_files(self): + """ + 'Extend' the VPNLauncher's missing_other_files to check if the polkit + files is outdated. If the polkit file that is in OTHER_FILES exists but + is not up to date, it is added to the missing list. + + :returns: a list of missing files + :rtype: list of str + """ + missing = VPNLauncher.missing_other_files.im_func(self) + polkit_file = LinuxPolicyChecker.get_polkit_path() + if polkit_file not in missing: + if privilege_policies.is_policy_outdated(self.OPENVPN_BIN_PATH): + missing.append(polkit_file) + + return missing + + @classmethod + def cmd_for_missing_scripts(kls, frompath, pol_file): + """ + Returns a sh script that can copy the missing files. + + :param frompath: The path where the up/down scripts live + :type frompath: str + :param pol_file: The path where the dynamically generated + policy file lives + :type pol_file: str + + :rtype: str + """ + to = kls.SYSTEM_CONFIG + + cmd = '#!/bin/sh\n' + cmd += 'mkdir -p "%s"\n' % (to, ) + cmd += 'cp "%s/%s" "%s"\n' % (frompath, kls.UP_DOWN_FILE, to) + cmd += 'cp "%s" "%s"\n' % (pol_file, kls.POLKIT_PATH) + cmd += 'chmod 644 "%s"\n' % (kls.POLKIT_PATH, ) + + return cmd + + @classmethod + def maybe_pkexec(kls): + """ + Checks whether pkexec is available in the system, and + returns the path if found. + + Might raise EIPNoPkexecAvailable or EIPNoPolkitAuthAgentAvailable + + :returns: a list of the paths where pkexec is to be found + :rtype: list + """ + if _is_pkexec_in_system(): + if not _is_auth_agent_running(): + _try_to_launch_agent() + if _is_auth_agent_running(): + pkexec_possibilities = which(kls.PKEXEC_BIN) + leap_assert(len(pkexec_possibilities) > 0, + "We couldn't find pkexec") + return pkexec_possibilities + else: + logger.warning("No polkit auth agent found. pkexec " + + "will use its own auth agent.") + raise EIPNoPolkitAuthAgentAvailable() + else: + logger.warning("System has no pkexec") + raise EIPNoPkexecAvailable() + + @classmethod + def maybe_down_plugin(kls): + """ + Returns the path of the openvpn down-root-plugin, searching first + in the relative path for the standalone bundle, and then in the system + path where the debian package puts it. + + :returns: the path where the plugin was found, or None + :rtype: str or None + """ + cwd = os.getcwd() + rel_path_in_bundle = os.path.join( + 'apps', 'eip', 'files', kls.OPENVPN_DOWN_ROOT_FILE) + abs_path_in_bundle = os.path.join(cwd, rel_path_in_bundle) + if os.path.isfile(abs_path_in_bundle): + return abs_path_in_bundle + abs_path_in_system = kls.OPENVPN_DOWN_ROOT_FILE + if os.path.isfile(abs_path_in_system): + return abs_path_in_system + + logger.warning("We could not find the down-root-plugin, so no updown " + "scripts will be run. DNS leaks are likely!") + return None + + def get_vpn_command(self, eipconfig=None, providerconfig=None, + socket_host=None, socket_port="unix", openvpn_verb=1): + """ + Returns the platform dependant vpn launching command. It will + look for openvpn in the regular paths and algo in + path_prefix/apps/eip/ (in case standalone is set) + + Might raise: + VPNLauncherException, + OpenVPNNotFoundException. + + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + + :param socket_port: either string "unix" if it's a unix + socket, or port otherwise + :type socket_port: str + + :param openvpn_verb: openvpn verbosity wanted + :type openvpn_verb: int + + :return: A VPN command ready to be launched + :rtype: list + """ + leap_assert(eipconfig, "We need an eip config") + leap_assert_type(eipconfig, EIPConfig) + leap_assert(providerconfig, "We need a provider config") + leap_assert_type(providerconfig, ProviderConfig) + leap_assert(socket_host, "We need a socket host!") + leap_assert(socket_port, "We need a socket port!") + + kwargs = {} + if ProviderConfig.standalone: + kwargs['path_extension'] = os.path.join( + providerconfig.get_path_prefix(), + "..", "apps", "eip") + + openvpn_possibilities = which(self.OPENVPN_BIN, **kwargs) + + if len(openvpn_possibilities) == 0: + raise OpenVPNNotFoundException() + + openvpn = first(openvpn_possibilities) + args = [] + + pkexec = self.maybe_pkexec() + if pkexec: + args.append(openvpn) + openvpn = first(pkexec) + + if openvpn_verb is not None: + args += ['--verb', '%d' % (openvpn_verb,)] + + gateway_selector = VPNGatewaySelector(eipconfig) + gateways = gateway_selector.get_gateways() + + if not gateways: + logger.error('No gateway was found!') + raise VPNLauncherException(self.tr('No gateway was found!')) + + logger.debug("Using gateways ips: {}".format(', '.join(gateways))) + + for gw in gateways: + args += ['--remote', gw, '1194', 'udp'] + + args += [ + '--client', + '--dev', 'tun', + ############################################################## + # persist-tun makes ping-restart fail because it leaves a + # broken routing table + ############################################################## + # '--persist-tun', + '--persist-key', + '--tls-client', + '--remote-cert-tls', + 'server' + ] + + openvpn_configuration = eipconfig.get_openvpn_configuration() + + for key, value in openvpn_configuration.items(): + args += ['--%s' % (key,), value] + + ############################################################## + # The down-root plugin fails in some situations, so we don't + # drop privs for the time being + ############################################################## + # args += [ + # '--user', getpass.getuser(), + # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name + # ] + + if socket_port == "unix": # that's always the case for linux + args += [ + '--management-client-user', getpass.getuser() + ] + + args += [ + '--management-signal', + '--management', socket_host, socket_port, + '--script-security', '2' + ] + + plugin_path = self.maybe_down_plugin() + # If we do not have the down plugin neither in the bundle + # nor in the system, we do not do updown scripts. The alternative + # is leaving the user without the ability to restore dns and routes + # to its original state. + + if plugin_path and _has_updown_scripts(self.UP_DOWN_PATH): + args += [ + '--up', self.UP_DOWN_PATH, + '--down', self.UP_DOWN_PATH, + ############################################################## + # For the time being we are disabling the usage of the + # down-root plugin, because it doesn't quite work as + # expected (i.e. it doesn't run route -del as root + # when finishing, so it fails to properly + # restart/quit) + ############################################################## + # '--plugin', plugin_path, + # '\'script_type=down %s\'' % self.UP_DOWN_PATH + ] + + args += [ + '--cert', eipconfig.get_client_cert_path(providerconfig), + '--key', eipconfig.get_client_cert_path(providerconfig), + '--ca', providerconfig.get_ca_cert_path() + ] + + logger.debug("Running VPN with command:") + logger.debug("%s %s" % (openvpn, " ".join(args))) + + return [openvpn] + args + + def get_vpn_env(self, providerconfig): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :rtype: dict + """ + leap_assert(providerconfig, "We need a provider config") + leap_assert_type(providerconfig, ProviderConfig) + + return {"LD_LIBRARY_PATH": os.path.join( + providerconfig.get_path_prefix(), + "..", "lib")} + + +class DarwinVPNLauncher(VPNLauncher): + """ + VPN launcher for the Darwin Platform + """ + + COCOASUDO = "cocoasudo" + # XXX need the good old magic translate for these strings + # (look for magic in 0.2.0 release) + SUDO_MSG = ("Bitmask needs administrative privileges to run " + "Encrypted Internet.") + INSTALL_MSG = ("\"Bitmask needs administrative privileges to install " + "missing scripts and fix permissions.\"") + + INSTALL_PATH = os.path.realpath(os.getcwd() + "/../../") + INSTALL_PATH_ESCAPED = os.path.realpath(os.getcwd() + "/../../") + OPENVPN_BIN = 'openvpn.leap' + OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,) + OPENVPN_PATH_ESCAPED = "%s/Contents/Resources/openvpn" % ( + INSTALL_PATH_ESCAPED,) + + UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,) + DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,) + OPENVPN_DOWN_PLUGIN = '%s/openvpn-down-root.so' % (OPENVPN_PATH,) + + UPDOWN_FILES = (UP_SCRIPT, DOWN_SCRIPT, OPENVPN_DOWN_PLUGIN) + OTHER_FILES = [] + + @classmethod + def cmd_for_missing_scripts(kls, frompath): + """ + Returns a command that can copy the missing scripts. + :rtype: str + """ + to = kls.OPENVPN_PATH_ESCAPED + cmd = "#!/bin/sh\nmkdir -p %s\ncp \"%s/\"* %s\nchmod 744 %s/*" % ( + to, frompath, to, to) + return cmd + + @classmethod + def maybe_kextloaded(kls): + """ + Checks if the needed kext is loaded before launching openvpn. + """ + return bool(commands.getoutput('kextstat | grep "leap.tun"')) + + def _get_resource_path(self): + """ + Returns the absolute path to the app resources directory + + :rtype: str + """ + return os.path.abspath( + os.path.join( + os.getcwd(), + "../../Contents/Resources")) + + def _get_icon_path(self): + """ + Returns the absolute path to the app icon + + :rtype: str + """ + return os.path.join(self._get_resource_path(), + "leap-client.tiff") + + def get_cocoasudo_ovpn_cmd(self): + """ + Returns a string with the cocoasudo command needed to run openvpn + as admin with a nice password prompt. The actual command needs to be + appended. + + :rtype: (str, list) + """ + iconpath = self._get_icon_path() + has_icon = os.path.isfile(iconpath) + args = ["--icon=%s" % iconpath] if has_icon else [] + args.append("--prompt=%s" % (self.SUDO_MSG,)) + + return self.COCOASUDO, args + + def get_cocoasudo_installmissing_cmd(self): + """ + Returns a string with the cocoasudo command needed to install missing + files as admin with a nice password prompt. The actual command needs to + be appended. + + :rtype: (str, list) + """ + iconpath = self._get_icon_path() + has_icon = os.path.isfile(iconpath) + args = ["--icon=%s" % iconpath] if has_icon else [] + args.append("--prompt=%s" % (self.INSTALL_MSG,)) + + return self.COCOASUDO, args + + def get_vpn_command(self, eipconfig=None, providerconfig=None, + socket_host=None, socket_port="unix", openvpn_verb=1): + """ + Returns the platform dependant vpn launching command + + Might raise VPNException. + + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + + :param socket_port: either string "unix" if it's a unix + socket, or port otherwise + :type socket_port: str + + :param openvpn_verb: openvpn verbosity wanted + :type openvpn_verb: int + + :return: A VPN command ready to be launched + :rtype: list + """ + leap_assert(eipconfig, "We need an eip config") + leap_assert_type(eipconfig, EIPConfig) + leap_assert(providerconfig, "We need a provider config") + leap_assert_type(providerconfig, ProviderConfig) + leap_assert(socket_host, "We need a socket host!") + leap_assert(socket_port, "We need a socket port!") + + if not self.maybe_kextloaded(): + raise EIPNoTunKextLoaded + + kwargs = {} + if ProviderConfig.standalone: + kwargs['path_extension'] = os.path.join( + providerconfig.get_path_prefix(), + "..", "apps", "eip") + + openvpn_possibilities = which( + self.OPENVPN_BIN, + **kwargs) + if len(openvpn_possibilities) == 0: + raise OpenVPNNotFoundException() + + openvpn = first(openvpn_possibilities) + args = [openvpn] + + if openvpn_verb is not None: + args += ['--verb', '%d' % (openvpn_verb,)] + + gateway_selector = VPNGatewaySelector(eipconfig) + gateways = gateway_selector.get_gateways() + + logger.debug("Using gateways ips: {gw}".format( + gw=', '.join(gateways))) + + for gw in gateways: + args += ['--remote', gw, '1194', 'udp'] + + args += [ + '--client', + '--dev', 'tun', + ############################################################## + # persist-tun makes ping-restart fail because it leaves a + # broken routing table + ############################################################## + # '--persist-tun', + '--persist-key', + '--tls-client', + '--remote-cert-tls', + 'server' + ] + + openvpn_configuration = eipconfig.get_openvpn_configuration() + for key, value in openvpn_configuration.items(): + args += ['--%s' % (key,), value] + + user = getpass.getuser() + + ############################################################## + # The down-root plugin fails in some situations, so we don't + # drop privs for the time being + ############################################################## + # args += [ + # '--user', user, + # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name + # ] + + if socket_port == "unix": + args += [ + '--management-client-user', user + ] + + args += [ + '--management-signal', + '--management', socket_host, socket_port, + '--script-security', '2' + ] + + if _has_updown_scripts(self.UP_SCRIPT): + args += [ + '--up', '\"%s\"' % (self.UP_SCRIPT,), + ] + + if _has_updown_scripts(self.DOWN_SCRIPT): + args += [ + '--down', '\"%s\"' % (self.DOWN_SCRIPT,) + ] + + # should have the down script too + if _has_updown_scripts(self.OPENVPN_DOWN_PLUGIN): + args += [ + ########################################################### + # For the time being we are disabling the usage of the + # down-root plugin, because it doesn't quite work as + # expected (i.e. it doesn't run route -del as root + # when finishing, so it fails to properly + # restart/quit) + ########################################################### + # '--plugin', self.OPENVPN_DOWN_PLUGIN, + # '\'%s\'' % self.DOWN_SCRIPT + ] + + # we set user to be passed to the up/down scripts + args += [ + '--setenv', "LEAPUSER", "%s" % (user,)] + + args += [ + '--cert', eipconfig.get_client_cert_path(providerconfig), + '--key', eipconfig.get_client_cert_path(providerconfig), + '--ca', providerconfig.get_ca_cert_path() + ] + + command, cargs = self.get_cocoasudo_ovpn_cmd() + cmd_args = cargs + args + + logger.debug("Running VPN with command:") + logger.debug("%s %s" % (command, " ".join(cmd_args))) + + return [command] + cmd_args + + def get_vpn_env(self, providerconfig): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :rtype: dict + """ + return {"DYLD_LIBRARY_PATH": os.path.join( + providerconfig.get_path_prefix(), + "..", "lib")} + + +class WindowsVPNLauncher(VPNLauncher): + """ + VPN launcher for the Windows platform + """ + + OPENVPN_BIN = 'openvpn_leap.exe' + + # XXX UPDOWN_FILES ... we do not have updown files defined yet! + # (and maybe we won't) + + def get_vpn_command(self, eipconfig=None, providerconfig=None, + socket_host=None, socket_port="9876", openvpn_verb=1): + """ + Returns the platform dependant vpn launching command. It will + look for openvpn in the regular paths and algo in + path_prefix/apps/eip/ (in case standalone is set) + + Might raise VPNException. + + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + + :param socket_port: either string "unix" if it's a unix + socket, or port otherwise + :type socket_port: str + + :param openvpn_verb: the openvpn verbosity wanted + :type openvpn_verb: int + + :return: A VPN command ready to be launched + :rtype: list + """ + leap_assert(eipconfig, "We need an eip config") + leap_assert_type(eipconfig, EIPConfig) + leap_assert(providerconfig, "We need a provider config") + leap_assert_type(providerconfig, ProviderConfig) + leap_assert(socket_host, "We need a socket host!") + leap_assert(socket_port, "We need a socket port!") + leap_assert(socket_port != "unix", + "We cannot use unix sockets in windows!") + + openvpn_possibilities = which( + self.OPENVPN_BIN, + path_extension=os.path.join(providerconfig.get_path_prefix(), + "..", "apps", "eip")) + + if len(openvpn_possibilities) == 0: + raise OpenVPNNotFoundException() + + openvpn = first(openvpn_possibilities) + args = [] + if openvpn_verb is not None: + args += ['--verb', '%d' % (openvpn_verb,)] + + gateway_selector = VPNGatewaySelector(eipconfig) + gateways = gateway_selector.get_gateways() + + logger.debug("Using gateways ips: {}".format(', '.join(gateways))) + + for gw in gateways: + args += ['--remote', gw, '1194', 'udp'] + + args += [ + '--client', + '--dev', 'tun', + ############################################################## + # persist-tun makes ping-restart fail because it leaves a + # broken routing table + ############################################################## + # '--persist-tun', + '--persist-key', + '--tls-client', + # We make it log to a file because we cannot attach to the + # openvpn process' stdout since it's a process with more + # privileges than we are + '--log-append', 'eip.log', + '--remote-cert-tls', + 'server' + ] + + openvpn_configuration = eipconfig.get_openvpn_configuration() + for key, value in openvpn_configuration.items(): + args += ['--%s' % (key,), value] + + ############################################################## + # The down-root plugin fails in some situations, so we don't + # drop privs for the time being + ############################################################## + # args += [ + # '--user', getpass.getuser(), + # #'--group', grp.getgrgid(os.getgroups()[-1]).gr_name + # ] + + args += [ + '--management-signal', + '--management', socket_host, socket_port, + '--script-security', '2' + ] + + args += [ + '--cert', eipconfig.get_client_cert_path(providerconfig), + '--key', eipconfig.get_client_cert_path(providerconfig), + '--ca', providerconfig.get_ca_cert_path() + ] + + logger.debug("Running VPN with command:") + logger.debug("%s %s" % (openvpn, " ".join(args))) + + return [openvpn] + args + + def get_vpn_env(self, providerconfig): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :rtype: dict + """ + return {} + + +if __name__ == "__main__": + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + try: + abs_launcher = VPNLauncher() + except Exception as e: + assert isinstance(e, TypeError), "Something went wrong" + print "Abstract Prefixer class is working as expected" + + vpnlauncher = get_platform_launcher() + + eipconfig = EIPConfig() + eipconfig.set_api_version('1') + if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): + provider = ProviderConfig() + if provider.load("leap/providers/bitmask.net/provider.json"): + vpnlauncher.get_vpn_command(eipconfig=eipconfig, + providerconfig=provider, + socket_host="/blah") diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py new file mode 100644 index 00000000..5b07a3cf --- /dev/null +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -0,0 +1,791 @@ +# -*- coding: utf-8 -*- +# vpnprocess.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 . +""" +VPN Manager, spawned in a custom processProtocol. +""" +import logging +import os +import psutil +import psutil.error +import shutil +import socket + +from PySide import QtCore + +from leap.common.check import leap_assert, leap_assert_type +from leap.config.providerconfig import ProviderConfig +from leap.services.eip.vpnlaunchers import get_platform_launcher +from leap.services.eip.eipconfig import EIPConfig +from leap.services.eip.udstelnet import UDSTelnet +from leap.util import first + +logger = logging.getLogger(__name__) +vpnlog = logging.getLogger('leap.openvpn') + +from twisted.internet import protocol +from twisted.internet import defer +from twisted.internet.task import LoopingCall +from twisted.internet import error as internet_error + + +class VPNSignals(QtCore.QObject): + """ + These are the signals that we use to let the UI know + about the events we are polling. + They are instantiated in the VPN object and passed along + till the VPNProcess. + """ + state_changed = QtCore.Signal(dict) + status_changed = QtCore.Signal(dict) + process_finished = QtCore.Signal(int) + + def __init__(self): + QtCore.QObject.__init__(self) + + +class OpenVPNAlreadyRunning(Exception): + message = ("Another openvpn instance is already running, and could " + "not be stopped.") + + +class AlienOpenVPNAlreadyRunning(Exception): + message = ("Another openvpn instance is already running, and could " + "not be stopped because it was not launched by LEAP.") + + +class VPN(object): + """ + This is the high-level object that the GUI is dealing with. + It exposes the start and terminate methods. + + On start, it spawns a VPNProcess instance that will use a vpnlauncher + suited for the running platform and connect to the management interface + opened by the openvpn process, executing commands over that interface on + demand. + """ + TERMINATE_MAXTRIES = 10 + TERMINATE_WAIT = 1 # secs + + OPENVPN_VERB = "openvpn_verb" + + def __init__(self, **kwargs): + """ + Instantiate empty attributes and get a copy + of a QObject containing the QSignals that we will pass along + to the VPNManager. + """ + from twisted.internet import reactor + self._vpnproc = None + self._pollers = [] + self._reactor = reactor + self._qtsigs = VPNSignals() + + self._openvpn_verb = kwargs.get(self.OPENVPN_VERB, None) + + @property + def qtsigs(self): + return self._qtsigs + + def start(self, *args, **kwargs): + """ + Starts the openvpn subprocess. + + :param args: args to be passed to the VPNProcess + :type args: tuple + + :param kwargs: kwargs to be passed to the VPNProcess + :type kwargs: dict + """ + self._stop_pollers() + kwargs['qtsigs'] = self.qtsigs + kwargs['openvpn_verb'] = self._openvpn_verb + + # start the main vpn subprocess + vpnproc = VPNProcess(*args, **kwargs) + #qtsigs=self.qtsigs, + #openvpn_verb=self._openvpn_verb) + + if vpnproc.get_openvpn_process(): + logger.info("Another vpn process is running. Will try to stop it.") + vpnproc.stop_if_already_running() + + cmd = vpnproc.getCommand() + env = os.environ + for key, val in vpnproc.vpn_env.items(): + env[key] = val + + self._reactor.spawnProcess(vpnproc, cmd[0], cmd, env) + self._vpnproc = vpnproc + + # add pollers for status and state + # this could be extended to a collection of + # generic watchers + + poll_list = [LoopingCall(vpnproc.pollStatus), + LoopingCall(vpnproc.pollState)] + self._pollers.extend(poll_list) + self._start_pollers() + + def _kill_if_left_alive(self, tries=0): + """ + Check if the process is still alive, and sends a + SIGKILL after a timeout period. + + :param tries: counter of tries, used in recursion + :type tries: int + """ + from twisted.internet import reactor + while tries < self.TERMINATE_MAXTRIES: + if self._vpnproc.transport.pid is None: + logger.debug("Process has been happily terminated.") + return + else: + logger.debug("Process did not die, waiting...") + tries += 1 + reactor.callLater(self.TERMINATE_WAIT, + self._kill_if_left_alive, tries) + + # after running out of patience, we try a killProcess + logger.debug("Process did not died. Sending a SIGKILL.") + self.killit() + + def killit(self): + """ + Sends a kill signal to the process. + """ + self._stop_pollers() + self._vpnproc.aborted = True + self._vpnproc.killProcess() + + def terminate(self, shutdown=False): + """ + Stops the openvpn subprocess. + + Attempts to send a SIGTERM first, and after a timeout + it sends a SIGKILL. + """ + from twisted.internet import reactor + self._stop_pollers() + + # First we try to be polite and send a SIGTERM... + if self._vpnproc: + self._sentterm = True + self._vpnproc.terminate_openvpn(shutdown=shutdown) + + # ...but we also trigger a countdown to be unpolite + # if strictly needed. + + # XXX Watch out! This will fail NOW since we are running + # openvpn as root as a workaround for some connection issues. + reactor.callLater( + self.TERMINATE_WAIT, self._kill_if_left_alive) + + def _start_pollers(self): + """ + Iterate through the registered observers + and start the looping call for them. + """ + for poller in self._pollers: + poller.start(VPNManager.POLL_TIME) + + def _stop_pollers(self): + """ + Iterate through the registered observers + and stop the looping calls if they are running. + """ + for poller in self._pollers: + if poller.running: + poller.stop() + self._pollers = [] + + +class VPNManager(object): + """ + This is a mixin that we use in the VPNProcess class. + Here we get together all methods related with the openvpn management + interface. + + A copy of a QObject containing signals as attributes is passed along + upon initialization, and we use that object to emit signals to qt-land. + + For more info about management methods:: + + zcat `dpkg -L openvpn | grep management` + """ + + # Timers, in secs + POLL_TIME = 0.5 + CONNECTION_RETRY_TIME = 1 + + TS_KEY = "ts" + STATUS_STEP_KEY = "status_step" + OK_KEY = "ok" + IP_KEY = "ip" + REMOTE_KEY = "remote" + + TUNTAP_READ_KEY = "tun_tap_read" + TUNTAP_WRITE_KEY = "tun_tap_write" + TCPUDP_READ_KEY = "tcp_udp_read" + TCPUDP_WRITE_KEY = "tcp_udp_write" + AUTH_READ_KEY = "auth_read" + + def __init__(self, qtsigs=None): + """ + Initializes the VPNManager. + + :param qtsigs: a QObject containing the Qt signals used by the UI + to give feedback about state changes. + :type qtsigs: QObject + """ + from twisted.internet import reactor + self._reactor = reactor + self._tn = None + self._qtsigs = qtsigs + self._aborted = False + + @property + def qtsigs(self): + return self._qtsigs + + @property + def aborted(self): + return self._aborted + + @aborted.setter + def aborted(self, value): + self._aborted = value + + def _seek_to_eof(self): + """ + Read as much as available. Position seek pointer to end of stream + """ + try: + self._tn.read_eager() + except EOFError: + logger.debug("Could not read from socket. Assuming it died.") + return + + def _send_command(self, command, until=b"END"): + """ + Sends a command to the telnet connection and reads until END + is reached. + + :param command: command to send + :type command: str + + :param until: byte delimiter string for reading command output + :type until: byte str + + :return: response read + :rtype: list + """ + leap_assert(self._tn, "We need a tn connection!") + + try: + self._tn.write("%s\n" % (command,)) + buf = self._tn.read_until(until, 2) + self._seek_to_eof() + blist = buf.split('\r\n') + if blist[-1].startswith(until): + del blist[-1] + return blist + else: + return [] + + except socket.error: + # XXX should get a counter and repeat only + # after mod X times. + logger.warning('socket error (command was: "%s")' % (command,)) + self._close_management_socket(announce=False) + logger.debug('trying to connect to management again') + self.try_to_connect_to_management(max_retries=5) + return [] + + # XXX should move this to a errBack! + except Exception as e: + logger.warning("Error sending command %s: %r" % + (command, e)) + return [] + + def _close_management_socket(self, announce=True): + """ + Close connection to openvpn management interface. + """ + logger.debug('closing socket') + if announce: + self._tn.write("quit\n") + self._tn.read_all() + self._tn.get_socket().close() + self._tn = None + + def _connect_management(self, socket_host, socket_port): + """ + Connects to the management interface on the specified + socket_host socket_port. + + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + + :param socket_port: either string "unix" if it's a unix + socket, or port otherwise + :type socket_port: str + """ + if self.is_connected(): + self._close_management_socket() + + try: + self._tn = UDSTelnet(socket_host, socket_port) + + # XXX make password optional + # specially for win. we should generate + # the pass on the fly when invoking manager + # from conductor + + # self.tn.read_until('ENTER PASSWORD:', 2) + # self.tn.write(self.password + '\n') + # self.tn.read_until('SUCCESS:', 2) + if self._tn: + self._tn.read_eager() + + # XXX move this to the Errback + except Exception as e: + logger.warning("Could not connect to OpenVPN yet: %r" % (e,)) + self._tn = None + + def _connectCb(self, *args): + """ + Callback for connection. + + :param args: not used + """ + if self._tn: + logger.info('Connected to management') + else: + logger.debug('Cannot connect to management...') + + def _connectErr(self, failure): + """ + Errorback for connection. + + :param failure: Failure + """ + logger.warning(failure) + + def connect_to_management(self, host, port): + """ + Connect to a management interface. + + :param host: the host of the management interface + :type host: str + + :param port: the port of the management interface + :type port: str + + :returns: a deferred + """ + self.connectd = defer.maybeDeferred( + self._connect_management, host, port) + self.connectd.addCallbacks(self._connectCb, self._connectErr) + return self.connectd + + def is_connected(self): + """ + Returns the status of the management interface. + + :returns: True if connected, False otherwise + :rtype: bool + """ + return True if self._tn else False + + def try_to_connect_to_management(self, retry=0, max_retries=None): + """ + Attempts to connect to a management interface, and retries + after CONNECTION_RETRY_TIME if not successful. + + :param retry: number of the retry + :type retry: int + """ + if max_retries and retry > max_retries: + logger.warning("Max retries reached while attempting to connect " + "to management. Aborting.") + self.aborted = True + return + + # _alive flag is set in the VPNProcess class. + if not self._alive: + logger.debug('Tried to connect to management but process is ' + 'not alive.') + return + logger.debug('trying to connect to management') + if not self.aborted and not self.is_connected(): + self.connect_to_management(self._socket_host, self._socket_port) + self._reactor.callLater( + self.CONNECTION_RETRY_TIME, + self.try_to_connect_to_management, retry + 1) + + def _parse_state_and_notify(self, output): + """ + Parses the output of the state command and emits state_changed + signal when the state changes. + + :param output: list of lines that the state command printed as + its output + :type output: list + """ + for line in output: + stripped = line.strip() + if stripped == "END": + continue + parts = stripped.split(",") + if len(parts) < 5: + continue + ts, status_step, ok, ip, remote = parts + + state_dict = { + self.TS_KEY: ts, + self.STATUS_STEP_KEY: status_step, + self.OK_KEY: ok, + self.IP_KEY: ip, + self.REMOTE_KEY: remote + } + + if state_dict != self._last_state: + self.qtsigs.state_changed.emit(state_dict) + self._last_state = state_dict + + def _parse_status_and_notify(self, output): + """ + Parses the output of the status command and emits + status_changed signal when the status changes. + + :param output: list of lines that the status command printed + as its output + :type output: list + """ + tun_tap_read = "" + tun_tap_write = "" + tcp_udp_read = "" + tcp_udp_write = "" + auth_read = "" + for line in output: + stripped = line.strip() + if stripped.endswith("STATISTICS") or stripped == "END": + continue + parts = stripped.split(",") + if len(parts) < 2: + continue + if parts[0].strip() == "TUN/TAP read bytes": + tun_tap_read = parts[1] + elif parts[0].strip() == "TUN/TAP write bytes": + tun_tap_write = parts[1] + elif parts[0].strip() == "TCP/UDP read bytes": + tcp_udp_read = parts[1] + elif parts[0].strip() == "TCP/UDP write bytes": + tcp_udp_write = parts[1] + elif parts[0].strip() == "Auth read bytes": + auth_read = parts[1] + + status_dict = { + self.TUNTAP_READ_KEY: tun_tap_read, + self.TUNTAP_WRITE_KEY: tun_tap_write, + self.TCPUDP_READ_KEY: tcp_udp_read, + self.TCPUDP_WRITE_KEY: tcp_udp_write, + self.AUTH_READ_KEY: auth_read + } + + if status_dict != self._last_status: + self.qtsigs.status_changed.emit(status_dict) + self._last_status = status_dict + + def get_state(self): + """ + Notifies the gui of the output of the state command over + the openvpn management interface. + """ + if self.is_connected(): + return self._parse_state_and_notify(self._send_command("state")) + + def get_status(self): + """ + Notifies the gui of the output of the status command over + the openvpn management interface. + """ + if self.is_connected(): + return self._parse_status_and_notify(self._send_command("status")) + + @property + def vpn_env(self): + """ + Return a dict containing the vpn environment to be used. + """ + return self._launcher.get_vpn_env(self._providerconfig) + + def terminate_openvpn(self, shutdown=False): + """ + Attempts to terminate openvpn by sending a SIGTERM. + """ + if self.is_connected(): + self._send_command("signal SIGTERM") + if shutdown: + self._cleanup_tempfiles() + + def _cleanup_tempfiles(self): + """ + Remove all temporal files we might have left behind. + + Iif self.port is 'unix', we have created a temporal socket path that, + under normal circumstances, we should be able to delete. + """ + if self._socket_port == "unix": + logger.debug('cleaning socket file temp folder') + tempfolder = first(os.path.split(self._socket_host)) + if tempfolder and os.path.isdir(tempfolder): + try: + shutil.rmtree(tempfolder) + except OSError: + logger.error('could not delete tmpfolder %s' % tempfolder) + + def get_openvpn_process(self): + """ + Looks for openvpn instances running. + + :rtype: process + """ + openvpn_process = None + for p in psutil.process_iter(): + try: + # XXX Not exact! + # Will give false positives. + # we should check that cmdline BEGINS + # with openvpn or with our wrapper + # (pkexec / osascript / whatever) + + # This needs more work, see #3268, but for the moment + # we need to be able to filter out arguments in the form + # --openvpn-foo, since otherwise we are shooting ourselves + # in the feet. + if any(map(lambda s: s.startswith("openvpn"), p.cmdline)): + openvpn_process = p + break + except psutil.error.AccessDenied: + pass + return openvpn_process + + def stop_if_already_running(self): + """ + Checks if VPN is already running and tries to stop it. + + Might raise OpenVPNAlreadyRunning. + + :return: True if stopped, False otherwise + + """ + process = self.get_openvpn_process() + if not process: + logger.debug('Could not find openvpn process while ' + 'trying to stop it.') + return + + logger.debug("OpenVPN is already running, trying to stop it...") + cmdline = process.cmdline + + manag_flag = "--management" + if isinstance(cmdline, list) and manag_flag in cmdline: + # we know that our invocation has this distinctive fragment, so + # we use this fingerprint to tell other invocations apart. + # this might break if we change the configuration path in the + # launchers + smellslikeleap = lambda s: "leap" in s and "providers" in s + + if not any(map(smellslikeleap, cmdline)): + logger.debug("We cannot stop this instance since we do not " + "recognise it as a leap invocation.") + raise AlienOpenVPNAlreadyRunning + + try: + index = cmdline.index(manag_flag) + host = cmdline[index + 1] + port = cmdline[index + 2] + logger.debug("Trying to connect to %s:%s" + % (host, port)) + self.connect_to_management(host, port) + + # XXX this has a problem with connections to different + # remotes. So the reconnection will only work when we are + # terminating instances left running for the same provider. + # If we are killing an openvpn instance configured for another + # provider, we will get: + # TLS Error: local/remote TLS keys are out of sync + # However, that should be a rare case right now. + self._send_command("signal SIGTERM") + self._close_management_socket(announce=True) + except Exception as e: + logger.warning("Problem trying to terminate OpenVPN: %r" + % (e,)) + else: + logger.debug("Could not find the expected openvpn command line.") + + process = self.get_openvpn_process() + if process is None: + logger.debug("Successfully finished already running " + "openvpn process.") + return True + else: + logger.warning("Unable to terminate OpenVPN") + raise OpenVPNAlreadyRunning + + +class VPNProcess(protocol.ProcessProtocol, VPNManager): + """ + A ProcessProtocol class that can be used to spawn a process that will + launch openvpn and connect to its management interface to control it + programmatically. + """ + + def __init__(self, eipconfig, providerconfig, socket_host, socket_port, + qtsigs, openvpn_verb): + """ + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + + :param socket_port: either string "unix" if it's a unix + socket, or port otherwise + :type socket_port: str + + :param qtsigs: a QObject containing the Qt signals used to notify the + UI. + :type qtsigs: QObject + + :param openvpn_verb: the desired level of verbosity in the + openvpn invocation + :type openvpn_verb: int + """ + VPNManager.__init__(self, qtsigs=qtsigs) + leap_assert_type(eipconfig, EIPConfig) + leap_assert_type(providerconfig, ProviderConfig) + leap_assert_type(qtsigs, QtCore.QObject) + + #leap_assert(not self.isRunning(), "Starting process more than once!") + + self._eipconfig = eipconfig + self._providerconfig = providerconfig + self._socket_host = socket_host + self._socket_port = socket_port + + self._launcher = get_platform_launcher() + + self._last_state = None + self._last_status = None + self._alive = False + + self._openvpn_verb = openvpn_verb + + # processProtocol methods + + def connectionMade(self): + """ + Called when the connection is made. + + .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa + """ + self._alive = True + self.aborted = False + self.try_to_connect_to_management(max_retries=10) + + def outReceived(self, data): + """ + Called when new data is available on stdout. + + :param data: the data read on stdout + + .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa + """ + # truncate the newline + # should send this to the logging window + vpnlog.info(data[:-1]) + + def processExited(self, reason): + """ + Called when the child process exits. + + .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa + """ + exit_code = reason.value.exitCode + if isinstance(exit_code, int): + logger.debug("processExited, status %d" % (exit_code,)) + self.qtsigs.process_finished.emit(exit_code) + self._alive = False + + def processEnded(self, reason): + """ + Called when the child process exits and all file descriptors associated + with it have been closed. + + .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa + """ + exit_code = reason.value.exitCode + if isinstance(exit_code, int): + logger.debug("processEnded, status %d" % (exit_code,)) + + # polling + + def pollStatus(self): + """ + Polls connection status. + """ + if self._alive: + self.get_status() + + def pollState(self): + """ + Polls connection state. + """ + if self._alive: + self.get_state() + + # launcher + + def getCommand(self): + """ + Gets the vpn command from the aproppriate launcher. + + Might throw: VPNLauncherException, OpenVPNNotFoundException. + """ + cmd = self._launcher.get_vpn_command( + eipconfig=self._eipconfig, + providerconfig=self._providerconfig, + socket_host=self._socket_host, + socket_port=self._socket_port, + openvpn_verb=self._openvpn_verb) + return map(str, cmd) + + # shutdown + + def killProcess(self): + """ + Sends the KILL signal to the running process. + """ + try: + self.transport.signalProcess('KILL') + except internet_error.ProcessExitedAlready: + logger.debug('Process Exited Already') diff --git a/src/leap/bitmask/services/mail/__init__.py b/src/leap/bitmask/services/mail/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/services/mail/imap.py b/src/leap/bitmask/services/mail/imap.py new file mode 100644 index 00000000..4dceb2ad --- /dev/null +++ b/src/leap/bitmask/services/mail/imap.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# imap.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 . +""" +Initialization of imap service +""" +import logging +import sys + +from leap.mail.imap.service import imap +from twisted.python import log + +logger = logging.getLogger(__name__) + + +def start_imap_service(*args, **kwargs): + """ + Initializes and run imap service. + + :returns: twisted.internet.task.LoopingCall instance + """ + logger.debug('Launching imap service') + + # Uncomment the next two lines to get a separate debugging log + # TODO handle this by a separate flag. + #log.startLogging(open('/tmp/leap-imap.log', 'w')) + #log.startLogging(sys.stdout) + + return imap.run_service(*args, **kwargs) diff --git a/src/leap/bitmask/services/mail/smtpbootstrapper.py b/src/leap/bitmask/services/mail/smtpbootstrapper.py new file mode 100644 index 00000000..48040035 --- /dev/null +++ b/src/leap/bitmask/services/mail/smtpbootstrapper.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +# smtpbootstrapper.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 . + +""" +SMTP bootstrapping +""" + +import logging +import os + +from PySide import QtCore + +from leap.common.check import leap_assert, leap_assert_type +from leap.common.files import get_mtime +from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpauth import SRPAuth +from leap.util.request_helpers import get_content +from leap.services.abstractbootstrapper import AbstractBootstrapper + +logger = logging.getLogger(__name__) + + +class SMTPBootstrapper(AbstractBootstrapper): + """ + SMTP init procedure + """ + + # All dicts returned are of the form + # {"passed": bool, "error": str} + download_config = QtCore.Signal(dict) + + def __init__(self): + AbstractBootstrapper.__init__(self) + + self._provider_config = None + self._smtp_config = None + self._download_if_needed = False + + def _download_config(self, *args): + """ + Downloads the SMTP config for the given provider + """ + + leap_assert(self._provider_config, + "We need a provider configuration!") + + logger.debug("Downloading SMTP config for %s" % + (self._provider_config.get_domain(),)) + + headers = {} + mtime = get_mtime(os.path.join(self._smtp_config + .get_path_prefix(), + "leap", + "providers", + self._provider_config.get_domain(), + "smtp-service.json")) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + api_version = self._provider_config.get_api_version() + + # there is some confusion with this uri, + config_uri = "%s/%s/config/smtp-service.json" % ( + self._provider_config.get_api_uri(), api_version) + + logger.debug('Downloading SMTP config from: %s' % config_uri) + + srp_auth = SRPAuth(self._provider_config) + session_id = srp_auth.get_session_id() + cookies = None + if session_id: + cookies = {"_session_id": session_id} + + res = self._session.get(config_uri, + verify=self._provider_config + .get_ca_cert_path(), + headers=headers, + cookies=cookies) + res.raise_for_status() + + self._smtp_config.set_api_version(api_version) + + # Not modified + if res.status_code == 304: + logger.debug("SMTP definition has not been modified") + self._smtp_config.load(os.path.join( + "leap", "providers", + self._provider_config.get_domain(), + "smtp-service.json")) + else: + smtp_definition, mtime = get_content(res) + + self._smtp_config.load(data=smtp_definition, mtime=mtime) + self._smtp_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "smtp-service.json"]) + + def run_smtp_setup_checks(self, + provider_config, + smtp_config, + download_if_needed=False): + """ + Starts the checks needed for a new smtp setup + + :param provider_config: Provider configuration + :type provider_config: ProviderConfig + :param smtp_config: SMTP configuration to populate + :type smtp_config: SMTPConfig + :param download_if_needed: True if it should check for mtime + for the file + :type download_if_needed: bool + """ + leap_assert_type(provider_config, ProviderConfig) + + self._provider_config = provider_config + self._smtp_config = smtp_config + self._download_if_needed = download_if_needed + + cb_chain = [ + (self._download_config, self.download_config), + ] + + self.addCallbackChain(cb_chain) diff --git a/src/leap/bitmask/services/mail/smtpconfig.py b/src/leap/bitmask/services/mail/smtpconfig.py new file mode 100644 index 00000000..ea0f9c37 --- /dev/null +++ b/src/leap/bitmask/services/mail/smtpconfig.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# smtpconfig.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 . + +""" +SMTP configuration +""" +import logging + +from leap.common.config.baseconfig import BaseConfig +from leap.services.mail.smtpspec import get_schema + +logger = logging.getLogger(__name__) + + +class SMTPConfig(BaseConfig): + """ + SMTP configuration abstraction class + """ + + def __init__(self): + BaseConfig.__init__(self) + + def _get_schema(self): + """ + Returns the schema corresponding to the version given. + + :rtype: dict or None if the version is not supported. + """ + return get_schema(self._api_version) + + def get_hosts(self): + return self._safe_get_value("hosts") + + def get_locations(self): + return self._safe_get_value("locations") diff --git a/src/leap/bitmask/services/mail/smtpspec.py b/src/leap/bitmask/services/mail/smtpspec.py new file mode 100644 index 00000000..ff9d1bf8 --- /dev/null +++ b/src/leap/bitmask/services/mail/smtpspec.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# smtpspec.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 . + +# Schemas dict +# To add a schema for a version you should follow the form: +# { '1': schema_v1, '2': schema_v2, ... etc } +# so for instance, to add the '2' version, you should do: +# smtp_config_spec['2'] = schema_v2 +smtp_config_spec = {} + +smtp_config_spec['1'] = { + 'description': 'sample smtp service config', + 'type': 'object', + 'properties': { + 'serial': { + 'type': int, + 'default': 1, + 'required': ["True"] + }, + 'version': { + 'type': int, + 'default': 1, + 'required': ["True"] + }, + 'hosts': { + 'type': dict, + 'default': { + "walrus": { + "hostname": "someprovider", + "ip_address": "1.1.1.1", + "port": 1111 + }, + }, + }, + 'locations': { + 'type': dict, + 'default': { + "locations": { + + } + } + } + } +} + + +def get_schema(version): + """ + Returns the schema corresponding to the version given. + + :param version: the version of the schema to get. + :type version: str + :rtype: dict or None if the version is not supported. + """ + schema = smtp_config_spec.get(version, None) + return schema diff --git a/src/leap/bitmask/services/soledad/__init__.py b/src/leap/bitmask/services/soledad/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py new file mode 100644 index 00000000..c67bc004 --- /dev/null +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -0,0 +1,265 @@ +# -*- coding: utf-8 -*- +# soledadbootstrapper.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 . + +""" +Soledad bootstrapping +""" + +import logging +import os + +from PySide import QtCore +from u1db import errors as u1db_errors + +from leap.common.check import leap_assert, leap_assert_type +from leap.common.files import get_mtime +from leap.keymanager import KeyManager, openpgp +from leap.keymanager.errors import KeyNotFound +from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpauth import SRPAuth +from leap.services.soledad.soledadconfig import SoledadConfig +from leap.util.request_helpers import get_content +from leap.soledad import Soledad +from leap.services.abstractbootstrapper import AbstractBootstrapper + +logger = logging.getLogger(__name__) + + +class SoledadBootstrapper(AbstractBootstrapper): + """ + Soledad init procedure + """ + + SOLEDAD_KEY = "soledad" + KEYMANAGER_KEY = "keymanager" + + PUBKEY_KEY = "user[public_key]" + + # All dicts returned are of the form + # {"passed": bool, "error": str} + download_config = QtCore.Signal(dict) + gen_key = QtCore.Signal(dict) + + def __init__(self): + AbstractBootstrapper.__init__(self) + + self._provider_config = None + self._soledad_config = None + self._keymanager = None + self._download_if_needed = False + self._user = "" + self._password = "" + self._soledad = None + + @property + def keymanager(self): + return self._keymanager + + @property + def soledad(self): + return self._soledad + + def _load_and_sync_soledad(self, srp_auth): + """ + Once everthing is in the right place, we instantiate and sync + Soledad + + :param srp_auth: SRPAuth object used + :type srp_auth: SRPAuth + """ + uuid = srp_auth.get_uid() + + prefix = os.path.join(self._soledad_config.get_path_prefix(), + "leap", "soledad") + secrets_path = "%s/%s.secret" % (prefix, uuid) + local_db_path = "%s/%s.db" % (prefix, uuid) + + # TODO: Select server based on timezone (issue #3308) + server_dict = self._soledad_config.get_hosts() + + if server_dict.keys(): + selected_server = server_dict[server_dict.keys()[0]] + server_url = "https://%s:%s/user-%s" % ( + selected_server["hostname"], + selected_server["port"], + uuid) + + logger.debug("Using soledad server url: %s" % (server_url,)) + + cert_file = self._provider_config.get_ca_cert_path() + + # TODO: If selected server fails, retry with another host + # (issue #3309) + try: + self._soledad = Soledad( + uuid, + self._password.encode("utf-8"), + secrets_path=secrets_path, + local_db_path=local_db_path, + server_url=server_url, + cert_file=cert_file, + auth_token=srp_auth.get_token()) + self._soledad.sync() + except u1db_errors.Unauthorized: + logger.error("Error while initializing soledad.") + else: + raise Exception("No soledad server found") + + def _download_config(self): + """ + Downloads the Soledad config for the given provider + """ + + leap_assert(self._provider_config, + "We need a provider configuration!") + + logger.debug("Downloading Soledad config for %s" % + (self._provider_config.get_domain(),)) + + self._soledad_config = SoledadConfig() + + headers = {} + mtime = get_mtime( + os.path.join( + self._soledad_config.get_path_prefix(), + "leap", "providers", + self._provider_config.get_domain(), + "soledad-service.json")) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + api_version = self._provider_config.get_api_version() + + # there is some confusion with this uri, + config_uri = "%s/%s/config/soledad-service.json" % ( + self._provider_config.get_api_uri(), + api_version) + logger.debug('Downloading soledad config from: %s' % config_uri) + + srp_auth = SRPAuth(self._provider_config) + session_id = srp_auth.get_session_id() + cookies = None + if session_id: + cookies = {"_session_id": session_id} + + res = self._session.get(config_uri, + verify=self._provider_config + .get_ca_cert_path(), + headers=headers, + cookies=cookies) + res.raise_for_status() + + self._soledad_config.set_api_version(api_version) + + # Not modified + if res.status_code == 304: + logger.debug("Soledad definition has not been modified") + self._soledad_config.load( + os.path.join( + "leap", "providers", + self._provider_config.get_domain(), + "soledad-service.json")) + else: + soledad_definition, mtime = get_content(res) + + self._soledad_config.load(data=soledad_definition, mtime=mtime) + self._soledad_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "soledad-service.json"]) + + self._load_and_sync_soledad(srp_auth) + + def _gen_key(self, _): + """ + Generates the key pair if needed, uploads it to the webapp and + nickserver + """ + leap_assert(self._provider_config, + "We need a provider configuration!") + + address = "%s@%s" % (self._user, self._provider_config.get_domain()) + + logger.debug("Retrieving key for %s" % (address,)) + + srp_auth = SRPAuth(self._provider_config) + + # TODO: Fix for Windows + gpgbin = "/usr/bin/gpg" + + if self._standalone: + gpgbin = os.path.join(self._provider_config.get_path_prefix(), + "..", "apps", "mail", "gpg") + + self._keymanager = KeyManager( + address, + "https://nicknym.%s:6425" % (self._provider_config.get_domain(),), + self._soledad, + #token=srp_auth.get_token(), # TODO: enable token usage + session_id=srp_auth.get_session_id(), + ca_cert_path=self._provider_config.get_ca_cert_path(), + api_uri=self._provider_config.get_api_uri(), + api_version=self._provider_config.get_api_version(), + uid=srp_auth.get_uid(), + gpgbinary=gpgbin) + try: + self._keymanager.get_key(address, openpgp.OpenPGPKey, + private=True, fetch_remote=False) + except KeyNotFound: + logger.debug("Key not found. Generating key for %s" % (address,)) + self._keymanager.gen_key(openpgp.OpenPGPKey) + self._keymanager.send_key(openpgp.OpenPGPKey) + logger.debug("Key generated successfully.") + + def run_soledad_setup_checks(self, + provider_config, + user, + password, + download_if_needed=False, + standalone=False): + """ + Starts the checks needed for a new soledad setup + + :param provider_config: Provider configuration + :type provider_config: ProviderConfig + :param user: User's login + :type user: str + :param password: User's password + :type password: str + :param download_if_needed: If True, it will only download + files if the have changed since the + time it was previously downloaded. + :type download_if_needed: bool + :param standalone: If True, it'll look for paths inside the + bundle (like for gpg) + :type standalone: bool + """ + leap_assert_type(provider_config, ProviderConfig) + + self._provider_config = provider_config + self._download_if_needed = download_if_needed + self._user = user + self._password = password + self._standalone = standalone + + cb_chain = [ + (self._download_config, self.download_config), + (self._gen_key, self.gen_key) + ] + + self.addCallbackChain(cb_chain) diff --git a/src/leap/bitmask/services/soledad/soledadconfig.py b/src/leap/bitmask/services/soledad/soledadconfig.py new file mode 100644 index 00000000..a2367692 --- /dev/null +++ b/src/leap/bitmask/services/soledad/soledadconfig.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# soledadconfig.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 . + +""" +Soledad configuration +""" +import logging + +from leap.common.config.baseconfig import BaseConfig +from leap.services.soledad.soledadspec import get_schema + +logger = logging.getLogger(__name__) + + +class SoledadConfig(BaseConfig): + """ + Soledad configuration abstraction class + """ + + def __init__(self): + BaseConfig.__init__(self) + + def _get_schema(self): + """ + Returns the schema corresponding to the version given. + + :rtype: dict or None if the version is not supported. + """ + return get_schema(self._api_version) + + def get_hosts(self): + return self._safe_get_value("hosts") + + def get_locations(self): + return self._safe_get_value("locations") diff --git a/src/leap/bitmask/services/soledad/soledadspec.py b/src/leap/bitmask/services/soledad/soledadspec.py new file mode 100644 index 00000000..111175dd --- /dev/null +++ b/src/leap/bitmask/services/soledad/soledadspec.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# soledadspec.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 . + +# Schemas dict +# To add a schema for a version you should follow the form: +# { '1': schema_v1, '2': schema_v2, ... etc } +# so for instance, to add the '2' version, you should do: +# soledad_config_spec['2'] = schema_v2 +soledad_config_spec = {} + +soledad_config_spec['1'] = { + 'description': 'sample soledad service config', + 'type': 'object', + 'properties': { + 'serial': { + 'type': int, + 'default': 1, + 'required': ["True"] + }, + 'version': { + 'type': int, + 'default': 1, + 'required': ["True"] + }, + 'hosts': { + 'type': dict, + 'default': { + "python": { + "hostname": "someprovider", + "ip_address": "1.1.1.1", + "location": "loc", + "port": 1111 + }, + }, + }, + 'locations': { + 'type': dict, + 'default': { + "locations": { + "ankara": { + "country_code": "TR", + "hemisphere": "N", + "name": "loc", + "timezone": "+0" + } + } + } + } + } +} + + +def get_schema(version): + """ + Returns the schema corresponding to the version given. + + :param version: the version of the schema to get. + :type version: str + :rtype: dict or None if the version is not supported. + """ + schema = soledad_config_spec.get(version, None) + return schema diff --git a/src/leap/bitmask/services/tests/__init__.py b/src/leap/bitmask/services/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/services/tests/test_abstractbootstrapper.py b/src/leap/bitmask/services/tests/test_abstractbootstrapper.py new file mode 100644 index 00000000..a9ee220f --- /dev/null +++ b/src/leap/bitmask/services/tests/test_abstractbootstrapper.py @@ -0,0 +1,196 @@ +## -*- coding: utf-8 -*- +# test_abstrctbootstrapper.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 . + + +""" +Tests for the Abstract Boostrapper functionality +""" + +import mock + +from PySide import QtCore + +from nose.twistedtools import deferred + +from leap.services.abstractbootstrapper import AbstractBootstrapper +from leap.util.pyside_tests_helper import UsesQApplication, BasicPySlotCase + + +class TesterBootstrapper(AbstractBootstrapper): + test_signal1 = QtCore.Signal(dict) + test_signal2 = QtCore.Signal(dict) + test_signal3 = QtCore.Signal(dict) + + ERROR_MSG = "This is a test error msg" + + def _check_that_passes(self, *args): + pass + + def _second_check_that_passes(self, *args): + pass + + def _check_that_fails(self, *args): + raise Exception(self.ERROR_MSG) + + def run_checks_pass(self): + cb_chain = [ + (self._check_that_passes, self.test_signal1), + (self._second_check_that_passes, self.test_signal2), + ] + return self.addCallbackChain(cb_chain) + + def run_second_checks_pass(self): + cb_chain = [ + (self._check_that_passes, None), + ] + return self.addCallbackChain(cb_chain) + + def run_checks_fail(self): + cb_chain = [ + (self._check_that_passes, self.test_signal1), + (self._check_that_fails, self.test_signal2) + ] + return self.addCallbackChain(cb_chain) + + def run_second_checks_fail(self): + cb_chain = [ + (self._check_that_passes, self.test_signal1), + (self._check_that_fails, self.test_signal2), + (self._second_check_that_passes, self.test_signal1) + ] + return self.addCallbackChain(cb_chain) + + def run_third_checks_fail(self): + cb_chain = [ + (self._check_that_passes, self.test_signal1), + (self._check_that_fails, None) + ] + return self.addCallbackChain(cb_chain) + + +class AbstractBootstrapperTest(UsesQApplication, BasicPySlotCase): + def setUp(self): + UsesQApplication.setUp(self) + BasicPySlotCase.setUp(self) + + self.tbt = TesterBootstrapper() + self.called1 = self.called2 = 0 + + @deferred() + def test_all_checks_executed_once(self): + self.tbt._check_that_passes = mock.MagicMock() + self.tbt._second_check_that_passes = mock.MagicMock() + + d = self.tbt.run_checks_pass() + + def check(*args): + self.tbt._check_that_passes.assert_called_once_with() + self.tbt._second_check_that_passes.\ + assert_called_once_with(None) + + d.addCallback(check) + return d + + ####################################################################### + # Dummy callbacks that test the arguments expected from a certain + # signal and only allow being called once + + def cb1(self, *args): + if tuple(self.args1) == args: + self.called1 += 1 + else: + raise ValueError('Invalid arguments for callback') + + def cb2(self, *args): + if tuple(self.args2) == args: + self.called2 += 1 + else: + raise ValueError('Invalid arguments for callback') + + # + ####################################################################### + + def _check_cb12_once(self, *args): + self.assertEquals(self.called1, 1) + self.assertEquals(self.called2, 1) + + @deferred() + def test_emits_correct(self): + self.tbt.test_signal1.connect(self.cb1) + self.tbt.test_signal2.connect(self.cb2) + d = self.tbt.run_checks_pass() + + self.args1 = [{ + AbstractBootstrapper.PASSED_KEY: True, + AbstractBootstrapper.ERROR_KEY: "" + }] + + self.args2 = self.args1 + + d.addCallback(self._check_cb12_once) + return d + + @deferred() + def test_emits_failed(self): + self.tbt.test_signal1.connect(self.cb1) + self.tbt.test_signal2.connect(self.cb2) + d = self.tbt.run_checks_fail() + + self.args1 = [{ + AbstractBootstrapper.PASSED_KEY: True, + AbstractBootstrapper.ERROR_KEY: "" + }] + + self.args2 = [{ + AbstractBootstrapper.PASSED_KEY: False, + AbstractBootstrapper.ERROR_KEY: + TesterBootstrapper.ERROR_MSG + }] + + d.addCallback(self._check_cb12_once) + return d + + @deferred() + def test_emits_failed_and_stops(self): + self.tbt.test_signal1.connect(self.cb1) + self.tbt.test_signal2.connect(self.cb2) + self.tbt.test_signal3.connect(self.cb1) + d = self.tbt.run_second_checks_fail() + + self.args1 = [{ + AbstractBootstrapper.PASSED_KEY: True, + AbstractBootstrapper.ERROR_KEY: "" + }] + + self.args2 = [{ + AbstractBootstrapper.PASSED_KEY: False, + AbstractBootstrapper.ERROR_KEY: + TesterBootstrapper.ERROR_MSG + }] + + d.addCallback(self._check_cb12_once) + return d + + @deferred() + def test_failed_without_signal(self): + d = self.tbt.run_third_checks_fail() + return d + + @deferred() + def test_sucess_without_signal(self): + d = self.tbt.run_second_checks_pass() + return d diff --git a/src/leap/bitmask/services/tx.py b/src/leap/bitmask/services/tx.py new file mode 100644 index 00000000..7da1cb01 --- /dev/null +++ b/src/leap/bitmask/services/tx.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# twisted.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 . +""" +Twisted services launched by the client +""" +import logging + +from twisted.application.service import Application +from twisted.internet.task import LoopingCall + +logger = logging.getLogger(__name__) + + +def task(): + """ + stub periodic task, mainly for tests. + DELETE-ME when there's real meat here :) + """ + from datetime import datetime + logger.debug("hi there %s", datetime.now()) + + +def leap_services(): + """ + Check which twisted services are enabled and + register them. + """ + logger.debug('starting leap services') + application = Application("Bitmask Local Services") + #lc = LoopingCall(task) + #lc.start(5) + return application diff --git a/src/leap/bitmask/util/__init__.py b/src/leap/bitmask/util/__init__.py new file mode 100644 index 00000000..f39b52e9 --- /dev/null +++ b/src/leap/bitmask/util/__init__.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# __init__.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 . +""" +Initializes version and app info, plus some small and handy functions. +""" +import datetime +import os + +from pkg_resources import parse_version + + +def _is_release_version(version): + """ + Helper to determine whether a version is a final release or not. + The release needs to be of the form: w.x.y.z containing only numbers + and dots. + + :param version: the version string + :type version: str + :returns: if the version is a release version or not. + :rtype: bool + """ + parsed_version = parse_version(version) + not_number = 0 + for x in parsed_version: + try: + int(x) + except: + not_number += 1 + + return not_number == 1 + + +__version__ = "unknown" +IS_RELEASE_VERSION = False + +try: + from leap._version import get_versions + __version__ = get_versions()['version'] + IS_RELEASE_VERSION = _is_release_version(__version__) + del get_versions +except ImportError: + #running on a tree that has not run + #the setup.py setver + pass + +__appname__ = "unknown" +try: + from leap._appname import __appname__ +except ImportError: + #running on a tree that has not run + #the setup.py setver + pass + +__full_version__ = __appname__ + '/' + str(__version__) + + +def first(things): + """ + Return the head of a collection. + """ + try: + return things[0] + except TypeError: + return None + + +def get_modification_ts(path): + """ + Gets modification time of a file. + + :param path: the path to get ts from + :type path: str + :returns: modification time + :rtype: datetime object + """ + ts = os.path.getmtime(path) + return datetime.datetime.fromtimestamp(ts) + + +def update_modification_ts(path): + """ + Sets modification time of a file to current time. + + :param path: the path to set ts to. + :type path: str + :returns: modification time + :rtype: datetime object + """ + os.utime(path, None) + return get_modification_ts(path) diff --git a/src/leap/bitmask/util/constants.py b/src/leap/bitmask/util/constants.py new file mode 100644 index 00000000..63f6b1f7 --- /dev/null +++ b/src/leap/bitmask/util/constants.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# constants.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 . + +SIGNUP_TIMEOUT = 5 +REQUEST_TIMEOUT = 10 diff --git a/src/leap/bitmask/util/keyring_helpers.py b/src/leap/bitmask/util/keyring_helpers.py new file mode 100644 index 00000000..8f354f28 --- /dev/null +++ b/src/leap/bitmask/util/keyring_helpers.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# keyring_helpers.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 . + +""" +Keyring helpers. +""" + +import keyring + +OBSOLETE_KEYRINGS = [ + keyring.backends.file.EncryptedKeyring, + keyring.backends.file.PlaintextKeyring +] + + +def has_keyring(): + """ + Returns whether we have an useful keyring to use. + + :rtype: bool + """ + kr = keyring.get_keyring() + return kr is not None and kr.__class__ not in OBSOLETE_KEYRINGS diff --git a/src/leap/bitmask/util/leap_argparse.py b/src/leap/bitmask/util/leap_argparse.py new file mode 100644 index 00000000..f60c4e10 --- /dev/null +++ b/src/leap/bitmask/util/leap_argparse.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# leap_argparse.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 . + +import argparse + +from leap.util import IS_RELEASE_VERSION + + +def build_parser(): + """ + All the options for the leap arg parser + Some of these could be switched on only if debug flag is present! + """ + epilog = "Copyright 2012 The LEAP Encryption Access Project" + parser = argparse.ArgumentParser(description=""" +Launches Bitmask""", epilog=epilog) + parser.add_argument('-d', '--debug', action="store_true", + help=("Launches Bitmask in debug mode, writing debug" + "info to stdout")) + # TODO: when we are ready to disable the --danger flag remove 'True or ' + if True or not IS_RELEASE_VERSION: + help_text = "Bypasses the certificate check for bootstrap" + parser.add_argument('--danger', action="store_true", help=help_text) + + parser.add_argument('-l', '--logfile', metavar="LOG FILE", nargs='?', + action="store", dest="log_file", + #type=argparse.FileType('w'), + help='optional log file') + parser.add_argument('--openvpn-verbosity', nargs='?', + type=int, + action="store", dest="openvpn_verb", + help='verbosity level for openvpn logs [1-6]') + parser.add_argument('-s', '--standalone', action="store_true", + help='Makes Bitmask use standalone' + 'directories for configuration and binary' + 'searching') + + # 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') + return parser + + +def init_leapc_args(): + parser = build_parser() + opts, unknown = parser.parse_known_args() + return parser, opts diff --git a/src/leap/bitmask/util/leap_log_handler.py b/src/leap/bitmask/util/leap_log_handler.py new file mode 100644 index 00000000..9adb21a5 --- /dev/null +++ b/src/leap/bitmask/util/leap_log_handler.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# leap_log_handler.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 . +""" +Custom handler for the logger window. +""" +import logging + +from PySide import QtCore + + +class LogHandler(logging.Handler): + """ + This is the custom handler that implements our desired formatting + and also keeps a history of all the logged events. + """ + + MESSAGE_KEY = 'message' + RECORD_KEY = 'record' + + def __init__(self, qtsignal): + """ + LogHander initialization. + Calls parent method and keeps a reference to the qtsignal + that will be used to fire the gui update. + """ + # TODO This is going to eat lots of memory after some time. + # Should be pruned at some moment. + self._log_history = [] + + logging.Handler.__init__(self) + self._qtsignal = qtsignal + + def _get_format(self, logging_level): + """ + Sets the log format depending on the parameter. + It uses html and css to set the colors for the logs. + + :param logging_level: the debug level to define the color. + :type logging_level: str. + """ + log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + formatter = logging.Formatter(log_format) + + return formatter + + def emit(self, logRecord): + """ + This method is fired every time that a record is logged by the + logging module. + This method reimplements logging.Handler.emit that is fired + in every logged message. + + :param logRecord: the record emitted by the logging module. + :type logRecord: logging.LogRecord. + """ + self.setFormatter(self._get_format(logRecord.levelname)) + log = self.format(logRecord) + log_item = {self.RECORD_KEY: logRecord, self.MESSAGE_KEY: log} + self._log_history.append(log_item) + self._qtsignal(log_item) + + +class HandlerAdapter(object): + """ + New style class that accesses all attributes from the LogHandler. + + Used as a workaround for a problem with multiple inheritance with Pyside + that surfaced under OSX with pyside 1.1.0. + """ + MESSAGE_KEY = 'message' + RECORD_KEY = 'record' + + def __init__(self, qtsignal): + self._handler = LogHandler(qtsignal=qtsignal) + + def setLevel(self, *args, **kwargs): + return self._handler.setLevel(*args, **kwargs) + + def handle(self, *args, **kwargs): + return self._handler.handle(*args, **kwargs) + + @property + def level(self): + return self._handler.level + + +class LeapLogHandler(QtCore.QObject, HandlerAdapter): + """ + Custom logging handler. It emits Qt signals so it can be plugged to a gui. + + Its inner handler also stores an history of logs that can be fetched after + having been connected to a gui. + """ + # All dicts returned are of the form + # {'record': LogRecord, 'message': str} + new_log = QtCore.Signal(dict) + + def __init__(self): + """ + LeapLogHandler initialization. + Initializes parent classes. + """ + QtCore.QObject.__init__(self) + HandlerAdapter.__init__(self, qtsignal=self.qtsignal) + + def qtsignal(self, log_item): + # WARNING: the new-style connection does NOT work because PySide + # translates the emit method to self.emit, and that collides with + # the emit method for logging.Handler + # self.new_log.emit(log_item) + QtCore.QObject.emit( + self, + QtCore.SIGNAL('new_log(PyObject)'), log_item) + + @property + def log_history(self): + """ + Returns the history of the logged messages. + """ + return self._handler._log_history diff --git a/src/leap/bitmask/util/privilege_policies.py b/src/leap/bitmask/util/privilege_policies.py new file mode 100644 index 00000000..72442553 --- /dev/null +++ b/src/leap/bitmask/util/privilege_policies.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# privilege_policies.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 . +""" +Helpers to determine if the needed policies for privilege escalation +are operative under this client run. +""" +import logging +import os +import platform + +from abc import ABCMeta, abstractmethod + +logger = logging.getLogger(__name__) + + +POLICY_TEMPLATE = """ + + + + LEAP Project + https://leap.se/ + + + Runs the openvpn binary + Ejecuta el binario openvpn + OpenVPN needs that you authenticate to start + + OpenVPN necesita autorizacion para comenzar + + package-x-generic + + yes + yes + yes + + {path} + true + + +""" + + +def is_missing_policy_permissions(): + """ + Returns True if we do not have implemented a policy checker for this + platform, or if the policy checker exists but it cannot find the + appropriate policy mechanisms in place. + + :rtype: bool + """ + _system = platform.system() + platform_checker = _system + "PolicyChecker" + policy_checker = globals().get(platform_checker, None) + if not policy_checker: + # it is true that we miss permission to escalate + # privileges without asking for password each time. + logger.debug("we could not find a policy checker implementation " + "for %s" % (_system,)) + return True + return policy_checker().is_missing_policy_permissions() + + +def get_policy_contents(openvpn_path): + """ + Returns the contents that the policy file should have. + + :param openvpn_path: the openvpn path to use in the polkit file + :type openvpn_path: str + :rtype: str + """ + return POLICY_TEMPLATE.format(path=openvpn_path) + + +def is_policy_outdated(path): + """ + Returns if the existing polkit file is outdated, comparing if the path + is correct. + + :param path: the path that should have the polkit file. + :type path: str. + :rtype: bool + """ + _system = platform.system() + platform_checker = _system + "PolicyChecker" + policy_checker = globals().get(platform_checker, None) + if policy_checker is None: + logger.debug("we could not find a policy checker implementation " + "for %s" % (_system,)) + return False + return policy_checker().is_outdated(path) + + +class PolicyChecker: + """ + Abstract PolicyChecker class + """ + + __metaclass__ = ABCMeta + + @abstractmethod + def is_missing_policy_permissions(self): + """ + Returns True if we could not find any policy mechanisms that + are defined to be in used for this particular platform. + + :rtype: bool + """ + return True + + +class LinuxPolicyChecker(PolicyChecker): + """ + PolicyChecker for Linux + """ + LINUX_POLKIT_FILE = ("/usr/share/polkit-1/actions/" + "net.openvpn.gui.leap.policy") + + @classmethod + def get_polkit_path(self): + """ + Returns the polkit file path. + + :rtype: str + """ + return self.LINUX_POLKIT_FILE + + def is_missing_policy_permissions(self): + """ + Returns True if we could not find the appropriate policykit file + in place + + :rtype: bool + """ + return not os.path.isfile(self.LINUX_POLKIT_FILE) + + def is_outdated(self, path): + """ + Returns if the existing polkit file is outdated, comparing if the path + is correct. + + :param path: the path that should have the polkit file. + :type path: str. + :rtype: bool + """ + polkit = None + try: + with open(self.LINUX_POLKIT_FILE) as f: + polkit = f.read() + except IOError, e: + logger.error("Error reading polkit file(%s): %r" % ( + self.LINUX_POLKIT_FILE, e)) + + return get_policy_contents(path) != polkit diff --git a/src/leap/bitmask/util/pyside_tests_helper.py b/src/leap/bitmask/util/pyside_tests_helper.py new file mode 100644 index 00000000..5c0eb8d6 --- /dev/null +++ b/src/leap/bitmask/util/pyside_tests_helper.py @@ -0,0 +1,136 @@ + +'''Helper classes and functions''' + +import os +import unittest + +from random import randint + +from PySide.QtCore import QCoreApplication, QTimer + +try: + from PySide.QtGui import QApplication +except ImportError: + has_gui = False +else: + has_gui = True + + +def adjust_filename(filename, orig_mod_filename): + dirpath = os.path.dirname(os.path.abspath(orig_mod_filename)) + return os.path.join(dirpath, filename) + + +class NoQtGuiError(Exception): + def __init__(self): + Exception.__init__(self, 'No QtGui found') + + +class BasicPySlotCase(object): + '''Base class that tests python slots and signal emissions. + + Python slots are defined as any callable passed to QObject.connect(). + ''' + def setUp(self): + self.called = False + + def tearDown(self): + try: + del self.args + except: + pass + + def cb(self, *args): + '''Simple callback with arbitrary arguments. + + The test function must setup the 'args' attribute with a sequence + containing the arguments expected to be received by this slot. + Currently only a single connection is supported. + ''' + if tuple(self.args) == args: + self.called = True + else: + raise ValueError('Invalid arguments for callback') + + +_instance = None +_timed_instance = None + +if has_gui: + class UsesQApplication(unittest.TestCase): + '''Helper class to provide QApplication instances''' + + qapplication = True + + def setUp(self): + '''Creates the QApplication instance''' + + # Simple way of making instance a singleton + super(UsesQApplication, self).setUp() + global _instance + if _instance is None: + _instance = QApplication([]) + + self.app = _instance + + def tearDown(self): + '''Deletes the reference owned by self''' + del self.app + super(UsesQApplication, self).tearDown() + + class TimedQApplication(unittest.TestCase): + '''Helper class with timed QApplication exec loop''' + + def setUp(self, timeout=100): + '''Setups this Application. + + timeout - timeout in milisseconds''' + global _timed_instance + if _timed_instance is None: + _timed_instance = QApplication([]) + + self.app = _timed_instance + QTimer.singleShot(timeout, self.app.quit) + + def tearDown(self): + '''Delete resources''' + del self.app +else: + class UsesQApplication(unittest.TestCase): + def setUp(self): + raise NoQtGuiError() + + class TimedQapplication(unittest.TestCase): + def setUp(self): + raise NoQtGuiError() + +_core_instance = None + + +class UsesQCoreApplication(unittest.TestCase): + '''Helper class for test cases that require an QCoreApplication + Just connect or call self.exit_app_cb. When called, will ask + self.app to exit. + ''' + + def setUp(self): + '''Set up resources''' + + global _core_instance + if _core_instance is None: + _core_instance = QCoreApplication([]) + + self.app = _core_instance + + def tearDown(self): + '''Release resources''' + del self.app + + def exit_app_cb(self): + '''Quits the application''' + self.app.exit(0) + + +def random_string(size=5): + '''Generate random string with the given size''' + return ''.join(map(chr, [randint(33, 126) for x in range(size)])) diff --git a/src/leap/bitmask/util/reqs.txt b/src/leap/bitmask/util/reqs.txt new file mode 100644 index 00000000..0bcf85dc --- /dev/null +++ b/src/leap/bitmask/util/reqs.txt @@ -0,0 +1,14 @@ +requests +srp>=1.0.2 +pyopenssl +keyring +python-dateutil +psutil +ipaddr +twisted +qt4reactor +python-gnupg +leap.common>=0.2.5 +leap.soledad>=0.1.0 +mock +oauth \ No newline at end of file diff --git a/src/leap/bitmask/util/request_helpers.py b/src/leap/bitmask/util/request_helpers.py new file mode 100644 index 00000000..74aaa06b --- /dev/null +++ b/src/leap/bitmask/util/request_helpers.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# request_helpers.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 . + +""" +Request helpers for backward compatible "parsing" of requests +""" + +import time + +import json + +from dateutil import parser as dateparser + + +def get_content(request): + """ + Returns the content by trying to get it from the json + property/function or from content, in that order. + Also returns the mtime for that content if available + + :param request: request as it is given by requests + :type request: Response + + :rtype: tuple (contents, mtime) + """ + + contents = "" + mtime = None + + if request and request.content and request.json: + if callable(request.json): + contents = json.dumps(request.json()) + else: + contents = json.dumps(request.json) + else: + contents = request.content + + mtime = None + last_modified = request.headers.get('last-modified', None) + if last_modified: + dt = dateparser.parse(unicode(last_modified)) + mtime = int(time.mktime(dt.timetuple()) + dt.microsecond / 1000000.0) + + return contents, mtime diff --git a/src/leap/bitmask/util/requirement_checker.py b/src/leap/bitmask/util/requirement_checker.py new file mode 100644 index 00000000..1d9b9923 --- /dev/null +++ b/src/leap/bitmask/util/requirement_checker.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# requirement_checker.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 . + +""" +Utility to check the needed requirements. +""" + +import os +import logging + +from pkg_resources import (DistributionNotFound, + get_distribution, + Requirement, + resource_stream, + VersionConflict) + +logger = logging.getLogger(__name__) + + +def get_requirements(): + """ + This function returns a list with requirements. + It checks either if its running from the source or if its installed. + + :returns: a list with packages names, required for the app. + :return type: list of str. + """ + develop = True + requirements = [] + + try: + # if we are running from the source + from pkg import util + requirements = util.parse_requirements() + except ImportError: + develop = False + + # if we are running from the package + if not develop: + requires_file_name = os.path.join('leap', 'util', 'reqs.txt') + dist_name = Requirement.parse('leap-client') + + try: + with resource_stream(dist_name, requires_file_name) as stream: + requirements = [line.strip() for line in stream] + except Exception, e: + logger.error("Requirements file not found. %r" % (e, )) + + return requirements + + +def check_requirements(): + """ + This function check the dependencies declared in the + requirement(s) file(s) and logs the results. + """ + logger.debug("Checking requirements...") + requirements = get_requirements() + + for package in requirements: + try: + get_distribution(package) + except VersionConflict: + required_package = Requirement.parse(package) + required_version = required_package.specs[0] + required_name = required_package.key + + installed_package = get_distribution(required_name) + installed_version = installed_package.version + installed_location = installed_package.location + + msg = "Error: version not satisfied. " + msg += "Expected %s, installed %s (path: %s)." % ( + required_version, installed_version, installed_location) + + result = "%s ... %s" % (package, msg) + logger.error(result) + except DistributionNotFound: + msg = "Error: package not found!" + result = "%s ... %s" % (package, msg) + logger.error(result) + else: + msg = "OK" + result = "%s ... %s" % (package, msg) + logger.debug(result) + + logger.debug('Done') diff --git a/src/leap/bitmask/util/streamtologger.py b/src/leap/bitmask/util/streamtologger.py new file mode 100644 index 00000000..25a06718 --- /dev/null +++ b/src/leap/bitmask/util/streamtologger.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# streamtologger.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 . +""" +Stream object that redirects writes to a logger instance. +""" +import logging + + +class StreamToLogger(object): + """ + Fake file-like stream object that redirects writes to a logger instance. + + Credits to: + http://www.electricmonk.nl/log/2011/08/14/\ + redirect-stdout-and-stderr-to-a-logger-in-python/ + """ + def __init__(self, logger, log_level=logging.INFO): + """ + Constructor, defines the logger and level to use to log messages. + + :param logger: logger object to log messages. + :type logger: logging.Handler + :param log_level: the level to use to log messages through the logger. + :type log_level: int + look at logging-levels in 'logging' docs. + """ + self._logger = logger + self._log_level = log_level + + def write(self, data): + """ + Simulates the 'write' method in a file object. + It writes the data receibed in buf to the logger 'self._logger'. + + :param data: data to write to the 'file' + :type data: str + """ + for line in data.rstrip().splitlines(): + self._logger.log(self._log_level, line.rstrip()) + + def flush(self): + """ + Dummy method. Needed to replace the twisted.log output. + """ + pass diff --git a/src/leap/bitmask/util/tests/__init__.py b/src/leap/bitmask/util/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/util/tests/test_is_release_version.py b/src/leap/bitmask/util/tests/test_is_release_version.py new file mode 100644 index 00000000..4199f603 --- /dev/null +++ b/src/leap/bitmask/util/tests/test_is_release_version.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# test_is_release_version.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 . +""" +tests for _is_release_version function +""" + +import unittest +from leap.common.testing.basetest import BaseLeapTest +from leap.util import _is_release_version as is_release_version + + +class TestIsReleaseVersion(BaseLeapTest): + """Tests for release version check.""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_git_version(self): + version = '0.2.3-12-ge5b50a1' + self.assertFalse(is_release_version(version)) + + def test_release(self): + version = '0.2.4' + self.assertTrue(is_release_version(version)) + + def test_release_candidate(self): + version = '0.2.4-rc1' + self.assertFalse(is_release_version(version)) + + def test_complex_version(self): + version = '12.5.2.4-rc12.dev.alpha1' + self.assertFalse(is_release_version(version)) + + def test_super_high_version(self): + version = '12.5.2.4.45' + self.assertTrue(is_release_version(version)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/src/leap/bitmask/util/tests/test_leap_log_handler.py b/src/leap/bitmask/util/tests/test_leap_log_handler.py new file mode 100644 index 00000000..ea509ea8 --- /dev/null +++ b/src/leap/bitmask/util/tests/test_leap_log_handler.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# test_leap_log_handler.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 . +""" +tests for leap_log_handler +""" + +import unittest + +import logging + +from leap.util.leap_log_handler import LeapLogHandler +from leap.common.testing.basetest import BaseLeapTest +from leap.util.pyside_tests_helper import BasicPySlotCase + +from mock import Mock + + +class LeapLogHandlerTest(BaseLeapTest, BasicPySlotCase): + """ + LeapLogHandlerTest's tests. + """ + def _callback(self, *args): + """ + Simple callback to track if a signal was emitted. + """ + self.called = True + self.emitted_msg = args[0][LeapLogHandler.MESSAGE_KEY] + + def setUp(self): + BasicPySlotCase.setUp(self) + + # Create the logger + level = logging.DEBUG + self.logger = logging.getLogger(name='test') + self.logger.setLevel(level) + + # Create the handler + self.leap_handler = LeapLogHandler() + self.leap_handler.setLevel(level) + self.logger.addHandler(self.leap_handler) + + def tearDown(self): + BasicPySlotCase.tearDown(self) + try: + self.leap_handler.new_log.disconnect() + except Exception: + pass + + def test_history_starts_empty(self): + self.assertEqual(self.leap_handler.log_history, []) + + def test_one_log_captured(self): + self.logger.debug('test') + self.assertEqual(len(self.leap_handler.log_history), 1) + + def test_history_records_order(self): + self.logger.debug('test 01') + self.logger.debug('test 02') + self.logger.debug('test 03') + + logs = [] + for message in self.leap_handler.log_history: + logs.append(message[LeapLogHandler.RECORD_KEY].msg) + + self.assertIn('test 01', logs) + self.assertIn('test 02', logs) + self.assertIn('test 03', logs) + + def test_history_messages_order(self): + self.logger.debug('test 01') + self.logger.debug('test 02') + self.logger.debug('test 03') + + logs = [] + for message in self.leap_handler.log_history: + logs.append(message[LeapLogHandler.MESSAGE_KEY]) + + self.assertIn('test 01', logs[0]) + self.assertIn('test 02', logs[1]) + self.assertIn('test 03', logs[2]) + + def test_emits_signal(self): + log_format = '%(name)s - %(levelname)s - %(message)s' + formatter = logging.Formatter(log_format) + get_format = Mock(return_value=formatter) + self.leap_handler._handler._get_format = get_format + + self.leap_handler.new_log.connect(self._callback) + self.logger.debug('test') + + expected_log_msg = "test - DEBUG - test" + + # signal emitted + self.assertTrue(self.called) + + # emitted message + self.assertEqual(self.emitted_msg, expected_log_msg) + + # Mock called + self.assertTrue(get_format.called) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/leap/bitmask/util/tests/test_streamtologger.py b/src/leap/bitmask/util/tests/test_streamtologger.py new file mode 100644 index 00000000..4c98e562 --- /dev/null +++ b/src/leap/bitmask/util/tests/test_streamtologger.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +# test_streamtologger.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 . +""" +tests for streamtologger +""" + +try: + import unittest2 as unittest +except ImportError: + import unittest + +import logging +import sys + +from leap.util.streamtologger import StreamToLogger +from leap.common.testing.basetest import BaseLeapTest + + +class SimpleLogHandler(logging.Handler): + """ + The simplest log handler that allows to check if the log was + delivered to the handler correctly. + """ + def __init__(self): + logging.Handler.__init__(self) + self._last_log = "" + self._last_log_level = "" + + def emit(self, record): + self._last_log = record.getMessage() + self._last_log_level = record.levelno + + def get_last_log(self): + """ + Returns the last logged message by this handler. + + :return: the last logged message. + :rtype: str + """ + return self._last_log + + def get_last_log_level(self): + """ + Returns the level of the last logged message by this handler. + + :return: the last logged level. + :rtype: str + """ + return self._last_log_level + + +class StreamToLoggerTest(BaseLeapTest): + """ + StreamToLogger's tests. + + NOTE: we may need to find a way to test the use case that an exception + is raised. I couldn't catch the output of an exception because the + test failed if some exception is raised. + """ + def setUp(self): + # Create the logger + level = logging.DEBUG + self.logger = logging.getLogger(name='test') + self.logger.setLevel(level) + + # Simple log handler + self.handler = SimpleLogHandler() + self.logger.addHandler(self.handler) + + # Preserve original values + self._sys_stdout = sys.stdout + self._sys_stderr = sys.stderr + + # Create the handler + sys.stdout = StreamToLogger(self.logger, logging.DEBUG) + sys.stderr = StreamToLogger(self.logger, logging.ERROR) + + def tearDown(self): + # Restore original values + sys.stdout = self._sys_stdout + sys.stderr = self._sys_stderr + + def test_logger_starts_empty(self): + self.assertEqual(self.handler.get_last_log(), '') + + def test_standard_output(self): + message = 'Test string' + print message + + log = self.handler.get_last_log() + log_level = self.handler.get_last_log_level() + + self.assertEqual(log, message) + self.assertEqual(log_level, logging.DEBUG) + + def test_standard_error(self): + message = 'Test string' + sys.stderr.write(message) + + log_level = self.handler.get_last_log_level() + log = self.handler.get_last_log() + + self.assertEqual(log, message) + self.assertEqual(log_level, logging.ERROR) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/src/leap/config/__init__.py b/src/leap/config/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/config/leapsettings.py b/src/leap/config/leapsettings.py deleted file mode 100644 index 35010280..00000000 --- a/src/leap/config/leapsettings.py +++ /dev/null @@ -1,253 +0,0 @@ -# -*- coding: utf-8 -*- -# leapsettings.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 . - -""" -QSettings abstraction -""" -import os -import logging - -from PySide import QtCore - -from leap.common.check import leap_assert, leap_assert_type -from leap.common.config.prefixers import get_platform_prefixer - -logger = logging.getLogger(__name__) - - -def to_bool(val): - """ - Returns the boolean value corresponding to val. Will return False - in case val is not a string or something that behaves like one. - - :param val: value to cast - :type val: either bool already or str - - :rtype: bool - """ - if isinstance(val, bool): - return val - - bool_val = False - try: - bool_val = val.lower() == "true" - except: - pass - - return bool_val - - -class LeapSettings(object): - """ - Leap client QSettings wrapper - """ - - CONFIG_NAME = "leap.conf" - - # keys - GEOMETRY_KEY = "Geometry" - WINDOWSTATE_KEY = "WindowState" - USER_KEY = "User" - PROPERPROVIDER_KEY = "ProperProvider" - REMEMBER_KEY = "RememberUserAndPass" - DEFAULTPROVIDER_KEY = "DefaultProvider" - ALERTMISSING_KEY = "AlertMissingScripts" - - def __init__(self, standalone=False): - """ - Constructor - - :param standalone: parameter used to define the location of - the config - :type standalone: bool - """ - - settings_path = os.path.join(get_platform_prefixer() - .get_path_prefix(standalone=standalone), - "leap", - self.CONFIG_NAME) - self._settings = QtCore.QSettings(settings_path, - QtCore.QSettings.IniFormat) - - def get_geometry(self): - """ - Returns the saved geometry or None if it wasn't saved - - :rtype: bytearray or None - """ - return self._settings.value(self.GEOMETRY_KEY, None) - - def set_geometry(self, geometry): - """ - Saves the geometry to the settings - - :param geometry: bytearray representing the geometry - :type geometry: bytearray - """ - leap_assert(geometry, "We need a geometry") - self._settings.setValue(self.GEOMETRY_KEY, geometry) - - def get_windowstate(self): - """ - Returns the window state or None if it wasn't saved - - :rtype: bytearray or None - """ - return self._settings.value(self.WINDOWSTATE_KEY, None) - - def set_windowstate(self, windowstate): - """ - Saves the window state to the settings - - :param windowstate: bytearray representing the window state - :type windowstate: bytearray - """ - leap_assert(windowstate, "We need a window state") - self._settings.setValue(self.WINDOWSTATE_KEY, windowstate) - - def get_enabled_services(self, provider): - """ - Returns a list of enabled services for the given provider - - :param provider: provider domain - :type provider: str - - :rtype: list of str - """ - - leap_assert(len(provider) > 0, "We need a nonempty provider") - enabled_services = self._settings.value("%s/Services" % (provider,), - []) - if isinstance(enabled_services, (str, unicode)): - enabled_services = enabled_services.split(",") - - return enabled_services - - def set_enabled_services(self, provider, services): - """ - Saves the list of enabled services for the given provider - - :param provider: provider domain - :type provider: str - - :param services: list of services to save - :type services: list of str - """ - - leap_assert(len(provider) > 0, "We need a nonempty provider") - leap_assert_type(services, list) - - self._settings.setValue("%s/Services" % (provider,), - services) - - def get_user(self): - """ - Returns the configured user to remember, None if there isn't one - - :rtype: str or None - """ - return self._settings.value(self.USER_KEY, None) - - def set_user(self, user): - """ - Saves the user to remember - - :param user: user name to remember - :type user: str - """ - leap_assert(len(user) > 0, "We cannot save an empty user") - self._settings.setValue(self.USER_KEY, user) - - def get_remember(self): - """ - Returns the value of the remember selection. - - :rtype: bool - """ - return to_bool(self._settings.value(self.REMEMBER_KEY, False)) - - def set_remember(self, remember): - """ - Sets wheter the app should remember username and password - - :param remember: True if the app should remember username and - password, False otherwise - :rtype: bool - """ - leap_assert_type(remember, bool) - self._settings.setValue(self.REMEMBER_KEY, remember) - - # TODO: make this scale with multiple providers, we are assuming - # just one for now - def get_properprovider(self): - """ - Returns True if there is a properly configured provider. - - .. note:: this assumes only one provider for now. - - :rtype: bool - """ - return to_bool(self._settings.value(self.PROPERPROVIDER_KEY, False)) - - def set_properprovider(self, properprovider): - """ - Sets whether the app should automatically login. - - :param properprovider: True if the provider is properly configured, - False otherwise. - :type properprovider: bool - """ - leap_assert_type(properprovider, bool) - self._settings.setValue(self.PROPERPROVIDER_KEY, properprovider) - - def get_defaultprovider(self): - """ - Returns the default provider to be used for autostarting EIP - - :rtype: str or None - """ - return self._settings.value(self.DEFAULTPROVIDER_KEY, None) - - def set_defaultprovider(self, provider): - """ - Sets the default provider to be used for autostarting EIP - - :param provider: provider to use - :type provider: str or None - """ - if provider is None: - self._settings.remove(self.DEFAULTPROVIDER_KEY) - else: - self._settings.setValue(self.DEFAULTPROVIDER_KEY, provider) - - def get_alert_missing_scripts(self): - """ - Returns the setting for alerting of missing up/down scripts. - - :rtype: bool - """ - return to_bool(self._settings.value(self.ALERTMISSING_KEY, True)) - - def set_alert_missing_scripts(self, value): - """ - Sets the setting for alerting of missing up/down scripts. - - :param value: the value to set - :type value: bool - """ - leap_assert_type(value, bool) - self._settings.setValue(self.ALERTMISSING_KEY, value) diff --git a/src/leap/config/provider_spec.py b/src/leap/config/provider_spec.py deleted file mode 100644 index cf942c7b..00000000 --- a/src/leap/config/provider_spec.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- coding: utf-8 -*- -# provider_spec.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 . - -leap_provider_spec = { - 'description': 'provider definition', - 'type': 'object', - 'properties': { - 'version': { - 'type': unicode, - 'default': '0.1.0' - }, - "default_language": { - 'type': unicode, - 'default': 'en' - }, - 'domain': { - 'type': unicode, # XXX define uri type - 'default': 'testprovider.example.org' - }, - 'name': { - 'type': dict, - 'format': 'translatable', - 'default': {u'en': u'Test Provider'} - }, - 'description': { - #'type': LEAPTranslatable, - 'type': dict, - 'format': 'translatable', - 'default': {u'en': u'Test provider'} - }, - 'enrollment_policy': { - 'type': unicode, # oneof ?? - 'default': 'open' - }, - 'services': { - 'type': list, # oneof ?? - 'default': ['eip'] - }, - 'api_version': { - 'type': unicode, - 'default': '0.1.0' # version regexp - }, - 'api_uri': { - 'type': unicode # uri - }, - 'public_key': { - 'type': unicode # fingerprint - }, - 'ca_cert_fingerprint': { - 'type': unicode, - }, - 'ca_cert_uri': { - 'type': unicode, - 'format': 'https-uri' - }, - 'languages': { - 'type': list, - 'default': ['en'] - }, - 'service': { - 'levels': { - 'type': list - }, - 'default_service_level': { - 'type': int, - 'default': 1 - }, - 'allow_free': { - 'type': unicode - }, - 'allow_paid': { - 'type': unicode - }, - 'allow_anonymous': { - 'type': unicode - }, - 'allow_registration': { - 'type': unicode - }, - 'bandwidth_limit': { - 'type': int - }, - 'allow_limited_bandwidth': { - 'type': unicode - }, - 'allow_unlimited_bandwidth': { - 'type': unicode - } - } - } -} diff --git a/src/leap/config/providerconfig.py b/src/leap/config/providerconfig.py deleted file mode 100644 index f899b17c..00000000 --- a/src/leap/config/providerconfig.py +++ /dev/null @@ -1,218 +0,0 @@ -# -*- coding: utf-8 -*- -# providerconfig.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 . - -""" -Provider configuration -""" -import logging -import os - -from leap.common.check import leap_check -from leap.common.config.baseconfig import BaseConfig, LocalizedKey -from leap.config.provider_spec import leap_provider_spec - -logger = logging.getLogger(__name__) - - -class MissingCACert(Exception): - """ - Raised when a CA certificate is needed but not found. - """ - pass - - -class ProviderConfig(BaseConfig): - """ - Provider configuration abstraction class - """ - def __init__(self): - BaseConfig.__init__(self) - - def _get_schema(self): - """ - Returns the schema corresponding to the version given. - - :rtype: dict or None if the version is not supported. - """ - return leap_provider_spec - - def _get_spec(self): - """ - Returns the spec object for the specific configuration. - - Override the BaseConfig one because we do not support multiple schemas - for the provider yet. - - :rtype: dict or None if the version is not supported. - """ - return self._get_schema() - - def get_api_uri(self): - return self._safe_get_value("api_uri") - - def get_api_version(self): - return self._safe_get_value("api_version") - - def get_ca_cert_fingerprint(self): - return self._safe_get_value("ca_cert_fingerprint") - - def get_ca_cert_uri(self): - return self._safe_get_value("ca_cert_uri") - - def get_default_language(self): - return self._safe_get_value("default_language") - - @LocalizedKey - def get_description(self): - return self._safe_get_value("description") - - @classmethod - def sanitize_path_component(cls, component): - """ - If the provider tries to instrument the component of a path - that is controlled by them, this will take care of - removing/escaping all the necessary elements. - - :param component: Path component to process - :type component: unicode or str - - :returns: The path component properly escaped - :rtype: unicode or str - """ - # TODO: Fix for windows, names like "aux" or "con" aren't - # allowed. - return component.replace(os.path.sep, "") - - def get_domain(self): - return ProviderConfig.sanitize_path_component( - self._safe_get_value("domain")) - - def get_enrollment_policy(self): - """ - Returns the enrollment policy - - :rtype: string - """ - return self._safe_get_value("enrollment_policy") - - def get_languages(self): - return self._safe_get_value("languages") - - @LocalizedKey - def get_name(self): - return self._safe_get_value("name") - - def get_services(self): - """ - Returns a list with the available services in the current provider. - - :rtype: list - """ - services = self._safe_get_value("services") - return services - - def get_services_string(self): - """ - Returns a string with the available services in the current - provider, ready to be shown to the user. - """ - services_str = ", ".join(self.get_services()) - services_str = services_str.replace( - "openvpn", "Encrypted Internet") - return services_str - - def get_ca_cert_path(self, about_to_download=False): - """ - Returns the path to the certificate for the current provider. - It may raise MissingCACert if - the certificate does not exists and not about_to_download - - :param about_to_download: defines wether we want the path to - download the cert or not. This helps avoid - checking if the cert exists because we - are about to write it. - :type about_to_download: bool - """ - - cert_path = os.path.join(self.get_path_prefix(), - "leap", - "providers", - self.get_domain(), - "keys", - "ca", - "cacert.pem") - - if not about_to_download: - cert_exists = os.path.exists(cert_path) - error_msg = "You need to download the certificate first" - leap_check(cert_exists, error_msg, MissingCACert) - logger.debug("Going to verify SSL against %s" % (cert_path,)) - - return cert_path - - def provides_eip(self): - """ - Returns True if this particular provider has the EIP service, - False otherwise. - - :rtype: bool - """ - return "openvpn" in self.get_services() - - def provides_mx(self): - """ - Returns True if this particular provider has the MX service, - False otherwise. - - :rtype: bool - """ - return "mx" in self.get_services() - - -if __name__ == "__main__": - logger = logging.getLogger(name='leap') - logger.setLevel(logging.DEBUG) - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - provider = ProviderConfig() - - try: - provider.get_api_version() - except Exception as e: - assert isinstance(e, AssertionError), "Expected an assert" - print "Safe value getting is working" - - # standalone minitest - #if provider.load("provider_bad.json"): - if provider.load("leap/providers/bitmask.net/provider.json"): - print provider.get_api_version() - print provider.get_ca_cert_fingerprint() - print provider.get_ca_cert_uri() - print provider.get_default_language() - print provider.get_description() - print provider.get_description(lang="asd") - print provider.get_domain() - print provider.get_enrollment_policy() - print provider.get_languages() - print provider.get_name() - print provider.get_services() diff --git a/src/leap/config/tests/test_providerconfig.py b/src/leap/config/tests/test_providerconfig.py deleted file mode 100644 index ff2828e6..00000000 --- a/src/leap/config/tests/test_providerconfig.py +++ /dev/null @@ -1,279 +0,0 @@ -# -*- coding: utf-8 -*- -# test_providerconfig.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 . -""" -Tests for providerconfig -""" - -try: - import unittest2 as unittest -except ImportError: - import unittest - -import os -import json -import copy - -from leap.common.testing.basetest import BaseLeapTest -from leap.config.providerconfig import ProviderConfig, MissingCACert -from leap.services import get_supported - -from mock import Mock - - -sample_config = { - "api_uri": "https://api.test.bitmask.net:4430", - "api_version": "1", - "ca_cert_fingerprint": - "SHA256: 0f17c033115f6b76ff67871872303ff65034efe7dd1b910062ca323eb4da5c7e", - "ca_cert_uri": "https://test.bitmask.net/ca.crt", - "default_language": "en", - "description": { - "en": "Test description for provider", - "es": "Descripcion de prueba para el proveedor" - }, - "domain": "test.bitmask.net", - "enrollment_policy": "open", - "languages": [ - "en", - "es" - ], - "name": { - "en": "Bitmask testing environment", - "es": "Entorno de pruebas de Bitmask" - }, - "service": { - "allow_anonymous": True, - "allow_free": True, - "allow_limited_bandwidth": True, - "allow_paid": False, - "allow_registration": True, - "allow_unlimited_bandwidth": False, - "bandwidth_limit": 400000, - "default_service_level": 1, - "levels": [ - { - "bandwidth": "limited", - "id": 1, - "name": "anonymous" - }, - { - "bandwidth": "limited", - "id": 2, - "name": "free", - "storage": 50 - } - ] - }, - "services": [ - "openvpn" - ] -} - - -class ProviderConfigTest(BaseLeapTest): - """Tests for ProviderConfig""" - - def setUp(self): - self._provider_config = ProviderConfig() - json_string = json.dumps(sample_config) - self._provider_config.load(data=json_string) - - # At certain points we are going to be replacing these method - # to avoid creating a file. - # We need to save the old implementation and restore it in - # tearDown so we are sure everything is as expected for each - # test. If we do it inside each specific test, a failure in - # the test will leave the implementation with the mock. - self._old_ospath_exists = os.path.exists - - def tearDown(self): - os.path.exists = self._old_ospath_exists - - def test_configs_ok(self): - """ - Test if the configs loads ok - """ - # TODO: this test should go to the BaseConfig tests - pc = self._provider_config - self.assertEqual(pc.get_api_uri(), sample_config['api_uri']) - self.assertEqual(pc.get_api_version(), sample_config['api_version']) - self.assertEqual(pc.get_ca_cert_fingerprint(), - sample_config['ca_cert_fingerprint']) - self.assertEqual(pc.get_ca_cert_uri(), sample_config['ca_cert_uri']) - self.assertEqual(pc.get_default_language(), - sample_config['default_language']) - - self.assertEqual(pc.get_domain(), sample_config['domain']) - self.assertEqual(pc.get_enrollment_policy(), - sample_config['enrollment_policy']) - self.assertEqual(pc.get_languages(), sample_config['languages']) - - def test_localizations(self): - pc = self._provider_config - - self.assertEqual(pc.get_description(lang='en'), - sample_config['description']['en']) - self.assertEqual(pc.get_description(lang='es'), - sample_config['description']['es']) - - self.assertEqual(pc.get_name(lang='en'), sample_config['name']['en']) - self.assertEqual(pc.get_name(lang='es'), sample_config['name']['es']) - - def _localize(self, lang): - """ - Helper to change default language of the provider config. - """ - pc = self._provider_config - config = copy.deepcopy(sample_config) - config['default_language'] = lang - json_string = json.dumps(config) - pc.load(data=json_string) - - return config - - def test_default_localization1(self): - pc = self._provider_config - config = self._localize(sample_config['languages'][0]) - - default_language = config['default_language'] - default_description = config['description'][default_language] - default_name = config['name'][default_language] - - self.assertEqual(pc.get_description(lang='xx'), default_description) - self.assertEqual(pc.get_description(), default_description) - - self.assertEqual(pc.get_name(lang='xx'), default_name) - self.assertEqual(pc.get_name(), default_name) - - def test_default_localization2(self): - pc = self._provider_config - config = self._localize(sample_config['languages'][1]) - - default_language = config['default_language'] - default_description = config['description'][default_language] - default_name = config['name'][default_language] - - self.assertEqual(pc.get_description(lang='xx'), default_description) - self.assertEqual(pc.get_description(), default_description) - - self.assertEqual(pc.get_name(lang='xx'), default_name) - self.assertEqual(pc.get_name(), default_name) - - def test_get_ca_cert_path_as_expected(self): - pc = self._provider_config - pc.get_path_prefix = Mock(return_value='test') - - provider_domain = sample_config['domain'] - expected_path = os.path.join('test', 'leap', 'providers', - provider_domain, 'keys', 'ca', - 'cacert.pem') - - # mock 'os.path.exists' so we don't get an error for unexisting file - os.path.exists = Mock(return_value=True) - cert_path = pc.get_ca_cert_path() - - self.assertEqual(cert_path, expected_path) - - def test_get_ca_cert_path_about_to_download(self): - pc = self._provider_config - pc.get_path_prefix = Mock(return_value='test') - - provider_domain = sample_config['domain'] - expected_path = os.path.join('test', 'leap', 'providers', - provider_domain, 'keys', 'ca', - 'cacert.pem') - - cert_path = pc.get_ca_cert_path(about_to_download=True) - - self.assertEqual(cert_path, expected_path) - - def test_get_ca_cert_path_fails(self): - pc = self._provider_config - pc.get_path_prefix = Mock(return_value='test') - - # mock 'get_domain' so we don't need to load a config - provider_domain = 'test.provider.com' - pc.get_domain = Mock(return_value=provider_domain) - - with self.assertRaises(MissingCACert): - pc.get_ca_cert_path() - - def test_provides_eip(self): - pc = self._provider_config - config = copy.deepcopy(sample_config) - - # It provides - config['services'] = ['openvpn', 'test_service'] - json_string = json.dumps(config) - pc.load(data=json_string) - self.assertTrue(pc.provides_eip()) - - # It does not provides - config['services'] = ['test_service', 'other_service'] - json_string = json.dumps(config) - pc.load(data=json_string) - self.assertFalse(pc.provides_eip()) - - def test_provides_mx(self): - pc = self._provider_config - config = copy.deepcopy(sample_config) - - # It provides - config['services'] = ['mx', 'other_service'] - json_string = json.dumps(config) - pc.load(data=json_string) - self.assertTrue(pc.provides_mx()) - - # It does not provides - config['services'] = ['test_service', 'other_service'] - json_string = json.dumps(config) - pc.load(data=json_string) - self.assertFalse(pc.provides_mx()) - - def test_supports_unknown_service(self): - pc = self._provider_config - config = copy.deepcopy(sample_config) - - config['services'] = ['unknown'] - json_string = json.dumps(config) - pc.load(data=json_string) - self.assertFalse('unknown' in get_supported(pc.get_services())) - - def test_provides_unknown_service(self): - pc = self._provider_config - config = copy.deepcopy(sample_config) - - config['services'] = ['unknown'] - json_string = json.dumps(config) - pc.load(data=json_string) - self.assertTrue('unknown' in pc.get_services()) - - def test_get_services_string(self): - pc = self._provider_config - config = copy.deepcopy(sample_config) - config['services'] = [ - 'openvpn', 'asdf', 'openvpn', 'not_supported_service'] - json_string = json.dumps(config) - pc.load(data=json_string) - - self.assertEqual(pc.get_services_string(), - "Encrypted Internet, asdf, Encrypted Internet," - " not_supported_service") - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/crypto/__init__.py b/src/leap/crypto/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py deleted file mode 100644 index fc0533fc..00000000 --- a/src/leap/crypto/srpauth.py +++ /dev/null @@ -1,606 +0,0 @@ -# -*- coding: utf-8 -*- -# srpauth.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 . - -import binascii -import logging - -import requests -import srp -import json - -#this error is raised from requests -from simplejson.decoder import JSONDecodeError -from functools import partial - -from PySide import QtCore -from twisted.internet import threads - -from leap.common.check import leap_assert -from leap.util.constants import REQUEST_TIMEOUT -from leap.util import request_helpers as reqhelper -from leap.common.events import signal as events_signal -from leap.common.events import events_pb2 as proto - -logger = logging.getLogger(__name__) - - -class SRPAuthenticationError(Exception): - """ - Exception raised for authentication errors - """ - pass - - -class SRPAuthConnectionError(SRPAuthenticationError): - """ - Exception raised when there's a connection error - """ - pass - - -class SRPAuthUnknownUser(SRPAuthenticationError): - """ - Exception raised when trying to authenticate an unknown user - """ - pass - - -class SRPAuthBadStatusCode(SRPAuthenticationError): - """ - Exception raised when we received an unknown bad status code - """ - pass - - -class SRPAuthNoSalt(SRPAuthenticationError): - """ - Exception raised when we don't receive the salt param at a - specific point in the auth process - """ - pass - - -class SRPAuthNoB(SRPAuthenticationError): - """ - Exception raised when we don't receive the B param at a specific - point in the auth process - """ - pass - - -class SRPAuthBadDataFromServer(SRPAuthenticationError): - """ - Generic exception when we receive bad data from the server. - """ - pass - - -class SRPAuthJSONDecodeError(SRPAuthenticationError): - """ - Exception raised when there's a problem decoding the JSON content - parsed as received from th e server. - """ - pass - - -class SRPAuthBadPassword(SRPAuthenticationError): - """ - Exception raised when the user provided a bad password to auth. - """ - pass - - -class SRPAuthVerificationFailed(SRPAuthenticationError): - """ - Exception raised when we can't verify the SRP data received from - the server. - """ - pass - - -class SRPAuthNoSessionId(SRPAuthenticationError): - """ - Exception raised when we don't receive a session id from the - server. - """ - pass - - -class SRPAuth(QtCore.QObject): - """ - SRPAuth singleton - """ - - class __impl(QtCore.QObject): - """ - Implementation of the SRPAuth interface - """ - - LOGIN_KEY = "login" - A_KEY = "A" - CLIENT_AUTH_KEY = "client_auth" - SESSION_ID_KEY = "_session_id" - - def __init__(self, provider_config): - """ - Constructor for SRPAuth implementation - - :param server: Server to which we will authenticate - :type server: str - """ - QtCore.QObject.__init__(self) - - leap_assert(provider_config, - "We need a provider config to authenticate") - - self._provider_config = provider_config - - # **************************************************** # - # Dependency injection helpers, override this for more - # granular testing - self._fetcher = requests - self._srp = srp - self._hashfun = self._srp.SHA256 - self._ng = self._srp.NG_1024 - # **************************************************** # - - self._session = self._fetcher.session() - self._session_id = None - self._session_id_lock = QtCore.QMutex() - self._uid = None - self._uid_lock = QtCore.QMutex() - self._token = None - self._token_lock = QtCore.QMutex() - - self._srp_user = None - self._srp_a = None - - def _safe_unhexlify(self, val): - """ - Rounds the val to a multiple of 2 and returns the - unhexlified value - - :param val: hexlified value - :type val: str - - :rtype: binary hex data - :return: unhexlified val - """ - return binascii.unhexlify(val) \ - if (len(val) % 2 == 0) else binascii.unhexlify('0' + val) - - def _authentication_preprocessing(self, username, password): - """ - Generates the SRP.User to get the A SRP parameter - - :param username: username to login - :type username: str - :param password: password for the username - :type password: str - """ - logger.debug("Authentication preprocessing...") - self._srp_user = self._srp.User(username, - password, - self._hashfun, - self._ng) - _, A = self._srp_user.start_authentication() - - self._srp_a = A - - def _start_authentication(self, _, username): - """ - Sends the first request for authentication to retrieve the - salt and B parameter - - Might raise all SRPAuthenticationError based: - SRPAuthenticationError - SRPAuthConnectionError - SRPAuthUnknownUser - SRPAuthBadStatusCode - SRPAuthNoSalt - SRPAuthNoB - - :param _: IGNORED, output from the previous callback (None) - :type _: IGNORED - :param username: username to login - :type username: str - - :return: salt and B parameters - :rtype: tuple - """ - logger.debug("Starting authentication process...") - try: - auth_data = { - self.LOGIN_KEY: username, - self.A_KEY: binascii.hexlify(self._srp_a) - } - sessions_url = "%s/%s/%s/" % \ - (self._provider_config.get_api_uri(), - self._provider_config.get_api_version(), - "sessions") - init_session = self._session.post(sessions_url, - data=auth_data, - verify=self._provider_config. - get_ca_cert_path(), - timeout=REQUEST_TIMEOUT) - # Clean up A value, we don't need it anymore - self._srp_a = None - except requests.exceptions.ConnectionError as e: - logger.error("No connection made (salt): %r" % - (e,)) - raise SRPAuthConnectionError("Could not establish a " - "connection") - except Exception as e: - logger.error("Unknown error: %r" % (e,)) - raise SRPAuthenticationError("Unknown error: %r" % - (e,)) - - content, mtime = reqhelper.get_content(init_session) - - if init_session.status_code not in (200,): - logger.error("No valid response (salt): " - "Status code = %r. Content: %r" % - (init_session.status_code, content)) - if init_session.status_code == 422: - raise SRPAuthUnknownUser(self.tr("Unknown user")) - - raise SRPAuthBadStatusCode(self.tr("There was a problem with" - " authentication")) - - json_content = json.loads(content) - salt = json_content.get("salt", None) - B = json_content.get("B", None) - - if salt is None: - logger.error("No salt parameter sent") - raise SRPAuthNoSalt(self.tr("The server did not send " - "the salt parameter")) - if B is None: - logger.error("No B parameter sent") - raise SRPAuthNoB(self.tr("The server did not send " - "the B parameter")) - - return salt, B - - def _process_challenge(self, salt_B, username): - """ - Given the salt and B processes the auth challenge and - generates the M2 parameter - - Might raise SRPAuthenticationError based: - SRPAuthenticationError - SRPAuthBadDataFromServer - SRPAuthConnectionError - SRPAuthJSONDecodeError - SRPAuthBadPassword - - :param salt_B: salt and B parameters for the username - :type salt_B: tuple - :param username: username for this session - :type username: str - - :return: the M2 SRP parameter - :rtype: str - """ - logger.debug("Processing challenge...") - try: - salt, B = salt_B - unhex_salt = self._safe_unhexlify(salt) - unhex_B = self._safe_unhexlify(B) - except (TypeError, ValueError) as e: - logger.error("Bad data from server: %r" % (e,)) - raise SRPAuthBadDataFromServer( - self.tr("The data sent from the server had errors")) - M = self._srp_user.process_challenge(unhex_salt, unhex_B) - - auth_url = "%s/%s/%s/%s" % (self._provider_config.get_api_uri(), - self._provider_config. - get_api_version(), - "sessions", - username) - - auth_data = { - self.CLIENT_AUTH_KEY: binascii.hexlify(M) - } - - try: - auth_result = self._session.put(auth_url, - data=auth_data, - verify=self._provider_config. - get_ca_cert_path(), - timeout=REQUEST_TIMEOUT) - except requests.exceptions.ConnectionError as e: - logger.error("No connection made (HAMK): %r" % (e,)) - raise SRPAuthConnectionError(self.tr("Could not connect to " - "the server")) - - try: - content, mtime = reqhelper.get_content(auth_result) - except JSONDecodeError: - raise SRPAuthJSONDecodeError("Bad JSON content in auth result") - - if auth_result.status_code == 422: - error = "" - try: - error = json.loads(content).get("errors", "") - except ValueError: - logger.error("Problem parsing the received response: %s" - % (content,)) - except AttributeError: - logger.error("Expecting a dict but something else was " - "received: %s", (content,)) - logger.error("[%s] Wrong password (HAMK): [%s]" % - (auth_result.status_code, error)) - raise SRPAuthBadPassword(self.tr("Wrong password")) - - if auth_result.status_code not in (200,): - logger.error("No valid response (HAMK): " - "Status code = %s. Content = %r" % - (auth_result.status_code, content)) - raise SRPAuthBadStatusCode(self.tr("Unknown error (%s)") % - (auth_result.status_code,)) - - return json.loads(content) - - def _extract_data(self, json_content): - """ - Extracts the necessary parameters from json_content (M2, - id, token) - - Might raise SRPAuthenticationError based: - SRPBadDataFromServer - - :param json_content: Data received from the server - :type json_content: dict - """ - try: - M2 = json_content.get("M2", None) - uid = json_content.get("id", None) - token = json_content.get("token", None) - except Exception as e: - logger.error(e) - raise SRPAuthBadDataFromServer("Something went wrong with the " - "login") - - self.set_uid(uid) - self.set_token(token) - - if M2 is None or self.get_uid() is None: - logger.error("Something went wrong. Content = %r" % - (json_content,)) - raise SRPAuthBadDataFromServer(self.tr("Problem getting data " - "from server")) - - events_signal( - proto.CLIENT_UID, content=uid, - reqcbk=lambda req, res: None) # make the rpc call async - - return M2 - - def _verify_session(self, M2): - """ - Verifies the session based on the M2 parameter. If the - verification succeeds, it sets the session_id for this - session - - Might raise SRPAuthenticationError based: - SRPAuthBadDataFromServer - SRPAuthVerificationFailed - - :param M2: M2 SRP parameter - :type M2: str - """ - logger.debug("Verifying session...") - try: - unhex_M2 = self._safe_unhexlify(M2) - except TypeError: - logger.error("Bad data from server (HAWK)") - raise SRPAuthBadDataFromServer(self.tr("Bad data from server")) - - self._srp_user.verify_session(unhex_M2) - - if not self._srp_user.authenticated(): - logger.error("Auth verification failed") - raise SRPAuthVerificationFailed(self.tr("Auth verification " - "failed")) - logger.debug("Session verified.") - - session_id = self._session.cookies.get(self.SESSION_ID_KEY, None) - if not session_id: - logger.error("Bad cookie from server (missing _session_id)") - raise SRPAuthNoSessionId(self.tr("Session cookie " - "verification " - "failed")) - - events_signal( - proto.CLIENT_SESSION_ID, content=session_id, - reqcbk=lambda req, res: None) # make the rpc call async - - self.set_session_id(session_id) - - def _threader(self, cb, res, *args, **kwargs): - return threads.deferToThread(cb, res, *args, **kwargs) - - def authenticate(self, username, password): - """ - Executes the whole authentication process for a user - - Might raise SRPAuthenticationError - - :param username: username for this session - :type username: str - :param password: password for this user - :type password: str - - :returns: A defer on a different thread - :rtype: twisted.internet.defer.Deferred - """ - leap_assert(self.get_session_id() is None, "Already logged in") - - d = threads.deferToThread(self._authentication_preprocessing, - username=username, - password=password) - - d.addCallback( - partial(self._threader, - self._start_authentication), - username=username) - d.addCallback( - partial(self._threader, - self._process_challenge), - username=username) - d.addCallback( - partial(self._threader, - self._extract_data)) - d.addCallback(partial(self._threader, - self._verify_session)) - - return d - - def logout(self): - """ - Logs out the current session. - Expects a session_id to exists, might raise AssertionError - """ - logger.debug("Starting logout...") - - leap_assert(self.get_session_id(), - "Cannot logout an unexisting session") - - logout_url = "%s/%s/%s/" % (self._provider_config.get_api_uri(), - self._provider_config. - get_api_version(), - "sessions") - try: - self._session.delete(logout_url, - data=self.get_session_id(), - verify=self._provider_config. - get_ca_cert_path(), - timeout=REQUEST_TIMEOUT) - except Exception as e: - logger.warning("Something went wrong with the logout: %r" % - (e,)) - - self.set_session_id(None) - self.set_uid(None) - # Also reset the session - self._session = self._fetcher.session() - logger.debug("Successfully logged out.") - - def set_session_id(self, session_id): - QtCore.QMutexLocker(self._session_id_lock) - self._session_id = session_id - - def get_session_id(self): - QtCore.QMutexLocker(self._session_id_lock) - return self._session_id - - def set_uid(self, uid): - QtCore.QMutexLocker(self._uid_lock) - self._uid = uid - - def get_uid(self): - QtCore.QMutexLocker(self._uid_lock) - return self._uid - - def set_token(self, token): - QtCore.QMutexLocker(self._token_lock) - self._token = token - - def get_token(self): - QtCore.QMutexLocker(self._token_lock) - return self._token - - __instance = None - - authentication_finished = QtCore.Signal(bool, str) - logout_finished = QtCore.Signal(bool, str) - - def __init__(self, provider_config): - """ - Creates a singleton instance if needed - """ - QtCore.QObject.__init__(self) - - # Check whether we already have an instance - if SRPAuth.__instance is None: - # Create and remember instance - SRPAuth.__instance = SRPAuth.__impl(provider_config) - - # Store instance reference as the only member in the handle - self.__dict__['_SRPAuth__instance'] = SRPAuth.__instance - - def authenticate(self, username, password): - """ - Executes the whole authentication process for a user - - Might raise SRPAuthenticationError based - - :param username: username for this session - :type username: str - :param password: password for this user - :type password: str - """ - - d = self.__instance.authenticate(username, password) - d.addCallback(self._gui_notify) - d.addErrback(self._errback) - return d - - def _gui_notify(self, _): - """ - Callback that notifies the UI with the proper signal. - - :param _: IGNORED, output from the previous callback (None) - :type _: IGNORED - """ - logger.debug("Successful login!") - self.authentication_finished.emit(True, self.tr("Succeeded")) - - def _errback(self, failure): - """ - General errback for the whole login process. Will notify the - UI with the proper signal. - - :param failure: Failure object captured from a callback. - :type failure: twisted.python.failure.Failure - """ - logger.error("Error logging in %s" % (failure,)) - self.authentication_finished.emit(False, "%s" % (failure.value,)) - failure.trap(Exception) - - def get_session_id(self): - return self.__instance.get_session_id() - - def get_uid(self): - return self.__instance.get_uid() - - def get_token(self): - return self.__instance.get_token() - - def logout(self): - """ - Logs out the current session. - Expects a session_id to exists, might raise AssertionError - """ - try: - self.__instance.logout() - self.logout_finished.emit(True, self.tr("Succeeded")) - return True - except Exception as e: - self.logout_finished.emit(False, "%s" % (e,)) - return False diff --git a/src/leap/crypto/srpregister.py b/src/leap/crypto/srpregister.py deleted file mode 100644 index de1978b5..00000000 --- a/src/leap/crypto/srpregister.py +++ /dev/null @@ -1,168 +0,0 @@ -# -*- coding: utf-8 -*- -# srpregister.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 . - -import binascii -import logging - -import requests -import srp - -from PySide import QtCore -from urlparse import urlparse - -from leap.config.providerconfig import ProviderConfig -from leap.util.constants import SIGNUP_TIMEOUT -from leap.common.check import leap_assert, leap_assert_type - -logger = logging.getLogger(__name__) - - -class SRPRegister(QtCore.QObject): - """ - Registers a user to a specific provider using SRP - """ - - USER_LOGIN_KEY = 'user[login]' - USER_VERIFIER_KEY = 'user[password_verifier]' - USER_SALT_KEY = 'user[password_salt]' - - registration_finished = QtCore.Signal(bool, object) - - def __init__(self, - provider_config=None, - register_path="users"): - """ - Constructor - - :param provider_config: provider configuration instance, - properly loaded - :type privider_config: ProviderConfig - :param register_path: webapp path for registering users - :type register_path; str - """ - QtCore.QObject.__init__(self) - leap_assert(provider_config, "Please provide a provider") - leap_assert_type(provider_config, ProviderConfig) - - self._provider_config = provider_config - - # **************************************************** # - # Dependency injection helpers, override this for more - # granular testing - self._fetcher = requests - self._srp = srp - self._hashfun = self._srp.SHA256 - self._ng = self._srp.NG_1024 - # **************************************************** # - - parsed_url = urlparse(provider_config.get_api_uri()) - self._provider = parsed_url.hostname - self._port = parsed_url.port - if self._port is None: - self._port = "443" - - self._register_path = register_path - - self._session = self._fetcher.session() - - def _get_registration_uri(self): - """ - Returns the URI where the register request should be made for - the provider - - :rtype: str - """ - - uri = "https://%s:%s/%s/%s" % ( - self._provider, - self._port, - self._provider_config.get_api_version(), - self._register_path) - - return uri - - def register_user(self, username, password): - """ - Registers a user with the validator based on the password provider - - :param username: username to register - :type username: str - :param password: password for this username - :type password: str - - :rtype: tuple - :rparam: (ok, request) - """ - salt, verifier = self._srp.create_salted_verification_key( - username, - password, - self._hashfun, - self._ng) - - user_data = { - self.USER_LOGIN_KEY: username, - self.USER_VERIFIER_KEY: binascii.hexlify(verifier), - self.USER_SALT_KEY: binascii.hexlify(salt) - } - - uri = self._get_registration_uri() - - logger.debug('Post to uri: %s' % uri) - logger.debug("Will try to register user = %s" % (username,)) - - ok = False - # This should be None, but we don't like when PySide segfaults, - # so it something else. - # To reproduce it, just do: - # self.registration_finished.emit(False, None) - req = [] - try: - req = self._session.post(uri, - data=user_data, - timeout=SIGNUP_TIMEOUT, - verify=self._provider_config. - get_ca_cert_path()) - - except (requests.exceptions.SSLError, - requests.exceptions.ConnectionError) as exc: - logger.error(exc.message) - ok = False - else: - ok = req.ok - - self.registration_finished.emit(ok, req) - return ok - - -if __name__ == "__main__": - logger = logging.getLogger(name='leap') - logger.setLevel(logging.DEBUG) - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - provider = ProviderConfig() - - if provider.load("leap/providers/bitmask.net/provider.json"): - register = SRPRegister(provider_config=provider) - print "Registering user..." - print register.register_user("test1", "sarasaaaa") - print register.register_user("test2", "sarasaaaa") diff --git a/src/leap/crypto/tests/__init__.py b/src/leap/crypto/tests/__init__.py deleted file mode 100644 index 7f118735..00000000 --- a/src/leap/crypto/tests/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# __init__.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 . diff --git a/src/leap/crypto/tests/eip-service.json b/src/leap/crypto/tests/eip-service.json deleted file mode 100644 index 24df42a2..00000000 --- a/src/leap/crypto/tests/eip-service.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "gateways": [ - { - "capabilities": { - "adblock": false, - "filter_dns": false, - "limited": true, - "ports": [ - "1194", - "443", - "53", - "80" - ], - "protocols": [ - "tcp", - "udp" - ], - "transport": [ - "openvpn" - ], - "user_ips": false - }, - "host": "harrier.cdev.bitmask.net", - "ip_address": "199.254.238.50", - "location": "seattle__wa" - } - ], - "locations": { - "seattle__wa": { - "country_code": "US", - "hemisphere": "N", - "name": "Seattle, WA", - "timezone": "-7" - } - }, - "openvpn_configuration": { - "auth": "SHA1", - "cipher": "AES-128-CBC", - "tls-cipher": "DHE-RSA-AES128-SHA" - }, - "serial": 1, - "version": 1 -} \ No newline at end of file diff --git a/src/leap/crypto/tests/fake_provider.py b/src/leap/crypto/tests/fake_provider.py deleted file mode 100755 index 54af485d..00000000 --- a/src/leap/crypto/tests/fake_provider.py +++ /dev/null @@ -1,376 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# fake_provider.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 . -"""A server faking some of the provider resources and apis, -used for testing Leap Client requests - -It needs that you create a subfolder named 'certs', -and that you place the following files: - -XXX check if in use - -[ ] test-openvpn.pem -[ ] test-provider.json -[ ] test-eip-service.json -""" -import binascii -import json -import os -import sys -import time - -import srp - -from OpenSSL import SSL - -from zope.interface import Interface, Attribute, implements - -from twisted.web.server import Site, Request -from twisted.web.static import File, Data -from twisted.web.resource import Resource -from twisted.internet import reactor - -from leap.common.testing.https_server import where - -# See -# http://twistedmatrix.com/documents/current/web/howto/web-in-60/index.html -# for more examples - -""" -Testing the FAKE_API: -##################### - - 1) register an user - >> curl -d "user[login]=me" -d "user[password_salt]=foo" \ - -d "user[password_verifier]=beef" http://localhost:8000/1/users - << {"errors": null} - - 2) check that if you try to register again, it will fail: - >> curl -d "user[login]=me" -d "user[password_salt]=foo" \ - -d "user[password_verifier]=beef" http://localhost:8000/1/users - << {"errors": {"login": "already taken!"}} - -""" - -# Globals to mock user/sessiondb - -_USERDB = {} -_SESSIONDB = {} - -_here = os.path.split(__file__)[0] - - -safe_unhexlify = lambda x: binascii.unhexlify(x) \ - if (len(x) % 2 == 0) else binascii.unhexlify('0' + x) - - -class IUser(Interface): - """ - Defines the User Interface - """ - login = Attribute("User login.") - salt = Attribute("Password salt.") - verifier = Attribute("Password verifier.") - session = Attribute("Session.") - svr = Attribute("Server verifier.") - - -class User(object): - """ - User object. - We store it in our simple session mocks - """ - - implements(IUser) - - def __init__(self, login, salt, verifier): - self.login = login - self.salt = salt - self.verifier = verifier - self.session = None - self.svr = None - - def set_server_verifier(self, svr): - """ - Adds a svr verifier object to this - User instance - """ - self.svr = svr - - def set_session(self, session): - """ - Adds this instance of User to the - global session dict - """ - _SESSIONDB[session] = self - self.session = session - - -class FakeUsers(Resource): - """ - Resource that handles user registration. - """ - - def __init__(self, name): - self.name = name - - def render_POST(self, request): - """ - Handles POST to the users api resource - Simulates a login. - """ - args = request.args - - login = args['user[login]'][0] - salt = args['user[password_salt]'][0] - verifier = args['user[password_verifier]'][0] - - if login in _USERDB: - request.setResponseCode(422) - return "%s\n" % json.dumps( - {'errors': {'login': 'already taken!'}}) - - print '[server]', login, verifier, salt - user = User(login, salt, verifier) - _USERDB[login] = user - return json.dumps({'errors': None}) - - -def getSession(self, sessionInterface=None): - """ - we overwrite twisted.web.server.Request.getSession method to - put the right cookie name in place - """ - if not self.session: - #cookiename = b"_".join([b'TWISTED_SESSION'] + self.sitepath) - cookiename = b"_".join([b'_session_id'] + self.sitepath) - sessionCookie = self.getCookie(cookiename) - if sessionCookie: - try: - self.session = self.site.getSession(sessionCookie) - except KeyError: - pass - # if it still hasn't been set, fix it up. - if not self.session: - self.session = self.site.makeSession() - self.addCookie(cookiename, self.session.uid, path=b'/') - self.session.touch() - if sessionInterface: - return self.session.getComponent(sessionInterface) - return self.session - - -def get_user(request): - """ - Returns user from the session dict - """ - login = request.args.get('login') - if login: - user = _USERDB.get(login[0], None) - if user: - return user - - request.getSession = getSession.__get__(request, Request) - session = request.getSession() - - user = _SESSIONDB.get(session, None) - return user - - -class FakeSession(Resource): - def __init__(self, name): - """ - Initializes session - """ - self.name = name - - def render_GET(self, request): - """ - Handles GET requests. - """ - return "%s\n" % json.dumps({'errors': None}) - - def render_POST(self, request): - """ - Handles POST requests. - """ - user = get_user(request) - - if not user: - # XXX get real error from demo provider - return json.dumps({'errors': 'no such user'}) - - A = request.args['A'][0] - - _A = safe_unhexlify(A) - _salt = safe_unhexlify(user.salt) - _verifier = safe_unhexlify(user.verifier) - - svr = srp.Verifier( - user.login, - _salt, - _verifier, - _A, - hash_alg=srp.SHA256, - ng_type=srp.NG_1024) - - s, B = svr.get_challenge() - - _B = binascii.hexlify(B) - - print '[server] login = %s' % user.login - print '[server] salt = %s' % user.salt - print '[server] len(_salt) = %s' % len(_salt) - print '[server] vkey = %s' % user.verifier - print '[server] len(vkey) = %s' % len(_verifier) - print '[server] s = %s' % binascii.hexlify(s) - print '[server] B = %s' % _B - print '[server] len(B) = %s' % len(_B) - - # override Request.getSession - request.getSession = getSession.__get__(request, Request) - session = request.getSession() - - user.set_session(session) - user.set_server_verifier(svr) - - # yep, this is tricky. - # some things are *already* unhexlified. - data = { - 'salt': user.salt, - 'B': _B, - 'errors': None} - - return json.dumps(data) - - def render_PUT(self, request): - """ - Handles PUT requests. - """ - # XXX check session??? - user = get_user(request) - - if not user: - print '[server] NO USER' - return json.dumps({'errors': 'no such user'}) - - data = request.content.read() - auth = data.split("client_auth=") - M = auth[1] if len(auth) > 1 else None - # if not H, return - if not M: - return json.dumps({'errors': 'no M proof passed by client'}) - - svr = user.svr - HAMK = svr.verify_session(binascii.unhexlify(M)) - if HAMK is None: - print '[server] verification failed!!!' - raise Exception("Authentication failed!") - #import ipdb;ipdb.set_trace() - - assert svr.authenticated() - print "***" - print '[server] User successfully authenticated using SRP!' - print "***" - - return json.dumps( - {'M2': binascii.hexlify(HAMK), - 'id': '9c943eb9d96a6ff1b7a7030bdeadbeef', - 'errors': None}) - - -class API_Sessions(Resource): - """ - Top resource for the API v1 - """ - def getChild(self, name, request): - return FakeSession(name) - - -class FileModified(File): - def render_GET(self, request): - since = request.getHeader('if-modified-since') - if since: - tsince = time.strptime(since.replace(" GMT", "")) - tfrom = time.strptime(time.ctime(os.path.getmtime(self.path))) - if tfrom > tsince: - return File.render_GET(self, request) - else: - request.setResponseCode(304) - return "" - return File.render_GET(self, request) - - -class OpenSSLServerContextFactory(object): - - def getContext(self): - """ - Create an SSL context. - """ - ctx = SSL.Context(SSL.SSLv23_METHOD) - #ctx = SSL.Context(SSL.TLSv1_METHOD) - ctx.use_certificate_file(where('leaptestscert.pem')) - ctx.use_privatekey_file(where('leaptestskey.pem')) - - return ctx - - -def get_provider_factory(): - """ - Instantiates a Site that serves the resources - that we expect from a valid provider. - Listens on: - * port 8000 for http connections - * port 8443 for https connections - - :rparam: factory for a site - :rtype: Site instance - """ - root = Data("", "") - root.putChild("", root) - root.putChild("provider.json", FileModified( - os.path.join(_here, - "test_provider.json"))) - config = Resource() - config.putChild( - "eip-service.json", - FileModified( - os.path.join(_here, "eip-service.json"))) - apiv1 = Resource() - apiv1.putChild("config", config) - apiv1.putChild("sessions", API_Sessions()) - apiv1.putChild("users", FakeUsers(None)) - apiv1.putChild("cert", FileModified( - os.path.join(_here, - 'openvpn.pem'))) - root.putChild("1", apiv1) - - factory = Site(root) - return factory - - -if __name__ == "__main__": - - from twisted.python import log - log.startLogging(sys.stdout) - - factory = get_provider_factory() - - # regular http (for debugging with curl) - reactor.listenTCP(8000, factory) - reactor.listenSSL(8443, factory, OpenSSLServerContextFactory()) - reactor.run() diff --git a/src/leap/crypto/tests/openvpn.pem b/src/leap/crypto/tests/openvpn.pem deleted file mode 100644 index a95e9370..00000000 --- a/src/leap/crypto/tests/openvpn.pem +++ /dev/null @@ -1,33 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFtTCCA52gAwIBAgIJAIGJ8Dg+DtemMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV -BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX -aWRnaXRzIFB0eSBMdGQwHhcNMTMwNjI2MjAyMDIyWhcNMTgwNjI2MjAyMDIyWjBF -MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 -ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC -CgKCAgEAxJaN0lWjFu+3j48c0WG8BvmPUf026Xli5d5NE4EjGsirwfre0oTeWZT9 -WRxqLGd2wDh6Mc9r6UqH6dwqLZKbsgwB5zI2lag7UWFttJF1U1c6AJynhaLMoy73 -sL9USTmQ57iYRFrVP/nGj9/L6I1XnV6midPi7a5aZreH9q8dWaAhmc9eFDU+Y4vS -sTFS6aomajLrI6YWo5toKqLq8IMryD03IM78a7gJtLgfWs+pYZRUBlM5JaYX98eX -mVPAYYH9krWxLVN3hTt1ngECzK+epo275zQJh960/2fNCfVJSXqSXcficLs+bR7t -FEkNuOP1hFV6LuoLL+k5Su+hp5kXMYZTvYYDpW4nPJoBdSG1w5O5IxO6zh+9VLB7 -oLrlgoyWvBoou5coCBpZVU6UyWcOx58kuZF8wNr0GgdvWAFwOGVuVG5jmcVdhaKC -0C8NxHrxlhcrcp0zwtDaOxfmZfcxiXs35iwUip5vS18Nv+XBK8ad9T79Ox8nSzP3 -RGPVDpExz7gPbZglqSe47XBIk0ZuIzgOgYpJj4JrpoewoIYb+OmUgI7UZjoGsMrV -+B2BqOKs7kF0HW3i5bR9YAi0ZYvnhQgjBtwCKm4zvLqwuPZHz9VWgIk6uezgStCP -WyzQ8IcopK49fOjcKa6JT5JRU+27paIZf1BkQsTkJy/Nti4TvwMCAwEAAaOBpzCB -pDAdBgNVHQ4EFgQUEgXSd3Yl3xAzbkWa7xeNe27d99cwdQYDVR0jBG4wbIAUEgXS -d3Yl3xAzbkWa7xeNe27d99ehSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT -b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCB -ifA4Pg7XpjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQA6Vl9Ve4Qe -ewzXAxr0BabFRhtIuF7DV+/niT46qJhW2KgYe6rwZqdAhEbgH3kTPJ5JmmcUnAEH -nmrfoku/YAb5ObfdHUACsHy4cvSvFwBUQ9vXP6+oOFJhrGW4uzRI2pHGvnqB3lQ0 -JEPmPwduBCI5reRYauPbd4Wl4VhLGrjELb4JQZL24Q5ehXMnv415m7+aMkLzT2IA -p6B2xgRR+JAeUdyCNOV1f5AqJWyAUJPWGR0e1OTKNfc49+2skK0NmzrpGsoktSHa -uN6vGBCVGiZh7BTYblWMG5q9Am7idcdmC2fdpIf5yj7CKzV7WIPxPs0I7TuRcr41 -pUBLCAElcyCPB89lySol2BDs4gk4wZs4y2shUs3o0+mIpw/6o8tQF/9IL8ALkLqr -q9SuND7O1RXcg74o3HeVmRKtoI/KdgaVhJ0rFvcq83ftfu3KMyWB6SOKOu6ZYON8 -AcSjsDDpnDrwGFvjAYHiTkS9NaaJC1/g7Y6jjhxmbTkXPA6V8MvLKQiOvqk/9gCh -85FHsFkElIYnH6fbHIRxg20cnqmddTd+H5HgBIlhiKWuydtuoQFwzR/D3ypgLBaB -OWLcBP7I+RYhKlJFIWnfiyB0xbyI4W/UfL8p8jQI8TE9oIlm3WqxJXfebDEDEstj -8nS4Fb3G5Wr4pZMjfbtmBSAgHeWH6B90jg== ------END CERTIFICATE----- diff --git a/src/leap/crypto/tests/test_provider.json b/src/leap/crypto/tests/test_provider.json deleted file mode 100644 index c37bef8f..00000000 --- a/src/leap/crypto/tests/test_provider.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "api_uri": "https://localhost:8443", - "api_version": "1", - "ca_cert_fingerprint": "SHA256: 0f17c033115f6b76ff67871872303ff65034efe7dd1b910062ca323eb4da5c7e", - "ca_cert_uri": "https://bitmask.net/ca.crt", - "default_language": "en", - "domain": "example.com", - "enrollment_policy": "open", - "name": { - "en": "Bitmask" - }, - "services": [ - "openvpn" - ] -} diff --git a/src/leap/crypto/tests/test_srpauth.py b/src/leap/crypto/tests/test_srpauth.py deleted file mode 100644 index e3258fd3..00000000 --- a/src/leap/crypto/tests/test_srpauth.py +++ /dev/null @@ -1,790 +0,0 @@ -# -*- coding: utf-8 -*- -# test_srpauth.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 . -""" -Tests for: - * leap/crypto/srpauth.py -""" -try: - import unittest2 as unittest -except ImportError: - import unittest -import os -import sys -import binascii -import requests -import mock - -from mock import MagicMock -from nose.twistedtools import reactor, deferred -from twisted.python import log -from twisted.internet import threads -from functools import partial -from requests.models import Response -from simplejson.decoder import JSONDecodeError - -from leap.common.testing.https_server import where -from leap.config.providerconfig import ProviderConfig -from leap.crypto import srpregister, srpauth -from leap.crypto.tests import fake_provider -from leap.util.request_helpers import get_content - -log.startLogging(sys.stdout) - - -def _get_capath(): - return where("cacert.pem") - -_here = os.path.split(__file__)[0] - - -class ImproperlyConfiguredError(Exception): - """ - Raised if the test provider is missing configuration - """ - - -class SRPAuthTestCase(unittest.TestCase): - """ - Tests for the SRPAuth class - """ - __name__ = "SRPAuth tests" - - def setUp(self): - """ - Sets up this TestCase with a simple and faked provider instance: - - * runs a threaded reactor - * loads a mocked ProviderConfig that points to the certs in the - leap.common.testing module. - """ - factory = fake_provider.get_provider_factory() - http = reactor.listenTCP(0, factory) - https = reactor.listenSSL( - 0, factory, - fake_provider.OpenSSLServerContextFactory()) - get_port = lambda p: p.getHost().port - self.http_port = get_port(http) - self.https_port = get_port(https) - - provider = ProviderConfig() - provider.get_ca_cert_path = mock.create_autospec( - provider.get_ca_cert_path) - provider.get_ca_cert_path.return_value = _get_capath() - - provider.get_api_uri = mock.create_autospec( - provider.get_api_uri) - provider.get_api_uri.return_value = self._get_https_uri() - - loaded = provider.load(path=os.path.join( - _here, "test_provider.json")) - if not loaded: - raise ImproperlyConfiguredError( - "Could not load test provider config") - self.register = srpregister.SRPRegister(provider_config=provider) - self.provider = provider - self.TEST_USER = "register_test_auth" - self.TEST_PASS = "pass" - - # Reset the singleton - srpauth.SRPAuth._SRPAuth__instance = None - self.auth = srpauth.SRPAuth(self.provider) - self.auth_backend = self.auth._SRPAuth__instance - - self.old_post = self.auth_backend._session.post - self.old_put = self.auth_backend._session.put - self.old_delete = self.auth_backend._session.delete - - self.old_start_auth = self.auth_backend._start_authentication - self.old_proc_challenge = self.auth_backend._process_challenge - self.old_extract_data = self.auth_backend._extract_data - self.old_verify_session = self.auth_backend._verify_session - self.old_auth_preproc = self.auth_backend._authentication_preprocessing - self.old_get_sid = self.auth_backend.get_session_id - self.old_cookie_get = self.auth_backend._session.cookies.get - self.old_auth = self.auth_backend.authenticate - - def tearDown(self): - self.auth_backend._session.post = self.old_post - self.auth_backend._session.put = self.old_put - self.auth_backend._session.delete = self.old_delete - - self.auth_backend._start_authentication = self.old_start_auth - self.auth_backend._process_challenge = self.old_proc_challenge - self.auth_backend._extract_data = self.old_extract_data - self.auth_backend._verify_session = self.old_verify_session - self.auth_backend._authentication_preprocessing = self.old_auth_preproc - self.auth_backend.get_session_id = self.old_get_sid - self.auth_backend._session.cookies.get = self.old_cookie_get - self.auth_backend.authenticate = self.old_auth - - # helper methods - - def _get_https_uri(self): - """ - Returns a https uri with the right https port initialized - """ - return "https://localhost:%s" % (self.https_port,) - - # Auth tests - - def _prepare_auth_test(self, code=200, side_effect=None): - """ - Creates the needed defers to test several test situations. It - adds up to the auth preprocessing step. - - :param code: status code for the response of POST in requests - :type code: int - :param side_effect: side effect triggered by the POST method - in requests - :type side_effect: some kind of Exception - - :returns: the defer that is created - :rtype: defer.Deferred - """ - res = Response() - res.status_code = code - self.auth_backend._session.post = mock.create_autospec( - self.auth_backend._session.post, - return_value=res, - side_effect=side_effect) - - d = threads.deferToThread(self.register.register_user, - self.TEST_USER, - self.TEST_PASS) - - def wrapper_preproc(*args): - return threads.deferToThread( - self.auth_backend._authentication_preprocessing, - self.TEST_USER, self.TEST_PASS) - - d.addCallback(wrapper_preproc) - - return d - - def test_safe_unhexlify(self): - input_value = "somestring" - test_value = binascii.hexlify(input_value) - self.assertEqual( - self.auth_backend._safe_unhexlify(test_value), - input_value) - - def test_safe_unhexlify_not_raises(self): - input_value = "somestring" - test_value = binascii.hexlify(input_value)[:-1] - - with self.assertRaises(TypeError): - binascii.unhexlify(test_value) - - self.auth_backend._safe_unhexlify(test_value) - - def test_preprocessing_loads_a(self): - self.assertEqual(self.auth_backend._srp_a, None) - self.auth_backend._authentication_preprocessing("user", "pass") - self.assertIsNotNone(self.auth_backend._srp_a) - self.assertTrue(len(self.auth_backend._srp_a) > 0) - - @deferred() - def test_start_authentication(self): - d = threads.deferToThread(self.register.register_user, self.TEST_USER, - self.TEST_PASS) - - def wrapper_preproc(*args): - return threads.deferToThread( - self.auth_backend._authentication_preprocessing, - self.TEST_USER, self.TEST_PASS) - - d.addCallback(wrapper_preproc) - - def wrapper(_): - return threads.deferToThread( - self.auth_backend._start_authentication, - None, self.TEST_USER) - - d.addCallback(wrapper) - return d - - @deferred() - def test_start_authentication_fails_connerror(self): - d = self._prepare_auth_test( - side_effect=requests.exceptions.ConnectionError()) - - def wrapper(_): - with self.assertRaises(srpauth.SRPAuthConnectionError): - self.auth_backend._start_authentication(None, self.TEST_USER) - - d.addCallback(partial(threads.deferToThread, wrapper)) - return d - - @deferred() - def test_start_authentication_fails_any_error(self): - d = self._prepare_auth_test(side_effect=Exception()) - - def wrapper(_): - with self.assertRaises(srpauth.SRPAuthenticationError): - self.auth_backend._start_authentication(None, self.TEST_USER) - - d.addCallback(partial(threads.deferToThread, wrapper)) - return d - - @deferred() - def test_start_authentication_fails_unknown_user(self): - d = self._prepare_auth_test(422) - - def wrapper(_): - with self.assertRaises(srpauth.SRPAuthUnknownUser): - with mock.patch('leap.util.request_helpers.get_content', - new=mock.create_autospec(get_content)) as \ - content: - content.return_value = ("{}", 0) - - self.auth_backend._start_authentication( - None, self.TEST_USER) - - d.addCallback(partial(threads.deferToThread, wrapper)) - return d - - @deferred() - def test_start_authentication_fails_errorcode(self): - d = self._prepare_auth_test(302) - - def wrapper(_): - with self.assertRaises(srpauth.SRPAuthBadStatusCode): - with mock.patch('leap.util.request_helpers.get_content', - new=mock.create_autospec(get_content)) as \ - content: - content.return_value = ("{}", 0) - - self.auth_backend._start_authentication(None, - self.TEST_USER) - - d.addCallback(partial(threads.deferToThread, wrapper)) - return d - - @deferred() - def test_start_authentication_fails_no_salt(self): - d = self._prepare_auth_test(200) - - def wrapper(_): - with self.assertRaises(srpauth.SRPAuthNoSalt): - with mock.patch('leap.util.request_helpers.get_content', - new=mock.create_autospec(get_content)) as \ - content: - content.return_value = ("{}", 0) - - self.auth_backend._start_authentication(None, - self.TEST_USER) - - d.addCallback(partial(threads.deferToThread, wrapper)) - return d - - @deferred() - def test_start_authentication_fails_no_B(self): - d = self._prepare_auth_test(200) - - def wrapper(_): - with self.assertRaises(srpauth.SRPAuthNoB): - with mock.patch('leap.util.request_helpers.get_content', - new=mock.create_autospec(get_content)) as \ - content: - content.return_value = ('{"salt": ""}', 0) - - self.auth_backend._start_authentication(None, - self.TEST_USER) - - d.addCallback(partial(threads.deferToThread, wrapper)) - return d - - @deferred() - def test_start_authentication_correct_saltb(self): - d = self._prepare_auth_test(200) - - test_salt = "12345" - test_B = "67890" - - def wrapper(_): - with mock.patch('leap.util.request_helpers.get_content', - new=mock.create_autospec(get_content)) as \ - content: - content.return_value = ('{"salt":"%s", "B":"%s"}' % (test_salt, - test_B), - 0) - - salt, B = self.auth_backend._start_authentication( - None, - self.TEST_USER) - self.assertEqual(salt, test_salt) - self.assertEqual(B, test_B) - - d.addCallback(partial(threads.deferToThread, wrapper)) - return d - - def _prepare_auth_challenge(self): - """ - Creates the needed defers to test several test situations. It - adds up to the start authentication step. - - :returns: the defer that is created - :rtype: defer.Deferred - """ - d = threads.deferToThread(self.register.register_user, - self.TEST_USER, - self.TEST_PASS) - - def wrapper_preproc(*args): - return threads.deferToThread( - self.auth_backend._authentication_preprocessing, - self.TEST_USER, self.TEST_PASS) - - d.addCallback(wrapper_preproc) - - def wrapper_start(*args): - return threads.deferToThread( - self.auth_backend._start_authentication, - None, self.TEST_USER) - - d.addCallback(wrapper_start) - - return d - - @deferred() - def test_process_challenge_wrong_saltb(self): - d = self._prepare_auth_challenge() - - def wrapper(salt_B): - with self.assertRaises(srpauth.SRPAuthBadDataFromServer): - self.auth_backend._process_challenge("", - username=self.TEST_USER) - - d.addCallback(partial(threads.deferToThread, wrapper)) - return d - - @deferred() - def test_process_challenge_requests_problem_raises(self): - d = self._prepare_auth_challenge() - - self.auth_backend._session.put = mock.create_autospec( - self.auth_backend._session.put, - side_effect=requests.exceptions.ConnectionError()) - - def wrapper(salt_B): - with self.assertRaises(srpauth.SRPAuthConnectionError): - self.auth_backend._process_challenge(salt_B, - username=self.TEST_USER) - - d.addCallback(partial(threads.deferToThread, wrapper)) - - return d - - @deferred() - def test_process_challenge_json_decode_error(self): - d = self._prepare_auth_challenge() - - def wrapper(salt_B): - with mock.patch('leap.util.request_helpers.get_content', - new=mock.create_autospec(get_content)) as \ - content: - content.return_value = ("{", 0) - content.side_effect = JSONDecodeError("", "", 0) - - with self.assertRaises(srpauth.SRPAuthJSONDecodeError): - self.auth_backend._process_challenge( - salt_B, - username=self.TEST_USER) - - d.addCallback(partial(threads.deferToThread, wrapper)) - - return d - - @deferred() - def test_process_challenge_bad_password(self): - d = self._prepare_auth_challenge() - - res = Response() - res.status_code = 422 - self.auth_backend._session.put = mock.create_autospec( - self.auth_backend._session.put, - return_value=res) - - def wrapper(salt_B): - with mock.patch('leap.util.request_helpers.get_content', - new=mock.create_autospec(get_content)) as \ - content: - content.return_value = ("", 0) - with self.assertRaises(srpauth.SRPAuthBadPassword): - self.auth_backend._process_challenge( - salt_B, - username=self.TEST_USER) - - d.addCallback(partial(threads.deferToThread, wrapper)) - - return d - - @deferred() - def test_process_challenge_bad_password2(self): - d = self._prepare_auth_challenge() - - res = Response() - res.status_code = 422 - self.auth_backend._session.put = mock.create_autospec( - self.auth_backend._session.put, - return_value=res) - - def wrapper(salt_B): - with mock.patch('leap.util.request_helpers.get_content', - new=mock.create_autospec(get_content)) as \ - content: - content.return_value = ("[]", 0) - with self.assertRaises(srpauth.SRPAuthBadPassword): - self.auth_backend._process_challenge( - salt_B, - username=self.TEST_USER) - - d.addCallback(partial(threads.deferToThread, wrapper)) - - return d - - @deferred() - def test_process_challenge_other_error_code(self): - d = self._prepare_auth_challenge() - - res = Response() - res.status_code = 300 - self.auth_backend._session.put = mock.create_autospec( - self.auth_backend._session.put, - return_value=res) - - def wrapper(salt_B): - with mock.patch('leap.util.request_helpers.get_content', - new=mock.create_autospec(get_content)) as \ - content: - content.return_value = ("{}", 0) - with self.assertRaises(srpauth.SRPAuthBadStatusCode): - self.auth_backend._process_challenge( - salt_B, - username=self.TEST_USER) - - d.addCallback(partial(threads.deferToThread, wrapper)) - - return d - - @deferred() - def test_process_challenge(self): - d = self._prepare_auth_challenge() - - def wrapper(salt_B): - self.auth_backend._process_challenge(salt_B, - username=self.TEST_USER) - - d.addCallback(partial(threads.deferToThread, wrapper)) - - return d - - def test_extract_data_wrong_data(self): - with self.assertRaises(srpauth.SRPAuthBadDataFromServer): - self.auth_backend._extract_data(None) - - with self.assertRaises(srpauth.SRPAuthBadDataFromServer): - self.auth_backend._extract_data("") - - def test_extract_data_fails_on_wrong_data_from_server(self): - with self.assertRaises(srpauth.SRPAuthBadDataFromServer): - self.auth_backend._extract_data({}) - - with self.assertRaises(srpauth.SRPAuthBadDataFromServer): - self.auth_backend._extract_data({"M2": ""}) - - def test_extract_data_sets_uidtoken(self): - test_uid = "someuid" - test_m2 = "somem2" - test_token = "sometoken" - test_data = { - "M2": test_m2, - "id": test_uid, - "token": test_token - } - m2 = self.auth_backend._extract_data(test_data) - - self.assertEqual(m2, test_m2) - self.assertEqual(self.auth_backend.get_uid(), test_uid) - self.assertEqual(self.auth_backend.get_uid(), - self.auth.get_uid()) - self.assertEqual(self.auth_backend.get_token(), test_token) - self.assertEqual(self.auth_backend.get_token(), - self.auth.get_token()) - - def _prepare_verify_session(self): - """ - Prepares the tests for verify session with needed steps - before. It adds up to the extract_data step. - - :returns: The defer to chain to - :rtype: defer.Deferred - """ - d = self._prepare_auth_challenge() - - def wrapper_proc_challenge(salt_B): - return self.auth_backend._process_challenge( - salt_B, - username=self.TEST_USER) - - def wrapper_extract_data(data): - return self.auth_backend._extract_data(data) - - d.addCallback(partial(threads.deferToThread, wrapper_proc_challenge)) - d.addCallback(partial(threads.deferToThread, wrapper_extract_data)) - - return d - - @deferred() - def test_verify_session_unhexlifiable_m2(self): - d = self._prepare_verify_session() - - def wrapper(M2): - with self.assertRaises(srpauth.SRPAuthBadDataFromServer): - self.auth_backend._verify_session("za") # unhexlifiable value - - d.addCallback(wrapper) - - return d - - @deferred() - def test_verify_session_unverifiable_m2(self): - d = self._prepare_verify_session() - - def wrapper(M2): - with self.assertRaises(srpauth.SRPAuthVerificationFailed): - # Correctly unhelifiable value, but not for verifying the - # session - self.auth_backend._verify_session("abc12") - - d.addCallback(wrapper) - - return d - - @deferred() - def test_verify_session_fails_on_no_session_id(self): - d = self._prepare_verify_session() - - def wrapper(M2): - self.auth_backend._session.cookies.get = mock.create_autospec( - self.auth_backend._session.cookies.get, - return_value=None) - with self.assertRaises(srpauth.SRPAuthNoSessionId): - self.auth_backend._verify_session(M2) - - d.addCallback(wrapper) - - return d - - @deferred() - def test_verify_session_session_id(self): - d = self._prepare_verify_session() - - test_session_id = "12345" - - def wrapper(M2): - self.auth_backend._session.cookies.get = mock.create_autospec( - self.auth_backend._session.cookies.get, - return_value=test_session_id) - self.auth_backend._verify_session(M2) - self.assertEqual(self.auth_backend.get_session_id(), - test_session_id) - self.assertEqual(self.auth_backend.get_session_id(), - self.auth.get_session_id()) - - d.addCallback(wrapper) - - return d - - @deferred() - def test_verify_session(self): - d = self._prepare_verify_session() - - def wrapper(M2): - self.auth_backend._verify_session(M2) - - d.addCallback(wrapper) - - return d - - @deferred() - def test_authenticate(self): - self.auth_backend._authentication_preprocessing = mock.create_autospec( - self.auth_backend._authentication_preprocessing, - return_value=None) - self.auth_backend._start_authentication = mock.create_autospec( - self.auth_backend._start_authentication, - return_value=None) - self.auth_backend._process_challenge = mock.create_autospec( - self.auth_backend._process_challenge, - return_value=None) - self.auth_backend._extract_data = mock.create_autospec( - self.auth_backend._extract_data, - return_value=None) - self.auth_backend._verify_session = mock.create_autospec( - self.auth_backend._verify_session, - return_value=None) - - d = self.auth_backend.authenticate(self.TEST_USER, self.TEST_PASS) - - def check(*args): - self.auth_backend._authentication_preprocessing.\ - assert_called_once_with( - username=self.TEST_USER, - password=self.TEST_PASS - ) - self.auth_backend._start_authentication.assert_called_once_with( - None, - username=self.TEST_USER) - self.auth_backend._process_challenge.assert_called_once_with( - None, - username=self.TEST_USER) - self.auth_backend._extract_data.assert_called_once_with( - None) - self.auth_backend._verify_session.assert_called_once_with(None) - - d.addCallback(check) - - return d - - @deferred() - def test_logout_fails_if_not_logged_in(self): - - def wrapper(*args): - with self.assertRaises(AssertionError): - self.auth_backend.logout() - - d = threads.deferToThread(wrapper) - return d - - @deferred() - def test_logout_traps_delete(self): - self.auth_backend.get_session_id = mock.create_autospec( - self.auth_backend.get_session_id, - return_value="1234") - self.auth_backend._session.delete = mock.create_autospec( - self.auth_backend._session.delete, - side_effect=Exception()) - - def wrapper(*args): - self.auth_backend.logout() - - d = threads.deferToThread(wrapper) - return d - - @deferred() - def test_logout_clears(self): - self.auth_backend._session_id = "1234" - - def wrapper(*args): - old_session = self.auth_backend._session - self.auth_backend.logout() - self.assertIsNone(self.auth_backend.get_session_id()) - self.assertIsNone(self.auth_backend.get_uid()) - self.assertNotEqual(old_session, self.auth_backend._session) - - d = threads.deferToThread(wrapper) - return d - - -class SRPAuthSingletonTestCase(unittest.TestCase): - def setUp(self): - self.old_auth = srpauth.SRPAuth._SRPAuth__impl.authenticate - - def tearDown(self): - srpauth.SRPAuth._SRPAuth__impl.authenticate = self.old_auth - - def test_singleton(self): - obj1 = srpauth.SRPAuth(ProviderConfig()) - obj2 = srpauth.SRPAuth(ProviderConfig()) - self.assertEqual(obj1._SRPAuth__instance, obj2._SRPAuth__instance) - - @deferred() - def test_authenticate_notifies_gui(self): - auth = srpauth.SRPAuth(ProviderConfig()) - auth._SRPAuth__instance.authenticate = mock.create_autospec( - auth._SRPAuth__instance.authenticate, - return_value=threads.deferToThread(lambda: None)) - auth._gui_notify = mock.create_autospec( - auth._gui_notify) - - d = auth.authenticate("", "") - - def check(*args): - auth._gui_notify.assert_called_once_with(None) - - d.addCallback(check) - return d - - @deferred() - def test_authenticate_errsback(self): - auth = srpauth.SRPAuth(ProviderConfig()) - auth._SRPAuth__instance.authenticate = mock.create_autospec( - auth._SRPAuth__instance.authenticate, - return_value=threads.deferToThread(MagicMock( - side_effect=Exception()))) - auth._gui_notify = mock.create_autospec( - auth._gui_notify) - auth._errback = mock.create_autospec( - auth._errback) - - d = auth.authenticate("", "") - - def check(*args): - self.assertFalse(auth._gui_notify.called) - self.assertEqual(auth._errback.call_count, 1) - - d.addCallback(check) - return d - - @deferred() - def test_authenticate_runs_cleanly_when_raises(self): - auth = srpauth.SRPAuth(ProviderConfig()) - auth._SRPAuth__instance.authenticate = mock.create_autospec( - auth._SRPAuth__instance.authenticate, - return_value=threads.deferToThread(MagicMock( - side_effect=Exception()))) - - d = auth.authenticate("", "") - - return d - - @deferred() - def test_authenticate_runs_cleanly(self): - auth = srpauth.SRPAuth(ProviderConfig()) - auth._SRPAuth__instance.authenticate = mock.create_autospec( - auth._SRPAuth__instance.authenticate, - return_value=threads.deferToThread(MagicMock())) - - d = auth.authenticate("", "") - - return d - - def test_logout(self): - auth = srpauth.SRPAuth(ProviderConfig()) - auth._SRPAuth__instance.logout = mock.create_autospec( - auth._SRPAuth__instance.logout) - - self.assertTrue(auth.logout()) - - def test_logout_rets_false_when_raises(self): - auth = srpauth.SRPAuth(ProviderConfig()) - auth._SRPAuth__instance.logout = mock.create_autospec( - auth._SRPAuth__instance.logout, - side_effect=Exception()) - - self.assertFalse(auth.logout()) diff --git a/src/leap/crypto/tests/test_srpregister.py b/src/leap/crypto/tests/test_srpregister.py deleted file mode 100644 index 66b815f2..00000000 --- a/src/leap/crypto/tests/test_srpregister.py +++ /dev/null @@ -1,201 +0,0 @@ -# -*- coding: utf-8 -*- -# test_srpregister.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 . -""" -Tests for: - * leap/crypto/srpregister.py -""" -try: - import unittest2 as unittest -except ImportError: - import unittest -import os -import sys - -from mock import MagicMock -from nose.twistedtools import reactor, deferred -from twisted.python import log -from twisted.internet import threads - -from leap.common.testing.https_server import where -from leap.config.providerconfig import ProviderConfig -from leap.crypto import srpregister, srpauth -from leap.crypto.tests import fake_provider - -log.startLogging(sys.stdout) - - -def _get_capath(): - return where("cacert.pem") - -_here = os.path.split(__file__)[0] - - -class ImproperlyConfiguredError(Exception): - """ - Raised if the test provider is missing configuration - """ - - -class SRPTestCase(unittest.TestCase): - """ - Tests for the SRPRegister class - """ - __name__ = "SRPRegister tests" - - @classmethod - def setUpClass(cls): - """ - Sets up this TestCase with a simple and faked provider instance: - - * runs a threaded reactor - * loads a mocked ProviderConfig that points to the certs in the - leap.common.testing module. - """ - factory = fake_provider.get_provider_factory() - http = reactor.listenTCP(8001, factory) - https = reactor.listenSSL( - 0, factory, - fake_provider.OpenSSLServerContextFactory()) - get_port = lambda p: p.getHost().port - cls.http_port = get_port(http) - cls.https_port = get_port(https) - - provider = ProviderConfig() - provider.get_ca_cert_path = MagicMock() - provider.get_ca_cert_path.return_value = _get_capath() - - provider.get_api_uri = MagicMock() - provider.get_api_uri.return_value = cls._get_https_uri() - - loaded = provider.load(path=os.path.join( - _here, "test_provider.json")) - if not loaded: - raise ImproperlyConfiguredError( - "Could not load test provider config") - cls.register = srpregister.SRPRegister(provider_config=provider) - - cls.auth = srpauth.SRPAuth(provider) - - # helper methods - - @classmethod - def _get_https_uri(cls): - """ - Returns a https uri with the right https port initialized - """ - return "https://localhost:%s" % (cls.https_port,) - - # Register tests - - def test_none_port(self): - provider = ProviderConfig() - provider.get_api_uri = MagicMock() - provider.get_api_uri.return_value = "http://localhost/" - loaded = provider.load(path=os.path.join( - _here, "test_provider.json")) - if not loaded: - raise ImproperlyConfiguredError( - "Could not load test provider config") - - register = srpregister.SRPRegister(provider_config=provider) - self.assertEquals(register._port, "443") - - @deferred() - def test_wrong_cert(self): - provider = ProviderConfig() - loaded = provider.load(path=os.path.join( - _here, "test_provider.json")) - provider.get_ca_cert_path = MagicMock() - provider.get_ca_cert_path.return_value = os.path.join( - _here, - "wrongcert.pem") - provider.get_api_uri = MagicMock() - provider.get_api_uri.return_value = self._get_https_uri() - if not loaded: - raise ImproperlyConfiguredError( - "Could not load test provider config") - - register = srpregister.SRPRegister(provider_config=provider) - d = threads.deferToThread(register.register_user, "foouser_firsttime", - "barpass") - d.addCallback(self.assertFalse) - return d - - @deferred() - def test_register_user(self): - """ - Checks if the registration of an unused name works as expected when - it is the first time that we attempt to register that user, as well as - when we request a user that is taken. - """ - # pristine registration - d = threads.deferToThread(self.register.register_user, - "foouser_firsttime", - "barpass") - d.addCallback(self.assertTrue) - return d - - @deferred() - def test_second_register_user(self): - # second registration attempt with the same user should return errors - d = threads.deferToThread(self.register.register_user, - "foouser_second", - "barpass") - d.addCallback(self.assertTrue) - - # FIXME currently we are catching this in an upper layer, - # we could bring the error validation to the SRPRegister class - def register_wrapper(_): - return threads.deferToThread(self.register.register_user, - "foouser_second", - "barpass") - d.addCallback(register_wrapper) - d.addCallback(self.assertFalse) - return d - - @deferred() - def test_correct_http_uri(self): - """ - Checks that registration autocorrect http uris to https ones. - """ - HTTP_URI = "http://localhost:%s" % (self.https_port, ) - HTTPS_URI = "https://localhost:%s/1/users" % (self.https_port, ) - provider = ProviderConfig() - provider.get_ca_cert_path = MagicMock() - provider.get_ca_cert_path.return_value = _get_capath() - provider.get_api_uri = MagicMock() - - # we introduce a http uri in the config file... - provider.get_api_uri.return_value = HTTP_URI - loaded = provider.load(path=os.path.join( - _here, "test_provider.json")) - if not loaded: - raise ImproperlyConfiguredError( - "Could not load test provider config") - - register = srpregister.SRPRegister(provider_config=provider) - - # ... and we check that we're correctly taking the HTTPS protocol - # instead - reg_uri = register._get_registration_uri() - self.assertEquals(reg_uri, HTTPS_URI) - register._get_registration_uri = MagicMock(return_value=HTTPS_URI) - d = threads.deferToThread(register.register_user, "test_failhttp", - "barpass") - d.addCallback(self.assertTrue) - - return d diff --git a/src/leap/crypto/tests/wrongcert.pem b/src/leap/crypto/tests/wrongcert.pem deleted file mode 100644 index e6cff38a..00000000 --- a/src/leap/crypto/tests/wrongcert.pem +++ /dev/null @@ -1,33 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFtTCCA52gAwIBAgIJAIWZus5EIXNtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV -BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX -aWRnaXRzIFB0eSBMdGQwHhcNMTMwNjI1MTc0NjExWhcNMTgwNjI1MTc0NjExWjBF -MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 -ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC -CgKCAgEA2ObM7ESjyuxFZYD/Y68qOPQgjgggW+cdXfBpU2p4n7clsrUeMhWdW40Y -77Phzor9VOeqs3ZpHuyLzsYVp/kFDm8tKyo2ah5fJwzL0VCSLYaZkUQQ7GNUmTCk -furaxl8cQx/fg395V7/EngsS9B3/y5iHbctbA4MnH3jaotO5EGeo6hw7/eyCotQ9 -KbBV9GJMcY94FsXBCmUB+XypKklWTLhSaS6Cu4Fo8YLW6WmcnsyEOGS2F7WVf5at -7CBWFQZHaSgIBLmc818/mDYCnYmCVMFn/6Ndx7V2NTlz+HctWrQn0dmIOnCUeCwS -wXq9PnBR1rSx/WxwyF/WpyjOFkcIo7vm72kS70pfrYsXcZD4BQqkXYj3FyKnPt3O -ibLKtCxL8/83wOtErPcYpG6LgFkgAAlHQ9MkUi5dbmjCJtpqQmlZeK1RALdDPiB3 -K1KZimrGsmcE624dJxUIOJJpuwJDy21F8kh5ZAsAtE1prWETrQYNElNFjQxM83rS -ZR1Ql2MPSB4usEZT57+KvpEzlOnAT3elgCg21XrjSFGi14hCEao4g2OEZH5GAwm5 -frf6UlSRZ/g3tLTfI8Hv1prw15W2qO+7q7SBAplTODCRk+Yb0YoA2mMM/QXBUcXs -vKEDLSSxzNIBi3T62l39RB/ml+gPKo87ZMDivex1ZhrcJc3Yu3sCAwEAAaOBpzCB -pDAdBgNVHQ4EFgQUPjE+4pun+8FreIdpoR8v6N7xKtUwdQYDVR0jBG4wbIAUPjE+ -4pun+8FreIdpoR8v6N7xKtWhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT -b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCF -mbrORCFzbTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCpvCPdtvXJ -muTj379TZuCJs7/l0FhA7AHa1WAlHjsXHaA7N0+3ZWAbdtXDsowal6S+ldgU/kfV -Lq7NrRq+amJWC7SYj6cvVwhrSwSvu01fe/TWuOzHrRv1uTfJ/VXLonVufMDd9opo -bhqYxMaxLdIx6t/MYmZH4Wpiq0yfZuv//M8i7BBl/qvaWbLhg0yVAKRwjFvf59h6 -6tRFCLddELOIhLDQtk8zMbioPEbfAlKdwwP8kYGtDGj6/9/YTd/oTKRdgHuwyup3 -m0L20Y6LddC+tb0WpK5EyrNbCbEqj1L4/U7r6f/FKNA3bx6nfdXbscaMfYonKAKg -1cRrRg45sErmCz0QyTnWzXyvbjR4oQRzyW3kJ1JZudZ+AwOi00J5FYa3NiLuxl1u -gIGKWSrASQWhEdpa1nlCgX7PhdaQgYjEMpQvA0GCA0OF5JDu8en1yZqsOt1hCLIN -lkz/5jKPqrclY5hV99bE3hgCHRmIPNHCZG3wbZv2yJKxJX1YLMmQwAmSh2N7YwGG -yXRvCxQs5ChPHyRairuf/5MZCZnSVb45ppTVuNUijsbflKRUgfj/XvfqQ22f+C9N -Om2dmNvAiS2TOIfuP47CF2OUa5q4plUwmr+nyXQGM0SIoHNCj+MBdFfb3oxxAtI+ -SLhbnzQv5e84Doqz3YF0XW8jyR7q8GFLNA== ------END CERTIFICATE----- diff --git a/src/leap/gui/__init__.py b/src/leap/gui/__init__.py deleted file mode 100644 index 4b289442..00000000 --- a/src/leap/gui/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# __init__.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 . -""" -init file for leap.gui -""" -app = __import__("app", globals(), locals(), [], 2) -__all__ = [app] diff --git a/src/leap/gui/loggerwindow.py b/src/leap/gui/loggerwindow.py deleted file mode 100644 index fcbdbf19..00000000 --- a/src/leap/gui/loggerwindow.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -# loggerwindow.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 . - -""" -History log window -""" -import logging - -from PySide import QtGui -from ui_loggerwindow import Ui_LoggerWindow -from leap.common.check import leap_assert, leap_assert_type -from leap.util.leap_log_handler import LeapLogHandler - -logger = logging.getLogger(__name__) - - -class LoggerWindow(QtGui.QDialog): - """ - Window that displays a history of the logged messages in the app. - """ - def __init__(self, handler): - """ - Initialize the widget with the custom handler. - - :param handler: Custom handler that supports history and signal. - :type handler: LeapLogHandler. - """ - QtGui.QDialog.__init__(self) - leap_assert(handler, "We need a handler for the logger window") - leap_assert_type(handler, LeapLogHandler) - - # Load UI - self.ui = Ui_LoggerWindow() - self.ui.setupUi(self) - - # Make connections - self.ui.btnSave.clicked.connect(self._save_log_to_file) - self.ui.btnDebug.toggled.connect(self._load_history), - self.ui.btnInfo.toggled.connect(self._load_history), - self.ui.btnWarning.toggled.connect(self._load_history), - self.ui.btnError.toggled.connect(self._load_history), - self.ui.btnCritical.toggled.connect(self._load_history) - - # Load logging history and connect logger with the widget - self._logging_handler = handler - self._connect_to_handler() - self._load_history() - - def _connect_to_handler(self): - """ - This method connects the loggerwindow with the handler through a - signal communicate the logger events. - """ - self._logging_handler.new_log.connect(self._add_log_line) - - def _add_log_line(self, log): - """ - Adds a line to the history, only if it's in the desired levels to show. - - :param log: a log record to be inserted in the widget - :type log: a dict with RECORD_KEY and MESSAGE_KEY. - the record contains the LogRecord of the logging module, - the message contains the formatted message for the log. - """ - html_style = { - logging.DEBUG: "background: #CDFFFF;", - logging.INFO: "background: white;", - logging.WARNING: "background: #FFFF66;", - logging.ERROR: "background: red; color: white;", - logging.CRITICAL: "background: red; color: white; font: bold;" - } - level = log[LeapLogHandler.RECORD_KEY].levelno - message = log[LeapLogHandler.MESSAGE_KEY] - message = message.replace('\n', '
\n') - - if self._logs_to_display[level]: - open_tag = "" - open_tag += "" - close_tag = "" - message = open_tag + message + close_tag - - self.ui.txtLogHistory.append(message) - - def _load_history(self): - """ - Load the previous logged messages in the widget. - They are stored in the custom handler. - """ - self._set_logs_to_display() - self.ui.txtLogHistory.clear() - history = self._logging_handler.log_history - for line in history: - self._add_log_line(line) - - def _set_logs_to_display(self): - """ - Sets the logs_to_display dict getting the toggled options from the ui - """ - self._logs_to_display = { - logging.DEBUG: self.ui.btnDebug.isChecked(), - logging.INFO: self.ui.btnInfo.isChecked(), - logging.WARNING: self.ui.btnWarning.isChecked(), - logging.ERROR: self.ui.btnError.isChecked(), - logging.CRITICAL: self.ui.btnCritical.isChecked() - } - - def _save_log_to_file(self): - """ - Lets the user save the current log to a file - """ - fileName, filtr = QtGui.QFileDialog.getSaveFileName( - self, self.tr("Save As")) - - if fileName: - try: - with open(fileName, 'w') as output: - output.write(self.ui.txtLogHistory.toPlainText()) - output.write('\n') - logger.debug('Log saved in %s' % (fileName, )) - except IOError, e: - logger.error("Error saving log file: %r" % (e, )) - else: - logger.debug('Log not saved!') diff --git a/src/leap/gui/login.py b/src/leap/gui/login.py deleted file mode 100644 index de0b2d50..00000000 --- a/src/leap/gui/login.py +++ /dev/null @@ -1,245 +0,0 @@ -# -*- coding: utf-8 -*- -# login.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 . - -""" -Login widget implementation -""" -import logging - -from PySide import QtCore, QtGui -from ui_login import Ui_LoginWidget - -from leap.util.keyring_helpers import has_keyring - -logger = logging.getLogger(__name__) - - -class LoginWidget(QtGui.QWidget): - """ - Login widget that emits signals to display the wizard or to - perform login. - """ - - # Emitted when the login button is clicked - login = QtCore.Signal() - cancel_login = QtCore.Signal() - - # Emitted when the user selects "Other..." in the provider - # combobox or click "Create Account" - show_wizard = QtCore.Signal() - - MAX_STATUS_WIDTH = 40 - - BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" - - def __init__(self, settings, parent=None): - """ - Constructs the LoginWidget. - - :param settings: client wide settings - :type settings: LeapSettings - :param parent: The parent widget for this widget - :type parent: QWidget or None - """ - QtGui.QWidget.__init__(self, parent) - - self._settings = settings - self._selected_provider_index = -1 - - self.ui = Ui_LoginWidget() - self.ui.setupUi(self) - - self.ui.chkRemember.stateChanged.connect( - self._remember_state_changed) - self.ui.chkRemember.setEnabled(has_keyring()) - - self.ui.lnPassword.setEchoMode(QtGui.QLineEdit.Password) - - self.ui.btnLogin.clicked.connect(self.login) - self.ui.lnPassword.returnPressed.connect(self.login) - - self.ui.lnUser.returnPressed.connect(self._focus_password) - - self.ui.cmbProviders.currentIndexChanged.connect( - self._current_provider_changed) - self.ui.btnCreateAccount.clicked.connect( - self.show_wizard) - - username_re = QtCore.QRegExp(self.BARE_USERNAME_REGEX) - self.ui.lnUser.setValidator( - QtGui.QRegExpValidator(username_re, self)) - - def _remember_state_changed(self, state): - """ - Saves the remember state in the LeapSettings - - :param state: possible stats can be Checked, Unchecked and - PartiallyChecked - :type state: QtCore.Qt.CheckState - """ - enable = True if state == QtCore.Qt.Checked else False - self._settings.set_remember(enable) - - def set_providers(self, provider_list): - """ - Set the provider list to provider_list plus an "Other..." item - that triggers the wizard - - :param provider_list: list of providers - :type provider_list: list of str - """ - self.ui.cmbProviders.blockSignals(True) - self.ui.cmbProviders.clear() - self.ui.cmbProviders.addItems(provider_list + [self.tr("Other...")]) - self.ui.cmbProviders.blockSignals(False) - - def select_provider_by_name(self, name): - """ - Given a provider name/domain, it selects it in the combobox - - :param name: name or domain for the provider - :type name: str - """ - provider_index = self.ui.cmbProviders.findText(name) - self.ui.cmbProviders.setCurrentIndex(provider_index) - - def get_selected_provider(self): - """ - Returns the selected provider in the combobox - """ - return self.ui.cmbProviders.currentText() - - def set_remember(self, value): - """ - Checks the remember user and password checkbox - - :param value: True to mark it checked, False otherwise - :type value: bool - """ - self.ui.chkRemember.setChecked(value) - - def get_remember(self): - """ - Returns the remember checkbox state - - :rtype: bool - """ - return self.ui.chkRemember.isChecked() - - def set_user(self, user): - """ - Sets the user and focuses on the next field, password. - - :param user: user to set the field to - :type user: str - """ - self.ui.lnUser.setText(user) - self._focus_password() - - def get_user(self): - """ - Returns the user that appears in the widget. - - :rtype: str - """ - return self.ui.lnUser.text() - - def set_password(self, password): - """ - Sets the password for the widget - - :param password: password to set - :type password: str - """ - self.ui.lnPassword.setText(password) - - def get_password(self): - """ - Returns the password that appears in the widget - - :rtype: str - """ - return self.ui.lnPassword.text() - - def set_status(self, status, error=True): - """ - Sets the status label at the login stage to status - - :param status: status message - :type status: str - """ - if len(status) > self.MAX_STATUS_WIDTH: - status = status[:self.MAX_STATUS_WIDTH] + "..." - if error: - status = "%s" % (status,) - self.ui.lblStatus.setText(status) - - def set_enabled(self, enabled=False): - """ - Enables or disables all the login widgets - - :param enabled: wether they should be enabled or not - :type enabled: bool - """ - self.ui.lnUser.setEnabled(enabled) - self.ui.lnPassword.setEnabled(enabled) - self.ui.chkRemember.setEnabled(enabled) - self.ui.cmbProviders.setEnabled(enabled) - - self._set_cancel(not enabled) - - def _set_cancel(self, enabled=False): - """ - Enables or disables the cancel action in the "log in" process. - - :param enabled: wether it should be enabled or not - :type enabled: bool - """ - text = self.tr("Cancel") - login_or_cancel = self.cancel_login - - if not enabled: - text = self.tr("Log In") - login_or_cancel = self.login - - self.ui.btnLogin.setText(text) - - self.ui.btnLogin.clicked.disconnect() - self.ui.btnLogin.clicked.connect(login_or_cancel) - - def _focus_password(self): - """ - Focuses in the password lineedit - """ - self.ui.lnPassword.setFocus() - - def _current_provider_changed(self, param): - """ - SLOT - TRIGGERS: self.ui.cmbProviders.currentIndexChanged - """ - if param == (self.ui.cmbProviders.count() - 1): - self.show_wizard.emit() - # Leave the previously selected provider in the combobox - prev_provider = 0 - if self._selected_provider_index != -1: - prev_provider = self._selected_provider_index - self.ui.cmbProviders.blockSignals(True) - self.ui.cmbProviders.setCurrentIndex(prev_provider) - self.ui.cmbProviders.blockSignals(False) - else: - self._selected_provider_index = param diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py deleted file mode 100644 index 5ace1043..00000000 --- a/src/leap/gui/mainwindow.py +++ /dev/null @@ -1,1537 +0,0 @@ -# -*- coding: utf-8 -*- -# mainwindow.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 . - -""" -Main window for the leap client -""" -import logging -import os -import platform -import tempfile -from functools import partial - -import keyring - -from PySide import QtCore, QtGui -from twisted.internet import threads - -from leap.common.check import leap_assert -from leap.common.events import register -from leap.common.events import events_pb2 as proto -from leap.config.leapsettings import LeapSettings -from leap.config.providerconfig import ProviderConfig -from leap.crypto.srpauth import SRPAuth -from leap.gui.loggerwindow import LoggerWindow -from leap.gui.wizard import Wizard -from leap.gui.login import LoginWidget -from leap.gui.statuspanel import StatusPanelWidget -from leap.services.eip.eipbootstrapper import EIPBootstrapper -from leap.services.eip.eipconfig import EIPConfig -from leap.services.eip.providerbootstrapper import ProviderBootstrapper -# XXX: Soledad might not work out of the box in Windows, issue #2932 -from leap.services.soledad.soledadbootstrapper import SoledadBootstrapper -from leap.services.mail.smtpbootstrapper import SMTPBootstrapper -from leap.services.mail import imap -from leap.platform_init import IS_WIN, IS_MAC -from leap.platform_init.initializers import init_platform - -from leap.services.eip.vpnprocess import VPN -from leap.services.eip.vpnprocess import OpenVPNAlreadyRunning -from leap.services.eip.vpnprocess import AlienOpenVPNAlreadyRunning - -from leap.services.eip.vpnlaunchers import VPNLauncherException -from leap.services.eip.vpnlaunchers import OpenVPNNotFoundException -from leap.services.eip.vpnlaunchers import EIPNoPkexecAvailable -from leap.services.eip.vpnlaunchers import EIPNoPolkitAuthAgentAvailable -from leap.services.eip.vpnlaunchers import EIPNoTunKextLoaded - -from leap.util import __version__ as VERSION -from leap.util.keyring_helpers import has_keyring - -from leap.services.mail.smtpconfig import SMTPConfig - -if IS_WIN: - from leap.platform_init.locks import WindowsLock - from leap.platform_init.locks import raise_window_ack - -from ui_mainwindow import Ui_MainWindow - -logger = logging.getLogger(__name__) - - -class MainWindow(QtGui.QMainWindow): - """ - Main window for login and presenting status updates to the user - """ - - # StackedWidget indexes - LOGIN_INDEX = 0 - EIP_STATUS_INDEX = 1 - - # Keyring - KEYRING_KEY = "bitmask" - - # SMTP - PORT_KEY = "port" - IP_KEY = "ip_address" - - OPENVPN_SERVICE = "openvpn" - MX_SERVICE = "mx" - - # Signals - new_updates = QtCore.Signal(object) - raise_window = QtCore.Signal([]) - soledad_ready = QtCore.Signal([]) - - # We use this flag to detect abnormal terminations - user_stopped_eip = False - - def __init__(self, quit_callback, - standalone=False, - openvpn_verb=1, - bypass_checks=False): - """ - Constructor for the client main window - - :param quit_callback: Function to be called when closing - the application. - :type quit_callback: callable - - :param standalone: Set to true if the app should use configs - inside its pwd - :type standalone: bool - - :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.QMainWindow.__init__(self) - - # register leap events - register(signal=proto.UPDATER_NEW_UPDATES, - callback=self._new_updates_available, - reqcbk=lambda req, resp: None) # make rpc call async - register(signal=proto.RAISE_WINDOW, - callback=self._on_raise_window_event, - reqcbk=lambda req, resp: None) # make rpc call async - - self._quit_callback = quit_callback - - self._updates_content = "" - - self.ui = Ui_MainWindow() - self.ui.setupUi(self) - - self._settings = LeapSettings(standalone) - - self._login_widget = LoginWidget( - self._settings, - self.ui.stackedWidget.widget(self.LOGIN_INDEX)) - self.ui.loginLayout.addWidget(self._login_widget) - - # Signals - # TODO separate logic from ui signals. - - self._login_widget.login.connect(self._login) - self._login_widget.cancel_login.connect(self._cancel_login) - self._login_widget.show_wizard.connect( - self._launch_wizard) - - self.ui.btnShowLog.clicked.connect(self._show_logger_window) - - self._status_panel = StatusPanelWidget( - self.ui.stackedWidget.widget(self.EIP_STATUS_INDEX)) - self.ui.statusLayout.addWidget(self._status_panel) - - self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) - - self._status_panel.start_eip.connect(self._start_eip) - self._status_panel.stop_eip.connect(self._stop_eip) - - # This is loaded only once, there's a bug when doing that more - # than once - ProviderConfig.standalone = standalone - EIPConfig.standalone = standalone - self._standalone = standalone - self._provider_config = ProviderConfig() - # Used for automatic start of EIP - self._provisional_provider_config = ProviderConfig() - self._eip_config = EIPConfig() - - self._already_started_eip = False - - # This is created once we have a valid provider config - self._srp_auth = None - self._logged_user = None - - # This thread is always running, although it's quite - # lightweight when it's done setting up provider - # configuration and certificate. - self._provider_bootstrapper = ProviderBootstrapper(bypass_checks) - - # Intermediate stages, only do something if there was an error - self._provider_bootstrapper.name_resolution.connect( - self._intermediate_stage) - self._provider_bootstrapper.https_connection.connect( - self._intermediate_stage) - self._provider_bootstrapper.download_ca_cert.connect( - self._intermediate_stage) - - # Important stages, loads the provider config and checks - # certificates - self._provider_bootstrapper.download_provider_info.connect( - self._load_provider_config) - self._provider_bootstrapper.check_api_certificate.connect( - self._provider_config_loaded) - - # This thread is similar to the provider bootstrapper - self._eip_bootstrapper = EIPBootstrapper() - - self._eip_bootstrapper.download_config.connect( - self._eip_intermediate_stage) - self._eip_bootstrapper.download_client_certificate.connect( - self._finish_eip_bootstrap) - - self._soledad_bootstrapper = SoledadBootstrapper() - self._soledad_bootstrapper.download_config.connect( - self._soledad_intermediate_stage) - self._soledad_bootstrapper.gen_key.connect( - self._soledad_bootstrapped_stage) - - self._smtp_bootstrapper = SMTPBootstrapper() - self._smtp_bootstrapper.download_config.connect( - self._smtp_bootstrapped_stage) - - self._vpn = VPN(openvpn_verb=openvpn_verb) - self._vpn.qtsigs.state_changed.connect( - self._status_panel.update_vpn_state) - self._vpn.qtsigs.status_changed.connect( - self._status_panel.update_vpn_status) - self._vpn.qtsigs.process_finished.connect( - self._eip_finished) - - self.ui.action_log_out.setEnabled(False) - self.ui.action_log_out.triggered.connect(self._logout) - self.ui.action_about_leap.triggered.connect(self._about) - self.ui.action_quit.triggered.connect(self.quit) - self.ui.action_wizard.triggered.connect(self._launch_wizard) - self.ui.action_show_logs.triggered.connect(self._show_logger_window) - self.raise_window.connect(self._do_raise_mainwindow) - - # Used to differentiate between real quits and close to tray - self._really_quit = False - - self._systray = None - - self._action_eip_provider = QtGui.QAction( - self.tr("No default provider"), self) - self._action_eip_provider.setEnabled(False) - self._action_eip_status = QtGui.QAction( - self.tr("Encrypted internet is OFF"), - self) - self._action_eip_status.setEnabled(False) - - self._status_panel.set_action_eip_status( - self._action_eip_status) - - self._action_eip_startstop = QtGui.QAction( - self.tr("Turn OFF"), self) - self._action_eip_startstop.triggered.connect( - self._stop_eip) - self._action_eip_startstop.setEnabled(False) - self._status_panel.set_action_eip_startstop( - self._action_eip_startstop) - - self._action_visible = QtGui.QAction(self.tr("Hide Main Window"), self) - self._action_visible.triggered.connect(self._toggle_visible) - - self._enabled_services = [] - - self._center_window() - - self.ui.lblNewUpdates.setVisible(False) - self.ui.btnMore.setVisible(False) - self.ui.btnMore.clicked.connect(self._updates_details) - - self.new_updates.connect(self._react_to_new_updates) - self.soledad_ready.connect(self._start_imap_service) - - init_platform() - - self._wizard = None - self._wizard_firstrun = False - - self._logger_window = None - - self._bypass_checks = bypass_checks - - self._soledad = None - self._keymanager = None - self._imap_service = None - - self._login_defer = None - self._download_provider_defer = None - - self._smtp_config = SMTPConfig() - - if self._first_run(): - self._wizard_firstrun = True - self._wizard = Wizard(standalone=standalone, - bypass_checks=bypass_checks) - # Give this window time to finish init and then show the wizard - QtCore.QTimer.singleShot(1, self._launch_wizard) - self._wizard.accepted.connect(self._finish_init) - self._wizard.rejected.connect(self._rejected_wizard) - else: - self._finish_init() - - def _rejected_wizard(self): - """ - SLOT - TRIGGERS: self._wizard.rejected - - Called if the wizard has been cancelled or closed before - finishing. - """ - if self._wizard_firstrun: - self._settings.set_properprovider(False) - self.quit() - else: - self._finish_init() - - def _launch_wizard(self): - """ - SLOT - TRIGGERS: - self._login_widget.show_wizard - self.ui.action_wizard.triggered - - Also called in first run. - - Launches the wizard, creating the object itself if not already - there. - """ - if self._wizard is None: - self._wizard = Wizard(bypass_checks=self._bypass_checks) - self._wizard.accepted.connect(self._finish_init) - self._wizard.rejected.connect(self._wizard.close) - - self.setVisible(False) - # Do NOT use exec_, it will use a child event loop! - # Refer to http://www.themacaque.com/?p=1067 for funny details. - self._wizard.show() - if IS_MAC: - self._wizard.raise_() - self._wizard.finished.connect(self._wizard_finished) - - def _wizard_finished(self): - """ - SLOT - TRIGGERS - self._wizard.finished - - Called when the wizard has finished. - """ - self.setVisible(True) - - def _get_leap_logging_handler(self): - """ - Gets the leap handler from the top level logger - - :return: a logging handler or None - :rtype: LeapLogHandler or None - """ - from leap.util.leap_log_handler import LeapLogHandler - leap_logger = logging.getLogger('leap') - for h in leap_logger.handlers: - if isinstance(h, LeapLogHandler): - return h - return None - - def _show_logger_window(self): - """ - SLOT - TRIGGERS: - self.ui.action_show_logs.triggered - self.ui.btnShowLog.clicked - - Displays the window with the history of messages logged until now - and displays the new ones on arrival. - """ - if self._logger_window is None: - leap_log_handler = self._get_leap_logging_handler() - if leap_log_handler is None: - logger.error('Leap logger handler not found') - else: - self._logger_window = LoggerWindow(handler=leap_log_handler) - self._logger_window.setVisible( - not self._logger_window.isVisible()) - self.ui.btnShowLog.setChecked(self._logger_window.isVisible()) - else: - self._logger_window.setVisible(not self._logger_window.isVisible()) - self.ui.btnShowLog.setChecked(self._logger_window.isVisible()) - - self._logger_window.finished.connect(self._uncheck_logger_button) - - def _uncheck_logger_button(self): - """ - SLOT - Sets the checked state of the loggerwindow button to false. - """ - self.ui.btnShowLog.setChecked(False) - - def _new_updates_available(self, req): - """ - Callback for the new updates event - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - self.new_updates.emit(req) - - def _react_to_new_updates(self, req): - """ - SLOT - TRIGGER: self._new_updates_available - - Displays the new updates label and sets the updates_content - """ - self.moveToThread(QtCore.QCoreApplication.instance().thread()) - self.ui.lblNewUpdates.setVisible(True) - self.ui.btnMore.setVisible(True) - self._updates_content = req.content - - def _updates_details(self): - """ - SLOT - TRIGGER: self.ui.btnMore.clicked - - Parses and displays the updates details - """ - msg = self.tr("The Bitmask app is ready to update, please" - " restart the application.") - - # We assume that if there is nothing in the contents, then - # the Bitmask bundle is what needs updating. - if len(self._updates_content) > 0: - files = self._updates_content.split(", ") - files_str = "" - for f in files: - final_name = f.replace("/data/", "") - final_name = final_name.replace(".thp", "") - files_str += final_name - files_str += "\n" - msg += self.tr(" The following components will be updated:\n%s") \ - % (files_str,) - - QtGui.QMessageBox.information(self, - self.tr("Updates available"), - msg) - - def _finish_init(self): - """ - SLOT - TRIGGERS: - self._wizard.accepted - - Also called at the end of the constructor if not first run, - and after _rejected_wizard if not first run. - - Implements the behavior after either constructing the - mainwindow object, loading the saved user/password, or after - the wizard has been executed. - """ - # XXX: May be this can be divided into two methods? - - self._login_widget.set_providers(self._configured_providers()) - self._show_systray() - self.show() - if IS_MAC: - self.raise_() - - if self._wizard: - possible_username = self._wizard.get_username() - possible_password = self._wizard.get_password() - - # select the configured provider in the combo box - domain = self._wizard.get_domain() - self._login_widget.select_provider_by_name(domain) - - self._login_widget.set_remember(self._wizard.get_remember()) - self._enabled_services = list(self._wizard.get_services()) - self._settings.set_enabled_services( - self._login_widget.get_selected_provider(), - self._enabled_services) - if possible_username is not None: - self._login_widget.set_user(possible_username) - if possible_password is not None: - self._login_widget.set_password(possible_password) - self._login() - self._wizard = None - self._settings.set_properprovider(True) - else: - self._try_autostart_eip() - if not self._settings.get_remember(): - # nothing to do here - return - - saved_user = self._settings.get_user() - - try: - username, domain = saved_user.split('@') - except (ValueError, AttributeError) as e: - # if the saved_user does not contain an '@' or its None - logger.error('Username@provider malformed. %r' % (e, )) - saved_user = None - - if saved_user is not None and has_keyring(): - # fill the username - self._login_widget.set_user(username) - - # select the configured provider in the combo box - self._login_widget.select_provider_by_name(domain) - - self._login_widget.set_remember(True) - - saved_password = None - try: - saved_password = keyring.get_password(self.KEYRING_KEY, - saved_user - .encode("utf8")) - except ValueError, e: - logger.debug("Incorrect Password. %r." % (e,)) - - if saved_password is not None: - self._login_widget.set_password( - saved_password.decode("utf8")) - self._login() - - def _try_autostart_eip(self): - """ - Tries to autostart EIP - """ - default_provider = self._settings.get_defaultprovider() - - if default_provider is None: - logger.info("Cannot autostart Encrypted Internet because there is " - "no default provider configured") - return - - self._action_eip_provider.setText(default_provider) - - self._enabled_services = self._settings.get_enabled_services( - default_provider) - - if self._provisional_provider_config.load( - os.path.join("leap", - "providers", - default_provider, - "provider.json")): - self._download_eip_config() - else: - # XXX: Display a proper message to the user - logger.error("Unable to load %s config, cannot autostart." % - (default_provider,)) - - def _show_systray(self): - """ - Sets up the systray icon - """ - if self._systray is not None: - self._systray.setVisible(True) - return - - # Placeholder actions - # They are temporary to display the tray as designed - preferences_action = QtGui.QAction(self.tr("Preferences"), self) - preferences_action.setEnabled(False) - help_action = QtGui.QAction(self.tr("Help"), self) - help_action.setEnabled(False) - - systrayMenu = QtGui.QMenu(self) - systrayMenu.addAction(self._action_visible) - systrayMenu.addSeparator() - systrayMenu.addAction(self._action_eip_provider) - systrayMenu.addAction(self._action_eip_status) - systrayMenu.addAction(self._action_eip_startstop) - systrayMenu.addSeparator() - systrayMenu.addAction(preferences_action) - systrayMenu.addAction(help_action) - systrayMenu.addSeparator() - systrayMenu.addAction(self.ui.action_log_out) - systrayMenu.addAction(self.ui.action_quit) - self._systray = QtGui.QSystemTrayIcon(self) - self._systray.setContextMenu(systrayMenu) - self._systray.setIcon(self._status_panel.ERROR_ICON_TRAY) - self._systray.setVisible(True) - self._systray.activated.connect(self._tray_activated) - - self._status_panel.set_systray(self._systray) - - def _tray_activated(self, reason=None): - """ - SLOT - TRIGGER: self._systray.activated - - Displays the context menu from the tray icon - """ - self._update_hideshow_menu() - - context_menu = self._systray.contextMenu() - if not IS_MAC: - # for some reason, context_menu.show() - # is failing in a way beyond my understanding. - # (not working the first time it's clicked). - # this works however. - context_menu.exec_(self._systray.geometry().center()) - - def _update_hideshow_menu(self): - """ - Updates the Hide/Show main window menu text based on the - visibility of the window. - """ - get_action = lambda visible: ( - self.tr("Show Main Window"), - self.tr("Hide Main Window"))[int(visible)] - - # set labels - visible = self.isVisible() - self._action_visible.setText(get_action(visible)) - - def _toggle_visible(self): - """ - SLOT - TRIGGER: self._action_visible.triggered - - Toggles the window visibility - """ - if not self.isVisible(): - self.show() - self.raise_() - else: - self.hide() - - self._update_hideshow_menu() - - def _center_window(self): - """ - Centers the mainwindow based on the desktop geometry - """ - geometry = self._settings.get_geometry() - state = self._settings.get_windowstate() - - if geometry is None: - app = QtGui.QApplication.instance() - width = app.desktop().width() - height = app.desktop().height() - window_width = self.size().width() - window_height = self.size().height() - x = (width / 2.0) - (window_width / 2.0) - y = (height / 2.0) - (window_height / 2.0) - self.move(x, y) - else: - self.restoreGeometry(geometry) - - if state is not None: - self.restoreState(state) - - def _about(self): - """ - SLOT - TRIGGERS: self.ui.action_about_leap.triggered - - Display the About Bitmask dialog - """ - QtGui.QMessageBox.about( - self, self.tr("About Bitmask - %s") % (VERSION,), - self.tr("Version: %s
" - "
" - "Bitmask is the Desktop client application for " - "the LEAP platform, supporting encrypted internet " - "proxy, secure email, and secure chat (coming soon).
" - "
" - "LEAP is a non-profit dedicated to giving " - "all internet users access to secure " - "communication. Our focus is on adapting " - "encryption technology to make it easy to use " - "and widely available.
" - "
" - "More about LEAP" - "") % (VERSION,)) - - def changeEvent(self, e): - """ - Reimplements the changeEvent method to minimize to tray - """ - if QtGui.QSystemTrayIcon.isSystemTrayAvailable() and \ - e.type() == QtCore.QEvent.WindowStateChange and \ - self.isMinimized(): - self._toggle_visible() - e.accept() - return - QtGui.QMainWindow.changeEvent(self, e) - - def closeEvent(self, e): - """ - Reimplementation of closeEvent to close to tray - """ - if QtGui.QSystemTrayIcon.isSystemTrayAvailable() and \ - not self._really_quit: - self._toggle_visible() - e.ignore() - return - - self._settings.set_geometry(self.saveGeometry()) - self._settings.set_windowstate(self.saveState()) - - QtGui.QMainWindow.closeEvent(self, e) - - def _configured_providers(self): - """ - Returns the available providers based on the file structure - - :rtype: list - """ - - # TODO: check which providers have a valid certificate among - # other things, not just the directories - providers = [] - try: - providers = os.listdir( - os.path.join(self._provider_config.get_path_prefix(), - "leap", - "providers")) - except Exception as e: - logger.debug("Error listing providers, assume there are none. %r" - % (e,)) - - return providers - - def _first_run(self): - """ - Returns True if there are no configured providers. False otherwise - - :rtype: bool - """ - has_provider_on_disk = len(self._configured_providers()) != 0 - is_proper_provider = self._settings.get_properprovider() - return not (has_provider_on_disk and is_proper_provider) - - def _download_provider_config(self): - """ - Starts the bootstrapping sequence. It will download the - provider configuration if it's not present, otherwise will - emit the corresponding signals inmediately - """ - provider = self._login_widget.get_selected_provider() - - pb = self._provider_bootstrapper - d = pb.run_provider_select_checks(provider, download_if_needed=True) - self._download_provider_defer = d - - def _load_provider_config(self, data): - """ - SLOT - TRIGGER: self._provider_bootstrapper.download_provider_info - - Once the provider config has been downloaded, this loads the - self._provider_config instance with it and starts the second - part of the bootstrapping sequence - - :param data: result from the last stage of the - run_provider_select_checks - :type data: dict - """ - if data[self._provider_bootstrapper.PASSED_KEY]: - provider = self._login_widget.get_selected_provider() - - # If there's no loaded provider or - # we want to connect to other provider... - if (not self._provider_config.loaded() or - self._provider_config.get_domain() != provider): - self._provider_config.load( - os.path.join("leap", "providers", - provider, "provider.json")) - - if self._provider_config.loaded(): - self._provider_bootstrapper.run_provider_setup_checks( - self._provider_config, - download_if_needed=True) - else: - self._login_widget.set_status( - self.tr("Unable to login: Problem with provider")) - logger.error("Could not load provider configuration.") - self._login_widget.set_enabled(True) - else: - self._login_widget.set_status( - self.tr("Unable to login: Problem with provider")) - logger.error(data[self._provider_bootstrapper.ERROR_KEY]) - self._login_widget.set_enabled(True) - - def _login(self): - """ - SLOT - TRIGGERS: - self._login_widget.login - - Starts the login sequence. Which involves bootstrapping the - selected provider if the selection is valid (not empty), then - start the SRP authentication, and as the last step - bootstrapping the EIP service - """ - leap_assert(self._provider_config, "We need a provider config") - - username = self._login_widget.get_user() - password = self._login_widget.get_password() - provider = self._login_widget.get_selected_provider() - - self._enabled_services = self._settings.get_enabled_services( - self._login_widget.get_selected_provider()) - - if len(provider) == 0: - self._login_widget.set_status( - self.tr("Please select a valid provider")) - return - - if len(username) == 0: - self._login_widget.set_status( - self.tr("Please provide a valid username")) - return - - if len(password) == 0: - self._login_widget.set_status( - self.tr("Please provide a valid Password")) - return - - self._login_widget.set_status(self.tr("Logging in..."), error=False) - self._login_widget.set_enabled(False) - - if self._login_widget.get_remember() and has_keyring(): - # in the keyring and in the settings - # we store the value 'usename@provider' - username_domain = (username + '@' + provider).encode("utf8") - try: - keyring.set_password(self.KEYRING_KEY, - username_domain, - password.encode("utf8")) - # Only save the username if it was saved correctly in - # the keyring - self._settings.set_user(username_domain) - except Exception as e: - logger.error("Problem saving data to keyring. %r" - % (e,)) - - self._download_provider_config() - - def _cancel_login(self): - """ - SLOT - TRIGGERS: - self._login_widget.cancel_login - - Stops the login sequence. - """ - logger.debug("Cancelling log in.") - - if self._download_provider_defer: - logger.debug("Cancelling download provider defer.") - self._download_provider_defer.cancel() - - if self._login_defer: - logger.debug("Cancelling login defer.") - self._login_defer.cancel() - - def _provider_config_loaded(self, data): - """ - SLOT - TRIGGER: self._provider_bootstrapper.check_api_certificate - - Once the provider configuration is loaded, this starts the SRP - authentication - """ - leap_assert(self._provider_config, "We need a provider config!") - - if data[self._provider_bootstrapper.PASSED_KEY]: - username = self._login_widget.get_user().encode("utf8") - password = self._login_widget.get_password().encode("utf8") - - if self._srp_auth is None: - self._srp_auth = SRPAuth(self._provider_config) - self._srp_auth.authentication_finished.connect( - self._authentication_finished) - self._srp_auth.logout_finished.connect( - self._done_logging_out) - - # TODO: Add errback! - self._login_defer = self._srp_auth.authenticate(username, password) - else: - self._login_widget.set_status( - "Unable to login: Problem with provider") - logger.error(data[self._provider_bootstrapper.ERROR_KEY]) - self._login_widget.set_enabled(True) - - def _authentication_finished(self, ok, message): - """ - SLOT - TRIGGER: self._srp_auth.authentication_finished - - Once the user is properly authenticated, try starting the EIP - service - """ - - # In general we want to "filter" likely complicated error - # messages, but in this case, the messages make more sense as - # they come. Since they are "Unknown user" or "Unknown - # password" - self._login_widget.set_status(message, error=not ok) - - if ok: - self._logged_user = self._login_widget.get_user() - self.ui.action_log_out.setEnabled(True) - # We leave a bit of room for the user to see the - # "Succeeded" message and then we switch to the EIP status - # panel - QtCore.QTimer.singleShot(1000, self._switch_to_status) - self._login_defer = None - else: - self._login_widget.set_enabled(True) - - def _switch_to_status(self): - """ - Changes the stackedWidget index to the EIP status one and - triggers the eip bootstrapping - """ - if not self._already_started_eip: - self._status_panel.set_provider( - "%s@%s" % (self._login_widget.get_user(), - self._get_best_provider_config().get_domain())) - - self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX) - - self._soledad_bootstrapper.run_soledad_setup_checks( - self._provider_config, - self._login_widget.get_user(), - self._login_widget.get_password(), - download_if_needed=True, - standalone=self._standalone) - - self._download_eip_config() - - def _soledad_intermediate_stage(self, data): - """ - SLOT - TRIGGERS: - self._soledad_bootstrapper.download_config - - If there was a problem, displays it, otherwise it does nothing. - This is used for intermediate bootstrapping stages, in case - they fail. - """ - passed = data[self._soledad_bootstrapper.PASSED_KEY] - if not passed: - # TODO: display in the GUI: - # should pass signal to a slot in status_panel - # that sets the global status - logger.warning("Soledad failed to start: %s" % - (data[self._soledad_bootstrapper.ERROR_KEY],)) - - def _soledad_bootstrapped_stage(self, data): - """ - SLOT - TRIGGERS: - self._soledad_bootstrapper.gen_key - - If there was a problem, displays it, otherwise it does nothing. - This is used for intermediate bootstrapping stages, in case - they fail. - - :param data: result from the bootstrapping stage for Soledad - :type data: dict - """ - passed = data[self._soledad_bootstrapper.PASSED_KEY] - if not passed: - logger.error(data[self._soledad_bootstrapper.ERROR_KEY]) - return - - logger.debug("Done bootstrapping Soledad") - - self._soledad = self._soledad_bootstrapper.soledad - self._keymanager = self._soledad_bootstrapper.keymanager - - # Ok, now soledad is ready, so we can allow other things that - # depend on soledad to start. - - # this will trigger start_imap_service - self.soledad_ready.emit() - - # TODO connect all these activations to the soledad_ready - # signal so the logic is clearer to follow. - - if self._provider_config.provides_mx() and \ - self._enabled_services.count(self.MX_SERVICE) > 0: - self._smtp_bootstrapper.run_smtp_setup_checks( - self._provider_config, - self._smtp_config, - True) - else: - if self._enabled_services.count(self.MX_SERVICE) > 0: - pass # TODO: show MX status - #self._status_panel.set_eip_status( - # self.tr("%s does not support MX") % - # (self._provider_config.get_domain(),), - # error=True) - else: - pass # TODO: show MX status - #self._status_panel.set_eip_status( - # self.tr("MX is disabled")) - - # Service control methods: smtp - - def _smtp_bootstrapped_stage(self, data): - """ - SLOT - TRIGGERS: - self._smtp_bootstrapper.download_config - - If there was a problem, displays it, otherwise it does nothing. - This is used for intermediate bootstrapping stages, in case - they fail. - - :param data: result from the bootstrapping stage for Soledad - :type data: dict - """ - passed = data[self._smtp_bootstrapper.PASSED_KEY] - if not passed: - logger.error(data[self._smtp_bootstrapper.ERROR_KEY]) - return - logger.debug("Done bootstrapping SMTP") - - hosts = self._smtp_config.get_hosts() - # TODO: handle more than one host and define how to choose - if len(hosts) > 0: - hostname = hosts.keys()[0] - logger.debug("Using hostname %s for SMTP" % (hostname,)) - host = hosts[hostname][self.IP_KEY].encode("utf-8") - port = hosts[hostname][self.PORT_KEY] - # TODO: pick local smtp port in a better way - # TODO: Make the encrypted_only configurable - - from leap.mail.smtp import setup_smtp_relay - client_cert = self._eip_config.get_client_cert_path( - self._provider_config) - setup_smtp_relay(port=2013, - keymanager=self._keymanager, - smtp_host=host, - smtp_port=port, - smtp_cert=client_cert, - smtp_key=client_cert, - encrypted_only=False) - - def _start_imap_service(self): - """ - SLOT - TRIGGERS: - soledad_ready - """ - logger.debug('Starting imap service') - - self._imap_service = imap.start_imap_service( - self._soledad, - self._keymanager) - - def _get_socket_host(self): - """ - Returns the socket and port to be used for VPN - - :rtype: tuple (str, str) (host, port) - """ - - # TODO: make this properly multiplatform - - if platform.system() == "Windows": - host = "localhost" - port = "9876" - else: - host = os.path.join(tempfile.mkdtemp(prefix="leap-tmp"), - 'openvpn.socket') - port = "unix" - - return host, port - - def _start_eip(self): - """ - SLOT - TRIGGERS: - self._status_panel.start_eip - self._action_eip_startstop.triggered - or called from _finish_eip_bootstrap - - Starts EIP - """ - self._status_panel.eip_pre_up() - self.user_stopped_eip = False - provider_config = self._get_best_provider_config() - - try: - host, port = self._get_socket_host() - self._vpn.start(eipconfig=self._eip_config, - providerconfig=provider_config, - socket_host=host, - socket_port=port) - - self._settings.set_defaultprovider( - provider_config.get_domain()) - - provider = provider_config.get_domain() - if self._logged_user is not None: - provider = "%s@%s" % (self._logged_user, provider) - - self._status_panel.set_provider(provider) - - self._action_eip_provider.setText(provider_config.get_domain()) - - self._status_panel.eip_started() - - # XXX refactor into status_panel method? - self._action_eip_startstop.setText(self.tr("Turn OFF")) - self._action_eip_startstop.disconnect(self) - self._action_eip_startstop.triggered.connect( - self._stop_eip) - except EIPNoPolkitAuthAgentAvailable: - self._status_panel.set_global_status( - # XXX this should change to polkit-kde where - # applicable. - self.tr("We could not find any " - "authentication " - "agent in your system.
" - "Make sure you have " - "polkit-gnome-authentication-" - "agent-1 " - "running and try again."), - error=True) - self._set_eipstatus_off() - except EIPNoTunKextLoaded: - self._status_panel.set_global_status( - self.tr("Encrypted Internet cannot be started because " - "the tuntap extension is not installed properly " - "in your system.")) - self._set_eipstatus_off() - except EIPNoPkexecAvailable: - self._status_panel.set_global_status( - self.tr("We could not find pkexec " - "in your system."), - error=True) - self._set_eipstatus_off() - except OpenVPNNotFoundException: - self._status_panel.set_global_status( - self.tr("We could not find openvpn binary."), - error=True) - self._set_eipstatus_off() - except OpenVPNAlreadyRunning as e: - self._status_panel.set_global_status( - self.tr("Another openvpn instance is already running, and " - "could not be stopped."), - error=True) - self._set_eipstatus_off() - except AlienOpenVPNAlreadyRunning as e: - self._status_panel.set_global_status( - self.tr("Another openvpn instance is already running, and " - "could not be stopped because it was not launched by " - "Bitmask. Please stop it and try again."), - error=True) - self._set_eipstatus_off() - except VPNLauncherException as e: - # XXX We should implement again translatable exceptions so - # we can pass a translatable string to the panel (usermessage attr) - self._status_panel.set_global_status("%s" % (e,), error=True) - self._set_eipstatus_off() - else: - self._already_started_eip = True - - def _set_eipstatus_off(self): - """ - Sets eip status to off - """ - self._status_panel.set_eip_status(self.tr("OFF"), error=True) - self._status_panel.set_eip_status_icon("error") - self._status_panel.set_startstop_enabled(True) - self._status_panel.eip_stopped() - - self._set_action_eipstart_off() - - def _set_action_eipstart_off(self): - """ - Sets eip startstop action to OFF status. - """ - self._action_eip_startstop.setText(self.tr("Turn ON")) - self._action_eip_startstop.disconnect(self) - self._action_eip_startstop.triggered.connect( - self._start_eip) - - def _stop_eip(self, abnormal=False): - """ - SLOT - TRIGGERS: - self._status_panel.stop_eip - self._action_eip_startstop.triggered - or called from _eip_finished - - Stops vpn process and makes gui adjustments to reflect - the change of state. - - :param abnormal: whether this was an abnormal termination. - :type abnormal: bool - """ - if abnormal: - logger.warning("Abnormal EIP termination.") - - self.user_stopped_eip = True - self._vpn.terminate() - - self._set_eipstatus_off() - - self._already_started_eip = False - self._settings.set_defaultprovider(None) - if self._logged_user: - self._status_panel.set_provider( - "%s@%s" % (self._logged_user, - self._get_best_provider_config().get_domain())) - - def _get_best_provider_config(self): - """ - Returns the best ProviderConfig to use at a moment. We may - have to use self._provider_config or - self._provisional_provider_config depending on the start - status. - - :rtype: ProviderConfig - """ - leap_assert(self._provider_config is not None or - self._provisional_provider_config is not None, - "We need a provider config") - - provider_config = None - if self._provider_config.loaded(): - provider_config = self._provider_config - elif self._provisional_provider_config.loaded(): - provider_config = self._provisional_provider_config - else: - leap_assert(False, "We could not find any usable ProviderConfig.") - - return provider_config - - def _download_eip_config(self): - """ - Starts the EIP bootstrapping sequence - """ - leap_assert(self._eip_bootstrapper, "We need an eip bootstrapper!") - - provider_config = self._get_best_provider_config() - - if provider_config.provides_eip() and \ - self._enabled_services.count(self.OPENVPN_SERVICE) > 0 and \ - not self._already_started_eip: - - self._status_panel.set_eip_status( - self.tr("Starting...")) - self._eip_bootstrapper.run_eip_setup_checks( - provider_config, - download_if_needed=True) - self._already_started_eip = True - elif not self._already_started_eip: - if self._enabled_services.count(self.OPENVPN_SERVICE) > 0: - self._status_panel.set_eip_status( - self.tr("Not supported"), - error=True) - else: - self._status_panel.set_eip_status(self.tr("Disabled")) - self._status_panel.set_startstop_enabled(False) - - def _finish_eip_bootstrap(self, data): - """ - SLOT - TRIGGER: self._eip_bootstrapper.download_client_certificate - - Starts the VPN thread if the eip configuration is properly - loaded - """ - leap_assert(self._eip_config, "We need an eip config!") - passed = data[self._eip_bootstrapper.PASSED_KEY] - - if not passed: - error_msg = self.tr("There was a problem with the provider") - self._status_panel.set_eip_status(error_msg, error=True) - logger.error(data[self._eip_bootstrapper.ERROR_KEY]) - self._already_started_eip = False - return - - provider_config = self._get_best_provider_config() - - domain = provider_config.get_domain() - - loaded = self._eip_config.loaded() - if not loaded: - eip_config_path = os.path.join("leap", "providers", - domain, "eip-service.json") - api_version = provider_config.get_api_version() - self._eip_config.set_api_version(api_version) - loaded = self._eip_config.load(eip_config_path) - - if loaded: - self._start_eip() - else: - self._status_panel.set_eip_status( - self.tr("Could not load Encrypted Internet " - "Configuration."), - error=True) - - def _logout(self): - """ - SLOT - TRIGGER: self.ui.action_log_out.triggered - - Starts the logout sequence - """ - # XXX: If other defers are doing authenticated stuff, this - # might conflict with those. CHECK! - threads.deferToThread(self._srp_auth.logout) - - def _done_logging_out(self, ok, message): - """ - SLOT - TRIGGER: self._srp_auth.logout_finished - - Switches the stackedWidget back to the login stage after - logging out - """ - self._logged_user = None - self.ui.action_log_out.setEnabled(False) - self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) - self._login_widget.set_password("") - self._login_widget.set_enabled(True) - self._login_widget.set_status("") - - def _intermediate_stage(self, data): - """ - SLOT - TRIGGERS: - self._provider_bootstrapper.name_resolution - self._provider_bootstrapper.https_connection - self._provider_bootstrapper.download_ca_cert - self._eip_bootstrapper.download_config - - If there was a problem, displays it, otherwise it does nothing. - This is used for intermediate bootstrapping stages, in case - they fail. - """ - passed = data[self._provider_bootstrapper.PASSED_KEY] - if not passed: - self._login_widget.set_enabled(True) - self._login_widget.set_status( - self.tr("Unable to connect: Problem with provider")) - logger.error(data[self._provider_bootstrapper.ERROR_KEY]) - - def _eip_intermediate_stage(self, data): - """ - SLOT - TRIGGERS: - self._eip_bootstrapper.download_config - - If there was a problem, displays it, otherwise it does nothing. - This is used for intermediate bootstrapping stages, in case - they fail. - """ - passed = data[self._provider_bootstrapper.PASSED_KEY] - if not passed: - self._login_widget.set_status( - self.tr("Unable to connect: Problem with provider")) - logger.error(data[self._provider_bootstrapper.ERROR_KEY]) - self._already_started_eip = False - - def _eip_finished(self, exitCode): - """ - SLOT - TRIGGERS: - self._vpn.process_finished - - Triggered when the EIP/VPN process finishes to set the UI - accordingly. - """ - logger.info("VPN process finished with exitCode %s..." - % (exitCode,)) - - # Ideally we would have the right exit code here, - # but the use of different wrappers (pkexec, cocoasudo) swallows - # the openvpn exit code so we get zero exit in some cases where we - # shouldn't. As a workaround we just use a flag to indicate - # a purposeful switch off, and mark everything else as unexpected. - - # In the near future we should trigger a native notification from here, - # since the user really really wants to know she is unprotected asap. - # And the right thing to do will be to fail-close. - - # TODO we should have a way of parsing the latest lines in the vpn - # log buffer so we can have a more precise idea of which type - # of error did we have (server side, local problem, etc) - abnormal = True - - # XXX check if these exitCodes are pkexec/cocoasudo specific - if exitCode in (126, 127): - self._status_panel.set_global_status( - self.tr("Encrypted Internet could not be launched " - "because you did not authenticate properly."), - error=True) - self._vpn.killit() - elif exitCode != 0 or not self.user_stopped_eip: - self._status_panel.set_global_status( - self.tr("Encrypted Internet finished in an " - "unexpected manner!"), error=True) - else: - abnormal = False - if exitCode == 0 and IS_MAC: - # XXX remove this warning after I fix cocoasudo. - logger.warning("The above exit code MIGHT BE WRONG.") - self._stop_eip(abnormal) - - def _on_raise_window_event(self, req): - """ - Callback for the raise window event - """ - if IS_WIN: - raise_window_ack() - self.raise_window.emit() - - def _do_raise_mainwindow(self): - """ - SLOT - TRIGGERS: - self._on_raise_window_event - - Triggered when we receive a RAISE_WINDOW event. - """ - TOPFLAG = QtCore.Qt.WindowStaysOnTopHint - self.setWindowFlags(self.windowFlags() | TOPFLAG) - self.show() - self.setWindowFlags(self.windowFlags() & ~TOPFLAG) - self.show() - if IS_MAC: - self.raise_() - - def _cleanup_pidfiles(self): - """ - Removes lockfiles on a clean shutdown. - - Triggered after aboutToQuit signal. - """ - if IS_WIN: - WindowsLock.release_all_locks() - - def _cleanup_and_quit(self): - """ - Call all the cleanup actions in a serialized way. - Should be called from the quit function. - """ - logger.debug('About to quit, doing cleanup...') - - if self._imap_service is not None: - self._imap_service.stop() - - if self._srp_auth is not None: - if self._srp_auth.get_session_id() is not None or \ - self._srp_auth.get_token() is not None: - # XXX this can timeout after loong time: See #3368 - self._srp_auth.logout() - - if self._soledad: - logger.debug("Closing soledad...") - self._soledad.close() - else: - logger.error("No instance of soledad was found.") - - logger.debug('Terminating vpn') - self._vpn.terminate(shutdown=True) - - if self._login_defer: - logger.debug("Cancelling login defer.") - self._login_defer.cancel() - - if self._download_provider_defer: - logger.debug("Cancelling download provider defer.") - self._download_provider_defer.cancel() - - # TODO missing any more cancels? - - logger.debug('Cleaning pidfiles') - self._cleanup_pidfiles() - - def quit(self): - """ - Cleanup and tidely close the main window before quitting. - """ - # TODO: separate the shutting down of services from the - # UI stuff. - self._cleanup_and_quit() - - self._really_quit = True - - if self._wizard: - self._wizard.close() - - if self._logger_window: - self._logger_window.close() - - self.close() - - if self._quit_callback: - self._quit_callback() - - logger.debug('Bye.') - - -if __name__ == "__main__": - import signal - - def sigint_handler(*args, **kwargs): - logger.debug('SIGINT catched. shutting down...') - mainwindow = args[0] - mainwindow.quit() - - import sys - - logger = logging.getLogger(name='leap') - logger.setLevel(logging.DEBUG) - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - app = QtGui.QApplication(sys.argv) - mainwindow = MainWindow() - mainwindow.show() - - timer = QtCore.QTimer() - timer.start(500) - timer.timeout.connect(lambda: None) - - sigint = partial(sigint_handler, mainwindow) - signal.signal(signal.SIGINT, sigint) - - sys.exit(app.exec_()) diff --git a/src/leap/gui/statuspanel.py b/src/leap/gui/statuspanel.py deleted file mode 100644 index f3424c7c..00000000 --- a/src/leap/gui/statuspanel.py +++ /dev/null @@ -1,459 +0,0 @@ -# -*- coding: utf-8 -*- -# statuspanel.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 . - -""" -Status Panel widget implementation -""" -import logging - -from datetime import datetime -from functools import partial -from PySide import QtCore, QtGui - -from ui_statuspanel import Ui_StatusPanel - -from leap.common.check import leap_assert_type -from leap.services.eip.vpnprocess import VPNManager -from leap.platform_init import IS_WIN, IS_LINUX -from leap.util import first - -logger = logging.getLogger(__name__) - - -class RateMovingAverage(object): - """ - Moving window average for calculating - upload and download rates. - """ - SAMPLE_SIZE = 5 - - def __init__(self): - """ - Initializes an empty array of fixed size - """ - self.reset() - - def reset(self): - self._data = [None for i in xrange(self.SAMPLE_SIZE)] - - def append(self, x): - """ - Appends a new data point to the collection. - - :param x: A tuple containing timestamp and traffic points - in the form (timestamp, traffic) - :type x: tuple - """ - self._data.pop(0) - self._data.append(x) - - def get(self): - """ - Gets the collection. - """ - return self._data - - def get_average(self): - """ - Gets the moving average. - """ - data = filter(None, self.get()) - traff = [traffic for (ts, traffic) in data] - times = [ts for (ts, traffic) in data] - - try: - deltatraffic = traff[-1] - first(traff) - deltat = (times[-1] - first(times)).seconds - except IndexError: - deltatraffic = 0 - deltat = 0 - - try: - rate = float(deltatraffic) / float(deltat) / 1024 - except ZeroDivisionError: - rate = 0 - - # In some cases we get negative rates - if rate < 0: - rate = 0 - - return rate - - def get_total(self): - """ - Gets the total accumulated throughput. - """ - try: - return self._data[-1][1] / 1024 - except TypeError: - return 0 - - -class StatusPanelWidget(QtGui.QWidget): - """ - Status widget that displays the current state of the LEAP services - """ - - start_eip = QtCore.Signal() - stop_eip = QtCore.Signal() - - DISPLAY_TRAFFIC_RATES = True - RATE_STR = "%14.2f KB/s" - TOTAL_STR = "%14.2f Kb" - - def __init__(self, parent=None): - QtGui.QWidget.__init__(self, parent) - - self._systray = None - self._action_eip_status = None - - self.ui = Ui_StatusPanel() - self.ui.setupUi(self) - - self.ui.btnEipStartStop.setEnabled(False) - self.ui.btnEipStartStop.clicked.connect( - self.start_eip) - - self.hide_status_box() - - # Set the EIP status icons - self.CONNECTING_ICON = None - self.CONNECTED_ICON = None - self.ERROR_ICON = None - self.CONNECTING_ICON_TRAY = None - self.CONNECTED_ICON_TRAY = None - self.ERROR_ICON_TRAY = None - self._set_eip_icons() - - self._set_traffic_rates() - self._make_status_clickable() - - def _make_status_clickable(self): - """ - Makes upload and download figures clickable. - """ - onclicked = self._on_VPN_status_clicked - self.ui.btnUpload.clicked.connect(onclicked) - self.ui.btnDownload.clicked.connect(onclicked) - - def _on_VPN_status_clicked(self): - """ - SLOT - TRIGGER: self.ui.btnUpload.clicked - self.ui.btnDownload.clicked - - Toggles between rate and total throughput display for vpn - status figures. - """ - self.DISPLAY_TRAFFIC_RATES = not self.DISPLAY_TRAFFIC_RATES - self.update_vpn_status(None) # refresh - - def _set_traffic_rates(self): - """ - Initializes up and download rates. - """ - self._up_rate = RateMovingAverage() - self._down_rate = RateMovingAverage() - - self.ui.btnUpload.setText(self.RATE_STR % (0,)) - self.ui.btnDownload.setText(self.RATE_STR % (0,)) - - def _reset_traffic_rates(self): - """ - Resets up and download rates, and cleans up the labels. - """ - self._up_rate.reset() - self._down_rate.reset() - self.update_vpn_status(None) - - def _update_traffic_rates(self, up, down): - """ - Updates up and download rates. - - :param up: upload total. - :type up: int - :param down: download total. - :type down: int - """ - ts = datetime.now() - self._up_rate.append((ts, up)) - self._down_rate.append((ts, down)) - - def _get_traffic_rates(self): - """ - Gets the traffic rates (in KB/s). - - :returns: a tuple with the (up, down) rates - :rtype: tuple - """ - up = self._up_rate - down = self._down_rate - - return (up.get_average(), down.get_average()) - - def _get_traffic_totals(self): - """ - Gets the traffic total throughput (in Kb). - - :returns: a tuple with the (up, down) totals - :rtype: tuple - """ - up = self._up_rate - down = self._down_rate - - return (up.get_total(), down.get_total()) - - def _set_eip_icons(self): - """ - Sets the EIP status icons for the main window and for the tray - - MAC : dark icons - LINUX : dark icons in window, light icons in tray - WIN : light icons - """ - EIP_ICONS = EIP_ICONS_TRAY = ( - ":/images/conn_connecting-light.png", - ":/images/conn_connected-light.png", - ":/images/conn_error-light.png") - - if IS_LINUX: - EIP_ICONS_TRAY = ( - ":/images/conn_connecting.png", - ":/images/conn_connected.png", - ":/images/conn_error.png") - elif IS_WIN: - EIP_ICONS = EIP_ICONS_TRAY = ( - ":/images/conn_connecting.png", - ":/images/conn_connected.png", - ":/images/conn_error.png") - - self.CONNECTING_ICON = QtGui.QPixmap(EIP_ICONS[0]) - self.CONNECTED_ICON = QtGui.QPixmap(EIP_ICONS[1]) - self.ERROR_ICON = QtGui.QPixmap(EIP_ICONS[2]) - - self.CONNECTING_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[0]) - self.CONNECTED_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[1]) - self.ERROR_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[2]) - - def set_systray(self, systray): - """ - Sets the systray object to use. - - :param systray: Systray object - :type systray: QtGui.QSystemTrayIcon - """ - leap_assert_type(systray, QtGui.QSystemTrayIcon) - self._systray = systray - - def set_action_eip_startstop(self, action_eip_startstop): - """ - Sets the action_eip_startstop to use. - - :param action_eip_startstop: action_eip_status to be used - :type action_eip_startstop: QtGui.QAction - """ - self._action_eip_startstop = action_eip_startstop - - def set_action_eip_status(self, action_eip_status): - """ - Sets the action_eip_status to use. - - :param action_eip_status: action_eip_status to be used - :type action_eip_status: QtGui.QAction - """ - leap_assert_type(action_eip_status, QtGui.QAction) - self._action_eip_status = action_eip_status - - def set_global_status(self, status, error=False): - """ - Sets the global status label. - - :param status: status message - :type status: str or unicode - :param error: if the status is an erroneous one, then set this - to True - :type error: bool - """ - leap_assert_type(error, bool) - if error: - status = "%s" % (status,) - self.ui.lblGlobalStatus.setText(status) - self.ui.globalStatusBox.show() - - def hide_status_box(self): - """ - Hide global status box. - """ - self.ui.globalStatusBox.hide() - - def set_eip_status(self, status, error=False): - """ - Sets the status label at the VPN stage to status - - :param status: status message - :type status: str or unicode - :param error: if the status is an erroneous one, then set this - to True - :type error: bool - """ - leap_assert_type(error, bool) - - self._systray.setToolTip(status) - if error: - status = "%s" % (status,) - self.ui.lblEIPStatus.setText(status) - - def set_startstop_enabled(self, value): - """ - Enable or disable btnEipStartStop and _action_eip_startstop - based on value - - :param value: True for enabled, False otherwise - :type value: bool - """ - leap_assert_type(value, bool) - self.ui.btnEipStartStop.setEnabled(value) - self._action_eip_startstop.setEnabled(value) - - def eip_pre_up(self): - """ - Triggered when the app activates eip. - Hides the status box and disables the start/stop button. - """ - self.hide_status_box() - self.set_startstop_enabled(False) - - def eip_started(self): - """ - Sets the state of the widget to how it should look after EIP - has started - """ - self.ui.btnEipStartStop.setText(self.tr("Turn OFF")) - self.ui.btnEipStartStop.disconnect(self) - self.ui.btnEipStartStop.clicked.connect( - self.stop_eip) - - def eip_stopped(self): - """ - Sets the state of the widget to how it should look after EIP - has stopped - """ - self._reset_traffic_rates() - self.ui.btnEipStartStop.setText(self.tr("Turn ON")) - self.ui.btnEipStartStop.disconnect(self) - self.ui.btnEipStartStop.clicked.connect( - self.start_eip) - - def set_icon(self, icon): - """ - Sets the icon to display for EIP - - :param icon: icon to display - :type icon: QPixmap - """ - self.ui.lblVPNStatusIcon.setPixmap(icon) - - def update_vpn_status(self, data): - """ - SLOT - TRIGGER: VPN.status_changed - - Updates the download/upload labels based on the data provided - by the VPN thread. - - :param data: a dictionary with the tcp/udp write and read totals. - If data is None, we just will refresh the display based - on the previous data. - :type data: dict - """ - if data: - upload = float(data[VPNManager.TCPUDP_WRITE_KEY] or "0") - download = float(data[VPNManager.TCPUDP_READ_KEY] or "0") - self._update_traffic_rates(upload, download) - - if self.DISPLAY_TRAFFIC_RATES: - uprate, downrate = self._get_traffic_rates() - upload_str = self.RATE_STR % (uprate,) - download_str = self.RATE_STR % (downrate,) - - else: # display total throughput - uptotal, downtotal = self._get_traffic_totals() - upload_str = self.TOTAL_STR % (uptotal,) - download_str = self.TOTAL_STR % (downtotal,) - - self.ui.btnUpload.setText(upload_str) - self.ui.btnDownload.setText(download_str) - - def update_vpn_state(self, data): - """ - SLOT - TRIGGER: VPN.state_changed - - Updates the displayed VPN state based on the data provided by - the VPN thread - """ - status = data[VPNManager.STATUS_STEP_KEY] - self.set_eip_status_icon(status) - if status == "CONNECTED": - self.set_eip_status(self.tr("ON")) - # Only now we can properly enable the button. - self.set_startstop_enabled(True) - elif status == "AUTH": - self.set_eip_status(self.tr("Authenticating...")) - elif status == "GET_CONFIG": - self.set_eip_status(self.tr("Retrieving configuration...")) - elif status == "WAIT": - self.set_eip_status(self.tr("Waiting to start...")) - elif status == "ASSIGN_IP": - self.set_eip_status(self.tr("Assigning IP")) - elif status == "ALREADYRUNNING": - # Put the following calls in Qt's event queue, otherwise - # the UI won't update properly - QtCore.QTimer.singleShot(0, self.stop_eip) - QtCore.QTimer.singleShot(0, partial(self.set_global_status, - self.tr("Unable to start VPN, " - "it's already " - "running."))) - else: - self.set_eip_status(status) - - def set_eip_status_icon(self, status): - """ - Given a status step from the VPN thread, set the icon properly - - :param status: status step - :type status: str - """ - selected_pixmap = self.ERROR_ICON - selected_pixmap_tray = self.ERROR_ICON_TRAY - tray_message = self.tr("Encryption is OFF") - if status in ("WAIT", "AUTH", "GET_CONFIG", - "RECONNECTING", "ASSIGN_IP"): - selected_pixmap = self.CONNECTING_ICON - selected_pixmap_tray = self.CONNECTING_ICON_TRAY - tray_message = self.tr("Turning ON") - elif status in ("CONNECTED"): - tray_message = self.tr("Encryption is ON") - selected_pixmap = self.CONNECTED_ICON - selected_pixmap_tray = self.CONNECTED_ICON_TRAY - - self.set_icon(selected_pixmap) - self._systray.setIcon(QtGui.QIcon(selected_pixmap_tray)) - self._action_eip_status.setText(tray_message) - - def set_provider(self, provider): - self.ui.lblProvider.setText(provider) diff --git a/src/leap/gui/twisted_main.py b/src/leap/gui/twisted_main.py deleted file mode 100644 index c7add3ee..00000000 --- a/src/leap/gui/twisted_main.py +++ /dev/null @@ -1,60 +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 . -""" -Main functions for integration of twisted reactor -""" -import logging - -from twisted.internet import error - -# Resist the temptation of putting the import reactor here, -# it will raise an "reactor already imported" error. - -logger = logging.getLogger(__name__) - - -def start(app): - """ - Start the mainloop. - - :param app: the main qt QApplication instance. - :type app: QtCore.QApplication - """ - from twisted.internet import reactor - logger.debug('starting twisted reactor') - - # this seems to be troublesome under some - # unidentified settings. - #reactor.run() - - reactor.runReturn() - app.exec_() - - -def quit(app): - """ - Stop the mainloop. - - :param app: the main qt QApplication instance. - :type app: QtCore.QApplication - """ - from twisted.internet import reactor - logger.debug('stopping twisted reactor') - try: - reactor.stop() - except error.ReactorNotRunning: - logger.debug('reactor not running') diff --git a/src/leap/gui/ui/loggerwindow.ui b/src/leap/gui/ui/loggerwindow.ui deleted file mode 100644 index b08428a9..00000000 --- a/src/leap/gui/ui/loggerwindow.ui +++ /dev/null @@ -1,155 +0,0 @@ - - - LoggerWindow - - - - 0 - 0 - 648 - 469 - - - - Logs - - - - :/images/mask-icon.png:/images/mask-icon.png - - - - - - - - - - - Debug - - - - :/images/oxygen-icons/script-error.png:/images/oxygen-icons/script-error.png - - - true - - - true - - - true - - - - - - - Info - - - - :/images/oxygen-icons/dialog-information.png:/images/oxygen-icons/dialog-information.png - - - true - - - true - - - true - - - - - - - Warning - - - - :/images/oxygen-icons/dialog-warning.png:/images/oxygen-icons/dialog-warning.png - - - true - - - true - - - true - - - - - - - Error - - - - :/images/oxygen-icons/dialog-error.png:/images/oxygen-icons/dialog-error.png - - - true - - - true - - - true - - - - - - - Critical - - - - :/images/oxygen-icons/edit-bomb.png:/images/oxygen-icons/edit-bomb.png - - - true - - - true - - - true - - - - - - - Save to file - - - - :/images/oxygen-icons/document-save-as.png:/images/oxygen-icons/document-save-as.png - - - - - - - - - btnDebug - btnInfo - btnWarning - btnError - btnCritical - btnSave - txtLogHistory - - - - - - - diff --git a/src/leap/gui/ui/login.ui b/src/leap/gui/ui/login.ui deleted file mode 100644 index 42a6897a..00000000 --- a/src/leap/gui/ui/login.ui +++ /dev/null @@ -1,132 +0,0 @@ - - - LoginWidget - - - - 0 - 0 - 356 - 223 - - - - Form - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Create a new account - - - - - - - <b>Provider:</b> - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - - - - - - Remember username and password - - - - - - - <b>Username:</b> - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - <b>Password:</b> - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Log In - - - - - - - - - - Qt::AlignCenter - - - true - - - - - - - cmbProviders - lnUser - lnPassword - chkRemember - btnLogin - btnCreateAccount - - - - diff --git a/src/leap/gui/ui/mainwindow.ui b/src/leap/gui/ui/mainwindow.ui deleted file mode 100644 index ecd3cbe9..00000000 --- a/src/leap/gui/ui/mainwindow.ui +++ /dev/null @@ -1,315 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 429 - 579 - - - - Bitmask - - - - :/images/mask-icon.png:/images/mask-icon.png - - - Qt::ImhHiddenText - - - - 128 - 128 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - - 40 - 0 - - - - - - - - There are new updates available, please restart. - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - More... - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - 1 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - - - - - - false - - - - - - :/images/mask-launcher.png - - - Qt::AlignCenter - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Show Log - - - true - - - false - - - true - - - - - - - - - - - 0 - 0 - 429 - 21 - - - - - &Session - - - - - - - - Help - - - - - - - - - - - - Log &out - - - - - &Quit - - - - - About &Bitmask - - - - - &Help - - - - - &Wizard - - - - - Show &logs - - - - - - - - - diff --git a/src/leap/gui/ui/statuspanel.ui b/src/leap/gui/ui/statuspanel.ui deleted file mode 100644 index 3482ac7c..00000000 --- a/src/leap/gui/ui/statuspanel.ui +++ /dev/null @@ -1,289 +0,0 @@ - - - StatusPanel - - - - 0 - 0 - 384 - 477 - - - - Form - - - - - - font: bold; - - - user@domain.org - - - true - - - - - - - true - - - - - - - - - - - Encrypted Internet: - - - - - - - font: bold; - - - Off - - - Qt::AutoText - - - Qt::AlignCenter - - - false - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Turn On - - - - - - - - - Qt::Vertical - - - QSizePolicy::Preferred - - - - 0 - 11 - - - - - - - - - 64 - 64 - - - - - - - :/images/light/64/network-eip-down.png - - - Qt::AlignCenter - - - - - - - 4 - - - QLayout::SetDefaultConstraint - - - - - - - - :/images/light/16/down-arrow.png - - - - - - - - 0 - 0 - - - - - 100 - 0 - - - - - 120 - 16777215 - - - - PointingHandCursor - - - 0.0 KB/s - - - true - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - - - - :/images/light/16/up-arrow.png - - - - - - - - 0 - 0 - - - - - 100 - 0 - - - - - 120 - 16777215 - - - - PointingHandCursor - - - 0.0 KB/s - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - false - - - - - - false - - - ... - - - true - - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - diff --git a/src/leap/gui/ui/wizard.ui b/src/leap/gui/ui/wizard.ui deleted file mode 100644 index a8f66bbc..00000000 --- a/src/leap/gui/ui/wizard.ui +++ /dev/null @@ -1,846 +0,0 @@ - - - Wizard - - - - 0 - 0 - 536 - 452 - - - - Bitmask first run - - - - :/images/mask-icon.png:/images/mask-icon.png - - - true - - - QWizard::ModernStyle - - - QWizard::IndependentPages - - - - Welcome - - - This is the Bitmask first run wizard - - - 0 - - - - - - Log In with my credentials - - - - - - - <html><head/><body><p>Now we will guide you through some configuration that is needed before you can connect for the first time.</p><p>If you ever need to modify these options again, you can find the wizard in the <span style=" font-style:italic;">'Settings'</span> menu from the main window.</p><p>Do you want to <span style=" font-weight:600;">sign up</span> for a new account, or <span style=" font-weight:600;">log in</span> with an already existing username?</p></body></html> - - - Qt::RichText - - - true - - - - - - - Sign up for a new account - - - true - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - Provider selection - - - Please enter the domain of the provider you want to use for your connection - - - 1 - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 60 - - - - - - - - - - - Check - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - https:// - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Checking for a valid provider - - - - - - Getting provider information - - - - - - - Can we stablish a secure connection? - - - - - - - - 0 - 0 - - - - - 24 - 24 - - - - - - - :/images/Emblem-question.png - - - - - - - - 0 - 0 - - - - - 24 - 24 - - - - - - - :/images/Emblem-question.png - - - - - - - - 0 - 0 - - - - - 24 - 24 - - - - - - - :/images/Emblem-question.png - - - - - - - Can we reach this provider? - - - - - - - Qt::Horizontal - - - - 40 - 0 - - - - - - - - - - - - - - - - - - - Provider Information - - - Description of services offered by this provider - - - 2 - - - - - - Name - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - 0 - 0 - - - - - 200 - 0 - - - - Desc - - - true - - - - - - - <b>Services offered:</b> - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - services - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - <b>Enrollment policy:</b> - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - policy - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - <b>URL:</b> - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - URL - - - - - - - <b>Description:</b> - - - Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing - - - - - - - - Provider setup - - - Gathering configuration options for this provider - - - 3 - - - - - - Qt::Vertical - - - - 20 - 60 - - - - - - - - We are downloading some bits that we need to establish a secure connection with the provider for the first time. - - - true - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Setting up provider - - - - - - - 0 - 0 - - - - - 24 - 24 - - - - - - - :/images/Emblem-question.png - - - - - - - - 0 - 0 - - - - - 24 - 24 - - - - - - - :/images/Emblem-question.png - - - - - - - Getting info from the Certificate Authority - - - - - - - Do we trust this Certificate Authority? - - - - - - - Establishing a trust relationship with this provider - - - - - - - - 0 - 0 - - - - - 24 - 24 - - - - - - - :/images/Emblem-question.png - - - - - - - Qt::Horizontal - - - - 40 - 0 - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - Register new user - - - Register a new user with provider - - - 4 - - - - QLayout::SetDefaultConstraint - - - 4 - - - - - <b>Password:</b> - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - - - - - <b>Re-enter password:</b> - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Register - - - - - - - Qt::Vertical - - - - 20 - 60 - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - <b>Username:</b> - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - false - - - Remember my username and password - - - - - - - - - - Qt::AutoText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - Service selection - - - Please select the services you would like to have - - - 5 - - - - - - Services by PROVIDER - - - - - - - - - - - - - Congratulations! - - - You have successfully configured Bitmask. - - - 6 - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - :/images/leap-color-big.png - - - - - - - - 0 - 0 - - - - - - - :/images/Globe.png - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - WizardPage - QWizardPage -
wizardpage.h
- 1 -
-
- - lblUser - lblPassword - lblPassword2 - btnRegister - rdoRegister - rdoLogin - lnProvider - btnCheck - - - - - - -
diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py deleted file mode 100644 index 2b48fc81..00000000 --- a/src/leap/gui/wizard.py +++ /dev/null @@ -1,626 +0,0 @@ -# -*- coding: utf-8 -*- -# wizard.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 . - -""" -First run wizard -""" -import os -import logging -import json - -from PySide import QtCore, QtGui -from functools import partial -from twisted.internet import threads - -from ui_wizard import Ui_Wizard -from leap.config.providerconfig import ProviderConfig -from leap.crypto.srpregister import SRPRegister -from leap.util.privilege_policies import is_missing_policy_permissions -from leap.util.request_helpers import get_content -from leap.util.keyring_helpers import has_keyring -from leap.services.eip.providerbootstrapper import ProviderBootstrapper -from leap.services import get_supported - -logger = logging.getLogger(__name__) - - -class Wizard(QtGui.QWizard): - """ - First run wizard to register a user and setup a provider - """ - - INTRO_PAGE = 0 - SELECT_PROVIDER_PAGE = 1 - PRESENT_PROVIDER_PAGE = 2 - SETUP_PROVIDER_PAGE = 3 - REGISTER_USER_PAGE = 4 - SERVICES_PAGE = 5 - FINISH_PAGE = 6 - - WEAK_PASSWORDS = ("123456", "qweasd", "qwerty", - "password") - - BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" - - def __init__(self, standalone=False, bypass_checks=False): - """ - Constructor for the main Wizard. - - :param standalone: If True, the application is running as standalone - and the wizard should display some messages according to this. - :type standalone: bool - :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) - - self.standalone = standalone - - self.ui = Ui_Wizard() - self.ui.setupUi(self) - - self.setPixmap(QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(":/images/leap-color-small.png")) - - self.QUESTION_ICON = QtGui.QPixmap(":/images/Emblem-question.png") - self.ERROR_ICON = QtGui.QPixmap(":/images/Dialog-error.png") - self.OK_ICON = QtGui.QPixmap(":/images/Dialog-accept.png") - - # Correspondence for services and their name to display - EIP_LABEL = self.tr("Encrypted Internet") - MX_LABEL = self.tr("Encrypted Mail") - - if self._is_need_eip_password_warning(): - EIP_LABEL += " " + self.tr( - "(will need admin password to start)") - - self.SERVICE_DISPLAY = [ - EIP_LABEL, - MX_LABEL - ] - self.SERVICE_CONFIG = [ - "openvpn", - "mx" - ] - - self._selected_services = set() - self._shown_services = set() - - self._show_register = False - - self.ui.grpCheckProvider.setVisible(False) - self.ui.btnCheck.clicked.connect(self._check_provider) - self.ui.lnProvider.returnPressed.connect(self._check_provider) - - self._provider_bootstrapper = ProviderBootstrapper(bypass_checks) - self._provider_bootstrapper.name_resolution.connect( - self._name_resolution) - self._provider_bootstrapper.https_connection.connect( - self._https_connection) - self._provider_bootstrapper.download_provider_info.connect( - self._download_provider_info) - - self._provider_bootstrapper.download_ca_cert.connect( - self._download_ca_cert) - self._provider_bootstrapper.check_ca_fingerprint.connect( - self._check_ca_fingerprint) - self._provider_bootstrapper.check_api_certificate.connect( - self._check_api_certificate) - - self._domain = None - self._provider_config = ProviderConfig() - - # We will store a reference to the defers for eventual use - # (eg, to cancel them) but not doing anything with them right now. - self._provider_select_defer = None - self._provider_setup_defer = None - - self.currentIdChanged.connect(self._current_id_changed) - - self.ui.lblPassword.setEchoMode(QtGui.QLineEdit.Password) - self.ui.lblPassword2.setEchoMode(QtGui.QLineEdit.Password) - - self.ui.lnProvider.textChanged.connect( - self._enable_check) - - self.ui.lblUser.returnPressed.connect( - self._focus_password) - self.ui.lblPassword.returnPressed.connect( - self._focus_second_password) - self.ui.lblPassword2.returnPressed.connect( - self._register) - self.ui.btnRegister.clicked.connect( - self._register) - - usernameRe = QtCore.QRegExp(self.BARE_USERNAME_REGEX) - self.ui.lblUser.setValidator( - QtGui.QRegExpValidator(usernameRe, self)) - - self.page(self.REGISTER_USER_PAGE).setCommitPage(True) - - self._username = None - self._password = None - - self.page(self.REGISTER_USER_PAGE).setButtonText( - QtGui.QWizard.CommitButton, self.tr("&Next >")) - self.page(self.FINISH_PAGE).setButtonText( - QtGui.QWizard.FinishButton, self.tr("Connect")) - - # XXX: Temporary removal for enrollment policy - # https://leap.se/code/issues/2922 - self.ui.label_12.setVisible(False) - self.ui.lblProviderPolicy.setVisible(False) - - def get_domain(self): - return self._domain - - def get_username(self): - return self._username - - def get_password(self): - return self._password - - def get_remember(self): - return has_keyring() and self.ui.chkRemember.isChecked() - - def get_services(self): - return self._selected_services - - def _enable_check(self, text): - self.ui.btnCheck.setEnabled(len(self.ui.lnProvider.text()) != 0) - self._reset_provider_check() - - def _focus_password(self): - """ - Focuses at the password lineedit for the registration page - """ - self.ui.lblPassword.setFocus() - - def _focus_second_password(self): - """ - Focuses at the second password lineedit for the registration page - """ - self.ui.lblPassword2.setFocus() - - def _basic_password_checks(self, username, password, password2): - """ - Performs basic password checks to avoid really easy passwords. - - :param username: username provided at the registrarion form - :type username: str - :param password: password from the registration form - :type password: str - :param password2: second password from the registration form - :type password: str - - :return: returns True if all the checks pass, False otherwise - :rtype: bool - """ - message = None - - if message is None and password != password2: - message = self.tr("Passwords don't match") - - if message is None and len(password) < 6: - message = self.tr("Password too short") - - if message is None and password in self.WEAK_PASSWORDS: - message = self.tr("Password too easy") - - if message is None and username == password: - message = self.tr("Password equal to username") - - if message is not None: - self._set_register_status(message, error=True) - self._focus_password() - return False - - return True - - def _register(self): - """ - Performs the registration based on the values provided in the form - """ - self.ui.btnRegister.setEnabled(False) - - username = self.ui.lblUser.text() - password = self.ui.lblPassword.text() - password2 = self.ui.lblPassword2.text() - - if self._basic_password_checks(username, password, password2): - register = SRPRegister(provider_config=self._provider_config) - register.registration_finished.connect( - self._registration_finished) - - threads.deferToThread( - partial(register.register_user, - username.encode("utf8"), - password.encode("utf8"))) - - self._username = username - self._password = password - self._set_register_status(self.tr("Starting registration...")) - else: - self.ui.btnRegister.setEnabled(True) - - def _set_registration_fields_visibility(self, visible): - """ - This method hides the username and password labels and inputboxes. - - :param visible: sets the visibility of the widgets - True: widgets are visible or False: are not - :type visible: bool - """ - # username and password inputs - self.ui.lblUser.setVisible(visible) - self.ui.lblPassword.setVisible(visible) - self.ui.lblPassword2.setVisible(visible) - - # username and password labels - self.ui.label_15.setVisible(visible) - self.ui.label_16.setVisible(visible) - self.ui.label_17.setVisible(visible) - - # register button - self.ui.btnRegister.setVisible(visible) - - def _registration_finished(self, ok, req): - if ok: - user_domain = self._username + "@" + self._domain - message = "

" - message += self.tr("User %s successfully registered.") % ( - user_domain, ) - message += "

" - self._set_register_status(message) - - self.ui.lblPassword2.clearFocus() - self._set_registration_fields_visibility(False) - - # Allow the user to remember his password - if has_keyring(): - self.ui.chkRemember.setVisible(True) - self.ui.chkRemember.setEnabled(True) - - self.page(self.REGISTER_USER_PAGE).set_completed() - self.button(QtGui.QWizard.BackButton).setEnabled(False) - else: - old_username = self._username - self._username = None - self._password = None - error_msg = self.tr("Unknown error") - try: - content, _ = get_content(req) - json_content = json.loads(content) - error_msg = json_content.get("errors").get("login")[0] - if not error_msg.istitle(): - error_msg = "%s %s" % (old_username, error_msg) - except Exception as e: - logger.error("Unknown error: %r" % (e,)) - - self._set_register_status(error_msg, error=True) - self.ui.btnRegister.setEnabled(True) - - def _set_register_status(self, status, error=False): - """ - Sets the status label in the registration page to status - - :param status: status message to display, can be HTML - :type status: str - """ - if error: - status = "%s" % (status,) - self.ui.lblRegisterStatus.setText(status) - - def _reset_provider_check(self): - """ - Resets the UI for checking a provider. Also resets the domain - in this object. - """ - self.ui.lblNameResolution.setPixmap(None) - self.ui.lblHTTPS.setPixmap(None) - self.ui.lblProviderInfo.setPixmap(None) - self.ui.lblProviderSelectStatus.setText("") - self._domain = None - self.button(QtGui.QWizard.NextButton).setEnabled(False) - self.page(self.SELECT_PROVIDER_PAGE).set_completed(False) - - def _reset_provider_setup(self): - """ - Resets the UI for setting up a provider. - """ - self.ui.lblDownloadCaCert.setPixmap(None) - self.ui.lblCheckCaFpr.setPixmap(None) - self.ui.lblCheckApiCert.setPixmap(None) - - def _check_provider(self): - """ - SLOT - TRIGGERS: - self.ui.btnCheck.clicked - self.ui.lnProvider.returnPressed - - Starts the checks for a given provider - """ - if len(self.ui.lnProvider.text()) == 0: - return - - self.ui.grpCheckProvider.setVisible(True) - self.ui.btnCheck.setEnabled(False) - self.ui.lnProvider.setEnabled(False) - self.button(QtGui.QWizard.BackButton).clearFocus() - self._domain = self.ui.lnProvider.text() - - self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) - self._provider_select_defer = self._provider_bootstrapper.\ - run_provider_select_checks(self._domain) - - def _complete_task(self, data, label, complete=False, complete_page=-1): - """ - Checks a task and completes a page if specified - - :param data: data as it comes from the bootstrapper thread for - a specific check - :type data: dict - :param label: label that displays the status icon for a - specific check that corresponds to the data - :type label: QtGui.QLabel - :param complete: if True, it completes the page specified, - which must be of type WizardPage - :type complete: bool - :param complete_page: page id to complete - :type complete_page: int - """ - passed = data[self._provider_bootstrapper.PASSED_KEY] - error = data[self._provider_bootstrapper.ERROR_KEY] - if passed: - label.setPixmap(self.OK_ICON) - if complete: - self.page(complete_page).set_completed() - self.button(QtGui.QWizard.NextButton).setFocus() - else: - label.setPixmap(self.ERROR_ICON) - logger.error(error) - - def _name_resolution(self, data): - """ - SLOT - TRIGGER: self._provider_bootstrapper.name_resolution - - Sets the status for the name resolution check - """ - self._complete_task(data, self.ui.lblNameResolution) - status = "" - passed = data[self._provider_bootstrapper.PASSED_KEY] - if not passed: - status = self.tr("Non-existent " - "provider") - else: - self.ui.lblHTTPS.setPixmap(self.QUESTION_ICON) - self.ui.lblProviderSelectStatus.setText(status) - self.ui.btnCheck.setEnabled(not passed) - self.ui.lnProvider.setEnabled(not passed) - - def _https_connection(self, data): - """ - SLOT - TRIGGER: self._provider_bootstrapper.https_connection - - Sets the status for the https connection check - """ - self._complete_task(data, self.ui.lblHTTPS) - status = "" - passed = data[self._provider_bootstrapper.PASSED_KEY] - if not passed: - status = self.tr("%s") \ - % (data[self._provider_bootstrapper.ERROR_KEY]) - self.ui.lblProviderSelectStatus.setText(status) - else: - self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) - self.ui.btnCheck.setEnabled(not passed) - self.ui.lnProvider.setEnabled(not passed) - - def _download_provider_info(self, data): - """ - SLOT - TRIGGER: self._provider_bootstrapper.download_provider_info - - Sets the status for the provider information download - check. Since this check is the last of this set, it also - completes the page if passed - """ - if self._provider_config.load(os.path.join("leap", - "providers", - self._domain, - "provider.json")): - self._complete_task(data, self.ui.lblProviderInfo, - True, self.SELECT_PROVIDER_PAGE) - else: - new_data = { - self._provider_bootstrapper.PASSED_KEY: False, - self._provider_bootstrapper.ERROR_KEY: - self.tr("Unable to load provider configuration") - } - self._complete_task(new_data, self.ui.lblProviderInfo) - - status = "" - if not data[self._provider_bootstrapper.PASSED_KEY]: - status = self.tr("Not a valid provider" - "") - self.ui.lblProviderSelectStatus.setText(status) - self.ui.btnCheck.setEnabled(True) - self.ui.lnProvider.setEnabled(True) - - def _download_ca_cert(self, data): - """ - SLOT - TRIGGER: self._provider_bootstrapper.download_ca_cert - - Sets the status for the download of the CA certificate check - """ - self._complete_task(data, self.ui.lblDownloadCaCert) - passed = data[self._provider_bootstrapper.PASSED_KEY] - if passed: - self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON) - - def _check_ca_fingerprint(self, data): - """ - SLOT - TRIGGER: self._provider_bootstrapper.check_ca_fingerprint - - Sets the status for the CA fingerprint check - """ - self._complete_task(data, self.ui.lblCheckCaFpr) - passed = data[self._provider_bootstrapper.PASSED_KEY] - if passed: - self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON) - - def _check_api_certificate(self, data): - """ - SLOT - TRIGGER: self._provider_bootstrapper.check_api_certificate - - Sets the status for the API certificate check. Also finishes - the provider bootstrapper thread since it's not needed anymore - from this point on, unless the whole check chain is restarted - """ - self._complete_task(data, self.ui.lblCheckApiCert, - True, self.SETUP_PROVIDER_PAGE) - - def _service_selection_changed(self, service, state): - """ - SLOT - TRIGGER: service_checkbox.stateChanged - Adds the service to the state if the state is checked, removes - it otherwise - - :param service: service to handle - :type service: str - :param state: state of the checkbox - :type state: int - """ - if state == QtCore.Qt.Checked: - self._selected_services = \ - self._selected_services.union(set([service])) - else: - self._selected_services = \ - self._selected_services.difference(set([service])) - - def _populate_services(self): - """ - Loads the services that the provider provides into the UI for - the user to enable or disable. - """ - self.ui.grpServices.setTitle( - self.tr("Services by %s") % - (self._provider_config.get_name(),)) - - services = get_supported( - self._provider_config.get_services()) - - for service in services: - try: - if service not in self._shown_services: - checkbox = QtGui.QCheckBox(self) - service_index = self.SERVICE_CONFIG.index(service) - checkbox.setText(self.SERVICE_DISPLAY[service_index]) - self.ui.serviceListLayout.addWidget(checkbox) - checkbox.stateChanged.connect( - partial(self._service_selection_changed, service)) - checkbox.setChecked(True) - self._shown_services.add(service) - except ValueError: - logger.error( - self.tr("Something went wrong while trying to " - "load service %s" % (service,))) - - def _current_id_changed(self, pageId): - """ - SLOT - TRIGGER: self.currentIdChanged - - Prepares the pages when they appear - """ - if pageId == self.SELECT_PROVIDER_PAGE: - self._reset_provider_check() - self._enable_check("") - - if pageId == self.SETUP_PROVIDER_PAGE: - self._reset_provider_setup() - self.page(pageId).setSubTitle(self.tr("Gathering configuration " - "options for %s") % - (self._provider_config - .get_name(),)) - self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON) - self._provider_setup_defer = self._provider_bootstrapper.\ - run_provider_setup_checks(self._provider_config) - - if pageId == self.PRESENT_PROVIDER_PAGE: - self.page(pageId).setSubTitle(self.tr("Description of services " - "offered by %s") % - (self._provider_config - .get_name(),)) - - lang = QtCore.QLocale.system().name() - self.ui.lblProviderName.setText( - "%s" % - (self._provider_config.get_name(lang=lang),)) - self.ui.lblProviderURL.setText( - "https://%s" % (self._provider_config.get_domain(),)) - self.ui.lblProviderDesc.setText( - "%s" % - (self._provider_config.get_description(lang=lang),)) - - self.ui.lblServicesOffered.setText(self._provider_config - .get_services_string()) - self.ui.lblProviderPolicy.setText(self._provider_config - .get_enrollment_policy()) - - if pageId == self.REGISTER_USER_PAGE: - self.page(pageId).setSubTitle(self.tr("Register a new user with " - "%s") % - (self._provider_config - .get_name(),)) - self.ui.chkRemember.setVisible(False) - - if pageId == self.SERVICES_PAGE: - self._populate_services() - - def _is_need_eip_password_warning(self): - """ - Returns True if we need to add a warning about eip needing - administrative permissions to start. That can be either - because we are running in standalone mode, or because we could - not find the needed privilege escalation mechanisms being operative. - """ - return self.standalone or is_missing_policy_permissions() - - def nextId(self): - """ - Sets the next page id for the wizard based on wether the user - wants to register a new identity or uses an existing one - """ - if self.currentPage() == self.page(self.INTRO_PAGE): - self._show_register = self.ui.rdoRegister.isChecked() - - if self.currentPage() == self.page(self.SETUP_PROVIDER_PAGE): - if self._show_register: - return self.REGISTER_USER_PAGE - else: - return self.SERVICES_PAGE - - return QtGui.QWizard.nextId(self) diff --git a/src/leap/gui/wizardpage.py b/src/leap/gui/wizardpage.py deleted file mode 100644 index b2a00028..00000000 --- a/src/leap/gui/wizardpage.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# wizardpage.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 . - -from PySide import QtGui - - -class WizardPage(QtGui.QWizardPage): - """ - Simple wizard page helper - """ - - def __init__(self): - QtGui.QWizardPage.__init__(self) - self._completed = False - - def set_completed(self, val=True): - self._completed = val - if val: - self.completeChanged.emit() - - def isComplete(self): - return self._completed - - def cleanupPage(self): - self._completed = False - QtGui.QWizardPage.cleanupPage(self) diff --git a/src/leap/platform_init/__init__.py b/src/leap/platform_init/__init__.py deleted file mode 100644 index 2a262a30..00000000 --- a/src/leap/platform_init/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -# __init__.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 . - -""" -System constants -""" -import platform - -_system = platform.system() - -IS_WIN = True if _system == "Windows" else False -IS_MAC = True if _system == "Darwin" else False -IS_LINUX = True if _system == "Linux" else False -IS_UNIX = IS_MAC or IS_LINUX diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py deleted file mode 100644 index 46488250..00000000 --- a/src/leap/platform_init/initializers.py +++ /dev/null @@ -1,409 +0,0 @@ -# -*- coding: utf-8 -*- -# initializers.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 . - -""" -Platform dependant initializing code -""" - -import logging -import os -import platform -import stat -import subprocess -import tempfile - -from PySide import QtGui - -from leap.config.leapsettings import LeapSettings -from leap.services.eip import vpnlaunchers -from leap.util import first -from leap.util import privilege_policies - - -logger = logging.getLogger(__name__) - -# NOTE we could use a deferToThread here, but should -# be aware of this bug: http://www.themacaque.com/?p=1067 - -__all__ = ["init_platform"] - -_system = platform.system() - - -def init_platform(): - """ - Returns the right initializer for the platform we are running in, or - None if no proper initializer is found - """ - initializer = None - try: - initializer = globals()[_system + "Initializer"] - except: - pass - if initializer: - logger.debug("Running initializer for %s" % (platform.system(),)) - initializer() - else: - logger.debug("Initializer not found for %s" % (platform.system(),)) - - -# -# common utils -# - -NOTFOUND_MSG = ("Tried to install %s, but %s " - "not found inside this bundle.") -BADEXEC_MSG = ("Tried to install %s, but %s " - "failed to %s.") - -UPDOWN_NOTFOUND_MSG = NOTFOUND_MSG % ( - "updown scripts", "those were") -UPDOWN_BADEXEC_MSG = BADEXEC_MSG % ( - "updown scripts", "they", "be copied") - - -def get_missing_updown_dialog(): - """ - Creates a dialog for notifying of missing updown scripts. - Returns that dialog. - - :rtype: QtGui.QMessageBox instance - """ - WE_NEED_POWERS = ("To better protect your privacy, " - "Bitmask needs administrative privileges " - "to install helper files. " - "Do you want to proceed?") - msg = QtGui.QMessageBox() - msg.setWindowTitle(msg.tr("Missing up/down scripts")) - msg.setText(msg.tr(WE_NEED_POWERS)) - # but maybe the user really deserve to know more - #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) - return msg - - -def check_missing(): - """ - Checks for the need of installing missing scripts, and - raises a dialog to ask user for permission to do it. - """ - config = LeapSettings() - alert_missing = config.get_alert_missing_scripts() - - launcher = vpnlaunchers.get_platform_launcher() - missing_scripts = launcher.missing_updown_scripts - missing_other = launcher.missing_other_files - - if alert_missing and (missing_scripts() or missing_other()): - msg = get_missing_updown_dialog() - ret = msg.exec_() - - if ret == QtGui.QMessageBox.Yes: - install_missing_fun = globals().get( - "_%s_install_missing_scripts" % (_system.lower(),), - None) - if not install_missing_fun: - logger.warning( - "Installer not found for platform %s." % (_system,)) - return - - # XXX maybe move constants to fun - ok = install_missing_fun(UPDOWN_BADEXEC_MSG, UPDOWN_NOTFOUND_MSG) - if not ok: - msg = QtGui.QMessageBox() - msg.setWindowTitle(msg.tr("Problem installing files")) - msg.setText(msg.tr('Some of the files could not be copied.')) - msg.setIcon(QtGui.QMessageBox.Warning) - msg.exec_() - - elif ret == QtGui.QMessageBox.No: - logger.debug("Not installing missing scripts, " - "user decided to ignore our warning.") - - elif ret == QtGui.QMessageBox.Rejected: - logger.debug( - "Setting alert_missing_scripts to False, we will not " - "ask again") - config.set_alert_missing_scripts(False) -# -# windows initializers -# - - -def _windows_has_tap_device(): - """ - Loops over the windows registry trying to find if the tap0901 tap driver - has been installed on this machine. - """ - import _winreg as reg - - adapter_key = 'SYSTEM\CurrentControlSet\Control\Class' \ - '\{4D36E972-E325-11CE-BFC1-08002BE10318}' - with reg.OpenKey(reg.HKEY_LOCAL_MACHINE, adapter_key) as adapters: - try: - for i in xrange(10000): - key_name = reg.EnumKey(adapters, i) - with reg.OpenKey(adapters, key_name) as adapter: - try: - component_id = reg.QueryValueEx(adapter, - 'ComponentId')[0] - if component_id.startswith("tap0901"): - return True - except WindowsError: - pass - except WindowsError: - pass - return False - - -def WindowsInitializer(): - """ - Raises a dialog in case that the windows tap driver has not been found - in the registry, asking the user for permission to install the driver - """ - if not _windows_has_tap_device(): - msg = QtGui.QMessageBox() - msg.setWindowTitle(msg.tr("TAP Driver")) - msg.setText(msg.tr("Bitmask needs to install the necessary drivers " - "for Encrypted Internet to work. Would you like to " - "proceed?")) - msg.setInformativeText(msg.tr("Encrypted Internet uses VPN, which " - "needs a TAP device installed and none " - "has been found. This will ask for " - "administrative privileges.")) - msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) - msg.setDefaultButton(QtGui.QMessageBox.Yes) - ret = msg.exec_() - - if ret == QtGui.QMessageBox.Yes: - # XXX should do this only if executed inside bundle. - # Let's assume it's the only way it's gonna be executed under win - # by now. - driver_path = os.path.join(os.getcwd(), - "apps", - "eip", - "tap_driver") - dev_installer = os.path.join(driver_path, - "devcon.exe") - if os.path.isfile(dev_installer) and \ - stat.S_IXUSR & os.stat(dev_installer)[stat.ST_MODE] != 0: - inf_path = os.path.join(driver_path, - "OemWin2k.inf") - cmd = [dev_installer, "install", inf_path, "tap0901"] - ret = subprocess.call(cmd, stdout=subprocess.PIPE, shell=True) - else: - logger.error("Tried to install TAP driver, but the installer " - "is not found or not executable") - -# -# Darwin initializer functions -# - - -def _darwin_has_tun_kext(): - """ - Returns True only if we found a directory under the system kext folder - containing a kext named tun.kext, AND we found a startup item named 'tun' - """ - # XXX we should be smarter here and use kextstats output. - - has_kext = os.path.isdir("/System/Library/Extensions/tun.kext") - has_startup = os.path.isdir("/System/Library/StartupItems/tun") - has_tun_and_startup = has_kext and has_startup - logger.debug( - 'platform initializer check: has tun_and_startup = %s' % - (has_tun_and_startup,)) - return has_tun_and_startup - - -def _darwin_install_missing_scripts(badexec, notfound): - """ - Tries to install the missing up/down scripts. - - :param badexec: error for notifying execution error during command. - :type badexec: str - :param notfound: error for notifying missing path. - :type notfound: str - :returns: True if the files could be copied successfully. - :rtype: bool - """ - # We expect to execute this from some way of bundle, since - # the up/down scripts should be put in place by the installer. - success = False - installer_path = os.path.join( - os.getcwd(), - "..", - "Resources", - "openvpn") - launcher = vpnlaunchers.DarwinVPNLauncher - - if os.path.isdir(installer_path): - fd, tempscript = tempfile.mkstemp(prefix="leap_installer-") - try: - scriptlines = launcher.cmd_for_missing_scripts(installer_path) - with os.fdopen(fd, 'w') as f: - f.write(scriptlines) - st = os.stat(tempscript) - os.chmod(tempscript, st.st_mode | stat.S_IEXEC | stat.S_IXUSR | - stat.S_IXGRP | stat.S_IXOTH) - - cmd, args = launcher().get_cocoasudo_installmissing_cmd() - args.append(tempscript) - cmdline = " ".join([cmd] + args) - ret = subprocess.call( - cmdline, stdout=subprocess.PIPE, - shell=True) - success = ret == 0 - if not success: - logger.error("Install missing scripts failed.") - except Exception as exc: - logger.error(badexec) - logger.error("Error was: %r" % (exc,)) - finally: - try: - os.remove(tempscript) - except OSError as exc: - logger.error("%r" % (exc,)) - else: - logger.error(notfound) - logger.debug('path searched: %s' % (installer_path,)) - - return success - - -def DarwinInitializer(): - """ - Raises a dialog in case that the osx tuntap driver has not been found - in the registry, asking the user for permission to install the driver - """ - # XXX split this function into several - - TUNTAP_NOTFOUND_MSG = NOTFOUND_MSG % ( - "tuntaposx kext", "the installer") - TUNTAP_BADEXEC_MSG = BADEXEC_MSG % ( - "tuntaposx kext", "the installer", "be launched") - - # TODO DRY this with other cases, and - # factor out to _should_install() function. - # Leave the dialog as a more generic thing. - - if not _darwin_has_tun_kext(): - msg = QtGui.QMessageBox() - msg.setWindowTitle(msg.tr("TUN Driver")) - msg.setText(msg.tr("Bitmask needs to install the necessary drivers " - "for Encrypted Internet to work. Would you like to " - "proceed?")) - msg.setInformativeText(msg.tr("Encrypted Internet uses VPN, which " - "needs a kernel extension for a TUN " - "device installed, and none " - "has been found. This will ask for " - "administrative privileges.")) - msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) - msg.setDefaultButton(QtGui.QMessageBox.Yes) - ret = msg.exec_() - - if ret == QtGui.QMessageBox.Yes: - installer_path = os.path.abspath( - os.path.join( - os.getcwd(), - "..", - "Resources", - "tuntap-installer.app")) - if os.path.isdir(installer_path): - cmd = ["open '%s'" % (installer_path,)] - try: - ret = subprocess.call( - cmd, stdout=subprocess.PIPE, - shell=True) - except: - logger.error(TUNTAP_BADEXEC_MSG) - else: - logger.error(TUNTAP_NOTFOUND_MSG) - - # Second check, for missing scripts. - check_missing() - - -# -# Linux initializers -# -def _linux_install_missing_scripts(badexec, notfound): - """ - Tries to install the missing up/down scripts. - - :param badexec: error for notifying execution error during command. - :type badexec: str - :param notfound: error for notifying missing path. - :type notfound: str - :returns: True if the files could be copied successfully. - :rtype: bool - """ - success = False - installer_path = os.path.join(os.getcwd(), "apps", "eip", "files") - launcher = vpnlaunchers.LinuxVPNLauncher - - # XXX refactor with darwin, same block. - - if os.path.isdir(installer_path): - fd, tempscript = tempfile.mkstemp(prefix="leap_installer-") - polfd, pol_tempfile = tempfile.mkstemp(prefix="leap_installer-") - try: - path = launcher.OPENVPN_BIN_PATH - policy_contents = privilege_policies.get_policy_contents(path) - - with os.fdopen(polfd, 'w') as f: - f.write(policy_contents) - - pkexec = first(launcher.maybe_pkexec()) - scriptlines = launcher.cmd_for_missing_scripts(installer_path, - pol_tempfile) - with os.fdopen(fd, 'w') as f: - f.write(scriptlines) - - st = os.stat(tempscript) - os.chmod(tempscript, st.st_mode | stat.S_IEXEC | stat.S_IXUSR | - stat.S_IXGRP | stat.S_IXOTH) - cmdline = ["%s %s" % (pkexec, tempscript)] - ret = subprocess.call( - cmdline, stdout=subprocess.PIPE, - shell=True) - success = ret == 0 - if not success: - logger.error("Install missing scripts failed.") - except Exception as exc: - logger.error(badexec) - logger.error("Error was: %r" % (exc,)) - finally: - try: - os.remove(tempscript) - except OSError as exc: - logger.error("%r" % (exc,)) - else: - logger.error(notfound) - logger.debug('path searched: %s' % (installer_path,)) - - return success - - -def LinuxInitializer(): - """ - Raises a dialog in case that either updown scripts or policykit file - are missing or they have incorrect permissions. - """ - check_missing() diff --git a/src/leap/platform_init/locks.py b/src/leap/platform_init/locks.py deleted file mode 100644 index 39b18648..00000000 --- a/src/leap/platform_init/locks.py +++ /dev/null @@ -1,405 +0,0 @@ -# -*- coding: utf-8 -*- -# locks.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 . -""" -Utilities for handling multi-platform file locking mechanisms -""" -import logging -import errno -import os -import platform - -from leap.common.events import signal as signal_event -from leap.common.events import events_pb2 as proto -from leap import platform_init - -if platform_init.IS_UNIX: - from fcntl import flock, LOCK_EX, LOCK_NB -else: # WINDOWS - import datetime - import glob - import shutil - import time - - from tempfile import gettempdir - - from leap.util import get_modification_ts, update_modification_ts - -logger = logging.getLogger(__name__) - -if platform_init.IS_UNIX: - - class UnixLock(object): - """ - Uses flock to get an exclusive lock over a file. - See man 2 flock - """ - - def __init__(self, path): - """ - iniializes t he UnixLock with the path of the - desired lockfile - """ - - self._fd = None - self.path = path - - def get_lock(self): - """ - Tries to get a lock, and writes the running pid there if successful - """ - gotit, pid = self._get_lock_and_pid() - return gotit - - def get_pid(self): - """ - Returns the pid of the locking process - """ - gotit, pid = self._get_lock_and_pid() - return pid - - def _get_lock(self): - """ - Tries to get a lock, returning True if successful - - :rtype: bool - """ - self._fd = os.open(self.path, os.O_CREAT | os.O_RDWR) - - try: - flock(self._fd, LOCK_EX | LOCK_NB) - except IOError as exc: - # could not get the lock - #import ipdb; ipdb.set_trace() - - if exc.args[0] in (errno.EDEADLK, errno.EAGAIN): - # errno 11 or 35 - # Resource temporarily unavailable - return False - else: - raise - return True - - @property - def locked_by_us(self): - """ - Returns True if the pid in the pidfile - is ours. - - :rtype: bool - """ - gotit, pid = self._get_lock_and_pid() - return pid == os.getpid() - - def _get_lock_and_pid(self): - """ - Tries to get a lock over the file. - Returns (locked, pid) tuple. - - :rtype: tuple - """ - - if self._get_lock(): - self._write_to_pidfile() - return True, None - - return False, self._read_from_pidfile() - - def _read_from_pidfile(self): - """ - Tries to read pid from the pidfile, - returns False if no content found. - """ - - pidfile = os.read( - self._fd, 16) - if not pidfile: - return False - - try: - return int(pidfile.strip()) - except Exception as exc: - exc.args += (pidfile, self.lock_file) - raise - - def _write_to_pidfile(self): - """ - Writes the pid of the running process - to the pidfile - """ - fd = self._fd - os.ftruncate(fd, 0) - os.write(fd, '%d\n' % os.getpid()) - os.fsync(fd) - - -if platform_init.IS_WIN: - - # Time to wait (in secs) before assuming a raise window signal has not been - # ack-ed. - - RAISE_WINDOW_TIMEOUT = 2 - - # How many steps to do while checking lockfile ts update. - - RAISE_WINDOW_WAIT_STEPS = 10 - - def _release_lock(name): - """ - Tries to remove a folder path. - - :param name: folder lock to remove - :type name: str - """ - try: - shutil.rmtree(name) - return True - except WindowsError as exc: - if exc.errno in (errno.EPIPE, errno.ENOENT, - errno.ESRCH, errno.EACCES): - logger.warning( - 'exception while trying to remove the lockfile dir') - logger.warning('errno %s: %s' % (exc.errno, exc.args[1])) - # path does not exist - return False - else: - logger.debug('errno = %s' % (exc.errno,)) - # we did not foresee this error, better add it explicitely - raise - - class WindowsLock(object): - """ - Creates a lock based on the atomic nature of mkdir on Windows - system calls. - """ - LOCKBASE = os.path.join(gettempdir(), "leap-client-lock") - - def __init__(self): - """ - Initializes the lock. - Sets the lock name to basename plus the process pid. - """ - self._fd = None - pid = os.getpid() - self.name = "%s-%s" % (self.LOCKBASE, pid) - self.pid = pid - - def get_lock(self): - """ - Tries to get a lock, and writes the running pid there if successful - """ - gotit = self._get_lock() - return gotit - - def _get_lock(self): - """ - Tries to write to a file with the current pid as part of the name - """ - try: - self._fd = os.makedirs(self.name) - except OSError as exc: - # could not create the dir - if exc.args[0] == 183: - logger.debug('cannot create dir') - # cannot create dir with existing name - return False - else: - raise - return self._is_one_pidfile()[0] - - def _is_one_pidfile(self): - """ - Returns True, pid if there is only one pidfile with the expected - base path - - :rtype: tuple - """ - pidfiles = glob.glob(self.LOCKBASE + '-*') - if len(pidfiles) == 1: - pid = pidfiles[0].split('-')[-1] - return True, int(pid) - else: - return False, None - - def get_pid(self): - """ - Returns the pid of the locking process. - - :rtype: int - """ - # XXX assert there is only one? - _, pid = self._is_one_pidfile() - return pid - - def get_locking_path(self): - """ - Returns the pid path of the locking process. - - :rtype: str - """ - pid = self.get_pid() - if pid: - return "%s-%s" % (self.LOCKBASE, pid) - - def release_lock(self, name=None): - """ - Releases the pidfile dir for this process, by removing it. - """ - if not name: - name = self.name - _release_lock(name) - - @classmethod - def release_all_locks(self): - """ - Releases all locks. Used for clean shutdown. - """ - for lockdir in glob.glob("%s-%s" % (self.LOCKBASE, '*')): - _release_lock(lockdir) - - @property - def locked_by_us(self): - """ - Returns True if the pid in the pidfile - is ours. - - :rtype: bool - """ - _, pid = self._is_one_pidfile() - return pid == self.pid - - def update_ts(self): - """ - Updates the timestamp of the lock. - """ - if self.locked_by_us: - update_modification_ts(self.name) - - def write_port(self, port): - """ - Writes the port for windows control to the pidfile folder - Returns True if successful. - - :rtype: bool - """ - if not self.locked_by_us: - logger.warning("Tried to write control port to a " - "non-unique pidfile folder") - return False - port_file = os.path.join(self.name, "port") - with open(port_file, 'w') as f: - f.write("%s" % port) - return True - - def get_control_port(self): - """ - Reads control port of the main instance from the port file - in the pidfile dir - - :rtype: int - """ - pid = self.get_pid() - port_file = os.path.join(self.LOCKBASE + "-%s" % pid, "port") - port = None - try: - with open(port_file) as f: - port_str = f.read() - port = int(port_str.strip()) - except IOError as exc: - if exc.errno == errno.ENOENT: - logger.error("Tried to read port from non-existent file") - else: - # we did not know explicitely about this error - raise - return port - - def raise_window_ack(): - """ - This function is called from the windows callback that is registered - with the raise_window event. It just updates the modification time - of the lock file so we can signal an ack to the instance that tried - to raise the window. - """ - lock = WindowsLock() - lock.update_ts() - - -def we_are_the_one_and_only(): - """ - Returns True if we are the only instance running, False otherwise. - If we came later, send a raise signal to the main instance of the - application. - - Under windows we are not using flock magic, so we wait during - RAISE_WINDOW_TIMEOUT time, if not ack is - received, we assume it was a stalled lock, so we remove it and continue - with initialization. - - :rtype: bool - """ - _sys = platform.system() - - if _sys in ("Linux", "Darwin"): - locker = UnixLock('/tmp/leap-client.lock') - locker.get_lock() - we_are_the_one = locker.locked_by_us - if not we_are_the_one: - signal_event(proto.RAISE_WINDOW) - return we_are_the_one - - elif _sys == "Windows": - locker = WindowsLock() - locker.get_lock() - we_are_the_one = locker.locked_by_us - - if not we_are_the_one: - locker.release_lock() - lock_path = locker.get_locking_path() - ts = get_modification_ts(lock_path) - - nowfun = datetime.datetime.now - t0 = nowfun() - pause = RAISE_WINDOW_TIMEOUT / float(RAISE_WINDOW_WAIT_STEPS) - timeout_delta = datetime.timedelta(0, RAISE_WINDOW_TIMEOUT) - check_interval = lambda: nowfun() - t0 < timeout_delta - - # let's assume it's a stalled lock - we_are_the_one = True - signal_event(proto.RAISE_WINDOW) - - while check_interval(): - if get_modification_ts(lock_path) > ts: - # yay! someone claimed their control over the lock. - # so the lock is alive - logger.debug('Raise window ACK-ed') - we_are_the_one = False - break - else: - time.sleep(pause) - - if we_are_the_one: - # ok, it really was a stalled lock. let's remove all - # that is left, and put only ours there. - WindowsLock.release_all_locks() - WindowsLock().get_lock() - - return we_are_the_one - - else: - logger.warning("Multi-instance checker " - "not implemented for %s" % (_sys)) - # lies, lies, lies... - return True diff --git a/src/leap/provider/__init__.py b/src/leap/provider/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/provider/supportedapis.py b/src/leap/provider/supportedapis.py deleted file mode 100644 index 3e650ba2..00000000 --- a/src/leap/provider/supportedapis.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# supportedapis.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 . - -""" -API Support check. -""" - - -class SupportedAPIs(object): - """ - Class responsible of checking for API compatibility. - """ - SUPPORTED_APIS = ["1"] - - @classmethod - def supports(self, api_version): - """ - :param api_version: the version number of the api that we need to check - :type api_version: str - - :returns: if that version is supported or not. - :return type: bool - """ - return api_version in self.SUPPORTED_APIS diff --git a/src/leap/services/__init__.py b/src/leap/services/__init__.py deleted file mode 100644 index 253359cd..00000000 --- a/src/leap/services/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -# __init__.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 . -""" -Services module. -""" -DEPLOYED = ["openvpn", "mx"] - - -def get_supported(services): - """ - Returns a list of the available services. - - :param services: a list containing the services to be filtered. - :type services: list of str - - :returns: a list of the available services - :rtype: list of str - """ - return filter(lambda s: s in DEPLOYED, services) diff --git a/src/leap/services/abstractbootstrapper.py b/src/leap/services/abstractbootstrapper.py deleted file mode 100644 index 9e50948c..00000000 --- a/src/leap/services/abstractbootstrapper.py +++ /dev/null @@ -1,163 +0,0 @@ -# -*- coding: utf-8 -*- -# abstractbootstrapper.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 . - -""" -Abstract bootstrapper implementation -""" -import logging - -import requests - -from functools import partial - -from PySide import QtCore -from twisted.internet import threads -from leap.common.check import leap_assert, leap_assert_type - -logger = logging.getLogger(__name__) - - -class AbstractBootstrapper(QtCore.QObject): - """ - Abstract Bootstrapper that implements the needed deferred callbacks - """ - - PASSED_KEY = "passed" - ERROR_KEY = "error" - - def __init__(self, bypass_checks=False): - """ - Constructor for the abstract bootstrapper - - :param bypass_checks: Set to true if the app should bypass - first round of checks for CA - certificates at bootstrap - :type bypass_checks: bool - """ - QtCore.QObject.__init__(self) - - leap_assert(self._gui_errback.im_func == - AbstractBootstrapper._gui_errback.im_func, - "Cannot redefine _gui_errback") - leap_assert(self._errback.im_func == - AbstractBootstrapper._errback.im_func, - "Cannot redefine _errback") - leap_assert(self._gui_notify.im_func == - AbstractBootstrapper._gui_notify.im_func, - "Cannot redefine _gui_notify") - - # **************************************************** # - # Dependency injection helpers, override this for more - # granular testing - self._fetcher = requests - # **************************************************** # - - self._session = self._fetcher.session() - self._bypass_checks = bypass_checks - self._signal_to_emit = None - self._err_msg = None - - def _gui_errback(self, failure): - """ - Errback used to notify the GUI of a problem, it should be used - as the last errback of the whole chain. - - Traps all exceptions if a signal is defined, otherwise it just - lets it continue. - - NOTE: This method is final, it should not be redefined. - - :param failure: failure object that Twisted generates - :type failure: twisted.python.failure.Failure - """ - if self._signal_to_emit: - err_msg = self._err_msg \ - if self._err_msg is not None \ - else str(failure.value) - self._signal_to_emit.emit({ - self.PASSED_KEY: False, - self.ERROR_KEY: err_msg - }) - failure.trap(Exception) - - def _errback(self, failure, signal=None): - """ - Regular errback used for the middle of the chain. If it's - executed, the first one will set the signal to emit as - failure. - - NOTE: This method is final, it should not be redefined. - - :param failure: failure object that Twisted generates - :type failure: twisted.python.failure.Failure - :param signal: Signal to emit if it fails here first - :type signal: QtCore.SignalInstance - - :returns: failure object that Twisted generates - :rtype: twisted.python.failure.Failure - """ - if self._signal_to_emit is None: - self._signal_to_emit = signal - return failure - - def _gui_notify(self, _, signal=None): - """ - Callback used to notify the GUI of a success. Will emit signal - if specified - - NOTE: This method is final, it should not be redefined. - - :param _: IGNORED. Returned from the previous callback - :type _: IGNORED - :param signal: Signal to emit if it fails here first - :type signal: QtCore.SignalInstance - """ - if signal: - logger.debug("Emitting %s" % (signal,)) - signal.emit({self.PASSED_KEY: True, self.ERROR_KEY: ""}) - - def _callback_threader(self, cb, res, *args, **kwargs): - return threads.deferToThread(cb, res, *args, **kwargs) - - def addCallbackChain(self, callbacks): - """ - Creates a callback/errback chain on another thread using - deferToThread and adds the _gui_errback to the end to notify - the GUI on an error. - - :param callbacks: List of tuples of callbacks and the signal - associated to that callback - :type callbacks: list(tuple(func, func)) - - :returns: the defer with the callback chain - :rtype: deferred - """ - leap_assert_type(callbacks, list) - - self._signal_to_emit = None - self._err_msg = None - - d = None - for cb, sig in callbacks: - if d is None: - d = threads.deferToThread(cb) - else: - d.addCallback(partial(self._callback_threader, cb)) - d.addErrback(self._errback, signal=sig) - d.addCallback(self._gui_notify, signal=sig) - d.addErrback(self._gui_errback) - return d diff --git a/src/leap/services/eip/__init__.py b/src/leap/services/eip/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py deleted file mode 100644 index 1d7bc342..00000000 --- a/src/leap/services/eip/eipbootstrapper.py +++ /dev/null @@ -1,183 +0,0 @@ -# -*- coding: utf-8 -*- -# eipbootstrapper.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 . - -""" -EIP bootstrapping -""" - -import logging -import os - -from PySide import QtCore - -from leap.common.check import leap_assert, leap_assert_type -from leap.common import certs -from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p -from leap.config.providerconfig import ProviderConfig -from leap.crypto.srpauth import SRPAuth -from leap.services.eip.eipconfig import EIPConfig -from leap.util.request_helpers import get_content -from leap.util.constants import REQUEST_TIMEOUT -from leap.services.abstractbootstrapper import AbstractBootstrapper - -logger = logging.getLogger(__name__) - - -class EIPBootstrapper(AbstractBootstrapper): - """ - Sets up EIP for a provider a series of checks and emits signals - after they are passed. - If a check fails, the subsequent checks are not executed - """ - - # All dicts returned are of the form - # {"passed": bool, "error": str} - download_config = QtCore.Signal(dict) - download_client_certificate = QtCore.Signal(dict) - - def __init__(self): - AbstractBootstrapper.__init__(self) - - self._provider_config = None - self._eip_config = None - self._download_if_needed = False - - def _download_config(self, *args): - """ - Downloads the EIP config for the given provider - """ - - leap_assert(self._provider_config, - "We need a provider configuration!") - - logger.debug("Downloading EIP config for %s" % - (self._provider_config.get_domain(),)) - - api_version = self._provider_config.get_api_version() - self._eip_config = EIPConfig() - self._eip_config.set_api_version(api_version) - - headers = {} - mtime = get_mtime(os.path.join(self._eip_config - .get_path_prefix(), - "leap", - "providers", - self._provider_config.get_domain(), - "eip-service.json")) - - if self._download_if_needed and mtime: - headers['if-modified-since'] = mtime - - # there is some confusion with this uri, - # it's in 1/config/eip, config/eip and config/1/eip... - config_uri = "%s/%s/config/eip-service.json" % ( - self._provider_config.get_api_uri(), - api_version) - logger.debug('Downloading eip config from: %s' % config_uri) - - res = self._session.get(config_uri, - verify=self._provider_config - .get_ca_cert_path(), - headers=headers, - timeout=REQUEST_TIMEOUT) - res.raise_for_status() - - # Not modified - if res.status_code == 304: - logger.debug("EIP definition has not been modified") - else: - eip_definition, mtime = get_content(res) - - self._eip_config.load(data=eip_definition, mtime=mtime) - self._eip_config.save(["leap", - "providers", - self._provider_config.get_domain(), - "eip-service.json"]) - - def _download_client_certificates(self, *args): - """ - Downloads the EIP client certificate for the given provider - """ - leap_assert(self._provider_config, "We need a provider configuration!") - leap_assert(self._eip_config, "We need an eip configuration!") - - logger.debug("Downloading EIP client certificate for %s" % - (self._provider_config.get_domain(),)) - - client_cert_path = self._eip_config.\ - get_client_cert_path(self._provider_config, - about_to_download=True) - - # For re-download if something is wrong with the cert - self._download_if_needed = self._download_if_needed and \ - not certs.should_redownload(client_cert_path) - - if self._download_if_needed and \ - os.path.exists(client_cert_path): - check_and_fix_urw_only(client_cert_path) - return - - srp_auth = SRPAuth(self._provider_config) - session_id = srp_auth.get_session_id() - cookies = None - if session_id: - cookies = {"_session_id": session_id} - cert_uri = "%s/%s/cert" % ( - self._provider_config.get_api_uri(), - self._provider_config.get_api_version()) - logger.debug('getting cert from uri: %s' % cert_uri) - res = self._session.get(cert_uri, - verify=self._provider_config - .get_ca_cert_path(), - cookies=cookies, - timeout=REQUEST_TIMEOUT) - res.raise_for_status() - client_cert = res.content - - if not certs.is_valid_pemfile(client_cert): - raise Exception(self.tr("The downloaded certificate is not a " - "valid PEM file")) - - mkdir_p(os.path.dirname(client_cert_path)) - - with open(client_cert_path, "w") as f: - f.write(client_cert) - - check_and_fix_urw_only(client_cert_path) - - def run_eip_setup_checks(self, - provider_config, - download_if_needed=False): - """ - Starts the checks needed for a new eip setup - - :param provider_config: Provider configuration - :type provider_config: ProviderConfig - """ - leap_assert(provider_config, "We need a provider config!") - leap_assert_type(provider_config, ProviderConfig) - - self._provider_config = provider_config - self._download_if_needed = download_if_needed - - cb_chain = [ - (self._download_config, self.download_config), - (self._download_client_certificates, - self.download_client_certificate) - ] - - return self.addCallbackChain(cb_chain) diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py deleted file mode 100644 index d69e1fd8..00000000 --- a/src/leap/services/eip/eipconfig.py +++ /dev/null @@ -1,263 +0,0 @@ -# -*- coding: utf-8 -*- -# eipconfig.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 . - -""" -Provider configuration -""" -import logging -import os -import re -import time - -import ipaddr - -from leap.common.check import leap_assert, leap_assert_type -from leap.common.config.baseconfig import BaseConfig -from leap.config.providerconfig import ProviderConfig -from leap.services.eip.eipspec import get_schema - -logger = logging.getLogger(__name__) - - -class VPNGatewaySelector(object): - """ - VPN Gateway selector. - """ - # http://www.timeanddate.com/time/map/ - equivalent_timezones = {13: -11, 14: -10} - - def __init__(self, eipconfig, tz_offset=None): - ''' - Constructor for VPNGatewaySelector. - - :param eipconfig: a valid EIP Configuration. - :type eipconfig: EIPConfig - :param tz_offset: use this offset as a local distance to GMT. - :type tz_offset: int - ''' - leap_assert_type(eipconfig, EIPConfig) - - self._local_offset = tz_offset - if tz_offset is None: - tz_offset = self._get_local_offset() - - if tz_offset in self.equivalent_timezones: - tz_offset = self.equivalent_timezones[tz_offset] - - self._local_offset = tz_offset - - self._eipconfig = eipconfig - - def get_gateways(self): - """ - Returns the 4 best gateways, sorted by timezone proximity. - - :rtype: list of IPv4Address or IPv6Address object. - """ - gateways_timezones = [] - locations = self._eipconfig.get_locations() - gateways = self._eipconfig.get_gateways() - - for idx, gateway in enumerate(gateways): - gateway_location = gateway.get('location') - gateway_distance = 99 # if hasn't location -> should go last - - if gateway_location is not None: - gw_offset = int(locations[gateway['location']]['timezone']) - if gw_offset in self.equivalent_timezones: - gw_offset = self.equivalent_timezones[gw_offset] - - gateway_distance = self._get_timezone_distance(gw_offset) - - ip = self._eipconfig.get_gateway_ip(idx) - gateways_timezones.append((ip, gateway_distance)) - - gateways_timezones = sorted(gateways_timezones, - key=lambda gw: gw[1])[:4] - - gateways = [ip for ip, dist in gateways_timezones] - return gateways - - def _get_timezone_distance(self, offset): - ''' - Returns the distance between the local timezone and - the one with offset 'offset'. - - :param offset: the distance of a timezone to GMT. - :type offset: int - :returns: distance between local offset and param offset. - :rtype: int - ''' - timezones = range(-11, 13) - tz1 = offset - tz2 = self._local_offset - distance = abs(timezones.index(tz1) - timezones.index(tz2)) - if distance > 12: - if tz1 < 0: - distance = timezones.index(tz1) + timezones[::-1].index(tz2) - else: - distance = timezones[::-1].index(tz1) + timezones.index(tz2) - - return distance - - def _get_local_offset(self): - ''' - Returns the distance between GMT and the local timezone. - - :rtype: int - ''' - local_offset = time.timezone - if time.daylight: - local_offset = time.altzone - - return local_offset / 3600 - - -class EIPConfig(BaseConfig): - """ - Provider configuration abstraction class - """ - OPENVPN_ALLOWED_KEYS = ("auth", "cipher", "tls-cipher") - OPENVPN_CIPHERS_REGEX = re.compile("[A-Z0-9\-]+") - - def __init__(self): - BaseConfig.__init__(self) - self._api_version = None - - def _get_schema(self): - """ - Returns the schema corresponding to the version given. - - :rtype: dict or None if the version is not supported. - """ - return get_schema(self._api_version) - - def get_clusters(self): - # TODO: create an abstraction for clusters - return self._safe_get_value("clusters") - - def get_gateways(self): - # TODO: create an abstraction for gateways - return self._safe_get_value("gateways") - - def get_locations(self): - ''' - Returns a list of locations - - :rtype: dict - ''' - return self._safe_get_value("locations") - - def get_openvpn_configuration(self): - """ - Returns a dictionary containing the openvpn configuration - parameters. - - These are sanitized with alphanumeric whitelist. - - :returns: openvpn configuration dict - :rtype: C{dict} - """ - ovpncfg = self._safe_get_value("openvpn_configuration") - config = {} - for key, value in ovpncfg.items(): - if key in self.OPENVPN_ALLOWED_KEYS and value is not None: - sanitized_val = self.OPENVPN_CIPHERS_REGEX.findall(value) - if len(sanitized_val) != 0: - _val = sanitized_val[0] - config[str(key)] = str(_val) - return config - - def get_serial(self): - return self._safe_get_value("serial") - - def get_version(self): - return self._safe_get_value("version") - - def get_gateway_ip(self, index=0): - """ - Returns the ip of the gateway. - - :rtype: An IPv4Address or IPv6Address object. - """ - gateways = self.get_gateways() - leap_assert(len(gateways) > 0, "We don't have any gateway!") - if index > len(gateways): - index = 0 - logger.warning("Provided an unknown gateway index %s, " + - "defaulting to 0") - ip_addr_str = gateways[index]["ip_address"] - - try: - ipaddr.IPAddress(ip_addr_str) - return ip_addr_str - except ValueError: - logger.error("Invalid ip address in config: %s" % (ip_addr_str,)) - return None - - def get_client_cert_path(self, - providerconfig=None, - about_to_download=False): - """ - Returns the path to the certificate used by openvpn - """ - - leap_assert(providerconfig, "We need a provider") - leap_assert_type(providerconfig, ProviderConfig) - - cert_path = os.path.join(self.get_path_prefix(), - "leap", - "providers", - providerconfig.get_domain(), - "keys", - "client", - "openvpn.pem") - - if not about_to_download: - leap_assert(os.path.exists(cert_path), - "You need to download the certificate first") - logger.debug("Using OpenVPN cert %s" % (cert_path,)) - - return cert_path - - -if __name__ == "__main__": - logger = logging.getLogger(name='leap') - logger.setLevel(logging.DEBUG) - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - eipconfig = EIPConfig('1') - - try: - eipconfig.get_clusters() - except Exception as e: - assert isinstance(e, AssertionError), "Expected an assert" - print "Safe value getting is working" - - if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): - print eipconfig.get_clusters() - print eipconfig.get_gateways() - print eipconfig.get_locations() - print eipconfig.get_openvpn_configuration() - print eipconfig.get_serial() - print eipconfig.get_version() diff --git a/src/leap/services/eip/eipspec.py b/src/leap/services/eip/eipspec.py deleted file mode 100644 index 9cc56be3..00000000 --- a/src/leap/services/eip/eipspec.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -# eipspec.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 . - - -# Schemas dict -# To add a schema for a version you should follow the form: -# { '1': schema_v1, '2': schema_v2, ... etc } -# so for instance, to add the '2' version, you should do: -# eipservice_config_spec['2'] = schema_v2 -eipservice_config_spec = {} - -eipservice_config_spec['1'] = { - 'description': 'sample eip service config', - 'type': 'object', - 'properties': { - 'serial': { - 'type': int, - 'default': 1, - 'required': ["True"] - }, - 'version': { - 'type': int, - 'default': 1, - 'required': ["True"] - }, - 'clusters': { - 'type': list, - 'default': [ - {"label": { - "en": "Location Unknown"}, - "name": "location_unknown"}] - }, - 'gateways': { - 'type': list, - 'default': [ - {"capabilities": { - "adblock": True, - "filter_dns": True, - "ports": ["80", "53", "443", "1194"], - "protocols": ["udp", "tcp"], - "transport": ["openvpn"], - "user_ips": False}, - "cluster": "location_unknown", - "host": "location.example.org", - "ip_address": "127.0.0.1"}] - }, - 'locations': { - 'type': dict, - 'default': {} - }, - 'openvpn_configuration': { - 'type': dict, - 'default': { - "auth": None, - "cipher": None, - "tls-cipher": None} - } - } -} - - -def get_schema(version): - """ - Returns the schema corresponding to the version given. - - :param version: the version of the schema to get. - :type version: str - :rtype: dict or None if the version is not supported. - """ - schema = eipservice_config_spec.get(version, None) - return schema diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py deleted file mode 100644 index bf5938dc..00000000 --- a/src/leap/services/eip/providerbootstrapper.py +++ /dev/null @@ -1,340 +0,0 @@ -# -*- coding: utf-8 -*- -# providerbootstrapper.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 . - -""" -Provider bootstrapping -""" -import logging -import socket -import os - -import requests - -from PySide import QtCore - -from leap.common.certs import get_digest -from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p -from leap.common.check import leap_assert, leap_assert_type, leap_check -from leap.config.providerconfig import ProviderConfig, MissingCACert -from leap.util.request_helpers import get_content -from leap.util.constants import REQUEST_TIMEOUT -from leap.services.abstractbootstrapper import AbstractBootstrapper -from leap.provider.supportedapis import SupportedAPIs - - -logger = logging.getLogger(__name__) - - -class UnsupportedProviderAPI(Exception): - """ - Raised when attempting to use a provider with an incompatible API. - """ - pass - - -class WrongFingerprint(Exception): - """ - Raised when a fingerprint comparison does not match. - """ - pass - - -class ProviderBootstrapper(AbstractBootstrapper): - """ - Given a provider URL performs a series of checks and emits signals - after they are passed. - If a check fails, the subsequent checks are not executed - """ - - # All dicts returned are of the form - # {"passed": bool, "error": str} - name_resolution = QtCore.Signal(dict) - https_connection = QtCore.Signal(dict) - download_provider_info = QtCore.Signal(dict) - - download_ca_cert = QtCore.Signal(dict) - check_ca_fingerprint = QtCore.Signal(dict) - check_api_certificate = QtCore.Signal(dict) - - def __init__(self, bypass_checks=False): - """ - Constructor for provider bootstrapper object - - :param bypass_checks: Set to true if the app should bypass - first round of checks for CA certificates at bootstrap - :type bypass_checks: bool - """ - AbstractBootstrapper.__init__(self, bypass_checks) - - self._domain = None - self._provider_config = None - self._download_if_needed = False - - def _check_name_resolution(self): - """ - Checks that the name resolution for the provider name works - """ - leap_assert(self._domain, "Cannot check DNS without a domain") - - logger.debug("Checking name resolution for %s" % (self._domain)) - - # We don't skip this check, since it's basic for the whole - # system to work - socket.gethostbyname(self._domain) - - def _check_https(self, *args): - """ - Checks that https is working and that the provided certificate - checks out - """ - - leap_assert(self._domain, "Cannot check HTTPS without a domain") - - logger.debug("Checking https for %s" % (self._domain)) - - # We don't skip this check, since it's basic for the whole - # system to work - - try: - res = self._session.get("https://%s" % (self._domain,), - verify=not self._bypass_checks, - timeout=REQUEST_TIMEOUT) - res.raise_for_status() - except requests.exceptions.SSLError: - self._err_msg = self.tr("Provider certificate could " - "not be verified") - raise - except Exception: - self._err_msg = self.tr("Provider does not support HTTPS") - raise - - def _download_provider_info(self, *args): - """ - Downloads the provider.json defition - """ - leap_assert(self._domain, - "Cannot download provider info without a domain") - - logger.debug("Downloading provider info for %s" % (self._domain)) - - headers = {} - - provider_json = os.path.join( - ProviderConfig().get_path_prefix(), "leap", "providers", - self._domain, "provider.json") - mtime = get_mtime(provider_json) - - if self._download_if_needed and mtime: - headers['if-modified-since'] = mtime - - uri = "https://%s/%s" % (self._domain, "provider.json") - verify = not self._bypass_checks - - if mtime: # the provider.json exists - provider_config = ProviderConfig() - provider_config.load(provider_json) - try: - verify = provider_config.get_ca_cert_path() - uri = provider_config.get_api_uri() + '/provider.json' - except MissingCACert: - # get_ca_cert_path fails if the certificate does not exists. - pass - - logger.debug("Requesting for provider.json... " - "uri: {0}, verify: {1}, headers: {2}".format( - uri, verify, headers)) - res = self._session.get(uri, verify=verify, - headers=headers, timeout=REQUEST_TIMEOUT) - res.raise_for_status() - logger.debug("Request status code: {0}".format(res.status_code)) - - # Not modified - if res.status_code == 304: - logger.debug("Provider definition has not been modified") - else: - provider_definition, mtime = get_content(res) - - provider_config = ProviderConfig() - provider_config.load(data=provider_definition, mtime=mtime) - provider_config.save(["leap", - "providers", - self._domain, - "provider.json"]) - - api_version = provider_config.get_api_version() - if SupportedAPIs.supports(api_version): - logger.debug("Provider definition has been modified") - else: - api_supported = ', '.join(SupportedAPIs.SUPPORTED_APIS) - error = ('Unsupported provider API version. ' - 'Supported versions are: {}. ' - 'Found: {}.').format(api_supported, api_version) - - logger.error(error) - raise UnsupportedProviderAPI(error) - - def run_provider_select_checks(self, domain, download_if_needed=False): - """ - Populates the check queue. - - :param domain: domain to check - :type domain: str - - :param download_if_needed: if True, makes the checks do not - overwrite already downloaded data - :type download_if_needed: bool - """ - leap_assert(domain and len(domain) > 0, "We need a domain!") - - self._domain = ProviderConfig.sanitize_path_component(domain) - self._download_if_needed = download_if_needed - - cb_chain = [ - (self._check_name_resolution, self.name_resolution), - (self._check_https, self.https_connection), - (self._download_provider_info, self.download_provider_info) - ] - - return self.addCallbackChain(cb_chain) - - def _should_proceed_cert(self): - """ - Returns False if the certificate already exists for the given - provider. True otherwise - - :rtype: bool - """ - leap_assert(self._provider_config, "We need a provider config!") - - if not self._download_if_needed: - return True - - return not os.path.exists(self._provider_config - .get_ca_cert_path(about_to_download=True)) - - def _download_ca_cert(self, *args): - """ - Downloads the CA cert that is going to be used for the api URL - """ - - leap_assert(self._provider_config, "Cannot download the ca cert " - "without a provider config!") - - logger.debug("Downloading ca cert for %s at %s" % - (self._domain, self._provider_config.get_ca_cert_uri())) - - if not self._should_proceed_cert(): - check_and_fix_urw_only( - self._provider_config - .get_ca_cert_path(about_to_download=True)) - return - - res = self._session.get(self._provider_config.get_ca_cert_uri(), - verify=not self._bypass_checks, - timeout=REQUEST_TIMEOUT) - res.raise_for_status() - - cert_path = self._provider_config.get_ca_cert_path( - about_to_download=True) - cert_dir = os.path.dirname(cert_path) - mkdir_p(cert_dir) - with open(cert_path, "w") as f: - f.write(res.content) - - check_and_fix_urw_only(cert_path) - - def _check_ca_fingerprint(self, *args): - """ - Checks the CA cert fingerprint against the one provided in the - json definition - """ - leap_assert(self._provider_config, "Cannot check the ca cert " - "without a provider config!") - - logger.debug("Checking ca fingerprint for %s and cert %s" % - (self._domain, - self._provider_config.get_ca_cert_path())) - - if not self._should_proceed_cert(): - return - - parts = self._provider_config.get_ca_cert_fingerprint().split(":") - - error_msg = "Wrong fingerprint format" - leap_check(len(parts) == 2, error_msg, WrongFingerprint) - - method = parts[0].strip() - fingerprint = parts[1].strip() - cert_data = None - with open(self._provider_config.get_ca_cert_path()) as f: - cert_data = f.read() - - leap_assert(len(cert_data) > 0, "Could not read certificate data") - digest = get_digest(cert_data, method) - - error_msg = "Downloaded certificate has a different fingerprint!" - leap_check(digest == fingerprint, error_msg, WrongFingerprint) - - def _check_api_certificate(self, *args): - """ - Tries to make an API call with the downloaded cert and checks - if it validates against it - """ - leap_assert(self._provider_config, "Cannot check the ca cert " - "without a provider config!") - - logger.debug("Checking api certificate for %s and cert %s" % - (self._provider_config.get_api_uri(), - self._provider_config.get_ca_cert_path())) - - if not self._should_proceed_cert(): - return - - test_uri = "%s/%s/cert" % (self._provider_config.get_api_uri(), - self._provider_config.get_api_version()) - res = self._session.get(test_uri, - verify=self._provider_config - .get_ca_cert_path(), - timeout=REQUEST_TIMEOUT) - res.raise_for_status() - - def run_provider_setup_checks(self, - provider_config, - download_if_needed=False): - """ - Starts the checks needed for a new provider setup. - - :param provider_config: Provider configuration - :type provider_config: ProviderConfig - - :param download_if_needed: if True, makes the checks do not - overwrite already downloaded data. - :type download_if_needed: bool - """ - leap_assert(provider_config, "We need a provider config!") - leap_assert_type(provider_config, ProviderConfig) - - self._provider_config = provider_config - self._download_if_needed = download_if_needed - - cb_chain = [ - (self._download_ca_cert, self.download_ca_cert), - (self._check_ca_fingerprint, self.check_ca_fingerprint), - (self._check_api_certificate, self.check_api_certificate) - ] - - return self.addCallbackChain(cb_chain) diff --git a/src/leap/services/eip/tests/__init__.py b/src/leap/services/eip/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/services/eip/tests/test_eipbootstrapper.py b/src/leap/services/eip/tests/test_eipbootstrapper.py deleted file mode 100644 index f2331eca..00000000 --- a/src/leap/services/eip/tests/test_eipbootstrapper.py +++ /dev/null @@ -1,347 +0,0 @@ -# -*- coding: utf-8 -*- -# test_eipbootstrapper.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 . - - -""" -Tests for the EIP Boostrapper checks - -These will be whitebox tests since we want to make sure the private -implementation is checking what we expect. -""" - -import os -import mock -import tempfile -import time -try: - import unittest2 as unittest -except ImportError: - import unittest - -from nose.twistedtools import deferred, reactor -from twisted.internet import threads -from requests.models import Response - -from leap.common.testing.basetest import BaseLeapTest -from leap.services.eip.eipbootstrapper import EIPBootstrapper -from leap.services.eip.eipconfig import EIPConfig -from leap.config.providerconfig import ProviderConfig -from leap.crypto.tests import fake_provider -from leap.common.files import mkdir_p -from leap.crypto.srpauth import SRPAuth - - -class EIPBootstrapperActiveTest(BaseLeapTest): - @classmethod - def setUpClass(cls): - BaseLeapTest.setUpClass() - factory = fake_provider.get_provider_factory() - http = reactor.listenTCP(0, factory) - https = reactor.listenSSL( - 0, factory, - fake_provider.OpenSSLServerContextFactory()) - get_port = lambda p: p.getHost().port - cls.http_port = get_port(http) - cls.https_port = get_port(https) - - def setUp(self): - self.eb = EIPBootstrapper() - self.old_pp = EIPConfig.get_path_prefix - self.old_save = EIPConfig.save - self.old_load = EIPConfig.load - self.old_si = SRPAuth.get_session_id - - def tearDown(self): - EIPConfig.get_path_prefix = self.old_pp - EIPConfig.save = self.old_save - EIPConfig.load = self.old_load - SRPAuth.get_session_id = self.old_si - - def _download_config_test_template(self, ifneeded, new): - """ - All download config tests have the same structure, so this is - a parametrized test for that. - - :param ifneeded: sets _download_if_needed - :type ifneeded: bool - :param new: if True uses time.time() as mtime for the mocked - eip-service file, otherwise it uses 100 (a really - old mtime) - :type new: float or int (will be coersed) - """ - pc = ProviderConfig() - pc.get_domain = mock.MagicMock( - return_value="localhost:%s" % (self.https_port)) - self.eb._provider_config = pc - - pc.get_api_uri = mock.MagicMock( - return_value="https://%s" % (pc.get_domain())) - pc.get_api_version = mock.MagicMock(return_value="1") - - # This is to ignore https checking, since it's not the point - # of this test - pc.get_ca_cert_path = mock.MagicMock(return_value=False) - - path_prefix = tempfile.mkdtemp() - EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix) - EIPConfig.save = mock.MagicMock() - EIPConfig.load = mock.MagicMock() - - self.eb._download_if_needed = ifneeded - - provider_dir = os.path.join(EIPConfig.get_path_prefix(), - "leap", - "providers", - pc.get_domain()) - mkdir_p(provider_dir) - eip_config_path = os.path.join(provider_dir, - "eip-service.json") - - with open(eip_config_path, "w") as ec: - ec.write("A") - - # set mtime to something really new - if new: - os.utime(eip_config_path, (-1, time.time())) - else: - os.utime(eip_config_path, (-1, 100)) - - @deferred() - def test_download_config_not_modified(self): - self._download_config_test_template(True, True) - - d = threads.deferToThread(self.eb._download_config) - - def check(*args): - self.assertFalse(self.eb._eip_config.save.called) - d.addCallback(check) - return d - - @deferred() - def test_download_config_modified(self): - self._download_config_test_template(True, False) - - d = threads.deferToThread(self.eb._download_config) - - def check(*args): - self.assertTrue(self.eb._eip_config.save.called) - d.addCallback(check) - return d - - @deferred() - def test_download_config_ignores_mtime(self): - self._download_config_test_template(False, True) - - d = threads.deferToThread(self.eb._download_config) - - def check(*args): - self.eb._eip_config.save.assert_called_once_with( - ["leap", - "providers", - self.eb._provider_config.get_domain(), - "eip-service.json"]) - d.addCallback(check) - return d - - def _download_certificate_test_template(self, ifneeded, createcert): - """ - All download client certificate tests have the same structure, - so this is a parametrized test for that. - - :param ifneeded: sets _download_if_needed - :type ifneeded: bool - :param createcert: if True it creates a dummy file to play the - part of a downloaded certificate - :type createcert: bool - - :returns: the temp eip cert path and the dummy cert contents - :rtype: tuple of str, str - """ - pc = ProviderConfig() - ec = EIPConfig() - self.eb._provider_config = pc - self.eb._eip_config = ec - - pc.get_domain = mock.MagicMock( - return_value="localhost:%s" % (self.https_port)) - pc.get_api_uri = mock.MagicMock( - return_value="https://%s" % (pc.get_domain())) - pc.get_api_version = mock.MagicMock(return_value="1") - pc.get_ca_cert_path = mock.MagicMock(return_value=False) - - path_prefix = tempfile.mkdtemp() - EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix) - EIPConfig.save = mock.MagicMock() - EIPConfig.load = mock.MagicMock() - - self.eb._download_if_needed = ifneeded - - provider_dir = os.path.join(EIPConfig.get_path_prefix(), - "leap", - "providers", - "somedomain") - mkdir_p(provider_dir) - eip_cert_path = os.path.join(provider_dir, - "cert") - - ec.get_client_cert_path = mock.MagicMock( - return_value=eip_cert_path) - - cert_content = "A" - if createcert: - with open(eip_cert_path, "w") as ec: - ec.write(cert_content) - - return eip_cert_path, cert_content - - def test_download_client_certificate_not_modified(self): - cert_path, old_cert_content = self._download_certificate_test_template( - True, True) - - with mock.patch('leap.common.certs.should_redownload', - new_callable=mock.MagicMock, - return_value=False): - self.eb._download_client_certificates() - with open(cert_path, "r") as c: - self.assertEqual(c.read(), old_cert_content) - - @deferred() - def test_download_client_certificate_old_cert(self): - cert_path, old_cert_content = self._download_certificate_test_template( - True, True) - - def wrapper(*args): - with mock.patch('leap.common.certs.should_redownload', - new_callable=mock.MagicMock, - return_value=True): - with mock.patch('leap.common.certs.is_valid_pemfile', - new_callable=mock.MagicMock, - return_value=True): - self.eb._download_client_certificates() - - def check(*args): - with open(cert_path, "r") as c: - self.assertNotEqual(c.read(), old_cert_content) - d = threads.deferToThread(wrapper) - d.addCallback(check) - - return d - - @deferred() - def test_download_client_certificate_no_cert(self): - cert_path, _ = self._download_certificate_test_template( - True, False) - - def wrapper(*args): - with mock.patch('leap.common.certs.should_redownload', - new_callable=mock.MagicMock, - return_value=False): - with mock.patch('leap.common.certs.is_valid_pemfile', - new_callable=mock.MagicMock, - return_value=True): - self.eb._download_client_certificates() - - def check(*args): - self.assertTrue(os.path.exists(cert_path)) - d = threads.deferToThread(wrapper) - d.addCallback(check) - - return d - - @deferred() - def test_download_client_certificate_force_not_valid(self): - cert_path, old_cert_content = self._download_certificate_test_template( - True, True) - - def wrapper(*args): - with mock.patch('leap.common.certs.should_redownload', - new_callable=mock.MagicMock, - return_value=True): - with mock.patch('leap.common.certs.is_valid_pemfile', - new_callable=mock.MagicMock, - return_value=True): - self.eb._download_client_certificates() - - def check(*args): - with open(cert_path, "r") as c: - self.assertNotEqual(c.read(), old_cert_content) - d = threads.deferToThread(wrapper) - d.addCallback(check) - - return d - - @deferred() - def test_download_client_certificate_invalid_download(self): - cert_path, _ = self._download_certificate_test_template( - False, False) - - def wrapper(*args): - with mock.patch('leap.common.certs.should_redownload', - new_callable=mock.MagicMock, - return_value=True): - with mock.patch('leap.common.certs.is_valid_pemfile', - new_callable=mock.MagicMock, - return_value=False): - with self.assertRaises(Exception): - self.eb._download_client_certificates() - d = threads.deferToThread(wrapper) - - return d - - @deferred() - def test_download_client_certificate_uses_session_id(self): - _, _ = self._download_certificate_test_template( - False, False) - - SRPAuth.get_session_id = mock.MagicMock(return_value="1") - - def check_cookie(*args, **kwargs): - cookies = kwargs.get("cookies", None) - self.assertEqual(cookies, {'_session_id': '1'}) - return Response() - - def wrapper(*args): - with mock.patch('leap.common.certs.should_redownload', - new_callable=mock.MagicMock, - return_value=False): - with mock.patch('leap.common.certs.is_valid_pemfile', - new_callable=mock.MagicMock, - return_value=True): - with mock.patch('requests.sessions.Session.get', - new_callable=mock.MagicMock, - side_effect=check_cookie): - with mock.patch('requests.models.Response.content', - new_callable=mock.PropertyMock, - return_value="A"): - self.eb._download_client_certificates() - - d = threads.deferToThread(wrapper) - - return d - - @deferred() - def test_run_eip_setup_checks(self): - self.eb._download_config = mock.MagicMock() - self.eb._download_client_certificates = mock.MagicMock() - - d = self.eb.run_eip_setup_checks(ProviderConfig()) - - def check(*args): - self.eb._download_config.assert_called_once_with() - self.eb._download_client_certificates.assert_called_once_with(None) - d.addCallback(check) - return d diff --git a/src/leap/services/eip/tests/test_eipconfig.py b/src/leap/services/eip/tests/test_eipconfig.py deleted file mode 100644 index 87ce04c2..00000000 --- a/src/leap/services/eip/tests/test_eipconfig.py +++ /dev/null @@ -1,324 +0,0 @@ -# -*- coding: utf-8 -*- -# test_eipconfig.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 . -""" -Tests for eipconfig -""" -import copy -import json -import os -import unittest - -from leap.common.testing.basetest import BaseLeapTest -from leap.services.eip.eipconfig import EIPConfig -from leap.config.providerconfig import ProviderConfig - -from mock import Mock - - -sample_config = { - "gateways": [ - { - "capabilities": { - "adblock": False, - "filter_dns": True, - "limited": True, - "ports": [ - "1194", - "443", - "53", - "80"], - "protocols": [ - "tcp", - "udp"], - "transport": ["openvpn"], - "user_ips": False}, - "host": "host.dev.example.org", - "ip_address": "11.22.33.44", - "location": "cyberspace" - }, { - "capabilities": { - "adblock": False, - "filter_dns": True, - "limited": True, - "ports": [ - "1194", - "443", - "53", - "80"], - "protocols": [ - "tcp", - "udp"], - "transport": ["openvpn"], - "user_ips": False}, - "host": "host2.dev.example.org", - "ip_address": "22.33.44.55", - "location": "cyberspace" - } - ], - "locations": { - "ankara": { - "country_code": "XX", - "hemisphere": "S", - "name": "Antarctica", - "timezone": "+2" - }, - "cyberspace": { - "country_code": "XX", - "hemisphere": "X", - "name": "outer space", - "timezone": "" - } - }, - "openvpn_configuration": { - "auth": "SHA1", - "cipher": "AES-128-CBC", - "tls-cipher": "DHE-RSA-AES128-SHA" - }, - "serial": 1, - "version": 1 -} - - -class EIPConfigTest(BaseLeapTest): - - __name__ = "eip_config_tests" - - maxDiff = None - - def setUp(self): - self._old_ospath_exists = os.path.exists - - def tearDown(self): - os.path.exists = self._old_ospath_exists - - def _write_config(self, data): - """ - Helper to write some data to a temp config file. - - :param data: data to be used to save in the config file. - :data type: dict (valid json) - """ - self.configfile = os.path.join(self.tempdir, "eipconfig.json") - conf = open(self.configfile, "w") - conf.write(json.dumps(data)) - conf.close() - - def _get_eipconfig(self, fromfile=True, data=sample_config, api_ver='1'): - """ - Helper that returns an EIPConfig object using the data parameter - or a sample data. - - :param fromfile: sets if we should use a file or a string - :type fromfile: bool - :param data: sets the data to be used to load in the EIPConfig object - :type data: dict (valid json) - :param api_ver: the api_version schema to use. - :type api_ver: str - :rtype: EIPConfig - """ - config = EIPConfig() - config.set_api_version(api_ver) - - loaded = False - if fromfile: - self._write_config(data) - loaded = config.load(self.configfile, relative=False) - else: - json_string = json.dumps(data) - loaded = config.load(data=json_string) - - if not loaded: - return None - - return config - - def test_loads_from_file(self): - config = self._get_eipconfig() - self.assertIsNotNone(config) - - def test_loads_from_data(self): - config = self._get_eipconfig(fromfile=False) - self.assertIsNotNone(config) - - def test_load_valid_config_from_file(self): - config = self._get_eipconfig() - self.assertIsNotNone(config) - - self.assertEqual( - config.get_openvpn_configuration(), - sample_config["openvpn_configuration"]) - - sample_ip = sample_config["gateways"][0]["ip_address"] - self.assertEqual( - config.get_gateway_ip(), - sample_ip) - self.assertEqual(config.get_version(), sample_config["version"]) - self.assertEqual(config.get_serial(), sample_config["serial"]) - self.assertEqual(config.get_gateways(), sample_config["gateways"]) - self.assertEqual(config.get_locations(), sample_config["locations"]) - self.assertEqual(config.get_clusters(), None) - - def test_load_valid_config_from_data(self): - config = self._get_eipconfig(fromfile=False) - self.assertIsNotNone(config) - - self.assertEqual( - config.get_openvpn_configuration(), - sample_config["openvpn_configuration"]) - - sample_ip = sample_config["gateways"][0]["ip_address"] - self.assertEqual( - config.get_gateway_ip(), - sample_ip) - - self.assertEqual(config.get_version(), sample_config["version"]) - self.assertEqual(config.get_serial(), sample_config["serial"]) - self.assertEqual(config.get_gateways(), sample_config["gateways"]) - self.assertEqual(config.get_locations(), sample_config["locations"]) - self.assertEqual(config.get_clusters(), None) - - def test_sanitize_extra_parameters(self): - data = copy.deepcopy(sample_config) - data['openvpn_configuration']["extra_param"] = "FOO" - config = self._get_eipconfig(data=data) - - self.assertEqual( - config.get_openvpn_configuration(), - sample_config["openvpn_configuration"]) - - def test_sanitize_non_allowed_chars(self): - data = copy.deepcopy(sample_config) - data['openvpn_configuration']["auth"] = "SHA1;" - config = self._get_eipconfig(data=data) - - self.assertEqual( - config.get_openvpn_configuration(), - sample_config["openvpn_configuration"]) - - data = copy.deepcopy(sample_config) - data['openvpn_configuration']["auth"] = "SHA1>`&|" - config = self._get_eipconfig(data=data) - - self.assertEqual( - config.get_openvpn_configuration(), - sample_config["openvpn_configuration"]) - - def test_sanitize_lowercase(self): - data = copy.deepcopy(sample_config) - data['openvpn_configuration']["auth"] = "shaSHA1" - config = self._get_eipconfig(data=data) - - self.assertEqual( - config.get_openvpn_configuration(), - sample_config["openvpn_configuration"]) - - def test_all_characters_invalid(self): - data = copy.deepcopy(sample_config) - data['openvpn_configuration']["auth"] = "sha&*!@#;" - config = self._get_eipconfig(data=data) - - self.assertEqual( - config.get_openvpn_configuration(), - {'cipher': 'AES-128-CBC', - 'tls-cipher': 'DHE-RSA-AES128-SHA'}) - - def test_sanitize_bad_ip(self): - data = copy.deepcopy(sample_config) - data['gateways'][0]["ip_address"] = "11.22.33.44;" - config = self._get_eipconfig(data=data) - - self.assertEqual(config.get_gateway_ip(), None) - - data = copy.deepcopy(sample_config) - data['gateways'][0]["ip_address"] = "11.22.33.44`" - config = self._get_eipconfig(data=data) - - self.assertEqual(config.get_gateway_ip(), None) - - def test_default_gateway_on_unknown_index(self): - config = self._get_eipconfig() - sample_ip = sample_config["gateways"][0]["ip_address"] - self.assertEqual(config.get_gateway_ip(999), sample_ip) - - def test_get_gateway_by_index(self): - config = self._get_eipconfig() - sample_ip_0 = sample_config["gateways"][0]["ip_address"] - sample_ip_1 = sample_config["gateways"][1]["ip_address"] - self.assertEqual(config.get_gateway_ip(0), sample_ip_0) - self.assertEqual(config.get_gateway_ip(1), sample_ip_1) - - def test_get_client_cert_path_as_expected(self): - config = self._get_eipconfig() - config.get_path_prefix = Mock(return_value='test') - - provider_config = ProviderConfig() - - # mock 'get_domain' so we don't need to load a config - provider_domain = 'test.provider.com' - provider_config.get_domain = Mock(return_value=provider_domain) - - expected_path = os.path.join('test', 'leap', 'providers', - provider_domain, 'keys', 'client', - 'openvpn.pem') - - # mock 'os.path.exists' so we don't get an error for unexisting file - os.path.exists = Mock(return_value=True) - cert_path = config.get_client_cert_path(provider_config) - - self.assertEqual(cert_path, expected_path) - - def test_get_client_cert_path_about_to_download(self): - config = self._get_eipconfig() - config.get_path_prefix = Mock(return_value='test') - - provider_config = ProviderConfig() - - # mock 'get_domain' so we don't need to load a config - provider_domain = 'test.provider.com' - provider_config.get_domain = Mock(return_value=provider_domain) - - expected_path = os.path.join('test', 'leap', 'providers', - provider_domain, 'keys', 'client', - 'openvpn.pem') - - cert_path = config.get_client_cert_path( - provider_config, about_to_download=True) - - self.assertEqual(cert_path, expected_path) - - def test_get_client_cert_path_fails(self): - config = self._get_eipconfig() - provider_config = ProviderConfig() - - # mock 'get_domain' so we don't need to load a config - provider_domain = 'test.provider.com' - provider_config.get_domain = Mock(return_value=provider_domain) - - with self.assertRaises(AssertionError): - config.get_client_cert_path(provider_config) - - def test_fails_without_api_set(self): - config = EIPConfig() - with self.assertRaises(AssertionError): - config.load('non-relevant-path') - - def test_fails_with_api_without_schema(self): - with self.assertRaises(AssertionError): - self._get_eipconfig(api_ver='123') - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/services/eip/tests/test_providerbootstrapper.py b/src/leap/services/eip/tests/test_providerbootstrapper.py deleted file mode 100644 index b24334a2..00000000 --- a/src/leap/services/eip/tests/test_providerbootstrapper.py +++ /dev/null @@ -1,531 +0,0 @@ -# -*- coding: utf-8 -*- -# test_providerbootstrapper.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 . - - -""" -Tests for the Provider Boostrapper checks - -These will be whitebox tests since we want to make sure the private -implementation is checking what we expect. -""" - -import os -import mock -import socket -import stat -import tempfile -import time -import requests -try: - import unittest2 as unittest -except ImportError: - import unittest - -from nose.twistedtools import deferred, reactor -from twisted.internet import threads -from requests.models import Response - -from leap.common.testing.https_server import where -from leap.common.testing.basetest import BaseLeapTest -from leap.services.eip.providerbootstrapper import ProviderBootstrapper -from leap.services.eip.providerbootstrapper import UnsupportedProviderAPI -from leap.services.eip.providerbootstrapper import WrongFingerprint -from leap.provider.supportedapis import SupportedAPIs -from leap.config.providerconfig import ProviderConfig -from leap.crypto.tests import fake_provider -from leap.common.files import mkdir_p - - -class ProviderBootstrapperTest(BaseLeapTest): - def setUp(self): - self.pb = ProviderBootstrapper() - - def tearDown(self): - pass - - def test_name_resolution_check(self): - # Something highly likely to success - self.pb._domain = "google.com" - self.pb._check_name_resolution() - # Something highly likely to fail - self.pb._domain = "uquhqweuihowquie.abc.def" - with self.assertRaises(socket.gaierror): - self.pb._check_name_resolution() - - @deferred() - def test_run_provider_select_checks(self): - self.pb._check_name_resolution = mock.MagicMock() - self.pb._check_https = mock.MagicMock() - self.pb._download_provider_info = mock.MagicMock() - - d = self.pb.run_provider_select_checks("somedomain") - - def check(*args): - self.pb._check_name_resolution.assert_called_once_with() - self.pb._check_https.assert_called_once_with(None) - self.pb._download_provider_info.assert_called_once_with(None) - d.addCallback(check) - return d - - @deferred() - def test_run_provider_setup_checks(self): - self.pb._download_ca_cert = mock.MagicMock() - self.pb._check_ca_fingerprint = mock.MagicMock() - self.pb._check_api_certificate = mock.MagicMock() - - d = self.pb.run_provider_setup_checks(ProviderConfig()) - - def check(*args): - self.pb._download_ca_cert.assert_called_once_with() - self.pb._check_ca_fingerprint.assert_called_once_with(None) - self.pb._check_api_certificate.assert_called_once_with(None) - d.addCallback(check) - return d - - def test_should_proceed_cert(self): - self.pb._provider_config = mock.Mock() - self.pb._provider_config.get_ca_cert_path = mock.MagicMock( - return_value=where("cacert.pem")) - - self.pb._download_if_needed = False - self.assertTrue(self.pb._should_proceed_cert()) - - self.pb._download_if_needed = True - self.assertFalse(self.pb._should_proceed_cert()) - - self.pb._provider_config.get_ca_cert_path = mock.MagicMock( - return_value=where("somefilethatdoesntexist.pem")) - self.assertTrue(self.pb._should_proceed_cert()) - - def _check_download_ca_cert(self, should_proceed): - """ - Helper to check different paths easily for the download ca - cert check - - :param should_proceed: sets the _should_proceed_cert in the - provider bootstrapper being tested - :type should_proceed: bool - - :returns: The contents of the certificate, the expected - content depending on should_proceed, and the mode of - the file to be checked by the caller - :rtype: tuple of str, str, int - """ - old_content = "NOT THE NEW CERT" - new_content = "NEW CERT" - new_cert_path = os.path.join(tempfile.mkdtemp(), - "mynewcert.pem") - - with open(new_cert_path, "w") as c: - c.write(old_content) - - self.pb._provider_config = mock.Mock() - self.pb._provider_config.get_ca_cert_path = mock.MagicMock( - return_value=new_cert_path) - self.pb._domain = "somedomain" - - self.pb._should_proceed_cert = mock.MagicMock( - return_value=should_proceed) - - read = None - content_to_check = None - mode = None - - with mock.patch('requests.models.Response.content', - new_callable=mock.PropertyMock) as \ - content: - content.return_value = new_content - response_obj = Response() - response_obj.raise_for_status = mock.MagicMock() - - self.pb._session.get = mock.MagicMock(return_value=response_obj) - self.pb._download_ca_cert() - with open(new_cert_path, "r") as nc: - read = nc.read() - if should_proceed: - content_to_check = new_content - else: - content_to_check = old_content - mode = stat.S_IMODE(os.stat(new_cert_path).st_mode) - - os.unlink(new_cert_path) - return read, content_to_check, mode - - def test_download_ca_cert_no_saving(self): - read, expected_read, mode = self._check_download_ca_cert(False) - self.assertEqual(read, expected_read) - self.assertEqual(mode, int("600", 8)) - - def test_download_ca_cert_saving(self): - read, expected_read, mode = self._check_download_ca_cert(True) - self.assertEqual(read, expected_read) - self.assertEqual(mode, int("600", 8)) - - def test_check_ca_fingerprint_skips(self): - self.pb._provider_config = mock.Mock() - self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( - return_value="") - self.pb._domain = "somedomain" - - self.pb._should_proceed_cert = mock.MagicMock(return_value=False) - - self.pb._check_ca_fingerprint() - self.assertFalse(self.pb._provider_config. - get_ca_cert_fingerprint.called) - - def test_check_ca_cert_fingerprint_raises_bad_format(self): - self.pb._provider_config = mock.Mock() - self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( - return_value="wrongfprformat!!") - self.pb._domain = "somedomain" - - self.pb._should_proceed_cert = mock.MagicMock(return_value=True) - - with self.assertRaises(WrongFingerprint): - self.pb._check_ca_fingerprint() - - # This two hashes different in the last byte, but that's good enough - # for the tests - KNOWN_BAD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034efe" \ - "7dd1b910062ca323eb4da5c7f" - KNOWN_GOOD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034ef" \ - "e7dd1b910062ca323eb4da5c7e" - KNOWN_GOOD_CERT = """ ------BEGIN CERTIFICATE----- -MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt -YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v -Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw -FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV -BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw -ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai -dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB -7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84 -CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+ -znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4 -MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4 -lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0 -bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl -DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB -lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy -YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw -XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE -MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w -DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl -cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY -k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj -RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG -htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX -EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J -aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l -mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK -G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co -Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d -69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e -yV8e ------END CERTIFICATE----- -""" - - def _prepare_provider_config_with(self, cert_path, cert_hash): - """ - Mocks the provider config to give the cert_path and cert_hash - specified - - :param cert_path: path for the certificate - :type cert_path: str - :param cert_hash: hash for the certificate as it would appear - in the provider config json - :type cert_hash: str - """ - self.pb._provider_config = mock.Mock() - self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( - return_value=cert_hash) - self.pb._provider_config.get_ca_cert_path = mock.MagicMock( - return_value=cert_path) - self.pb._domain = "somedomain" - - def test_check_ca_fingerprint_checksout(self): - cert_path = os.path.join(tempfile.mkdtemp(), - "mynewcert.pem") - - with open(cert_path, "w") as c: - c.write(self.KNOWN_GOOD_CERT) - - self._prepare_provider_config_with(cert_path, self.KNOWN_GOOD_HASH) - - self.pb._should_proceed_cert = mock.MagicMock(return_value=True) - - self.pb._check_ca_fingerprint() - - os.unlink(cert_path) - - def test_check_ca_fingerprint_fails(self): - cert_path = os.path.join(tempfile.mkdtemp(), - "mynewcert.pem") - - with open(cert_path, "w") as c: - c.write(self.KNOWN_GOOD_CERT) - - self._prepare_provider_config_with(cert_path, self.KNOWN_BAD_HASH) - - self.pb._should_proceed_cert = mock.MagicMock(return_value=True) - - with self.assertRaises(WrongFingerprint): - self.pb._check_ca_fingerprint() - - os.unlink(cert_path) - - -############################################################################### -# Tests with a fake provider # -############################################################################### - -class ProviderBootstrapperActiveTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - factory = fake_provider.get_provider_factory() - http = reactor.listenTCP(8002, factory) - https = reactor.listenSSL( - 0, factory, - fake_provider.OpenSSLServerContextFactory()) - get_port = lambda p: p.getHost().port - cls.http_port = get_port(http) - cls.https_port = get_port(https) - - def setUp(self): - self.pb = ProviderBootstrapper() - - # At certain points we are going to be replacing these methods - # directly in ProviderConfig to be able to catch calls from - # new ProviderConfig objects inside the methods tested. We - # need to save the old implementation and restore it in - # tearDown so we are sure everything is as expected for each - # test. If we do it inside each specific test, a failure in - # the test will leave the implementation with the mock. - self.old_gpp = ProviderConfig.get_path_prefix - self.old_load = ProviderConfig.load - self.old_save = ProviderConfig.save - self.old_api_version = ProviderConfig.get_api_version - - def tearDown(self): - ProviderConfig.get_path_prefix = self.old_gpp - ProviderConfig.load = self.old_load - ProviderConfig.save = self.old_save - ProviderConfig.get_api_version = self.old_api_version - - def test_check_https_succeeds(self): - # XXX: Need a proper CA signed cert to test this - pass - - @deferred() - def test_check_https_fails(self): - self.pb._domain = "localhost:%s" % (self.https_port,) - - def check(*args): - with self.assertRaises(requests.exceptions.SSLError): - self.pb._check_https() - return threads.deferToThread(check) - - @deferred() - def test_second_check_https_fails(self): - self.pb._domain = "localhost:1234" - - def check(*args): - with self.assertRaises(Exception): - self.pb._check_https() - return threads.deferToThread(check) - - @deferred() - def test_check_https_succeeds_if_danger(self): - self.pb._domain = "localhost:%s" % (self.https_port,) - self.pb._bypass_checks = True - - def check(*args): - self.pb._check_https() - - return threads.deferToThread(check) - - def _setup_provider_config_with(self, api, path_prefix): - """ - Sets up the ProviderConfig with mocks for the path prefix, the - api returned and load/save methods. - It modifies ProviderConfig directly instead of an object - because the object used is created in the method itself and we - cannot control that. - - :param api: API to return - :type api: str - :param path_prefix: path prefix to be used when calculating - paths - :type path_prefix: str - """ - ProviderConfig.get_path_prefix = mock.MagicMock( - return_value=path_prefix) - ProviderConfig.get_api_version = mock.MagicMock( - return_value=api) - ProviderConfig.load = mock.MagicMock() - ProviderConfig.save = mock.MagicMock() - - def _setup_providerbootstrapper(self, ifneeded): - """ - Sets the provider bootstrapper's domain to - localhost:https_port, sets it to bypass https checks and sets - the download if needed based on the ifneeded value. - - :param ifneeded: Value for _download_if_needed - :type ifneeded: bool - """ - self.pb._domain = "localhost:%s" % (self.https_port,) - self.pb._bypass_checks = True - self.pb._download_if_needed = ifneeded - - def _produce_dummy_provider_json(self): - """ - Creates a dummy provider json on disk in order to test - behaviour around it (download if newer online, etc) - - :returns: the provider.json path used - :rtype: str - """ - provider_dir = os.path.join(ProviderConfig() - .get_path_prefix(), - "leap", - "providers", - self.pb._domain) - mkdir_p(provider_dir) - provider_path = os.path.join(provider_dir, - "provider.json") - - with open(provider_path, "w") as p: - p.write("A") - return provider_path - - def test_download_provider_info_new_provider(self): - self._setup_provider_config_with("1", tempfile.mkdtemp()) - self._setup_providerbootstrapper(True) - - self.pb._download_provider_info() - self.assertTrue(ProviderConfig.save.called) - - @mock.patch('leap.config.providerconfig.ProviderConfig.get_ca_cert_path', - lambda x: where('cacert.pem')) - def test_download_provider_info_not_modified(self): - self._setup_provider_config_with("1", tempfile.mkdtemp()) - self._setup_providerbootstrapper(True) - provider_path = self._produce_dummy_provider_json() - - # set mtime to something really new - os.utime(provider_path, (-1, time.time())) - - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - self.pb._download_provider_info() - # we check that it doesn't save the provider - # config, because it's new enough - self.assertFalse(ProviderConfig.save.called) - - @mock.patch('leap.config.providerconfig.ProviderConfig.get_ca_cert_path', - lambda x: where('cacert.pem')) - def test_download_provider_info_modified(self): - self._setup_provider_config_with("1", tempfile.mkdtemp()) - self._setup_providerbootstrapper(True) - provider_path = self._produce_dummy_provider_json() - - # set mtime to something really old - os.utime(provider_path, (-1, 100)) - - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - self.pb._download_provider_info() - self.assertTrue(ProviderConfig.load.called) - self.assertTrue(ProviderConfig.save.called) - - @mock.patch('leap.config.providerconfig.ProviderConfig.get_ca_cert_path', - lambda x: where('cacert.pem')) - def test_download_provider_info_unsupported_api_raises(self): - self._setup_provider_config_with("9999999", tempfile.mkdtemp()) - self._setup_providerbootstrapper(False) - self._produce_dummy_provider_json() - - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - with self.assertRaises(UnsupportedProviderAPI): - self.pb._download_provider_info() - - @mock.patch('leap.config.providerconfig.ProviderConfig.get_ca_cert_path', - lambda x: where('cacert.pem')) - def test_download_provider_info_unsupported_api(self): - self._setup_provider_config_with(SupportedAPIs.SUPPORTED_APIS[0], - tempfile.mkdtemp()) - self._setup_providerbootstrapper(False) - self._produce_dummy_provider_json() - - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - self.pb._download_provider_info() - - @mock.patch('leap.config.providerconfig.ProviderConfig.get_api_uri', - lambda x: 'api.uri') - @mock.patch('leap.config.providerconfig.ProviderConfig.get_ca_cert_path', - lambda x: '/cert/path') - def test_check_api_certificate_skips(self): - self.pb._provider_config = ProviderConfig() - self.pb._session.get = mock.MagicMock(return_value=Response()) - - self.pb._should_proceed_cert = mock.MagicMock(return_value=False) - self.pb._check_api_certificate() - self.assertFalse(self.pb._session.get.called) - - @deferred() - def test_check_api_certificate_fails(self): - self.pb._provider_config = ProviderConfig() - self.pb._provider_config.get_api_uri = mock.MagicMock( - return_value="https://localhost:%s" % (self.https_port,)) - self.pb._provider_config.get_ca_cert_path = mock.MagicMock( - return_value=os.path.join( - os.path.split(__file__)[0], - "wrongcert.pem")) - self.pb._provider_config.get_api_version = mock.MagicMock( - return_value="1") - - self.pb._should_proceed_cert = mock.MagicMock(return_value=True) - - def check(*args): - with self.assertRaises(requests.exceptions.SSLError): - self.pb._check_api_certificate() - d = threads.deferToThread(check) - return d - - @deferred() - def test_check_api_certificate_succeeds(self): - self.pb._provider_config = ProviderConfig() - self.pb._provider_config.get_api_uri = mock.MagicMock( - return_value="https://localhost:%s" % (self.https_port,)) - self.pb._provider_config.get_ca_cert_path = mock.MagicMock( - return_value=where('cacert.pem')) - self.pb._provider_config.get_api_version = mock.MagicMock( - return_value="1") - - self.pb._should_proceed_cert = mock.MagicMock(return_value=True) - - def check(*args): - self.pb._check_api_certificate() - d = threads.deferToThread(check) - return d diff --git a/src/leap/services/eip/tests/test_vpngatewayselector.py b/src/leap/services/eip/tests/test_vpngatewayselector.py deleted file mode 100644 index c90681d7..00000000 --- a/src/leap/services/eip/tests/test_vpngatewayselector.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- -# test_vpngatewayselector.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 . -""" -tests for vpngatewayselector -""" - -import unittest - -from leap.services.eip.eipconfig import EIPConfig, VPNGatewaySelector -from leap.common.testing.basetest import BaseLeapTest -from mock import Mock - - -sample_gateways = [ - {u'host': u'gateway1.com', - u'ip_address': u'1.2.3.4', - u'location': u'location1'}, - {u'host': u'gateway2.com', - u'ip_address': u'2.3.4.5', - u'location': u'location2'}, - {u'host': u'gateway3.com', - u'ip_address': u'3.4.5.6', - u'location': u'location3'}, - {u'host': u'gateway4.com', - u'ip_address': u'4.5.6.7', - u'location': u'location4'} -] - -sample_gateways_no_location = [ - {u'host': u'gateway1.com', - u'ip_address': u'1.2.3.4'}, - {u'host': u'gateway2.com', - u'ip_address': u'2.3.4.5'}, - {u'host': u'gateway3.com', - u'ip_address': u'3.4.5.6'} -] - -sample_locations = { - u'location1': {u'timezone': u'2'}, - u'location2': {u'timezone': u'-7'}, - u'location3': {u'timezone': u'-4'}, - u'location4': {u'timezone': u'+13'} -} - -# 0 is not used, only for indexing from 1 in tests -ips = (0, u'1.2.3.4', u'2.3.4.5', u'3.4.5.6', u'4.5.6.7') - - -class VPNGatewaySelectorTest(BaseLeapTest): - """ - VPNGatewaySelector's tests. - """ - def setUp(self): - self.eipconfig = EIPConfig() - self.eipconfig.get_gateways = Mock(return_value=sample_gateways) - self.eipconfig.get_locations = Mock(return_value=sample_locations) - - def tearDown(self): - pass - - def test_get_no_gateways(self): - gateway_selector = VPNGatewaySelector(self.eipconfig) - self.eipconfig.get_gateways = Mock(return_value=[]) - gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, []) - - def test_get_gateway_with_no_locations(self): - gateway_selector = VPNGatewaySelector(self.eipconfig) - self.eipconfig.get_gateways = Mock( - return_value=sample_gateways_no_location) - self.eipconfig.get_locations = Mock(return_value=[]) - gateways = gateway_selector.get_gateways() - gateways_default_order = [ - sample_gateways[0]['ip_address'], - sample_gateways[1]['ip_address'], - sample_gateways[2]['ip_address'] - ] - self.assertEqual(gateways, gateways_default_order) - - def test_correct_order_gmt(self): - gateway_selector = VPNGatewaySelector(self.eipconfig, 0) - gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [ips[1], ips[3], ips[2], ips[4]]) - - def test_correct_order_gmt_minus_3(self): - gateway_selector = VPNGatewaySelector(self.eipconfig, -3) - gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [ips[3], ips[2], ips[1], ips[4]]) - - def test_correct_order_gmt_minus_7(self): - gateway_selector = VPNGatewaySelector(self.eipconfig, -7) - gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [ips[2], ips[3], ips[4], ips[1]]) - - def test_correct_order_gmt_plus_5(self): - gateway_selector = VPNGatewaySelector(self.eipconfig, 5) - gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [ips[1], ips[4], ips[3], ips[2]]) - - def test_correct_order_gmt_plus_12(self): - gateway_selector = VPNGatewaySelector(self.eipconfig, 12) - gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [ips[4], ips[2], ips[3], ips[1]]) - - def test_correct_order_gmt_minus_11(self): - gateway_selector = VPNGatewaySelector(self.eipconfig, -11) - gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [ips[4], ips[2], ips[3], ips[1]]) - - def test_correct_order_gmt_plus_14(self): - gateway_selector = VPNGatewaySelector(self.eipconfig, 14) - gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [ips[4], ips[2], ips[3], ips[1]]) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/services/eip/tests/wrongcert.pem b/src/leap/services/eip/tests/wrongcert.pem deleted file mode 100644 index e6cff38a..00000000 --- a/src/leap/services/eip/tests/wrongcert.pem +++ /dev/null @@ -1,33 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFtTCCA52gAwIBAgIJAIWZus5EIXNtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV -BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX -aWRnaXRzIFB0eSBMdGQwHhcNMTMwNjI1MTc0NjExWhcNMTgwNjI1MTc0NjExWjBF -MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 -ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC -CgKCAgEA2ObM7ESjyuxFZYD/Y68qOPQgjgggW+cdXfBpU2p4n7clsrUeMhWdW40Y -77Phzor9VOeqs3ZpHuyLzsYVp/kFDm8tKyo2ah5fJwzL0VCSLYaZkUQQ7GNUmTCk -furaxl8cQx/fg395V7/EngsS9B3/y5iHbctbA4MnH3jaotO5EGeo6hw7/eyCotQ9 -KbBV9GJMcY94FsXBCmUB+XypKklWTLhSaS6Cu4Fo8YLW6WmcnsyEOGS2F7WVf5at -7CBWFQZHaSgIBLmc818/mDYCnYmCVMFn/6Ndx7V2NTlz+HctWrQn0dmIOnCUeCwS -wXq9PnBR1rSx/WxwyF/WpyjOFkcIo7vm72kS70pfrYsXcZD4BQqkXYj3FyKnPt3O -ibLKtCxL8/83wOtErPcYpG6LgFkgAAlHQ9MkUi5dbmjCJtpqQmlZeK1RALdDPiB3 -K1KZimrGsmcE624dJxUIOJJpuwJDy21F8kh5ZAsAtE1prWETrQYNElNFjQxM83rS -ZR1Ql2MPSB4usEZT57+KvpEzlOnAT3elgCg21XrjSFGi14hCEao4g2OEZH5GAwm5 -frf6UlSRZ/g3tLTfI8Hv1prw15W2qO+7q7SBAplTODCRk+Yb0YoA2mMM/QXBUcXs -vKEDLSSxzNIBi3T62l39RB/ml+gPKo87ZMDivex1ZhrcJc3Yu3sCAwEAAaOBpzCB -pDAdBgNVHQ4EFgQUPjE+4pun+8FreIdpoR8v6N7xKtUwdQYDVR0jBG4wbIAUPjE+ -4pun+8FreIdpoR8v6N7xKtWhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT -b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCF -mbrORCFzbTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCpvCPdtvXJ -muTj379TZuCJs7/l0FhA7AHa1WAlHjsXHaA7N0+3ZWAbdtXDsowal6S+ldgU/kfV -Lq7NrRq+amJWC7SYj6cvVwhrSwSvu01fe/TWuOzHrRv1uTfJ/VXLonVufMDd9opo -bhqYxMaxLdIx6t/MYmZH4Wpiq0yfZuv//M8i7BBl/qvaWbLhg0yVAKRwjFvf59h6 -6tRFCLddELOIhLDQtk8zMbioPEbfAlKdwwP8kYGtDGj6/9/YTd/oTKRdgHuwyup3 -m0L20Y6LddC+tb0WpK5EyrNbCbEqj1L4/U7r6f/FKNA3bx6nfdXbscaMfYonKAKg -1cRrRg45sErmCz0QyTnWzXyvbjR4oQRzyW3kJ1JZudZ+AwOi00J5FYa3NiLuxl1u -gIGKWSrASQWhEdpa1nlCgX7PhdaQgYjEMpQvA0GCA0OF5JDu8en1yZqsOt1hCLIN -lkz/5jKPqrclY5hV99bE3hgCHRmIPNHCZG3wbZv2yJKxJX1YLMmQwAmSh2N7YwGG -yXRvCxQs5ChPHyRairuf/5MZCZnSVb45ppTVuNUijsbflKRUgfj/XvfqQ22f+C9N -Om2dmNvAiS2TOIfuP47CF2OUa5q4plUwmr+nyXQGM0SIoHNCj+MBdFfb3oxxAtI+ -SLhbnzQv5e84Doqz3YF0XW8jyR7q8GFLNA== ------END CERTIFICATE----- diff --git a/src/leap/services/eip/udstelnet.py b/src/leap/services/eip/udstelnet.py deleted file mode 100644 index e6c82350..00000000 --- a/src/leap/services/eip/udstelnet.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -# udstelnet.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 . - -import os -import socket -import telnetlib - - -class ConnectionRefusedError(Exception): - pass - - -class MissingSocketError(Exception): - pass - - -class UDSTelnet(telnetlib.Telnet): - """ - A telnet-alike class, that can listen on unix domain sockets - """ - - def open(self, host, port=23, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): - """ - Connect to a host. If port is 'unix', it will open a - connection over unix docmain sockets. - - The optional second argument is the port number, which - defaults to the standard telnet port (23). - Don't try to reopen an already connected instance. - """ - self.eof = 0 - self.host = host - self.port = port - self.timeout = timeout - - if self.port == "unix": - # unix sockets spoken - if not os.path.exists(self.host): - raise MissingSocketError() - self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - try: - self.sock.connect(self.host) - except socket.error: - raise ConnectionRefusedError() - else: - self.sock = socket.create_connection((host, port), timeout) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py deleted file mode 100644 index 17950a25..00000000 --- a/src/leap/services/eip/vpnlaunchers.py +++ /dev/null @@ -1,927 +0,0 @@ -# -*- coding: utf-8 -*- -# vpnlaunchers.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 . - -""" -Platform dependant VPN launchers -""" -import commands -import logging -import getpass -import os -import platform -import subprocess -import stat -try: - import grp -except ImportError: - pass # ignore, probably windows - -from abc import ABCMeta, abstractmethod -from functools import partial - -from leap.common.check import leap_assert, leap_assert_type -from leap.common.files import which -from leap.config.providerconfig import ProviderConfig -from leap.services.eip.eipconfig import EIPConfig, VPNGatewaySelector -from leap.util import first -from leap.util.privilege_policies import LinuxPolicyChecker -from leap.util import privilege_policies - -logger = logging.getLogger(__name__) - - -class VPNLauncherException(Exception): - pass - - -class OpenVPNNotFoundException(VPNLauncherException): - pass - - -class EIPNoPolkitAuthAgentAvailable(VPNLauncherException): - pass - - -class EIPNoPkexecAvailable(VPNLauncherException): - pass - - -class EIPNoTunKextLoaded(VPNLauncherException): - pass - - -class VPNLauncher(object): - """ - Abstract launcher class - """ - __metaclass__ = ABCMeta - - UPDOWN_FILES = None - OTHER_FILES = None - - @abstractmethod - def get_vpn_command(self, eipconfig=None, providerconfig=None, - socket_host=None, socket_port=None): - """ - Returns the platform dependant vpn launching command - - :param eipconfig: eip configuration object - :type eipconfig: EIPConfig - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - :param socket_host: either socket path (unix) or socket IP - :type socket_host: str - :param socket_port: either string "unix" if it's a unix - socket, or port otherwise - :type socket_port: str - - :return: A VPN command ready to be launched - :rtype: list - """ - return [] - - @abstractmethod - def get_vpn_env(self, providerconfig): - """ - Returns a dictionary with the custom env for the platform. - This is mainly used for setting LD_LIBRARY_PATH to the correct - path when distributing a standalone client - - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - - :rtype: dict - """ - return {} - - @classmethod - def missing_updown_scripts(kls): - """ - Returns what updown scripts are missing. - :rtype: list - """ - leap_assert(kls.UPDOWN_FILES is not None, - "Need to define UPDOWN_FILES for this particular " - "auncher before calling this method") - file_exist = partial(_has_updown_scripts, warn=False) - zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) - missing = filter(lambda (path, exists): exists is False, zipped) - return [path for path, exists in missing] - - @classmethod - def missing_other_files(kls): - """ - Returns what other important files are missing during startup. - Same as missing_updown_scripts but does not check for exec bit. - :rtype: list - """ - leap_assert(kls.UPDOWN_FILES is not None, - "Need to define OTHER_FILES for this particular " - "auncher before calling this method") - file_exist = partial(_has_other_files, warn=False) - zipped = zip(kls.OTHER_FILES, map(file_exist, kls.OTHER_FILES)) - missing = filter(lambda (path, exists): exists is False, zipped) - return [path for path, exists in missing] - - -def get_platform_launcher(): - launcher = globals()[platform.system() + "VPNLauncher"] - leap_assert(launcher, "Unimplemented platform launcher: %s" % - (platform.system(),)) - return launcher() - - -def _is_pkexec_in_system(): - """ - Checks the existence of the pkexec binary in system. - """ - pkexec_path = which('pkexec') - if len(pkexec_path) == 0: - return False - return True - - -def _has_updown_scripts(path, warn=True): - """ - Checks the existence of the up/down scripts and its - exec bit if applicable. - - :param path: the path to be checked - :type path: str - - :param warn: whether we should log the absence - :type warn: bool - - :rtype: bool - """ - is_file = os.path.isfile(path) - if warn and not is_file: - logger.error("Could not find up/down script %s. " - "Might produce DNS leaks." % (path,)) - - # XXX check if applies in win - is_exe = False - try: - is_exe = (stat.S_IXUSR & os.stat(path)[stat.ST_MODE] != 0) - except OSError as e: - logger.warn("%s" % (e,)) - if warn and not is_exe: - logger.error("Up/down script %s is not executable. " - "Might produce DNS leaks." % (path,)) - return is_file and is_exe - - -def _has_other_files(path, warn=True): - """ - Checks the existence of other important files. - - :param path: the path to be checked - :type path: str - - :param warn: whether we should log the absence - :type warn: bool - - :rtype: bool - """ - is_file = os.path.isfile(path) - if warn and not is_file: - logger.warning("Could not find file during checks: %s. " % ( - path,)) - return is_file - - -def _is_auth_agent_running(): - """ - Checks if a polkit daemon is running. - - :return: True if it's running, False if it's not. - :rtype: boolean - """ - ps = 'ps aux | grep polkit-%s-authentication-agent-1' - opts = (ps % case for case in ['[g]nome', '[k]de']) - is_running = map(lambda l: commands.getoutput(l), opts) - return any(is_running) - - -def _try_to_launch_agent(): - """ - Tries to launch a polkit daemon. - """ - opts = [ - "/usr/lib/policykit-1-gnome/polkit-gnome-authentication-agent-1", - # XXX add kde thing here - ] - for cmd in opts: - try: - subprocess.Popen([cmd], shell=True) - except: - pass - - -class LinuxVPNLauncher(VPNLauncher): - """ - VPN launcher for the Linux platform - """ - - PKEXEC_BIN = 'pkexec' - OPENVPN_BIN = 'openvpn' - OPENVPN_BIN_PATH = os.path.join( - ProviderConfig().get_path_prefix(), - "..", "apps", "eip", OPENVPN_BIN) - - SYSTEM_CONFIG = "/etc/leap" - UP_DOWN_FILE = "resolv-update" - UP_DOWN_PATH = "%s/%s" % (SYSTEM_CONFIG, UP_DOWN_FILE) - - # We assume this is there by our openvpn dependency, and - # we will put it there on the bundle too. - # TODO adapt to the bundle path. - OPENVPN_DOWN_ROOT_BASE = "/usr/lib/openvpn/" - OPENVPN_DOWN_ROOT_FILE = "openvpn-plugin-down-root.so" - OPENVPN_DOWN_ROOT_PATH = "%s/%s" % ( - OPENVPN_DOWN_ROOT_BASE, - OPENVPN_DOWN_ROOT_FILE) - - UPDOWN_FILES = (UP_DOWN_PATH,) - POLKIT_PATH = LinuxPolicyChecker.get_polkit_path() - OTHER_FILES = (POLKIT_PATH, ) - - def missing_other_files(self): - """ - 'Extend' the VPNLauncher's missing_other_files to check if the polkit - files is outdated. If the polkit file that is in OTHER_FILES exists but - is not up to date, it is added to the missing list. - - :returns: a list of missing files - :rtype: list of str - """ - missing = VPNLauncher.missing_other_files.im_func(self) - polkit_file = LinuxPolicyChecker.get_polkit_path() - if polkit_file not in missing: - if privilege_policies.is_policy_outdated(self.OPENVPN_BIN_PATH): - missing.append(polkit_file) - - return missing - - @classmethod - def cmd_for_missing_scripts(kls, frompath, pol_file): - """ - Returns a sh script that can copy the missing files. - - :param frompath: The path where the up/down scripts live - :type frompath: str - :param pol_file: The path where the dynamically generated - policy file lives - :type pol_file: str - - :rtype: str - """ - to = kls.SYSTEM_CONFIG - - cmd = '#!/bin/sh\n' - cmd += 'mkdir -p "%s"\n' % (to, ) - cmd += 'cp "%s/%s" "%s"\n' % (frompath, kls.UP_DOWN_FILE, to) - cmd += 'cp "%s" "%s"\n' % (pol_file, kls.POLKIT_PATH) - cmd += 'chmod 644 "%s"\n' % (kls.POLKIT_PATH, ) - - return cmd - - @classmethod - def maybe_pkexec(kls): - """ - Checks whether pkexec is available in the system, and - returns the path if found. - - Might raise EIPNoPkexecAvailable or EIPNoPolkitAuthAgentAvailable - - :returns: a list of the paths where pkexec is to be found - :rtype: list - """ - if _is_pkexec_in_system(): - if not _is_auth_agent_running(): - _try_to_launch_agent() - if _is_auth_agent_running(): - pkexec_possibilities = which(kls.PKEXEC_BIN) - leap_assert(len(pkexec_possibilities) > 0, - "We couldn't find pkexec") - return pkexec_possibilities - else: - logger.warning("No polkit auth agent found. pkexec " + - "will use its own auth agent.") - raise EIPNoPolkitAuthAgentAvailable() - else: - logger.warning("System has no pkexec") - raise EIPNoPkexecAvailable() - - @classmethod - def maybe_down_plugin(kls): - """ - Returns the path of the openvpn down-root-plugin, searching first - in the relative path for the standalone bundle, and then in the system - path where the debian package puts it. - - :returns: the path where the plugin was found, or None - :rtype: str or None - """ - cwd = os.getcwd() - rel_path_in_bundle = os.path.join( - 'apps', 'eip', 'files', kls.OPENVPN_DOWN_ROOT_FILE) - abs_path_in_bundle = os.path.join(cwd, rel_path_in_bundle) - if os.path.isfile(abs_path_in_bundle): - return abs_path_in_bundle - abs_path_in_system = kls.OPENVPN_DOWN_ROOT_FILE - if os.path.isfile(abs_path_in_system): - return abs_path_in_system - - logger.warning("We could not find the down-root-plugin, so no updown " - "scripts will be run. DNS leaks are likely!") - return None - - def get_vpn_command(self, eipconfig=None, providerconfig=None, - socket_host=None, socket_port="unix", openvpn_verb=1): - """ - Returns the platform dependant vpn launching command. It will - look for openvpn in the regular paths and algo in - path_prefix/apps/eip/ (in case standalone is set) - - Might raise: - VPNLauncherException, - OpenVPNNotFoundException. - - :param eipconfig: eip configuration object - :type eipconfig: EIPConfig - - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - - :param socket_host: either socket path (unix) or socket IP - :type socket_host: str - - :param socket_port: either string "unix" if it's a unix - socket, or port otherwise - :type socket_port: str - - :param openvpn_verb: openvpn verbosity wanted - :type openvpn_verb: int - - :return: A VPN command ready to be launched - :rtype: list - """ - leap_assert(eipconfig, "We need an eip config") - leap_assert_type(eipconfig, EIPConfig) - leap_assert(providerconfig, "We need a provider config") - leap_assert_type(providerconfig, ProviderConfig) - leap_assert(socket_host, "We need a socket host!") - leap_assert(socket_port, "We need a socket port!") - - kwargs = {} - if ProviderConfig.standalone: - kwargs['path_extension'] = os.path.join( - providerconfig.get_path_prefix(), - "..", "apps", "eip") - - openvpn_possibilities = which(self.OPENVPN_BIN, **kwargs) - - if len(openvpn_possibilities) == 0: - raise OpenVPNNotFoundException() - - openvpn = first(openvpn_possibilities) - args = [] - - pkexec = self.maybe_pkexec() - if pkexec: - args.append(openvpn) - openvpn = first(pkexec) - - if openvpn_verb is not None: - args += ['--verb', '%d' % (openvpn_verb,)] - - gateway_selector = VPNGatewaySelector(eipconfig) - gateways = gateway_selector.get_gateways() - - if not gateways: - logger.error('No gateway was found!') - raise VPNLauncherException(self.tr('No gateway was found!')) - - logger.debug("Using gateways ips: {}".format(', '.join(gateways))) - - for gw in gateways: - args += ['--remote', gw, '1194', 'udp'] - - args += [ - '--client', - '--dev', 'tun', - ############################################################## - # persist-tun makes ping-restart fail because it leaves a - # broken routing table - ############################################################## - # '--persist-tun', - '--persist-key', - '--tls-client', - '--remote-cert-tls', - 'server' - ] - - openvpn_configuration = eipconfig.get_openvpn_configuration() - - for key, value in openvpn_configuration.items(): - args += ['--%s' % (key,), value] - - ############################################################## - # The down-root plugin fails in some situations, so we don't - # drop privs for the time being - ############################################################## - # args += [ - # '--user', getpass.getuser(), - # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name - # ] - - if socket_port == "unix": # that's always the case for linux - args += [ - '--management-client-user', getpass.getuser() - ] - - args += [ - '--management-signal', - '--management', socket_host, socket_port, - '--script-security', '2' - ] - - plugin_path = self.maybe_down_plugin() - # If we do not have the down plugin neither in the bundle - # nor in the system, we do not do updown scripts. The alternative - # is leaving the user without the ability to restore dns and routes - # to its original state. - - if plugin_path and _has_updown_scripts(self.UP_DOWN_PATH): - args += [ - '--up', self.UP_DOWN_PATH, - '--down', self.UP_DOWN_PATH, - ############################################################## - # For the time being we are disabling the usage of the - # down-root plugin, because it doesn't quite work as - # expected (i.e. it doesn't run route -del as root - # when finishing, so it fails to properly - # restart/quit) - ############################################################## - # '--plugin', plugin_path, - # '\'script_type=down %s\'' % self.UP_DOWN_PATH - ] - - args += [ - '--cert', eipconfig.get_client_cert_path(providerconfig), - '--key', eipconfig.get_client_cert_path(providerconfig), - '--ca', providerconfig.get_ca_cert_path() - ] - - logger.debug("Running VPN with command:") - logger.debug("%s %s" % (openvpn, " ".join(args))) - - return [openvpn] + args - - def get_vpn_env(self, providerconfig): - """ - Returns a dictionary with the custom env for the platform. - This is mainly used for setting LD_LIBRARY_PATH to the correct - path when distributing a standalone client - - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - - :rtype: dict - """ - leap_assert(providerconfig, "We need a provider config") - leap_assert_type(providerconfig, ProviderConfig) - - return {"LD_LIBRARY_PATH": os.path.join( - providerconfig.get_path_prefix(), - "..", "lib")} - - -class DarwinVPNLauncher(VPNLauncher): - """ - VPN launcher for the Darwin Platform - """ - - COCOASUDO = "cocoasudo" - # XXX need the good old magic translate for these strings - # (look for magic in 0.2.0 release) - SUDO_MSG = ("Bitmask needs administrative privileges to run " - "Encrypted Internet.") - INSTALL_MSG = ("\"Bitmask needs administrative privileges to install " - "missing scripts and fix permissions.\"") - - INSTALL_PATH = os.path.realpath(os.getcwd() + "/../../") - INSTALL_PATH_ESCAPED = os.path.realpath(os.getcwd() + "/../../") - OPENVPN_BIN = 'openvpn.leap' - OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,) - OPENVPN_PATH_ESCAPED = "%s/Contents/Resources/openvpn" % ( - INSTALL_PATH_ESCAPED,) - - UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,) - DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,) - OPENVPN_DOWN_PLUGIN = '%s/openvpn-down-root.so' % (OPENVPN_PATH,) - - UPDOWN_FILES = (UP_SCRIPT, DOWN_SCRIPT, OPENVPN_DOWN_PLUGIN) - OTHER_FILES = [] - - @classmethod - def cmd_for_missing_scripts(kls, frompath): - """ - Returns a command that can copy the missing scripts. - :rtype: str - """ - to = kls.OPENVPN_PATH_ESCAPED - cmd = "#!/bin/sh\nmkdir -p %s\ncp \"%s/\"* %s\nchmod 744 %s/*" % ( - to, frompath, to, to) - return cmd - - @classmethod - def maybe_kextloaded(kls): - """ - Checks if the needed kext is loaded before launching openvpn. - """ - return bool(commands.getoutput('kextstat | grep "leap.tun"')) - - def _get_resource_path(self): - """ - Returns the absolute path to the app resources directory - - :rtype: str - """ - return os.path.abspath( - os.path.join( - os.getcwd(), - "../../Contents/Resources")) - - def _get_icon_path(self): - """ - Returns the absolute path to the app icon - - :rtype: str - """ - return os.path.join(self._get_resource_path(), - "leap-client.tiff") - - def get_cocoasudo_ovpn_cmd(self): - """ - Returns a string with the cocoasudo command needed to run openvpn - as admin with a nice password prompt. The actual command needs to be - appended. - - :rtype: (str, list) - """ - iconpath = self._get_icon_path() - has_icon = os.path.isfile(iconpath) - args = ["--icon=%s" % iconpath] if has_icon else [] - args.append("--prompt=%s" % (self.SUDO_MSG,)) - - return self.COCOASUDO, args - - def get_cocoasudo_installmissing_cmd(self): - """ - Returns a string with the cocoasudo command needed to install missing - files as admin with a nice password prompt. The actual command needs to - be appended. - - :rtype: (str, list) - """ - iconpath = self._get_icon_path() - has_icon = os.path.isfile(iconpath) - args = ["--icon=%s" % iconpath] if has_icon else [] - args.append("--prompt=%s" % (self.INSTALL_MSG,)) - - return self.COCOASUDO, args - - def get_vpn_command(self, eipconfig=None, providerconfig=None, - socket_host=None, socket_port="unix", openvpn_verb=1): - """ - Returns the platform dependant vpn launching command - - Might raise VPNException. - - :param eipconfig: eip configuration object - :type eipconfig: EIPConfig - - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - - :param socket_host: either socket path (unix) or socket IP - :type socket_host: str - - :param socket_port: either string "unix" if it's a unix - socket, or port otherwise - :type socket_port: str - - :param openvpn_verb: openvpn verbosity wanted - :type openvpn_verb: int - - :return: A VPN command ready to be launched - :rtype: list - """ - leap_assert(eipconfig, "We need an eip config") - leap_assert_type(eipconfig, EIPConfig) - leap_assert(providerconfig, "We need a provider config") - leap_assert_type(providerconfig, ProviderConfig) - leap_assert(socket_host, "We need a socket host!") - leap_assert(socket_port, "We need a socket port!") - - if not self.maybe_kextloaded(): - raise EIPNoTunKextLoaded - - kwargs = {} - if ProviderConfig.standalone: - kwargs['path_extension'] = os.path.join( - providerconfig.get_path_prefix(), - "..", "apps", "eip") - - openvpn_possibilities = which( - self.OPENVPN_BIN, - **kwargs) - if len(openvpn_possibilities) == 0: - raise OpenVPNNotFoundException() - - openvpn = first(openvpn_possibilities) - args = [openvpn] - - if openvpn_verb is not None: - args += ['--verb', '%d' % (openvpn_verb,)] - - gateway_selector = VPNGatewaySelector(eipconfig) - gateways = gateway_selector.get_gateways() - - logger.debug("Using gateways ips: {gw}".format( - gw=', '.join(gateways))) - - for gw in gateways: - args += ['--remote', gw, '1194', 'udp'] - - args += [ - '--client', - '--dev', 'tun', - ############################################################## - # persist-tun makes ping-restart fail because it leaves a - # broken routing table - ############################################################## - # '--persist-tun', - '--persist-key', - '--tls-client', - '--remote-cert-tls', - 'server' - ] - - openvpn_configuration = eipconfig.get_openvpn_configuration() - for key, value in openvpn_configuration.items(): - args += ['--%s' % (key,), value] - - user = getpass.getuser() - - ############################################################## - # The down-root plugin fails in some situations, so we don't - # drop privs for the time being - ############################################################## - # args += [ - # '--user', user, - # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name - # ] - - if socket_port == "unix": - args += [ - '--management-client-user', user - ] - - args += [ - '--management-signal', - '--management', socket_host, socket_port, - '--script-security', '2' - ] - - if _has_updown_scripts(self.UP_SCRIPT): - args += [ - '--up', '\"%s\"' % (self.UP_SCRIPT,), - ] - - if _has_updown_scripts(self.DOWN_SCRIPT): - args += [ - '--down', '\"%s\"' % (self.DOWN_SCRIPT,) - ] - - # should have the down script too - if _has_updown_scripts(self.OPENVPN_DOWN_PLUGIN): - args += [ - ########################################################### - # For the time being we are disabling the usage of the - # down-root plugin, because it doesn't quite work as - # expected (i.e. it doesn't run route -del as root - # when finishing, so it fails to properly - # restart/quit) - ########################################################### - # '--plugin', self.OPENVPN_DOWN_PLUGIN, - # '\'%s\'' % self.DOWN_SCRIPT - ] - - # we set user to be passed to the up/down scripts - args += [ - '--setenv', "LEAPUSER", "%s" % (user,)] - - args += [ - '--cert', eipconfig.get_client_cert_path(providerconfig), - '--key', eipconfig.get_client_cert_path(providerconfig), - '--ca', providerconfig.get_ca_cert_path() - ] - - command, cargs = self.get_cocoasudo_ovpn_cmd() - cmd_args = cargs + args - - logger.debug("Running VPN with command:") - logger.debug("%s %s" % (command, " ".join(cmd_args))) - - return [command] + cmd_args - - def get_vpn_env(self, providerconfig): - """ - Returns a dictionary with the custom env for the platform. - This is mainly used for setting LD_LIBRARY_PATH to the correct - path when distributing a standalone client - - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - - :rtype: dict - """ - return {"DYLD_LIBRARY_PATH": os.path.join( - providerconfig.get_path_prefix(), - "..", "lib")} - - -class WindowsVPNLauncher(VPNLauncher): - """ - VPN launcher for the Windows platform - """ - - OPENVPN_BIN = 'openvpn_leap.exe' - - # XXX UPDOWN_FILES ... we do not have updown files defined yet! - # (and maybe we won't) - - def get_vpn_command(self, eipconfig=None, providerconfig=None, - socket_host=None, socket_port="9876", openvpn_verb=1): - """ - Returns the platform dependant vpn launching command. It will - look for openvpn in the regular paths and algo in - path_prefix/apps/eip/ (in case standalone is set) - - Might raise VPNException. - - :param eipconfig: eip configuration object - :type eipconfig: EIPConfig - - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - - :param socket_host: either socket path (unix) or socket IP - :type socket_host: str - - :param socket_port: either string "unix" if it's a unix - socket, or port otherwise - :type socket_port: str - - :param openvpn_verb: the openvpn verbosity wanted - :type openvpn_verb: int - - :return: A VPN command ready to be launched - :rtype: list - """ - leap_assert(eipconfig, "We need an eip config") - leap_assert_type(eipconfig, EIPConfig) - leap_assert(providerconfig, "We need a provider config") - leap_assert_type(providerconfig, ProviderConfig) - leap_assert(socket_host, "We need a socket host!") - leap_assert(socket_port, "We need a socket port!") - leap_assert(socket_port != "unix", - "We cannot use unix sockets in windows!") - - openvpn_possibilities = which( - self.OPENVPN_BIN, - path_extension=os.path.join(providerconfig.get_path_prefix(), - "..", "apps", "eip")) - - if len(openvpn_possibilities) == 0: - raise OpenVPNNotFoundException() - - openvpn = first(openvpn_possibilities) - args = [] - if openvpn_verb is not None: - args += ['--verb', '%d' % (openvpn_verb,)] - - gateway_selector = VPNGatewaySelector(eipconfig) - gateways = gateway_selector.get_gateways() - - logger.debug("Using gateways ips: {}".format(', '.join(gateways))) - - for gw in gateways: - args += ['--remote', gw, '1194', 'udp'] - - args += [ - '--client', - '--dev', 'tun', - ############################################################## - # persist-tun makes ping-restart fail because it leaves a - # broken routing table - ############################################################## - # '--persist-tun', - '--persist-key', - '--tls-client', - # We make it log to a file because we cannot attach to the - # openvpn process' stdout since it's a process with more - # privileges than we are - '--log-append', 'eip.log', - '--remote-cert-tls', - 'server' - ] - - openvpn_configuration = eipconfig.get_openvpn_configuration() - for key, value in openvpn_configuration.items(): - args += ['--%s' % (key,), value] - - ############################################################## - # The down-root plugin fails in some situations, so we don't - # drop privs for the time being - ############################################################## - # args += [ - # '--user', getpass.getuser(), - # #'--group', grp.getgrgid(os.getgroups()[-1]).gr_name - # ] - - args += [ - '--management-signal', - '--management', socket_host, socket_port, - '--script-security', '2' - ] - - args += [ - '--cert', eipconfig.get_client_cert_path(providerconfig), - '--key', eipconfig.get_client_cert_path(providerconfig), - '--ca', providerconfig.get_ca_cert_path() - ] - - logger.debug("Running VPN with command:") - logger.debug("%s %s" % (openvpn, " ".join(args))) - - return [openvpn] + args - - def get_vpn_env(self, providerconfig): - """ - Returns a dictionary with the custom env for the platform. - This is mainly used for setting LD_LIBRARY_PATH to the correct - path when distributing a standalone client - - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - - :rtype: dict - """ - return {} - - -if __name__ == "__main__": - logger = logging.getLogger(name='leap') - logger.setLevel(logging.DEBUG) - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - try: - abs_launcher = VPNLauncher() - except Exception as e: - assert isinstance(e, TypeError), "Something went wrong" - print "Abstract Prefixer class is working as expected" - - vpnlauncher = get_platform_launcher() - - eipconfig = EIPConfig() - eipconfig.set_api_version('1') - if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): - provider = ProviderConfig() - if provider.load("leap/providers/bitmask.net/provider.json"): - vpnlauncher.get_vpn_command(eipconfig=eipconfig, - providerconfig=provider, - socket_host="/blah") diff --git a/src/leap/services/eip/vpnprocess.py b/src/leap/services/eip/vpnprocess.py deleted file mode 100644 index 5b07a3cf..00000000 --- a/src/leap/services/eip/vpnprocess.py +++ /dev/null @@ -1,791 +0,0 @@ -# -*- coding: utf-8 -*- -# vpnprocess.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 . -""" -VPN Manager, spawned in a custom processProtocol. -""" -import logging -import os -import psutil -import psutil.error -import shutil -import socket - -from PySide import QtCore - -from leap.common.check import leap_assert, leap_assert_type -from leap.config.providerconfig import ProviderConfig -from leap.services.eip.vpnlaunchers import get_platform_launcher -from leap.services.eip.eipconfig import EIPConfig -from leap.services.eip.udstelnet import UDSTelnet -from leap.util import first - -logger = logging.getLogger(__name__) -vpnlog = logging.getLogger('leap.openvpn') - -from twisted.internet import protocol -from twisted.internet import defer -from twisted.internet.task import LoopingCall -from twisted.internet import error as internet_error - - -class VPNSignals(QtCore.QObject): - """ - These are the signals that we use to let the UI know - about the events we are polling. - They are instantiated in the VPN object and passed along - till the VPNProcess. - """ - state_changed = QtCore.Signal(dict) - status_changed = QtCore.Signal(dict) - process_finished = QtCore.Signal(int) - - def __init__(self): - QtCore.QObject.__init__(self) - - -class OpenVPNAlreadyRunning(Exception): - message = ("Another openvpn instance is already running, and could " - "not be stopped.") - - -class AlienOpenVPNAlreadyRunning(Exception): - message = ("Another openvpn instance is already running, and could " - "not be stopped because it was not launched by LEAP.") - - -class VPN(object): - """ - This is the high-level object that the GUI is dealing with. - It exposes the start and terminate methods. - - On start, it spawns a VPNProcess instance that will use a vpnlauncher - suited for the running platform and connect to the management interface - opened by the openvpn process, executing commands over that interface on - demand. - """ - TERMINATE_MAXTRIES = 10 - TERMINATE_WAIT = 1 # secs - - OPENVPN_VERB = "openvpn_verb" - - def __init__(self, **kwargs): - """ - Instantiate empty attributes and get a copy - of a QObject containing the QSignals that we will pass along - to the VPNManager. - """ - from twisted.internet import reactor - self._vpnproc = None - self._pollers = [] - self._reactor = reactor - self._qtsigs = VPNSignals() - - self._openvpn_verb = kwargs.get(self.OPENVPN_VERB, None) - - @property - def qtsigs(self): - return self._qtsigs - - def start(self, *args, **kwargs): - """ - Starts the openvpn subprocess. - - :param args: args to be passed to the VPNProcess - :type args: tuple - - :param kwargs: kwargs to be passed to the VPNProcess - :type kwargs: dict - """ - self._stop_pollers() - kwargs['qtsigs'] = self.qtsigs - kwargs['openvpn_verb'] = self._openvpn_verb - - # start the main vpn subprocess - vpnproc = VPNProcess(*args, **kwargs) - #qtsigs=self.qtsigs, - #openvpn_verb=self._openvpn_verb) - - if vpnproc.get_openvpn_process(): - logger.info("Another vpn process is running. Will try to stop it.") - vpnproc.stop_if_already_running() - - cmd = vpnproc.getCommand() - env = os.environ - for key, val in vpnproc.vpn_env.items(): - env[key] = val - - self._reactor.spawnProcess(vpnproc, cmd[0], cmd, env) - self._vpnproc = vpnproc - - # add pollers for status and state - # this could be extended to a collection of - # generic watchers - - poll_list = [LoopingCall(vpnproc.pollStatus), - LoopingCall(vpnproc.pollState)] - self._pollers.extend(poll_list) - self._start_pollers() - - def _kill_if_left_alive(self, tries=0): - """ - Check if the process is still alive, and sends a - SIGKILL after a timeout period. - - :param tries: counter of tries, used in recursion - :type tries: int - """ - from twisted.internet import reactor - while tries < self.TERMINATE_MAXTRIES: - if self._vpnproc.transport.pid is None: - logger.debug("Process has been happily terminated.") - return - else: - logger.debug("Process did not die, waiting...") - tries += 1 - reactor.callLater(self.TERMINATE_WAIT, - self._kill_if_left_alive, tries) - - # after running out of patience, we try a killProcess - logger.debug("Process did not died. Sending a SIGKILL.") - self.killit() - - def killit(self): - """ - Sends a kill signal to the process. - """ - self._stop_pollers() - self._vpnproc.aborted = True - self._vpnproc.killProcess() - - def terminate(self, shutdown=False): - """ - Stops the openvpn subprocess. - - Attempts to send a SIGTERM first, and after a timeout - it sends a SIGKILL. - """ - from twisted.internet import reactor - self._stop_pollers() - - # First we try to be polite and send a SIGTERM... - if self._vpnproc: - self._sentterm = True - self._vpnproc.terminate_openvpn(shutdown=shutdown) - - # ...but we also trigger a countdown to be unpolite - # if strictly needed. - - # XXX Watch out! This will fail NOW since we are running - # openvpn as root as a workaround for some connection issues. - reactor.callLater( - self.TERMINATE_WAIT, self._kill_if_left_alive) - - def _start_pollers(self): - """ - Iterate through the registered observers - and start the looping call for them. - """ - for poller in self._pollers: - poller.start(VPNManager.POLL_TIME) - - def _stop_pollers(self): - """ - Iterate through the registered observers - and stop the looping calls if they are running. - """ - for poller in self._pollers: - if poller.running: - poller.stop() - self._pollers = [] - - -class VPNManager(object): - """ - This is a mixin that we use in the VPNProcess class. - Here we get together all methods related with the openvpn management - interface. - - A copy of a QObject containing signals as attributes is passed along - upon initialization, and we use that object to emit signals to qt-land. - - For more info about management methods:: - - zcat `dpkg -L openvpn | grep management` - """ - - # Timers, in secs - POLL_TIME = 0.5 - CONNECTION_RETRY_TIME = 1 - - TS_KEY = "ts" - STATUS_STEP_KEY = "status_step" - OK_KEY = "ok" - IP_KEY = "ip" - REMOTE_KEY = "remote" - - TUNTAP_READ_KEY = "tun_tap_read" - TUNTAP_WRITE_KEY = "tun_tap_write" - TCPUDP_READ_KEY = "tcp_udp_read" - TCPUDP_WRITE_KEY = "tcp_udp_write" - AUTH_READ_KEY = "auth_read" - - def __init__(self, qtsigs=None): - """ - Initializes the VPNManager. - - :param qtsigs: a QObject containing the Qt signals used by the UI - to give feedback about state changes. - :type qtsigs: QObject - """ - from twisted.internet import reactor - self._reactor = reactor - self._tn = None - self._qtsigs = qtsigs - self._aborted = False - - @property - def qtsigs(self): - return self._qtsigs - - @property - def aborted(self): - return self._aborted - - @aborted.setter - def aborted(self, value): - self._aborted = value - - def _seek_to_eof(self): - """ - Read as much as available. Position seek pointer to end of stream - """ - try: - self._tn.read_eager() - except EOFError: - logger.debug("Could not read from socket. Assuming it died.") - return - - def _send_command(self, command, until=b"END"): - """ - Sends a command to the telnet connection and reads until END - is reached. - - :param command: command to send - :type command: str - - :param until: byte delimiter string for reading command output - :type until: byte str - - :return: response read - :rtype: list - """ - leap_assert(self._tn, "We need a tn connection!") - - try: - self._tn.write("%s\n" % (command,)) - buf = self._tn.read_until(until, 2) - self._seek_to_eof() - blist = buf.split('\r\n') - if blist[-1].startswith(until): - del blist[-1] - return blist - else: - return [] - - except socket.error: - # XXX should get a counter and repeat only - # after mod X times. - logger.warning('socket error (command was: "%s")' % (command,)) - self._close_management_socket(announce=False) - logger.debug('trying to connect to management again') - self.try_to_connect_to_management(max_retries=5) - return [] - - # XXX should move this to a errBack! - except Exception as e: - logger.warning("Error sending command %s: %r" % - (command, e)) - return [] - - def _close_management_socket(self, announce=True): - """ - Close connection to openvpn management interface. - """ - logger.debug('closing socket') - if announce: - self._tn.write("quit\n") - self._tn.read_all() - self._tn.get_socket().close() - self._tn = None - - def _connect_management(self, socket_host, socket_port): - """ - Connects to the management interface on the specified - socket_host socket_port. - - :param socket_host: either socket path (unix) or socket IP - :type socket_host: str - - :param socket_port: either string "unix" if it's a unix - socket, or port otherwise - :type socket_port: str - """ - if self.is_connected(): - self._close_management_socket() - - try: - self._tn = UDSTelnet(socket_host, socket_port) - - # XXX make password optional - # specially for win. we should generate - # the pass on the fly when invoking manager - # from conductor - - # self.tn.read_until('ENTER PASSWORD:', 2) - # self.tn.write(self.password + '\n') - # self.tn.read_until('SUCCESS:', 2) - if self._tn: - self._tn.read_eager() - - # XXX move this to the Errback - except Exception as e: - logger.warning("Could not connect to OpenVPN yet: %r" % (e,)) - self._tn = None - - def _connectCb(self, *args): - """ - Callback for connection. - - :param args: not used - """ - if self._tn: - logger.info('Connected to management') - else: - logger.debug('Cannot connect to management...') - - def _connectErr(self, failure): - """ - Errorback for connection. - - :param failure: Failure - """ - logger.warning(failure) - - def connect_to_management(self, host, port): - """ - Connect to a management interface. - - :param host: the host of the management interface - :type host: str - - :param port: the port of the management interface - :type port: str - - :returns: a deferred - """ - self.connectd = defer.maybeDeferred( - self._connect_management, host, port) - self.connectd.addCallbacks(self._connectCb, self._connectErr) - return self.connectd - - def is_connected(self): - """ - Returns the status of the management interface. - - :returns: True if connected, False otherwise - :rtype: bool - """ - return True if self._tn else False - - def try_to_connect_to_management(self, retry=0, max_retries=None): - """ - Attempts to connect to a management interface, and retries - after CONNECTION_RETRY_TIME if not successful. - - :param retry: number of the retry - :type retry: int - """ - if max_retries and retry > max_retries: - logger.warning("Max retries reached while attempting to connect " - "to management. Aborting.") - self.aborted = True - return - - # _alive flag is set in the VPNProcess class. - if not self._alive: - logger.debug('Tried to connect to management but process is ' - 'not alive.') - return - logger.debug('trying to connect to management') - if not self.aborted and not self.is_connected(): - self.connect_to_management(self._socket_host, self._socket_port) - self._reactor.callLater( - self.CONNECTION_RETRY_TIME, - self.try_to_connect_to_management, retry + 1) - - def _parse_state_and_notify(self, output): - """ - Parses the output of the state command and emits state_changed - signal when the state changes. - - :param output: list of lines that the state command printed as - its output - :type output: list - """ - for line in output: - stripped = line.strip() - if stripped == "END": - continue - parts = stripped.split(",") - if len(parts) < 5: - continue - ts, status_step, ok, ip, remote = parts - - state_dict = { - self.TS_KEY: ts, - self.STATUS_STEP_KEY: status_step, - self.OK_KEY: ok, - self.IP_KEY: ip, - self.REMOTE_KEY: remote - } - - if state_dict != self._last_state: - self.qtsigs.state_changed.emit(state_dict) - self._last_state = state_dict - - def _parse_status_and_notify(self, output): - """ - Parses the output of the status command and emits - status_changed signal when the status changes. - - :param output: list of lines that the status command printed - as its output - :type output: list - """ - tun_tap_read = "" - tun_tap_write = "" - tcp_udp_read = "" - tcp_udp_write = "" - auth_read = "" - for line in output: - stripped = line.strip() - if stripped.endswith("STATISTICS") or stripped == "END": - continue - parts = stripped.split(",") - if len(parts) < 2: - continue - if parts[0].strip() == "TUN/TAP read bytes": - tun_tap_read = parts[1] - elif parts[0].strip() == "TUN/TAP write bytes": - tun_tap_write = parts[1] - elif parts[0].strip() == "TCP/UDP read bytes": - tcp_udp_read = parts[1] - elif parts[0].strip() == "TCP/UDP write bytes": - tcp_udp_write = parts[1] - elif parts[0].strip() == "Auth read bytes": - auth_read = parts[1] - - status_dict = { - self.TUNTAP_READ_KEY: tun_tap_read, - self.TUNTAP_WRITE_KEY: tun_tap_write, - self.TCPUDP_READ_KEY: tcp_udp_read, - self.TCPUDP_WRITE_KEY: tcp_udp_write, - self.AUTH_READ_KEY: auth_read - } - - if status_dict != self._last_status: - self.qtsigs.status_changed.emit(status_dict) - self._last_status = status_dict - - def get_state(self): - """ - Notifies the gui of the output of the state command over - the openvpn management interface. - """ - if self.is_connected(): - return self._parse_state_and_notify(self._send_command("state")) - - def get_status(self): - """ - Notifies the gui of the output of the status command over - the openvpn management interface. - """ - if self.is_connected(): - return self._parse_status_and_notify(self._send_command("status")) - - @property - def vpn_env(self): - """ - Return a dict containing the vpn environment to be used. - """ - return self._launcher.get_vpn_env(self._providerconfig) - - def terminate_openvpn(self, shutdown=False): - """ - Attempts to terminate openvpn by sending a SIGTERM. - """ - if self.is_connected(): - self._send_command("signal SIGTERM") - if shutdown: - self._cleanup_tempfiles() - - def _cleanup_tempfiles(self): - """ - Remove all temporal files we might have left behind. - - Iif self.port is 'unix', we have created a temporal socket path that, - under normal circumstances, we should be able to delete. - """ - if self._socket_port == "unix": - logger.debug('cleaning socket file temp folder') - tempfolder = first(os.path.split(self._socket_host)) - if tempfolder and os.path.isdir(tempfolder): - try: - shutil.rmtree(tempfolder) - except OSError: - logger.error('could not delete tmpfolder %s' % tempfolder) - - def get_openvpn_process(self): - """ - Looks for openvpn instances running. - - :rtype: process - """ - openvpn_process = None - for p in psutil.process_iter(): - try: - # XXX Not exact! - # Will give false positives. - # we should check that cmdline BEGINS - # with openvpn or with our wrapper - # (pkexec / osascript / whatever) - - # This needs more work, see #3268, but for the moment - # we need to be able to filter out arguments in the form - # --openvpn-foo, since otherwise we are shooting ourselves - # in the feet. - if any(map(lambda s: s.startswith("openvpn"), p.cmdline)): - openvpn_process = p - break - except psutil.error.AccessDenied: - pass - return openvpn_process - - def stop_if_already_running(self): - """ - Checks if VPN is already running and tries to stop it. - - Might raise OpenVPNAlreadyRunning. - - :return: True if stopped, False otherwise - - """ - process = self.get_openvpn_process() - if not process: - logger.debug('Could not find openvpn process while ' - 'trying to stop it.') - return - - logger.debug("OpenVPN is already running, trying to stop it...") - cmdline = process.cmdline - - manag_flag = "--management" - if isinstance(cmdline, list) and manag_flag in cmdline: - # we know that our invocation has this distinctive fragment, so - # we use this fingerprint to tell other invocations apart. - # this might break if we change the configuration path in the - # launchers - smellslikeleap = lambda s: "leap" in s and "providers" in s - - if not any(map(smellslikeleap, cmdline)): - logger.debug("We cannot stop this instance since we do not " - "recognise it as a leap invocation.") - raise AlienOpenVPNAlreadyRunning - - try: - index = cmdline.index(manag_flag) - host = cmdline[index + 1] - port = cmdline[index + 2] - logger.debug("Trying to connect to %s:%s" - % (host, port)) - self.connect_to_management(host, port) - - # XXX this has a problem with connections to different - # remotes. So the reconnection will only work when we are - # terminating instances left running for the same provider. - # If we are killing an openvpn instance configured for another - # provider, we will get: - # TLS Error: local/remote TLS keys are out of sync - # However, that should be a rare case right now. - self._send_command("signal SIGTERM") - self._close_management_socket(announce=True) - except Exception as e: - logger.warning("Problem trying to terminate OpenVPN: %r" - % (e,)) - else: - logger.debug("Could not find the expected openvpn command line.") - - process = self.get_openvpn_process() - if process is None: - logger.debug("Successfully finished already running " - "openvpn process.") - return True - else: - logger.warning("Unable to terminate OpenVPN") - raise OpenVPNAlreadyRunning - - -class VPNProcess(protocol.ProcessProtocol, VPNManager): - """ - A ProcessProtocol class that can be used to spawn a process that will - launch openvpn and connect to its management interface to control it - programmatically. - """ - - def __init__(self, eipconfig, providerconfig, socket_host, socket_port, - qtsigs, openvpn_verb): - """ - :param eipconfig: eip configuration object - :type eipconfig: EIPConfig - - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - - :param socket_host: either socket path (unix) or socket IP - :type socket_host: str - - :param socket_port: either string "unix" if it's a unix - socket, or port otherwise - :type socket_port: str - - :param qtsigs: a QObject containing the Qt signals used to notify the - UI. - :type qtsigs: QObject - - :param openvpn_verb: the desired level of verbosity in the - openvpn invocation - :type openvpn_verb: int - """ - VPNManager.__init__(self, qtsigs=qtsigs) - leap_assert_type(eipconfig, EIPConfig) - leap_assert_type(providerconfig, ProviderConfig) - leap_assert_type(qtsigs, QtCore.QObject) - - #leap_assert(not self.isRunning(), "Starting process more than once!") - - self._eipconfig = eipconfig - self._providerconfig = providerconfig - self._socket_host = socket_host - self._socket_port = socket_port - - self._launcher = get_platform_launcher() - - self._last_state = None - self._last_status = None - self._alive = False - - self._openvpn_verb = openvpn_verb - - # processProtocol methods - - def connectionMade(self): - """ - Called when the connection is made. - - .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa - """ - self._alive = True - self.aborted = False - self.try_to_connect_to_management(max_retries=10) - - def outReceived(self, data): - """ - Called when new data is available on stdout. - - :param data: the data read on stdout - - .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa - """ - # truncate the newline - # should send this to the logging window - vpnlog.info(data[:-1]) - - def processExited(self, reason): - """ - Called when the child process exits. - - .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa - """ - exit_code = reason.value.exitCode - if isinstance(exit_code, int): - logger.debug("processExited, status %d" % (exit_code,)) - self.qtsigs.process_finished.emit(exit_code) - self._alive = False - - def processEnded(self, reason): - """ - Called when the child process exits and all file descriptors associated - with it have been closed. - - .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa - """ - exit_code = reason.value.exitCode - if isinstance(exit_code, int): - logger.debug("processEnded, status %d" % (exit_code,)) - - # polling - - def pollStatus(self): - """ - Polls connection status. - """ - if self._alive: - self.get_status() - - def pollState(self): - """ - Polls connection state. - """ - if self._alive: - self.get_state() - - # launcher - - def getCommand(self): - """ - Gets the vpn command from the aproppriate launcher. - - Might throw: VPNLauncherException, OpenVPNNotFoundException. - """ - cmd = self._launcher.get_vpn_command( - eipconfig=self._eipconfig, - providerconfig=self._providerconfig, - socket_host=self._socket_host, - socket_port=self._socket_port, - openvpn_verb=self._openvpn_verb) - return map(str, cmd) - - # shutdown - - def killProcess(self): - """ - Sends the KILL signal to the running process. - """ - try: - self.transport.signalProcess('KILL') - except internet_error.ProcessExitedAlready: - logger.debug('Process Exited Already') diff --git a/src/leap/services/mail/__init__.py b/src/leap/services/mail/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/services/mail/imap.py b/src/leap/services/mail/imap.py deleted file mode 100644 index 4dceb2ad..00000000 --- a/src/leap/services/mail/imap.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -# imap.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 . -""" -Initialization of imap service -""" -import logging -import sys - -from leap.mail.imap.service import imap -from twisted.python import log - -logger = logging.getLogger(__name__) - - -def start_imap_service(*args, **kwargs): - """ - Initializes and run imap service. - - :returns: twisted.internet.task.LoopingCall instance - """ - logger.debug('Launching imap service') - - # Uncomment the next two lines to get a separate debugging log - # TODO handle this by a separate flag. - #log.startLogging(open('/tmp/leap-imap.log', 'w')) - #log.startLogging(sys.stdout) - - return imap.run_service(*args, **kwargs) diff --git a/src/leap/services/mail/smtpbootstrapper.py b/src/leap/services/mail/smtpbootstrapper.py deleted file mode 100644 index 48040035..00000000 --- a/src/leap/services/mail/smtpbootstrapper.py +++ /dev/null @@ -1,139 +0,0 @@ -# -*- coding: utf-8 -*- -# smtpbootstrapper.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 . - -""" -SMTP bootstrapping -""" - -import logging -import os - -from PySide import QtCore - -from leap.common.check import leap_assert, leap_assert_type -from leap.common.files import get_mtime -from leap.config.providerconfig import ProviderConfig -from leap.crypto.srpauth import SRPAuth -from leap.util.request_helpers import get_content -from leap.services.abstractbootstrapper import AbstractBootstrapper - -logger = logging.getLogger(__name__) - - -class SMTPBootstrapper(AbstractBootstrapper): - """ - SMTP init procedure - """ - - # All dicts returned are of the form - # {"passed": bool, "error": str} - download_config = QtCore.Signal(dict) - - def __init__(self): - AbstractBootstrapper.__init__(self) - - self._provider_config = None - self._smtp_config = None - self._download_if_needed = False - - def _download_config(self, *args): - """ - Downloads the SMTP config for the given provider - """ - - leap_assert(self._provider_config, - "We need a provider configuration!") - - logger.debug("Downloading SMTP config for %s" % - (self._provider_config.get_domain(),)) - - headers = {} - mtime = get_mtime(os.path.join(self._smtp_config - .get_path_prefix(), - "leap", - "providers", - self._provider_config.get_domain(), - "smtp-service.json")) - - if self._download_if_needed and mtime: - headers['if-modified-since'] = mtime - - api_version = self._provider_config.get_api_version() - - # there is some confusion with this uri, - config_uri = "%s/%s/config/smtp-service.json" % ( - self._provider_config.get_api_uri(), api_version) - - logger.debug('Downloading SMTP config from: %s' % config_uri) - - srp_auth = SRPAuth(self._provider_config) - session_id = srp_auth.get_session_id() - cookies = None - if session_id: - cookies = {"_session_id": session_id} - - res = self._session.get(config_uri, - verify=self._provider_config - .get_ca_cert_path(), - headers=headers, - cookies=cookies) - res.raise_for_status() - - self._smtp_config.set_api_version(api_version) - - # Not modified - if res.status_code == 304: - logger.debug("SMTP definition has not been modified") - self._smtp_config.load(os.path.join( - "leap", "providers", - self._provider_config.get_domain(), - "smtp-service.json")) - else: - smtp_definition, mtime = get_content(res) - - self._smtp_config.load(data=smtp_definition, mtime=mtime) - self._smtp_config.save(["leap", - "providers", - self._provider_config.get_domain(), - "smtp-service.json"]) - - def run_smtp_setup_checks(self, - provider_config, - smtp_config, - download_if_needed=False): - """ - Starts the checks needed for a new smtp setup - - :param provider_config: Provider configuration - :type provider_config: ProviderConfig - :param smtp_config: SMTP configuration to populate - :type smtp_config: SMTPConfig - :param download_if_needed: True if it should check for mtime - for the file - :type download_if_needed: bool - """ - leap_assert_type(provider_config, ProviderConfig) - - self._provider_config = provider_config - self._smtp_config = smtp_config - self._download_if_needed = download_if_needed - - cb_chain = [ - (self._download_config, self.download_config), - ] - - self.addCallbackChain(cb_chain) diff --git a/src/leap/services/mail/smtpconfig.py b/src/leap/services/mail/smtpconfig.py deleted file mode 100644 index ea0f9c37..00000000 --- a/src/leap/services/mail/smtpconfig.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -# smtpconfig.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 . - -""" -SMTP configuration -""" -import logging - -from leap.common.config.baseconfig import BaseConfig -from leap.services.mail.smtpspec import get_schema - -logger = logging.getLogger(__name__) - - -class SMTPConfig(BaseConfig): - """ - SMTP configuration abstraction class - """ - - def __init__(self): - BaseConfig.__init__(self) - - def _get_schema(self): - """ - Returns the schema corresponding to the version given. - - :rtype: dict or None if the version is not supported. - """ - return get_schema(self._api_version) - - def get_hosts(self): - return self._safe_get_value("hosts") - - def get_locations(self): - return self._safe_get_value("locations") diff --git a/src/leap/services/mail/smtpspec.py b/src/leap/services/mail/smtpspec.py deleted file mode 100644 index ff9d1bf8..00000000 --- a/src/leap/services/mail/smtpspec.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- coding: utf-8 -*- -# smtpspec.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 . - -# Schemas dict -# To add a schema for a version you should follow the form: -# { '1': schema_v1, '2': schema_v2, ... etc } -# so for instance, to add the '2' version, you should do: -# smtp_config_spec['2'] = schema_v2 -smtp_config_spec = {} - -smtp_config_spec['1'] = { - 'description': 'sample smtp service config', - 'type': 'object', - 'properties': { - 'serial': { - 'type': int, - 'default': 1, - 'required': ["True"] - }, - 'version': { - 'type': int, - 'default': 1, - 'required': ["True"] - }, - 'hosts': { - 'type': dict, - 'default': { - "walrus": { - "hostname": "someprovider", - "ip_address": "1.1.1.1", - "port": 1111 - }, - }, - }, - 'locations': { - 'type': dict, - 'default': { - "locations": { - - } - } - } - } -} - - -def get_schema(version): - """ - Returns the schema corresponding to the version given. - - :param version: the version of the schema to get. - :type version: str - :rtype: dict or None if the version is not supported. - """ - schema = smtp_config_spec.get(version, None) - return schema diff --git a/src/leap/services/soledad/__init__.py b/src/leap/services/soledad/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/services/soledad/soledadbootstrapper.py b/src/leap/services/soledad/soledadbootstrapper.py deleted file mode 100644 index c67bc004..00000000 --- a/src/leap/services/soledad/soledadbootstrapper.py +++ /dev/null @@ -1,265 +0,0 @@ -# -*- coding: utf-8 -*- -# soledadbootstrapper.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 . - -""" -Soledad bootstrapping -""" - -import logging -import os - -from PySide import QtCore -from u1db import errors as u1db_errors - -from leap.common.check import leap_assert, leap_assert_type -from leap.common.files import get_mtime -from leap.keymanager import KeyManager, openpgp -from leap.keymanager.errors import KeyNotFound -from leap.config.providerconfig import ProviderConfig -from leap.crypto.srpauth import SRPAuth -from leap.services.soledad.soledadconfig import SoledadConfig -from leap.util.request_helpers import get_content -from leap.soledad import Soledad -from leap.services.abstractbootstrapper import AbstractBootstrapper - -logger = logging.getLogger(__name__) - - -class SoledadBootstrapper(AbstractBootstrapper): - """ - Soledad init procedure - """ - - SOLEDAD_KEY = "soledad" - KEYMANAGER_KEY = "keymanager" - - PUBKEY_KEY = "user[public_key]" - - # All dicts returned are of the form - # {"passed": bool, "error": str} - download_config = QtCore.Signal(dict) - gen_key = QtCore.Signal(dict) - - def __init__(self): - AbstractBootstrapper.__init__(self) - - self._provider_config = None - self._soledad_config = None - self._keymanager = None - self._download_if_needed = False - self._user = "" - self._password = "" - self._soledad = None - - @property - def keymanager(self): - return self._keymanager - - @property - def soledad(self): - return self._soledad - - def _load_and_sync_soledad(self, srp_auth): - """ - Once everthing is in the right place, we instantiate and sync - Soledad - - :param srp_auth: SRPAuth object used - :type srp_auth: SRPAuth - """ - uuid = srp_auth.get_uid() - - prefix = os.path.join(self._soledad_config.get_path_prefix(), - "leap", "soledad") - secrets_path = "%s/%s.secret" % (prefix, uuid) - local_db_path = "%s/%s.db" % (prefix, uuid) - - # TODO: Select server based on timezone (issue #3308) - server_dict = self._soledad_config.get_hosts() - - if server_dict.keys(): - selected_server = server_dict[server_dict.keys()[0]] - server_url = "https://%s:%s/user-%s" % ( - selected_server["hostname"], - selected_server["port"], - uuid) - - logger.debug("Using soledad server url: %s" % (server_url,)) - - cert_file = self._provider_config.get_ca_cert_path() - - # TODO: If selected server fails, retry with another host - # (issue #3309) - try: - self._soledad = Soledad( - uuid, - self._password.encode("utf-8"), - secrets_path=secrets_path, - local_db_path=local_db_path, - server_url=server_url, - cert_file=cert_file, - auth_token=srp_auth.get_token()) - self._soledad.sync() - except u1db_errors.Unauthorized: - logger.error("Error while initializing soledad.") - else: - raise Exception("No soledad server found") - - def _download_config(self): - """ - Downloads the Soledad config for the given provider - """ - - leap_assert(self._provider_config, - "We need a provider configuration!") - - logger.debug("Downloading Soledad config for %s" % - (self._provider_config.get_domain(),)) - - self._soledad_config = SoledadConfig() - - headers = {} - mtime = get_mtime( - os.path.join( - self._soledad_config.get_path_prefix(), - "leap", "providers", - self._provider_config.get_domain(), - "soledad-service.json")) - - if self._download_if_needed and mtime: - headers['if-modified-since'] = mtime - - api_version = self._provider_config.get_api_version() - - # there is some confusion with this uri, - config_uri = "%s/%s/config/soledad-service.json" % ( - self._provider_config.get_api_uri(), - api_version) - logger.debug('Downloading soledad config from: %s' % config_uri) - - srp_auth = SRPAuth(self._provider_config) - session_id = srp_auth.get_session_id() - cookies = None - if session_id: - cookies = {"_session_id": session_id} - - res = self._session.get(config_uri, - verify=self._provider_config - .get_ca_cert_path(), - headers=headers, - cookies=cookies) - res.raise_for_status() - - self._soledad_config.set_api_version(api_version) - - # Not modified - if res.status_code == 304: - logger.debug("Soledad definition has not been modified") - self._soledad_config.load( - os.path.join( - "leap", "providers", - self._provider_config.get_domain(), - "soledad-service.json")) - else: - soledad_definition, mtime = get_content(res) - - self._soledad_config.load(data=soledad_definition, mtime=mtime) - self._soledad_config.save(["leap", - "providers", - self._provider_config.get_domain(), - "soledad-service.json"]) - - self._load_and_sync_soledad(srp_auth) - - def _gen_key(self, _): - """ - Generates the key pair if needed, uploads it to the webapp and - nickserver - """ - leap_assert(self._provider_config, - "We need a provider configuration!") - - address = "%s@%s" % (self._user, self._provider_config.get_domain()) - - logger.debug("Retrieving key for %s" % (address,)) - - srp_auth = SRPAuth(self._provider_config) - - # TODO: Fix for Windows - gpgbin = "/usr/bin/gpg" - - if self._standalone: - gpgbin = os.path.join(self._provider_config.get_path_prefix(), - "..", "apps", "mail", "gpg") - - self._keymanager = KeyManager( - address, - "https://nicknym.%s:6425" % (self._provider_config.get_domain(),), - self._soledad, - #token=srp_auth.get_token(), # TODO: enable token usage - session_id=srp_auth.get_session_id(), - ca_cert_path=self._provider_config.get_ca_cert_path(), - api_uri=self._provider_config.get_api_uri(), - api_version=self._provider_config.get_api_version(), - uid=srp_auth.get_uid(), - gpgbinary=gpgbin) - try: - self._keymanager.get_key(address, openpgp.OpenPGPKey, - private=True, fetch_remote=False) - except KeyNotFound: - logger.debug("Key not found. Generating key for %s" % (address,)) - self._keymanager.gen_key(openpgp.OpenPGPKey) - self._keymanager.send_key(openpgp.OpenPGPKey) - logger.debug("Key generated successfully.") - - def run_soledad_setup_checks(self, - provider_config, - user, - password, - download_if_needed=False, - standalone=False): - """ - Starts the checks needed for a new soledad setup - - :param provider_config: Provider configuration - :type provider_config: ProviderConfig - :param user: User's login - :type user: str - :param password: User's password - :type password: str - :param download_if_needed: If True, it will only download - files if the have changed since the - time it was previously downloaded. - :type download_if_needed: bool - :param standalone: If True, it'll look for paths inside the - bundle (like for gpg) - :type standalone: bool - """ - leap_assert_type(provider_config, ProviderConfig) - - self._provider_config = provider_config - self._download_if_needed = download_if_needed - self._user = user - self._password = password - self._standalone = standalone - - cb_chain = [ - (self._download_config, self.download_config), - (self._gen_key, self.gen_key) - ] - - self.addCallbackChain(cb_chain) diff --git a/src/leap/services/soledad/soledadconfig.py b/src/leap/services/soledad/soledadconfig.py deleted file mode 100644 index a2367692..00000000 --- a/src/leap/services/soledad/soledadconfig.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -# soledadconfig.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 . - -""" -Soledad configuration -""" -import logging - -from leap.common.config.baseconfig import BaseConfig -from leap.services.soledad.soledadspec import get_schema - -logger = logging.getLogger(__name__) - - -class SoledadConfig(BaseConfig): - """ - Soledad configuration abstraction class - """ - - def __init__(self): - BaseConfig.__init__(self) - - def _get_schema(self): - """ - Returns the schema corresponding to the version given. - - :rtype: dict or None if the version is not supported. - """ - return get_schema(self._api_version) - - def get_hosts(self): - return self._safe_get_value("hosts") - - def get_locations(self): - return self._safe_get_value("locations") diff --git a/src/leap/services/soledad/soledadspec.py b/src/leap/services/soledad/soledadspec.py deleted file mode 100644 index 111175dd..00000000 --- a/src/leap/services/soledad/soledadspec.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -# soledadspec.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 . - -# Schemas dict -# To add a schema for a version you should follow the form: -# { '1': schema_v1, '2': schema_v2, ... etc } -# so for instance, to add the '2' version, you should do: -# soledad_config_spec['2'] = schema_v2 -soledad_config_spec = {} - -soledad_config_spec['1'] = { - 'description': 'sample soledad service config', - 'type': 'object', - 'properties': { - 'serial': { - 'type': int, - 'default': 1, - 'required': ["True"] - }, - 'version': { - 'type': int, - 'default': 1, - 'required': ["True"] - }, - 'hosts': { - 'type': dict, - 'default': { - "python": { - "hostname": "someprovider", - "ip_address": "1.1.1.1", - "location": "loc", - "port": 1111 - }, - }, - }, - 'locations': { - 'type': dict, - 'default': { - "locations": { - "ankara": { - "country_code": "TR", - "hemisphere": "N", - "name": "loc", - "timezone": "+0" - } - } - } - } - } -} - - -def get_schema(version): - """ - Returns the schema corresponding to the version given. - - :param version: the version of the schema to get. - :type version: str - :rtype: dict or None if the version is not supported. - """ - schema = soledad_config_spec.get(version, None) - return schema diff --git a/src/leap/services/tests/__init__.py b/src/leap/services/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/services/tests/test_abstractbootstrapper.py b/src/leap/services/tests/test_abstractbootstrapper.py deleted file mode 100644 index a9ee220f..00000000 --- a/src/leap/services/tests/test_abstractbootstrapper.py +++ /dev/null @@ -1,196 +0,0 @@ -## -*- coding: utf-8 -*- -# test_abstrctbootstrapper.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 . - - -""" -Tests for the Abstract Boostrapper functionality -""" - -import mock - -from PySide import QtCore - -from nose.twistedtools import deferred - -from leap.services.abstractbootstrapper import AbstractBootstrapper -from leap.util.pyside_tests_helper import UsesQApplication, BasicPySlotCase - - -class TesterBootstrapper(AbstractBootstrapper): - test_signal1 = QtCore.Signal(dict) - test_signal2 = QtCore.Signal(dict) - test_signal3 = QtCore.Signal(dict) - - ERROR_MSG = "This is a test error msg" - - def _check_that_passes(self, *args): - pass - - def _second_check_that_passes(self, *args): - pass - - def _check_that_fails(self, *args): - raise Exception(self.ERROR_MSG) - - def run_checks_pass(self): - cb_chain = [ - (self._check_that_passes, self.test_signal1), - (self._second_check_that_passes, self.test_signal2), - ] - return self.addCallbackChain(cb_chain) - - def run_second_checks_pass(self): - cb_chain = [ - (self._check_that_passes, None), - ] - return self.addCallbackChain(cb_chain) - - def run_checks_fail(self): - cb_chain = [ - (self._check_that_passes, self.test_signal1), - (self._check_that_fails, self.test_signal2) - ] - return self.addCallbackChain(cb_chain) - - def run_second_checks_fail(self): - cb_chain = [ - (self._check_that_passes, self.test_signal1), - (self._check_that_fails, self.test_signal2), - (self._second_check_that_passes, self.test_signal1) - ] - return self.addCallbackChain(cb_chain) - - def run_third_checks_fail(self): - cb_chain = [ - (self._check_that_passes, self.test_signal1), - (self._check_that_fails, None) - ] - return self.addCallbackChain(cb_chain) - - -class AbstractBootstrapperTest(UsesQApplication, BasicPySlotCase): - def setUp(self): - UsesQApplication.setUp(self) - BasicPySlotCase.setUp(self) - - self.tbt = TesterBootstrapper() - self.called1 = self.called2 = 0 - - @deferred() - def test_all_checks_executed_once(self): - self.tbt._check_that_passes = mock.MagicMock() - self.tbt._second_check_that_passes = mock.MagicMock() - - d = self.tbt.run_checks_pass() - - def check(*args): - self.tbt._check_that_passes.assert_called_once_with() - self.tbt._second_check_that_passes.\ - assert_called_once_with(None) - - d.addCallback(check) - return d - - ####################################################################### - # Dummy callbacks that test the arguments expected from a certain - # signal and only allow being called once - - def cb1(self, *args): - if tuple(self.args1) == args: - self.called1 += 1 - else: - raise ValueError('Invalid arguments for callback') - - def cb2(self, *args): - if tuple(self.args2) == args: - self.called2 += 1 - else: - raise ValueError('Invalid arguments for callback') - - # - ####################################################################### - - def _check_cb12_once(self, *args): - self.assertEquals(self.called1, 1) - self.assertEquals(self.called2, 1) - - @deferred() - def test_emits_correct(self): - self.tbt.test_signal1.connect(self.cb1) - self.tbt.test_signal2.connect(self.cb2) - d = self.tbt.run_checks_pass() - - self.args1 = [{ - AbstractBootstrapper.PASSED_KEY: True, - AbstractBootstrapper.ERROR_KEY: "" - }] - - self.args2 = self.args1 - - d.addCallback(self._check_cb12_once) - return d - - @deferred() - def test_emits_failed(self): - self.tbt.test_signal1.connect(self.cb1) - self.tbt.test_signal2.connect(self.cb2) - d = self.tbt.run_checks_fail() - - self.args1 = [{ - AbstractBootstrapper.PASSED_KEY: True, - AbstractBootstrapper.ERROR_KEY: "" - }] - - self.args2 = [{ - AbstractBootstrapper.PASSED_KEY: False, - AbstractBootstrapper.ERROR_KEY: - TesterBootstrapper.ERROR_MSG - }] - - d.addCallback(self._check_cb12_once) - return d - - @deferred() - def test_emits_failed_and_stops(self): - self.tbt.test_signal1.connect(self.cb1) - self.tbt.test_signal2.connect(self.cb2) - self.tbt.test_signal3.connect(self.cb1) - d = self.tbt.run_second_checks_fail() - - self.args1 = [{ - AbstractBootstrapper.PASSED_KEY: True, - AbstractBootstrapper.ERROR_KEY: "" - }] - - self.args2 = [{ - AbstractBootstrapper.PASSED_KEY: False, - AbstractBootstrapper.ERROR_KEY: - TesterBootstrapper.ERROR_MSG - }] - - d.addCallback(self._check_cb12_once) - return d - - @deferred() - def test_failed_without_signal(self): - d = self.tbt.run_third_checks_fail() - return d - - @deferred() - def test_sucess_without_signal(self): - d = self.tbt.run_second_checks_pass() - return d diff --git a/src/leap/services/tx.py b/src/leap/services/tx.py deleted file mode 100644 index 7da1cb01..00000000 --- a/src/leap/services/tx.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -# twisted.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 . -""" -Twisted services launched by the client -""" -import logging - -from twisted.application.service import Application -from twisted.internet.task import LoopingCall - -logger = logging.getLogger(__name__) - - -def task(): - """ - stub periodic task, mainly for tests. - DELETE-ME when there's real meat here :) - """ - from datetime import datetime - logger.debug("hi there %s", datetime.now()) - - -def leap_services(): - """ - Check which twisted services are enabled and - register them. - """ - logger.debug('starting leap services') - application = Application("Bitmask Local Services") - #lc = LoopingCall(task) - #lc.start(5) - return application diff --git a/src/leap/util/__init__.py b/src/leap/util/__init__.py deleted file mode 100644 index f39b52e9..00000000 --- a/src/leap/util/__init__.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- coding: utf-8 -*- -# __init__.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 . -""" -Initializes version and app info, plus some small and handy functions. -""" -import datetime -import os - -from pkg_resources import parse_version - - -def _is_release_version(version): - """ - Helper to determine whether a version is a final release or not. - The release needs to be of the form: w.x.y.z containing only numbers - and dots. - - :param version: the version string - :type version: str - :returns: if the version is a release version or not. - :rtype: bool - """ - parsed_version = parse_version(version) - not_number = 0 - for x in parsed_version: - try: - int(x) - except: - not_number += 1 - - return not_number == 1 - - -__version__ = "unknown" -IS_RELEASE_VERSION = False - -try: - from leap._version import get_versions - __version__ = get_versions()['version'] - IS_RELEASE_VERSION = _is_release_version(__version__) - del get_versions -except ImportError: - #running on a tree that has not run - #the setup.py setver - pass - -__appname__ = "unknown" -try: - from leap._appname import __appname__ -except ImportError: - #running on a tree that has not run - #the setup.py setver - pass - -__full_version__ = __appname__ + '/' + str(__version__) - - -def first(things): - """ - Return the head of a collection. - """ - try: - return things[0] - except TypeError: - return None - - -def get_modification_ts(path): - """ - Gets modification time of a file. - - :param path: the path to get ts from - :type path: str - :returns: modification time - :rtype: datetime object - """ - ts = os.path.getmtime(path) - return datetime.datetime.fromtimestamp(ts) - - -def update_modification_ts(path): - """ - Sets modification time of a file to current time. - - :param path: the path to set ts to. - :type path: str - :returns: modification time - :rtype: datetime object - """ - os.utime(path, None) - return get_modification_ts(path) diff --git a/src/leap/util/constants.py b/src/leap/util/constants.py deleted file mode 100644 index 63f6b1f7..00000000 --- a/src/leap/util/constants.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# constants.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 . - -SIGNUP_TIMEOUT = 5 -REQUEST_TIMEOUT = 10 diff --git a/src/leap/util/keyring_helpers.py b/src/leap/util/keyring_helpers.py deleted file mode 100644 index 8f354f28..00000000 --- a/src/leap/util/keyring_helpers.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -# keyring_helpers.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 . - -""" -Keyring helpers. -""" - -import keyring - -OBSOLETE_KEYRINGS = [ - keyring.backends.file.EncryptedKeyring, - keyring.backends.file.PlaintextKeyring -] - - -def has_keyring(): - """ - Returns whether we have an useful keyring to use. - - :rtype: bool - """ - kr = keyring.get_keyring() - return kr is not None and kr.__class__ not in OBSOLETE_KEYRINGS diff --git a/src/leap/util/leap_argparse.py b/src/leap/util/leap_argparse.py deleted file mode 100644 index f60c4e10..00000000 --- a/src/leap/util/leap_argparse.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- coding: utf-8 -*- -# leap_argparse.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 . - -import argparse - -from leap.util import IS_RELEASE_VERSION - - -def build_parser(): - """ - All the options for the leap arg parser - Some of these could be switched on only if debug flag is present! - """ - epilog = "Copyright 2012 The LEAP Encryption Access Project" - parser = argparse.ArgumentParser(description=""" -Launches Bitmask""", epilog=epilog) - parser.add_argument('-d', '--debug', action="store_true", - help=("Launches Bitmask in debug mode, writing debug" - "info to stdout")) - # TODO: when we are ready to disable the --danger flag remove 'True or ' - if True or not IS_RELEASE_VERSION: - help_text = "Bypasses the certificate check for bootstrap" - parser.add_argument('--danger', action="store_true", help=help_text) - - parser.add_argument('-l', '--logfile', metavar="LOG FILE", nargs='?', - action="store", dest="log_file", - #type=argparse.FileType('w'), - help='optional log file') - parser.add_argument('--openvpn-verbosity', nargs='?', - type=int, - action="store", dest="openvpn_verb", - help='verbosity level for openvpn logs [1-6]') - parser.add_argument('-s', '--standalone', action="store_true", - help='Makes Bitmask use standalone' - 'directories for configuration and binary' - 'searching') - - # 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') - return parser - - -def init_leapc_args(): - parser = build_parser() - opts, unknown = parser.parse_known_args() - return parser, opts diff --git a/src/leap/util/leap_log_handler.py b/src/leap/util/leap_log_handler.py deleted file mode 100644 index 9adb21a5..00000000 --- a/src/leap/util/leap_log_handler.py +++ /dev/null @@ -1,134 +0,0 @@ -# -*- coding: utf-8 -*- -# leap_log_handler.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 . -""" -Custom handler for the logger window. -""" -import logging - -from PySide import QtCore - - -class LogHandler(logging.Handler): - """ - This is the custom handler that implements our desired formatting - and also keeps a history of all the logged events. - """ - - MESSAGE_KEY = 'message' - RECORD_KEY = 'record' - - def __init__(self, qtsignal): - """ - LogHander initialization. - Calls parent method and keeps a reference to the qtsignal - that will be used to fire the gui update. - """ - # TODO This is going to eat lots of memory after some time. - # Should be pruned at some moment. - self._log_history = [] - - logging.Handler.__init__(self) - self._qtsignal = qtsignal - - def _get_format(self, logging_level): - """ - Sets the log format depending on the parameter. - It uses html and css to set the colors for the logs. - - :param logging_level: the debug level to define the color. - :type logging_level: str. - """ - log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' - formatter = logging.Formatter(log_format) - - return formatter - - def emit(self, logRecord): - """ - This method is fired every time that a record is logged by the - logging module. - This method reimplements logging.Handler.emit that is fired - in every logged message. - - :param logRecord: the record emitted by the logging module. - :type logRecord: logging.LogRecord. - """ - self.setFormatter(self._get_format(logRecord.levelname)) - log = self.format(logRecord) - log_item = {self.RECORD_KEY: logRecord, self.MESSAGE_KEY: log} - self._log_history.append(log_item) - self._qtsignal(log_item) - - -class HandlerAdapter(object): - """ - New style class that accesses all attributes from the LogHandler. - - Used as a workaround for a problem with multiple inheritance with Pyside - that surfaced under OSX with pyside 1.1.0. - """ - MESSAGE_KEY = 'message' - RECORD_KEY = 'record' - - def __init__(self, qtsignal): - self._handler = LogHandler(qtsignal=qtsignal) - - def setLevel(self, *args, **kwargs): - return self._handler.setLevel(*args, **kwargs) - - def handle(self, *args, **kwargs): - return self._handler.handle(*args, **kwargs) - - @property - def level(self): - return self._handler.level - - -class LeapLogHandler(QtCore.QObject, HandlerAdapter): - """ - Custom logging handler. It emits Qt signals so it can be plugged to a gui. - - Its inner handler also stores an history of logs that can be fetched after - having been connected to a gui. - """ - # All dicts returned are of the form - # {'record': LogRecord, 'message': str} - new_log = QtCore.Signal(dict) - - def __init__(self): - """ - LeapLogHandler initialization. - Initializes parent classes. - """ - QtCore.QObject.__init__(self) - HandlerAdapter.__init__(self, qtsignal=self.qtsignal) - - def qtsignal(self, log_item): - # WARNING: the new-style connection does NOT work because PySide - # translates the emit method to self.emit, and that collides with - # the emit method for logging.Handler - # self.new_log.emit(log_item) - QtCore.QObject.emit( - self, - QtCore.SIGNAL('new_log(PyObject)'), log_item) - - @property - def log_history(self): - """ - Returns the history of the logged messages. - """ - return self._handler._log_history diff --git a/src/leap/util/privilege_policies.py b/src/leap/util/privilege_policies.py deleted file mode 100644 index 72442553..00000000 --- a/src/leap/util/privilege_policies.py +++ /dev/null @@ -1,169 +0,0 @@ -# -*- coding: utf-8 -*- -# privilege_policies.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 . -""" -Helpers to determine if the needed policies for privilege escalation -are operative under this client run. -""" -import logging -import os -import platform - -from abc import ABCMeta, abstractmethod - -logger = logging.getLogger(__name__) - - -POLICY_TEMPLATE = """ - - - - LEAP Project - https://leap.se/ - - - Runs the openvpn binary - Ejecuta el binario openvpn - OpenVPN needs that you authenticate to start - - OpenVPN necesita autorizacion para comenzar - - package-x-generic - - yes - yes - yes - - {path} - true - - -""" - - -def is_missing_policy_permissions(): - """ - Returns True if we do not have implemented a policy checker for this - platform, or if the policy checker exists but it cannot find the - appropriate policy mechanisms in place. - - :rtype: bool - """ - _system = platform.system() - platform_checker = _system + "PolicyChecker" - policy_checker = globals().get(platform_checker, None) - if not policy_checker: - # it is true that we miss permission to escalate - # privileges without asking for password each time. - logger.debug("we could not find a policy checker implementation " - "for %s" % (_system,)) - return True - return policy_checker().is_missing_policy_permissions() - - -def get_policy_contents(openvpn_path): - """ - Returns the contents that the policy file should have. - - :param openvpn_path: the openvpn path to use in the polkit file - :type openvpn_path: str - :rtype: str - """ - return POLICY_TEMPLATE.format(path=openvpn_path) - - -def is_policy_outdated(path): - """ - Returns if the existing polkit file is outdated, comparing if the path - is correct. - - :param path: the path that should have the polkit file. - :type path: str. - :rtype: bool - """ - _system = platform.system() - platform_checker = _system + "PolicyChecker" - policy_checker = globals().get(platform_checker, None) - if policy_checker is None: - logger.debug("we could not find a policy checker implementation " - "for %s" % (_system,)) - return False - return policy_checker().is_outdated(path) - - -class PolicyChecker: - """ - Abstract PolicyChecker class - """ - - __metaclass__ = ABCMeta - - @abstractmethod - def is_missing_policy_permissions(self): - """ - Returns True if we could not find any policy mechanisms that - are defined to be in used for this particular platform. - - :rtype: bool - """ - return True - - -class LinuxPolicyChecker(PolicyChecker): - """ - PolicyChecker for Linux - """ - LINUX_POLKIT_FILE = ("/usr/share/polkit-1/actions/" - "net.openvpn.gui.leap.policy") - - @classmethod - def get_polkit_path(self): - """ - Returns the polkit file path. - - :rtype: str - """ - return self.LINUX_POLKIT_FILE - - def is_missing_policy_permissions(self): - """ - Returns True if we could not find the appropriate policykit file - in place - - :rtype: bool - """ - return not os.path.isfile(self.LINUX_POLKIT_FILE) - - def is_outdated(self, path): - """ - Returns if the existing polkit file is outdated, comparing if the path - is correct. - - :param path: the path that should have the polkit file. - :type path: str. - :rtype: bool - """ - polkit = None - try: - with open(self.LINUX_POLKIT_FILE) as f: - polkit = f.read() - except IOError, e: - logger.error("Error reading polkit file(%s): %r" % ( - self.LINUX_POLKIT_FILE, e)) - - return get_policy_contents(path) != polkit diff --git a/src/leap/util/pyside_tests_helper.py b/src/leap/util/pyside_tests_helper.py deleted file mode 100644 index 5c0eb8d6..00000000 --- a/src/leap/util/pyside_tests_helper.py +++ /dev/null @@ -1,136 +0,0 @@ - -'''Helper classes and functions''' - -import os -import unittest - -from random import randint - -from PySide.QtCore import QCoreApplication, QTimer - -try: - from PySide.QtGui import QApplication -except ImportError: - has_gui = False -else: - has_gui = True - - -def adjust_filename(filename, orig_mod_filename): - dirpath = os.path.dirname(os.path.abspath(orig_mod_filename)) - return os.path.join(dirpath, filename) - - -class NoQtGuiError(Exception): - def __init__(self): - Exception.__init__(self, 'No QtGui found') - - -class BasicPySlotCase(object): - '''Base class that tests python slots and signal emissions. - - Python slots are defined as any callable passed to QObject.connect(). - ''' - def setUp(self): - self.called = False - - def tearDown(self): - try: - del self.args - except: - pass - - def cb(self, *args): - '''Simple callback with arbitrary arguments. - - The test function must setup the 'args' attribute with a sequence - containing the arguments expected to be received by this slot. - Currently only a single connection is supported. - ''' - if tuple(self.args) == args: - self.called = True - else: - raise ValueError('Invalid arguments for callback') - - -_instance = None -_timed_instance = None - -if has_gui: - class UsesQApplication(unittest.TestCase): - '''Helper class to provide QApplication instances''' - - qapplication = True - - def setUp(self): - '''Creates the QApplication instance''' - - # Simple way of making instance a singleton - super(UsesQApplication, self).setUp() - global _instance - if _instance is None: - _instance = QApplication([]) - - self.app = _instance - - def tearDown(self): - '''Deletes the reference owned by self''' - del self.app - super(UsesQApplication, self).tearDown() - - class TimedQApplication(unittest.TestCase): - '''Helper class with timed QApplication exec loop''' - - def setUp(self, timeout=100): - '''Setups this Application. - - timeout - timeout in milisseconds''' - global _timed_instance - if _timed_instance is None: - _timed_instance = QApplication([]) - - self.app = _timed_instance - QTimer.singleShot(timeout, self.app.quit) - - def tearDown(self): - '''Delete resources''' - del self.app -else: - class UsesQApplication(unittest.TestCase): - def setUp(self): - raise NoQtGuiError() - - class TimedQapplication(unittest.TestCase): - def setUp(self): - raise NoQtGuiError() - -_core_instance = None - - -class UsesQCoreApplication(unittest.TestCase): - '''Helper class for test cases that require an QCoreApplication - Just connect or call self.exit_app_cb. When called, will ask - self.app to exit. - ''' - - def setUp(self): - '''Set up resources''' - - global _core_instance - if _core_instance is None: - _core_instance = QCoreApplication([]) - - self.app = _core_instance - - def tearDown(self): - '''Release resources''' - del self.app - - def exit_app_cb(self): - '''Quits the application''' - self.app.exit(0) - - -def random_string(size=5): - '''Generate random string with the given size''' - return ''.join(map(chr, [randint(33, 126) for x in range(size)])) diff --git a/src/leap/util/request_helpers.py b/src/leap/util/request_helpers.py deleted file mode 100644 index 74aaa06b..00000000 --- a/src/leap/util/request_helpers.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -# request_helpers.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 . - -""" -Request helpers for backward compatible "parsing" of requests -""" - -import time - -import json - -from dateutil import parser as dateparser - - -def get_content(request): - """ - Returns the content by trying to get it from the json - property/function or from content, in that order. - Also returns the mtime for that content if available - - :param request: request as it is given by requests - :type request: Response - - :rtype: tuple (contents, mtime) - """ - - contents = "" - mtime = None - - if request and request.content and request.json: - if callable(request.json): - contents = json.dumps(request.json()) - else: - contents = json.dumps(request.json) - else: - contents = request.content - - mtime = None - last_modified = request.headers.get('last-modified', None) - if last_modified: - dt = dateparser.parse(unicode(last_modified)) - mtime = int(time.mktime(dt.timetuple()) + dt.microsecond / 1000000.0) - - return contents, mtime diff --git a/src/leap/util/requirement_checker.py b/src/leap/util/requirement_checker.py deleted file mode 100644 index 1d9b9923..00000000 --- a/src/leap/util/requirement_checker.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- -# requirement_checker.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 . - -""" -Utility to check the needed requirements. -""" - -import os -import logging - -from pkg_resources import (DistributionNotFound, - get_distribution, - Requirement, - resource_stream, - VersionConflict) - -logger = logging.getLogger(__name__) - - -def get_requirements(): - """ - This function returns a list with requirements. - It checks either if its running from the source or if its installed. - - :returns: a list with packages names, required for the app. - :return type: list of str. - """ - develop = True - requirements = [] - - try: - # if we are running from the source - from pkg import util - requirements = util.parse_requirements() - except ImportError: - develop = False - - # if we are running from the package - if not develop: - requires_file_name = os.path.join('leap', 'util', 'reqs.txt') - dist_name = Requirement.parse('leap-client') - - try: - with resource_stream(dist_name, requires_file_name) as stream: - requirements = [line.strip() for line in stream] - except Exception, e: - logger.error("Requirements file not found. %r" % (e, )) - - return requirements - - -def check_requirements(): - """ - This function check the dependencies declared in the - requirement(s) file(s) and logs the results. - """ - logger.debug("Checking requirements...") - requirements = get_requirements() - - for package in requirements: - try: - get_distribution(package) - except VersionConflict: - required_package = Requirement.parse(package) - required_version = required_package.specs[0] - required_name = required_package.key - - installed_package = get_distribution(required_name) - installed_version = installed_package.version - installed_location = installed_package.location - - msg = "Error: version not satisfied. " - msg += "Expected %s, installed %s (path: %s)." % ( - required_version, installed_version, installed_location) - - result = "%s ... %s" % (package, msg) - logger.error(result) - except DistributionNotFound: - msg = "Error: package not found!" - result = "%s ... %s" % (package, msg) - logger.error(result) - else: - msg = "OK" - result = "%s ... %s" % (package, msg) - logger.debug(result) - - logger.debug('Done') diff --git a/src/leap/util/streamtologger.py b/src/leap/util/streamtologger.py deleted file mode 100644 index 25a06718..00000000 --- a/src/leap/util/streamtologger.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- -# streamtologger.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 . -""" -Stream object that redirects writes to a logger instance. -""" -import logging - - -class StreamToLogger(object): - """ - Fake file-like stream object that redirects writes to a logger instance. - - Credits to: - http://www.electricmonk.nl/log/2011/08/14/\ - redirect-stdout-and-stderr-to-a-logger-in-python/ - """ - def __init__(self, logger, log_level=logging.INFO): - """ - Constructor, defines the logger and level to use to log messages. - - :param logger: logger object to log messages. - :type logger: logging.Handler - :param log_level: the level to use to log messages through the logger. - :type log_level: int - look at logging-levels in 'logging' docs. - """ - self._logger = logger - self._log_level = log_level - - def write(self, data): - """ - Simulates the 'write' method in a file object. - It writes the data receibed in buf to the logger 'self._logger'. - - :param data: data to write to the 'file' - :type data: str - """ - for line in data.rstrip().splitlines(): - self._logger.log(self._log_level, line.rstrip()) - - def flush(self): - """ - Dummy method. Needed to replace the twisted.log output. - """ - pass diff --git a/src/leap/util/tests/__init__.py b/src/leap/util/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/util/tests/test_is_release_version.py b/src/leap/util/tests/test_is_release_version.py deleted file mode 100644 index 4199f603..00000000 --- a/src/leap/util/tests/test_is_release_version.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- -# test_is_release_version.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 . -""" -tests for _is_release_version function -""" - -import unittest -from leap.common.testing.basetest import BaseLeapTest -from leap.util import _is_release_version as is_release_version - - -class TestIsReleaseVersion(BaseLeapTest): - """Tests for release version check.""" - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_git_version(self): - version = '0.2.3-12-ge5b50a1' - self.assertFalse(is_release_version(version)) - - def test_release(self): - version = '0.2.4' - self.assertTrue(is_release_version(version)) - - def test_release_candidate(self): - version = '0.2.4-rc1' - self.assertFalse(is_release_version(version)) - - def test_complex_version(self): - version = '12.5.2.4-rc12.dev.alpha1' - self.assertFalse(is_release_version(version)) - - def test_super_high_version(self): - version = '12.5.2.4.45' - self.assertTrue(is_release_version(version)) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/src/leap/util/tests/test_leap_log_handler.py b/src/leap/util/tests/test_leap_log_handler.py deleted file mode 100644 index ea509ea8..00000000 --- a/src/leap/util/tests/test_leap_log_handler.py +++ /dev/null @@ -1,118 +0,0 @@ -# -*- coding: utf-8 -*- -# test_leap_log_handler.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 . -""" -tests for leap_log_handler -""" - -import unittest - -import logging - -from leap.util.leap_log_handler import LeapLogHandler -from leap.common.testing.basetest import BaseLeapTest -from leap.util.pyside_tests_helper import BasicPySlotCase - -from mock import Mock - - -class LeapLogHandlerTest(BaseLeapTest, BasicPySlotCase): - """ - LeapLogHandlerTest's tests. - """ - def _callback(self, *args): - """ - Simple callback to track if a signal was emitted. - """ - self.called = True - self.emitted_msg = args[0][LeapLogHandler.MESSAGE_KEY] - - def setUp(self): - BasicPySlotCase.setUp(self) - - # Create the logger - level = logging.DEBUG - self.logger = logging.getLogger(name='test') - self.logger.setLevel(level) - - # Create the handler - self.leap_handler = LeapLogHandler() - self.leap_handler.setLevel(level) - self.logger.addHandler(self.leap_handler) - - def tearDown(self): - BasicPySlotCase.tearDown(self) - try: - self.leap_handler.new_log.disconnect() - except Exception: - pass - - def test_history_starts_empty(self): - self.assertEqual(self.leap_handler.log_history, []) - - def test_one_log_captured(self): - self.logger.debug('test') - self.assertEqual(len(self.leap_handler.log_history), 1) - - def test_history_records_order(self): - self.logger.debug('test 01') - self.logger.debug('test 02') - self.logger.debug('test 03') - - logs = [] - for message in self.leap_handler.log_history: - logs.append(message[LeapLogHandler.RECORD_KEY].msg) - - self.assertIn('test 01', logs) - self.assertIn('test 02', logs) - self.assertIn('test 03', logs) - - def test_history_messages_order(self): - self.logger.debug('test 01') - self.logger.debug('test 02') - self.logger.debug('test 03') - - logs = [] - for message in self.leap_handler.log_history: - logs.append(message[LeapLogHandler.MESSAGE_KEY]) - - self.assertIn('test 01', logs[0]) - self.assertIn('test 02', logs[1]) - self.assertIn('test 03', logs[2]) - - def test_emits_signal(self): - log_format = '%(name)s - %(levelname)s - %(message)s' - formatter = logging.Formatter(log_format) - get_format = Mock(return_value=formatter) - self.leap_handler._handler._get_format = get_format - - self.leap_handler.new_log.connect(self._callback) - self.logger.debug('test') - - expected_log_msg = "test - DEBUG - test" - - # signal emitted - self.assertTrue(self.called) - - # emitted message - self.assertEqual(self.emitted_msg, expected_log_msg) - - # Mock called - self.assertTrue(get_format.called) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/util/tests/test_streamtologger.py b/src/leap/util/tests/test_streamtologger.py deleted file mode 100644 index 4c98e562..00000000 --- a/src/leap/util/tests/test_streamtologger.py +++ /dev/null @@ -1,122 +0,0 @@ -# -*- coding: utf-8 -*- -# test_streamtologger.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 . -""" -tests for streamtologger -""" - -try: - import unittest2 as unittest -except ImportError: - import unittest - -import logging -import sys - -from leap.util.streamtologger import StreamToLogger -from leap.common.testing.basetest import BaseLeapTest - - -class SimpleLogHandler(logging.Handler): - """ - The simplest log handler that allows to check if the log was - delivered to the handler correctly. - """ - def __init__(self): - logging.Handler.__init__(self) - self._last_log = "" - self._last_log_level = "" - - def emit(self, record): - self._last_log = record.getMessage() - self._last_log_level = record.levelno - - def get_last_log(self): - """ - Returns the last logged message by this handler. - - :return: the last logged message. - :rtype: str - """ - return self._last_log - - def get_last_log_level(self): - """ - Returns the level of the last logged message by this handler. - - :return: the last logged level. - :rtype: str - """ - return self._last_log_level - - -class StreamToLoggerTest(BaseLeapTest): - """ - StreamToLogger's tests. - - NOTE: we may need to find a way to test the use case that an exception - is raised. I couldn't catch the output of an exception because the - test failed if some exception is raised. - """ - def setUp(self): - # Create the logger - level = logging.DEBUG - self.logger = logging.getLogger(name='test') - self.logger.setLevel(level) - - # Simple log handler - self.handler = SimpleLogHandler() - self.logger.addHandler(self.handler) - - # Preserve original values - self._sys_stdout = sys.stdout - self._sys_stderr = sys.stderr - - # Create the handler - sys.stdout = StreamToLogger(self.logger, logging.DEBUG) - sys.stderr = StreamToLogger(self.logger, logging.ERROR) - - def tearDown(self): - # Restore original values - sys.stdout = self._sys_stdout - sys.stderr = self._sys_stderr - - def test_logger_starts_empty(self): - self.assertEqual(self.handler.get_last_log(), '') - - def test_standard_output(self): - message = 'Test string' - print message - - log = self.handler.get_last_log() - log_level = self.handler.get_last_log_level() - - self.assertEqual(log, message) - self.assertEqual(log_level, logging.DEBUG) - - def test_standard_error(self): - message = 'Test string' - sys.stderr.write(message) - - log_level = self.handler.get_last_log_level() - log = self.handler.get_last_log() - - self.assertEqual(log, message) - self.assertEqual(log_level, logging.ERROR) - - -if __name__ == "__main__": - unittest.main(verbosity=2) -- cgit v1.2.3