summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/leap/bitmask/__init__.py57
-rw-r--r--src/leap/bitmask/_version.py576
-rw-r--r--src/leap/bitmask/app.py26
-rw-r--r--src/leap/bitmask/backend/api.py4
-rw-r--r--src/leap/bitmask/backend/components.py98
-rw-r--r--src/leap/bitmask/backend/leapbackend.py13
-rw-r--r--src/leap/bitmask/backend/leapsignaler.py1
-rw-r--r--src/leap/bitmask/cli/__init__.py0
-rwxr-xr-xsrc/leap/bitmask/cli/bitmask_cli.py341
-rw-r--r--src/leap/bitmask/config/leapsettings.py8
-rw-r--r--src/leap/bitmask/core/__init__.py2
-rw-r--r--src/leap/bitmask/core/_zmq.py68
-rw-r--r--src/leap/bitmask/core/api.py54
-rw-r--r--src/leap/bitmask/core/bitmaskd.tac11
-rw-r--r--src/leap/bitmask/core/configurable.py106
-rw-r--r--src/leap/bitmask/core/dispatcher.py273
-rw-r--r--src/leap/bitmask/core/dummy.py80
-rw-r--r--src/leap/bitmask/core/flags.py1
-rw-r--r--src/leap/bitmask/core/launcher.py47
-rw-r--r--src/leap/bitmask/core/mail_services.py688
-rw-r--r--src/leap/bitmask/core/service.py209
-rw-r--r--src/leap/bitmask/core/uuid_map.py115
-rw-r--r--src/leap/bitmask/core/web/__init__.py0
-rw-r--r--src/leap/bitmask/core/web/index.html70
-rw-r--r--src/leap/bitmask/core/web/root.py0
-rw-r--r--src/leap/bitmask/core/websocket.py98
-rw-r--r--src/leap/bitmask/gui/account.py9
-rw-r--r--src/leap/bitmask/gui/advanced_key_management.py3
-rw-r--r--src/leap/bitmask/gui/app.py34
-rw-r--r--src/leap/bitmask/gui/eip_status.py2
-rw-r--r--src/leap/bitmask/gui/logwindow.py4
-rw-r--r--src/leap/bitmask/gui/mail_status.py65
-rw-r--r--src/leap/bitmask/gui/mainwindow.py72
-rw-r--r--src/leap/bitmask/gui/passwordwindow.py2
-rw-r--r--src/leap/bitmask/gui/preferences_account_page.py30
-rw-r--r--src/leap/bitmask/gui/preferences_email_page.py204
-rw-r--r--src/leap/bitmask/gui/preferences_page.py50
-rw-r--r--src/leap/bitmask/gui/preferences_vpn_page.py31
-rw-r--r--src/leap/bitmask/gui/preferenceswindow.py144
-rw-r--r--src/leap/bitmask/gui/qt_browser.py72
-rw-r--r--src/leap/bitmask/gui/statemachines.py5
-rw-r--r--src/leap/bitmask/gui/ui/mail_status.ui235
-rw-r--r--src/leap/bitmask/gui/ui/mainwindow.ui4
-rw-r--r--src/leap/bitmask/gui/ui/preferences.ui22
-rw-r--r--src/leap/bitmask/gui/ui/preferences_email_page.ui629
-rw-r--r--src/leap/bitmask/gui/ui/wizard.ui4
-rw-r--r--src/leap/bitmask/pix.py207
-rw-r--r--src/leap/bitmask/platform_init/initializers.py82
-rw-r--r--src/leap/bitmask/provider/__init__.py2
-rw-r--r--src/leap/bitmask/services/__init__.py3
-rw-r--r--src/leap/bitmask/services/eip/darwinvpnlauncher.py65
-rw-r--r--src/leap/bitmask/services/eip/vpnlauncher.py8
-rw-r--r--src/leap/bitmask/services/eip/vpnprocess.py190
-rw-r--r--src/leap/bitmask/services/mail/conductor.py25
-rw-r--r--src/leap/bitmask/services/mail/imap.py30
-rw-r--r--src/leap/bitmask/services/mail/imapcontroller.py8
-rw-r--r--src/leap/bitmask/services/mail/plumber.py3
-rw-r--r--src/leap/bitmask/services/mail/smtpbootstrapper.py42
-rw-r--r--src/leap/bitmask/services/soledad/soledadbootstrapper.py5
-rw-r--r--src/leap/bitmask/util/keyring_helpers.py5
-rw-r--r--src/leap/bitmask/util/pastebin.py4
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>&nbsp;{3}</li>"
- "<li>&nbsp;{4}</li>"
- "<li>&nbsp;{5}</li>"
- "<li>&nbsp;{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 &lt;a href=&quot;https://bitmask.net/en/help/email&quot;&gt;https://bitmask.net/en/help/email&lt;/a&gt; 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.