diff options
Diffstat (limited to 'src')
61 files changed, 4620 insertions, 626 deletions
diff --git a/src/leap/bitmask/__init__.py b/src/leap/bitmask/__init__.py index 9ec5aae7..c25ae999 100644 --- a/src/leap/bitmask/__init__.py +++ b/src/leap/bitmask/__init__.py @@ -19,11 +19,7 @@ Init file for leap.bitmask Initializes version and app info. """ -import re - -from pkg_resources import parse_version - -from leap.bitmask.util import first +from ._version import get_versions # HACK: This is a hack so that py2app copies _scrypt.so to the right # place, it can't be technically imported, but that doesn't matter @@ -32,7 +28,7 @@ if False: import _scrypt # noqa - skip 'not used' warning -def _is_release_version(version): +def _is_release_version(version_str): """ 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 @@ -43,40 +39,15 @@ def _is_release_version(version): :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 - -__short_version__ = "unknown" - -try: - from leap.bitmask._version import get_versions - __version__ = get_versions()['version'] - __version_hash__ = get_versions()['full'] - 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.bitmask._appname import __appname__ -except ImportError: - # running on a tree that has not run - # the setup.py setver - pass - -__short_version__ = first(re.findall('\d+\.\d+\.\d+', __version__)) -__full_version__ = __appname__ + '/' + str(__version__) + parts = __version__.split('.') + try: + patch = parts[2] + except IndexError: + return False + return patch.isdigit() + + +__version__ = get_versions()['version'] +__version_hash__ = get_versions()['full-revisionid'] +IS_RELEASE_VERSION = _is_release_version(__version__) +del get_versions diff --git a/src/leap/bitmask/_version.py b/src/leap/bitmask/_version.py index 412b0c9e..d64032a7 100644 --- a/src/leap/bitmask/_version.py +++ b/src/leap/bitmask/_version.py @@ -1,201 +1,483 @@ - -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 +# feature). Distribution tarballs (built 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$" +# versioneer-0.16 (https://github.com/warner/python-versioneer) +"""Git implementation of _version.py.""" +import errno +import os +import re 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] +def get_keywords(): + """Get the keywords needed to look up the version information.""" + # these strings will be replaced by git during git-archive. + # setup.py/versioneer.py will grep for the variable names, so they must + # each be defined on a line of their own. _version.py will just call + # get_keywords(). + git_refnames = "$Format:%d$" + git_full = "$Format:%H$" + keywords = {"refnames": git_refnames, "full": git_full} + return keywords + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + +def get_config(): + """Create, populate and return the VersioneerConfig() object.""" + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "pep440" + cfg.tag_prefix = "" + cfg.parentdir_prefix = "None" + cfg.versionfile_source = "src/leap/bitmask/_version.py" + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): + """Call the given command(s).""" + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % dispcmd) + print(e) + return None + else: if verbose: - print("unable to run %s" % args[0]) - print(e) + print("unable to find command, tried %s" % (commands,)) return None stdout = p.communicate()[0].strip() - if sys.version >= '3': + if sys.version_info[0] >= 3: stdout = stdout.decode() if p.returncode != 0: if verbose: - print("unable to run %s (error)" % args[0]) + print("unable to run %s (error)" % dispcmd) return None return stdout -def get_expanded_variables(versionfile_source): +def versions_from_parentdir(parentdir_prefix, root, verbose): + """Try to determine the version from the parent directory name. + + 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)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None} + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + """Extract version information from the given file.""" # 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 = {} + # keywords. 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. + keywords = {} try: - for line in open(versionfile_source, "r").readlines(): + f = open(versionfile_abs, "r") + for line in f.readlines(): if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) if mo: - variables["refnames"] = mo.group(1) + keywords["refnames"] = mo.group(1) if line.strip().startswith("git_full ="): mo = re.search(r'=\s*"(.*)"', line) if mo: - variables["full"] = mo.group(1) + keywords["full"] = mo.group(1) + f.close() except EnvironmentError: pass - return variables + return keywords -def versions_from_expanded_variables(variables, tag_prefix, verbose=False): - refnames = variables["refnames"].strip() +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + """Get version information from git keywords.""" + if not keywords: + raise NotThisMethod("no keywords at all, weird") + refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: - print("variables are unexpanded, not using") - return {} # unexpanded, so not in an unpacked git-archive tarball + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a 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". + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %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". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: - print("remaining refs: %s" % ",".join(sorted(refs))) - for ref in sorted(refs): + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): # 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 + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None + } + # no suitable tags, so version is "0+unknown", but full hex is still there 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. + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags"} - 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) + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords 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. + """ if not os.path.exists(os.path.join(root, ".git")): if verbose: print("no .git in %s" % root) - return {} + raise NotThisMethod("no .git directory") - GIT = "git" + GITS = ["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 + GITS = ["git.cmd", "git.exe"] + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%s*" % tag_prefix], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + return pieces + + +def plus_or_dot(pieces): + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + """TAG[.post.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post.devDISTANCE + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%d" % pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_old(pieces): + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Eexceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"]} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None} + + +def get_versions(): + """Get version information or return default if unable to do so.""" + # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have + # __file__, we can work backwards from there to the root. Some + # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which + # case we can only use expanded keywords. + + cfg = get_config() + verbose = cfg.verbose + + try: + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass + + try: + root = os.path.realpath(__file__) # 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("/"))): + # tree (where the .git directory might live) to this file. Invert + # this to find the root from __file__. + for i in cfg.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) + except NameError: + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree"} - # 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/bitmask/_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 + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version"} diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index a1b7481a..e5189e23 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -46,12 +46,16 @@ import os import platform import sys + if platform.system() == "Darwin": + # XXX please ignore pep8 complains, this needs to be executed + # early. # We need to tune maximum number of files, due to zmq usage # we hit the limit. import resource resource.setrlimit(resource.RLIMIT_NOFILE, (4096, 10240)) + from leap.bitmask import __version__ as VERSION from leap.bitmask.backend.backend_proxy import BackendProxy from leap.bitmask.backend_app import run_backend @@ -73,6 +77,12 @@ codecs.register(lambda name: codecs.lookup('utf-8') import psutil +def qt_hack_ubuntu(): + """Export two env vars to avoid gui corruption, see #8028""" + os.environ['QT_GRAPHICSSYSTEM'] = 'native' + os.environ['LIBOVERLAY_SCROLLBAR'] = '0' + + def kill_the_children(): """ Make sure no lingering subprocesses are left in case of a bad termination. @@ -150,6 +160,8 @@ def start_app(): """ Starts the main event loop and launches the main window. """ + qt_hack_ubuntu() + # Ignore the signals since we handle them in the subprocesses # signal.signal(signal.SIGINT, signal.SIG_IGN) @@ -163,6 +175,12 @@ def start_app(): } flags.STANDALONE = opts.standalone + + if platform.system() != 'Darwin': + # XXX this hangs the OSX bundles. + if getattr(sys, 'frozen', False): + flags.STANDALONE = True + flags.OFFLINE = opts.offline flags.MAIL_LOGFILE = opts.mail_log_file flags.APP_VERSION_CHECK = opts.app_version_check @@ -223,9 +241,10 @@ def start_app(): backend_pid = None if not backend_running: frontend_pid = os.getpid() - backend = lambda: run_backend(opts.danger, flags_dict, frontend_pid) - backend_process = multiprocessing.Process(target=backend, - name='Backend') + backend_process = multiprocessing.Process( + target=run_backend, + name='Backend', + args=(opts.danger, flags_dict, frontend_pid)) # we don't set the 'daemon mode' since we need to start child processes # in the backend # backend_process.daemon = True @@ -237,4 +256,5 @@ def start_app(): if __name__ == "__main__": + multiprocessing.freeze_support() start_app() diff --git a/src/leap/bitmask/backend/api.py b/src/leap/bitmask/backend/api.py index 48aa2090..2fd983ae 100644 --- a/src/leap/bitmask/backend/api.py +++ b/src/leap/bitmask/backend/api.py @@ -42,6 +42,8 @@ API = ( "keymanager_export_keys", "keymanager_get_key_details", "keymanager_list_keys", + "pixelated_start_service", + "pixelated_stop_service", "provider_bootstrap", "provider_cancel_setup", "provider_get_all_services", @@ -57,6 +59,7 @@ API = ( "soledad_change_password", "soledad_close", "soledad_load_offline", + "soledad_get_service_token", "tear_fw_down", "bitmask_root_vpn_down", "user_cancel_login", @@ -135,6 +138,7 @@ SIGNALS = ( "soledad_offline_finished", "soledad_password_change_error", "soledad_password_change_ok", + "soledad_got_service_token", "srp_auth_bad_user_or_password", "srp_auth_connection_error", "srp_auth_error", diff --git a/src/leap/bitmask/backend/components.py b/src/leap/bitmask/backend/components.py index 5f34d290..ba64fd65 100644 --- a/src/leap/bitmask/backend/components.py +++ b/src/leap/bitmask/backend/components.py @@ -20,13 +20,16 @@ Backend components # TODO [ ] Get rid of all this deferToThread mess, or at least contain # all of it into its own threadpool. +import json import os +import shutil import socket +import tempfile import time from functools import partial -from twisted.internet import threads, defer +from twisted.internet import threads, defer, reactor from twisted.python import log import zope.interface @@ -38,9 +41,10 @@ from leap.bitmask.crypto.srpauth import SRPAuth from leap.bitmask.crypto.srpregister import SRPRegister from leap.bitmask.logs.utils import get_logger from leap.bitmask.platform_init import IS_LINUX +from leap.bitmask import pix from leap.bitmask.provider.pinned import PinnedProviders from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper -from leap.bitmask.services import get_supported +from leap.bitmask.services import get_supported, EIP_SERVICE from leap.bitmask.services.eip import eipconfig from leap.bitmask.services.eip import get_openvpn_management from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper @@ -572,8 +576,10 @@ class EIP(object): self._signaler.eip_uninitialized_provider) return - eip_config = eipconfig.EIPConfig() provider_config = ProviderConfig.get_provider_config(domain) + if EIP_SERVICE not in provider_config.get_services(): + return + eip_config = eipconfig.EIPConfig() api_version = provider_config.get_api_version() eip_config.set_api_version(api_version) @@ -643,12 +649,14 @@ class EIP(object): :param domain: the domain for the provider to check :type domain: str """ - if not LinuxPolicyChecker.is_up(): + if IS_LINUX and not LinuxPolicyChecker.is_up(): logger.error("No polkit agent running.") return False - eip_config = eipconfig.EIPConfig() provider_config = ProviderConfig.get_provider_config(domain) + if EIP_SERVICE not in provider_config.get_services(): + return False + eip_config = eipconfig.EIPConfig() api_version = provider_config.get_api_version() eip_config.set_api_version(api_version) @@ -763,6 +771,7 @@ class Soledad(object): self._signaler = signaler self._soledad_bootstrapper = SoledadBootstrapper(signaler) self._soledad_defer = None + self._service_tokens = {} def bootstrap(self, username, domain, password): """ @@ -786,6 +795,9 @@ class Soledad(object): provider_config, username, password, download_if_needed=True) self._soledad_defer.addCallback(self._set_proxies_cb) + self._soledad_defer.addCallback(self._set_service_tokens_cb) + self._soledad_defer.addCallback(self._write_tokens_file, + username, domain) else: if self._signaler is not None: self._signaler.signal(self._signaler.soledad_bootstrap_failed) @@ -793,6 +805,38 @@ class Soledad(object): return self._soledad_defer + def _set_service_tokens_cb(self, result): + + def register_service_token(token, service): + self._service_tokens[service] = token + if self._signaler is not None: + self._signaler.signal( + self._signaler.soledad_got_service_token, + (service, token)) + + sol = self._soledad_bootstrapper.soledad + d = sol.get_or_create_service_token('mail_auth') + d.addCallback(register_service_token, 'mail_auth') + d.addCallback(lambda _: result) + return d + + def _write_tokens_file(self, result, username, domain): + tokens_folder = os.path.join(tempfile.gettempdir(), "bitmask_tokens") + if os.path.exists(tokens_folder): + try: + shutil.rmtree(tokens_folder) + except OSError as e: + logger.error("Can't remove tokens folder %s: %s" + % (tokens_folder, e)) + return + os.mkdir(tokens_folder, 0700) + + tokens_path = os.path.join(tokens_folder, + "%s@%s.json" % (username, domain)) + with open(tokens_path, 'w') as ftokens: + json.dump(self._service_tokens, ftokens) + return result + def _set_proxies_cb(self, _): """ Update the soledad and keymanager proxies to reference the ones created @@ -803,6 +847,12 @@ class Soledad(object): zope.proxy.setProxiedObject(self._keymanager_proxy, self._soledad_bootstrapper.keymanager) + def get_service_token(self, service): + """ + Get an authentication token for a given service. + """ + return self._service_tokens.get(service, '') + def load_offline(self, username, password, uuid): """ Load the soledad database in offline mode. @@ -944,23 +994,22 @@ class Keymanager(object): d.addCallback(export) d.addErrback(log_error) + @defer.inlineCallbacks def list_keys(self): """ List all the keys stored in the local DB. """ - d = self._keymanager_proxy.get_all_keys() - d.addCallback( - lambda keys: - self._signaler.signal(self._signaler.keymanager_keys_list, keys)) + keys = yield self._keymanager_proxy.get_all_keys() + keydicts = [dict(key) for key in keys] + self._signaler.signal(self._signaler.keymanager_keys_list, keydicts) def get_key_details(self, username): """ - List all the keys stored in the local DB. + Get information on our primary key pair """ def signal_details(public_key): - details = (public_key.key_id, public_key.fingerprint) self._signaler.signal(self._signaler.keymanager_key_details, - details) + dict(public_key)) d = self._keymanager_proxy.get_key(username, openpgp.OpenPGPKey) @@ -1012,7 +1061,8 @@ class Mail(object): """ return threads.deferToThread( self._smtp_bootstrapper.start_smtp_service, - self._keymanager_proxy, full_user_id, download_if_needed) + self._soledad_proxy, self._keymanager_proxy, full_user_id, + download_if_needed) def start_imap_service(self, full_user_id, offline=False): """ @@ -1058,6 +1108,18 @@ class Mail(object): """ return threads.deferToThread(self._stop_imap_service) + def start_pixelated_service(self, full_user_id): + if pix.HAS_PIXELATED: + reactor.callFromThread( + pix.start_pixelated_user_agent, + full_user_id, + self._soledad_proxy, + self._keymanager_proxy) + + def stop_pixelated_service(self): + # TODO stop it, somehow + pass + class Authenticate(object): """ @@ -1153,15 +1215,13 @@ class Authenticate(object): def get_logged_in_status(self): """ - Signal if the user is currently logged in or not. + Signal if the user is currently logged in or not. If logged in, + authenticated username is passed as argument to the signal. """ if self._signaler is None: return - signal = None if self._is_logged_in(): - signal = self._signaler.srp_status_logged_in + self._signaler.signal(self._signaler.srp_status_logged_in) else: - signal = self._signaler.srp_status_not_logged_in - - self._signaler.signal(signal) + self._signaler.signal(self._signaler.srp_status_not_logged_in) diff --git a/src/leap/bitmask/backend/leapbackend.py b/src/leap/bitmask/backend/leapbackend.py index cf45c4f8..56b1597c 100644 --- a/src/leap/bitmask/backend/leapbackend.py +++ b/src/leap/bitmask/backend/leapbackend.py @@ -35,6 +35,7 @@ class LeapBackend(Backend): """ Backend server subclass, used to implement the API methods. """ + def __init__(self, bypass_checks=False, frontend_pid=None): """ Constructor for the backend. @@ -438,6 +439,12 @@ class LeapBackend(Backend): """ self._soledad.load_offline(username, password, uuid) + def soledad_get_service_token(self, service): + """ + Attempt to get an authentication token for a given service. + """ + self._soledad.get_service_token(service) + def soledad_cancel_bootstrap(self): """ Cancel the ongoing soledad bootstrapping process (if any). @@ -524,6 +531,12 @@ class LeapBackend(Backend): """ self._mail.stop_imap_service() + def pixelated_start_service(self, full_user_id): + self._mail.start_pixelated_service(full_user_id) + + def pixelated_stop_service(self): + self._mail.stop_pixelated_service() + def settings_set_selected_gateway(self, provider, gateway): """ Set the selected gateway for a given provider. diff --git a/src/leap/bitmask/backend/leapsignaler.py b/src/leap/bitmask/backend/leapsignaler.py index 1ac51f5e..13a9fa5f 100644 --- a/src/leap/bitmask/backend/leapsignaler.py +++ b/src/leap/bitmask/backend/leapsignaler.py @@ -97,6 +97,7 @@ class LeapSignaler(SignalerQt): soledad_offline_finished = QtCore.Signal() soledad_password_change_error = QtCore.Signal() soledad_password_change_ok = QtCore.Signal() + soledad_got_service_token = QtCore.Signal(object) srp_auth_bad_user_or_password = QtCore.Signal() srp_auth_connection_error = QtCore.Signal() diff --git a/src/leap/bitmask/cli/__init__.py b/src/leap/bitmask/cli/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/leap/bitmask/cli/__init__.py diff --git a/src/leap/bitmask/cli/bitmask_cli.py b/src/leap/bitmask/cli/bitmask_cli.py new file mode 100755 index 00000000..c2b1ba71 --- /dev/null +++ b/src/leap/bitmask/cli/bitmask_cli.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# bitmask_cli +# Copyright (C) 2015, 2016 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Bitmask Command Line interface: zmq client. +""" +import json +import sys +import getpass +import argparse + +from colorama import init as color_init +from colorama import Fore +from twisted.internet import reactor +from txzmq import ZmqEndpoint, ZmqEndpointType +from txzmq import ZmqFactory, ZmqREQConnection +from txzmq import ZmqRequestTimeoutError + +from leap.bitmask.core import ENDPOINT + + +class BitmaskCLI(object): + + def __init__(self): + parser = argparse.ArgumentParser( + usage='''bitmask_cli <command> [<args>] + +Controls the Bitmask application. + +SERVICE COMMANDS: + + user Handles Bitmask accounts + mail Bitmask Encrypted Mail + eip Encrypted Internet Proxy + keys Bitmask Keymanager + +GENERAL COMMANDS: + + version prints version number and exit + launch launch the Bitmask backend daemon + shutdown shutdown Bitmask backend daemon + status displays general status about the running Bitmask services + debug show some debug info about bitmask-core + + +''', epilog=("Use 'bitmask_cli <command> --help' to learn more " + "about each command.")) + parser.add_argument('command', help='Subcommand to run') + + # parse_args defaults to [1:] for args, but you need to + # exclude the rest of the args too, or validation will fail + args = parser.parse_args(sys.argv[1:2]) + self.args = args + self.subargs = None + + if not hasattr(self, args.command): + print 'Unrecognized command' + parser.print_help() + exit(1) + + # use dispatch pattern to invoke method with same name + getattr(self, args.command)() + + def user(self): + parser = argparse.ArgumentParser( + description=('Handles Bitmask accounts: creation, authentication ' + 'and modification'), + prog='bitmask_cli user') + parser.add_argument('username', nargs='?', + help='username ID, in the form <user@example.org>') + parser.add_argument('--create', action='store_true', + help='register a new user, if possible') + parser.add_argument('--authenticate', action='store_true', + help='logs in against the provider') + parser.add_argument('--logout', action='store_true', + help='ends any active session with the provider') + parser.add_argument('--active', action='store_true', + help='shows the active user, if any') + # now that we're inside a subcommand, ignore the first + # TWO argvs, ie the command (bitmask_cli) and the subcommand (user) + args = parser.parse_args(sys.argv[2:]) + self.subargs = args + + def mail(self): + parser = argparse.ArgumentParser( + description='Bitmask Encrypted Mail service', + prog='bitmask_cli mail') + parser.add_argument('--start', action='store_true', + help='tries to start the mail service') + parser.add_argument('--stop', action='store_true', + help='stops the mail service if running') + parser.add_argument('--status', action='store_true', + help='displays status about the mail service') + parser.add_argument('--enable', action='store_true') + parser.add_argument('--disable', action='store_true') + parser.add_argument('--get-imap-token', action='store_true', + help='returns token for the IMAP service') + parser.add_argument('--get-smtp-token', action='store_true', + help='returns token for the SMTP service') + parser.add_argument('--get-smtp-certificate', action='store_true', + help='downloads a new smtp certificate') + parser.add_argument('--check-smtp-certificate', action='store_true', + help='downloads a new smtp certificate ' + '(NOT IMPLEMENTED)') + + args = parser.parse_args(sys.argv[2:]) + self.subargs = args + + def eip(self): + parser = argparse.ArgumentParser( + description='Encrypted Internet Proxy service', + prog='bitmask_cli eip') + parser.add_argument('--start', action='store_true', + help='Start service') + parser.add_argument('--stop', action='store_true', help='Stop service') + parser.add_argument('--status', action='store_true', + help='Display status about service') + parser.add_argument('--enable', action='store_true') + parser.add_argument('--disable', action='store_true') + args = parser.parse_args(sys.argv[2:]) + self.subargs = args + + def keys(self): + parser = argparse.ArgumentParser( + description='Bitmask Keymanager management service', + prog='bitmask_cli keys') + parser.add_argument('--status', action='store_true', + help='Display status about service') + parser.add_argument('--list-keys', action='store_true', + help='List all known keys') + parser.add_argument('--export-key', action='store_true', + help='Export the given key') + args = parser.parse_args(sys.argv[2:]) + self.subargs = args + + # Single commands + + def launch(self): + pass + + def shutdown(self): + pass + + def status(self): + pass + + def version(self): + pass + + def debug(self): + pass + + +def get_zmq_connection(): + zf = ZmqFactory() + e = ZmqEndpoint(ZmqEndpointType.connect, ENDPOINT) + return ZmqREQConnection(zf, e) + + +def error(msg, stop=False): + print Fore.RED + "[!] %s" % msg + Fore.RESET + if stop: + reactor.stop() + else: + sys.exit(1) + + +def timeout_handler(failure, stop_reactor=True): + # TODO ---- could try to launch the bitmask daemon here and retry + + if failure.trap(ZmqRequestTimeoutError) == ZmqRequestTimeoutError: + print (Fore.RED + "[ERROR] Timeout contacting the bitmask daemon. " + "Is it running?" + Fore.RESET) + reactor.stop() + + +def do_print_result(stuff): + obj = json.loads(stuff[0]) + if not obj['error']: + print Fore.GREEN + '%s' % obj['result'] + Fore.RESET + else: + print Fore.RED + 'ERROR:' + '%s' % obj['error'] + Fore.RESET + + +def send_command(cli): + + args = cli.args + subargs = cli.subargs + cb = do_print_result + + cmd = args.command + + if cmd == 'launch': + # XXX careful! Should see if the process in PID is running, + # avoid launching again. + import commands + commands.getoutput('bitmaskd') + reactor.stop() + return + + elif cmd == 'version': + do_print_result([json.dumps( + {'result': 'bitmask_cli: 0.0.1', + 'error': None})]) + data = ('version',) + + elif cmd == 'status': + data = ('status',) + + elif cmd == 'shutdown': + data = ('shutdown',) + + elif cmd == 'debug': + data = ('stats',) + + elif cmd == 'user': + username = subargs.username + if username and '@' not in username: + error("Username ID must be in the form <user@example.org>", + stop=True) + return + if not username: + username = '' + + # TODO check that ONLY ONE FLAG is True + # TODO check that AT LEAST ONE FLAG is True + + passwd = getpass.getpass() + data = ['user'] + + if subargs.active: + data += ['active', '', ''] + + elif subargs.create: + data += ['signup', username, passwd] + + elif subargs.authenticate: + data += ['authenticate', username, passwd] + + elif subargs.logout: + data += ['logout', username, passwd] + + else: + error('Use bitmask_cli user --help to see available subcommands') + return + + elif cmd == 'mail': + data = ['mail'] + + if subargs.status: + data += ['status'] + + elif subargs.enable: + data += ['enable'] + + elif subargs.disable: + data += ['disable'] + + elif subargs.get_imap_token: + data += ['get_imap_token'] + + elif subargs.get_smtp_token: + data += ['get_smtp_token'] + + elif subargs.get_smtp_certificate: + data += ['get_smtp_certificate'] + + else: + error('Use bitmask_cli mail --help to see available subcommands') + return + + elif cmd == 'eip': + data = ['eip'] + + if subargs.status: + data += ['status'] + + elif subargs.enable: + data += ['enable'] + + elif subargs.disable: + data += ['disable'] + + elif subargs.start: + data += ['start'] + + elif subargs.stop: + data += ['stop'] + + else: + error('Use bitmask_cli eip --help to see available subcommands', + stop=True) + return + + elif cmd == 'keys': + data = ['keys'] + + if subargs.status: + data += ['status'] + + elif subargs.list_keys: + data += ['list_keys'] + + elif subargs.export_key: + data += ['export_keys'] + + else: + error('Use bitmask_cli keys --help to see available subcommands', + stop=True) + return + + s = get_zmq_connection() + + d = s.sendMsg(*data, timeout=60) + d.addCallback(cb) + d.addCallback(lambda x: reactor.stop()) + d.addErrback(timeout_handler) + + +def main(): + color_init() + cli = BitmaskCLI() + reactor.callWhenRunning(reactor.callLater, 0, send_command, cli) + reactor.run() + +if __name__ == "__main__": + main() diff --git a/src/leap/bitmask/config/leapsettings.py b/src/leap/bitmask/config/leapsettings.py index 484a8a25..075be8a7 100644 --- a/src/leap/bitmask/config/leapsettings.py +++ b/src/leap/bitmask/config/leapsettings.py @@ -70,6 +70,7 @@ class LeapSettings(object): PINNED_KEY = "Pinned" SKIPFIRSTRUN_KEY = "SkipFirstRun" UUIDFORUSER_KEY = "%s/%s_uuid" + PIXELMAIL_KEY = "Pixmail" # values GATEWAY_AUTOMATIC = "Automatic" @@ -353,3 +354,10 @@ class LeapSettings(object): """ leap_assert_type(skip, bool) self._settings.setValue(self.SKIPFIRSTRUN_KEY, skip) + + def get_pixelmail_enabled(self): + return to_bool(self._settings.value(self.PIXELMAIL_KEY, False)) + + def set_pixelmail_enabled(self, enabled): + leap_assert_type(enabled, bool) + self._settings.setValue(self.PIXELMAIL_KEY, enabled) diff --git a/src/leap/bitmask/core/__init__.py b/src/leap/bitmask/core/__init__.py new file mode 100644 index 00000000..bda4b8d0 --- /dev/null +++ b/src/leap/bitmask/core/__init__.py @@ -0,0 +1,2 @@ +APPNAME = "bitmask.core" +ENDPOINT = "ipc:///tmp/%s.sock" % APPNAME diff --git a/src/leap/bitmask/core/_zmq.py b/src/leap/bitmask/core/_zmq.py new file mode 100644 index 00000000..a656fc65 --- /dev/null +++ b/src/leap/bitmask/core/_zmq.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# _zmq.py +# Copyright (C) 2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +ZMQ REQ-REP Dispatcher. +""" + +from twisted.application import service +from twisted.internet import reactor +from twisted.python import log + +from txzmq import ZmqEndpoint, ZmqEndpointType +from txzmq import ZmqFactory, ZmqREPConnection + +from leap.bitmask.core import ENDPOINT +from leap.bitmask.core.dispatcher import CommandDispatcher + + +class ZMQServerService(service.Service): + + def __init__(self, core): + self._core = core + + def startService(self): + zf = ZmqFactory() + e = ZmqEndpoint(ZmqEndpointType.bind, ENDPOINT) + + self._conn = _DispatcherREPConnection(zf, e, self._core) + reactor.callWhenRunning(self._conn.do_greet) + service.Service.startService(self) + + def stopService(self): + service.Service.stopService(self) + + +class _DispatcherREPConnection(ZmqREPConnection): + + def __init__(self, zf, e, core): + ZmqREPConnection.__init__(self, zf, e) + self.dispatcher = CommandDispatcher(core) + + def gotMessage(self, msgId, *parts): + + r = self.dispatcher.dispatch(parts) + r.addCallback(self.defer_reply, msgId) + + def defer_reply(self, response, msgId): + reactor.callLater(0, self.reply, msgId, str(response)) + + def log_err(self, failure, msgId): + log.err(failure) + self.defer_reply("ERROR: %r" % failure, msgId) + + def do_greet(self): + log.msg('starting ZMQ dispatcher') diff --git a/src/leap/bitmask/core/api.py b/src/leap/bitmask/core/api.py new file mode 100644 index 00000000..9f3725dc --- /dev/null +++ b/src/leap/bitmask/core/api.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# api.py +# Copyright (C) 2016 LEAP Encryption Acess Project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Registry for the public API for the Bitmask Backend. +""" +from collections import OrderedDict + +registry = OrderedDict() + + +class APICommand(type): + """ + A metaclass to keep a global registry of all the methods that compose the + public API for the Bitmask Backend. + """ + def __init__(cls, name, bases, attrs): + for key, val in attrs.iteritems(): + properties = getattr(val, 'register', None) + label = getattr(cls, 'label', None) + if label: + name = label + if properties is not None: + registry['%s.%s' % (name, key)] = properties + + +def register_method(*args): + """ + This method gathers info about all the methods that are supposed to + compose the public API to communicate with the backend. + + It sets up a register property for any method that uses it. + A type annotation is supposed to be in this property. + The APICommand metaclass collects these properties of the methods and + stores them in the global api_registry object, where they can be + introspected at runtime. + """ + def decorator(f): + f.register = tuple(args) + return f + return decorator diff --git a/src/leap/bitmask/core/bitmaskd.tac b/src/leap/bitmask/core/bitmaskd.tac new file mode 100644 index 00000000..3c9b1d8b --- /dev/null +++ b/src/leap/bitmask/core/bitmaskd.tac @@ -0,0 +1,11 @@ +# Service composition for bitmask-core. +# Run as: twistd -n -y bitmaskd.tac +# +from twisted.application import service + +from leap.bitmask.core.service import BitmaskBackend + + +bb = BitmaskBackend() +application = service.Application("bitmaskd") +bb.setServiceParent(application) diff --git a/src/leap/bitmask/core/configurable.py b/src/leap/bitmask/core/configurable.py new file mode 100644 index 00000000..8e33de95 --- /dev/null +++ b/src/leap/bitmask/core/configurable.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# configurable.py +# Copyright (C) 2015, 2016 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Configurable Backend for Bitmask Service. +""" +import ConfigParser +import os + +from twisted.application import service + +from leap.common import files +from leap.common.config import get_path_prefix + + +DEFAULT_BASEDIR = os.path.join(get_path_prefix(), 'leap') + + +class MissingConfigEntry(Exception): + """ + A required config entry was not found. + """ + + +class ConfigurableService(service.MultiService): + + config_file = u"bitmaskd.cfg" + service_names = ('mail', 'eip', 'zmq', 'web') + + def __init__(self, basedir=DEFAULT_BASEDIR): + service.MultiService.__init__(self) + + path = os.path.abspath(os.path.expanduser(basedir)) + if not os.path.isdir(path): + files.mkdir_p(path) + self.basedir = path + + # creates self.config + self.read_config() + + def get_config(self, section, option, default=None, boolean=False): + try: + if boolean: + return self.config.getboolean(section, option) + + item = self.config.get(section, option) + return item + + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): + if default is None: + fn = self._get_config_path() + raise MissingConfigEntry("%s is missing the [%s]%s entry" + % fn, section, option) + return default + + def set_config(self, section, option, value): + if not self.config.has_section(section): + self.config.add_section(section) + self.config.set(section, option, value) + self.save_config() + self.read_config() + assert self.config.get(section, option) == value + + def read_config(self): + self.config = ConfigParser.SafeConfigParser() + bitmaskd_cfg = self._get_config_path() + + if not os.path.isfile(bitmaskd_cfg): + self._create_default_config(bitmaskd_cfg) + + with open(bitmaskd_cfg, "rb") as f: + self.config.readfp(f) + + def save_config(self): + bitmaskd_cfg = self._get_config_path() + with open(bitmaskd_cfg, 'wb') as f: + self.config.write(f) + + def _create_default_config(self, path): + with open(path, 'w') as outf: + outf.write(DEFAULT_CONFIG) + + def _get_config_path(self): + return os.path.join(self.basedir, self.config_file) + + +DEFAULT_CONFIG = """ +[services] +mail = True +eip = True +zmq = True +web = False +""" diff --git a/src/leap/bitmask/core/dispatcher.py b/src/leap/bitmask/core/dispatcher.py new file mode 100644 index 00000000..e7c961fd --- /dev/null +++ b/src/leap/bitmask/core/dispatcher.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- +# dispatcher.py +# Copyright (C) 2016 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Command dispatcher. +""" +import json + +from twisted.internet import defer +from twisted.python import failure, log + +from .api import APICommand, register_method + + +class SubCommand(object): + + __metaclass__ = APICommand + + def dispatch(self, service, *parts, **kw): + subcmd = parts[1] + + _method = getattr(self, 'do_' + subcmd.upper(), None) + if not _method: + raise RuntimeError('No such subcommand') + return _method(service, *parts, **kw) + + +class UserCmd(SubCommand): + + label = 'user' + + @register_method("{'srp_token': unicode, 'uuid': unicode}") + def do_AUTHENTICATE(self, bonafide, *parts): + user, password = parts[2], parts[3] + d = defer.maybeDeferred(bonafide.do_authenticate, user, password) + return d + + @register_method("{'signup': 'ok', 'user': str}") + def do_SIGNUP(self, bonafide, *parts): + user, password = parts[2], parts[3] + d = defer.maybeDeferred(bonafide.do_signup, user, password) + return d + + @register_method("{'logout': 'ok'}") + def do_LOGOUT(self, bonafide, *parts): + user, password = parts[2], parts[3] + d = defer.maybeDeferred(bonafide.do_logout, user, password) + return d + + @register_method('str') + def do_ACTIVE(self, bonafide, *parts): + d = defer.maybeDeferred(bonafide.do_get_active_user) + return d + + +class EIPCmd(SubCommand): + + label = 'eip' + + @register_method('dict') + def do_ENABLE(self, service, *parts): + d = service.do_enable_service(self.label) + return d + + @register_method('dict') + def do_DISABLE(self, service, *parts): + d = service.do_disable_service(self.label) + return d + + @register_method('dict') + def do_STATUS(self, eip, *parts): + d = eip.do_status() + return d + + @register_method('dict') + def do_START(self, eip, *parts): + # TODO --- attempt to get active provider + # TODO or catch the exception and send error + provider = parts[2] + d = eip.do_start(provider) + return d + + @register_method('dict') + def do_STOP(self, eip, *parts): + d = eip.do_stop() + return d + + +class MailCmd(SubCommand): + + label = 'mail' + + @register_method('dict') + def do_ENABLE(self, service, *parts): + d = service.do_enable_service(self.label) + return d + + @register_method('dict') + def do_DISABLE(self, service, *parts): + d = service.do_disable_service(self.label) + return d + + @register_method('dict') + def do_STATUS(self, mail, *parts): + d = mail.do_status() + return d + + @register_method('dict') + def do_GET_IMAP_TOKEN(self, mail, *parts): + d = mail.get_imap_token() + return d + + @register_method('dict') + def do_GET_SMTP_TOKEN(self, mail, *parts): + d = mail.get_smtp_token() + return d + + @register_method('dict') + def do_GET_SMTP_CERTIFICATE(self, mail, *parts, **kw): + # TODO move to mail service + # TODO should ask for confirmation? like --force or something, + # if we already have a valid one. or better just refuse if cert + # exists. + # TODO how should we pass the userid?? + # - Keep an 'active' user in bonafide (last authenticated) + # (doing it now) + # - Get active user from Mail Service (maybe preferred?) + # - Have a command/method to set 'active' user. + + @defer.inlineCallbacks + def save_cert(cert_data): + userid, cert_str = cert_data + cert_path = yield mail.do_get_smtp_cert_path(userid) + with open(cert_path, 'w') as outf: + outf.write(cert_str) + defer.returnValue('certificate saved to %s' % cert_path) + + bonafide = kw['bonafide'] + d = bonafide.do_get_smtp_cert() + d.addCallback(save_cert) + return d + + +class CommandDispatcher(object): + + __metaclass__ = APICommand + + label = 'core' + + def __init__(self, core): + + self.core = core + self.subcommand_user = UserCmd() + self.subcommand_eip = EIPCmd() + self.subcommand_mail = MailCmd() + + # XXX -------------------------------------------- + # TODO move general services to another subclass + + @register_method("{'mem_usage': str}") + def do_STATS(self, *parts): + return _format_result(self.core.do_stats()) + + @register_method("{version_core': '0.0.0'}") + def do_VERSION(self, *parts): + return _format_result(self.core.do_version()) + + @register_method("{'mail': 'running'}") + def do_STATUS(self, *parts): + return _format_result(self.core.do_status()) + + @register_method("{'shutdown': 'ok'}") + def do_SHUTDOWN(self, *parts): + return _format_result(self.core.do_shutdown()) + + # ----------------------------------------------- + + def do_USER(self, *parts): + bonafide = self._get_service('bonafide') + d = self.subcommand_user.dispatch(bonafide, *parts) + d.addCallbacks(_format_result, _format_error) + return d + + def do_EIP(self, *parts): + eip = self._get_service(self.subcommand_eip.label) + if not eip: + return _format_result('eip: disabled') + subcmd = parts[1] + + dispatch = self._subcommand_eip.dispatch + if subcmd in ('enable', 'disable'): + d = dispatch(self.core, *parts) + else: + d = dispatch(eip, *parts) + + d.addCallbacks(_format_result, _format_error) + return d + + def do_MAIL(self, *parts): + subcmd = parts[1] + dispatch = self.subcommand_mail.dispatch + + if subcmd == 'enable': + d = dispatch(self.core, *parts) + + mail = self._get_service(self.subcommand_mail.label) + bonafide = self._get_service('bonafide') + kw = {'bonafide': bonafide} + + if not mail: + return _format_result('mail: disabled') + + if subcmd == 'disable': + d = dispatch(self.core) + else: + d = dispatch(mail, *parts, **kw) + + d.addCallbacks(_format_result, _format_error) + return d + + def do_KEYS(self, *parts): + subcmd = parts[1] + + keymanager_label = 'keymanager' + km = self._get_service(keymanager_label) + bf = self._get_service('bonafide') + + if not km: + return _format_result('keymanager: disabled') + + if subcmd == 'list_keys': + d = bf.do_get_active_user() + d.addCallback(km.do_list_keys) + d.addCallbacks(_format_result, _format_error) + return d + + def dispatch(self, msg): + cmd = msg[0] + + _method = getattr(self, 'do_' + cmd.upper(), None) + + if not _method: + return defer.fail(failure.Failure(RuntimeError('No such command'))) + + return defer.maybeDeferred(_method, *msg) + + def _get_service(self, name): + try: + return self.core.getServiceNamed(name) + except KeyError: + return None + + +def _format_result(result): + return json.dumps({'error': None, 'result': result}) + + +def _format_error(failure): + log.err(failure) + return json.dumps({'error': failure.value.message, 'result': None}) diff --git a/src/leap/bitmask/core/dummy.py b/src/leap/bitmask/core/dummy.py new file mode 100644 index 00000000..99dfafa5 --- /dev/null +++ b/src/leap/bitmask/core/dummy.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# dummy.py +# Copyright (C) 2016 LEAP Encryption Acess Project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +An authoritative dummy backend for tests. +""" +import json + +from leap.common.service_hooks import HookableService + + +class BackendCommands(object): + + """ + General commands for the BitmaskBackend Core Service. + """ + + def __init__(self, core): + self.core = core + + def do_status(self): + return json.dumps( + {'soledad': 'running', + 'keymanager': 'running', + 'mail': 'running', + 'eip': 'stopped', + 'backend': 'dummy'}) + + def do_version(self): + return {'version_core': '0.0.1'} + + def do_stats(self): + return {'mem_usage': '01 KB'} + + def do_shutdown(self): + return {'shutdown': 'ok'} + + +class mail_services(object): + + class SoledadService(HookableService): + pass + + class KeymanagerService(HookableService): + pass + + class StandardMailService(HookableService): + pass + + +class BonafideService(HookableService): + + def __init__(self, basedir): + pass + + def do_authenticate(self, user, password): + return {u'srp_token': u'deadbeef123456789012345678901234567890123', + u'uuid': u'01234567890abcde01234567890abcde'} + + def do_signup(self, user, password): + return {'signup': 'ok', 'user': 'dummyuser@provider.example.org'} + + def do_logout(self, user, password): + return {'logout': 'ok'} + + def do_get_active_user(self): + return 'dummyuser@provider.example.org' diff --git a/src/leap/bitmask/core/flags.py b/src/leap/bitmask/core/flags.py new file mode 100644 index 00000000..9a40c70c --- /dev/null +++ b/src/leap/bitmask/core/flags.py @@ -0,0 +1 @@ +BACKEND = 'default' diff --git a/src/leap/bitmask/core/launcher.py b/src/leap/bitmask/core/launcher.py new file mode 100644 index 00000000..b8916a1e --- /dev/null +++ b/src/leap/bitmask/core/launcher.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# launcher.py +# Copyright (C) 2016 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Run bitmask daemon. +""" +from os.path import join +from sys import argv + +from twisted.scripts.twistd import run + +from leap.bitmask.util import here +from leap.bitmask import core +from leap.bitmask.core import flags + + +def run_bitmaskd(): + # TODO --- configure where to put the logs... (get --logfile, --logdir + # from the bitmask_cli + for (index, arg) in enumerate(argv): + if arg == '--backend': + flags.BACKEND = argv[index + 1] + argv[1:] = [ + '-y', join(here(core), "bitmaskd.tac"), + '--pidfile', '/tmp/bitmaskd.pid', + '--logfile', '/tmp/bitmaskd.log', + '--umask=0022', + ] + print '[+] launching bitmaskd...' + run() + + +if __name__ == "__main__": + run_bitmaskd() diff --git a/src/leap/bitmask/core/mail_services.py b/src/leap/bitmask/core/mail_services.py new file mode 100644 index 00000000..fb9ee698 --- /dev/null +++ b/src/leap/bitmask/core/mail_services.py @@ -0,0 +1,688 @@ +# -*- coding: utf-8 -*- +# mail_services.py +# Copyright (C) 2016 LEAP Encryption Acess Project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Mail services. + +This is quite moving work still. +This should be moved to the different packages when it stabilizes. +""" +import json +import os +from collections import defaultdict +from collections import namedtuple + +from twisted.application import service +from twisted.internet import defer +from twisted.python import log + +from leap.bonafide import config +from leap.common.service_hooks import HookableService +from leap.keymanager import KeyManager, openpgp +from leap.keymanager.errors import KeyNotFound +from leap.soledad.client.api import Soledad +from leap.mail.constants import INBOX_NAME +from leap.mail.mail import Account +from leap.mail.imap.service import imap +from leap.mail.incoming.service import IncomingMail, INCOMING_CHECK_PERIOD +from leap.mail import smtp + +from leap.bitmask.core.uuid_map import UserMap +from leap.bitmask.core.configurable import DEFAULT_BASEDIR + + +class Container(object): + + def __init__(self, service=None): + self._instances = defaultdict(None) + if service is not None: + self.service = service + + def get_instance(self, key): + return self._instances.get(key, None) + + def add_instance(self, key, data): + self._instances[key] = data + + +class ImproperlyConfigured(Exception): + pass + + +class SoledadContainer(Container): + + def __init__(self, service=None, basedir=DEFAULT_BASEDIR): + self._basedir = os.path.expanduser(basedir) + self._usermap = UserMap() + super(SoledadContainer, self).__init__(service=service) + + def add_instance(self, userid, passphrase, uuid=None, token=None): + + if not uuid: + bootstrapped_uuid = self._usermap.lookup_uuid(userid, passphrase) + uuid = bootstrapped_uuid + if not uuid: + return + else: + self._usermap.add(userid, uuid, passphrase) + + user, provider = userid.split('@') + + soledad_path = os.path.join(self._basedir, 'soledad') + soledad_url = _get_soledad_uri(self._basedir, provider) + cert_path = _get_ca_cert_path(self._basedir, provider) + + soledad = self._create_soledad_instance( + uuid, passphrase, soledad_path, soledad_url, + cert_path, token) + + super(SoledadContainer, self).add_instance(userid, soledad) + + data = {'user': userid, 'uuid': uuid, 'token': token, + 'soledad': soledad} + self.service.trigger_hook('on_new_soledad_instance', **data) + + def _create_soledad_instance(self, uuid, passphrase, soledad_path, + server_url, cert_file, token): + # setup soledad info + secrets_path = os.path.join(soledad_path, '%s.secret' % uuid) + local_db_path = os.path.join(soledad_path, '%s.db' % uuid) + + if token is None: + syncable = False + token = '' + else: + syncable = True + + return Soledad( + uuid, + unicode(passphrase), + secrets_path=secrets_path, + local_db_path=local_db_path, + server_url=server_url, + cert_file=cert_file, + auth_token=token, + defer_encryption=True, + syncable=syncable) + + def set_remote_auth_token(self, userid, token): + self.get_instance(userid).token = token + + def set_syncable(self, userid, state): + # TODO should check that there's a token! + self.get_instance(userid).set_syncable(bool(state)) + + def sync(self, userid): + self.get_instance(userid).sync() + + +def _get_provider_from_full_userid(userid): + _, provider_id = config.get_username_and_provider(userid) + return config.Provider(provider_id) + + +def is_service_ready(service, provider): + """ + Returns True when the following conditions are met: + - Provider offers that service. + - We have the config files for the service. + - The service is enabled. + """ + has_service = provider.offers_service(service) + has_config = provider.has_config_for_service(service) + is_enabled = provider.is_service_enabled(service) + return has_service and has_config and is_enabled + + +class SoledadService(HookableService): + + def __init__(self, basedir): + service.Service.__init__(self) + self._basedir = basedir + + def startService(self): + log.msg('Starting Soledad Service') + self._container = SoledadContainer(service=self) + super(SoledadService, self).startService() + + # hooks + + def hook_on_passphrase_entry(self, **kw): + userid = kw.get('username') + provider = _get_provider_from_full_userid(userid) + provider.callWhenReady(self._hook_on_passphrase_entry, provider, **kw) + + def _hook_on_passphrase_entry(self, provider, **kw): + if is_service_ready('mx', provider): + userid = kw.get('username') + password = kw.get('password') + uuid = kw.get('uuid') + container = self._container + log.msg("on_passphrase_entry: New Soledad Instance: %s" % userid) + if not container.get_instance(userid): + container.add_instance(userid, password, uuid=uuid, token=None) + else: + log.msg('Service MX is not ready...') + + def hook_on_bonafide_auth(self, **kw): + userid = kw['username'] + provider = _get_provider_from_full_userid(userid) + provider.callWhenReady(self._hook_on_bonafide_auth, provider, **kw) + + def _hook_on_bonafide_auth(self, provider, **kw): + if provider.offers_service('mx'): + userid = kw['username'] + password = kw['password'] + token = kw['token'] + uuid = kw['uuid'] + + container = self._container + if container.get_instance(userid): + log.msg("Passing a new SRP Token to Soledad: %s" % userid) + container.set_remote_auth_token(userid, token) + container.set_syncable(userid, True) + else: + log.msg("Adding a new Soledad Instance: %s" % userid) + container.add_instance( + userid, password, uuid=uuid, token=token) + + +class KeymanagerContainer(Container): + + def __init__(self, service=None, basedir=DEFAULT_BASEDIR): + self._basedir = os.path.expanduser(basedir) + super(KeymanagerContainer, self).__init__(service=service) + + def add_instance(self, userid, token, uuid, soledad): + + keymanager = self._create_keymanager_instance( + userid, token, uuid, soledad) + + d = self._get_or_generate_keys(keymanager, userid) + d.addCallback(self._on_keymanager_ready_cb, userid, soledad) + return d + + def set_remote_auth_token(self, userid, token): + self.get_instance(userid)._token = token + + def _on_keymanager_ready_cb(self, keymanager, userid, soledad): + # TODO use onready-deferreds instead + self.add_instance(userid, keymanager) + + log.msg("Adding Keymanager instance for: %s" % userid) + data = {'userid': userid, 'soledad': soledad, 'keymanager': keymanager} + self.service.trigger_hook('on_new_keymanager_instance', **data) + + def _get_or_generate_keys(self, keymanager, userid): + + def if_not_found_generate(failure): + # TODO -------------- should ONLY generate if INITIAL_SYNC_DONE. + # ie: put callback on_soledad_first_sync_ready ----------------- + # -------------------------------------------------------------- + failure.trap(KeyNotFound) + log.msg("Core: Key not found. Generating key for %s" % (userid,)) + d = keymanager.gen_key(openpgp.OpenPGPKey) + d.addCallbacks(send_key, log_key_error("generating")) + return d + + def send_key(ignored): + # ---------------------------------------------------------------- + # It might be the case that we have generated a key-pair + # but this hasn't been successfully uploaded. How do we know that? + # XXX Should this be a method of bonafide instead? + # ----------------------------------------------------------------- + d = keymanager.send_key(openpgp.OpenPGPKey) + d.addCallbacks( + lambda _: log.msg( + "Key generated successfully for %s" % userid), + log_key_error("sending")) + return d + + def log_key_error(step): + def log_error(failure): + log.err("Error while %s key!" % step) + log.err(failure) + return failure + return log_error + + d = keymanager.get_key( + userid, openpgp.OpenPGPKey, private=True, fetch_remote=False) + d.addErrback(if_not_found_generate) + d.addCallback(lambda _: keymanager) + return d + + def _create_keymanager_instance(self, userid, token, uuid, soledad): + user, provider = userid.split('@') + nickserver_uri = self._get_nicknym_uri(provider) + + cert_path = _get_ca_cert_path(self._basedir, provider) + api_uri = self._get_api_uri(provider) + + if not token: + token = self.service.tokens.get(userid) + + km_args = (userid, nickserver_uri, soledad) + + # TODO use the method in + # services.soledadbootstrapper._get_gpg_bin_path. + # That should probably live in keymanager package. + + km_kwargs = { + "token": token, "uid": uuid, + "api_uri": api_uri, "api_version": "1", + "ca_cert_path": cert_path, + "gpgbinary": "/usr/bin/gpg" + } + keymanager = KeyManager(*km_args, **km_kwargs) + return keymanager + + def _get_api_uri(self, provider): + # TODO get this from service.json (use bonafide service) + api_uri = "https://api.{provider}:4430".format( + provider=provider) + return api_uri + + def _get_nicknym_uri(self, provider): + return 'https://nicknym.{provider}:6425'.format( + provider=provider) + + +class KeymanagerService(HookableService): + + def __init__(self, basedir=DEFAULT_BASEDIR): + service.Service.__init__(self) + self._basedir = basedir + + def startService(self): + log.msg('Starting Keymanager Service') + self._container = KeymanagerContainer(self._basedir) + self._container.service = self + self.tokens = {} + super(KeymanagerService, self).startService() + + # hooks + + def hook_on_new_soledad_instance(self, **kw): + container = self._container + user = kw['user'] + token = kw['token'] + uuid = kw['uuid'] + soledad = kw['soledad'] + if not container.get_instance(user): + log.msg('Adding a new Keymanager instance for %s' % user) + if not token: + token = self.tokens.get(user) + container.add_instance(user, token, uuid, soledad) + + def hook_on_bonafide_auth(self, **kw): + userid = kw['username'] + provider = _get_provider_from_full_userid(userid) + provider.callWhenReady(self._hook_on_bonafide_auth, provider, **kw) + + def _hook_on_bonafide_auth(self, provider, **kw): + if provider.offers_service('mx'): + userid = kw['username'] + token = kw['token'] + + container = self._container + if container.get_instance(userid): + log.msg('Passing a new SRP Token to Keymanager: %s' % userid) + container.set_remote_auth_token(userid, token) + else: + log.msg('storing the keymanager token... %s ' % token) + self.tokens[userid] = token + + # commands + + def do_list_keys(self, userid): + km = self._container.get_instance(userid) + d = km.get_all_keys() + d.addCallback( + lambda keys: [ + (key.uids, key.fingerprint) for key in keys]) + return d + + +class StandardMailService(service.MultiService, HookableService): + """ + A collection of Services. + + This is the parent service, that launches 3 different services that expose + Encrypted Mail Capabilities on specific ports: + + - SMTP service, on port 2013 + - IMAP service, on port 1984 + - The IncomingMail Service, which doesn't listen on any port, but + watches and processes the Incoming Queue and saves the processed mail + into the matching INBOX. + """ + + name = 'mail' + + # TODO factor out Mail Service to inside mail package. + + subscribed_to_hooks = ('on_new_keymanager_instance',) + + def __init__(self, basedir): + self._basedir = basedir + self._soledad_sessions = {} + self._keymanager_sessions = {} + self._sendmail_opts = {} + self._imap_tokens = {} + self._smtp_tokens = {} + self._active_user = None + super(StandardMailService, self).__init__() + self.initializeChildrenServices() + + def initializeChildrenServices(self): + self.addService(IMAPService(self._soledad_sessions)) + self.addService(SMTPService( + self._soledad_sessions, self._keymanager_sessions, + self._sendmail_opts)) + # TODO adapt the service to receive soledad/keymanager sessions object. + # See also the TODO before IncomingMailService.startInstance + self.addService(IncomingMailService(self)) + + def startService(self): + log.msg('Starting Mail Service...') + super(StandardMailService, self).startService() + + def stopService(self): + super(StandardMailService, self).stopService() + + def startInstance(self, userid, soledad, keymanager): + username, provider = userid.split('@') + + self._soledad_sessions[userid] = soledad + self._keymanager_sessions[userid] = keymanager + + sendmail_opts = _get_sendmail_opts(self._basedir, provider, username) + self._sendmail_opts[userid] = sendmail_opts + + incoming = self.getServiceNamed('incoming_mail') + incoming.startInstance(userid) + + def registerIMAPToken(token): + self._imap_tokens[userid] = token + self._active_user = userid + return token + + def registerSMTPToken(token): + self._smtp_tokens[userid] = token + return token + + d = soledad.get_or_create_service_token('imap') + d.addCallback(registerIMAPToken) + d.addCallback( + lambda _: soledad.get_or_create_service_token('smtp')) + d.addCallback(registerSMTPToken) + return d + + def stopInstance(self): + pass + + # hooks + + def hook_on_new_keymanager_instance(self, **kw): + # XXX we can specify this as a waterfall, or just AND the two + # conditions. + userid = kw['userid'] + soledad = kw['soledad'] + keymanager = kw['keymanager'] + + # TODO --- only start instance if "autostart" is True. + self.startInstance(userid, soledad, keymanager) + + # commands + + def do_status(self): + return 'mail: %s' % 'running' if self.running else 'disabled' + + def get_imap_token(self): + active_user = self._active_user + if not active_user: + return defer.succeed('NO ACTIVE USER') + token = self._imap_tokens.get(active_user) + # TODO return just the tuple, no format. + return defer.succeed("IMAP TOKEN (%s): %s" % (active_user, token)) + + def get_smtp_token(self): + active_user = self._active_user + if not active_user: + return defer.succeed('NO ACTIVE USER') + token = self._smtp_tokens.get(active_user) + # TODO return just the tuple, no format. + return defer.succeed("SMTP TOKEN (%s): %s" % (active_user, token)) + + def do_get_smtp_cert_path(self, userid): + username, provider = userid.split('@') + return _get_smtp_client_cert_path(self._basedir, provider, username) + + # access to containers + + def get_soledad_session(self, userid): + return self._soledad_sessions.get(userid) + + def get_keymanager_session(self, userid): + return self._keymanager_sessions.get(userid) + + +class IMAPService(service.Service): + + name = 'imap' + + def __init__(self, soledad_sessions): + port, factory = imap.run_service(soledad_sessions) + + self._port = port + self._factory = factory + self._soledad_sessions = soledad_sessions + super(IMAPService, self).__init__() + + def startService(self): + log.msg('Starting IMAP Service') + super(IMAPService, self).startService() + + def stopService(self): + self._port.stopListening() + self._factory.doStop() + super(IMAPService, self).stopService() + + +class SMTPService(service.Service): + + name = 'smtp' + + def __init__(self, soledad_sessions, keymanager_sessions, sendmail_opts, + basedir=DEFAULT_BASEDIR): + + self._basedir = os.path.expanduser(basedir) + port, factory = smtp.run_service( + soledad_sessions, keymanager_sessions, sendmail_opts) + self._port = port + self._factory = factory + self._soledad_sessions = soledad_sessions + self._keymanager_sessions = keymanager_sessions + self._sendmail_opts = sendmail_opts + super(SMTPService, self).__init__() + + def startService(self): + log.msg('Starting SMTP Service') + super(SMTPService, self).startService() + + def stopService(self): + # TODO cleanup all instances + super(SMTPService, self).stopService() + + +class IncomingMailService(service.Service): + + name = 'incoming_mail' + + def __init__(self, mail_service): + super(IncomingMailService, self).__init__() + self._mail = mail_service + self._instances = {} + + def startService(self): + log.msg('Starting IncomingMail Service') + super(IncomingMailService, self).startService() + + def stopService(self): + super(IncomingMailService, self).stopService() + + # Individual accounts + + # TODO IncomingMail *IS* already a service. + # I think we should better model the current Service + # as a startInstance inside a container, and get this + # multi-tenant service inside the leap.mail.incoming.service. + # ... or just simply make it a multiService and set per-user + # instances as Child of this parent. + + def startInstance(self, userid): + soledad = self._mail.get_soledad_session(userid) + keymanager = self._mail.get_keymanager_session(userid) + + log.msg('Starting Incoming Mail instance for %s' % userid) + self._start_incoming_mail_instance( + keymanager, soledad, userid) + + def stopInstance(self, userid): + # TODO toggle offline! + pass + + def _start_incoming_mail_instance(self, keymanager, soledad, + userid, start_sync=True): + + def setUpIncomingMail(inbox): + incoming_mail = IncomingMail( + keymanager, soledad, + inbox, userid, + check_period=INCOMING_CHECK_PERIOD) + return incoming_mail + + def registerInstance(incoming_instance): + self._instances[userid] = incoming_instance + if start_sync: + incoming_instance.startService() + + acc = Account(soledad, userid) + d = acc.callWhenReady( + lambda _: acc.get_collection_by_mailbox(INBOX_NAME)) + d.addCallback(setUpIncomingMail) + d.addCallback(registerInstance) + d.addErrback(log.err) + return d + +# -------------------------------------------------------------------- +# +# config utilities. should be moved to bonafide +# + +SERVICES = ('soledad', 'smtp', 'eip') + + +Provider = namedtuple( + 'Provider', ['hostname', 'ip_address', 'location', 'port']) + +SendmailOpts = namedtuple( + 'SendmailOpts', ['cert', 'key', 'hostname', 'port']) + + +def _get_ca_cert_path(basedir, provider): + path = os.path.join( + basedir, 'providers', provider, 'keys', 'ca', 'cacert.pem') + return path + + +def _get_sendmail_opts(basedir, provider, username): + cert = _get_smtp_client_cert_path(basedir, provider, username) + key = cert + prov = _get_provider_for_service('smtp', basedir, provider) + hostname = prov.hostname + port = prov.port + opts = SendmailOpts(cert, key, hostname, port) + return opts + + +def _get_smtp_client_cert_path(basedir, provider, username): + path = os.path.join( + basedir, 'providers', provider, 'keys', 'client', 'stmp_%s.pem' % + username) + return path + + +def _get_config_for_service(service, basedir, provider): + if service not in SERVICES: + raise ImproperlyConfigured('Tried to use an unknown service') + + config_path = os.path.join( + basedir, 'providers', provider, '%s-service.json' % service) + try: + with open(config_path) as config: + config = json.loads(config.read()) + except IOError: + # FIXME might be that the provider DOES NOT offer this service! + raise ImproperlyConfigured( + 'could not open config file %s' % config_path) + else: + return config + + +def first(xs): + return xs[0] + + +def _pick_server(config, strategy=first): + """ + Picks a server from a list of possible choices. + The service files have a <describe>. + This implementation just picks the FIRST available server. + """ + servers = config['hosts'].keys() + choice = config['hosts'][strategy(servers)] + return choice + + +def _get_subdict(d, keys): + return {key: d.get(key) for key in keys} + + +def _get_provider_for_service(service, basedir, provider): + + if service not in SERVICES: + raise ImproperlyConfigured('Tried to use an unknown service') + + config = _get_config_for_service(service, basedir, provider) + p = _pick_server(config) + attrs = _get_subdict(p, ('hostname', 'ip_address', 'location', 'port')) + provider = Provider(**attrs) + return provider + + +def _get_smtp_uri(basedir, provider): + prov = _get_provider_for_service('smtp', basedir, provider) + url = 'https://{hostname}:{port}'.format( + hostname=prov.hostname, port=prov.port) + return url + + +def _get_soledad_uri(basedir, provider): + prov = _get_provider_for_service('soledad', basedir, provider) + url = 'https://{hostname}:{port}'.format( + hostname=prov.hostname, port=prov.port) + return url diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py new file mode 100644 index 00000000..99132c2d --- /dev/null +++ b/src/leap/bitmask/core/service.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# service.py +# Copyright (C) 2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Bitmask-core Service. +""" +import json +import resource + +from twisted.internet import reactor +from twisted.python import log + +from leap.bitmask import __version__ +from leap.bitmask.core import configurable +from leap.bitmask.core import _zmq +from leap.bitmask.core import flags +from leap.common.events import server as event_server +# from leap.vpn import EIPService + + +backend = flags.BACKEND + +if backend == 'default': + from leap.bitmask.core import mail_services + from leap.bonafide.service import BonafideService +elif backend == 'dummy': + from leap.bitmask.core.dummy import mail_services + from leap.bitmask.core.dummy import BonafideService +else: + raise RuntimeError('Backend not supported') + + +class BitmaskBackend(configurable.ConfigurableService): + + def __init__(self, basedir=configurable.DEFAULT_BASEDIR): + + configurable.ConfigurableService.__init__(self, basedir) + self.core_commands = BackendCommands(self) + + def enabled(service): + return self.get_config('services', service, False, boolean=True) + + on_start = reactor.callWhenRunning + + on_start(self.init_events) + on_start(self.init_bonafide) + + if enabled('mail'): + on_start(self.init_soledad) + on_start(self.init_keymanager) + on_start(self.init_mail) + + if enabled('eip'): + on_start(self.init_eip) + + if enabled('zmq'): + on_start(self.init_zmq) + + if enabled('web'): + on_start(self.init_web) + + def init_events(self): + event_server.ensure_server() + + def init_bonafide(self): + bf = BonafideService(self.basedir) + bf.setName("bonafide") + bf.setServiceParent(self) + # TODO ---- these hooks should be activated only if + # (1) we have enabled that service + # (2) provider offers this service + bf.register_hook('on_passphrase_entry', listener='soledad') + bf.register_hook('on_bonafide_auth', listener='soledad') + bf.register_hook('on_bonafide_auth', listener='keymanager') + + def init_soledad(self): + service = mail_services.SoledadService + sol = self._maybe_start_service( + 'soledad', service, self.basedir) + if sol: + sol.register_hook( + 'on_new_soledad_instance', listener='keymanager') + + def init_keymanager(self): + service = mail_services.KeymanagerService + km = self._maybe_start_service( + 'keymanager', service, self.basedir) + if km: + km.register_hook('on_new_keymanager_instance', listener='mail') + + def init_mail(self): + service = mail_services.StandardMailService + self._maybe_start_service('mail', service, self.basedir) + + def init_eip(self): + # FIXME -- land EIP into leap.vpn + pass + # self._maybe_start_service('eip', EIPService) + + def init_zmq(self): + zs = _zmq.ZMQServerService(self) + zs.setServiceParent(self) + + def init_web(self): + from leap.bitmask.core import websocket + ws = websocket.WebSocketsDispatcherService(self) + ws.setServiceParent(self) + + def _maybe_start_service(self, label, klass, *args, **kw): + try: + self.getServiceNamed(label) + except KeyError: + service = klass(*args, **kw) + service.setName(label) + service.setServiceParent(self) + return service + + def do_stats(self): + return self.core_commands.do_stats() + + def do_status(self): + return self.core_commands.do_status() + + def do_version(self): + return self.core_commands.do_version() + + def do_shutdown(self): + return self.core_commands.do_shutdown() + + # Service Toggling + + def do_enable_service(self, service): + assert service in self.service_names + self.set_config('services', service, 'True') + + if service == 'mail': + self.init_soledad() + self.init_keymanager() + self.init_mail() + + elif service == 'eip': + self.init_eip() + + elif service == 'zmq': + self.init_zmq() + + elif service == 'web': + self.init_web() + + return 'ok' + + def do_disable_service(self, service): + assert service in self.service_names + # TODO -- should stop also? + self.set_config('services', service, 'False') + return 'ok' + + +class BackendCommands(object): + + """ + General commands for the BitmaskBackend Core Service. + """ + + def __init__(self, core): + self.core = core + + def do_status(self): + # we may want to make this tuple a class member + services = ('soledad', 'keymanager', 'mail', 'eip') + + status = {} + for name in services: + _status = 'stopped' + try: + if self.core.getServiceNamed(name).running: + _status = 'running' + except KeyError: + pass + status[name] = _status + status['backend'] = flags.BACKEND + + return json.dumps(status) + + def do_version(self): + return {'version_core': __version__} + + def do_stats(self): + log.msg('BitmaskCore Service STATS') + mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + return {'mem_usage': '%s KB' % (mem / 1024)} + + def do_shutdown(self): + self.core.stopService() + reactor.callLater(1, reactor.stop) + return {'shutdown': 'ok'} diff --git a/src/leap/bitmask/core/uuid_map.py b/src/leap/bitmask/core/uuid_map.py new file mode 100644 index 00000000..5edc7216 --- /dev/null +++ b/src/leap/bitmask/core/uuid_map.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# uuid_map.py +# Copyright (C) 2015,2016 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +UUID Map: a persistent mapping between user-ids and uuids. +""" + +import base64 +import os +import re + +import scrypt + +from leap.common.config import get_path_prefix + + +MAP_PATH = os.path.join(get_path_prefix(), 'leap', 'uuids') + + +class UserMap(object): + + """ + A persistent mapping between user-ids and uuids. + """ + + # TODO Add padding to the encrypted string + + def __init__(self): + self._d = {} + self._lines = set([]) + if os.path.isfile(MAP_PATH): + self.load() + + def add(self, userid, uuid, passwd): + """ + Add a new userid-uuid mapping, and encrypt the record with the user + password. + """ + self._add_to_cache(userid, uuid) + self._lines.add(_encode_uuid_map(userid, uuid, passwd)) + self.dump() + + def _add_to_cache(self, userid, uuid): + self._d[userid] = uuid + + def load(self): + """ + Load a mapping from a default file. + """ + with open(MAP_PATH, 'r') as infile: + lines = infile.readlines() + self._lines = set(lines) + + def dump(self): + """ + Dump the mapping to a default file. + """ + with open(MAP_PATH, 'w') as out: + out.write('\n'.join(self._lines)) + + def lookup_uuid(self, userid, passwd=None): + """ + Lookup the uuid for a given userid. + + If no password is given, try to lookup on cache. + Else, try to decrypt all the records that we know about with the + passed password. + """ + if not passwd: + return self._d.get(userid) + + for line in self._lines: + guess = _decode_uuid_line(line, passwd) + if guess: + record_userid, uuid = guess + if record_userid == userid: + self._add_to_cache(userid, uuid) + return uuid + + def lookup_userid(self, uuid): + """ + Get the userid for the given uuid from cache. + """ + rev_d = {v: k for (k, v) in self._d.items()} + return rev_d.get(uuid) + + +def _encode_uuid_map(userid, uuid, passwd): + data = 'userid:%s:uuid:%s' % (userid, uuid) + encrypted = scrypt.encrypt(data, passwd, maxtime=0.05) + return base64.encodestring(encrypted).replace('\n', '') + + +def _decode_uuid_line(line, passwd): + decoded = base64.decodestring(line) + try: + maybe_decrypted = scrypt.decrypt(decoded, passwd, maxtime=0.1) + except scrypt.error: + return None + match = re.findall("userid\:(.+)\:uuid\:(.+)", maybe_decrypted) + if match: + return match[0] diff --git a/src/leap/bitmask/core/web/__init__.py b/src/leap/bitmask/core/web/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/leap/bitmask/core/web/__init__.py diff --git a/src/leap/bitmask/core/web/index.html b/src/leap/bitmask/core/web/index.html new file mode 100644 index 00000000..9490eca8 --- /dev/null +++ b/src/leap/bitmask/core/web/index.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<html> + <head> + <title>Bitmask WebSockets Endpoint</title> + <script type="text/javascript"> + var sock = null; + var ellog = null; + + window.onload = function() { + + ellog = document.getElementById('log'); + + var wsuri; + if (window.location.protocol === "file:") { + wsuri = "ws://127.0.0.1:8080/ws"; + } else { + wsuri = "ws://" + window.location.hostname + ":8080/bitmask"; + } + + if ("WebSocket" in window) { + sock = new WebSocket(wsuri); + } else if ("MozWebSocket" in window) { + sock = new MozWebSocket(wsuri); + } else { + log("Browser does not support WebSocket!"); + window.location = "http://autobahn.ws/unsupportedbrowser"; + } + + if (sock) { + sock.onopen = function() { + log("Connected to " + wsuri); + } + + sock.onclose = function(e) { + log("Connection closed (wasClean = " + e.wasClean + ", code = " + e.code + ", reason = '" + e.reason + "')"); + sock = null; + } + + sock.onmessage = function(e) { + log("[res] " + e.data + '\n'); + } + } + }; + + function send() { + var msg = document.getElementById('message').value; + if (sock) { + sock.send(msg); + log("[cmd] " + msg); + } else { + log("Not connected."); + } + }; + + function log(m) { + ellog.innerHTML += m + '\n'; + ellog.scrollTop = ellog.scrollHeight; + }; + </script> + </head> + <body> + <h1>Bitmask Control Panel</h1> + <noscript>You must enable JavaScript</noscript> + <form> + <p>Command: <input id="message" type="text" size="50" maxlength="50" value="status"></p> + </form> + <button onclick='send();'>Send Command</button> + <pre id="log" style="height: 20em; overflow-y: scroll; background-color: #faa;"></pre> + </body> +</html> diff --git a/src/leap/bitmask/core/web/root.py b/src/leap/bitmask/core/web/root.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/leap/bitmask/core/web/root.py diff --git a/src/leap/bitmask/core/websocket.py b/src/leap/bitmask/core/websocket.py new file mode 100644 index 00000000..5569c6c7 --- /dev/null +++ b/src/leap/bitmask/core/websocket.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# websocket.py +# Copyright (C) 2015, 2016 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +WebSockets Dispatcher Service. +""" + +import os +import pkg_resources + +from twisted.internet import reactor +from twisted.application import service + +from twisted.web.server import Site +from twisted.web.static import File + +from autobahn.twisted.resource import WebSocketResource +from autobahn.twisted.websocket import WebSocketServerFactory +from autobahn.twisted.websocket import WebSocketServerProtocol + +from leap.bitmask.core.dispatcher import CommandDispatcher + + +class WebSocketsDispatcherService(service.Service): + + """ + A Dispatcher for BitmaskCore exposing a WebSockets Endpoint. + """ + + def __init__(self, core, port=8080, debug=False): + self._core = core + self.port = port + self.debug = debug + + def startService(self): + + factory = WebSocketServerFactory(u"ws://127.0.0.1:%d" % self.port, + debug=self.debug) + factory.protocol = DispatcherProtocol + factory.protocol.dispatcher = CommandDispatcher(self._core) + + # FIXME: Site.start/stopFactory should start/stop factories wrapped as + # Resources + factory.startFactory() + + resource = WebSocketResource(factory) + + # we server static files under "/" .. + webdir = os.path.abspath( + pkg_resources.resource_filename("leap.bitmask.core", "web")) + root = File(webdir) + + # and our WebSocket server under "/ws" + root.putChild(u"bitmask", resource) + + # both under one Twisted Web Site + site = Site(root) + + self.site = site + self.factory = factory + + self.listener = reactor.listenTCP(self.port, site) + + def stopService(self): + self.factory.stopFactory() + self.site.stopFactory() + self.listener.stopListening() + + +class DispatcherProtocol(WebSocketServerProtocol): + + def onMessage(self, msg, binary): + parts = msg.split() + r = self.dispatcher.dispatch(parts) + r.addCallback(self.defer_reply, binary) + + def reply(self, response, binary): + self.sendMessage(response, binary) + + def defer_reply(self, response, binary): + reactor.callLater(0, self.reply, response, binary) + + def _get_service(self, name): + return self.core.getServiceNamed(name) diff --git a/src/leap/bitmask/gui/account.py b/src/leap/bitmask/gui/account.py index 81f96389..b8b9509a 100644 --- a/src/leap/bitmask/gui/account.py +++ b/src/leap/bitmask/gui/account.py @@ -20,6 +20,7 @@ A frontend GUI object to hold the current username and domain. from leap.bitmask.util import make_address from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.services import EIP_SERVICE, MX_SERVICE +from leap.bitmask._components import HAS_EIP, HAS_MAIL class Account(): @@ -42,8 +43,8 @@ class Account(): """ return self._settings.get_enabled_services(self.domain) - def is_email_enabled(self): - return MX_SERVICE in self.services() + def has_email(self): + return HAS_MAIL and MX_SERVICE in self.services() - def is_eip_enabled(self): - return EIP_SERVICE in self.services() + def has_eip(self): + return HAS_EIP and EIP_SERVICE in self.services() diff --git a/src/leap/bitmask/gui/advanced_key_management.py b/src/leap/bitmask/gui/advanced_key_management.py index 2e315d18..bc496a57 100644 --- a/src/leap/bitmask/gui/advanced_key_management.py +++ b/src/leap/bitmask/gui/advanced_key_management.py @@ -94,6 +94,7 @@ class AdvancedKeyManagement(QtGui.QDialog): """ Set the current user's key details into the gui. """ + # XXX: We should avoid the key-id self.ui.leKeyID.setText(details[0]) self.ui.leFingerprint.setText(details[1]) @@ -246,7 +247,7 @@ class AdvancedKeyManagement(QtGui.QDialog): row = keys_table.rowCount() keys_table.insertRow(row) keys_table.setItem(row, 0, QtGui.QTableWidgetItem(key.address)) - keys_table.setItem(row, 1, QtGui.QTableWidgetItem(key.key_id)) + keys_table.setItem(row, 1, QtGui.QTableWidgetItem(key.fingerprint)) def _backend_connect(self): """ diff --git a/src/leap/bitmask/gui/app.py b/src/leap/bitmask/gui/app.py index 97fd0549..1011454e 100644 --- a/src/leap/bitmask/gui/app.py +++ b/src/leap/bitmask/gui/app.py @@ -20,6 +20,7 @@ and the signaler get signals from the backend. """ from PySide import QtCore, QtGui +from leap.bitmask.gui.account import Account from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.backend.backend_proxy import BackendProxy from leap.bitmask.backend.leapsignaler import LeapSignaler @@ -44,12 +45,37 @@ class App(QtGui.QWidget): self.signaler.start() self.soledad_started = False + self.service_tokens = {} + self.login_state = None + self.providers_widget = None # periodically check if the backend is alive self._backend_checker = QtCore.QTimer(self) self._backend_checker.timeout.connect(self._check_backend_status) self._backend_checker.start(2000) + # store the service tokens for later use, once they are known. + self.signaler.soledad_got_service_token.connect( + self._set_service_tokens) + + def current_account(self): + """ + Alas, the only definitive account information is buried in the memory + of QT widgets. + + :returns: an object representing the current user account. + :rtype: Account + """ + if self.login_state is None or self.providers_widget is None: + return None + + if self.login_state.full_logged_username is not None: + username, domain = self.login_state.full_logged_username.split('@') + return Account(username, domain) + else: + domain = self.providers_widget.get_selected_provider() + return Account(None, domain) + def _check_backend_status(self): """ TRIGGERS: @@ -64,3 +90,11 @@ class App(QtGui.QWidget): self.tr("There is a problem contacting the backend, please " "restart Bitmask.")) self._backend_checker.stop() + + def _set_service_tokens(self, data): + """ + Triggered by signal soledad_got_service_token. + Saves the service tokens. + """ + service, token = data + self.service_tokens[service] = token diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py index 64a408c4..470ef88a 100644 --- a/src/leap/bitmask/gui/eip_status.py +++ b/src/leap/bitmask/gui/eip_status.py @@ -733,7 +733,7 @@ class EIPStatusWidget(QtGui.QWidget): self.set_eip_status( # XXX this should change to polkit-kde where # applicable. - self.tr("We could not find any authentication agent in your " + self.tr("We could not find any authentication agent on your " "system.<br/>Make sure you have " "<b>polkit-gnome-authentication-agent-1</b> running and " "try again."), diff --git a/src/leap/bitmask/gui/logwindow.py b/src/leap/bitmask/gui/logwindow.py index 718269c9..5d8c99fc 100644 --- a/src/leap/bitmask/gui/logwindow.py +++ b/src/leap/bitmask/gui/logwindow.py @@ -173,7 +173,7 @@ class LoggerWindow(QtGui.QDialog): :type sending: bool """ if sending: - self.ui.btnPastebin.setText(self.tr("Sending to pastebin...")) + self.ui.btnPastebin.setText(self.tr("Sending to Pastebin.com…")) self.ui.btnPastebin.setEnabled(False) else: self.ui.btnPastebin.setText(self.tr("Send to Pastebin.com")) @@ -193,7 +193,7 @@ class LoggerWindow(QtGui.QDialog): # We save the dialog in an instance member to avoid dialog being # deleted right after we exit this method self._msgBox = msgBox = QtGui.QMessageBox( - QtGui.QMessageBox.Information, self.tr("Pastebin OK"), msg) + QtGui.QMessageBox.Information, self.tr("Pastebin is OK"), msg) msgBox.setWindowModality(QtCore.Qt.NonModal) msgBox.show() diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py index 8b4329d7..cb0314b5 100644 --- a/src/leap/bitmask/gui/mail_status.py +++ b/src/leap/bitmask/gui/mail_status.py @@ -26,7 +26,9 @@ from leap.common.check import leap_assert, leap_assert_type from leap.common.events import register from leap.common.events import catalog +from leap.bitmask.gui.preferenceswindow import PreferencesWindow from ui_mail_status import Ui_MailStatusWidget +from .qt_browser import PixelatedWindow logger = get_logger() @@ -52,13 +54,21 @@ class MailStatusWidget(QtGui.QWidget): self._systray = None self._disabled = True self._started = False + self._mainwindow = parent self._unread_mails = 0 self.ui = Ui_MailStatusWidget() self.ui.setupUi(self) - self.ui.lblMailReadyHelp.setVisible(False) + self.ui.email_ready.setVisible(False) + self.ui.configure_button.clicked.connect( + self._show_configure) + self.ui.open_mail_button.clicked.connect( + self._show_pix_ua) + if not self._mainwindow._settings.get_pixelmail_enabled(): + self.ui.open_mail_button.setVisible(False) + self.ui.or_label.setVisible(False) # set systray tooltip status self._mx_status = "" @@ -144,7 +154,23 @@ class MailStatusWidget(QtGui.QWidget): self.CONNECTED_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[1]) self.ERROR_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[2]) - # Systray and actions + # + # Button actions + # + + def _show_configure(self): + pref_win = PreferencesWindow(self._mainwindow, self._mainwindow.app) + pref_win.set_page("email") + pref_win.show() + + def _show_pix_ua(self): + win = PixelatedWindow(self._mainwindow) + win.show() + win.load_app() + + # + # Systray + # def set_systray(self, systray): """ @@ -166,6 +192,10 @@ class MailStatusWidget(QtGui.QWidget): mx_status = u"{0}: {1}".format(self._service_name, self._mx_status) self._systray.set_service_tooltip(MX_SERVICE, mx_status) + # + # Status + # + def set_action_mail_status(self, action_mail_status): """ Sets the action_mail_status to use. @@ -186,7 +216,7 @@ class MailStatusWidget(QtGui.QWidget): msg = self.tr("There was an unexpected problem with Soledad.") self._set_mail_status(msg, ready=-1) - def set_soledad_invalid_auth_token(self, event, content): + def set_soledad_invalid_auth_token(self, event, content=None): """ This method is called when the auth token is invalid @@ -229,16 +259,22 @@ class MailStatusWidget(QtGui.QWidget): elif ready < 0: tray_status = self.tr("Mail is disabled") + if ready < 1: + self._hide_mail_ready() + self.ui.lblMailStatusIcon.setPixmap(icon) self._action_mail_status.setText(tray_status) self._update_systray_tooltip() - def _mail_handle_soledad_events(self, event, content): + def _mail_handle_soledad_events(self, event, user_data, content=""): """ Callback for handling events that are emitted from Soledad :param event: The event that triggered the callback. :type event: str + :param user_id: The user_data of the soledad user. Ignored right now, + since we're only contemplating single-user in soledad. + :type user_id: dict :param content: The content of the event. :type content: dict """ @@ -346,7 +382,7 @@ class MailStatusWidget(QtGui.QWidget): logger.warning("don't know to to handle %s" % (event,)) self._set_mail_status(ext_status, ready=1) - def _mail_handle_smtp_events(self, event): + def _mail_handle_smtp_events(self, event, content=""): """ Callback for the SMTP events @@ -380,12 +416,14 @@ class MailStatusWidget(QtGui.QWidget): # ----- XXX deprecate (move to mail conductor) - def _mail_handle_imap_events(self, event, content): + def _mail_handle_imap_events(self, event, uuid, content=""): """ Callback for the IMAP events :param event: The event that triggered the callback. :type event: str + :param uuid: The UUID for the user. Ignored right now. + :type uuid: str :param content: The content of the event. :type content: list """ @@ -419,9 +457,10 @@ class MailStatusWidget(QtGui.QWidget): self._show_unread_mails() elif event == catalog.IMAP_SERVICE_STARTED: self._imap_started = True - elif event == catalog.IMAP_CLIENT_LOGIN: - # If a MUA has logged in then we don't need to show this. - self._hide_mail_ready_help() + # this is disabled for now, because this event was being + # triggered at weird times. + # elif event == catalog.IMAP_CLIENT_LOGIN: + # self._hide_mail_ready() if ext_status is not None: self._set_mail_status(ext_status, ready=1) @@ -490,15 +529,13 @@ class MailStatusWidget(QtGui.QWidget): Display the correct UI for the connected state. """ self._set_mail_status(self.tr("ON"), 2) + self.ui.email_ready.setVisible(True) - # this help message will hide when the MUA connects - self.ui.lblMailReadyHelp.setVisible(True) - - def _hide_mail_ready_help(self): + def _hide_mail_ready(self): """ Hide the mail help message on the UI. """ - self.ui.lblMailReadyHelp.setVisible(False) + self.ui.email_ready.setVisible(False) def mail_state_disabled(self): """ diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index a8a4e41d..6637f170 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -46,7 +46,6 @@ from leap.bitmask.gui.signaltracker import SignalTracker from leap.bitmask.gui.systray import SysTray from leap.bitmask.gui.wizard import Wizard from leap.bitmask.gui.providers import Providers -from leap.bitmask.gui.account import Account from leap.bitmask.gui.app import App from leap.bitmask.platform_init import IS_WIN, IS_MAC, IS_LINUX @@ -152,6 +151,14 @@ class MainWindow(QtGui.QMainWindow, SignalTracker): # Provider List self._providers = Providers(self.ui.cmbProviders) + ## + # tmphack: important state information about the application is stored + # in widgets. Rather than rewrite the UI, for now we simulate this + # info being stored in an application object: + ## + self.app.login_state = self._login_widget._state + self.app.providers_widget = self._providers + # Qt Signal Connections ##################################### # TODO separate logic from ui signals. @@ -218,6 +225,7 @@ class MainWindow(QtGui.QMainWindow, SignalTracker): self._backend_connect() self.ui.action_preferences.triggered.connect(self._show_preferences) + self.ui.action_about_leap.triggered.connect(self._about) self.ui.action_quit.triggered.connect(self.quit) self.ui.action_wizard.triggered.connect(self._show_wizard) @@ -294,6 +302,7 @@ class MainWindow(QtGui.QMainWindow, SignalTracker): self._mail_conductor.connect_mail_signals(self._mail_status) if not init_platform(): + logger.critical('init_platform failed, quitting application.') self.quit() return @@ -436,6 +445,9 @@ class MainWindow(QtGui.QMainWindow, SignalTracker): # Refer to http://www.themacaque.com/?p=1067 for funny details. self._wizard.show() if IS_MAC: + # XXX hack. For some reason, there's a signal that doesn't arrive + # on time, so that the next button is disabled. See #8041 + self._wizard.page(self._wizard.INTRO_PAGE).set_completed() self._wizard.raise_() self._settings.set_skip_first_run(True) @@ -553,15 +565,7 @@ class MainWindow(QtGui.QMainWindow, SignalTracker): Display the preferences window. """ - logged_user = self._login_widget.get_logged_user() - if logged_user is not None: - user, domain = logged_user.split('@') - else: - user = None - domain = self._providers.get_selected_provider() - - account = Account(user, domain) - pref_win = PreferencesWindow(self, account, self.app) + pref_win = PreferencesWindow(self, self.app) pref_win.show() def _update_eip_enabled_status(self, account=None, services=None): @@ -1014,12 +1018,9 @@ class MainWindow(QtGui.QMainWindow, SignalTracker): Display the About Bitmask dialog """ today = datetime.now().date() - greet = ("Happy New 1984!... or not ;)<br><br>" - if today.month == 1 and today.day < 15 else "") title = self.tr("About Bitmask - %s") % (VERSION,) msg = self.tr( "Version: <b>{ver}</b> ({ver_hash})<br>" - "<br>{greet}" "Bitmask is the Desktop client application for the LEAP " "platform, supporting Encrypted Internet Proxy " "and <a href='https://bitmask.net/help/email'> " @@ -1030,7 +1031,7 @@ class MainWindow(QtGui.QMainWindow, SignalTracker): "available.<br>" "<br>" "<a href='https://leap.se'>More about LEAP</a>") - msg = msg.format(ver=VERSION, ver_hash=VERSION_HASH[:10], greet=greet) + msg = msg.format(ver=VERSION, ver_hash=VERSION_HASH[:10]) QtGui.QMessageBox.about(self, title, msg) def _help(self): @@ -1038,46 +1039,9 @@ class MainWindow(QtGui.QMainWindow, SignalTracker): TRIGGERS: self.ui.action_help.triggered - Display the Bitmask help dialog. - """ - # TODO: don't hardcode! - smtp_port = 2013 - - help_url = "<p><a href='https://{0}'>{0}</a></p>".format( - self.tr("bitmask.net/help")) - - lang = QtCore.QLocale.system().name().replace('_', '-') - thunderbird_extension_url = \ - "https://addons.mozilla.org/{0}/" \ - "thunderbird/addon/bitmask/".format(lang) - - email_quick_reference = self.tr("Email quick reference") - thunderbird_text = self.tr( - "For Thunderbird, you can use the " - "Bitmask extension. Search for \"Bitmask\" in the add-on " - "manager or download it from <a href='{0}'>" - "addons.mozilla.org</a>.".format(thunderbird_extension_url)) - manual_text = self.tr( - "Alternatively, you can manually configure " - "your mail client to use Bitmask Email with these options:") - manual_imap = self.tr("IMAP: localhost, port {0}".format(IMAP_PORT)) - manual_smtp = self.tr("SMTP: localhost, port {0}".format(smtp_port)) - manual_username = self.tr("Username: your full email address") - manual_password = self.tr("Password: any non-empty text") - - msg = help_url + self.tr( - "<p><strong>{0}</strong></p>" - "<p>{1}</p>" - "<p>{2}" - "<ul>" - "<li> {3}</li>" - "<li> {4}</li>" - "<li> {5}</li>" - "<li> {6}</li>" - "</ul></p>").format(email_quick_reference, thunderbird_text, - manual_text, manual_imap, manual_smtp, - manual_username, manual_password) - QtGui.QMessageBox.about(self, self.tr("Bitmask Help"), msg) + Open bitmask.net/help + """ + QtGui.QDesktopServices.openUrl("https://bitmask.net/help") def _needs_update(self): """ diff --git a/src/leap/bitmask/gui/passwordwindow.py b/src/leap/bitmask/gui/passwordwindow.py index dedfcb10..fe70b250 100644 --- a/src/leap/bitmask/gui/passwordwindow.py +++ b/src/leap/bitmask/gui/passwordwindow.py @@ -83,7 +83,7 @@ class PasswordWindow(QtGui.QDialog, Flashable): Returns true if the current account needs to change the soledad password as well as the SRP password. """ - return self.account.is_email_enabled() + return self.account.has_email() # # MANAGE WIDGETS diff --git a/src/leap/bitmask/gui/preferences_account_page.py b/src/leap/bitmask/gui/preferences_account_page.py index da9da14d..141523c8 100644 --- a/src/leap/bitmask/gui/preferences_account_page.py +++ b/src/leap/bitmask/gui/preferences_account_page.py @@ -22,6 +22,7 @@ from PySide import QtCore, QtGui from leap.bitmask.logs.utils import get_logger from leap.bitmask.gui import ui_preferences_account_page as ui_pref +from leap.bitmask.gui.preferences_page import PreferencesPage from leap.bitmask.gui.passwordwindow import PasswordWindow from leap.bitmask.services import get_service_display_name from leap.bitmask._components import HAS_EIP @@ -29,7 +30,7 @@ from leap.bitmask._components import HAS_EIP logger = get_logger() -class PreferencesAccountPage(QtGui.QWidget): +class PreferencesAccountPage(PreferencesPage): def __init__(self, parent, account, app): """ @@ -42,20 +43,15 @@ class PreferencesAccountPage(QtGui.QWidget): :param app: the current App object :type app: App """ - QtGui.QWidget.__init__(self, parent) + PreferencesPage.__init__(self, parent, account, app) self.ui = ui_pref.Ui_PreferencesAccountPage() self.ui.setupUi(self) - self.account = account - self.app = app - self._selected_services = set() self.ui.change_password_label.setVisible(False) self.ui.provider_services_label.setVisible(False) - self.ui.change_password_button.clicked.connect( - self._show_change_password) - app.signaler.prov_get_supported_services.connect(self._load_services) + self.setup_connections() app.backend.provider_get_supported_services(domain=account.domain) if account.username is None: @@ -64,6 +60,24 @@ class PreferencesAccountPage(QtGui.QWidget): self.ui.change_password_label.setVisible(True) self.ui.change_password_button.setEnabled(False) + def setup_connections(self): + """ + connect signals + """ + self.ui.change_password_button.clicked.connect( + self._show_change_password) + self.app.signaler.prov_get_supported_services.connect( + self._load_services) + + def teardown_connections(self): + """ + disconnect signals + """ + self.ui.change_password_button.clicked.disconnect( + self._show_change_password) + self.app.signaler.prov_get_supported_services.disconnect( + self._load_services) + def _service_selection_changed(self, service, state): """ TRIGGERS: diff --git a/src/leap/bitmask/gui/preferences_email_page.py b/src/leap/bitmask/gui/preferences_email_page.py index 3087f343..93c77df1 100644 --- a/src/leap/bitmask/gui/preferences_email_page.py +++ b/src/leap/bitmask/gui/preferences_email_page.py @@ -16,20 +16,214 @@ """ Widget for "email" preferences """ -from PySide import QtGui +from PySide import QtCore, QtGui +from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.logs.utils import get_logger from leap.bitmask.gui.ui_preferences_email_page import Ui_PreferencesEmailPage +from leap.bitmask.gui.preferences_page import PreferencesPage +from leap.bitmask.pix import HAS_PIXELATED +from leap.mail.imap.service.imap import IMAP_PORT + logger = get_logger() -class PreferencesEmailPage(QtGui.QWidget): +class PreferencesEmailPage(PreferencesPage): def __init__(self, parent, account, app): - QtGui.QWidget.__init__(self, parent) + """ + :param parent: parent object of the PreferencesWindow. + :parent type: QWidget + + :param account: user account (user + provider or just provider) + :type account: Account + + :param app: the current App object + :type app: App + """ + PreferencesPage.__init__(self, parent, account, app) + self.settings = LeapSettings() self.ui = Ui_PreferencesEmailPage() self.ui.setupUi(self) - self.account = account - self.app = app + # the only way to set the tab titles is to re-add them: + self.ui.email_tabs.addTab(self.ui.config_tab, + self.tr("Mail Client")) + self.ui.email_tabs.addTab(self.ui.my_key_tab, + self.tr("My Key")) + self.ui.email_tabs.addTab(self.ui.other_keys_tab, + self.tr("Other Keys")) + + # set mail client configuration help text + lang = QtCore.QLocale.system().name().replace('_', '-') + thunderbird_extension_url = \ + "https://addons.mozilla.org/{0}/" \ + "thunderbird/addon/bitmask/".format(lang) + self.ui.thunderbird_label.setText(self.tr( + "For Thunderbird, you can use the Bitmask extension. " + "Search for \"Bitmask\" in the add-on manager or " + "download it from <a href='{0}'>addons.mozilla.org</a>.".format( + thunderbird_extension_url))) + + self.ui.mail_client_label.setText(self.tr( + "Alternatively, you can manually configure your mail client to " + "use Bitmask with these options:")) + + self.ui.webmail_label.setText(self.tr( + "Bitmask Mail is an integrated mail client based " + "on <a href='https://pixelated-project.org/'>Pixelated " + "User Agent</a>. If enabled, any user on your device " + "can read your mail by opening http://localhost:9090")) + + self.ui.keys_table.horizontalHeader().setResizeMode( + 0, QtGui.QHeaderView.Stretch) + + self.setup_connections() + + def setup_connections(self): + """ + connect signals + """ + self.app.signaler.keymanager_key_details.connect(self._key_details) + self.app.signaler.keymanager_keys_list.connect( + self._keymanager_keys_list) + self.app.signaler.keymanager_export_ok.connect( + self._keymanager_export_ok) + self.app.signaler.keymanager_export_error.connect( + self._keymanager_export_error) + self.ui.import_button.clicked.connect(self._import_keys) + self.ui.export_button.clicked.connect(self._export_keys) + self.ui.webmail_checkbox.stateChanged.connect(self._toggle_webmail) + + def teardown_connections(self): + """ + disconnect signals + """ + self.app.signaler.keymanager_key_details.disconnect(self._key_details) + self.app.signaler.keymanager_export_ok.disconnect( + self._keymanager_export_ok) + self.app.signaler.keymanager_export_error.disconnect( + self._keymanager_export_error) + + def showEvent(self, event): + """ + called whenever this widget is shown + """ + self.ui.keys_table.clearContents() + + if self.account.username is None: + self.ui.email_tabs.setVisible(False) + self.ui.message_label.setVisible(True) + self.ui.message_label.setText( + self.tr('You must be logged in to edit email settings.')) + else: + webmail_enabled = self.settings.get_pixelmail_enabled() + self.ui.webmail_checkbox.setChecked(webmail_enabled) + if not HAS_PIXELATED: + self.ui.webmail_box.setVisible(False) + self.ui.import_button.setVisible(False) # hide this until working + self.ui.message_label.setVisible(False) + self.ui.email_tabs.setVisible(True) + smtp_port = 2013 + self.ui.imap_port_edit.setText(str(IMAP_PORT)) + self.ui.imap_host_edit.setText("127.0.0.1") + self.ui.smtp_port_edit.setText(str(smtp_port)) + self.ui.smtp_host_edit.setText("127.0.0.1") + self.ui.username_edit.setText(self.account.address) + self.ui.password_edit.setText( + self.app.service_tokens.get('mail_auth', '')) + + self.app.backend.keymanager_list_keys() + self.app.backend.keymanager_get_key_details( + username=self.account.address) + + def _key_details(self, details): + """ + Trigger by signal: keymanager_key_details + Set the current user's key details into the gui. + """ + self.ui.fingerprint_edit.setPlainText( + self._format_fingerprint(details["fingerprint"])) + self.ui.expiration_edit.setText(details["expiry_date"]) + self.ui.uid_edit.setText(" ".join(details["uids"])) + self.ui.public_key_edit.setPlainText(details["key_data"]) + + def _format_fingerprint(self, fingerprint): + """ + formats an openpgp fingerprint in a manner similar to what gpg + produces, wrapped to two lines. + """ + fp = fingerprint.upper() + fp_list = [fp[i:i + 4] for i in range(0, len(fp), 4)] + fp_wrapped = " ".join(fp_list[0:5]) + "\n" + " ".join(fp_list[5:10]) + return fp_wrapped + + def _export_keys(self): + """ + Exports the user's key pair. + """ + file_name, filtr = QtGui.QFileDialog.getSaveFileName( + self, self.tr("Save private key file"), + filter="*.pem", + options=QtGui.QFileDialog.DontUseNativeDialog) + + if file_name: + if not file_name.endswith('.pem'): + file_name += '.pem' + self.app.backend.keymanager_export_keys( + username=self.account.address, + filename=file_name) + else: + logger.debug('Export canceled by the user.') + + def _keymanager_export_ok(self): + """ + TRIGGERS: + Signaler.keymanager_export_ok + + Notify the user that the key export went OK. + """ + QtGui.QMessageBox.information( + self, self.tr("Export Successful"), + self.tr("The key pair was exported successfully.\n" + "Please, store your private key in a safe place.")) + + def _keymanager_export_error(self): + """ + TRIGGERS: + Signaler.keymanager_export_error + + Notify the user that the key export didn't go well. + """ + QtGui.QMessageBox.critical( + self, self.tr("Input/Output error"), + self.tr("There was an error accessing the file.\n" + "Export canceled.")) + + def _import_keys(self): + """ + not yet supported + """ + + def _keymanager_keys_list(self, keys): + """ + TRIGGERS: + Signaler.keymanager_keys_list + + Load the keys given as parameter in the table. + + :param keys: the list of keys to load. + :type keys: list + """ + for key in keys: + row = self.ui.keys_table.rowCount() + self.ui.keys_table.insertRow(row) + self.ui.keys_table.setItem( + row, 0, QtGui.QTableWidgetItem(" ".join(key["uids"]))) + self.ui.keys_table.setItem( + row, 1, QtGui.QTableWidgetItem(key["fingerprint"])) + + def _toggle_webmail(self, state): + value = True if state == QtCore.Qt.Checked else False + self.settings.set_pixelmail_enabled(value) diff --git a/src/leap/bitmask/gui/preferences_page.py b/src/leap/bitmask/gui/preferences_page.py new file mode 100644 index 00000000..a5d811f9 --- /dev/null +++ b/src/leap/bitmask/gui/preferences_page.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Base class for preference pages +""" + +from PySide import QtGui + + +class PreferencesPage(QtGui.QWidget): + + def __init__(self, parent, account=None, app=None): + """ + :param parent: parent object of the EIPPreferencesWindow. + :type parent: QWidget + + :param account: the currently active account + :type account: Account + + :param app: shared App instance + :type app: App + """ + QtGui.QWidget.__init__(self, parent) + self.app = app + self.account = account + + def setup_connections(self): + """ + connect signals + must be overridden by subclass + """ + + def teardown_connections(self): + """ + disconnect signals + must be overridden by subclass + """ diff --git a/src/leap/bitmask/gui/preferences_vpn_page.py b/src/leap/bitmask/gui/preferences_vpn_page.py index 5b5c9604..87b86c4e 100644 --- a/src/leap/bitmask/gui/preferences_vpn_page.py +++ b/src/leap/bitmask/gui/preferences_vpn_page.py @@ -20,11 +20,11 @@ Widget for "vpn" preferences from PySide import QtCore, QtGui from leap.bitmask.gui.ui_preferences_vpn_page import Ui_PreferencesVpnPage -from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.gui.flashable import Flashable +from leap.bitmask.gui.preferences_page import PreferencesPage -class PreferencesVpnPage(QtGui.QWidget, Flashable): +class PreferencesVpnPage(PreferencesPage, Flashable): """ Page in the preferences window that shows VPN settings @@ -41,19 +41,24 @@ class PreferencesVpnPage(QtGui.QWidget, Flashable): :param app: shared App instance :type app: App """ - QtGui.QWidget.__init__(self, parent) + PreferencesPage.__init__(self, parent, account, app) self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") - self.account = account - self.app = app - # Load UI self.ui = Ui_PreferencesVpnPage() self.ui.setupUi(self) self.ui.flash_label.setVisible(False) self.hide_flash() - # Connections + self.setup_connections() + + # Trigger update + self.app.backend.eip_get_gateways_list(domain=self.account.domain) + + def setup_connections(self): + """ + connect signals + """ self.ui.gateways_list.clicked.connect(self._save_selected_gateway) sig = self.app.signaler sig.eip_get_gateways_list.connect(self._update_gateways_list) @@ -61,8 +66,16 @@ class PreferencesVpnPage(QtGui.QWidget, Flashable): sig.eip_uninitialized_provider.connect( self._gateways_list_uninitialized) - # Trigger update - self.app.backend.eip_get_gateways_list(domain=self.account.domain) + def teardown_connections(self): + """ + disconnect signals + """ + self.ui.gateways_list.clicked.disconnect(self._save_selected_gateway) + sig = self.app.signaler + sig.eip_get_gateways_list.disconnect(self._update_gateways_list) + sig.eip_get_gateways_list_error.disconnect(self._gateways_list_error) + sig.eip_uninitialized_provider.disconnect( + self._gateways_list_uninitialized) def _save_selected_gateway(self, index): """ diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index 44c4641c..50a972e1 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -20,11 +20,9 @@ Preferences window """ from PySide import QtCore, QtGui -from leap.bitmask.services import EIP_SERVICE -from leap.bitmask._components import HAS_EIP - from leap.bitmask.logs.utils import get_logger from leap.bitmask.gui.ui_preferences import Ui_Preferences +from leap.bitmask.gui.preferences_page import PreferencesPage from leap.bitmask.gui.preferences_account_page import PreferencesAccountPage from leap.bitmask.gui.preferences_vpn_page import PreferencesVpnPage from leap.bitmask.gui.preferences_email_page import PreferencesEmailPage @@ -40,39 +38,52 @@ class PreferencesWindow(QtGui.QDialog): _current_window = None # currently visible preferences window - def __init__(self, parent, account, app): + _panels = { + "account": 0, + "vpn": 1, + "email": 2 + } + + def __init__(self, parent, app): """ :param parent: parent object of the PreferencesWindow. :parent type: QWidget - :param account: the user or provider - :type account: Account - :param app: the current App object :type app: App """ QtGui.QDialog.__init__(self, parent) - self.account = account self.app = app self.ui = Ui_Preferences() self.ui.setupUi(self) - self.ui.close_button.clicked.connect(self.close) - self.ui.account_label.setText(account.address) - - self.app.service_selection_changed.connect(self._update_icons) + self._account_page = None + self._vpn_page = None + self._email_page = None self._add_icons() - self._add_pages() - self._update_icons(self.account, self.account.services()) + self._set_account(app.current_account()) + self._setup_connections() # only allow a single preferences window at a time. if PreferencesWindow._current_window is not None: PreferencesWindow._current_window.close() PreferencesWindow._current_window = self + def _set_account(self, account): + """ + Initially sets, or resets, the currently viewed account. + The account might not represent an authenticated user, but + just a domain. + """ + self.ui.account_label.setText(account.address) + self._add_pages(account) + self._update_icons(account) + self.ui.pages_widget.setCurrentIndex(0) + self.ui.nav_widget.setCurrentRow(0) + def _add_icons(self): """ Adds all the icons for the different configuration categories. @@ -114,22 +125,71 @@ class PreferencesWindow(QtGui.QDialog): email_item.setSizeHint(QtCore.QSize(98, 56)) self._email_item = email_item - self.ui.nav_widget.currentItemChanged.connect(self._change_page) - self.ui.nav_widget.setCurrentRow(0) - - def _add_pages(self): + def _add_pages(self, account): """ Adds the pages for the different configuration categories. """ - self._account_page = PreferencesAccountPage( - self, self.account, self.app) - self._vpn_page = PreferencesVpnPage(self, self.account, self.app) - self._email_page = PreferencesEmailPage(self, self.account, self.app) - + self._remove_pages() # in case different account was loaded. + + # load placeholder widgets if the page should not be loaded. + # the order of the pages is important, and must match the order + # of the nav_widget icons. + self._account_page = PreferencesAccountPage(self, account, self.app) + if account.has_eip(): + self._vpn_page = PreferencesVpnPage(self, account, self.app) + else: + self._vpn_page = PreferencesPage(self) + if account.has_email(): + self._email_page = PreferencesEmailPage(self, account, self.app) + else: + self._email_page = PreferencesPage(self) self.ui.pages_widget.addWidget(self._account_page) self.ui.pages_widget.addWidget(self._vpn_page) self.ui.pages_widget.addWidget(self._email_page) + def _remove_pages(self): + # deleteLater does not seem to cascade to items in stackLayout + # (even with QtCore.Qt.WA_DeleteOnClose attribute). + # so, here we call deleteLater() explicitly. + if self._account_page is not None: + self.ui.pages_widget.removeWidget(self._account_page) + self._account_page.teardown_connections() + self._account_page.deleteLater() + if self._vpn_page is not None: + self.ui.pages_widget.removeWidget(self._vpn_page) + self._vpn_page.teardown_connections() + self._vpn_page.deleteLater() + if self._email_page is not None: + self.ui.pages_widget.removeWidget(self._email_page) + self._email_page.teardown_connections() + self._email_page.deleteLater() + + def _setup_connections(self): + """ + setup signal connections + """ + self.ui.nav_widget.currentItemChanged.connect(self._change_page) + self.ui.close_button.clicked.connect(self.close) + self.app.service_selection_changed.connect(self._update_icons) + sig = self.app.signaler + sig.srp_auth_ok.connect(self._login_status_changed) + sig.srp_logout_ok.connect(self._login_status_changed) + sig.srp_status_logged_in.connect(self._update_account) + sig.srp_status_not_logged_in.connect(self._update_account) + + def _teardown_connections(self): + """ + clean up signal connections + """ + self.ui.nav_widget.currentItemChanged.disconnect(self._change_page) + self.ui.close_button.clicked.disconnect(self.close) + self.app.service_selection_changed.disconnect(self._update_icons) + sig = self.app.signaler + sig.srp_auth_ok.disconnect(self._login_status_changed) + sig.srp_logout_ok.disconnect(self._login_status_changed) + sig.srp_status_logged_in.disconnect(self._update_account) + sig.srp_status_not_logged_in.disconnect(self._update_account) + # # Slots # @@ -144,13 +204,8 @@ class PreferencesWindow(QtGui.QDialog): Close this dialog and destroy it. """ PreferencesWindow._current_window = None - - # deleteLater does not seem to cascade to items in stackLayout - # (even with QtCore.Qt.WA_DeleteOnClose attribute). - # so, here we call deleteLater() explicitly: - self._account_page.deleteLater() - self._vpn_page.deleteLater() - self._email_page.deleteLater() + self._teardown_connections() + self._remove_pages() self.deleteLater() def _change_page(self, current, previous): @@ -170,17 +225,32 @@ class PreferencesWindow(QtGui.QDialog): current = previous self.ui.pages_widget.setCurrentIndex(self.ui.nav_widget.row(current)) - def _update_icons(self, account, services): + def _update_icons(self, account): """ TRIGGERS: self.app.service_selection_changed Change which icons are visible. """ - if account != self.account: - return + self._vpn_item.setHidden(not account.has_eip()) + self._email_item.setHidden(not account.has_email()) - if HAS_EIP: - self._vpn_item.setHidden(EIP_SERVICE not in services) - # self._email_item.setHidden(not MX_SERVICE in services) - # ^^ disable email for now, there is nothing there yet. + def _login_status_changed(self): + """ + Triggered by signal srp_auth_ok, srp_logout_ok + """ + self.app.backend.user_get_logged_in_status() + + def _update_account(self): + """ + Triggered by get srp_status_logged_in, srp_status_not_logged_in + """ + self._set_account(self.app.current_account()) + + def set_page(self, page): + """ + Jump to a particular page + """ + index = PreferencesWindow._panels[page] + self.ui.nav_widget.setCurrentRow(index) + self.ui.pages_widget.setCurrentIndex(index) diff --git a/src/leap/bitmask/gui/qt_browser.py b/src/leap/bitmask/gui/qt_browser.py new file mode 100644 index 00000000..2d9e20e6 --- /dev/null +++ b/src/leap/bitmask/gui/qt_browser.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# qt_browser.py +# Copyright (C) 2016 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +QtWebKit-based browser to display Pixelated User Agent +""" +import os +import urlparse + +from PySide import QtCore, QtWebKit, QtGui, QtNetwork + +PIXELATED_URI = 'http://localhost:9090' + + +class PixelatedWindow(QtGui.QDialog): + + def __init__(self, parent): + super(PixelatedWindow, self).__init__(parent) + self.view = QtWebKit.QWebView(self) + + layout = QtGui.QGridLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.view) + self.setLayout(layout) + self.setWindowTitle('Bitmask Mail') + + # For the moment, we need to resize to a sensible default to avoid the + # "send" button to be out of view in the compose pane. This should be + # removed as soon as pixelated becomes size-responsive. + self.resize(800, 700) + + def load_app(self): + self.view.load(QtCore.QUrl(PIXELATED_URI)) + self.view.page().setForwardUnsupportedContent(True) + self.view.page().unsupportedContent.connect(self.download) + + self.manager = QtNetwork.QNetworkAccessManager() + self.manager.finished.connect(self.finished) + + def download(self, reply): + self.request = QtNetwork.QNetworkRequest(reply.url()) + self.reply = self.manager.get(self.request) + + def finished(self): + url = self.reply.url().toString() + + filename = urlparse.parse_qs(url).get('filename', None) + if filename: + filename = filename[0] + name = filename or url + + path = os.path.expanduser(os.path.join( + '~', unicode(name).split('/')[-1])) + destination = QtGui.QFileDialog.getSaveFileName(self, "Save", path) + if destination: + filename = destination[0] + with open(filename, 'wb') as f: + f.write(str(self.reply.readAll())) + f.close() diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py index ab48b756..92c5431d 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -40,6 +40,7 @@ class SignallingState(QState): """ A state that emits a custom signal on entry. """ + def __init__(self, signal, parent=None, name=None): """ Initializer. @@ -134,6 +135,7 @@ class States(object): class CompositeEvent(QtCore.QEvent): + def __init__(self): super(CompositeEvent, self).__init__( QtCore.QEvent.Type(self.ID)) @@ -174,6 +176,7 @@ class Events(QtCore.QObject): A Wrapper object for containing the events that will be posted to a composite state machine. """ + def __init__(self, parent=None): """ Initializes the QObject with the given parent. @@ -289,6 +292,7 @@ class ConnectionMachineBuilder(object): """ Builder class for state machines made from LEAPConnections. """ + def __init__(self, connection): """ :param connection: an instance of a concrete LEAPConnection @@ -352,7 +356,6 @@ class ConnectionMachineBuilder(object): :rtype: QStateMachine """ # TODO split this method in smaller utility functions. - parent = kwargs.get('parent', None) # 1. create machine machine = CompositeMachine() diff --git a/src/leap/bitmask/gui/ui/mail_status.ui b/src/leap/bitmask/gui/ui/mail_status.ui index 8e8f1848..f8ebb5a8 100644 --- a/src/leap/bitmask/gui/ui/mail_status.ui +++ b/src/leap/bitmask/gui/ui/mail_status.ui @@ -6,12 +6,12 @@ <rect> <x>0</x> <y>0</y> - <width>417</width> - <height>185</height> + <width>427</width> + <height>157</height> </rect> </property> <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> @@ -20,26 +20,131 @@ <string>Form</string> </property> <layout class="QGridLayout" name="gridLayout"> - <property name="topMargin"> - <number>0</number> - </property> + <item row="1" column="1"> + <widget class="QLabel" name="lblMailStatus"> + <property name="styleSheet"> + <string notr="true">color: rgb(80, 80, 80);</string> + </property> + <property name="text"> + <string>You must login to use encrypted email.</string> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QLabel" name="lblMailStatusIcon"> + <property name="minimumSize"> + <size> + <width>22</width> + <height>22</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>22</width> + <height>22</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="pixmap"> + <pixmap resource="../../../../../data/resources/icons.qrc">:/images/black/22/off.png</pixmap> + </property> + <property name="scaledContents"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QWidget" name="email_ready" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>6</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QPushButton" name="configure_button"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Configure Client</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="or_label"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>or</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="open_mail_button"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Open Bitmask Mail</string> + </property> + </widget> + </item> + <item> + <spacer name="email_ready_spacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>10</width> + <height>10</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> <item row="0" column="0"> - <layout class="QGridLayout" name="gridLayout_3"> - <item row="0" column="2"> - <spacer name="horizontalSpacer_4"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item row="0" column="1"> - <widget class="QLabel" name="label_4"> + <widget class="QLabel" name="mail_icon"> + <property name="text"> + <string/> + </property> + <property name="pixmap"> + <pixmap resource="../../../../../data/resources/icons.qrc">:/images/black/32/email.png</pixmap> + </property> + </widget> + </item> + <item row="0" column="1"> + <layout class="QHBoxLayout" name="email_line"> + <item> + <widget class="QLabel" name="email_label"> <property name="sizePolicy"> <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <horstretch>0</horstretch> @@ -51,85 +156,18 @@ </property> </widget> </item> - <item row="0" column="4"> - <widget class="QLabel" name="lblMailStatusIcon"> - <property name="maximumSize"> + <item> + <spacer name="horizontal1_spacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> <size> - <width>24</width> - <height>24</height> + <width>1</width> + <height>1</height> </size> </property> - <property name="text"> - <string/> - </property> - <property name="pixmap"> - <pixmap resource="../../../../../data/resources/icons.qrc">:/images/black/22/off.png</pixmap> - </property> - <property name="scaledContents"> - <bool>true</bool> - </property> - </widget> - </item> - <item row="3" column="1" colspan="2"> - <widget class="QLabel" name="lblMailReadyHelp"> - <property name="autoFillBackground"> - <bool>false</bool> - </property> - <property name="styleSheet"> - <string notr="true">background-color: #e0efd8; -padding: 10px;</string> - </property> - <property name="frameShape"> - <enum>QFrame::NoFrame</enum> - </property> - <property name="frameShadow"> - <enum>QFrame::Plain</enum> - </property> - <property name="lineWidth"> - <number>0</number> - </property> - <property name="text"> - <string>Congratulations! You are ready to use Bitmask to encrypt your email. Go to <a href="https://bitmask.net/en/help/email">https://bitmask.net/en/help/email</a> for instructions on how to set up your mail client.</string> - </property> - <property name="textFormat"> - <enum>Qt::AutoText</enum> - </property> - <property name="scaledContents"> - <bool>false</bool> - </property> - <property name="alignment"> - <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> - </property> - <property name="wordWrap"> - <bool>true</bool> - </property> - <property name="margin"> - <number>0</number> - </property> - <property name="indent"> - <number>-1</number> - </property> - </widget> - </item> - <item row="0" column="0"> - <widget class="QLabel" name="label"> - <property name="text"> - <string/> - </property> - <property name="pixmap"> - <pixmap resource="../../../../../data/resources/icons.qrc">:/images/black/32/email.png</pixmap> - </property> - </widget> - </item> - <item row="2" column="1" colspan="2"> - <widget class="QLabel" name="lblMailStatus"> - <property name="styleSheet"> - <string notr="true">color: rgb(80, 80, 80);</string> - </property> - <property name="text"> - <string>You must login to use encrypted email.</string> - </property> - </widget> + </spacer> </item> </layout> </item> @@ -137,6 +175,7 @@ padding: 10px;</string> </widget> <resources> <include location="../../../../../data/resources/icons.qrc"/> + <include location="dev/leap/client/bitmask_client/data/resources/icons.qrc"/> </resources> <connections/> </ui> diff --git a/src/leap/bitmask/gui/ui/mainwindow.ui b/src/leap/bitmask/gui/ui/mainwindow.ui index b1d68c4a..5d8e0f35 100644 --- a/src/leap/bitmask/gui/ui/mainwindow.ui +++ b/src/leap/bitmask/gui/ui/mainwindow.ui @@ -75,7 +75,7 @@ <x>0</x> <y>0</y> <width>524</width> - <height>549</height> + <height>541</height> </rect> </property> <layout class="QVBoxLayout" name="verticalLayout"> @@ -306,7 +306,7 @@ <x>0</x> <y>0</y> <width>524</width> - <height>21</height> + <height>25</height> </rect> </property> <widget class="QMenu" name="menuFile"> diff --git a/src/leap/bitmask/gui/ui/preferences.ui b/src/leap/bitmask/gui/ui/preferences.ui index 5e30ea57..8e884a63 100644 --- a/src/leap/bitmask/gui/ui/preferences.ui +++ b/src/leap/bitmask/gui/ui/preferences.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>520</width> - <height>439</height> + <width>630</width> + <height>560</height> </rect> </property> <property name="windowTitle"> @@ -60,6 +60,24 @@ <height>16777215</height> </size> </property> + <property name="styleSheet"> + <string notr="true">background: palette(base); border: 1px solid palette(dark); border-radius: 2px;</string> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="lineWidth"> + <number>1</number> + </property> + <property name="midLineWidth"> + <number>0</number> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> <property name="iconSize"> <size> <width>32</width> diff --git a/src/leap/bitmask/gui/ui/preferences_email_page.ui b/src/leap/bitmask/gui/ui/preferences_email_page.ui index 7cc5bb3c..610a43c7 100644 --- a/src/leap/bitmask/gui/ui/preferences_email_page.ui +++ b/src/leap/bitmask/gui/ui/preferences_email_page.ui @@ -6,13 +6,638 @@ <rect> <x>0</x> <y>0</y> - <width>400</width> - <height>300</height> + <width>526</width> + <height>605</height> </rect> </property> <property name="windowTitle"> <string>Form</string> </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="margin"> + <number>0</number> + </property> + <item> + <widget class="QTabWidget" name="email_tabs"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="config_tab"> + <property name="accessibleName"> + <string/> + </property> + <property name="accessibleDescription"> + <string/> + </property> + <property name="styleSheet"> + <string notr="true">background: palette(base);</string> + </property> + <attribute name="title"> + <string>Tab 1</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_7"> + <item> + <widget class="QScrollArea" name="scrollArea"> + <property name="styleSheet"> + <string notr="true"/> + </property> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="scrollAreaWidgetContents"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>504</width> + <height>537</height> + </rect> + </property> + <property name="autoFillBackground"> + <bool>false</bool> + </property> + <property name="styleSheet"> + <string notr="true">background: palette(base);</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_8"> + <property name="spacing"> + <number>6</number> + </property> + <property name="margin"> + <number>0</number> + </property> + <item> + <widget class="QGroupBox" name="webmail_box"> + <property name="title"> + <string>Bitmask Mail</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QCheckBox" name="webmail_checkbox"> + <property name="text"> + <string>Enable Bitmask Mail (needs restart)</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="webmail_label"> + <property name="text"> + <string>webmail info</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="thunderbird_box"> + <property name="title"> + <string>Thunderbird Configuration</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QLabel" name="thunderbird_label"> + <property name="text"> + <string>thunderbird information</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="imap_box"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Other Mail Clients Configuration</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QLabel" name="mail_client_label"> + <property name="text"> + <string>mail client information</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="1" column="1"> + <layout class="QGridLayout" name="gridLayout_5"> + <item row="0" column="0"> + <widget class="QLabel" name="label_8"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Host</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QLabel" name="label_9"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Port</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="smtp_host_edit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>100</width> + <height>16777215</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="4"> + <widget class="QLabel" name="label_10"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>TLS: off</string> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QLineEdit" name="smtp_port_edit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>50</width> + <height>16777215</height> + </size> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="5"> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>IMAP</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Username</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>SMTP</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <layout class="QGridLayout" name="gridLayout_4"> + <item row="1" column="0"> + <widget class="QLabel" name="label_5"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Host</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="imap_host_edit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>100</width> + <height>16777215</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QLabel" name="label_6"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Port</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="1" column="6"> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="5"> + <widget class="QLabel" name="label_7"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>TLS: off</string> + </property> + </widget> + </item> + <item row="1" column="3"> + <widget class="QLineEdit" name="imap_port_edit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>50</width> + <height>16777215</height> + </size> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="username_edit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Password</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLineEdit" name="password_edit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>4</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="my_key_tab"> + <attribute name="title"> + <string>Tab 2</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="QWidget" name="mykey_box" native="true"> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="4" column="0"> + <widget class="QLabel" name="pub_label"> + <property name="text"> + <string>Public Key</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QPlainTextEdit" name="public_key_edit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <family>Courier</family> + </font> + </property> + <property name="lineWrapMode"> + <enum>QPlainTextEdit::NoWrap</enum> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QPlainTextEdit" name="fingerprint_edit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>42</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>48</height> + </size> + </property> + <property name="font"> + <font> + <family>Courier</family> + <weight>50</weight> + <bold>false</bold> + </font> + </property> + <property name="acceptDrops"> + <bool>false</bool> + </property> + <property name="styleSheet"> + <string notr="true"/> + </property> + <property name="lineWrapMode"> + <enum>QPlainTextEdit::NoWrap</enum> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="plainText"> + <string notr="true"/> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="expiration_label"> + <property name="text"> + <string>Expiration</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="fp_label"> + <property name="text"> + <string>Fingerprint</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="uid_edit"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="uid_label"> + <property name="text"> + <string>Address</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="expiration_edit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="5" column="1"> + <layout class="QGridLayout" name="gridLayout"> + <item row="2" column="1"> + <widget class="QPushButton" name="export_button"> + <property name="text"> + <string>Export Private Key</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QPushButton" name="import_button"> + <property name="text"> + <string>Import Private Key</string> + </property> + </widget> + </item> + <item row="2" column="3"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + <zorder>uid_edit</zorder> + <zorder>fp_label</zorder> + <zorder>uid_label</zorder> + <zorder>expiration_edit</zorder> + <zorder>expiration_label</zorder> + <zorder>fingerprint_edit</zorder> + <zorder>public_key_edit</zorder> + <zorder>pub_label</zorder> + <zorder></zorder> + </widget> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="other_keys_tab"> + <attribute name="title"> + <string>Page</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QTableWidget" name="keys_table"> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="textElideMode"> + <enum>Qt::ElideRight</enum> + </property> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + <column> + <property name="text"> + <string>Email</string> + </property> + </column> + <column> + <property name="text"> + <string>Fingerprint</string> + </property> + </column> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <widget class="QLabel" name="message_label"> + <property name="text"> + <string>this message should be hidden</string> + </property> + </widget> + </item> + </layout> </widget> <resources/> <connections/> diff --git a/src/leap/bitmask/gui/ui/wizard.ui b/src/leap/bitmask/gui/ui/wizard.ui index b125577e..37226d13 100644 --- a/src/leap/bitmask/gui/ui/wizard.ui +++ b/src/leap/bitmask/gui/ui/wizard.ui @@ -316,7 +316,7 @@ <item row="1" column="1"> <widget class="QLabel" name="label"> <property name="text"> - <string>https://</string> + <string notr="true">https://</string> </property> <property name="alignment"> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> @@ -339,7 +339,7 @@ <item row="0" column="1"> <widget class="QLabel" name="label_8"> <property name="text"> - <string>https://</string> + <string notr="true">https://</string> </property> <property name="alignment"> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> diff --git a/src/leap/bitmask/pix.py b/src/leap/bitmask/pix.py new file mode 100644 index 00000000..b510106d --- /dev/null +++ b/src/leap/bitmask/pix.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +# pix.py +# Copyright (C) 2016 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Pixelated plugin integration. +""" +import json +import os +import sys + +from twisted.internet import defer +from twisted.python import log + +from leap.bitmask.util import get_path_prefix +from leap.mail.mail import Account +from leap.keymanager import openpgp, KeyNotFound + +try: + from pixelated.adapter.mailstore import LeapMailStore + from pixelated.adapter.welcome_mail import add_welcome_mail + from pixelated.application import SingleUserServicesFactory + from pixelated.application import UserAgentMode + from pixelated.application import start_site + from pixelated.bitmask_libraries.smtp import LeapSMTPConfig + from pixelated.bitmask_libraries.session import SessionCache + from pixelated.config import services + from pixelated.resources.root_resource import RootResource + import pixelated_www + HAS_PIXELATED = True +except ImportError: + HAS_PIXELATED = False + + +def start_pixelated_user_agent(userid, soledad, keymanager): + + leap_session = LeapSessionAdapter( + userid, soledad, keymanager) + + config = Config() + leap_home = os.path.join(get_path_prefix(), 'leap') + config.leap_home = leap_home + leap_session.config = config + + services_factory = SingleUserServicesFactory( + UserAgentMode(is_single_user=True)) + + if getattr(sys, 'frozen', False): + # we are running in a |PyInstaller| bundle + static_folder = os.path.join(sys._MEIPASS, 'pixelated_www') + else: + static_folder = os.path.abspath(pixelated_www.__path__[0]) + + resource = RootResource(services_factory, static_folder=static_folder) + + config.host = 'localhost' + config.port = 9090 + config.sslkey = None + config.sslcert = None + + d = leap_session.account.callWhenReady( + lambda _: _start_in_single_user_mode( + leap_session, config, + resource, services_factory)) + return d + + +def get_smtp_config(provider): + config_path = os.path.join( + get_path_prefix(), 'leap', 'providers', provider, 'smtp-service.json') + json_config = json.loads(open(config_path).read()) + chosen_host = json_config['hosts'].keys()[0] + hostname = json_config['hosts'][chosen_host]['hostname'] + port = json_config['hosts'][chosen_host]['port'] + + config = Config() + config.host = hostname + config.port = port + return config + + +class NickNym(object): + + def __init__(self, keymanager, userid): + self._email = userid + self.keymanager = keymanager + + @defer.inlineCallbacks + def generate_openpgp_key(self): + key_present = yield self._key_exists(self._email) + if not key_present: + yield self._gen_key() + yield self._send_key_to_leap() + + @defer.inlineCallbacks + def _key_exists(self, email): + try: + yield self.fetch_key(email, private=True, fetch_remote=False) + defer.returnValue(True) + except KeyNotFound: + defer.returnValue(False) + + def fetch_key(self, email, private=False, fetch_remote=True): + return self.keymanager.get_key( + email, openpgp.OpenPGPKey, + private=private, fetch_remote=fetch_remote) + + def _gen_key(self): + return self.keymanager.gen_key(openpgp.OpenPGPKey) + + def _send_key_to_leap(self): + return self.keymanager.send_key(openpgp.OpenPGPKey) + + +class LeapSessionAdapter(object): + + def __init__(self, userid, soledad, keymanager): + self.userid = userid + + self.soledad = soledad + + # XXX this needs to be converged with our public apis. + self.nicknym = NickNym(keymanager, userid) + self.mail_store = LeapMailStore(soledad) + + self.user_auth = Config() + self.user_auth.uuid = soledad.uuid + + self.fresh_account = False + self.incoming_mail_fetcher = None + self.account = Account(soledad, userid) + + username, provider = userid.split('@') + smtp_client_cert = os.path.join( + get_path_prefix(), + 'leap', 'providers', provider, 'keys', + 'client', + 'smtp_{username}.pem'.format( + username=username)) + + assert(os.path.isfile(smtp_client_cert)) + + smtp_config = get_smtp_config(provider) + smtp_host = smtp_config.host + smtp_port = smtp_config.port + + self.smtp_config = LeapSMTPConfig( + userid, + smtp_client_cert, smtp_host, smtp_port) + + def account_email(self): + return self.userid + + def close(self): + pass + + @property + def is_closed(self): + return self._is_closed + + def remove_from_cache(self): + key = SessionCache.session_key(self.provider, self.userid) + SessionCache.remove_session(key) + + def sync(self): + return self.soledad.sync() + + +class Config(object): + pass + + +def _start_in_single_user_mode(leap_session, config, resource, + services_factory): + start_site(config, resource) + return start_user_agent_in_single_user_mode( + resource, services_factory, + leap_session.config.leap_home, leap_session) + + +@defer.inlineCallbacks +def start_user_agent_in_single_user_mode( + root_resource, services_factory, leap_home, leap_session): + log.msg('Bootstrap done, loading services for user %s' + % leap_session.userid) + + _services = services.Services(leap_session) + yield _services.setup() + + if leap_session.fresh_account: + yield add_welcome_mail(leap_session.mail_store) + + services_factory.add_session(leap_session.user_auth.uuid, _services) + root_resource.initialize() + log.msg('Done, the user agent is ready to be used') diff --git a/src/leap/bitmask/platform_init/initializers.py b/src/leap/bitmask/platform_init/initializers.py index eb892cce..751762df 100644 --- a/src/leap/bitmask/platform_init/initializers.py +++ b/src/leap/bitmask/platform_init/initializers.py @@ -126,7 +126,7 @@ def check_missing(): if alert_missing and not flags.STANDALONE: # We refuse to install missing stuff if not running with standalone # flag. Right now we rely on the flag alone, but we can disable this - # by overwriting some constant from within the debian package. + # by overwriting some constant from within the Debian package. alert_missing = False complain_missing = True @@ -257,36 +257,19 @@ def WindowsInitializer(): 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.setText(msg.tr("Bitmask needs a TAP Driver installed " + "for Encrypted Internet to work. Please reinstall " + "bitmask-client 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") + "has been found. The bitmask-client " + "installer prompts for installing the " + "TAP Driver.")) + msg.setStandardButtons(QtGui.QMessageBox.Ok) + logger.error('TAP Drivers not installed') + msg.exec_() + return False + return True # # Darwin initializer functions @@ -368,6 +351,9 @@ def DarwinInitializer(): Raise a dialog in case that the osx tuntap driver has not been found in the registry, asking the user for permission to install the driver """ + logger.debug("Skipping darwin initialization, only-mail build") + return True + # XXX split this function into several TUNTAP_NOTFOUND_MSG = NOTFOUND_MSG % ( @@ -440,32 +426,32 @@ def _get_missing_complain_dialog(stuff): class ComplainDialog(QtGui.QDialog): def __init__(self, parent=None): - super(ComplainDialog, self).__init__(parent) + super(ComplainDialog, self).__init__(parent) - label = QtGui.QLabel(msgstr.NO_HELPERS) - label.setAlignment(QtCore.Qt.AlignLeft) + label = QtGui.QLabel(msgstr.NO_HELPERS) + label.setAlignment(QtCore.Qt.AlignLeft) - label2 = QtGui.QLabel(msgstr.EXPLAIN) - label2.setAlignment(QtCore.Qt.AlignLeft) + label2 = QtGui.QLabel(msgstr.EXPLAIN) + label2.setAlignment(QtCore.Qt.AlignLeft) - textedit = QtGui.QTextEdit() - textedit.setText("\n".join(stuff)) + textedit = QtGui.QTextEdit() + textedit.setText("\n".join(stuff)) - ok = QtGui.QPushButton() - ok.setText(self.tr("Ok, thanks")) - self.ok = ok - self.ok.clicked.connect(self.close) + ok = QtGui.QPushButton() + ok.setText(self.tr("Ok, thanks")) + self.ok = ok + self.ok.clicked.connect(self.close) - mainLayout = QtGui.QGridLayout() - mainLayout.addWidget(label, 0, 0) - mainLayout.addWidget(label2, 1, 0) - mainLayout.addWidget(textedit, 2, 0) - mainLayout.addWidget(ok, 3, 0) + mainLayout = QtGui.QGridLayout() + mainLayout.addWidget(label, 0, 0) + mainLayout.addWidget(label2, 1, 0) + mainLayout.addWidget(textedit, 2, 0) + mainLayout.addWidget(ok, 3, 0) - self.setLayout(mainLayout) + self.setLayout(mainLayout) msg = ComplainDialog() - msg.setWindowTitle(msg.tr("Missing Bitmask helpers")) + msg.setWindowTitle(msg.tr("Missing Bitmask helper files")) return msg @@ -482,7 +468,7 @@ def _linux_install_missing_scripts(badexec, notfound): """ success = False installer_path = os.path.abspath( - os.path.join(os.getcwd(), "apps", "eip", "files")) + os.path.join(os.getcwd(), "..", "apps", "eip", "files")) install_helper = "leap-install-helper.sh" install_helper_path = os.path.join(installer_path, install_helper) diff --git a/src/leap/bitmask/provider/__init__.py b/src/leap/bitmask/provider/__init__.py index 4385a92f..60a41181 100644 --- a/src/leap/bitmask/provider/__init__.py +++ b/src/leap/bitmask/provider/__init__.py @@ -22,7 +22,7 @@ import os from pkg_resources import parse_version -from leap.bitmask import __short_version__ as BITMASK_VERSION +from leap.bitmask import __version__ as BITMASK_VERSION from leap.common.check import leap_assert logger = logging.getLogger(__name__) diff --git a/src/leap/bitmask/services/__init__.py b/src/leap/bitmask/services/__init__.py index 54426669..d86f8aa4 100644 --- a/src/leap/bitmask/services/__init__.py +++ b/src/leap/bitmask/services/__init__.py @@ -154,6 +154,9 @@ def download_service_config(provider_config, service_config, # Not modified service_path = ("leap", "providers", provider_config.get_domain(), service_json) + + service_config.__class__.standalone = flags.STANDALONE + if res.status_code == 304: logger.debug( "{0} definition has not been modified".format( diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py index 17fc11c2..e697b118 100644 --- a/src/leap/bitmask/services/eip/darwinvpnlauncher.py +++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py @@ -20,6 +20,7 @@ Darwin VPN launcher implementation. import commands import getpass import os +import socket import sys from leap.bitmask.logs.utils import get_logger @@ -34,17 +35,46 @@ class EIPNoTunKextLoaded(VPNLauncherException): pass +class DarwinHelperCommand(object): + + SOCKET_ADDR = '/tmp/bitmask-helper.socket' + + def __init__(self): + pass + + def _connect(self): + self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + self._sock.connect(self.SOCKET_ADDR) + except socket.error, msg: + raise RuntimeError(msg) + + def send(self, cmd, args=''): + # TODO check cmd is in allowed list + self._connect() + sock = self._sock + data = "" + + command = cmd + ' ' + args + '/CMD' + + try: + sock.sendall(command) + while '\n' not in data: + data += sock.recv(32) + finally: + sock.close() + + return data + + 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.\"") + UP_SCRIPT = None + DOWN_SCRIPT = None + + # TODO -- move this to bitmask-helper # Hardcode the installation path for OSX for security, openvpn is # run as root @@ -56,14 +86,9 @@ class DarwinVPNLauncher(VPNLauncher): INSTALL_PATH_ESCAPED,) OPENVPN_BIN_PATH = "%s/Contents/Resources/%s" % (INSTALL_PATH, OPENVPN_BIN) - - UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,) - DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,) - OPENVPN_DOWN_PLUGIN = '%s/openvpn-down-root.so' % (OPENVPN_PATH,) - - UPDOWN_FILES = (UP_SCRIPT, DOWN_SCRIPT, OPENVPN_DOWN_PLUGIN) OTHER_FILES = [] + # TODO deprecate ------------------------------------------------ @classmethod def cmd_for_missing_scripts(kls, frompath): """ @@ -87,7 +112,11 @@ class DarwinVPNLauncher(VPNLauncher): :returns: True if kext is loaded, False otherwise. :rtype: bool """ - return bool(commands.getoutput('kextstat | grep "leap.tun"')) + loaded = bool(commands.getoutput( + 'kextstat | grep "net.sf.tuntaposx.tun"')) + if not loaded: + logger.error("tuntaposx extension not loaded!") + return loaded @classmethod def _get_icon_path(kls): @@ -101,6 +130,7 @@ class DarwinVPNLauncher(VPNLauncher): return os.path.join(resources_path, "bitmask.tiff") + # TODO deprecate --------------------------------------------------------- @classmethod def get_cocoasudo_ovpn_cmd(kls): """ @@ -120,6 +150,7 @@ class DarwinVPNLauncher(VPNLauncher): return kls.COCOASUDO, args + # TODO deprecate --------------------------------------------------------- @classmethod def get_cocoasudo_installmissing_cmd(kls): """ @@ -171,12 +202,6 @@ class DarwinVPNLauncher(VPNLauncher): # we use `super` in order to send the class to use command = super(DarwinVPNLauncher, kls).get_vpn_command( eipconfig, providerconfig, socket_host, socket_port, openvpn_verb) - - cocoa, cargs = kls.get_cocoasudo_ovpn_cmd() - cargs.extend(command) - command = cargs - command.insert(0, cocoa) - command.extend(['--setenv', "LEAPUSER", getpass.getuser()]) return command diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py index c48f857c..16dfd9cf 100644 --- a/src/leap/bitmask/services/eip/vpnlauncher.py +++ b/src/leap/bitmask/services/eip/vpnlauncher.py @@ -29,7 +29,7 @@ from leap.bitmask.config import flags from leap.bitmask.logs.utils import get_logger from leap.bitmask.backend.settings import Settings, GATEWAY_AUTOMATIC from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.platform_init import IS_LINUX +from leap.bitmask.platform_init import IS_LINUX, IS_MAC from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector from leap.bitmask.util import force_eval from leap.common.check import leap_assert, leap_assert_type @@ -286,8 +286,8 @@ class VPNLauncher(object): :rtype: list """ # FIXME - # XXX remove method when we ditch UPDOWN in osx and win too - if IS_LINUX: + # XXX remove method when we ditch UPDOWN in win too + if IS_LINUX or IS_MAC: return [] else: leap_assert(kls.UPDOWN_FILES is not None, @@ -308,7 +308,7 @@ class VPNLauncher(object): """ leap_assert(kls.OTHER_FILES is not None, "Need to define OTHER_FILES for this particular " - "auncher before calling this method") + "launcher before calling this method") other = force_eval(kls.OTHER_FILES) file_exist = partial(_has_other_files, warn=False) diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index 586b50f5..580bd572 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -41,6 +41,7 @@ from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.logs.utils import get_logger from leap.bitmask.services.eip import get_vpn_launcher from leap.bitmask.services.eip import linuxvpnlauncher +from leap.bitmask.services.eip import darwinvpnlauncher from leap.bitmask.services.eip.eipconfig import EIPConfig from leap.bitmask.services.eip.udstelnet import UDSTelnet from leap.bitmask.util import first, force_eval @@ -145,7 +146,7 @@ class VPN(object): demand. """ TERMINATE_MAXTRIES = 10 - TERMINATE_WAIT = 1 # secs + TERMINATE_WAIT = 2 # secs OPENVPN_VERB = "openvpn_verb" @@ -173,7 +174,8 @@ class VPN(object): :param kwargs: kwargs to be passed to the VPNProcess :type kwargs: dict """ - logger.debug('VPN: start') + logger.debug( + 'VPN: start ---------------------------------------------------') self._user_stopped = False self._stop_pollers() kwargs['openvpn_verb'] = self._openvpn_verb @@ -181,23 +183,6 @@ class VPN(object): restart = kwargs.pop('restart', False) - # start the main vpn subprocess - vpnproc = VPNProcess(*args, **kwargs) - - if vpnproc.get_openvpn_process(): - logger.info("Another vpn process is running. Will try to stop it.") - vpnproc.stop_if_already_running() - - # we try to bring the firewall up - if IS_LINUX: - gateways = vpnproc.getGateways() - firewall_up = self._launch_firewall(gateways, - restart=restart) - if not restart and not firewall_up: - logger.error("Could not bring firewall up, " - "aborting openvpn launch.") - return - # FIXME it would be good to document where the # errors here are catched, since we currently handle them # at the frontend layer. This *should* move to be handled entirely @@ -211,17 +196,54 @@ class VPN(object): # the ping-pong to the frontend, and without adding any logical checks # in the frontend. We should just communicate UI changes to frontend, # and abstract us away from anything else. - try: + + # TODO factor this out to the platform-launchers + + if IS_LINUX: + # start the main vpn subprocess + vpnproc = VPNProcess(*args, **kwargs) cmd = vpnproc.getCommand() - except Exception as e: - logger.error("Error while getting vpn command... {0!r}".format(e)) - raise + + if vpnproc.get_openvpn_process(): + logger.info( + "Another vpn process is running. Will try to stop it.") + vpnproc.stop_if_already_running() + + # we try to bring the firewall up + gateways = vpnproc.getGateways() + firewall_up = self._launch_firewall_linux( + gateways, restart=restart) + if not restart and not firewall_up: + logger.error("Could not bring firewall up, " + "aborting openvpn launch.") + return + + if IS_MAC: + # start the main vpn subprocess + vpnproc = VPNCanary(*args, **kwargs) + + # we try to bring the firewall up + gateways = vpnproc.getGateways() + firewall_up = self._launch_firewall_osx( + gateways, restart=restart) + if not restart and not firewall_up: + logger.error("Could not bring firewall up, " + "aborting openvpn launch.") + return + + helper = darwinvpnlauncher.DarwinHelperCommand() + cmd = vpnproc.getVPNCommand() + result = helper.send('openvpn_start %s' % ' '.join(cmd)) + + # TODO Windows version -- should be similar to osx. env = os.environ for key, val in vpnproc.vpn_env.items(): env[key] = val - reactor.spawnProcess(vpnproc, cmd[0], cmd, env) + cmd = vpnproc.getCommand() + running_proc = reactor.spawnProcess(vpnproc, cmd[0], cmd, env) + vpnproc.pid = running_proc.pid self._vpnproc = vpnproc # add pollers for status and state @@ -233,9 +255,9 @@ class VPN(object): self._pollers.extend(poll_list) self._start_pollers() - def _launch_firewall(self, gateways, restart=False): + def _launch_firewall_linux(self, gateways, restart=False): """ - Launch the firewall using the privileged wrapper. + Launch the firewall using the privileged wrapper (linux). :param gateways: :type gateways: list @@ -254,40 +276,65 @@ class VPN(object): exitCode = subprocess.call(cmd + gateways) return True if exitCode is 0 else False + def _launch_firewall_osx(self, gateways, restart=False): + cmd = 'firewall_start %s' % ' '.join(gateways) + helper = darwinvpnlauncher.DarwinHelperCommand() + result = helper.send(cmd) + return True + + # TODO -- write LINUX/OSX VERSION too ------------------------------------ def is_fw_down(self): """ Return whether the firewall is down or not. :rtype: bool """ - BM_ROOT = force_eval(linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT) - fw_up_cmd = "pkexec {0} firewall isup".format(BM_ROOT) - fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256 - return fw_is_down() + if IS_LINUX: + BM_ROOT = force_eval( + linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT) + fw_up_cmd = "pkexec {0} firewall isup".format(BM_ROOT) + fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256 + return fw_is_down() + + if IS_MAC: + cmd = 'firewall_isup' + helper = darwinvpnlauncher.DarwinHelperCommand() + result = helper.send(cmd) + return True def tear_down_firewall(self): """ Tear the firewall down using the privileged wrapper. """ if IS_MAC: - # We don't support Mac so far + cmd = 'firewall_stop' + helper = darwinvpnlauncher.DarwinHelperCommand() + result = helper.send(cmd) return True - BM_ROOT = force_eval(linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT) - exitCode = subprocess.call(["pkexec", - BM_ROOT, "firewall", "stop"]) - return True if exitCode is 0 else False + + if IS_LINUX: + BM_ROOT = force_eval( + linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT) + exitCode = subprocess.call(["pkexec", + BM_ROOT, "firewall", "stop"]) + return True if exitCode is 0 else False def bitmask_root_vpn_down(self): """ Bring openvpn down using the privileged wrapper. """ if IS_MAC: - # We don't support Mac so far + cmd = 'openvpn_stop' + helper = darwinvpnlauncher.DarwinHelperCommand() + result = helper.send(cmd) return True - BM_ROOT = force_eval(linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT) - exitCode = subprocess.call(["pkexec", - BM_ROOT, "openvpn", "stop"]) - return True if exitCode is 0 else False + + if IS_LINUX: + BM_ROOT = force_eval( + linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT) + exitCode = subprocess.call(["pkexec", + BM_ROOT, "openvpn", "stop"]) + return True if exitCode is 0 else False def _kill_if_left_alive(self, tries=0): """ @@ -297,18 +344,18 @@ class VPN(object): :param tries: counter of tries, used in recursion :type tries: int """ + # we try to tear the firewall down + if (IS_LINUX or IS_MAC) and self._user_stopped: + logger.debug('trying to bring firewall down...') + firewall_down = self.tear_down_firewall() + if firewall_down: + logger.debug("Firewall down") + else: + logger.warning("Could not tear firewall down") + while tries < self.TERMINATE_MAXTRIES: if self._vpnproc.transport.pid is None: logger.debug("Process has been happily terminated.") - - # we try to tear the firewall down - if IS_LINUX and self._user_stopped: - firewall_down = self.tear_down_firewall() - if firewall_down: - logger.debug("Firewall down") - else: - logger.warning("Could not tear firewall down") - return else: logger.debug("Process did not die, waiting...") @@ -813,6 +860,8 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): programmatically. """ + pid = None + def __init__(self, eipconfig, providerconfig, socket_host, socket_port, signaler, openvpn_verb): """ @@ -861,7 +910,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): self._vpn_observer = VPNObserver(signaler) self.is_restart = False - # processProtocol methods + # ProcessProtocol methods def connectionMade(self): """ @@ -893,8 +942,11 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): .. 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,)) + else: + exit_code = 0 self._signaler.signal( self._signaler.eip_process_finished, exit_code) self._alive = False @@ -976,3 +1028,41 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): self.transport.signalProcess('KILL') except internet_error.ProcessExitedAlready: logger.debug('Process Exited Already') + + +class VPNCanary(VPNProcess): + + """ + This is a Canary Process that does not run openvpn itself, but it's + notified by the privileged process when the process dies. + + This is an ugly workaround to allow the qt signals and the processprotocol + to live happily together until we refactor EIP out of the qt model + completely. + """ + + def connectionMade(self): + VPNProcess.connectionMade(self) + reactor.callLater(2, self.registerPID) + + def registerPID(self): + helper = darwinvpnlauncher.DarwinHelperCommand() + cmd = 'openvpn_set_watcher %s' % self.pid + result = helper.send(cmd) + + def killProcess(self): + helper = darwinvpnlauncher.DarwinHelperCommand() + cmd = 'openvpn_force_stop' + result = helper.send(cmd) + + def getVPNCommand(self): + return VPNProcess.getCommand(self) + + def getCommand(self): + canary = '''import sys, signal, time +def receive_signal(signum, stack): + sys.exit() +signal.signal(signal.SIGTERM, receive_signal) +while True: + time.sleep(60)''' + return ['python', '-c', '%s' % canary] diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py index 68197d9d..cccbcf14 100644 --- a/src/leap/bitmask/services/mail/conductor.py +++ b/src/leap/bitmask/services/mail/conductor.py @@ -18,6 +18,7 @@ Mail Services Conductor """ from leap.bitmask.config import flags +from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.logs.utils import get_logger from leap.bitmask.gui import statemachines from leap.bitmask.services.mail import connection as mail_connection @@ -34,6 +35,7 @@ class IMAPControl(object): """ Methods related to IMAP control. """ + def __init__(self): """ Initializes smtp variables. @@ -73,12 +75,13 @@ class IMAPControl(object): self._backend.imap_stop_service() - def _handle_imap_events(self, event, content): + def _handle_imap_events(self, event, userid=None, content=None): """ Callback handler for the IMAP events :param event: The event that triggered the callback. :type event: str + :param userid: The user id of the logged in user. Ignored. :param content: The content of the event. :type content: list """ @@ -113,10 +116,11 @@ class IMAPControl(object): """ Callback for IMAP failed state. """ - self.imap_connection.qtsigs.connetion_aborted_signal.emit() + self.imap_connection.qtsigs.connection_aborted_signal.emit() class SMTPControl(object): + def __init__(self): """ Initializes smtp variables. @@ -189,7 +193,17 @@ class SMTPControl(object): self.smtp_connection.qtsigs.connection_aborted_signal.emit() -class MailConductor(IMAPControl, SMTPControl): +class PixelatedControl(object): + + def start_pixelated_service(self): + self._backend.pixelated_start_service( + full_user_id=self.userid) + + def stop_pixelated_service(self): + pass + + +class MailConductor(IMAPControl, SMTPControl, PixelatedControl): """ This class encapsulates everything related to the initialization and process control for the mail services. @@ -266,6 +280,11 @@ class MailConductor(IMAPControl, SMTPControl): self.start_smtp_service(download_if_needed=download_if_needed) self.start_imap_service() + settings = LeapSettings() + pixelmail = settings.get_pixelmail_enabled() + if pixelmail: + self.start_pixelated_service() + self._mail_services_started = True def stop_mail_services(self): diff --git a/src/leap/bitmask/services/mail/imap.py b/src/leap/bitmask/services/mail/imap.py index 5934756d..7875a4af 100644 --- a/src/leap/bitmask/services/mail/imap.py +++ b/src/leap/bitmask/services/mail/imap.py @@ -20,11 +20,14 @@ Initialization of imap service import os import sys +from twisted.python import log + from leap.bitmask.logs.utils import get_logger from leap.mail.constants import INBOX_NAME from leap.mail.imap.service import imap from leap.mail.incoming.service import IncomingMail, INCOMING_CHECK_PERIOD -from twisted.python import log +from leap.mail.mail import Account + logger = get_logger() @@ -57,11 +60,13 @@ def get_mail_check_period(): return period -def start_imap_service(*args, **kwargs): +def start_imap_service(soledad_sessions): """ Initializes and run imap service. - :returns: twisted.internet.task.LoopingCall instance + :returns: the port as returned by the reactor when starts listening, and + the factory for the protocol. + :rtype: tuple """ from leap.bitmask.config import flags logger.debug('Launching imap service') @@ -70,10 +75,10 @@ def start_imap_service(*args, **kwargs): log.startLogging(open(flags.MAIL_LOGFILE, 'w')) log.startLogging(sys.stdout) - return imap.run_service(*args, **kwargs) + return imap.run_service(soledad_sessions) -def start_incoming_mail_service(keymanager, soledad, imap_factory, userid): +def start_incoming_mail_service(keymanager, soledad, userid): """ Initalizes and starts the incomming mail service. @@ -81,19 +86,12 @@ def start_incoming_mail_service(keymanager, soledad, imap_factory, userid): """ def setUpIncomingMail(inbox): incoming_mail = IncomingMail( - keymanager, - soledad, - inbox.collection, - userid, + keymanager, soledad, + inbox, userid, check_period=get_mail_check_period()) return incoming_mail - # XXX: do I really need to know here how to get a mailbox?? - # XXX: ideally, the parent service in mail would take care of initializing - # the account, and passing the mailbox to the incoming service. - # In an even better world, we just would subscribe to a channel that would - # pass us the serialized object to be inserted. - acc = imap_factory.theAccount - d = acc.callWhenReady(lambda _: acc.getMailbox(INBOX_NAME)) + acc = Account(soledad, userid) + d = acc.callWhenReady(lambda _: acc.get_collection_by_mailbox(INBOX_NAME)) d.addCallback(setUpIncomingMail) return d diff --git a/src/leap/bitmask/services/mail/imapcontroller.py b/src/leap/bitmask/services/mail/imapcontroller.py index 5053d897..855fb74b 100644 --- a/src/leap/bitmask/services/mail/imapcontroller.py +++ b/src/leap/bitmask/services/mail/imapcontroller.py @@ -60,9 +60,9 @@ class IMAPController(object): """ logger.debug('Starting imap service') + soledad_sessions = {userid: self._soledad} self.imap_port, self.imap_factory = imap.start_imap_service( - self._soledad, - userid=userid) + soledad_sessions) def start_and_assign_incoming_service(incoming_mail): # this returns a deferred that will be called when the looping call @@ -74,9 +74,7 @@ class IMAPController(object): if offline is False: d = imap.start_incoming_mail_service( - self._keymanager, - self._soledad, - self.imap_factory, + self._keymanager, self._soledad, userid) d.addCallback(start_and_assign_incoming_service) d.addErrback(lambda f: logger.error(f.printTraceback())) diff --git a/src/leap/bitmask/services/mail/plumber.py b/src/leap/bitmask/services/mail/plumber.py index 43203f0c..cd1f06bb 100644 --- a/src/leap/bitmask/services/mail/plumber.py +++ b/src/leap/bitmask/services/mail/plumber.py @@ -60,6 +60,7 @@ def initialize_soledad(uuid, email, passwd, cert_file = "" class Mock(object): + def __init__(self, return_value=None): self._return = return_value @@ -140,7 +141,7 @@ class MBOXPlumber(object): self.sol = initialize_soledad( self.uuid, self.userid, self.passwd, secrets, localdb, "/tmp", "/tmp") - self.acct = IMAPAccount(self.userid, self.sol) + self.acct = IMAPAccount(self.sol, self.userid) return True # diff --git a/src/leap/bitmask/services/mail/smtpbootstrapper.py b/src/leap/bitmask/services/mail/smtpbootstrapper.py index a577509e..f73687a7 100644 --- a/src/leap/bitmask/services/mail/smtpbootstrapper.py +++ b/src/leap/bitmask/services/mail/smtpbootstrapper.py @@ -19,6 +19,7 @@ SMTP bootstrapping """ import os import warnings +from collections import namedtuple from requests.exceptions import HTTPError @@ -28,7 +29,6 @@ from leap.bitmask.logs.utils import get_logger from leap.bitmask.services import download_service_config from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper from leap.bitmask.services.mail.smtpconfig import SMTPConfig -from leap.bitmask.util import is_file from leap.common import certs as leap_certs from leap.common.check import leap_assert @@ -92,11 +92,13 @@ class SMTPBootstrapper(AbstractBootstrapper): client_cert_path = self._smtp_config.get_client_cert_path( self._userid, self._provider_config, about_to_download=True) - if not is_file(client_cert_path): + needs_download = leap_certs.should_redownload(client_cert_path) + + if needs_download: # For re-download if something is wrong with the cert + # FIXME this doesn't read well. should reword the logic here. self._download_if_needed = ( - self._download_if_needed and - not leap_certs.should_redownload(client_cert_path)) + self._download_if_needed and not needs_download) if self._download_if_needed and os.path.isfile(client_cert_path): check_and_fix_urw_only(client_cert_path) @@ -127,9 +129,6 @@ class SMTPBootstrapper(AbstractBootstrapper): Start the smtp service using the downloaded configurations. """ # TODO Make the encrypted_only configurable - # TODO pick local smtp port in a better way - # TODO remove hard-coded port and let leap.mail set - # the specific default. # TODO handle more than one host and define how to choose hosts = self._smtp_config.get_hosts() hostname = hosts.keys()[0] @@ -138,19 +137,25 @@ class SMTPBootstrapper(AbstractBootstrapper): client_cert_path = self._smtp_config.get_client_cert_path( self._userid, self._provider_config, about_to_download=True) - from leap.mail.smtp import setup_smtp_gateway + # XXX this should be defined in leap.mail.smtp, it's in bitmask.core + # right now. + SendmailOpts = namedtuple( + 'SendmailOpts', ['cert', 'key', 'hostname', 'port']) + + userid = self._userid + soledad_sessions = {userid: self._soledad} + keymanager_sessions = {userid: self._keymanager} + + key = cert = client_cert_path + opts = SendmailOpts(cert, key, host, port) + sendmail_opts = {userid: opts} - self._smtp_service, self._smtp_port = setup_smtp_gateway( - port=2013, - userid=self._userid, - keymanager=self._keymanager, - smtp_host=host, - smtp_port=port, - smtp_cert=client_cert_path, - smtp_key=client_cert_path, - encrypted_only=False) + from leap.mail.smtp import run_service + self._smtp_service, self._smtp_port = run_service( + soledad_sessions, keymanager_sessions, sendmail_opts) - def start_smtp_service(self, keymanager, userid, download_if_needed=False): + def start_smtp_service(self, soledad, keymanager, userid, + download_if_needed=False): """ Starts the SMTP service. @@ -170,6 +175,7 @@ class SMTPBootstrapper(AbstractBootstrapper): raise MalformedUserId() self._provider_config = ProviderConfig.get_provider_config(domain) + self._soledad = soledad self._keymanager = keymanager self._smtp_config = SMTPConfig() self._userid = str(userid) diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index 60a2130b..21cdee31 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -331,6 +331,9 @@ class SoledadBootstrapper(AbstractBootstrapper): :returns: the server url :rtype: unicode """ + if not self._soledad_config: + self._soledad_config = SoledadConfig() + # TODO: Select server based on timezone (issue #3308) server_dict = self._soledad_config.get_hosts() @@ -654,7 +657,7 @@ class Syncer(object): logger.error('Invalid auth token while trying to sync Soledad') self._signaler.signal( self._signaler.soledad_invalid_auth_token) - self._callback_deferred.fail(failure) + self._callback_deferred.errback(failure) elif failure.check(sqlite_ProgrammingError, sqlcipher_ProgrammingError): logger.exception("%r" % (failure.value,)) diff --git a/src/leap/bitmask/util/keyring_helpers.py b/src/leap/bitmask/util/keyring_helpers.py index d81f39b1..c5181348 100644 --- a/src/leap/bitmask/util/keyring_helpers.py +++ b/src/leap/bitmask/util/keyring_helpers.py @@ -24,8 +24,9 @@ try: EncryptedKeyring, PlaintextKeyring ] - canuse = lambda kr: (kr is not None - and kr.__class__ not in OBSOLETE_KEYRINGS) + canuse = lambda kr: ( + kr is not None and + kr.__class__ not in OBSOLETE_KEYRINGS) except Exception: # Problems when importing keyring! It might be a problem binding to the diff --git a/src/leap/bitmask/util/pastebin.py b/src/leap/bitmask/util/pastebin.py index 6d50ac9e..b476ad88 100644 --- a/src/leap/bitmask/util/pastebin.py +++ b/src/leap/bitmask/util/pastebin.py @@ -25,13 +25,13 @@ #############################################################################
+import urllib
+
__ALL__ = ['delete_paste', 'user_details', 'trending', 'pastes_by_user',
'generate_user_key', 'paste', 'Pastebin', 'PastebinError',
'PostLimitError']
-import urllib
-
class PastebinError(RuntimeError):
"""Pastebin API error.
|