summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKali Kaneko <kali@leap.se>2015-08-31 14:54:52 -0400
committerKali Kaneko <kali@leap.se>2015-08-31 14:54:52 -0400
commitf4547479fc050f338845f4f546d8dd7c0e4512eb (patch)
tree0f737c7f102674230f5467ecaf17720e1d28f6eb
parentdd43dad4b150adb66e571a56a8a5c053dec858d0 (diff)
parentfd27f48a35736d8ba186c423a7de15ffee5143dd (diff)
Merge tag '0.9.0rc2' into debian/experimental
Tag leap.bitmask version 0.9.0rc2
-rw-r--r--AUTHORS16
-rw-r--r--CHANGELOG.rst63
-rw-r--r--Makefile51
-rw-r--r--README.rst2
-rw-r--r--changes/VERSION_COMPAT5
-rw-r--r--data/images/menubar-mask-icon.pngbin0 -> 1818 bytes
-rw-r--r--data/resources/icons.qrc1
-rw-r--r--docs/dev/workflow.rst2
-rwxr-xr-xdocs/leap-autopep8.post-commit.hook15
-rwxr-xr-xdocs/leap-autopep8.post-commit.hook.ADD2
-rw-r--r--docs/leap-commit-template7
-rw-r--r--docs/leap-commit-template.README47
-rwxr-xr-xdocs/leap-flake8.pre-commit.hook7
-rw-r--r--docs/testing-rcs.README35
-rwxr-xr-xpkg/generate_wheels.sh13
-rw-r--r--pkg/leap_versions.txt4
-rwxr-xr-xpkg/linux/bitmask-launcher9
-rw-r--r--pkg/linux/bitmask-root4
-rw-r--r--pkg/linux/pyinst-notes.txt20
-rw-r--r--pkg/osx/Info.plist24
-rw-r--r--pkg/osx/README.rst27
-rw-r--r--pkg/osx/bitmask.icnsbin47303 -> 239193 bytes
-rw-r--r--pkg/osx/leap-client.spec36
-rwxr-xr-xpkg/pip_install_requirements.sh86
-rwxr-xr-xpkg/postmkvenv.sh11
l---------pkg/pyinst/bitmask.py1
-rw-r--r--pkg/pyinst/bitmask.spec38
-rw-r--r--pkg/requirements-dev.pip14
-rw-r--r--pkg/requirements-leap.pip4
-rw-r--r--pkg/requirements-testing.pip9
-rw-r--r--pkg/requirements.pip13
-rwxr-xr-xpkg/scripts/checkout_leap_versions.sh7
-rwxr-xr-xpkg/scripts/filter-bitmask-deps35
-rwxr-xr-xpkg/tools/get_authors.sh2
-rw-r--r--pkg/tuf/apply_updates.py83
-rw-r--r--pkg/utils.py18
-rw-r--r--relnotes.txt49
-rwxr-xr-xrun_tests.sh4
-rw-r--r--setup.cfg8
-rwxr-xr-xsetup.py275
-rw-r--r--src/leap/bitmask/_components.py6
-rw-r--r--src/leap/bitmask/app.py91
-rw-r--r--src/leap/bitmask/backend/api.py1
-rw-r--r--src/leap/bitmask/backend/backend.py135
-rw-r--r--src/leap/bitmask/backend/backend_proxy.py242
-rw-r--r--src/leap/bitmask/backend/components.py119
-rw-r--r--src/leap/bitmask/backend/leapbackend.py5
-rw-r--r--src/leap/bitmask/backend/leapsignaler.py1
-rw-r--r--src/leap/bitmask/backend/settings.py31
-rw-r--r--src/leap/bitmask/backend/signaler.py4
-rw-r--r--src/leap/bitmask/backend/signaler_qt.py4
-rw-r--r--src/leap/bitmask/backend/utils.py4
-rw-r--r--src/leap/bitmask/backend_app.py41
-rw-r--r--src/leap/bitmask/config/flags.py4
-rw-r--r--src/leap/bitmask/config/leapsettings.py36
-rw-r--r--src/leap/bitmask/config/providerconfig.py14
-rw-r--r--src/leap/bitmask/crypto/certs.py4
-rw-r--r--src/leap/bitmask/crypto/srpauth.py961
-rw-r--r--src/leap/bitmask/crypto/srpregister.py115
-rw-r--r--src/leap/bitmask/crypto/tests/test_srpregister.py64
-rw-r--r--src/leap/bitmask/frontend_app.py12
-rw-r--r--src/leap/bitmask/gui/advanced_key_management.py7
-rw-r--r--src/leap/bitmask/gui/app.py6
-rw-r--r--src/leap/bitmask/gui/eip_preferenceswindow.py5
-rw-r--r--src/leap/bitmask/gui/eip_status.py5
-rw-r--r--src/leap/bitmask/gui/login.py12
-rw-r--r--src/leap/bitmask/gui/logwindow.py (renamed from src/leap/bitmask/gui/loggerwindow.py)68
-rw-r--r--src/leap/bitmask/gui/mail_status.py223
-rw-r--r--src/leap/bitmask/gui/mainwindow.py400
-rw-r--r--src/leap/bitmask/gui/passwordwindow.py8
-rw-r--r--src/leap/bitmask/gui/preferences_account_page.py13
-rw-r--r--src/leap/bitmask/gui/preferences_email_page.py6
-rw-r--r--src/leap/bitmask/gui/preferenceswindow.py14
-rw-r--r--src/leap/bitmask/gui/signaltracker.py6
-rw-r--r--src/leap/bitmask/gui/statemachines.py5
-rw-r--r--src/leap/bitmask/gui/ui/wizard.ui56
-rw-r--r--src/leap/bitmask/gui/wizard.py76
-rw-r--r--src/leap/bitmask/logs/__init__.py6
-rw-r--r--src/leap/bitmask/logs/leap_log_handler.py137
-rw-r--r--src/leap/bitmask/logs/log_silencer.py90
-rw-r--r--src/leap/bitmask/logs/safezmqhandler.py131
-rw-r--r--src/leap/bitmask/logs/tests/test_leap_log_handler.py120
-rw-r--r--src/leap/bitmask/logs/utils.py255
-rw-r--r--src/leap/bitmask/platform_init/initializers.py6
-rw-r--r--src/leap/bitmask/platform_init/locks.py11
-rw-r--r--src/leap/bitmask/provider/__init__.py13
-rw-r--r--src/leap/bitmask/provider/pinned.py5
-rw-r--r--src/leap/bitmask/provider/pinned_riseup.py2
-rw-r--r--src/leap/bitmask/provider/providerbootstrapper.py4
-rw-r--r--src/leap/bitmask/services/__init__.py4
-rw-r--r--src/leap/bitmask/services/abstractbootstrapper.py10
-rw-r--r--src/leap/bitmask/services/eip/conductor.py5
-rw-r--r--src/leap/bitmask/services/eip/darwinvpnlauncher.py4
-rw-r--r--src/leap/bitmask/services/eip/eipbootstrapper.py4
-rw-r--r--src/leap/bitmask/services/eip/eipconfig.py28
-rw-r--r--src/leap/bitmask/services/eip/linuxvpnlauncher.py4
-rw-r--r--src/leap/bitmask/services/eip/vpnlauncher.py39
-rw-r--r--src/leap/bitmask/services/eip/vpnprocess.py15
-rw-r--r--src/leap/bitmask/services/eip/windowsvpnlauncher.py5
-rw-r--r--src/leap/bitmask/services/mail/conductor.py64
-rw-r--r--src/leap/bitmask/services/mail/imap.py39
-rw-r--r--src/leap/bitmask/services/mail/imapcontroller.py68
-rw-r--r--src/leap/bitmask/services/mail/plumber.py16
-rw-r--r--src/leap/bitmask/services/mail/smtpbootstrapper.py7
-rw-r--r--src/leap/bitmask/services/mail/smtpconfig.py4
-rw-r--r--src/leap/bitmask/services/soledad/soledadbootstrapper.py451
-rw-r--r--src/leap/bitmask/services/soledad/soledadconfig.py5
-rw-r--r--src/leap/bitmask/updater.py174
-rw-r--r--src/leap/bitmask/util/__init__.py12
-rw-r--r--src/leap/bitmask/util/autostart.py4
-rw-r--r--src/leap/bitmask/util/keyring_helpers.py5
-rw-r--r--src/leap/bitmask/util/leap_argparse.py61
-rw-r--r--src/leap/bitmask/util/polkit_agent.py4
-rw-r--r--src/leap/bitmask/util/privilege_policies.py5
-rw-r--r--src/leap/bitmask/util/requirement_checker.py6
115 files changed, 3415 insertions, 2259 deletions
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 00000000..cbd35da3
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,16 @@
+Tomás Touceda <chiiph@leap.se>
+Ivan Alejandro <ivanalejandro0@gmail.com>
+Kali Kaneko <kali@leap.se>
+drebs <drebs@leap.se>
+antialias <antialias@leap.se>
+elijah <elijah@riseup.net>
+Ruben Pollan <meskio@sindominio.net>
+k clair <kclair@riseup.net>
+Jaromil <jaromil@dyne.org>
+kwadronaut <kwadronaut@leap.se>
+Duda Dornelles <ddornell@thoughtworks.com>
+Bruno Wagner Goncalves <bwagner@thoughtworks.com>
+Parménides GV <parmegv@sdf.org>
+Neissi Lima <neissi.lima@gmail.com>
+Micah Anderson <micah@riseup.net>
+irregulator <irregulator@riseup.net>
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c46ff20d..952574dc 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -3,6 +3,69 @@
Changelog
---------
+0.9.0rc2 August 27
+++++++++++++++++++
+
+Features
+~~~~~~~~
+- `#7250 <https://leap.se/code/issues/7250>`_: Enable '--danger' for stable versions.
+- `#7291 <https://leap.se/code/issues/7291>`_: Move the updater code from the launcher to the client.
+- `#7342 <https://leap.se/code/issues/7342>`_: Added apply_updates.py script for the pyinstaller bundle.
+- `#7353 <https://leap.se/code/issues/7353>`_: Add notifications of soledad sync progress to UI.
+- `#7356 <https://leap.se/code/issues/7356>`_: Allow to disable EIP component on build.
+
+Bugfixes
+~~~~~~~~
+- `#6594 <https://leap.se/code/issues/6594>`_: Handle disabled registration on provider.
+- `#7149 <https://leap.se/code/issues/7149>`_: Start the events server when reactor is running.
+- `#7273 <https://leap.se/code/issues/7273>`_: Logbook subscriber stop fails if not started.
+- `#7273 <https://leap.se/code/issues/7273>`_: ZMQError: address already in use - logbook subscriber already started.
+- `#7281 <https://leap.se/code/issues/7281>`_: Support a provider not providing location for the eip gateways.
+- `#7319 <https://leap.se/code/issues/7319>`_: Raise the maxfiles limit in OSX
+- `#7343 <https://leap.se/code/issues/7343>`_: Clean up and fix the tests.
+
+
+
+0.9.0rc1 July 10
+++++++++++++++++
+
+Features
+~~~~~~~~
+- `#5526 <https://leap.se/code/issues/5526>`_: Make "check" button selected by default.
+- `#6359 <https://leap.se/code/issues/6359>`_: Adapt bitmask to the new events api on leap.common.
+- `#6360 <https://leap.se/code/issues/6360>`_: Use txzmq in backend.
+- `#6368 <https://leap.se/code/issues/6368>`_: Add support to the new async-api of keymanager.
+- `#6683 <https://leap.se/code/issues/6683>`_: Add ability to generate sumo tarball.
+- `#6713 <https://leap.se/code/issues/6713>`_: Add support for xfce-polkit agent.
+- `#6876 <https://leap.se/code/issues/6876>`_: Update api port for pinned riseup.
+- `#7139 <https://leap.se/code/issues/7139>`_: Use logbook zmq handler to centralize logging.
+- `#7140 <https://leap.se/code/issues/7140>`_: Implement a thread-safe zmq handler for logbook.
+- `#7141 <https://leap.se/code/issues/7141>`_: Add log handler to display colored logs on the terminal.
+- `#7142 <https://leap.se/code/issues/7142>`_: Add log handler to store logs on bitmask.log.
+- `#7143 <https://leap.se/code/issues/7143>`_: Adapt existing log filter/silencer to the new logbook handler.
+- `#7144 <https://leap.se/code/issues/7144>`_: Replace logging handler with logbook handler bitmask-wide.
+- `#7162 <https://leap.se/code/issues/7162>`_: Log LSB-release info if available.
+- `#7180 <https://leap.se/code/issues/7180>`_: Add log rotation for bitmask.log.
+- `#7184 <https://leap.se/code/issues/7184>`_: Forward twisted logs to logging and handle logging logs with logbook.
+- Add support to the new async-api of soledad
+
+Bugfixes
+~~~~~~~~
+- `#6418 <https://leap.se/code/issues/6418>`_: Cannot change preseeded providers if checks for one fail.
+- `#6424 <https://leap.se/code/issues/6424>`_: Do not disable autostart if the quit is triggered by a system logout.
+- `#6541 <https://leap.se/code/issues/6541>`_: Client must honor the ports specified in eip-service.json.
+- `#6654 <https://leap.se/code/issues/6654>`_: Regression fix, login attempt is made against previously selected provider.
+- `#6682 <https://leap.se/code/issues/6682>`_: Handle user cancel keyring open operation, this prevents a bitmask freeze.
+- `#6894 <https://leap.se/code/issues/6894>`_: Change 'ip' command location to support Fedora/RHEL distros.
+- `#7093 <https://leap.se/code/issues/7093>`_: Fix controller attribute error.
+- `#7126 <https://leap.se/code/issues/7126>`_: Don't run the event server on the backend for the standalone bundle since the launcher takes care of that.
+- `#7185 <https://leap.se/code/issues/7185>`_: Log contains exported PGP Private Key.
+- `#7222 <https://leap.se/code/issues/7222>`_: Run the zmq log subscriber in the background to avoid hitting the zmq's buffer limits.
+- `#6536 <https://leap.se/code/issues/6536>`_, `#6568 <https://leap.se/code/issues/6568>`_, `#6691 <https://leap.se/code/issues/6691>`_: Refactor soledad sync to do it the twisted way.
+- Fix the bootstrap script for developers so it works on Fedora/RHEL systems where there is /usr/lib64 for python libs.
+- Fix soledad bootstrap sync retries.
+
+
0.8.1 February 25
+++++++++++++++++
diff --git a/Makefile b/Makefile
index 59d253a6..7d7c8e23 100644
--- a/Makefile
+++ b/Makefile
@@ -43,6 +43,8 @@ LRELE = lrelease
#################################
# DO NOT EDIT FOLLOWING
+LEAP_REPOS = leap_pycommon keymanager leap_mail soledad
+
COMPILED_UI = $(UI_FILES:%.ui=$(COMPILED_DIR)/ui_%.py)
COMPILED_RESOURCES = $(RESOURCES:%.qrc=$(COMPILED_DIR)/%_rc.py)
@@ -56,7 +58,9 @@ ifndef RESOURCE_TIME
export RESOURCE_TIME=10
endif
-#
+CURDIR = $(shell pwd)
+
+###########################################
all : resources ui
@@ -75,11 +79,6 @@ $(COMPILED_DIR)/ui_%.py : $(UI_DIR)/%.ui
$(COMPILED_DIR)/%_rc.py : $(RESOURCE_DIR)/%.qrc
$(PYRCC) $< -o $@
-deb:
- #XXX finish this!
- #should tag upstream/VERSION in upstream branch...
- #@git tag -a upstream/$(DEBVER) -m "..."
- @git-buildpackage --git-ignore-new --git-builder="debuild -us -uc -i'.*|bin|share|lib|local|include|\.git'" --git-upstream-branch=upstream --git-upstream-tree=branch --git-debian-branch=debian
manpages:
rst2man docs/man/bitmask.1.rst docs/man/bitmask.1
@@ -124,5 +123,45 @@ install_wheel:
# if it's the first time, you'll need to get_wheels first
pip install --pre --use-wheel --no-index --find-links=../wheelhouse -r pkg/requirements.pip
+gather_deps:
+ pipdeptree | pkg/scripts/filter-bitmask-deps
+
+install_base_deps:
+ for repo in leap_pycommon keymanager leap_mail soledad/common soledad/client; do cd $(CURDIR)/../$$repo && pkg/pip_install_requirements.sh; done
+ pkg/pip_install_requirements.sh
+
+pull_leapdeps:
+ for repo in $(LEAP_REPOS); do cd $(CURDIR)/../$$repo && git pull; done
+
+checkout_leapdeps_develop:
+ for repo in $(LEAP_REPOS); do cd $(CURDIR)/../$$repo && git checkout develop; done
+
+checkout_leapdeps_release:
+ pkg/scripts/checkout_leap_versions.sh
+
+sumo_tarball_release: checkout_leapdeps_release
+ python setup.py sdist --sumo
+ git checkout -- src/leap/__init__.py
+ git checkout -- src/leap/bitmask/_version.py
+ rm -rf src/leap/soledad
+
+# XXX We need two sets of sumo-tarballs: the one published for a release
+# (that will pick the pinned leap deps), and the other which will be used
+# for the nightly builds.
+# TODO change naming scheme for sumo-latest: should include date (in case
+# bitmask is not updated bu the dependencies are)
+
+sumo_tarball_latest: checkout_leapdeps_develop pull_leapdeps
+ python setup.py sdist --sumo # --latest
+ git checkout -- src/leap/__init__.py
+ git checkout -- src/leap/bitmask/_version.py
+ rm -rf src/leap/soledad
+
+pyinst:
+ pyinstaller -y pkg/pyinst/bitmask.spec
+
+clean_pkg:
+ rm -rf build dist
+
clean :
$(RM) $(COMPILED_UI) $(COMPILED_RESOURCES) $(COMPILED_UI:.py=.pyc) $(COMPILED_RESOURCES:.py=.pyc)
diff --git a/README.rst b/README.rst
index ff84d56b..5b611b95 100644
--- a/README.rst
+++ b/README.rst
@@ -6,8 +6,6 @@ Bitmask
.. image:: https://pypip.in/v/leap.bitmask/badge.png
:target: https://crate.io/packages/leap.bitmask
.. image:: https://pypip.in/d/leap.bitmask/badge.png
-.. image:: http://lemur.leap.se:8010/png?builder=bitmask-linux-quick
-
**Bitmask** is the multiplatform desktop client for the services offered by
`the LEAP Platform`_.
diff --git a/changes/VERSION_COMPAT b/changes/VERSION_COMPAT
index 1eadcbe0..f35d01c6 100644
--- a/changes/VERSION_COMPAT
+++ b/changes/VERSION_COMPAT
@@ -4,8 +4,11 @@
# Add your changes here so we can properly update
# requirements.pip during the release process.
# (leave header when resetting)
+#
+# When bumping it during the release cycle
+# remember to update also pkg/leap_versions.txt
#################################################
#
# BEGIN DEPENDENCY LIST -------------------------
# leap.foo.bar>=x.y.z
-leap.keymanager>=0.4.0
+leap.mail>=0.4.0 # this is not tagged/released yet
diff --git a/data/images/menubar-mask-icon.png b/data/images/menubar-mask-icon.png
new file mode 100644
index 00000000..a478cc96
--- /dev/null
+++ b/data/images/menubar-mask-icon.png
Binary files differ
diff --git a/data/resources/icons.qrc b/data/resources/icons.qrc
index 7fda6197..177a80ec 100644
--- a/data/resources/icons.qrc
+++ b/data/resources/icons.qrc
@@ -3,6 +3,7 @@
<!-- used as window icon -->
<file>../images/mask-icon.png</file>
+ <file>../images/menubar-mask-icon.png</file>
<!-- round status icons -->
<file>../images/black/off.svg</file>
diff --git a/docs/dev/workflow.rst b/docs/dev/workflow.rst
index f217df24..689b8feb 100644
--- a/docs/dev/workflow.rst
+++ b/docs/dev/workflow.rst
@@ -48,7 +48,7 @@ All code ready to be merged into the integration branch is expected to:
* Have tests
* Be documented
-* Pass existing tests: do **run_tests.sh** and **tox -v**. All feature branches are automagically built by our `buildbot farm <http://lemur.leap.se:8010/grid>`_. So please check your branch is green before merging it it to `develop`. Rebasing against the current tip of the integration when possible is preferred in order to keep a clean history.
+* Pass existing tests: do **run_tests.sh** and **tox -v**. Rebasing against the current tip of the integration when possible is preferred in order to keep a clean history.
Using Github
------------
diff --git a/docs/leap-autopep8.post-commit.hook b/docs/leap-autopep8.post-commit.hook
new file mode 100755
index 00000000..cffb1d53
--- /dev/null
+++ b/docs/leap-autopep8.post-commit.hook
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+# Auto pep8 correction as a post-commit hook.
+# Thanks to http://victorlin.me/posts/2014/02/05/auto-post-commit-pep8-correction
+
+echo "[+] running autopep8..."
+FILES=$(git diff HEAD^ HEAD --name-only --diff-filter=ACM | grep -e '\.py$')
+
+for f in $FILES
+do
+ # auto pep8 correction
+ autopep8 --in-place $f
+done
+
+git status
diff --git a/docs/leap-autopep8.post-commit.hook.ADD b/docs/leap-autopep8.post-commit.hook.ADD
new file mode 100755
index 00000000..b6e07ae5
--- /dev/null
+++ b/docs/leap-autopep8.post-commit.hook.ADD
@@ -0,0 +1,2 @@
+ #!/bin/sh
+ cd .git/hooks && ln -s ../../docs/leap-autopep8.post-commit.hook post-commit
diff --git a/docs/leap-commit-template b/docs/leap-commit-template
new file mode 100644
index 00000000..8a5c7cd0
--- /dev/null
+++ b/docs/leap-commit-template
@@ -0,0 +1,7 @@
+[bug|feat|docs|style|refactor|test|pkg|i18n] ...
+...
+
+- Resolves: #XYZ
+- Related: #XYZ
+- Documentation: #XYZ
+- Releases: XYZ
diff --git a/docs/leap-commit-template.README b/docs/leap-commit-template.README
new file mode 100644
index 00000000..ce8809e7
--- /dev/null
+++ b/docs/leap-commit-template.README
@@ -0,0 +1,47 @@
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+HOW TO USE THIS TEMPLATE:
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Run `git config commit.template docs/leap-commit-template` or
+edit the .git/config for this project and add
+`template = docs/leap-commit-template`
+under the [commit] block
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+COMMIT TEMPLATE FORMAT EXPLAINED
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+[type] <subject>
+
+<body>
+<footer>
+
+Type should be one of the following:
+- bug (bug fix)
+- feat (new feature)
+- docs (changes to documentation)
+- style (formatting, pep8 violations, etc; no code change)
+- refactor (refactoring production code)
+- test (adding missing tests, refactoring tests; no production code change)
+- pkg (packaging related changes; no production code change)
+- i18n translation related changes
+
+Subject should use imperative tone and say what you did.
+For example, use 'change', NOT 'changed' or 'changes'.
+
+The body should go into detail about changes made.
+
+The footer should contain any issue references or actions.
+You can use one or several of the following:
+
+- Resolves: #XYZ
+- Related: #XYZ
+- Documentation: #XYZ
+- Releases: XYZ
+
+The Documentation field should be included in every new feature commit, and it
+should link to an issue in the bug tracker where the new feature is analyzed
+and documented.
+
+For a full example of how to write a good commit message, check out
+https://github.com/sparkbox/how_to/tree/master/style/git
diff --git a/docs/leap-flake8.pre-commit.hook b/docs/leap-flake8.pre-commit.hook
new file mode 100755
index 00000000..b00fd08a
--- /dev/null
+++ b/docs/leap-flake8.pre-commit.hook
@@ -0,0 +1,7 @@
+#!/bin/sh
+# Auto-check for pep8 so I don't check in bad code
+FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -e '\.py$')
+
+if [ -n "$FILES" ]; then
+ flake8 -r $FILES
+fi
diff --git a/docs/testing-rcs.README b/docs/testing-rcs.README
new file mode 100644
index 00000000..b0340f0a
--- /dev/null
+++ b/docs/testing-rcs.README
@@ -0,0 +1,35 @@
+Tips for QA
+------------
+
+From time to time, we'll ask the community for help testing a new alpha release
+or a release candidate. Normally, we'll offer a link for the download of a
+self-contained bundle just for internal testing purposes. These will be updated
+quite often, as soon as there are fixes available to fix the release-critical
+bugs.
+
+If you want to give a hand in this process, please follow the following tips:
+
+- Focus all your efforts, if possible, on whatever is *the* golden distro at
+ the time of the release. This currently is: Ubuntu 14.04.x LTS, 64bits, with
+Unity as the default desktop environment.
+ It's very important to have a reference environment as bug-free as possible,
+ before trying to solve issues that are present in other distributions or window
+ managers.
+- Identify all issues that need help in the QA phase. You can do that going to
+ the bug tracker, and filtering all the issues for a given release that are in
+ the QA state.
+- If the issue is solved in your tests for this alpha release, please add a
+ comment to the issue stating the results of your tests, and the platform and
+ desktop environment in which your tests took place. But please do not change
+ the QA status on the issue. We generally leave this role to the author of the
+ original issue, or to the person playing the role of the release QA master.
+- Always test with a newly created account (specially relevant when testing
+ email candidates)
+- Always test with the reference Mail User Agent (currently, Thunderbird, in
+ whatever version is present in the reference distribution).
+- Remove also any thunderbird configuration, start a freshly configured account.
+- If you find a new bug, please make sure that it hasn't already been reported
+ in the issue tracker. If you are absolutely certain that you have found a new
+ bug, please attach a log of a new bitmask session, which should contain
+ *only* the behaviour needed to reproduce the bug you are reporting.
+
diff --git a/pkg/generate_wheels.sh b/pkg/generate_wheels.sh
new file mode 100755
index 00000000..a13e2c7a
--- /dev/null
+++ b/pkg/generate_wheels.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+# Generate wheels for dependencies
+# Use at your own risk.
+
+if [ "$WHEELHOUSE" = "" ]; then
+ WHEELHOUSE=$HOME/wheelhouse
+fi
+
+pip wheel --wheel-dir $WHEELHOUSE pip
+pip wheel --wheel-dir $WHEELHOUSE -r pkg/requirements.pip
+if [ -f pkg/requirements-testing.pip ]; then
+ pip wheel --wheel-dir $WHEELHOUSE -r pkg/requirements-testing.pip
+fi
diff --git a/pkg/leap_versions.txt b/pkg/leap_versions.txt
new file mode 100644
index 00000000..0351c758
--- /dev/null
+++ b/pkg/leap_versions.txt
@@ -0,0 +1,4 @@
+soledad 0.7.1
+keymanager 0.4.1
+leap_common 0.4.0
+leap_mail 0.4.0rc1
diff --git a/pkg/linux/bitmask-launcher b/pkg/linux/bitmask-launcher
new file mode 100755
index 00000000..550dd134
--- /dev/null
+++ b/pkg/linux/bitmask-launcher
@@ -0,0 +1,9 @@
+#!/bin/sh
+# The Bitmask Launcher
+# (c) 2015 - The LEAP Encryption Access Project
+
+[ -f libQtCore.so.4 ] || ln -s libQtCore.so.4.orig libQtCore.so.4
+[ -f libQtGui.so.4 ] || ln -s libQtGui.so.4.orig libQtGui.so.4
+cat /etc/os-release | grep ID | grep -i ubuntu && unlink libQtCore.so.4 && unlink libQtGui.so.4
+
+./bitmask-app "$@"
diff --git a/pkg/linux/bitmask-root b/pkg/linux/bitmask-root
index 6fb1f0b3..80ac12e8 100644
--- a/pkg/linux/bitmask-root
+++ b/pkg/linux/bitmask-root
@@ -73,7 +73,7 @@ def get_no_group_name():
return None
-VERSION = "5"
+VERSION = "6"
SCRIPT = "bitmask-root"
NAMESERVER = "10.42.0.1"
BITMASK_CHAIN = "bitmask"
@@ -85,7 +85,7 @@ LOCAL_INTERFACE = "lo"
IMAP_PORT = "1984"
SMTP_PORT = "2013"
-IP = "/bin/ip"
+IP = "/sbin/ip"
IPTABLES = "/sbin/iptables"
IP6TABLES = "/sbin/ip6tables"
diff --git a/pkg/linux/pyinst-notes.txt b/pkg/linux/pyinst-notes.txt
new file mode 100644
index 00000000..e4310e6d
--- /dev/null
+++ b/pkg/linux/pyinst-notes.txt
@@ -0,0 +1,20 @@
+hacks
+-----
+**because nobody's perfect, at least the first time**
+
+missing osrandom_engine header
+===============================
+
+1. extract osrandom_engine.* from original cryptography tarball:
+
+cp /src/cryptography/hazmat/bindings/openssl/src/osrandom_engine.h /tmp
+cp /src/cryptography/hazmat/bindings/openssl/src/osrandom_engine.c /tmp
+
+cd dist/bitmask
+mkdir -p cryptography/hazmat/bindings/openssl/src
+cp /tmp/osrandom_engine.* cryptography/hazmat/bindings/openssl/src
+
+missing dbschema.sql
+====================
+mkdir -p u1db/backends
+cp ~VIRTUAL_ENV/lib/python2.7/site-packages/u1db/backends/dbschema.sql u1db/backends
diff --git a/pkg/osx/Info.plist b/pkg/osx/Info.plist
deleted file mode 100644
index dc427c4a..00000000
--- a/pkg/osx/Info.plist
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>CFBundleDisplayName</key>
- <string>Bitmask</string>
- <key>CFBundleExecutable</key>
- <string>app</string>
- <key>CFBundleIconFile</key>
- <string>bitmask.icns</string>
- <key>CFBundleInfoDictionaryVersion</key>
- <string>6.0</string>
- <key>CFBundleName</key>
- <string>Bitmask</string>
- <key>CFBundlePackageType</key>
- <string>APPL</string>
- <key>CFBundleShortVersionString</key>
- <string>1</string>
- <key>LSBackgroundOnly</key>
- <false/>
- <key>CFBundleIdentifier</key>
- <string>se.leap.bitmask</string>
-</dict>
-</plist>
diff --git a/pkg/osx/README.rst b/pkg/osx/README.rst
index 03aac4f2..92799ebd 100644
--- a/pkg/osx/README.rst
+++ b/pkg/osx/README.rst
@@ -1,31 +1,28 @@
environment setup in osx
========================
-(I rm'd my README by mistake at some point. Re-do).
+
+TODO:: REALLY old notes, adapting to newest flow.
basically you need this to setup your environment:
# check and consolidate
-# install xcode and macports
-# port -v selfupdate
-# port install python26
-# port install python_select
-# port select python python26
-# port install py26-pyqt4
-# port install py26-pip
-# port install py26-virtualenv
-# port install git-core
-# port install platypus
-# port install upx
+# install xcode and homebrew
+
+# brew install python2.7
+# brew install python-virtualenwrapper
+# brew install qt
+# brew install git
+# brew install platypus
+# brew install upx
Requirements
============
pyinstaller
-----------
-Expected in ~/pyinstaller
-You need the development version.
-Tested with: 2.0.373
+You need the development version. do `python setup.py develop` inside your
+virtualenv.
platypus (tested with latest macports)
diff --git a/pkg/osx/bitmask.icns b/pkg/osx/bitmask.icns
index 7cc3e752..74fa0af6 100644
--- a/pkg/osx/bitmask.icns
+++ b/pkg/osx/bitmask.icns
Binary files differ
diff --git a/pkg/osx/leap-client.spec b/pkg/osx/leap-client.spec
deleted file mode 100644
index 91aa20d6..00000000
--- a/pkg/osx/leap-client.spec
+++ /dev/null
@@ -1,36 +0,0 @@
-# -*- mode: python -*-
-a = Analysis(['../../src/leap/app.py'],
- pathex=[
- '../../src/leap',
- '/Users/kaliy/leap/leap_client/src/leap-client/pkg/osx'],
- hiddenimports=['atexit', 'leap.common'],
- hookspath=None)
-pyz = PYZ(a.pure)
-exe = EXE(pyz,
- a.scripts,
- exclude_binaries=1,
- name=os.path.join('build/pyi.darwin/leap-client', 'app'),
- debug=False,
- strip=True,
- upx=True,
- console=False)
-coll = COLLECT(exe,
- a.binaries +
- # this will easitly break if we setup the venv
- # somewhere else. FIXME
- [('cacert.pem', '/Users/kaliy/.Virtualenvs/leap-client/lib/python2.6/site-packages/requests-1.1.0-py2.6.egg/requests/cacert.pem', 'DATA'),
- ],
- a.zipfiles,
- a.datas,
- strip=True,
- upx=True,
- name=os.path.join('dist', 'app'))
-app = BUNDLE(coll,
- name=os.path.join('dist', 'leap-client.app'))
-
-import sys
-if sys.platform.startswith("darwin"):
- app = BUNDLE(coll,
- name=os.path.join('dist', 'LEAP Client.app'),
- appname='LEAP Client',
- version=1)
diff --git a/pkg/pip_install_requirements.sh b/pkg/pip_install_requirements.sh
new file mode 100755
index 00000000..6d8ed28b
--- /dev/null
+++ b/pkg/pip_install_requirements.sh
@@ -0,0 +1,86 @@
+#!/bin/bash
+# Update pip and install LEAP base/testing requirements.
+# For convenience, $insecure_packages are allowed with insecure flags enabled.
+# Use at your own risk.
+# See $usage for help
+
+insecure_packages="dirspec"
+leap_wheelhouse=https://lizard.leap.se/wheels
+
+show_help() {
+ usage="Usage: $0 [--testing] [--use-leap-wheels]\n --testing\t\tInstall dependencies from requirements-testing.pip\n
+\t\t\tOtherwise, it will install requirements.pip\n
+--use-leap-wheels\tUse wheels from leap.se"
+ echo -e $usage
+
+ exit 1
+}
+
+process_arguments() {
+ testing=false
+ use_leap_wheels=false
+
+ while [ "$#" -gt 0 ]; do
+ # From http://stackoverflow.com/a/31443098
+ case "$1" in
+ --help) show_help;;
+ --testing) testing=true; shift 1;;
+ --use-leap-wheels) use_leap_wheels=true; shift 1;;
+
+ -h) show_help;;
+ -*) echo "unknown option: $1" >&2; exit 1;;
+ esac
+ done
+}
+
+return_wheelhouse() {
+ if $use_leap_wheels ; then
+ WHEELHOUSE=$leap_wheelhouse
+ elif [ "$WHEELHOUSE" = "" ]; then
+ WHEELHOUSE=$HOME/wheelhouse
+ fi
+
+ # Tested with bash and zsh
+ if [[ $WHEELHOUSE != http* && ! -d "$WHEELHOUSE" ]]; then
+ mkdir $WHEELHOUSE
+ fi
+
+ echo "$WHEELHOUSE"
+}
+
+return_install_options() {
+ wheelhouse=`return_wheelhouse`
+ install_options="-U --find-links=$wheelhouse"
+ if $use_leap_wheels ; then
+ install_options="$install_options --trusted-host lizard.leap.se"
+ fi
+
+ echo $install_options
+}
+
+return_insecure_flags() {
+ for insecure_package in $insecure_packages; do
+ flags="$flags --allow-external $insecure_package --allow-unverified $insecure_package"
+ done
+
+ echo $flags
+}
+
+return_packages() {
+ if $testing ; then
+ packages="-r pkg/requirements-testing.pip"
+ else
+ packages="-r pkg/requirements.pip"
+ fi
+
+ echo $packages
+}
+
+process_arguments $@
+install_options=`return_install_options`
+insecure_flags=`return_insecure_flags`
+packages=`return_packages`
+
+pip install -U wheel
+pip install $install_options pip
+pip install $install_options $insecure_flags $packages
diff --git a/pkg/postmkvenv.sh b/pkg/postmkvenv.sh
index 7b06fa6d..2407c69b 100755
--- a/pkg/postmkvenv.sh
+++ b/pkg/postmkvenv.sh
@@ -21,18 +21,21 @@ LIBS=( PySide pysideuic )
PYTHON_VERSION=python$(python -c "import sys; print (str(sys.version_info[0])+'.'+str(sys.version_info[1]))")
VAR=( $(which -a $PYTHON_VERSION) )
-GET_PYTHON_LIB_CMD="from distutils.sysconfig import get_python_lib; print (get_python_lib())"
+# this takes care of the /usr/lib vs /usr/lib64 differences between platforms
+GET_PYTHON_LIB_CMD="from distutils.sysconfig import get_python_lib; print (get_python_lib(plat_specific=True))"
+GET_PYSIDE_LIB_CMD="import PySide; print '/'.join(PySide.__path__[0].split('/')[:-1])"
+
LIB_VIRTUALENV_PATH=$(python -c "$GET_PYTHON_LIB_CMD")
if [[ $platform == 'linux' ]]; then
LIB_SYSTEM_PATH=$(${VAR[-1]} -c "$GET_PYTHON_LIB_CMD")
elif [[ $platform == 'darwin' ]]; then
ORIGINAL_PATH=$PATH
- #change first colon of path to | because path substitution is greedy
+ # change first colon of path to | because path substitution is greedy
PATH=${PATH/:/|}
- #remove everything up to | from path
+ # remove everything up to | from path
PATH=${PATH/*|/}
- LIB_SYSTEM_PATH=$(python -c "$GET_PYTHON_LIB_CMD")
+ LIB_SYSTEM_PATH=$(/usr/bin/python -c "$GET_PYSIDE_LIB_CMD")
PATH=$ORIGINAL_PATH
else
echo "unsupported platform; not doing symlinks"
diff --git a/pkg/pyinst/bitmask.py b/pkg/pyinst/bitmask.py
new file mode 120000
index 00000000..3da791e6
--- /dev/null
+++ b/pkg/pyinst/bitmask.py
@@ -0,0 +1 @@
+../../src/leap/bitmask/app.py \ No newline at end of file
diff --git a/pkg/pyinst/bitmask.spec b/pkg/pyinst/bitmask.spec
new file mode 100644
index 00000000..2bc2f9d2
--- /dev/null
+++ b/pkg/pyinst/bitmask.spec
@@ -0,0 +1,38 @@
+# -*- mode: python -*-
+
+block_cipher = None
+
+
+a = Analysis([os.path.join('pkg', 'pyinst', 'bitmask.py')],
+ hiddenimports=[
+ 'zope.interface', 'zope.proxy',
+ 'PySide.QtCore', 'PySide.QtGui'],
+ hookspath=None,
+ runtime_hooks=None,
+ excludes=None,
+ cipher=block_cipher)
+pyz = PYZ(a.pure,
+ cipher=block_cipher)
+exe = EXE(pyz,
+ a.scripts,
+ exclude_binaries=True,
+ name='bitmask',
+ debug=False,
+ strip=False,
+ upx=True,
+ console=False )
+coll = COLLECT(exe,
+ a.binaries,
+ a.zipfiles,
+ a.datas,
+ strip=False,
+ upx=True,
+ name='bitmask')
+if sys.platform.startswith("darwin"):
+ app = BUNDLE(coll,
+ name=os.path.join(
+ 'dist', 'Bitmask.app'),
+ appname='Bitmask',
+ version='0.9.0rc2',
+ icon='pkg/osx/bitmask.icns',
+ bundle_identifier='bitmask-0.9.0rc2')
diff --git a/pkg/requirements-dev.pip b/pkg/requirements-dev.pip
index 799376d2..45f5fa70 100644
--- a/pkg/requirements-dev.pip
+++ b/pkg/requirements-dev.pip
@@ -1,3 +1,13 @@
+# ------------------------------------
+# -- useful tools that you probably --
+# -- will want during development --
+# ------------------------------------
+
+wheel
+sphinx
+ipdb
+pipdeptree
+
# ---------------------------
# -- external requirements --
# -- during development --
@@ -11,11 +21,7 @@
# to install it. (do it after python setup.py develop and it
# will only install this)
#
-wheel
-sphinx
-ipdb
# in case you want to install a package from a git source, you can use this:
# Useful to test pre-release branches together.
#-e git+https://github.com/leapcode/leap_pycommon.git@develop#egg=leap.common
-#-e git+https://github.com/leapcode/soledad.git@develop#egg=leap.soledad
diff --git a/pkg/requirements-leap.pip b/pkg/requirements-leap.pip
new file mode 100644
index 00000000..df2d1974
--- /dev/null
+++ b/pkg/requirements-leap.pip
@@ -0,0 +1,4 @@
+leap.soledad.client>=0.6.0
+leap.keymanager>=0.4.0
+leap.mail>=0.4.0rc1
+leap.common>=0.4.0
diff --git a/pkg/requirements-testing.pip b/pkg/requirements-testing.pip
index e789664a..ede94516 100644
--- a/pkg/requirements-testing.pip
+++ b/pkg/requirements-testing.pip
@@ -7,12 +7,5 @@ mock
unittest2 # TODO we should include this dep only for python2.6
coverage
pep8>=1.1
+flake8
tox
-
-#sphinx>=1.1.2
-
-# double reqs
-# (the client already includes, which gives some errors)
-# -----------
-#twisted
-#zope.interface
diff --git a/pkg/requirements.pip b/pkg/requirements.pip
index 8baaecdb..85645763 100644
--- a/pkg/requirements.pip
+++ b/pkg/requirements.pip
@@ -9,9 +9,7 @@ argparse
requests>=1.1.0
srp>=1.0.2
pyopenssl
-
-# This won't be needed after we refactor leap.common.events to use zmq.
-python-dateutil
+coloredlogs
psutil
@@ -24,11 +22,10 @@ zope.proxy
# You will want to install this bundled if you don't have sodium in your system:
# pip install pyzmq --install-option="--zmq=bundled"
pyzmq
-
-leap.common>=0.3.7
-leap.soledad.client>=0.6.0
-leap.keymanager>=0.3.8
-leap.mail>=0.3.9
+txzmq
# Remove this when u1db fixes its dependency on oauth
oauth
+
+taskthread
+logbook
diff --git a/pkg/scripts/checkout_leap_versions.sh b/pkg/scripts/checkout_leap_versions.sh
new file mode 100755
index 00000000..5381625b
--- /dev/null
+++ b/pkg/scripts/checkout_leap_versions.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+cat pkg/leap_versions.txt | while read line
+do
+ package=$(echo $line | cut -f1 -d' ')
+ tag=$(echo $line | cut -f2 -d' ')
+ cd ../$package && git fetch origin && git checkout $tag
+done
diff --git a/pkg/scripts/filter-bitmask-deps b/pkg/scripts/filter-bitmask-deps
new file mode 100755
index 00000000..9808d394
--- /dev/null
+++ b/pkg/scripts/filter-bitmask-deps
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+
+"""
+Filter bitmask dependencies.
+
+Usage: pipdeptree | filter-bitmask-deps
+"""
+import fileinput
+
+TARGET = "leap.bitmask"
+
+
+def get_bitmask_deps(dep_lines):
+ res = []
+ begin = False
+ for dep in dep_lines:
+ if dep.startswith(TARGET):
+ begin = True
+ res.append(dep)
+ continue
+ elif dep.startswith(' ') and begin:
+ res.append(dep)
+ continue
+ if begin:
+ return res
+
+
+if __name__ == "__main__":
+ lines = []
+ for line in fileinput.input():
+ lines.append(line)
+
+ bitmask_deps = get_bitmask_deps(lines)
+ for line in bitmask_deps:
+ print line[:-1]
diff --git a/pkg/tools/get_authors.sh b/pkg/tools/get_authors.sh
new file mode 100755
index 00000000..0169bb17
--- /dev/null
+++ b/pkg/tools/get_authors.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+git log --format='%aN <%aE>' | awk '{arr[$0]++} END{for (i in arr){print arr[i], i;}}' | sort -rn | cut -d' ' -f2-
diff --git a/pkg/tuf/apply_updates.py b/pkg/tuf/apply_updates.py
new file mode 100644
index 00000000..7b52ef11
--- /dev/null
+++ b/pkg/tuf/apply_updates.py
@@ -0,0 +1,83 @@
+#!/usr/local/bin python
+# -*- coding: utf-8 -*-
+# apply_updates.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/>.
+"""
+Apply downloaded updates to the bundle
+"""
+
+import os
+import os.path
+import shutil
+import tuf.client.updater
+
+
+REPO_DIR = "repo/"
+UPDATES_DIR = "updates/"
+
+
+def update_if_needed():
+ if not os.path.isdir(UPDATES_DIR):
+ print "No updates found"
+ return
+
+ print "Found updates, merging directories before doing anything..."
+ try:
+ remove_obsolete()
+ merge_directories(UPDATES_DIR, ".")
+ shutil.rmtree(UPDATES_DIR)
+ except Exception as e:
+ print "An error has ocurred while updating: " + e.message
+
+
+def remove_obsolete():
+ tuf.conf.repository_directory = REPO_DIR
+ updater = tuf.client.updater.Updater('leap-updater', {})
+ updater.remove_obsolete_targets(".")
+
+
+def merge_directories(src, dest):
+ for root, dirs, files in os.walk(src):
+ if not os.path.exists(root):
+ # It was moved as the dir din't exist in dest
+ continue
+
+ destroot = os.path.join(dest, root[len(src):])
+
+ for f in files:
+ srcpath = os.path.join(root, f)
+ destpath = os.path.join(destroot, f)
+ if os.path.exists(destpath):
+ # FIXME: On windows we can't remove, but we can rename and
+ # afterwards remove. is that still true with python?
+ # or was just something specific of our implementation
+ # with C++?
+ os.remove(destpath)
+ os.rename(srcpath, destpath)
+
+ for d in dirs:
+ srcpath = os.path.join(root, d)
+ destpath = os.path.join(destroot, d)
+
+ if os.path.exists(destpath) and not os.path.isdir(destpath):
+ os.remove(destpath)
+
+ if not os.path.exists(destpath):
+ os.rename(srcpath, destpath)
+
+
+if __name__ == "__main__":
+ update_if_needed()
diff --git a/pkg/utils.py b/pkg/utils.py
index deace14b..2e316d45 100644
--- a/pkg/utils.py
+++ b/pkg/utils.py
@@ -14,16 +14,30 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
"""
Utils to help in the setup process
"""
-
import os
import re
import sys
+def is_develop_mode():
+ """
+ Returns True if we're calling the setup script using the argument for
+ setuptools development mode.
+
+ This avoids messing up with dependency pinning and order, the
+ responsibility of installing the leap dependencies is left to the
+ developer.
+ """
+ args = sys.argv
+ devflags = "setup.py", "develop"
+ if (args[0], args[1]) == devflags:
+ return True
+ return False
+
+
def get_reqs_from_files(reqfiles):
"""
Returns the contents of the top requirement file listed as a
diff --git a/relnotes.txt b/relnotes.txt
index 3ac453a2..7a09f336 100644
--- a/relnotes.txt
+++ b/relnotes.txt
@@ -1,23 +1,40 @@
-ANNOUNCING Bitmask, the Internet Encryption Toolkit, release 0.8.1.
+ANNOUNCING Bitmask 0.9.0rc2 release candidate
-The LEAP team is pleased to announce the immediate availability of version
-0.8.1 of Bitmask, the Internet Encryption Toolkit, a bugfix release for 0.8.0.
+The LEAP team is pleased to announce the immediate availability of Bitmask
+0.9.0rc2.
-This release includes a couple of important bugfixes and a secure ZMQ fallback
-for distros that does not have CurveZMQ available.
+This is the second public *release candidate* of the Bitmask client that
+supports our user friendly end to end encrypted mail service. Our work has
+focused on speed and scale optimization by adapting the underlying components
+(leap.keymanager and leap.mail) to use the new async API of Soledad. This
+reduces code complexity by making use of the reactor pattern and by having
+blocking code (i.e. general i/o) be executed asynchronously. We have revamped
+how bitmask frontend and backend communicates (this improves performance and
+prevent bugs), improved log handling for better bug reports, and much more (see
+the changelog file for a more detailed list).
-Currently, Bitmask desktop client only support Debian and Ubuntu Linux. Support
-for Mac, Windows, and other Linux distributions is coming.
+This is a release candidate aimed at getting more user feedback to influence
+our next round of development. We have a list of known issues that we are
+cranking through and will add more to the list as they come in.
-Upgrading:
+* Changelog: https://github.com/leapcode/bitmask_client/blob/0.9.0rc2/CHANGELOG.rst
-* From bundle: if you are running bundle version 0.7 or new then Bitmask should
- update automatically.
+TESTING
-* From package: if you have added deb.bitmask.net to your sources.list, then
- Bitmask should update automatically (make sure it is not commented out).
+* Setup Instructions: https://bitmask.net/help
-If you have a bundle version older than 0.7, please reinstall Bitmask.
+* Fresh install: https://dl.bitmask.net/client/linux/release-candidate/
+
+* Upgrading from bundle: if you are running bundle version 0.7 or new then
+ Bitmask should update automatically.
+
+* Upgrading from package: if you have added deb.bitmask.net to your
+ sources.list, then Bitmask should update automatically (make sure it is not
+ commented out).
+
+Note: If you have a bundle version older than 0.7, please reinstall Bitmask.
+
+How to test: https://github.com/leapcode/bitmask_client/blob/develop/docs/testing-rcs.README
LICENSE
@@ -25,10 +42,6 @@ You may use Bitmask under the GNU General Public License, version 3 or, at your
option, any later version. See the file "LICENSE" for the terms of the GNU
General Public License, version 3.
-USAGE
-
-See https://bitmask.net/help
-
HACKING
See https://leap.se/en/docs/get-involved for tips on contacting the developers,
@@ -39,6 +52,6 @@ trains, rooftops, rainforests, lonely islands and, always, beyond any border.
The LEAP team,
-February 25, 2015
+August 27, 2015
Somewhere in the middle of the intertubes.
EOF
diff --git a/run_tests.sh b/run_tests.sh
index 13050872..0d7e7463 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -113,9 +113,7 @@ function run_pep8 {
echo "Running pep8 ..."
srcfiles="src/leap"
# Just run PEP8 in current environment
- pep8_opts="--ignore=E202,W602 --exclude=*_rc.py,ui_*,_version.py --repeat"
-
- ${wrapper} pep8 ${pep8_opts} ${srcfiles}
+ ${wrapper} flake8 ${pep8_opts} ${srcfiles}
}
# XXX we cannot run tests that need X server
diff --git a/setup.cfg b/setup.cfg
index 6c1d4f05..e91c4adb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,3 +6,11 @@ all_files = 1
[upload_sphinx]
upload-dir = docs/_build/html
repository = https://pypi.python.org/pypi
+
+[pep8]
+ignore = E731
+exclude = *_rc.py,ui_*,_version.py
+
+[flake8]
+ignore = E731
+exclude = *_rc.py,ui_*,_version.py
diff --git a/setup.py b/setup.py
index bfd7eff0..b10189a2 100755
--- a/setup.py
+++ b/setup.py
@@ -94,25 +94,13 @@ from setuptools import Command
class freeze_debianver(Command):
+
"""
Freezes the version in a debian branch.
To be used after merging the development branch onto the debian one.
"""
user_options = []
-
- def initialize_options(self):
- pass
-
- def finalize_options(self):
- pass
-
- def run(self):
- proceed = str(raw_input(
- "This will overwrite the file _version.py. Continue? [y/N] "))
- if proceed != "y":
- print("He. You scared. Aborting.")
- return
- template = r"""
+ template = r"""
# This file was generated by the `freeze_debianver` command in setup.py
# Using 'versioneer.py' (0.7+) from
# revision-control system data, or from the parent directory name of an
@@ -122,21 +110,61 @@ class freeze_debianver(Command):
version_version = '{version}'
version_full = '{version_full}'
"""
- templatefun = r"""
+ templatefun = r"""
def get_versions(default={}, verbose=False):
return {'version': version_version, 'full': version_full}
"""
- subst_template = template.format(
+
+ def initialize_options(self):
+ pass
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ proceed = str(raw_input(
+ "This will overwrite the file _version.py. Continue? [y/N] "))
+ if proceed != "y":
+ print("He. You scared. Aborting.")
+ return
+ subst_template = self.template.format(
version=VERSION_SHORT,
- version_full=VERSION_FULL) + templatefun
+ version_full=VERSION_FULL) + self.templatefun
with open(versioneer.versionfile_source, 'w') as f:
f.write(subst_template)
+def freeze_pkg_ver(path, version_short, version_full):
+ """
+ Freeze the _version in other modules, used during the gathering of
+ all the leap modules in the sumo tarball.
+ """
+ subst_template = freeze_debianver.template.format(
+ version=version_short,
+ version_full=version_full) + freeze_debianver.templatefun
+ with open(path, 'w') as f:
+ f.write(subst_template)
+
+
cmdclass["freeze_debianver"] = freeze_debianver
parsed_reqs = utils.parse_requirements()
+if utils.is_develop_mode():
+ print("")
+ print ("[WARNING] Skipping leap-specific dependencies "
+ "because development mode is detected.")
+ print ("[WARNING] You can install "
+ "the latest published versions with "
+ "'pip install -r pkg/requirements-leap.pip'")
+ print ("[WARNING] Or you can instead do 'python setup.py develop' "
+ "from the parent folder of each one of them.")
+ print("")
+else:
+ parsed_reqs += utils.parse_requirements(
+ reqfiles=["pkg/requirements-leap.pip"])
+
+
leap_launcher = 'bitmask=leap.bitmask.app:start_app'
from setuptools.command.develop import develop as _develop
@@ -157,6 +185,7 @@ def copy_reqs(path, withsrc=False):
class cmd_develop(_develop):
+
def run(self):
# versioneer:
versions = versioneer.get_versions(verbose=True)
@@ -171,6 +200,7 @@ cmdclass["develop"] = cmd_develop
class cmd_binary_hash(Command):
+
"""
Update the _binaries.py file with hashes for the different helpers.
This is used from within the bundle.
@@ -234,19 +264,187 @@ versioneer_sdist = cmdclass['sdist']
class cmd_build(versioneer_build):
+
def run(self):
versioneer_build.run(self)
copy_reqs(self.build_lib)
class cmd_sdist(versioneer_sdist):
+
+ user_options = versioneer_sdist.user_options + \
+ [('sumo', 's',
+ "create a 'sumo' sdist which includes the contents of all "
+ "the leap.* packages")
+ ]
+ boolean_options = ['sumo']
+ leap_sumo_packages = ['soledad.common', 'soledad.client',
+ 'keymanager', 'mail', 'common']
+
+ def initialize_options(self):
+ versioneer_sdist.initialize_options(self)
+ self.sumo = False
+
def run(self):
return versioneer_sdist.run(self)
def make_release_tree(self, base_dir, files):
versioneer_sdist.make_release_tree(self, base_dir, files)
+ # We need to copy the requirements to the specified path
+ # so that the client has a copy to do the startup checks.
copy_reqs(base_dir, withsrc=True)
-
+ with open(os.path.join(base_dir,
+ 'src', 'leap', '__init__.py'),
+ 'w') as nuke_top_init:
+ nuke_top_init.write('')
+ with open(os.path.join(base_dir,
+ 'src', 'leap', 'soledad', '__init__.py'),
+ 'w') as nuke_soledad_ns:
+ nuke_soledad_ns.write('')
+
+ def make_distribution(self):
+ # add our extra files to the list just before building the
+ # tarball/zipfile. We override make_distribution() instead of run()
+ # because setuptools.command.sdist.run() does not lend itself to
+ # easy/robust subclassing (the code we need to add goes right smack
+ # in the middle of a 12-line method). If this were the distutils
+ # version, we'd override get_file_list().
+
+ if self.sumo:
+ # If '--sumo' was specified, include all the leap.* in the sdist.
+ vdict = _get_leap_versions()
+ vdict['soledad.common'] = vdict['soledad']
+ vdict['soledad.client'] = vdict['soledad']
+ import importlib
+ for module in self.leap_sumo_packages:
+ full_module = "leap." + module
+ importlib.import_module(full_module)
+ src_path = "src/leap/" + _fix_namespace(module)
+ imported_module = importlib.sys.modules[full_module]
+ copy_recursively(
+ imported_module.__path__[0] + "/",
+ src_path)
+ all_module_files = list_recursively(src_path)
+ self.filelist.extend(all_module_files)
+ module_ver = vdict[module]
+ freeze_pkg_ver(
+ src_path + "/_version.py",
+ module_ver, "%s-sumo" % module_ver)
+ freeze_pkg_ver(
+ "src/leap/bitmask/_version.py",
+ VERSION, "%s-sumo" % VERSION)
+
+ # In addition, we want the tarball/zipfile to have -SUMO in the
+ # name, and the unpacked directory to have -SUMO too. The easiest
+ # way to do this is to patch self.distribution and override the
+ # get_fullname() method. (an alternative is to modify
+ # self.distribution.metadata.version, but that also affects the
+ # contents of PKG-INFO).
+ fullname = self.distribution.get_fullname()
+
+ def get_fullname():
+ return fullname + "-SUMO"
+ self.distribution.get_fullname = get_fullname
+
+ try:
+ old_mask = os.umask(int("022", 8))
+ return versioneer_sdist.make_distribution(self)
+ finally:
+ os.umask(old_mask)
+ for module in self.leap_sumo_packages:
+ # check, just in case...
+ if module and module != "bitmask":
+ shutil.rmtree("src/leap/" + _fix_namespace(module))
+
+
+import shutil
+import glob
+
+
+def _get_leap_versions():
+ versions = {}
+ with open("pkg/leap_versions.txt") as vf:
+ lines = vf.readlines()
+ for line in lines:
+ pkg, ver = line.split('\t')
+ versions[pkg.strip().replace('leap_', '')] = ver.strip()
+ return versions
+
+
+def _fix_namespace(path):
+ if path in ('soledad.common', 'soledad.client'):
+ return path.replace('.', '/')
+ return path
+
+
+_ignore_files = ('*.pyc', '_trial*', '*.swp', '.*', 'cert', 'test*')
+_ignore_dirs = ('tests', '_trial*', 'test*')
+_ignore_paths = _ignore_files + _ignore_dirs
+
+is_excluded_path = lambda path: any(
+ map(lambda pattern: glob.fnmatch.fnmatch(path, pattern),
+ _ignore_paths))
+
+
+def _should_exclude(path):
+ folder, f = os.path.split(path)
+ if is_excluded_path(f):
+ return True
+ upper, leaf = os.path.split(folder)
+ if is_excluded_path(leaf):
+ return True
+ return False
+
+
+def list_recursively(root_dir):
+ file_list = []
+ for root, sub_dirs, files in os.walk(root_dir):
+ for f in files:
+ is_excluded = _should_exclude(f)
+ if not is_excluded:
+ file_list.append(os.path.join(root, f))
+ return file_list
+
+
+def _mkdir_recursively(path):
+ sub_path = os.path.dirname(path)
+ if not os.path.exists(sub_path):
+ _mkdir_recursively(sub_path)
+ if not os.path.exists(path):
+ os.mkdir(path)
+
+
+def copy_recursively(source_folder, destination_folder):
+
+ if not os.path.exists(destination_folder):
+ _mkdir_recursively(destination_folder)
+
+ for root, dirs, files in os.walk(source_folder):
+ if _should_exclude(root):
+ continue
+ for item in files:
+ if _should_exclude(item):
+ continue
+ src_path = os.path.join(root, item)
+ dst_path = os.path.join(
+ destination_folder, src_path.replace(source_folder, ""))
+ if _should_exclude(dst_path):
+ continue
+ if os.path.exists(dst_path):
+ if os.stat(src_path).st_mtime > os.stat(dst_path).st_mtime:
+ shutil.copy2(src_path, dst_path)
+ else:
+ shutil.copy2(src_path, dst_path)
+ for item in dirs:
+ if _should_exclude(item):
+ continue
+ src_path = os.path.join(root, item)
+ dst_path = os.path.join(
+ destination_folder, src_path.replace(source_folder, ""))
+ if _should_exclude(dst_path):
+ continue
+ if not os.path.exists(dst_path):
+ os.mkdir(dst_path)
cmdclass["build"] = cmd_build
cmdclass["sdist"] = cmd_sdist
@@ -258,42 +456,23 @@ IS_MAC = _system == "Darwin"
data_files = []
+
if IS_LINUX:
# XXX use check_for_permissions to install data
# globally. Or make specific install command. See #3805
- data_files = [
- ("share/polkit-1/actions",
- ["pkg/linux/polkit/se.leap.bitmask.policy"]),
- ("/usr/sbin",
- ["pkg/linux/bitmask-root"]),
- ]
+ isset = lambda var: os.environ.get(var, None)
+ if isset('VIRTUAL_ENV') or isset('LEAP_SKIP_INIT'):
+ data_files = None
+ else:
+ data_files = [
+ ("share/polkit-1/actions",
+ ["pkg/linux/polkit/se.leap.bitmask.policy"]),
+ ("/usr/sbin",
+ ["pkg/linux/bitmask-root"]),
+ ]
extra_options = {}
-if IS_MAC:
- extra_options["app"] = ['src/leap/bitmask/app.py']
- OPTIONS = {
- 'argv_emulation': True,
- 'plist': 'pkg/osx/Info.plist',
- 'iconfile': 'pkg/osx/bitmask.icns',
- }
- extra_options["options"] = {'py2app': OPTIONS}
- extra_options["setup_requires"] = ['py2app']
-
- class jsonschema_recipe(object):
- def check(self, dist, mf):
- m = mf.findNode('jsonschema')
- if m is None:
- return None
-
- # Don't put jsonschema in the site-packages.zip file
- return dict(
- packages=['jsonschema']
- )
-
- import py2app.recipes
- py2app.recipes.jsonschema = jsonschema_recipe()
-
setup(
name="leap.bitmask",
package_dir={"": "src"},
diff --git a/src/leap/bitmask/_components.py b/src/leap/bitmask/_components.py
new file mode 100644
index 00000000..9be0e6bc
--- /dev/null
+++ b/src/leap/bitmask/_components.py
@@ -0,0 +1,6 @@
+"""
+Enabled Modules in Bitmask.
+Change these values for builds of the client with only one module enabled.
+"""
+HAS_EIP = True
+HAS_MAIL = True
diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py
index 9056d2a6..a2e2aa1a 100644
--- a/src/leap/bitmask/app.py
+++ b/src/leap/bitmask/app.py
@@ -39,30 +39,35 @@
# M:::::::::::~NMMM7???7MMMM:::::::::::::::::::::::NMMMI??I7MMMM:::::::::::::M
# M::::::::::::::7MMMMMMM+:::::::::::::::::::::::::::?MMMMMMMZ:::::::::::::::M
# (thanks to: http://www.glassgiant.com/ascii/)
+import atexit
+import commands
import multiprocessing
import os
+import platform
import sys
-
-from leap.bitmask.backend.backend_proxy import BackendProxy
+if platform.system() == "Darwin":
+ # 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
from leap.bitmask.config import flags
from leap.bitmask.frontend_app import run_frontend
-from leap.bitmask.backend_app import run_backend
-from leap.bitmask.logs.utils import create_logger
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.platform_init.locks import we_are_the_one_and_only
from leap.bitmask.services.mail import plumber
-from leap.bitmask.util import leap_argparse, flags_to_dict
+from leap.bitmask.util import leap_argparse, flags_to_dict, here
from leap.bitmask.util.requirement_checker import check_requirements
-from leap.common.events import server as event_server
from leap.mail import __version__ as MAIL_VERSION
import codecs
codecs.register(lambda name: codecs.lookup('utf-8')
if name == 'cp65001' else None)
-
import psutil
@@ -73,7 +78,16 @@ def kill_the_children():
me = os.getpid()
parent = psutil.Process(me)
print "Killing all the children processes..."
- for child in parent.get_children(recursive=True):
+
+ children = None
+ try:
+ # for psutil 0.2.x
+ children = parent.get_children(recursive=True)
+ except:
+ # for psutil 0.3.x
+ children = parent.children(recursive=True)
+
+ for child in children:
try:
child.terminate()
except Exception as exc:
@@ -81,7 +95,7 @@ def kill_the_children():
# XXX This is currently broken, but we need to fix it to avoid
# orphaned processes in case of a crash.
-# atexit.register(kill_the_children)
+atexit.register(kill_the_children)
def do_display_version(opts):
@@ -109,6 +123,26 @@ def do_mail_plumbing(opts):
# XXX catch when import is used w/o acct
+def log_lsb_release_info(logger):
+ """
+ Attempt to log distribution info from the lsb_release utility
+ """
+ if commands.getoutput('which lsb_release'):
+ distro_info = commands.getoutput('lsb_release -a').split('\n')[-4:]
+ logger.info("LSB Release info:")
+ for line in distro_info:
+ logger.info(line)
+
+def fix_qtplugins_path():
+ # This is a small workaround for a bug in macholib, there is a slight typo
+ # in the path for the qt plugins that is added to the dynamic loader path
+ # in the libs.
+ if sys.platform in ('win32', 'darwin'):
+ from PySide import QtCore
+ plugins_path = os.path.join(os.path.dirname(here(QtCore)), 'plugins')
+ QtCore.QCoreApplication.setLibraryPaths([plugins_path])
+
+
def start_app():
"""
Starts the main event loop and launches the main window.
@@ -123,13 +157,10 @@ def start_app():
options = {
'start_hidden': opts.start_hidden,
'debug': opts.debug,
- 'log_file': opts.log_file,
}
flags.STANDALONE = opts.standalone
- # XXX Disabled right now since it's not tested after login refactor
- # flags.OFFLINE = opts.offline
- flags.OFFLINE = False
+ flags.OFFLINE = opts.offline
flags.MAIL_LOGFILE = opts.mail_log_file
flags.APP_VERSION_CHECK = opts.app_version_check
flags.API_VERSION_CHECK = opts.api_version_check
@@ -138,23 +169,24 @@ def start_app():
flags.CA_CERT_FILE = opts.ca_cert_file
- replace_stdout = True
- if opts.repair or opts.import_maildir:
- # We don't want too much clutter on the comand mode
- # this could be more generic with a Command class.
- replace_stdout = False
+ flags.DEBUG = opts.debug
+
+ logger = get_logger(perform_rollover=True)
- logger = create_logger(opts.debug, opts.log_file, replace_stdout)
+ # NOTE: since we are not using this right now, the code that replaces the
+ # stdout needs to be reviewed when we enable this again
+ # replace_stdout = True
+
+ # XXX mail repair commands disabled for now
+ # if opts.repair or opts.import_maildir:
+ # We don't want too much clutter on the comand mode
+ # this could be more generic with a Command class.
+ # replace_stdout = False
# ok, we got logging in place, we can satisfy mail plumbing requests
# and show logs there. it normally will exit there if we got that path.
- do_mail_plumbing(opts)
-
- try:
- event_server.ensure_server(event_server.SERVER_PORT)
- except Exception as e:
- # We don't even have logger configured in here
- print "Could not ensure server: %r" % (e,)
+ # XXX mail repair commands disabled for now
+ # do_mail_plumbing(opts)
PLAY_NICE = os.environ.get("LEAP_NICE")
if PLAY_NICE and PLAY_NICE.isdigit():
@@ -172,10 +204,10 @@ def start_app():
check_requirements()
logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
- logger.info('Bitmask version %s', VERSION)
- logger.info('leap.mail version %s', MAIL_VERSION)
+ logger.info('Bitmask version %s' % VERSION)
+ logger.info('leap.mail version %s' % MAIL_VERSION)
+ log_lsb_release_info(logger)
logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
-
logger.info('Starting app')
backend_running = BackendProxy().check_online()
@@ -195,6 +227,7 @@ def start_app():
backend_process.start()
backend_pid = backend_process.pid
+ fix_qtplugins_path()
run_frontend(options, flags_dict, backend_pid=backend_pid)
diff --git a/src/leap/bitmask/backend/api.py b/src/leap/bitmask/backend/api.py
index 3f6c0ad1..48aa2090 100644
--- a/src/leap/bitmask/backend/api.py
+++ b/src/leap/bitmask/backend/api.py
@@ -146,6 +146,7 @@ SIGNALS = (
"srp_password_change_badpw",
"srp_password_change_error",
"srp_password_change_ok",
+ "srp_registration_disabled",
"srp_registration_failed",
"srp_registration_finished",
"srp_registration_taken",
diff --git a/src/leap/bitmask/backend/backend.py b/src/leap/bitmask/backend/backend.py
index cff731ba..4a98d146 100644
--- a/src/leap/bitmask/backend/backend.py
+++ b/src/leap/bitmask/backend/backend.py
@@ -17,17 +17,15 @@
# FIXME this is missing module documentation. It would be fine to say a couple
# of lines about the whole backend architecture.
-# TODO use txzmq bindings instead.
-
import json
import os
-import threading
import time
import psutil
-from twisted.internet import defer, reactor, threads
+from twisted.internet import defer, reactor, threads, task
+import txzmq
import zmq
try:
from zmq.auth.thread import ThreadAuthenticator
@@ -35,51 +33,50 @@ except ImportError:
pass
from leap.bitmask.backend.api import API, PING_REQUEST
+from leap.bitmask.backend.signaler import Signaler
from leap.bitmask.backend.utils import get_backend_certificates
from leap.bitmask.config import flags
-from leap.bitmask.backend.signaler import Signaler
+from leap.bitmask.logs.utils import get_logger
-import logging
-logger = logging.getLogger(__name__)
+logger = get_logger()
-class Backend(object):
+class TxZmqREPConnection(object):
"""
- Backend server.
- Receives signals from backend_proxy and emit signals if needed.
+ A twisted based zmq rep connection.
"""
- # XXX we might want to make this configurable per-platform,
- # and use the most performant socket type on each one.
- if flags.ZMQ_HAS_CURVE:
- # XXX this should not be hardcoded. Make it configurable.
- PORT = '5556'
- BIND_ADDR = "tcp://127.0.0.1:%s" % PORT
- else:
- SOCKET_FILE = "/tmp/bitmask.socket.0"
- BIND_ADDR = "ipc://%s" % SOCKET_FILE
- PING_INTERVAL = 2 # secs
+ def __init__(self, server_address, process_request):
+ """
+ Initialize the connection.
- def __init__(self, frontend_pid=None):
+ :param server_address: The address of the backend zmq server.
+ :type server: str
+ :param process_request: A callable used to process incoming requests.
+ :type process_request: callable(messageParts)
"""
- Backend constructor, create needed instances.
+ self._server_address = server_address
+ self._process_request = process_request
+ self._zmq_factory = None
+ self._zmq_connection = None
+ self._init_txzmq()
+
+ def _init_txzmq(self):
"""
- self._signaler = Signaler()
+ Configure the txzmq components and connection.
+ """
+ self._zmq_factory = txzmq.ZmqFactory()
+ self._zmq_factory.registerForShutdown()
+ self._zmq_connection = txzmq.ZmqREPConnection(self._zmq_factory)
- self._frontend_pid = frontend_pid
+ context = self._zmq_factory.context
+ socket = self._zmq_connection.socket
- self._do_work = threading.Event() # used to stop the worker thread.
- self._zmq_socket = None
+ def _gotMessage(messageId, messageParts):
+ self._zmq_connection.reply(messageId, "OK")
+ self._process_request(messageParts)
- self._ongoing_defers = []
- self._init_zmq()
-
- def _init_zmq(self):
- """
- Configure the zmq components and connection.
- """
- context = zmq.Context()
- socket = context.socket(zmq.REP)
+ self._zmq_connection.gotMessage = _gotMessage
if flags.ZMQ_HAS_CURVE:
# Start an authenticator for this context.
@@ -95,37 +92,39 @@ class Backend(object):
socket.curve_secretkey = secret
socket.curve_server = True # must come before bind
- socket.bind(self.BIND_ADDR)
- if not flags.ZMQ_HAS_CURVE:
- os.chmod(self.SOCKET_FILE, 0600)
+ proto, addr = self._server_address.split('://') # tcp/ipc, ip/socket
+ socket.bind(self._server_address)
+ if proto == 'ipc':
+ os.chmod(addr, 0600)
- self._zmq_socket = socket
- def _worker(self):
- """
- Receive requests and send it to process.
+class Backend(object):
+ """
+ Backend server.
+ Receives signals from backend_proxy and emit signals if needed.
+ """
+ # XXX we might want to make this configurable per-platform,
+ # and use the most performant socket type on each one.
+ if flags.ZMQ_HAS_CURVE:
+ # XXX this should not be hardcoded. Make it configurable.
+ PORT = '5556'
+ BIND_ADDR = "tcp://127.0.0.1:%s" % PORT
+ else:
+ SOCKET_FILE = "/tmp/bitmask.socket.0"
+ BIND_ADDR = "ipc://%s" % SOCKET_FILE
- Note: we use a simple while since is less resource consuming than a
- Twisted's LoopingCall.
+ PING_INTERVAL = 2 # secs
+
+ def __init__(self, frontend_pid=None):
"""
- pid = self._frontend_pid
- check_wait = 0
- while self._do_work.is_set():
- # Wait for next request from client
- try:
- request = self._zmq_socket.recv(zmq.NOBLOCK)
- self._zmq_socket.send("OK")
- # logger.debug("Received request: '{0}'".format(request))
- self._process_request(request)
- except zmq.ZMQError as e:
- if e.errno != zmq.EAGAIN:
- raise
- time.sleep(0.01)
-
- check_wait += 0.01
- if pid is not None and check_wait > self.PING_INTERVAL:
- check_wait = 0
- self._check_frontend_alive()
+ Backend constructor, create needed instances.
+ """
+ self._signaler = Signaler()
+ self._frontend_pid = frontend_pid
+ self._frontend_checker = None
+ self._ongoing_defers = []
+ self._zmq_connection = TxZmqREPConnection(
+ self.BIND_ADDR, self._process_request)
def _check_frontend_alive(self):
"""
@@ -160,25 +159,27 @@ class Backend(object):
for d in self._ongoing_defers:
d.cancel()
+ logger.debug("Stopping the Twisted reactor...")
reactor.stop()
- logger.debug("Twisted reactor stopped.")
def run(self):
"""
Start the ZMQ server and run the loop to handle requests.
"""
self._signaler.start()
- self._do_work.set()
- threads.deferToThread(self._worker)
+ self._frontend_checker = task.LoopingCall(self._check_frontend_alive)
+ self._frontend_checker.start(self.PING_INTERVAL)
+ logger.debug("Starting Twisted reactor.")
reactor.run()
+ logger.debug("Finished Twisted reactor.")
def stop(self):
"""
Stop the server and the zmq request parse loop.
"""
- logger.debug("STOP received.")
+ logger.debug("Stopping the backend...")
self._signaler.stop()
- self._do_work.clear()
+ self._frontend_checker.stop()
threads.deferToThread(self._stop_reactor)
def _process_request(self, request_json):
diff --git a/src/leap/bitmask/backend/backend_proxy.py b/src/leap/bitmask/backend/backend_proxy.py
index b2f79a70..30b7c5d1 100644
--- a/src/leap/bitmask/backend/backend_proxy.py
+++ b/src/leap/bitmask/backend/backend_proxy.py
@@ -21,48 +21,57 @@ to the backend.
# XXX should document the relationship to the API here.
import functools
-import Queue
import threading
-import time
import zmq
+from zmq.eventloop import ioloop
+from zmq.eventloop import zmqstream
+
+from taskthread import TimerTask
from leap.bitmask.backend.api import API, STOP_REQUEST, PING_REQUEST
+from leap.bitmask.backend.settings import Settings
from leap.bitmask.backend.utils import generate_zmq_certificates_if_needed
from leap.bitmask.backend.utils import get_backend_certificates
from leap.bitmask.config import flags
+from leap.bitmask.logs.utils import get_logger
-import logging
-logger = logging.getLogger(__name__)
+logger = get_logger()
-class BackendProxy(object):
+class ZmqREQConnection(threading.Thread):
"""
- The BackendProxy handles calls from the GUI and forwards (through ZMQ)
- to the backend.
+ A threaded zmq req connection.
"""
- if flags.ZMQ_HAS_CURVE:
- PORT = '5556'
- SERVER = "tcp://localhost:%s" % PORT
- else:
- SERVER = "ipc:///tmp/bitmask.socket.0"
-
- POLL_TIMEOUT = 4000 # ms
- POLL_TRIES = 3
-
- PING_INTERVAL = 2 # secs
-
- def __init__(self):
- generate_zmq_certificates_if_needed()
+ def __init__(self, server_address, on_recv):
+ """
+ Initialize the connection.
- self._socket = None
+ :param server_address: The address of the backend zmq server.
+ :type server: str
+ :param on_recv: The callback to be executed when a message is
+ received.
+ :type on_recv: callable(msg)
+ """
+ threading.Thread.__init__(self)
+ self._server_address = server_address
+ self._on_recv = on_recv
+ self._stream = None
+ self._init_zmq()
- # initialize ZMQ stuff:
+ def _init_zmq(self):
+ """
+ Configure the zmq components and connection.
+ """
+ logger.debug("Setting up ZMQ connection to server...")
context = zmq.Context()
- logger.debug("Connecting to server...")
socket = context.socket(zmq.REQ)
+ # we use zmq's eventloop in order to asynchronously send requests
+ loop = ioloop.ZMQIOLoop.current()
+ self._stream = zmqstream.ZMQStream(socket, loop)
+
if flags.ZMQ_HAS_CURVE:
# public, secret = zmq.curve_keypair()
client_keys = zmq.curve_keypair()
@@ -76,66 +85,128 @@ class BackendProxy(object):
socket.setsockopt(zmq.RCVTIMEO, 1000)
socket.setsockopt(zmq.LINGER, 0) # Terminate early
- socket.connect(self.SERVER)
- self._socket = socket
- self._ping_at = 0
+ self._stream.on_recv(self._on_recv)
+
+ def run(self):
+ """
+ Run the threaded stream connection loop.
+ """
+ self._stream.socket.connect(self._server_address)
+ logger.debug("Starting ZMQ loop.")
+ self._stream.io_loop.start()
+ logger.debug("Finished ZMQ loop.")
+
+ def stop(self):
+ """
+ Stop the threaded connection loop.
+ """
+ self._stream.io_loop.stop()
+
+ def send(self, *args, **kwargs):
+ """
+ Send a message through this connection.
+ """
+ # Important note: calling send on the zmqstream from another
+ # thread doesn’t properly tell the IOLoop thread that there’s an
+ # event to process. This could cuase small delays if the IOLoop is
+ # already processing lots of events, but it can cause the message
+ # to never send if the zmq socket is the only one it’s handling.
+ #
+ # Because of that, we want ZmqREQConnection.send to hand off the
+ # stream.send to the IOLoop’s thread via IOLoop.add_callback:
+ self._stream.io_loop.add_callback(
+ lambda: self._stream.send(*args, **kwargs))
+
+
+class BackendProxy(object):
+ """
+ The BackendProxy handles calls from the GUI and forwards (through ZMQ)
+ to the backend.
+ """
+
+ if flags.ZMQ_HAS_CURVE:
+ PORT = '5556'
+ SERVER = "tcp://localhost:%s" % PORT
+ else:
+ SERVER = "ipc:///tmp/bitmask.socket.0"
+
+ PING_INTERVAL = 2 # secs
+
+ def __init__(self):
+ """
+ Initialize the backend proxy.
+ """
+ generate_zmq_certificates_if_needed()
+ self._do_work = threading.Event()
+ self._work_lock = threading.Lock()
+ self._connection = ZmqREQConnection(self.SERVER, self._set_online)
+ self._heartbeat = TimerTask(self._ping, delay=self.PING_INTERVAL)
+ self._ping_event = threading.Event()
self.online = False
+ self.settings = Settings()
- self._call_queue = Queue.Queue()
- self._worker_caller = threading.Thread(target=self._worker)
+ def _set_online(self, _):
+ """
+ Mark the backend as being online.
- def start(self):
- self._worker_caller.start()
+ This is used as the zmq connection's on_recv callback, and so it is
+ passed the received message as a parameter. Because we currently don't
+ use that message, we just ignore it for now.
+ """
+ self.online = True
+ # the following event is used when checking whether the backend is
+ # online
+ self._ping_event.set()
+
+ def _set_offline(self):
+ """
+ Mark the backend as being offline.
+ """
+ self.online = False
def check_online(self):
"""
Return whether the backend is accessible or not.
You don't need to do `run` in order to use this.
-
:rtype: bool
"""
- # we use a small timeout in order to response quickly if the backend is
- # offline
- self._send_request(PING_REQUEST, retry=False, timeout=500)
- self._socket.close()
+ logger.debug("Checking whether backend is online...")
+ self._send_request(PING_REQUEST)
+ # self._ping_event will eventually be set by the zmq connection's
+ # on_recv callback, so we use a small timeout in order to response
+ # quickly if the backend is offline
+ if not self._ping_event.wait(0.5):
+ logger.warning("Backend is offline!")
+ self._set_offline()
return self.online
- def _worker(self):
+ def start(self):
"""
- Worker loop that processes the Queue of pending requests to do.
+ Start the backend proxy.
"""
- while True:
- try:
- request = self._call_queue.get(block=False)
- # break the loop after sending the 'stop' action to the
- # backend.
- if request == STOP_REQUEST:
- break
-
- self._send_request(request)
- except Queue.Empty:
- pass
- time.sleep(0.01)
- self._ping()
+ logger.debug("Starting backend proxy...")
+ self._do_work.set()
+ self._connection.start()
+ self.check_online()
+ self._heartbeat.start()
- logger.debug("BackendProxy worker stopped.")
-
- def _reset_ping(self):
+ def _stop(self):
"""
- Reset the ping timeout counter.
- This is called for every ping and request.
+ Stop the backend proxy.
"""
- self._ping_at = time.time() + self.PING_INTERVAL
+ with self._work_lock: # avoid sending after connection was closed
+ self._do_work.clear()
+ self._heartbeat.stop()
+ self._connection.stop()
+ logger.debug("BackendProxy worker stopped.")
def _ping(self):
"""
Heartbeat helper.
Sends a PING request just to know that the server is alive.
"""
- if time.time() > self._ping_at:
- self._send_request(PING_REQUEST)
- self._reset_ping()
+ self._send_request(PING_REQUEST)
def _api_call(self, *args, **kwargs):
"""
@@ -159,6 +230,8 @@ class BackendProxy(object):
'arguments': kwargs,
}
+ request_json = None
+
try:
request_json = zmq.utils.jsonapi.dumps(request)
except Exception as e:
@@ -169,12 +242,12 @@ class BackendProxy(object):
raise
# queue the call in order to handle the request in a thread safe way.
- self._call_queue.put(request_json)
+ self._send_request(request_json)
if api_method == STOP_REQUEST:
- self._call_queue.put(STOP_REQUEST)
+ self._stop()
- def _send_request(self, request, retry=True, timeout=None):
+ def _send_request(self, request):
"""
Send the given request to the server.
This is used from a thread safe loop in order to avoid sending a
@@ -182,49 +255,10 @@ class BackendProxy(object):
:param request: the request to send.
:type request: str
- :param retry: whether we should retry or not in case of timeout.
- :type retry: bool
- :param timeout: a custom timeout (milliseconds) to wait for a response.
- :type timeout: int
"""
- # logger.debug("Sending request to backend: {0}".format(request))
- self._socket.send(request)
-
- poll = zmq.Poller()
- poll.register(self._socket, zmq.POLLIN)
-
- reply = None
-
- tries = 0
- if not retry:
- tries = self.POLL_TRIES + 1 # this means: no retries left
-
- if timeout is None:
- timeout = self.POLL_TIMEOUT
-
- while True:
- socks = dict(poll.poll(timeout))
- if socks.get(self._socket) == zmq.POLLIN:
- reply = self._socket.recv()
- break
-
- tries += 1
- if tries < self.POLL_TRIES:
- logger.warning('Retrying receive... {0}/{1}'.format(
- tries, self.POLL_TRIES))
- else:
- break
-
- if reply is None:
- msg = "Timeout error contacting backend."
- logger.critical(msg)
- self.online = False
- else:
- # msg = "Received reply for '{0}' -> '{1}'".format(request, reply)
- # logger.debug(msg)
- self.online = True
- # request received, no ping needed for other interval.
- self._reset_ping()
+ with self._work_lock: # avoid sending after connection was closed
+ if self._do_work.is_set():
+ self._connection.send(request)
def __getattribute__(self, name):
"""
diff --git a/src/leap/bitmask/backend/components.py b/src/leap/bitmask/backend/components.py
index 4b63af84..5f34d290 100644
--- a/src/leap/bitmask/backend/components.py
+++ b/src/leap/bitmask/backend/components.py
@@ -17,13 +17,14 @@
"""
Backend components
"""
-import logging
+# TODO [ ] Get rid of all this deferToThread mess, or at least contain
+# all of it into its own threadpool.
+
import os
import socket
import time
from functools import partial
-from threading import Condition
from twisted.internet import threads, defer
from twisted.python import log
@@ -35,9 +36,10 @@ from leap.bitmask.backend.settings import Settings, GATEWAY_AUTOMATIC
from leap.bitmask.config.providerconfig import ProviderConfig
from leap.bitmask.crypto.srpauth import SRPAuth
from leap.bitmask.crypto.srpregister import SRPRegister
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.platform_init import IS_LINUX
-from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper
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.eip import eipconfig
from leap.bitmask.services.eip import get_openvpn_management
@@ -59,11 +61,11 @@ from leap.bitmask.util.privilege_policies import LinuxPolicyChecker
from leap.common import certs as leap_certs
from leap.keymanager import openpgp
-from leap.keymanager.errors import KeyAddressMismatch, KeyFingerprintMismatch
-from leap.soledad.client import NoStorageSecret, PassphraseTooShort
+from leap.soledad.client.secrets import PassphraseTooShort
+from leap.soledad.client.secrets import NoStorageSecret
-logger = logging.getLogger(__name__)
+logger = get_logger()
class ILEAPComponent(zope.interface.Interface):
@@ -626,7 +628,9 @@ class EIP(object):
# this only works for selecting the first gateway, as we're
# currently doing.
ccodes = gateway_selector.get_gateways_country_code()
- gateway_ccode = ccodes[gateways[0]]
+ gateway_ccode = '' # '' instead of None due to needed signal argument
+ if ccodes is not None:
+ gateway_ccode = ccodes[gateways[0]]
self._signaler.signal(self._signaler.eip_get_gateway_country_code,
gateway_ccode)
@@ -777,8 +781,8 @@ class Soledad(object):
"""
provider_config = ProviderConfig.get_provider_config(domain)
if provider_config is not None:
- self._soledad_defer = threads.deferToThread(
- self._soledad_bootstrapper.run_soledad_setup_checks,
+ sb = self._soledad_bootstrapper
+ self._soledad_defer = sb.run_soledad_setup_checks(
provider_config, username, password,
download_if_needed=True)
self._soledad_defer.addCallback(self._set_proxies_cb)
@@ -814,8 +818,9 @@ class Soledad(object):
Signaler.soledad_offline_finished
Signaler.soledad_offline_failed
"""
- self._soledad_bootstrapper.load_offline_soledad(
+ d = self._soledad_bootstrapper.load_offline_soledad(
username, password, uuid)
+ d.addCallback(self._set_proxies_cb)
def cancel_bootstrap(self):
"""
@@ -825,7 +830,6 @@ class Soledad(object):
logger.debug("Cancelling soledad defer.")
self._soledad_defer.cancel()
self._soledad_defer = None
- zope.proxy.setProxiedObject(self._soledad_proxy, None)
def close(self):
"""
@@ -906,52 +910,6 @@ class Keymanager(object):
# NOTE: This feature is disabled right now since is dangerous
return
- new_key = ''
- signal = None
- try:
- with open(filename, 'r') as keys_file:
- new_key = keys_file.read()
- except IOError as e:
- logger.error("IOError importing key. {0!r}".format(e))
- signal = self._signaler.keymanager_import_ioerror
- self._signaler.signal(signal)
- return
-
- keymanager = self._keymanager_proxy
- try:
- # NOTE: parse_openpgp_ascii_key is not in keymanager anymore
- # the API for that will need some thinking
- public_key, private_key = keymanager.parse_openpgp_ascii_key(
- new_key)
- except (KeyAddressMismatch, KeyFingerprintMismatch) as e:
- logger.error(repr(e))
- signal = self._signaler.keymanager_import_datamismatch
- self._signaler.signal(signal)
- return
-
- if public_key is None or private_key is None:
- signal = self._signaler.keymanager_import_missingkey
- self._signaler.signal(signal)
- return
-
- current_public_key = keymanager.get_key(username, openpgp.OpenPGPKey)
- if public_key.address != current_public_key.address:
- logger.error("The key does not match the ID")
- signal = self._signaler.keymanager_import_addressmismatch
- self._signaler.signal(signal)
- return
-
- keymanager.delete_key(self._key)
- keymanager.delete_key(self._key_priv)
- keymanager.put_key(public_key)
- keymanager.put_key(private_key)
- keymanager.send_key(openpgp.OpenPGPKey)
-
- logger.debug('Import ok')
- signal = self._signaler.keymanager_import_ok
-
- self._signaler.signal(signal)
-
def export_keys(self, username, filename):
"""
Export the given username's keys to a file.
@@ -963,35 +921,50 @@ class Keymanager(object):
"""
keymanager = self._keymanager_proxy
- public_key = keymanager.get_key(username, openpgp.OpenPGPKey)
- private_key = keymanager.get_key(username, openpgp.OpenPGPKey,
- private=True)
- try:
+ def export(keys):
+ public_key, private_key = keys
+ # XXX: This is blocking. We could use writeToFD, but is POSIX only
+ # https://twistedmatrix.com/documents/current/api/twisted.internet.fdesc.html#writeToFD
with open(filename, 'w') as keys_file:
keys_file.write(public_key.key_data)
keys_file.write(private_key.key_data)
logger.debug('Export ok')
self._signaler.signal(self._signaler.keymanager_export_ok)
- except IOError as e:
- logger.error("IOError exporting key. {0!r}".format(e))
+
+ def log_error(failure):
+ logger.error(
+ "Error exporting key. {0!r}".format(failure.value))
self._signaler.signal(self._signaler.keymanager_export_error)
+ dpub = keymanager.get_key(username, openpgp.OpenPGPKey)
+ dpriv = keymanager.get_key(username, openpgp.OpenPGPKey,
+ private=True)
+ d = defer.gatherResults([dpub, dpriv])
+ d.addCallback(export)
+ d.addErrback(log_error)
+
def list_keys(self):
"""
List all the keys stored in the local DB.
"""
- keys = self._keymanager_proxy.get_all_keys()
- self._signaler.signal(self._signaler.keymanager_keys_list, keys)
+ d = self._keymanager_proxy.get_all_keys()
+ d.addCallback(
+ lambda keys:
+ self._signaler.signal(self._signaler.keymanager_keys_list, keys))
def get_key_details(self, username):
"""
List all the keys stored in the local DB.
"""
- public_key = self._keymanager_proxy.get_key(username,
- openpgp.OpenPGPKey)
- details = (public_key.key_id, public_key.fingerprint)
- self._signaler.signal(self._signaler.keymanager_key_details, details)
+ def signal_details(public_key):
+ details = (public_key.key_id, public_key.fingerprint)
+ self._signaler.signal(self._signaler.keymanager_key_details,
+ details)
+
+ d = self._keymanager_proxy.get_key(username,
+ openpgp.OpenPGPKey)
+ d.addCallback(signal_details)
class Mail(object):
@@ -1070,12 +1043,10 @@ class Mail(object):
"""
Stop imap and wait until the service is stopped to signal that is done.
"""
- cv = Condition()
- cv.acquire()
- threads.deferToThread(self._imap_controller.stop_imap_service, cv)
+ # FIXME just get a fucking deferred and signal as a callback, with
+ # timeout and cancellability
+ threads.deferToThread(self._imap_controller.stop_imap_service)
logger.debug('Waiting for imap service to stop.')
- cv.wait(self.SERVICE_STOP_TIMEOUT)
- logger.debug('IMAP stopped')
self._signaler.signal(self._signaler.imap_stopped)
def stop_imap_service(self):
diff --git a/src/leap/bitmask/backend/leapbackend.py b/src/leap/bitmask/backend/leapbackend.py
index 3b023563..cf45c4f8 100644
--- a/src/leap/bitmask/backend/leapbackend.py
+++ b/src/leap/bitmask/backend/leapbackend.py
@@ -17,16 +17,15 @@
"""
Backend for everything
"""
-import logging
-
import zope.interface
import zope.proxy
from leap.bitmask.backend import components
from leap.bitmask.backend.backend import Backend
from leap.bitmask.backend.settings import Settings
+from leap.bitmask.logs.utils import get_logger
-logger = logging.getLogger(__name__)
+logger = get_logger()
ERROR_KEY = "error"
PASSED_KEY = "passed"
diff --git a/src/leap/bitmask/backend/leapsignaler.py b/src/leap/bitmask/backend/leapsignaler.py
index c0fdffdc..1ac51f5e 100644
--- a/src/leap/bitmask/backend/leapsignaler.py
+++ b/src/leap/bitmask/backend/leapsignaler.py
@@ -109,6 +109,7 @@ class LeapSignaler(SignalerQt):
srp_password_change_badpw = QtCore.Signal()
srp_password_change_error = QtCore.Signal()
srp_password_change_ok = QtCore.Signal()
+ srp_registration_disabled = QtCore.Signal()
srp_registration_failed = QtCore.Signal()
srp_registration_finished = QtCore.Signal()
srp_registration_taken = QtCore.Signal()
diff --git a/src/leap/bitmask/backend/settings.py b/src/leap/bitmask/backend/settings.py
index 5cb4c616..dedfc13d 100644
--- a/src/leap/bitmask/backend/settings.py
+++ b/src/leap/bitmask/backend/settings.py
@@ -18,13 +18,13 @@
Backend settings
"""
import ConfigParser
-import logging
import os
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.util import get_path_prefix
from leap.common.check import leap_assert, leap_assert_type
-logger = logging.getLogger(__name__)
+logger = get_logger()
# We need this one available for the default decorator
GATEWAY_AUTOMATIC = "Automatic"
@@ -122,37 +122,36 @@ class Settings(object):
self._settings.set(provider, self.GATEWAY_KEY, gateway)
self._save()
- def get_uuid(self, username):
+ def get_uuid(self, full_user_id):
"""
Gets the uuid for a given username.
- :param username: the full user identifier in the form user@provider
- :type username: basestring
+ :param full_user_id: the full user identifier in the form user@provider
+ :type full_user_id: basestring
"""
- leap_assert("@" in username,
+ leap_assert("@" in full_user_id,
"Expected username in the form user@provider")
- user, provider = username.split('@')
+ username, provider = full_user_id.split('@')
+ return self._get_value(provider, full_user_id, "")
- return self._get_value(provider, username, "")
-
- def set_uuid(self, username, value):
+ def set_uuid(self, full_user_id, value):
"""
Sets the uuid for a given username.
- :param username: the full user identifier in the form user@provider
- :type username: str or unicode
+ :param full_user_id: the full user identifier in the form user@provider
+ :type full_user_id: str or unicode
:param value: the uuid to save or None to remove it
:type value: str or unicode or None
"""
- leap_assert("@" in username,
+ leap_assert("@" in full_user_id,
"Expected username in the form user@provider")
- user, provider = username.split('@')
+ user, provider = full_user_id.split('@')
if value is None:
- self._settings.remove_option(provider, username)
+ self._settings.remove_option(provider, full_user_id)
else:
leap_assert(len(value) > 0, "We cannot save an empty uuid")
self._add_section(provider)
- self._settings.set(provider, username, value)
+ self._settings.set(provider, full_user_id, value)
self._save()
diff --git a/src/leap/bitmask/backend/signaler.py b/src/leap/bitmask/backend/signaler.py
index aec2f606..c5335eb8 100644
--- a/src/leap/bitmask/backend/signaler.py
+++ b/src/leap/bitmask/backend/signaler.py
@@ -27,9 +27,9 @@ import zmq
from leap.bitmask.backend.api import SIGNALS
from leap.bitmask.backend.utils import get_frontend_certificates
from leap.bitmask.config import flags
+from leap.bitmask.logs.utils import get_logger
-import logging
-logger = logging.getLogger(__name__)
+logger = get_logger()
class Signaler(object):
diff --git a/src/leap/bitmask/backend/signaler_qt.py b/src/leap/bitmask/backend/signaler_qt.py
index b7f48d21..e3244934 100644
--- a/src/leap/bitmask/backend/signaler_qt.py
+++ b/src/leap/bitmask/backend/signaler_qt.py
@@ -33,9 +33,9 @@ except ImportError:
from leap.bitmask.backend.api import SIGNALS
from leap.bitmask.backend.utils import get_frontend_certificates
from leap.bitmask.config import flags
+from leap.bitmask.logs.utils import get_logger
-import logging
-logger = logging.getLogger(__name__)
+logger = get_logger()
class SignalerQt(QtCore.QObject):
diff --git a/src/leap/bitmask/backend/utils.py b/src/leap/bitmask/backend/utils.py
index b2674330..3b5effc5 100644
--- a/src/leap/bitmask/backend/utils.py
+++ b/src/leap/bitmask/backend/utils.py
@@ -17,7 +17,6 @@
"""
Backend utilities to handle ZMQ certificates.
"""
-import logging
import os
import shutil
import stat
@@ -30,11 +29,12 @@ except ImportError:
pass
from leap.bitmask.config import flags
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.util import get_path_prefix
from leap.common.files import mkdir_p
from leap.common.check import leap_assert
-logger = logging.getLogger(__name__)
+logger = get_logger()
KEYS_DIR = os.path.join(get_path_prefix(), 'leap', 'zmq_certificates')
diff --git a/src/leap/bitmask/backend_app.py b/src/leap/bitmask/backend_app.py
index 3e88a95a..1300ed05 100644
--- a/src/leap/bitmask/backend_app.py
+++ b/src/leap/bitmask/backend_app.py
@@ -17,18 +17,19 @@
"""
Start point for the Backend.
"""
-import logging
import multiprocessing
import signal
+from twisted.internet import reactor
+
+from leap.common.events import server as event_server
+
from leap.bitmask.backend.leapbackend import LeapBackend
from leap.bitmask.backend.utils import generate_zmq_certificates
from leap.bitmask.config import flags
-from leap.bitmask.logs.utils import create_logger
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.util import dict_to_flags
-logger = logging.getLogger(__name__)
-
def signal_handler(signum, frame):
"""
@@ -44,18 +45,33 @@ def signal_handler(signum, frame):
# In the future we may need to do the stop in here when the frontend and
# the backend are run separately (without multiprocessing)
pname = multiprocessing.current_process().name
- logger.debug("{0}: SIGNAL #{1} catched.".format(pname, signum))
+ print "{0}: SIGNAL #{1} catched.".format(pname, signum)
def run_backend(bypass_checks=False, flags_dict=None, frontend_pid=None):
"""
Run the backend for the application.
+ This is called from the main app.py entrypoint, and is run in a child
+ subprocess.
:param bypass_checks: whether we should bypass the checks or not
:type bypass_checks: bool
:param flags_dict: a dict containing the flag values set on app start.
:type flags_dict: dict
"""
+ # In the backend, we want all the components to log into logbook
+ # that is: logging handlers and twisted logs
+ from logbook.compat import redirect_logging
+ from twisted.python.log import PythonLoggingObserver
+ redirect_logging()
+ observer = PythonLoggingObserver()
+ observer.start()
+
+ # NOTE: this needs to be used here, within the call since this function is
+ # executed in a different process and it seems that the process/thread
+ # identification isn't working 100%
+ logger = get_logger() # noqa
+
# The backend is the one who always creates the certificates. Either if it
# is run separately or in a process in the same app as the frontend.
if flags.ZMQ_HAS_CURVE:
@@ -68,11 +84,24 @@ def run_backend(bypass_checks=False, flags_dict=None, frontend_pid=None):
if flags_dict is not None:
dict_to_flags(flags_dict)
+ reactor.callWhenRunning(start_events_and_updater, logger)
+
backend = LeapBackend(bypass_checks=bypass_checks,
frontend_pid=frontend_pid)
backend.run()
+def start_events_and_updater(logger):
+ event_server.ensure_server()
+
+ if flags.STANDALONE:
+ try:
+ from leap.bitmask.updater import Updater
+ updater = Updater()
+ updater.start()
+ except ImportError:
+ logger.error("Updates are not enabled in this distribution.")
+
+
if __name__ == '__main__':
- logger = create_logger(debug=True)
run_backend()
diff --git a/src/leap/bitmask/config/flags.py b/src/leap/bitmask/config/flags.py
index cdde1971..1cf1d15a 100644
--- a/src/leap/bitmask/config/flags.py
+++ b/src/leap/bitmask/config/flags.py
@@ -58,3 +58,7 @@ SKIP_WIZARD_CHECKS = False
# This flag tells us whether the current pyzmq supports using CurveZMQ or not.
ZMQ_HAS_CURVE = None
+
+# Store the needed loglevel globally since the logger handlers goes through
+# threads and processes
+DEBUG = False
diff --git a/src/leap/bitmask/config/leapsettings.py b/src/leap/bitmask/config/leapsettings.py
index 13a1e99e..484a8a25 100644
--- a/src/leap/bitmask/config/leapsettings.py
+++ b/src/leap/bitmask/config/leapsettings.py
@@ -18,14 +18,14 @@
QSettings abstraction.
"""
import os
-import logging
from PySide import QtCore
from leap.common.check import leap_assert, leap_assert_type
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.util import get_path_prefix
-logger = logging.getLogger(__name__)
+logger = get_logger()
def to_bool(val):
@@ -353,35 +353,3 @@ class LeapSettings(object):
"""
leap_assert_type(skip, bool)
self._settings.setValue(self.SKIPFIRSTRUN_KEY, skip)
-
- def get_uuid(self, username):
- """
- Gets the uuid for a given username.
-
- :param username: the full user identifier in the form user@provider
- :type username: basestring
- """
- leap_assert("@" in username,
- "Expected username in the form user@provider")
- user, provider = username.split('@')
- return self._settings.value(
- self.UUIDFORUSER_KEY % (provider, user), "")
-
- def set_uuid(self, username, value):
- """
- Sets the uuid for a given username.
-
- :param username: the full user identifier in the form user@provider
- :type username: str or unicode
- :param value: the uuid to save or None to remove it
- :type value: str or unicode or None
- """
- leap_assert("@" in username,
- "Expected username in the form user@provider")
- user, provider = username.split('@')
- key = self.UUIDFORUSER_KEY % (provider, user)
- if value is None:
- self._settings.remove(key)
- else:
- leap_assert(len(value) > 0, "We cannot save an empty uuid")
- self._settings.setValue(key, value)
diff --git a/src/leap/bitmask/config/providerconfig.py b/src/leap/bitmask/config/providerconfig.py
index 386c697d..d972b280 100644
--- a/src/leap/bitmask/config/providerconfig.py
+++ b/src/leap/bitmask/config/providerconfig.py
@@ -18,18 +18,18 @@
"""
Provider configuration
"""
-import logging
import os
from leap.bitmask import provider
from leap.bitmask.config import flags
from leap.bitmask.config.provider_spec import leap_provider_spec
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.services import get_service_display_name
from leap.bitmask.util import get_path_prefix
from leap.common.check import leap_check
from leap.common.config.baseconfig import BaseConfig, LocalizedKey
-logger = logging.getLogger(__name__)
+logger = get_logger()
class MissingCACert(Exception):
@@ -69,6 +69,7 @@ class ProviderConfig(BaseConfig):
details["description"] = config.get_description(lang=lang)
details["enrollment_policy"] = config.get_enrollment_policy()
details["services"] = config.get_services()
+ details["allow_registration"] = config.get_allow_registration()
services = []
for service in config.get_services():
@@ -177,6 +178,15 @@ class ProviderConfig(BaseConfig):
services = self._safe_get_value("services")
return services
+ def get_allow_registration(self):
+ """
+ Return whether the registration is allowed or not in the provider.
+
+ :rtype: bool
+ """
+ service = self._safe_get_value("service")
+ return service['allow_registration']
+
def get_ca_cert_path(self, about_to_download=False):
"""
Returns the path to the certificate for the current provider.
diff --git a/src/leap/bitmask/crypto/certs.py b/src/leap/bitmask/crypto/certs.py
index c3ca4efb..4b669376 100644
--- a/src/leap/bitmask/crypto/certs.py
+++ b/src/leap/bitmask/crypto/certs.py
@@ -17,17 +17,17 @@
"""
Utilities for dealing with client certs
"""
-import logging
import os
from leap.bitmask.crypto.srpauth import SRPAuth
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.util.constants import REQUEST_TIMEOUT
from leap.common.files import check_and_fix_urw_only
from leap.common.files import mkdir_p
from leap.common import certs as leap_certs
-logger = logging.getLogger(__name__)
+logger = get_logger()
def download_client_cert(provider_config, path, session):
diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py
index c2a5f158..452bfa66 100644
--- a/src/leap/bitmask/crypto/srpauth.py
+++ b/src/leap/bitmask/crypto/srpauth.py
@@ -14,9 +14,7 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
import binascii
-import logging
import threading
import sys
@@ -33,14 +31,14 @@ from twisted.internet import threads
from twisted.internet.defer import CancelledError
from leap.bitmask.backend.settings import Settings
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.util import request_helpers as reqhelper
from leap.bitmask.util.compat import requests_has_max_retries
from leap.bitmask.util.constants import REQUEST_TIMEOUT
from leap.common.check import leap_assert
-from leap.common.events import signal as events_signal
-from leap.common.events import events_pb2 as proto
+from leap.common.events import emit, catalog
-logger = logging.getLogger(__name__)
+logger = get_logger()
class SRPAuthenticationError(Exception):
@@ -118,475 +116,513 @@ class SRPAuthNoSessionId(SRPAuthenticationError):
pass
-class SRPAuth(object):
+class SRPAuthImpl(object):
"""
- SRPAuth singleton
+ Implementation of the SRPAuth interface
"""
- class __impl(object):
+ LOGIN_KEY = "login"
+ A_KEY = "A"
+ CLIENT_AUTH_KEY = "client_auth"
+ SESSION_ID_KEY = "_session_id"
+ USER_VERIFIER_KEY = 'user[password_verifier]'
+ USER_SALT_KEY = 'user[password_salt]'
+ AUTHORIZATION_KEY = "Authorization"
+
+ def __init__(self, provider_config):
+ """
+ Constructor for SRPAuth implementation
+
+ :param provider_config: ProviderConfig needed to authenticate.
+ :type provider_config: ProviderConfig
+ """
+ leap_assert(provider_config,
+ "We need a provider config to authenticate")
+
+ self._provider_config = provider_config
+ self._settings = Settings()
+
+ # **************************************************** #
+ # Dependency injection helpers, override this for more
+ # granular testing
+ self._fetcher = requests
+ self._srp = srp
+ self._hashfun = self._srp.SHA256
+ self._ng = self._srp.NG_1024
+ # **************************************************** #
+
+ self._reset_session()
+
+ self._session_id = None
+ self._session_id_lock = threading.Lock()
+ self._uuid = None
+ self._uuid_lock = threading.Lock()
+ self._token = None
+ self._token_lock = threading.Lock()
+
+ self._srp_user = None
+ self._srp_a = None
+
+ # User credentials stored for password changing checks
+ self._username = None
+ self._password = None
+
+ def _reset_session(self):
"""
- Implementation of the SRPAuth interface
+ Resets the current session and sets max retries to 30.
"""
+ self._session = self._fetcher.session()
+ # We need to bump the default retries, otherwise logout
+ # fails most of the times
+ # NOTE: This is a workaround for the moment, the server
+ # side seems to return correctly every time, but it fails
+ # on the client end.
+ if requests_has_max_retries:
+ adapter = HTTPAdapter(max_retries=30)
+ else:
+ adapter = HTTPAdapter()
+ self._session.mount('https://', adapter)
+
+ def _safe_unhexlify(self, val):
+ """
+ Rounds the val to a multiple of 2 and returns the
+ unhexlified value
- LOGIN_KEY = "login"
- A_KEY = "A"
- CLIENT_AUTH_KEY = "client_auth"
- SESSION_ID_KEY = "_session_id"
- USER_VERIFIER_KEY = 'user[password_verifier]'
- USER_SALT_KEY = 'user[password_salt]'
- AUTHORIZATION_KEY = "Authorization"
+ :param val: hexlified value
+ :type val: str
- def __init__(self, provider_config, signaler=None):
- """
- Constructor for SRPAuth implementation
+ :rtype: binary hex data
+ :return: unhexlified val
+ """
+ return binascii.unhexlify(val) \
+ if (len(val) % 2 == 0) else binascii.unhexlify('0' + val)
- :param provider_config: ProviderConfig needed to authenticate.
- :type provider_config: ProviderConfig
- :param signaler: Signaler object used to receive notifications
- from the backend
- :type signaler: Signaler
- """
- leap_assert(provider_config,
- "We need a provider config to authenticate")
+ def _authentication_preprocessing(self, username, password):
+ """
+ Generates the SRP.User to get the A SRP parameter
- self._provider_config = provider_config
- self._signaler = signaler
- self._settings = Settings()
-
- # **************************************************** #
- # Dependency injection helpers, override this for more
- # granular testing
- self._fetcher = requests
- self._srp = srp
- self._hashfun = self._srp.SHA256
- self._ng = self._srp.NG_1024
- # **************************************************** #
-
- self._reset_session()
-
- self._session_id = None
- self._session_id_lock = threading.Lock()
- self._uuid = None
- self._uuid_lock = threading.Lock()
- self._token = None
- self._token_lock = threading.Lock()
-
- self._srp_user = None
- self._srp_a = None
+ :param username: username to login
+ :type username: str
+ :param password: password for the username
+ :type password: str
+ """
+ logger.debug("Authentication preprocessing...")
- # User credentials stored for password changing checks
- self._username = None
- self._password = None
+ self._srp_user = self._srp.User(username.encode('utf-8'),
+ password.encode('utf-8'),
+ self._hashfun, self._ng)
+ _, A = self._srp_user.start_authentication()
- def _reset_session(self):
- """
- Resets the current session and sets max retries to 30.
- """
- self._session = self._fetcher.session()
- # We need to bump the default retries, otherwise logout
- # fails most of the times
- # NOTE: This is a workaround for the moment, the server
- # side seems to return correctly every time, but it fails
- # on the client end.
- if requests_has_max_retries:
- adapter = HTTPAdapter(max_retries=30)
- else:
- adapter = HTTPAdapter()
- self._session.mount('https://', adapter)
+ self._srp_a = A
- def _safe_unhexlify(self, val):
- """
- Rounds the val to a multiple of 2 and returns the
- unhexlified value
+ def _start_authentication(self, _, username):
+ """
+ Sends the first request for authentication to retrieve the
+ salt and B parameter
+
+ Might raise all SRPAuthenticationError based:
+ SRPAuthenticationError
+ SRPAuthConnectionError
+ SRPAuthBadStatusCode
+ SRPAuthNoSalt
+ SRPAuthNoB
+
+ :param _: IGNORED, output from the previous callback (None)
+ :type _: IGNORED
+ :param username: username to login
+ :type username: str
- :param val: hexlified value
- :type val: str
+ :return: salt and B parameters
+ :rtype: tuple
+ """
+ logger.debug("Starting authentication process...")
+ try:
+ auth_data = {
+ self.LOGIN_KEY: username,
+ self.A_KEY: binascii.hexlify(self._srp_a)
+ }
+ sessions_url = "%s/%s/%s/" % \
+ (self._provider_config.get_api_uri(),
+ self._provider_config.get_api_version(),
+ "sessions")
+
+ ca_cert_path = self._provider_config.get_ca_cert_path()
+ ca_cert_path = ca_cert_path.encode(sys.getfilesystemencoding())
+
+ init_session = self._session.post(sessions_url,
+ data=auth_data,
+ verify=ca_cert_path,
+ timeout=REQUEST_TIMEOUT)
+ # Clean up A value, we don't need it anymore
+ self._srp_a = None
+ except requests.exceptions.ConnectionError as e:
+ logger.error("No connection made (salt): {0!r}".format(e))
+ raise SRPAuthConnectionError()
+ except Exception as e:
+ logger.error("Unknown error: %r" % (e,))
+ raise SRPAuthenticationError()
- :rtype: binary hex data
- :return: unhexlified val
- """
- return binascii.unhexlify(val) \
- if (len(val) % 2 == 0) else binascii.unhexlify('0' + val)
+ content, mtime = reqhelper.get_content(init_session)
- def _authentication_preprocessing(self, username, password):
- """
- Generates the SRP.User to get the A SRP parameter
+ if init_session.status_code not in (200,):
+ logger.error("No valid response (salt): "
+ "Status code = %r. Content: %r" %
+ (init_session.status_code, content))
+ if init_session.status_code == 422:
+ logger.error("Invalid username or password.")
+ raise SRPAuthBadUserOrPassword()
- :param username: username to login
- :type username: str
- :param password: password for the username
- :type password: str
- """
- logger.debug("Authentication preprocessing...")
+ logger.error("There was a problem with authentication.")
+ raise SRPAuthBadStatusCode()
- self._srp_user = self._srp.User(username.encode('utf-8'),
- password.encode('utf-8'),
- self._hashfun, self._ng)
- _, A = self._srp_user.start_authentication()
+ json_content = json.loads(content)
+ salt = json_content.get("salt", None)
+ B = json_content.get("B", None)
- self._srp_a = A
+ if salt is None:
+ logger.error("The server didn't send the salt parameter.")
+ raise SRPAuthNoSalt()
+ if B is None:
+ logger.error("The server didn't send the B parameter.")
+ raise SRPAuthNoB()
- def _start_authentication(self, _, username):
- """
- Sends the first request for authentication to retrieve the
- salt and B parameter
+ return salt, B
- Might raise all SRPAuthenticationError based:
- SRPAuthenticationError
- SRPAuthConnectionError
- SRPAuthBadStatusCode
- SRPAuthNoSalt
- SRPAuthNoB
+ def _process_challenge(self, salt_B, username):
+ """
+ Given the salt and B processes the auth challenge and
+ generates the M2 parameter
+
+ Might raise SRPAuthenticationError based:
+ SRPAuthenticationError
+ SRPAuthBadDataFromServer
+ SRPAuthConnectionError
+ SRPAuthJSONDecodeError
+ SRPAuthBadUserOrPassword
+
+ :param salt_B: salt and B parameters for the username
+ :type salt_B: tuple
+ :param username: username for this session
+ :type username: str
- :param _: IGNORED, output from the previous callback (None)
- :type _: IGNORED
- :param username: username to login
- :type username: str
+ :return: the M2 SRP parameter
+ :rtype: str
+ """
+ logger.debug("Processing challenge...")
+ try:
+ salt, B = salt_B
+ unhex_salt = self._safe_unhexlify(salt)
+ unhex_B = self._safe_unhexlify(B)
+ except (TypeError, ValueError) as e:
+ logger.error("Bad data from server: %r" % (e,))
+ raise SRPAuthBadDataFromServer()
+ M = self._srp_user.process_challenge(unhex_salt, unhex_B)
+
+ auth_url = "%s/%s/%s/%s" % (self._provider_config.get_api_uri(),
+ self._provider_config.
+ get_api_version(),
+ "sessions",
+ username)
+
+ auth_data = {
+ self.CLIENT_AUTH_KEY: binascii.hexlify(M)
+ }
- :return: salt and B parameters
- :rtype: tuple
- """
- logger.debug("Starting authentication process...")
- try:
- auth_data = {
- self.LOGIN_KEY: username,
- self.A_KEY: binascii.hexlify(self._srp_a)
- }
- sessions_url = "%s/%s/%s/" % \
- (self._provider_config.get_api_uri(),
- self._provider_config.get_api_version(),
- "sessions")
-
- ca_cert_path = self._provider_config.get_ca_cert_path()
- ca_cert_path = ca_cert_path.encode(sys.getfilesystemencoding())
-
- init_session = self._session.post(sessions_url,
- data=auth_data,
- verify=ca_cert_path,
- timeout=REQUEST_TIMEOUT)
- # Clean up A value, we don't need it anymore
- self._srp_a = None
- except requests.exceptions.ConnectionError as e:
- logger.error("No connection made (salt): {0!r}".format(e))
- raise SRPAuthConnectionError()
- except Exception as e:
- logger.error("Unknown error: %r" % (e,))
- raise SRPAuthenticationError()
-
- content, mtime = reqhelper.get_content(init_session)
-
- if init_session.status_code not in (200,):
- logger.error("No valid response (salt): "
- "Status code = %r. Content: %r" %
- (init_session.status_code, content))
- if init_session.status_code == 422:
- logger.error("Invalid username or password.")
- raise SRPAuthBadUserOrPassword()
-
- logger.error("There was a problem with authentication.")
- raise SRPAuthBadStatusCode()
-
- json_content = json.loads(content)
- salt = json_content.get("salt", None)
- B = json_content.get("B", None)
-
- if salt is None:
- logger.error("The server didn't send the salt parameter.")
- raise SRPAuthNoSalt()
- if B is None:
- logger.error("The server didn't send the B parameter.")
- raise SRPAuthNoB()
-
- return salt, B
-
- def _process_challenge(self, salt_B, username):
- """
- Given the salt and B processes the auth challenge and
- generates the M2 parameter
-
- Might raise SRPAuthenticationError based:
- SRPAuthenticationError
- SRPAuthBadDataFromServer
- SRPAuthConnectionError
- SRPAuthJSONDecodeError
- SRPAuthBadUserOrPassword
-
- :param salt_B: salt and B parameters for the username
- :type salt_B: tuple
- :param username: username for this session
- :type username: str
-
- :return: the M2 SRP parameter
- :rtype: str
- """
- logger.debug("Processing challenge...")
- try:
- salt, B = salt_B
- unhex_salt = self._safe_unhexlify(salt)
- unhex_B = self._safe_unhexlify(B)
- except (TypeError, ValueError) as e:
- logger.error("Bad data from server: %r" % (e,))
- raise SRPAuthBadDataFromServer()
- M = self._srp_user.process_challenge(unhex_salt, unhex_B)
-
- auth_url = "%s/%s/%s/%s" % (self._provider_config.get_api_uri(),
- self._provider_config.
- get_api_version(),
- "sessions",
- username)
+ try:
+ auth_result = self._session.put(auth_url,
+ data=auth_data,
+ verify=self._provider_config.
+ get_ca_cert_path(),
+ timeout=REQUEST_TIMEOUT)
+ except requests.exceptions.ConnectionError as e:
+ logger.error("No connection made (HAMK): %r" % (e,))
+ raise SRPAuthConnectionError()
- auth_data = {
- self.CLIENT_AUTH_KEY: binascii.hexlify(M)
- }
+ try:
+ content, mtime = reqhelper.get_content(auth_result)
+ except JSONDecodeError:
+ logger.error("Bad JSON content in auth result.")
+ raise SRPAuthJSONDecodeError()
+ if auth_result.status_code == 422:
+ error = ""
try:
- auth_result = self._session.put(auth_url,
- data=auth_data,
- verify=self._provider_config.
- get_ca_cert_path(),
- timeout=REQUEST_TIMEOUT)
- except requests.exceptions.ConnectionError as e:
- logger.error("No connection made (HAMK): %r" % (e,))
- raise SRPAuthConnectionError()
+ error = json.loads(content).get("errors", "")
+ except ValueError:
+ logger.error("Problem parsing the received response: %s"
+ % (content,))
+ except AttributeError:
+ logger.error("Expecting a dict but something else was "
+ "received: %s", (content,))
+ logger.error("[%s] Wrong password (HAMK): [%s]" %
+ (auth_result.status_code, error))
+ raise SRPAuthBadUserOrPassword()
+
+ if auth_result.status_code not in (200,):
+ logger.error("No valid response (HAMK): "
+ "Status code = %s. Content = %r" %
+ (auth_result.status_code, content))
+ raise SRPAuthBadStatusCode()
+
+ return json.loads(content)
+
+ def _extract_data(self, json_content):
+ """
+ Extracts the necessary parameters from json_content (M2,
+ id, token)
- try:
- content, mtime = reqhelper.get_content(auth_result)
- except JSONDecodeError:
- logger.error("Bad JSON content in auth result.")
- raise SRPAuthJSONDecodeError()
-
- if auth_result.status_code == 422:
- error = ""
- try:
- error = json.loads(content).get("errors", "")
- except ValueError:
- logger.error("Problem parsing the received response: %s"
- % (content,))
- except AttributeError:
- logger.error("Expecting a dict but something else was "
- "received: %s", (content,))
- logger.error("[%s] Wrong password (HAMK): [%s]" %
- (auth_result.status_code, error))
- raise SRPAuthBadUserOrPassword()
+ Might raise SRPAuthenticationError based:
+ SRPBadDataFromServer
- if auth_result.status_code not in (200,):
- logger.error("No valid response (HAMK): "
- "Status code = %s. Content = %r" %
- (auth_result.status_code, content))
- raise SRPAuthBadStatusCode()
+ :param json_content: Data received from the server
+ :type json_content: dict
+ """
+ try:
+ M2 = json_content.get("M2", None)
+ uuid = json_content.get("id", None)
+ token = json_content.get("token", None)
+ except Exception as e:
+ logger.error(e)
+ raise SRPAuthBadDataFromServer()
- return json.loads(content)
+ self.set_uuid(uuid)
+ self.set_token(token)
- def _extract_data(self, json_content):
- """
- Extracts the necessary parameters from json_content (M2,
- id, token)
+ if M2 is None or self.get_uuid() is None:
+ logger.error("Something went wrong. Content = %r" %
+ (json_content,))
+ raise SRPAuthBadDataFromServer()
- Might raise SRPAuthenticationError based:
- SRPBadDataFromServer
+ emit(catalog.CLIENT_UID, uuid) # make the rpc call async
- :param json_content: Data received from the server
- :type json_content: dict
- """
- try:
- M2 = json_content.get("M2", None)
- uuid = json_content.get("id", None)
- token = json_content.get("token", None)
- except Exception as e:
- logger.error(e)
- raise SRPAuthBadDataFromServer()
+ return M2
- self.set_uuid(uuid)
- self.set_token(token)
+ def _verify_session(self, M2):
+ """
+ Verifies the session based on the M2 parameter. If the
+ verification succeeds, it sets the session_id for this
+ session
- if M2 is None or self.get_uuid() is None:
- logger.error("Something went wrong. Content = %r" %
- (json_content,))
- raise SRPAuthBadDataFromServer()
+ Might raise SRPAuthenticationError based:
+ SRPAuthBadDataFromServer
+ SRPAuthVerificationFailed
- events_signal(
- proto.CLIENT_UID, content=uuid,
- reqcbk=lambda req, res: None) # make the rpc call async
+ :param M2: M2 SRP parameter
+ :type M2: str
+ """
+ logger.debug("Verifying session...")
+ try:
+ unhex_M2 = self._safe_unhexlify(M2)
+ except TypeError:
+ logger.error("Bad data from server (HAWK)")
+ raise SRPAuthBadDataFromServer()
- return M2
+ self._srp_user.verify_session(unhex_M2)
- def _verify_session(self, M2):
- """
- Verifies the session based on the M2 parameter. If the
- verification succeeds, it sets the session_id for this
- session
+ if not self._srp_user.authenticated():
+ logger.error("Auth verification failed.")
+ raise SRPAuthVerificationFailed()
+ logger.debug("Session verified.")
- Might raise SRPAuthenticationError based:
- SRPAuthBadDataFromServer
- SRPAuthVerificationFailed
+ session_id = self._session.cookies.get(self.SESSION_ID_KEY, None)
+ if not session_id:
+ logger.error("Bad cookie from server (missing _session_id)")
+ raise SRPAuthNoSessionId()
- :param M2: M2 SRP parameter
- :type M2: str
- """
- logger.debug("Verifying session...")
- try:
- unhex_M2 = self._safe_unhexlify(M2)
- except TypeError:
- logger.error("Bad data from server (HAWK)")
- raise SRPAuthBadDataFromServer()
+ # make the rpc call async
+ emit(catalog.CLIENT_SESSION_ID, session_id)
- self._srp_user.verify_session(unhex_M2)
+ self.set_session_id(session_id)
+ logger.debug("SUCCESS LOGIN")
+ return True
- if not self._srp_user.authenticated():
- logger.error("Auth verification failed.")
- raise SRPAuthVerificationFailed()
- logger.debug("Session verified.")
+ def _threader(self, cb, res, *args, **kwargs):
+ return threads.deferToThread(cb, res, *args, **kwargs)
- session_id = self._session.cookies.get(self.SESSION_ID_KEY, None)
- if not session_id:
- logger.error("Bad cookie from server (missing _session_id)")
- raise SRPAuthNoSessionId()
+ def _change_password(self, current_password, new_password):
+ """
+ Changes the password for the currently logged user if the current
+ password match.
+ It requires to be authenticated.
- events_signal(
- proto.CLIENT_SESSION_ID, content=session_id,
- reqcbk=lambda req, res: None) # make the rpc call async
+ Might raise:
+ SRPAuthBadUserOrPassword
+ requests.exceptions.HTTPError
- self.set_session_id(session_id)
+ :param current_password: the current password for the logged user.
+ :type current_password: str
+ :param new_password: the new password for the user
+ :type new_password: str
+ """
+ leap_assert(self.get_uuid() is not None)
+
+ if current_password != self._password:
+ raise SRPAuthBadUserOrPassword
+
+ url = "%s/%s/users/%s.json" % (
+ self._provider_config.get_api_uri(),
+ self._provider_config.get_api_version(),
+ self.get_uuid())
+
+ salt, verifier = self._srp.create_salted_verification_key(
+ self._username.encode('utf-8'), new_password.encode('utf-8'),
+ self._hashfun, self._ng)
+
+ cookies = {self.SESSION_ID_KEY: self.get_session_id()}
+ headers = {
+ self.AUTHORIZATION_KEY:
+ "Token token={0}".format(self.get_token())
+ }
+ user_data = {
+ self.USER_VERIFIER_KEY: binascii.hexlify(verifier),
+ self.USER_SALT_KEY: binascii.hexlify(salt)
+ }
+
+ change_password = self._session.put(
+ url, data=user_data,
+ verify=self._provider_config.get_ca_cert_path(),
+ cookies=cookies,
+ timeout=REQUEST_TIMEOUT,
+ headers=headers)
+
+ # In case of non 2xx it raises HTTPError
+ change_password.raise_for_status()
+
+ self._password = new_password
- def _threader(self, cb, res, *args, **kwargs):
- return threads.deferToThread(cb, res, *args, **kwargs)
+ def change_password(self, current_password, new_password):
+ """
+ Changes the password for the currently logged user if the current
+ password match.
+ It requires to be authenticated.
- def _change_password(self, current_password, new_password):
- """
- Changes the password for the currently logged user if the current
- password match.
- It requires to be authenticated.
+ :param current_password: the current password for the logged user.
+ :type current_password: str
+ :param new_password: the new password for the user
+ :type new_password: str
+ """
+ d = threads.deferToThread(
+ self._change_password, current_password, new_password)
+ return d
- Might raise:
- SRPAuthBadUserOrPassword
- requests.exceptions.HTTPError
+ def authenticate(self, username, password):
+ """
+ Executes the whole authentication process for a user
- :param current_password: the current password for the logged user.
- :type current_password: str
- :param new_password: the new password for the user
- :type new_password: str
- """
- leap_assert(self.get_uuid() is not None)
+ Might raise SRPAuthenticationError
- if current_password != self._password:
- raise SRPAuthBadUserOrPassword
+ :param username: username for this session
+ :type username: unicode
+ :param password: password for this user
+ :type password: unicode
- url = "%s/%s/users/%s.json" % (
- self._provider_config.get_api_uri(),
- self._provider_config.get_api_version(),
- self.get_uuid())
+ :returns: A defer on a different thread
+ :rtype: twisted.internet.defer.Deferred
+ """
+ leap_assert(self.get_session_id() is None, "Already logged in")
- salt, verifier = self._srp.create_salted_verification_key(
- self._username.encode('utf-8'), new_password.encode('utf-8'),
- self._hashfun, self._ng)
+ # User credentials stored for password changing checks
+ self._username = username
+ self._password = password
- cookies = {self.SESSION_ID_KEY: self.get_session_id()}
- headers = {
- self.AUTHORIZATION_KEY:
- "Token token={0}".format(self.get_token())
- }
- user_data = {
- self.USER_VERIFIER_KEY: binascii.hexlify(verifier),
- self.USER_SALT_KEY: binascii.hexlify(salt)
- }
+ self._reset_session()
- change_password = self._session.put(
- url, data=user_data,
- verify=self._provider_config.get_ca_cert_path(),
- cookies=cookies,
- timeout=REQUEST_TIMEOUT,
- headers=headers)
+ d = threads.deferToThread(self._authentication_preprocessing,
+ username=username,
+ password=password)
+ d.addCallback(partial(self._start_authentication, username=username))
- # In case of non 2xx it raises HTTPError
- change_password.raise_for_status()
+ d.addCallback(partial(self._process_challenge, username=username))
+ d.addCallback(self._extract_data)
+ d.addCallback(self._verify_session)
+ return d
- self._password = new_password
+ def logout(self):
+ """
+ Logs out the current session.
+ Expects a session_id to exists, might raise AssertionError
+ """
+ logger.debug("Starting logout...")
- def change_password(self, current_password, new_password):
- """
- Changes the password for the currently logged user if the current
- password match.
- It requires to be authenticated.
+ if self.get_session_id() is None:
+ logger.debug("Already logged out")
+ return
- :param current_password: the current password for the logged user.
- :type current_password: str
- :param new_password: the new password for the user
- :type new_password: str
- """
- d = threads.deferToThread(
- self._change_password, current_password, new_password)
- d.addCallback(self._change_password_ok)
- d.addErrback(self._change_password_error)
+ logout_url = "%s/%s/%s/" % (self._provider_config.get_api_uri(),
+ self._provider_config.
+ get_api_version(),
+ "logout")
+ try:
+ self._session.delete(logout_url,
+ data=self.get_session_id(),
+ verify=self._provider_config.
+ get_ca_cert_path(),
+ timeout=REQUEST_TIMEOUT)
+ except Exception as e:
+ logger.warning("Something went wrong with the logout: %r" %
+ (e,))
+ raise
+ else:
+ self.set_session_id(None)
+ self.set_uuid(None)
+ self.set_token(None)
+ # Also reset the session
+ self._session = self._fetcher.session()
+ logger.debug("Successfully logged out.")
- def _change_password_ok(self, _):
- """
- Password change callback.
- """
- if self._signaler is not None:
- self._signaler.signal(self._signaler.srp_password_change_ok)
+ def set_session_id(self, session_id):
+ with self._session_id_lock:
+ self._session_id = session_id
- def _change_password_error(self, failure):
- """
- Password change errback.
- """
- logger.debug(
- "Error changing password. Failure: {0}".format(failure))
- if self._signaler is None:
- return
+ def get_session_id(self):
+ with self._session_id_lock:
+ return self._session_id
- if failure.check(SRPAuthBadUserOrPassword):
- self._signaler.signal(self._signaler.srp_password_change_badpw)
- else:
- self._signaler.signal(self._signaler.srp_password_change_error)
+ def set_uuid(self, uuid):
+ with self._uuid_lock:
+ full_uid = "%s@%s" % (
+ self._username, self._provider_config.get_domain())
+ if uuid is not None: # avoid removing the uuid from settings
+ self._settings.set_uuid(full_uid, uuid)
+ self._uuid = uuid
- def authenticate(self, username, password):
- """
- Executes the whole authentication process for a user
+ def get_uuid(self):
+ with self._uuid_lock:
+ return self._uuid
- Might raise SRPAuthenticationError
+ def set_token(self, token):
+ with self._token_lock:
+ self._token = token
- :param username: username for this session
- :type username: unicode
- :param password: password for this user
- :type password: unicode
+ def get_token(self):
+ with self._token_lock:
+ return self._token
- :returns: A defer on a different thread
- :rtype: twisted.internet.defer.Deferred
- """
- leap_assert(self.get_session_id() is None, "Already logged in")
-
- # User credentials stored for password changing checks
- self._username = username
- self._password = password
-
- self._reset_session()
-
- # FIXME ---------------------------------------------------------
- # 1. it makes no sense to defer each callback to a thread
- # 2. the decision to use threads should be at another level.
- # (although it's not really needed, that was a hack around
- # the gui blocks)
- # it makes very hard to test this. The __impl could be
- # separated and decoupled from the provider_config abstraction.
-
- d = threads.deferToThread(self._authentication_preprocessing,
- username=username,
- password=password)
-
- d.addCallback(
- partial(self._threader,
- self._start_authentication),
- username=username)
- d.addCallback(
- partial(self._threader,
- self._process_challenge),
- username=username)
- d.addCallback(
- partial(self._threader,
- self._extract_data))
- d.addCallback(partial(self._threader,
- self._verify_session))
+ def is_authenticated(self):
+ """
+ Return whether the user is authenticated or not.
+
+ :rtype: bool
+ """
+ user = self._srp_user
+ if user is not None:
+ return user.authenticated()
+
+ return False
+
+
+class SRPAuth(object):
+ """
+ SRPAuth singleton
+ """
+ class __impl(SRPAuthImpl):
+
+ def __init__(self, provider_config, signaler=None):
+ SRPAuthImpl.__init__(self, provider_config)
+ self._signaler = signaler
+ def authenticate(self, username, password):
+ d = SRPAuthImpl.authenticate(self, username, password)
d.addCallback(self._authenticate_ok)
d.addErrback(self._authenticate_error)
return d
@@ -630,82 +666,53 @@ class SRPAuth(object):
self._signaler.signal(signal)
- def logout(self):
+ def change_password(self, current_password, new_password):
+ """
+ Changes the password for the currently logged user if the current
+ password match.
+ It requires to be authenticated.
+
+ :param current_password: the current password for the logged user.
+ :type current_password: str
+ :param new_password: the new password for the user
+ :type new_password: str
"""
- Logs out the current session.
- Expects a session_id to exists, might raise AssertionError
+ d = SRPAuthImpl.change_password(self, current_password,
+ new_password)
+ d.addCallback(self._change_password_ok)
+ d.addErrback(self._change_password_error)
+ return d
+
+ def _change_password_ok(self, _):
"""
- logger.debug("Starting logout...")
+ Password change callback.
+ """
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.srp_password_change_ok)
- if self.get_session_id() is None:
- logger.debug("Already logged out")
+ def _change_password_error(self, failure):
+ """
+ Password change errback.
+ """
+ logger.debug(
+ "Error changing password. Failure: {0}".format(failure))
+ if self._signaler is None:
return
- logout_url = "%s/%s/%s/" % (self._provider_config.get_api_uri(),
- self._provider_config.
- get_api_version(),
- "logout")
+ if failure.check(SRPAuthBadUserOrPassword):
+ self._signaler.signal(self._signaler.srp_password_change_badpw)
+ else:
+ self._signaler.signal(self._signaler.srp_password_change_error)
+
+ def logout(self):
try:
- self._session.delete(logout_url,
- data=self.get_session_id(),
- verify=self._provider_config.
- get_ca_cert_path(),
- timeout=REQUEST_TIMEOUT)
- except Exception as e:
- logger.warning("Something went wrong with the logout: %r" %
- (e,))
+ SRPAuthImpl.logout(self)
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.srp_logout_ok)
+ except Exception:
if self._signaler is not None:
self._signaler.signal(self._signaler.srp_logout_error)
raise
- else:
- self.set_session_id(None)
- self.set_uuid(None)
- self.set_token(None)
- # Also reset the session
- self._session = self._fetcher.session()
- logger.debug("Successfully logged out.")
- if self._signaler is not None:
- self._signaler.signal(self._signaler.srp_logout_ok)
-
- def set_session_id(self, session_id):
- with self._session_id_lock:
- self._session_id = session_id
-
- def get_session_id(self):
- with self._session_id_lock:
- return self._session_id
-
- def set_uuid(self, uuid):
- with self._uuid_lock:
- full_uid = "%s@%s" % (
- self._username, self._provider_config.get_domain())
- if uuid is not None: # avoid removing the uuid from settings
- self._settings.set_uuid(full_uid, uuid)
- self._uuid = uuid
-
- def get_uuid(self):
- with self._uuid_lock:
- return self._uuid
-
- def set_token(self, token):
- with self._token_lock:
- self._token = token
-
- def get_token(self):
- with self._token_lock:
- return self._token
-
- def is_authenticated(self):
- """
- Return whether the user is authenticated or not.
-
- :rtype: bool
- """
- user = self._srp_user
- if user is not None:
- return user.authenticated()
-
- return False
__instance = None
diff --git a/src/leap/bitmask/crypto/srpregister.py b/src/leap/bitmask/crypto/srpregister.py
index 86510de1..9bf19377 100644
--- a/src/leap/bitmask/crypto/srpregister.py
+++ b/src/leap/bitmask/crypto/srpregister.py
@@ -26,47 +26,26 @@ from PySide import QtCore
from urlparse import urlparse
from leap.bitmask.config.providerconfig import ProviderConfig
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.util.constants import SIGNUP_TIMEOUT
from leap.bitmask.util.request_helpers import get_content
from leap.common.check import leap_assert, leap_assert_type
-logger = logging.getLogger(__name__)
+logger = get_logger()
-class SRPRegister(QtCore.QObject):
- """
- Registers a user to a specific provider using SRP
- """
+class SRPRegisterImpl:
USER_LOGIN_KEY = 'user[login]'
USER_VERIFIER_KEY = 'user[password_verifier]'
USER_SALT_KEY = 'user[password_salt]'
-
- STATUS_OK = (200, 201)
- STATUS_TAKEN = 422
STATUS_ERROR = -999 # Custom error status
- def __init__(self, signaler=None,
- provider_config=None, register_path="users"):
- """
- Constructor
-
- :param signaler: Signaler object used to receive notifications
- from the backend
- :type signaler: Signaler
- :param provider_config: provider configuration instance,
- properly loaded
- :type privider_config: ProviderConfig
- :param register_path: webapp path for registering users
- :type register_path; str
- """
- QtCore.QObject.__init__(self)
+ def __init__(self, provider_config, register_path):
leap_assert(provider_config, "Please provide a provider")
leap_assert_type(provider_config, ProviderConfig)
self._provider_config = provider_config
- self._signaler = signaler
-
# **************************************************** #
# Dependency injection helpers, override this for more
# granular testing
@@ -83,25 +62,8 @@ class SRPRegister(QtCore.QObject):
self._port = "443"
self._register_path = register_path
-
self._session = self._fetcher.session()
- def _get_registration_uri(self):
- """
- Returns the URI where the register request should be made for
- the provider
-
- :rtype: str
- """
-
- uri = "https://%s:%s/%s/%s" % (
- self._provider,
- self._port,
- self._provider_config.get_api_version(),
- self._register_path)
-
- return uri
-
def register_user(self, username, password):
"""
Registers a user with the validator based on the password provider
@@ -111,8 +73,9 @@ class SRPRegister(QtCore.QObject):
:param password: password for this username
:type password: str
- :returns: if the registration went ok or not.
- :rtype: bool
+ :returns: if the registration went ok or not, and the returned status
+ code of of the request
+ :rtype: (bool, int)
"""
username = username.lower().encode('utf-8')
@@ -152,7 +115,6 @@ class SRPRegister(QtCore.QObject):
status_code = self.STATUS_ERROR
if req is not None:
status_code = req.status_code
- self._emit_result(status_code)
if not ok:
try:
@@ -165,6 +127,67 @@ class SRPRegister(QtCore.QObject):
except Exception as e:
logger.error("Unknown error: %r" % (e, ))
+ return ok, status_code
+
+ def _get_registration_uri(self):
+ """
+ Returns the URI where the register request should be made for
+ the provider
+
+ :rtype: str
+ """
+
+ uri = "https://%s:%s/%s/%s" % (
+ self._provider,
+ self._port,
+ self._provider_config.get_api_version(),
+ self._register_path)
+
+ return uri
+
+
+class SRPRegister(QtCore.QObject):
+ """
+ Registers a user to a specific provider using SRP
+ """
+
+ STATUS_OK = (200, 201)
+ STATUS_TAKEN = 422
+ STATUS_FORBIDDEN = 403
+
+ def __init__(self, signaler=None,
+ provider_config=None, register_path="users"):
+ """
+ Constructor
+
+ :param signaler: Signaler object used to receive notifications
+ from the backend
+ :type signaler: Signaler
+ :param provider_config: provider configuration instance,
+ properly loaded
+ :type privider_config: ProviderConfig
+ :param register_path: webapp path for registering users
+ :type register_path; str
+ """
+ self._srp_register = SRPRegisterImpl(provider_config, register_path)
+ QtCore.QObject.__init__(self)
+
+ self._signaler = signaler
+
+ def register_user(self, username, password):
+ """
+ Registers a user with the validator based on the password provider
+
+ :param username: username to register
+ :type username: str
+ :param password: password for this username
+ :type password: str
+
+ :returns: if the registration went ok or not.
+ :rtype: bool
+ """
+ ok, status_code = self._srp_register.register_user(username, password)
+ self._emit_result(status_code)
return ok
def _emit_result(self, status_code):
@@ -182,6 +205,8 @@ class SRPRegister(QtCore.QObject):
self._signaler.signal(self._signaler.srp_registration_finished)
elif status_code == self.STATUS_TAKEN:
self._signaler.signal(self._signaler.srp_registration_taken)
+ elif status_code == self.STATUS_FORBIDDEN:
+ self._signaler.signal(self._signaler.srp_registration_disabled)
else:
self._signaler.signal(self._signaler.srp_registration_failed)
diff --git a/src/leap/bitmask/crypto/tests/test_srpregister.py b/src/leap/bitmask/crypto/tests/test_srpregister.py
index 4d6e7be3..c019a60c 100644
--- a/src/leap/bitmask/crypto/tests/test_srpregister.py
+++ b/src/leap/bitmask/crypto/tests/test_srpregister.py
@@ -26,9 +26,8 @@ import os
import sys
from mock import MagicMock
-from nose.twistedtools import reactor, deferred
+from nose.twistedtools import reactor
from twisted.python import log
-from twisted.internet import threads
from leap.bitmask.config.providerconfig import ProviderConfig
from leap.bitmask.crypto import srpregister, srpauth
@@ -111,10 +110,10 @@ class SRPTestCase(unittest.TestCase):
raise ImproperlyConfiguredError(
"Could not load test provider config")
- register = srpregister.SRPRegister(provider_config=provider)
- self.assertEquals(register._port, "443")
+ register = srpregister.SRPRegister(provider_config=provider,
+ register_path="users")
+ self.assertEquals(register._srp_register._port, "443")
- @deferred()
def test_wrong_cert(self):
provider = ProviderConfig()
loaded = provider.load(path=os.path.join(
@@ -129,13 +128,11 @@ class SRPTestCase(unittest.TestCase):
raise ImproperlyConfiguredError(
"Could not load test provider config")
- register = srpregister.SRPRegister(provider_config=provider)
- d = threads.deferToThread(register.register_user, "foouser_firsttime",
- "barpass")
- d.addCallback(self.assertFalse)
- return d
+ register = srpregister.SRPRegister(provider_config=provider,
+ register_path="users")
+ ok = register.register_user("foouser_firsttime", "barpass")
+ self.assertFalse(ok)
- @deferred()
def test_register_user(self):
"""
Checks if the registration of an unused name works as expected when
@@ -143,31 +140,17 @@ class SRPTestCase(unittest.TestCase):
when we request a user that is taken.
"""
# pristine registration
- d = threads.deferToThread(self.register.register_user,
- "foouser_firsttime",
- "barpass")
- d.addCallback(self.assertTrue)
- return d
+ ok = self.register.register_user("foouser_firsttime", "barpass")
+ self.assertTrue(ok)
- @deferred()
def test_second_register_user(self):
# second registration attempt with the same user should return errors
- d = threads.deferToThread(self.register.register_user,
- "foouser_second",
- "barpass")
- d.addCallback(self.assertTrue)
-
- # FIXME currently we are catching this in an upper layer,
- # we could bring the error validation to the SRPRegister class
- def register_wrapper(_):
- return threads.deferToThread(self.register.register_user,
- "foouser_second",
- "barpass")
- d.addCallback(register_wrapper)
- d.addCallback(self.assertFalse)
- return d
-
- @deferred()
+ ok = self.register.register_user("foouser_second", "barpass")
+ self.assertTrue(ok)
+
+ ok = self.register.register_user("foouser_second", "barpass")
+ self.assertFalse(ok)
+
def test_correct_http_uri(self):
"""
Checks that registration autocorrect http uris to https ones.
@@ -187,15 +170,14 @@ class SRPTestCase(unittest.TestCase):
raise ImproperlyConfiguredError(
"Could not load test provider config")
- register = srpregister.SRPRegister(provider_config=provider)
+ register = srpregister.SRPRegister(provider_config=provider,
+ register_path="users")
# ... and we check that we're correctly taking the HTTPS protocol
# instead
- reg_uri = register._get_registration_uri()
+ reg_uri = register._srp_register._get_registration_uri()
self.assertEquals(reg_uri, HTTPS_URI)
- register._get_registration_uri = MagicMock(return_value=HTTPS_URI)
- d = threads.deferToThread(register.register_user, "test_failhttp",
- "barpass")
- d.addCallback(self.assertTrue)
-
- return d
+ register._srp_register._get_registration_uri = MagicMock(
+ return_value=HTTPS_URI)
+ ok = register.register_user("test_failhttp", "barpass")
+ self.assertTrue(ok)
diff --git a/src/leap/bitmask/frontend_app.py b/src/leap/bitmask/frontend_app.py
index b0a149f9..fed24cfa 100644
--- a/src/leap/bitmask/frontend_app.py
+++ b/src/leap/bitmask/frontend_app.py
@@ -28,10 +28,10 @@ from PySide import QtCore, QtGui
from leap.bitmask.config import flags
from leap.bitmask.gui.mainwindow import MainWindow
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.util import dict_to_flags
-import logging
-logger = logging.getLogger(__name__)
+logger = get_logger()
def signal_handler(window, pid, signum, frame):
@@ -51,7 +51,13 @@ def signal_handler(window, pid, signum, frame):
if pid == my_pid:
pname = multiprocessing.current_process().name
logger.debug("{0}: SIGNAL #{1} catched.".format(pname, signum))
- window.quit()
+ disable_autostart = True
+ if signum == 15: # SIGTERM
+ # Do not disable autostart on SIGTERM since this is the signal that
+ # the system sends to bitmask when the user asks to do a system
+ # logout.
+ disable_autostart = False
+ window.quit(disable_autostart=disable_autostart)
def run_frontend(options, flags_dict, backend_pid=None):
diff --git a/src/leap/bitmask/gui/advanced_key_management.py b/src/leap/bitmask/gui/advanced_key_management.py
index 7d147b7b..2e315d18 100644
--- a/src/leap/bitmask/gui/advanced_key_management.py
+++ b/src/leap/bitmask/gui/advanced_key_management.py
@@ -17,14 +17,13 @@
"""
Advanced Key Management
"""
-import logging
-
-from PySide import QtCore, QtGui
+from PySide import QtGui
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.services import get_service_display_name, MX_SERVICE
from ui_advanced_key_management import Ui_AdvancedKeyManagement
-logger = logging.getLogger(__name__)
+logger = get_logger()
class AdvancedKeyManagement(QtGui.QDialog):
diff --git a/src/leap/bitmask/gui/app.py b/src/leap/bitmask/gui/app.py
index 5fe031b1..02357b2b 100644
--- a/src/leap/bitmask/gui/app.py
+++ b/src/leap/bitmask/gui/app.py
@@ -18,16 +18,14 @@ A single App instances holds the signals that are shared among different
frontend UI components. The App also keeps a reference to the backend object
and the signaler get signals from the backend.
"""
-import logging
-
-from functools import partial
from PySide import QtCore, QtGui
from leap.bitmask.config.leapsettings import LeapSettings
from leap.bitmask.backend.backend_proxy import BackendProxy
from leap.bitmask.backend.leapsignaler import LeapSignaler
+from leap.bitmask.logs.utils import get_logger
-logger = logging.getLogger(__name__)
+logger = get_logger()
class App(QtGui.QWidget):
diff --git a/src/leap/bitmask/gui/eip_preferenceswindow.py b/src/leap/bitmask/gui/eip_preferenceswindow.py
index 8939c709..756e8adf 100644
--- a/src/leap/bitmask/gui/eip_preferenceswindow.py
+++ b/src/leap/bitmask/gui/eip_preferenceswindow.py
@@ -18,15 +18,14 @@
"""
EIP Preferences window
"""
-import logging
-
from functools import partial
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_eippreferences import Ui_EIPPreferences
-logger = logging.getLogger(__name__)
+logger = get_logger()
class EIPPreferencesWindow(QtGui.QDialog):
diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py
index 83490cac..8334c2ee 100644
--- a/src/leap/bitmask/gui/eip_status.py
+++ b/src/leap/bitmask/gui/eip_status.py
@@ -17,13 +17,12 @@
"""
EIP Status Panel widget implementation
"""
-import logging
-
from datetime import datetime
from functools import partial
from PySide import QtCore, QtGui
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.services import get_service_display_name, EIP_SERVICE
from leap.bitmask.platform_init import IS_LINUX
from leap.bitmask.util.averages import RateMovingAverage
@@ -32,7 +31,7 @@ from leap.common.check import leap_assert_type
from ui_eip_status import Ui_EIPStatus
QtDelayedCall = QtCore.QTimer.singleShot
-logger = logging.getLogger(__name__)
+logger = get_logger()
class EIPStatusWidget(QtGui.QWidget):
diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py
index 90df0b73..756dd63c 100644
--- a/src/leap/bitmask/gui/login.py
+++ b/src/leap/bitmask/gui/login.py
@@ -30,8 +30,7 @@ The login sequence is the following:
- on success: _authentication_finished
"""
-import logging
-
+from keyring.errors import InitError as KeyringInitError
from PySide import QtCore, QtGui
from ui_login import Ui_LoginWidget
@@ -40,6 +39,7 @@ from ui_login import Ui_LoginWidget
from leap.bitmask.backend.leapbackend import ERROR_KEY, PASSED_KEY
from leap.bitmask.config import flags
from leap.bitmask.config.leapsettings import LeapSettings
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.gui.signaltracker import SignalTracker
from leap.bitmask.util import make_address
from leap.bitmask.util.credentials import USERNAME_REGEX
@@ -47,7 +47,7 @@ from leap.bitmask.util.keyring_helpers import has_keyring
from leap.bitmask.util.keyring_helpers import get_keyring
from leap.common.check import leap_assert_type
-logger = logging.getLogger(__name__)
+logger = get_logger()
class LoginState(object):
@@ -365,6 +365,9 @@ class LoginWidget(QtGui.QWidget, SignalTracker):
# Only save the username if it was saved correctly in
# the keyring
self._settings.set_user(full_user_id)
+ except KeyringInitError as e:
+ logger.error("Failed to unlock keyring, maybe the user "
+ "cancelled the operation {0!r}".format(e))
except Exception as e:
logger.exception("Problem saving data to keyring. %r" % (e,))
@@ -653,6 +656,9 @@ class LoginWidget(QtGui.QWidget, SignalTracker):
saved_password = keyring.get_password(self.KEYRING_KEY, u_user)
except ValueError as e:
logger.debug("Incorrect Password. %r." % (e,))
+ except KeyringInitError as e:
+ logger.error("Failed to unlock keyring, maybe the user "
+ "cancelled the operation {0!r}".format(e))
if saved_password is not None:
self.set_password(saved_password)
diff --git a/src/leap/bitmask/gui/loggerwindow.py b/src/leap/bitmask/gui/logwindow.py
index 463d2412..718269c9 100644
--- a/src/leap/bitmask/gui/loggerwindow.py
+++ b/src/leap/bitmask/gui/logwindow.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# loggerwindow.py
+# logwindow.py
# Copyright (C) 2013 LEAP
#
# This program is free software: you can redistribute it and/or modify
@@ -19,18 +19,18 @@
History log window
"""
import cgi
-import logging
from PySide import QtCore, QtGui
+import logbook
+
from ui_loggerwindow import Ui_LoggerWindow
+from leap.bitmask.logs.utils import get_logger, LOG_CONTROLLER
from leap.bitmask.util.constants import PASTEBIN_API_DEV_KEY
-from leap.bitmask.logs.leap_log_handler import LeapLogHandler
from leap.bitmask.util import pastebin
-from leap.common.check import leap_assert, leap_assert_type
-logger = logging.getLogger(__name__)
+logger = get_logger()
class LoggerWindow(QtGui.QDialog):
@@ -40,16 +40,11 @@ class LoggerWindow(QtGui.QDialog):
_paste_ok = QtCore.Signal(object)
_paste_error = QtCore.Signal(object)
- def __init__(self, parent, handler):
+ def __init__(self, parent):
"""
- Initialize the widget with the custom handler.
-
- :param handler: Custom handler that supports history and signal.
- :type handler: LeapLogHandler.
+ Initialize the widget.
"""
QtGui.QDialog.__init__(self, parent)
- leap_assert(handler, "We need a handler for the logger window")
- leap_assert_type(handler, LeapLogHandler)
# Load UI
self.ui = Ui_LoggerWindow()
@@ -72,36 +67,27 @@ class LoggerWindow(QtGui.QDialog):
self._current_filter = ""
self._current_history = ""
- # Load logging history and connect logger with the widget
- self._logging_handler = handler
- self._connect_to_handler()
- self._load_history()
+ self._set_logs_to_display()
- def _connect_to_handler(self):
- """
- This method connects the loggerwindow with the handler through a
- signal communicate the logger events.
- """
- self._logging_handler.new_log.connect(self._add_log_line)
+ LOG_CONTROLLER.new_log.connect(self._add_log_line)
+ self._load_history()
def _add_log_line(self, log):
"""
Adds a line to the history, only if it's in the desired levels to show.
:param log: a log record to be inserted in the widget
- :type log: a dict with RECORD_KEY and MESSAGE_KEY.
- the record contains the LogRecord of the logging module,
- the message contains the formatted message for the log.
+ :type log: Logbook.LogRecord.
"""
html_style = {
- logging.DEBUG: "background: #CDFFFF;",
- logging.INFO: "background: white;",
- logging.WARNING: "background: #FFFF66;",
- logging.ERROR: "background: red; color: white;",
- logging.CRITICAL: "background: red; color: white; font: bold;"
+ logbook.DEBUG: "background: #CDFFFF;",
+ logbook.INFO: "background: white;",
+ logbook.WARNING: "background: #FFFF66;",
+ logbook.ERROR: "background: red; color: white;",
+ logbook.CRITICAL: "background: red; color: white; font: bold;"
}
- level = log[LeapLogHandler.RECORD_KEY].levelno
- message = cgi.escape(log[LeapLogHandler.MESSAGE_KEY])
+ level = log.level
+ message = cgi.escape(log.msg)
if self._logs_to_display[level]:
open_tag = "<tr style='" + html_style[level] + "'>"
@@ -125,12 +111,10 @@ class LoggerWindow(QtGui.QDialog):
"""
self._set_logs_to_display()
self.ui.txtLogHistory.clear()
- history = self._logging_handler.log_history
current_history = []
- for line in history:
- self._add_log_line(line)
- message = line[LeapLogHandler.MESSAGE_KEY]
- current_history.append(message)
+ for record in LOG_CONTROLLER.get_logs():
+ self._add_log_line(record)
+ current_history.append(record.msg)
self._current_history = "\n".join(current_history)
@@ -139,11 +123,11 @@ class LoggerWindow(QtGui.QDialog):
Sets the logs_to_display dict getting the toggled options from the ui
"""
self._logs_to_display = {
- logging.DEBUG: self.ui.btnDebug.isChecked(),
- logging.INFO: self.ui.btnInfo.isChecked(),
- logging.WARNING: self.ui.btnWarning.isChecked(),
- logging.ERROR: self.ui.btnError.isChecked(),
- logging.CRITICAL: self.ui.btnCritical.isChecked()
+ logbook.DEBUG: self.ui.btnDebug.isChecked(),
+ logbook.INFO: self.ui.btnInfo.isChecked(),
+ logbook.WARNING: self.ui.btnWarning.isChecked(),
+ logbook.ERROR: self.ui.btnError.isChecked(),
+ logbook.CRITICAL: self.ui.btnCritical.isChecked()
}
def _filter_by(self, text):
diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py
index bbfbafb5..1a38c8cf 100644
--- a/src/leap/bitmask/gui/mail_status.py
+++ b/src/leap/bitmask/gui/mail_status.py
@@ -17,28 +17,27 @@
"""
Mail Status Panel widget implementation
"""
-import logging
-
from PySide import QtCore, QtGui
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.platform_init import IS_LINUX
from leap.bitmask.services import get_service_display_name, MX_SERVICE
from leap.common.check import leap_assert, leap_assert_type
from leap.common.events import register
-from leap.common.events import events_pb2 as proto
+from leap.common.events import catalog
from ui_mail_status import Ui_MailStatusWidget
-logger = logging.getLogger(__name__)
+logger = get_logger()
class MailStatusWidget(QtGui.QWidget):
"""
Status widget that displays the state of the LEAP Mail service
"""
- _soledad_event = QtCore.Signal(object)
+ _soledad_event = QtCore.Signal(object, object)
_smtp_event = QtCore.Signal(object)
- _imap_event = QtCore.Signal(object)
+ _imap_event = QtCore.Signal(object, object)
_keymanager_event = QtCore.Signal(object)
def __init__(self, parent=None):
@@ -70,51 +69,36 @@ class MailStatusWidget(QtGui.QWidget):
self.ERROR_ICON_TRAY = None
self._set_mail_icons()
- register(signal=proto.KEYMANAGER_LOOKING_FOR_KEY,
- callback=self._mail_handle_keymanager_events,
- reqcbk=lambda req, resp: None)
-
- register(signal=proto.KEYMANAGER_KEY_FOUND,
- callback=self._mail_handle_keymanager_events,
- reqcbk=lambda req, resp: None)
-
- # register(signal=proto.KEYMANAGER_KEY_NOT_FOUND,
- # callback=self._mail_handle_keymanager_events,
- # reqcbk=lambda req, resp: None)
-
- register(signal=proto.KEYMANAGER_STARTED_KEY_GENERATION,
- callback=self._mail_handle_keymanager_events,
- reqcbk=lambda req, resp: None)
-
- register(signal=proto.KEYMANAGER_FINISHED_KEY_GENERATION,
- callback=self._mail_handle_keymanager_events,
- reqcbk=lambda req, resp: None)
-
- register(signal=proto.KEYMANAGER_DONE_UPLOADING_KEYS,
- callback=self._mail_handle_keymanager_events,
- reqcbk=lambda req, resp: None)
-
- register(signal=proto.SOLEDAD_DONE_DOWNLOADING_KEYS,
- callback=self._mail_handle_soledad_events,
- reqcbk=lambda req, resp: None)
-
- register(signal=proto.SOLEDAD_DONE_UPLOADING_KEYS,
- callback=self._mail_handle_soledad_events,
- reqcbk=lambda req, resp: None)
-
- register(signal=proto.IMAP_UNREAD_MAIL,
- callback=self._mail_handle_imap_events,
- reqcbk=lambda req, resp: None)
- register(signal=proto.IMAP_SERVICE_STARTED,
- callback=self._mail_handle_imap_events,
- reqcbk=lambda req, resp: None)
- register(signal=proto.SMTP_SERVICE_STARTED,
- callback=self._mail_handle_imap_events,
- reqcbk=lambda req, resp: None)
-
- register(signal=proto.SOLEDAD_INVALID_AUTH_TOKEN,
- callback=self.set_soledad_invalid_auth_token,
- reqcbk=lambda req, resp: None)
+ register(event=catalog.KEYMANAGER_LOOKING_FOR_KEY,
+ callback=self._mail_handle_keymanager_events)
+ register(event=catalog.KEYMANAGER_KEY_FOUND,
+ callback=self._mail_handle_keymanager_events)
+ register(event=catalog.KEYMANAGER_KEY_NOT_FOUND,
+ callback=self._mail_handle_keymanager_events)
+ register(event=catalog.KEYMANAGER_STARTED_KEY_GENERATION,
+ callback=self._mail_handle_keymanager_events)
+ register(event=catalog.KEYMANAGER_FINISHED_KEY_GENERATION,
+ callback=self._mail_handle_keymanager_events)
+ register(event=catalog.KEYMANAGER_DONE_UPLOADING_KEYS,
+ callback=self._mail_handle_keymanager_events)
+
+ register(event=catalog.SOLEDAD_DONE_DOWNLOADING_KEYS,
+ callback=self._mail_handle_soledad_events)
+ register(event=catalog.SOLEDAD_DONE_UPLOADING_KEYS,
+ callback=self._mail_handle_soledad_events)
+ register(event=catalog.SOLEDAD_SYNC_RECEIVE_STATUS,
+ callback=self._mail_handle_soledad_events)
+ register(event=catalog.SOLEDAD_SYNC_SEND_STATUS,
+ callback=self._mail_handle_soledad_events)
+ register(event=catalog.SOLEDAD_INVALID_AUTH_TOKEN,
+ callback=self.set_soledad_invalid_auth_token)
+
+ register(event=catalog.MAIL_UNREAD_MESSAGES,
+ callback=self._mail_handle_imap_events)
+ register(event=catalog.IMAP_SERVICE_STARTED,
+ callback=self._mail_handle_imap_events)
+ register(event=catalog.SMTP_SERVICE_STARTED,
+ callback=self._mail_handle_imap_events)
self._soledad_event.connect(
self._mail_handle_soledad_events_slot)
@@ -194,12 +178,14 @@ 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):
+ def set_soledad_invalid_auth_token(self, event, content):
"""
- TRIGGERS:
- SoledadBootstrapper.soledad_invalid_token
-
This method is called when the auth token is invalid
+
+ :param event: The event that triggered the callback.
+ :type event: str
+ :param content: The content of the event.
+ :type content: list
"""
msg = self.tr("Invalid auth token, try logging in again.")
self._set_mail_status(msg, ready=-1)
@@ -239,58 +225,85 @@ class MailStatusWidget(QtGui.QWidget):
self._action_mail_status.setText(tray_status)
self._update_systray_tooltip()
- def _mail_handle_soledad_events(self, req):
+ def _mail_handle_soledad_events(self, event, content):
"""
Callback for handling events that are emitted from Soledad
- :param req: Request type
- :type req: leap.common.events.events_pb2.SignalRequest
+ :param event: The event that triggered the callback.
+ :type event: str
+ :param content: The content of the event.
+ :type content: dict
"""
- self._soledad_event.emit(req)
+ self._soledad_event.emit(event, content)
- def _mail_handle_soledad_events_slot(self, req):
+ def _mail_handle_soledad_events_slot(self, event, content):
"""
TRIGGERS:
_mail_handle_soledad_events
Reacts to an Soledad event
- :param req: Request type
- :type req: leap.common.events.events_pb2.SignalRequest
+ :param event: The event that triggered the callback.
+ :type event: str
+ :param content: The content of the event.
+ :type content: dict
"""
self._set_mail_status(self.tr("Starting..."), ready=1)
ext_status = ""
+ ready = None
- if req.event == proto.SOLEDAD_DONE_UPLOADING_KEYS:
+ if event == catalog.SOLEDAD_DONE_UPLOADING_KEYS:
ext_status = self.tr("Soledad has started...")
- elif req.event == proto.SOLEDAD_DONE_DOWNLOADING_KEYS:
+ ready = 1
+ elif event == catalog.SOLEDAD_DONE_DOWNLOADING_KEYS:
ext_status = self.tr("Soledad is starting, please wait...")
+ ready = 1
+ elif event == catalog.SOLEDAD_SYNC_RECEIVE_STATUS:
+ sync_progress = content['received'] * 100 / content['total']
+ if sync_progress < 100:
+ ext_status = self.tr("Sync: downloading ({0:02}%)")
+ ext_status = ext_status.format(sync_progress)
+ else:
+ ext_status = self.tr("Sync: download completed.")
+
+ ready = 2
+ elif event == catalog.SOLEDAD_SYNC_SEND_STATUS:
+ sync_progress = content['sent'] * 100 / content['total']
+ if sync_progress < 100:
+ ext_status = self.tr("Sync: uploading ({0:02}%)")
+ ext_status = ext_status.format(sync_progress)
+ else:
+ ext_status = self.tr("Sync: upload complete.")
+
+ ready = 2
else:
leap_assert(False,
"Don't know how to handle this state: %s"
- % (req.event))
+ % (event))
- self._set_mail_status(ext_status, ready=1)
+ self._set_mail_status(ext_status, ready=ready)
- def _mail_handle_keymanager_events(self, req):
+ def _mail_handle_keymanager_events(self, event, content):
"""
Callback for the KeyManager events
- :param req: Request type
- :type req: leap.common.events.events_pb2.SignalRequest
+ :param event: The event that triggered the callback.
+ :type event: str
+ :param content: The content of the event.
+ :type content: list
"""
- self._keymanager_event.emit(req)
+ self._keymanager_event.emit(event)
- def _mail_handle_keymanager_events_slot(self, req):
+ def _mail_handle_keymanager_events_slot(self, event):
"""
TRIGGERS:
_mail_handle_keymanager_events
Reacts to an KeyManager event
- :param req: Request type
- :type req: leap.common.events.events_pb2.SignalRequest
+ :param event: The event that triggered the callback.
+ :type event: str
"""
# We want to ignore this kind of events once everything has
# started
@@ -299,88 +312,90 @@ class MailStatusWidget(QtGui.QWidget):
ext_status = ""
- if req.event == proto.KEYMANAGER_LOOKING_FOR_KEY:
+ if event == catalog.KEYMANAGER_LOOKING_FOR_KEY:
ext_status = self.tr("Initial sync in progress, please wait...")
- elif req.event == proto.KEYMANAGER_KEY_FOUND:
+ elif event == catalog.KEYMANAGER_KEY_FOUND:
ext_status = self.tr("Found key! Starting mail...")
- # elif req.event == proto.KEYMANAGER_KEY_NOT_FOUND:
- # ext_status = self.tr("Key not found!")
- elif req.event == proto.KEYMANAGER_STARTED_KEY_GENERATION:
+ elif event == catalog.KEYMANAGER_KEY_NOT_FOUND:
+ ext_status = self.tr(
+ "Key not found...")
+ elif event == catalog.KEYMANAGER_STARTED_KEY_GENERATION:
ext_status = self.tr(
"Generating new key, this may take a few minutes.")
- elif req.event == proto.KEYMANAGER_FINISHED_KEY_GENERATION:
+ elif event == catalog.KEYMANAGER_FINISHED_KEY_GENERATION:
ext_status = self.tr("Finished generating key!")
- elif req.event == proto.KEYMANAGER_DONE_UPLOADING_KEYS:
+ elif event == catalog.KEYMANAGER_DONE_UPLOADING_KEYS:
ext_status = self.tr("Starting mail...")
else:
- leap_assert(False,
- "Don't know how to handle this state: %s"
- % (req.event))
-
+ logger.warning("don't know to to handle %s" % (event,))
self._set_mail_status(ext_status, ready=1)
- def _mail_handle_smtp_events(self, req):
+ def _mail_handle_smtp_events(self, event):
"""
Callback for the SMTP events
- :param req: Request type
- :type req: leap.common.events.events_pb2.SignalRequest
+ :param event: The event that triggered the callback.
+ :type event: str
"""
- self._smtp_event.emit(req)
+ self._smtp_event.emit(event)
- def _mail_handle_smtp_events_slot(self, req):
+ def _mail_handle_smtp_events_slot(self, event):
"""
TRIGGERS:
_mail_handle_smtp_events
Reacts to an SMTP event
- :param req: Request type
- :type req: leap.common.events.events_pb2.SignalRequest
+ :param event: The event that triggered the callback.
+ :type event: str
"""
ext_status = ""
- if req.event == proto.SMTP_SERVICE_STARTED:
+ if event == catalog.SMTP_SERVICE_STARTED:
self._smtp_started = True
- elif req.event == proto.SMTP_SERVICE_FAILED_TO_START:
+ elif event == catalog.SMTP_SERVICE_FAILED_TO_START:
ext_status = self.tr("SMTP failed to start, check the logs.")
else:
leap_assert(False,
"Don't know how to handle this state: %s"
- % (req.event))
+ % (event))
self._set_mail_status(ext_status, ready=2)
# ----- XXX deprecate (move to mail conductor)
- def _mail_handle_imap_events(self, req):
+ def _mail_handle_imap_events(self, event, content):
"""
Callback for the IMAP events
- :param req: Request type
- :type req: leap.common.events.events_pb2.SignalRequest
+ :param event: The event that triggered the callback.
+ :type event: str
+ :param content: The content of the event.
+ :type content: list
"""
- self._imap_event.emit(req)
+ self._imap_event.emit(event, content)
- def _mail_handle_imap_events_slot(self, req):
+ def _mail_handle_imap_events_slot(self, event, content):
"""
TRIGGERS:
_mail_handle_imap_events
Reacts to an IMAP event
- :param req: Request type
- :type req: leap.common.events.events_pb2.SignalRequest
+ :param event: The event that triggered the callback.
+ :type event: str
+ :param content: The content of the event.
+ :type content: list
"""
ext_status = None
- if req.event == proto.IMAP_UNREAD_MAIL:
+ if event == catalog.MAIL_UNREAD_MESSAGES:
# By now, the semantics of the UNREAD_MAIL event are
# limited to mails with the Unread flag *in the Inbox".
# We could make this configurable to include all unread mail
# or all unread mail in subscribed folders.
if self._started:
- count = req.content
+ count = content
if count != "0":
status = self.tr("{0} Unread Emails "
"in your Inbox").format(count)
@@ -390,7 +405,7 @@ class MailStatusWidget(QtGui.QWidget):
self._set_mail_status(status, ready=2)
else:
self._set_mail_status("", ready=2)
- elif req.event == proto.IMAP_SERVICE_STARTED:
+ elif event == catalog.IMAP_SERVICE_STARTED:
self._imap_started = True
if ext_status is not None:
self._set_mail_status(ext_status, ready=1)
diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py
index cbf7a636..312048ba 100644
--- a/src/leap/bitmask/gui/mainwindow.py
+++ b/src/leap/bitmask/gui/mainwindow.py
@@ -17,7 +17,6 @@
"""
Main window for Bitmask.
"""
-import logging
import time
from datetime import datetime
@@ -34,12 +33,11 @@ from leap.bitmask import __version_hash__ as VERSION_HASH
from leap.bitmask.backend.leapbackend import ERROR_KEY, PASSED_KEY
from leap.bitmask.config import flags
+from leap.bitmask.logs.utils import get_logger, LOG_CONTROLLER
from leap.bitmask.gui.advanced_key_management import AdvancedKeyManagement
-from leap.bitmask.gui.eip_status import EIPStatusWidget
-from leap.bitmask.gui.loggerwindow import LoggerWindow
+from leap.bitmask.gui.logwindow import LoggerWindow
from leap.bitmask.gui.login import LoginWidget
-from leap.bitmask.gui.mail_status import MailStatusWidget
from leap.bitmask.gui.preferenceswindow import PreferencesWindow
from leap.bitmask.gui.signaltracker import SignalTracker
from leap.bitmask.gui.systray import SysTray
@@ -53,24 +51,34 @@ from leap.bitmask.platform_init import locks
from leap.bitmask.platform_init.initializers import init_platform
from leap.bitmask.platform_init.initializers import init_signals
-from leap.bitmask.services.eip import conductor as eip_conductor
-from leap.bitmask.services.mail import conductor as mail_conductor
-
-from leap.bitmask.services import EIP_SERVICE, MX_SERVICE
-
from leap.bitmask.util import autostart, make_address
from leap.bitmask.util.keyring_helpers import has_keyring
-from leap.bitmask.logs.leap_log_handler import LeapLogHandler
from leap.common.events import register
-from leap.common.events import events_pb2 as proto
+from leap.common.events import catalog
from leap.mail.imap.service.imap import IMAP_PORT
from ui_mainwindow import Ui_MainWindow
+from leap.bitmask._components import HAS_EIP, HAS_MAIL
+
+if HAS_EIP:
+ from leap.bitmask.gui.eip_status import EIPStatusWidget
+ from leap.bitmask.services.eip import conductor as eip_conductor
+ from leap.bitmask.services import EIP_SERVICE
+
+if HAS_MAIL:
+ from leap.bitmask.gui.mail_status import MailStatusWidget
+ from leap.bitmask.services.mail import conductor as mail_conductor
+ from leap.bitmask.services import MX_SERVICE
+
QtDelayedCall = QtCore.QTimer.singleShot
-logger = logging.getLogger(__name__)
+
+logger = get_logger()
+
+if not HAS_EIP:
+ BITMASK_MAIL_ONLY_ICON = ":/images/menubar-mask-icon.png"
class MainWindow(QtGui.QMainWindow, SignalTracker):
@@ -78,17 +86,19 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
Main window for login and presenting status updates to the user
"""
# Signals
- eip_needs_login = QtCore.Signal([])
+
new_updates = QtCore.Signal(object)
raise_window = QtCore.Signal([])
soledad_ready = QtCore.Signal([])
all_services_stopped = QtCore.Signal()
- # We use this flag to detect abnormal terminations
- user_stopped_eip = False
+ if HAS_EIP:
+ eip_needs_login = QtCore.Signal([])
+ # We use this flag to detect abnormal terminations
+ user_stopped_eip = False
- # We give EIP some time to come up before starting soledad anyway
- EIP_START_TIMEOUT = 60000 # in milliseconds
+ # We give EIP some time to come up before starting soledad anyway
+ EIP_START_TIMEOUT = 60000 # in milliseconds
# We give the services some time to a halt before forcing quit.
SERVICES_STOP_TIMEOUT = 3000 # in milliseconds
@@ -107,12 +117,10 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
autostart.set_autostart(True)
# register leap events ########################################
- register(signal=proto.UPDATER_NEW_UPDATES,
- callback=self._new_updates_available,
- reqcbk=lambda req, resp: None) # make rpc call async
- register(signal=proto.RAISE_WINDOW,
- callback=self._on_raise_window_event,
- reqcbk=lambda req, resp: None) # make rpc call async
+ register(event=catalog.UPDATER_NEW_UPDATES,
+ callback=self._new_updates_available) # make rpc call async
+ register(event=catalog.RAISE_WINDOW,
+ callback=self._on_raise_window_event) # make rpc call async
# end register leap events ####################################
self._updates_content = ""
@@ -126,15 +134,17 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
self._backend = self.app.backend
self._leap_signaler = self.app.signaler
self._settings = self.app.settings
+ self._backend_settings = self._backend.settings
# Login Widget
self._login_widget = LoginWidget(self._backend,
self._leap_signaler, self)
self.ui.loginLayout.addWidget(self._login_widget)
- # Mail Widget
- self._mail_status = MailStatusWidget(self)
- self.ui.mailLayout.addWidget(self._mail_status)
+ if HAS_MAIL:
+ # Mail Widget
+ self._mail_status = MailStatusWidget(self)
+ self.ui.mailLayout.addWidget(self._mail_status)
# Provider List
self._providers = Providers(self.ui.cmbProviders)
@@ -151,41 +161,43 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
self._providers.connect_provider_changed(self._on_provider_changed)
- # EIP Control redux #########################################
- self._eip_conductor = eip_conductor.EIPConductor(
- self._settings, self._backend, self._leap_signaler)
- self._eip_status = EIPStatusWidget(self, self._eip_conductor,
- self._leap_signaler)
-
- init_signals.eip_missing_helpers.connect(
- self._disable_eip_missing_helpers)
-
- self.ui.eipLayout.addWidget(self._eip_status)
-
- # XXX we should get rid of the circular refs
- # conductor <-> status, right now keeping state on the widget ifself.
- self._eip_conductor.add_eip_widget(self._eip_status)
-
- self._eip_conductor.connect_signals()
- self._eip_conductor.qtsigs.connecting_signal.connect(
- self._on_eip_connecting)
- self._eip_conductor.qtsigs.connected_signal.connect(
- self._on_eip_connection_connected)
- self._eip_conductor.qtsigs.disconnected_signal.connect(
- self._on_eip_connection_disconnected)
- self._eip_conductor.qtsigs.connected_signal.connect(
- self._maybe_run_soledad_setup_checks)
+ if HAS_EIP:
+ # EIP Control redux #########################################
+ self._eip_conductor = eip_conductor.EIPConductor(
+ self._settings, self._backend, self._leap_signaler)
+ self._eip_status = EIPStatusWidget(self, self._eip_conductor,
+ self._leap_signaler)
+
+ init_signals.eip_missing_helpers.connect(
+ self._disable_eip_missing_helpers)
+
+ self.ui.eipLayout.addWidget(self._eip_status)
+
+ # XXX we should get rid of the circular refs
+ # conductor <-> status,
+ # right now keeping state on the widget ifself.
+ self._eip_conductor.add_eip_widget(self._eip_status)
+
+ self._eip_conductor.connect_signals()
+ self._eip_conductor.qtsigs.connecting_signal.connect(
+ self._on_eip_connecting)
+ self._eip_conductor.qtsigs.connected_signal.connect(
+ self._on_eip_connection_connected)
+ self._eip_conductor.qtsigs.disconnected_signal.connect(
+ self._on_eip_connection_disconnected)
+ self._eip_conductor.qtsigs.connected_signal.connect(
+ self._maybe_run_soledad_setup_checks)
+
+ self.eip_needs_login.connect(self._eip_status.disable_eip_start)
+ self.eip_needs_login.connect(self._disable_eip_start_action)
+
+ # XXX all this info about state should move to eip conductor too
+ self._already_started_eip = False
+ self._trying_to_start_eip = False
self._login_widget.login_offline_finished.connect(
self._maybe_run_soledad_setup_checks)
- self.eip_needs_login.connect(self._eip_status.disable_eip_start)
- self.eip_needs_login.connect(self._disable_eip_start_action)
-
- # XXX all this info about state should move to eip conductor too
- self._already_started_eip = False
- self._trying_to_start_eip = False
-
self._soledad_started = False
# This is created once we have a valid provider config
@@ -210,15 +222,14 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
self.ui.action_wizard.triggered.connect(self._show_wizard)
self.ui.action_show_logs.triggered.connect(self._show_logger_window)
- # XXX hide the help menu since it only shows email information and
- # right now we don't have stable mail and just confuses users.
- self.ui.action_help.setVisible(False)
- # self.ui.action_help.triggered.connect(self._help)
+ self.ui.action_help.setVisible(True)
+ self.ui.action_help.triggered.connect(self._help)
self.ui.action_create_new_account.triggered.connect(
self._on_provider_changed)
# Action item hidden since we don't provide stable mail yet.
+ # TODO enable for 0.9.0 release??
# self.ui.action_advanced_key_management.triggered.connect(
# self._show_AKM)
@@ -230,11 +241,16 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
self._systray = None
# XXX separate actions into a different module.
- self._action_mail_status = QtGui.QAction(self.tr("Mail is OFF"), self)
- self._mail_status.set_action_mail_status(self._action_mail_status)
+ if HAS_MAIL:
+ self._action_mail_status = QtGui.QAction(
+ self.tr("Mail is OFF"), self)
+ self._mail_status.set_action_mail_status(
+ self._action_mail_status)
- self._action_eip_startstop = QtGui.QAction("", self)
- self._eip_status.set_action_eip_startstop(self._action_eip_startstop)
+ if HAS_EIP:
+ self._action_eip_startstop = QtGui.QAction("", self)
+ self._eip_status.set_action_eip_startstop(
+ self._action_eip_startstop)
self._action_visible = QtGui.QAction(self.tr("Show Main Window"), self)
self._action_visible.triggered.connect(self._ensure_visible)
@@ -272,19 +288,21 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
self._start_hidden = start_hidden
self._backend_pid = backend_pid
- self._mail_conductor = mail_conductor.MailConductor(self._backend)
- self._mail_conductor.connect_mail_signals(self._mail_status)
+ if HAS_MAIL:
+ self._mail_conductor = mail_conductor.MailConductor(self._backend)
+ self._mail_conductor.connect_mail_signals(self._mail_status)
if not init_platform():
self.quit()
return
# start event machines from within the eip and mail conductors
-
# TODO should encapsulate all actions into one object
- self._eip_conductor.start_eip_machine(
- action=self._action_eip_startstop)
- self._mail_conductor.start_mail_machine()
+ if HAS_EIP:
+ self._eip_conductor.start_eip_machine(
+ action=self._action_eip_startstop)
+ if HAS_MAIL:
+ self._mail_conductor.start_mail_machine()
if self._first_run():
self._wizard_firstrun = True
@@ -363,17 +381,17 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
# here.
sig.srp_not_logged_in_error.connect(self._not_logged_in_error)
- # EIP start signals ==============================================
- self._eip_conductor.connect_backend_signals()
- sig.eip_can_start.connect(self._backend_can_start_eip)
- sig.eip_cannot_start.connect(self._backend_cannot_start_eip)
+ if HAS_EIP:
+ # EIP start signals ==============================================
+ self._eip_conductor.connect_backend_signals()
+ sig.eip_can_start.connect(self._backend_can_start_eip)
+ sig.eip_cannot_start.connect(self._backend_cannot_start_eip)
- sig.eip_dns_error.connect(self._eip_dns_error)
+ sig.eip_dns_error.connect(self._eip_dns_error)
- sig.eip_get_gateway_country_code.connect(self._set_eip_provider)
- sig.eip_no_gateway.connect(self._set_eip_provider)
-
- # ==================================================================
+ sig.eip_get_gateway_country_code.connect(self._set_eip_provider)
+ sig.eip_no_gateway.connect(self._set_eip_provider)
+ # ==================================================================
# Soledad signals
# TODO delegate connection to soledad bootstrapper
@@ -491,25 +509,11 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
self._login_widget.set_password(possible_password)
self._login()
else:
- self.eip_needs_login.emit()
+ if HAS_EIP:
+ self.eip_needs_login.emit()
self._wizard = None
- def _get_leap_logging_handler(self):
- """
- Gets the leap handler from the top level logger
-
- :return: a logging handler or None
- :rtype: LeapLogHandler or None
- """
- # TODO this can be a function, does not need
- # to be a method.
- leap_logger = logging.getLogger('leap')
- for h in leap_logger.handlers:
- if isinstance(h, LeapLogHandler):
- return h
- return None
-
def _show_logger_window(self):
"""
TRIGGERS:
@@ -518,13 +522,8 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
Display the window with the history of messages logged until now
and displays the new ones on arrival.
"""
- leap_log_handler = self._get_leap_logging_handler()
- if leap_log_handler is None:
- logger.error('Leap logger handler not found')
- return
- else:
- lw = LoggerWindow(self, handler=leap_log_handler)
- lw.show()
+ lw = LoggerWindow(self)
+ lw.show()
def _show_AKM(self):
"""
@@ -589,22 +588,26 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
self._backend_cannot_start_eip()
return
- if EIP_SERVICE not in self.app.settings.get_enabled_services(domain):
- self._eip_conductor.terminate()
+ services_enabled = self.app.settings.get_enabled_services(domain)
- def hide():
- self.app.backend.eip_can_start(domain=domain)
+ if HAS_EIP:
+ if EIP_SERVICE not in services_enabled:
+ self._eip_conductor.terminate()
- QtDelayedCall(100, hide)
- # ^^ VERY VERY Hacky, but with the simple state machine,
- # there is no way to signal 'disconnect and then disable'
+ def hide():
+ self.app.backend.eip_can_start(domain=domain)
- else:
- self._trying_to_start_eip = self.app.settings.get_autostart_eip()
- if not self._trying_to_start_eip:
- self._backend.eip_setup(provider=domain, skip_network=True)
- # check if EIP can start (will trigger widget update)
- self.app.backend.eip_can_start(domain=domain)
+ QtDelayedCall(100, hide)
+ # ^^ VERY VERY Hacky, but with the simple state machine,
+ # there is no way to signal 'disconnect and then disable'
+
+ else:
+ settings = self.app.settings
+ self._trying_to_start_eip = settings.get_autostart_eip()
+ if not self._trying_to_start_eip:
+ self._backend.eip_setup(provider=domain, skip_network=True)
+ # check if EIP can start (will trigger widget update)
+ self.app.backend.eip_can_start(domain=domain)
def _backend_can_start_eip(self):
"""
@@ -656,15 +659,16 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
if default_provider is not None:
enabled_services = settings.get_enabled_services(default_provider)
- if EIP_SERVICE in enabled_services:
- # we don't have a usable provider
- # so the user needs to log in first
- self._eip_status.disable_eip_start()
- else:
- self._eip_status.disable_eip_start()
- # NOTE: we shouldn't be setting the message here.
- if not self._eip_status.missing_helpers:
- self._eip_status.set_eip_status(self.tr("Disabled"))
+ if HAS_EIP:
+ if EIP_SERVICE in enabled_services:
+ # we don't have a usable provider
+ # so the user needs to log in first
+ self._eip_status.disable_eip_start()
+ else:
+ self._eip_status.disable_eip_start()
+ # NOTE: we shouldn't be setting the message here.
+ if not self._eip_status.missing_helpers:
+ self._eip_status.set_eip_status(self.tr("Disabled"))
# this state flag is responsible for deferring the login
# so we must update it, otherwise we're in a deadlock.
@@ -683,29 +687,31 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
# updates
#
- def _new_updates_available(self, req):
+ def _new_updates_available(self, event, content):
"""
Callback for the new updates event
- :param req: Request type
- :type req: leap.common.events.events_pb2.SignalRequest
+ :param event: The event that triggered the callback.
+ :type event: str
+ :param content: The content of the event.
+ :type content: list
"""
- self.new_updates.emit(req)
+ self.new_updates.emit(content)
- def _react_to_new_updates(self, req):
+ def _react_to_new_updates(self, content):
"""
TRIGGERS:
self.new_updates
Display the new updates label and sets the updates_content
- :param req: Request type
- :type req: leap.common.events.events_pb2.SignalRequest
+ :param content: The content of the event.
+ :type content: list
"""
self.moveToThread(QtCore.QCoreApplication.instance().thread())
self.ui.lblNewUpdates.setVisible(True)
self.ui.btnMore.setVisible(True)
- self._updates_content = req.content
+ self._updates_content = content
def _updates_details(self):
"""
@@ -761,6 +767,8 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
self._show_hide_unsupported_services()
+ LOG_CONTROLLER.start_logbook_subscriber()
+
# XXX - HACK, kind of...
# With the 1ms QTimer.singleShot call we schedule the call right after
# other signals waiting for the qt reactor to take control.
@@ -798,12 +806,13 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
only, the mail widget won't be displayed.
"""
providers = self._settings.get_configured_providers()
-
self._backend.provider_get_all_services(providers=providers)
def _provider_get_all_services(self, services):
- self._set_eip_visible(EIP_SERVICE in services)
- self._set_mx_visible(MX_SERVICE in services)
+ if HAS_EIP:
+ self._set_eip_visible(EIP_SERVICE in services)
+ if HAS_MAIL:
+ self._set_mx_visible(MX_SERVICE in services)
def _set_mx_visible(self, visible):
"""
@@ -856,23 +865,34 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
systrayMenu.addAction(self._action_visible)
systrayMenu.addSeparator()
- eip_status_label = u"{0}: {1}".format(
- self._eip_conductor.eip_name, self.tr("OFF"))
- self._eip_menu = eip_menu = systrayMenu.addMenu(eip_status_label)
- eip_menu.addAction(self._action_eip_startstop)
- self._eip_status.set_eip_status_menu(eip_menu)
- systrayMenu.addSeparator()
- systrayMenu.addAction(self._action_mail_status)
- systrayMenu.addSeparator()
+ if HAS_EIP:
+ eip_status_label = u"{0}: {1}".format(
+ self._eip_conductor.eip_name, self.tr("OFF"))
+ self._eip_menu = eip_menu = systrayMenu.addMenu(eip_status_label)
+ eip_menu.addAction(self._action_eip_startstop)
+ self._eip_status.set_eip_status_menu(eip_menu)
+ systrayMenu.addSeparator()
+ if HAS_MAIL:
+ systrayMenu.addAction(self._action_mail_status)
+ systrayMenu.addSeparator()
systrayMenu.addAction(self.ui.action_quit)
+
self._systray = SysTray(self)
self._systray.setContextMenu(systrayMenu)
- self._systray.setIcon(self._eip_status.ERROR_ICON_TRAY)
+
+ if HAS_EIP:
+ self._systray.setIcon(self._eip_status.ERROR_ICON_TRAY)
+ else:
+ mail_status_icon = QtGui.QPixmap(BITMASK_MAIL_ONLY_ICON)
+ self._systray.setIcon(mail_status_icon)
+
self._systray.setVisible(True)
self._systray.activated.connect(self._tray_activated)
- self._mail_status.set_systray(self._systray)
- self._eip_status.set_systray(self._systray)
+ if HAS_EIP:
+ self._eip_status.set_systray(self._systray)
+ if HAS_MAIL:
+ self._mail_status.set_systray(self._systray)
if self._start_hidden:
hello = lambda: self._systray.showMessage(
@@ -1004,7 +1024,7 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
"manager or download it from <a href='{0}'>"
"addons.mozilla.org</a>.".format(thunderbird_extension_url))
manual_text = self.tr(
- "Alternately, you can manually configure "
+ "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))
@@ -1055,7 +1075,7 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
if not e.spontaneous():
# if the system requested the `close` then we should quit.
self._system_quit = True
- self.quit()
+ self.quit(disable_autostart=False)
return
if QtGui.QSystemTrayIcon.isSystemTrayAvailable() and \
@@ -1112,8 +1132,11 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
# TODO: we should handle the case that EIP is autostarting since we
# won't get a warning until EIP has fully started.
# TODO: we need to add a check for the mail status (smtp/imap/soledad)
- something_runing = (self._login_widget.get_logged_user() is not None or
- self._already_started_eip)
+
+ something_runing = self._login_widget.get_logged_user() is not None
+ if HAS_EIP:
+ something_runing = something_runing or self._already_started_eip
+
provider = self._providers.get_selected_provider()
self._login_widget.set_provider(provider)
@@ -1192,22 +1215,34 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
self._disconnect_login_wait()
user = self._login_widget.get_logged_user()
- domain = self._providers.get_selected_provider()
- full_user_id = make_address(user, domain)
- self._mail_conductor.userid = full_user_id
- self._start_eip_bootstrap()
- self.ui.action_create_new_account.setEnabled(True)
+ # XXX the widget now gives us the full user id.
+ # this is confusing.
- # if soledad/mail is enabled:
- if MX_SERVICE in self._enabled_services:
- btn_enabled = self._login_widget.set_logout_btn_enabled
- btn_enabled(False)
- sig = self._leap_signaler
- sig.soledad_bootstrap_failed.connect(lambda: btn_enabled(True))
- sig.soledad_bootstrap_finished.connect(lambda: btn_enabled(True))
+ self.ui.action_create_new_account.setEnabled(True)
- if MX_SERVICE not in self._provider_details['services']:
- self._set_mx_visible(False)
+ if HAS_EIP:
+ self._start_eip_bootstrap()
+ if HAS_MAIL:
+ # XXX the casting to str (needed by smtp gateway) should be done
+ # in a better place.
+ self._mail_conductor.userid = str(user)
+ if MX_SERVICE in self._enabled_services:
+ btn_enabled = self._login_widget.set_logout_btn_enabled
+ btn_enabled(False)
+ sig = self._leap_signaler
+ sig.soledad_bootstrap_failed.connect(
+ lambda: btn_enabled(True))
+ sig.soledad_bootstrap_finished.connect(
+ lambda: btn_enabled(True))
+
+ if MX_SERVICE not in self._provider_details['services']:
+ self._set_mx_visible(False)
+
+ if not HAS_EIP:
+ # This has to be worked out in Bitmask 0.10.
+ # Since EIP won't start, we need to trigger
+ # the soledad setup service from here.
+ self._maybe_run_soledad_setup_checks()
def _on_user_logged_out(self):
"""
@@ -1217,8 +1252,9 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
Switch the stackedWidget back to the login stage after
logging out
"""
- self._mail_conductor.stop_mail_services()
- self._mail_status.mail_state_disabled()
+ if HAS_MAIL:
+ self._mail_conductor.stop_mail_services()
+ self._mail_status.mail_state_disabled()
self._show_hide_unsupported_services()
def _start_eip_bootstrap(self):
@@ -1312,10 +1348,10 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
if flags.OFFLINE:
full_user_id = make_address(username, provider_domain)
- uuid = self._settings.get_uuid(full_user_id)
+ uuid = self._backend_settings.get_uuid(full_user_id)
self._mail_conductor.userid = full_user_id
- if uuid is None:
+ if not uuid:
# We don't need more visibility at the moment,
# this is mostly for internal use/debug for now.
logger.warning("Sorry! Log-in at least one time.")
@@ -1479,9 +1515,9 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
missing_helpers = self._eip_status.missing_helpers
already_started = self._already_started_eip
- can_start = (should_start
- and not already_started
- and not missing_helpers)
+ can_start = (should_start and
+ not already_started and
+ not missing_helpers)
if can_start:
if self._eip_status.is_cold_start:
@@ -1518,7 +1554,9 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
msg = self.tr("Disabled")
self._eip_status.disable_eip_start()
self._eip_status.set_eip_status(msg)
+
# eip will not start, so we start soledad anyway
+ # XXX This is the entry point for soledad startup.
self._maybe_run_soledad_setup_checks()
def _finish_eip_bootstrap(self, data):
@@ -1566,9 +1604,14 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
# window handling methods
#
- def _on_raise_window_event(self, req):
+ def _on_raise_window_event(self, event, content):
"""
Callback for the raise window event
+
+ :param event: The event that triggered the callback.
+ :type event: str
+ :param content: The content of the event.
+ :type content: list
"""
if IS_WIN:
locks.raise_window_ack()
@@ -1618,18 +1661,24 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
logger.debug('Terminating vpn')
self._backend.eip_stop(shutdown=True)
- def quit(self):
+ def quit(self, disable_autostart=True):
"""
Start the quit sequence and wait for services to finish.
Cleanup and close the main window before quitting.
+
+ :param disable_autostart: whether we should disable the autostart
+ feature or not
+ :type disable_autostart: bool
"""
if self._quitting:
return
+ if disable_autostart:
+ autostart.set_autostart(False)
+
self._quitting = True
self._close_to_tray = False
logger.debug('Quitting...')
- autostart.set_autostart(False)
# first thing to do quitting, hide the mainwindow and show tooltip.
self.hide()
@@ -1712,6 +1761,7 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
self._leap_signaler.stop()
+ LOG_CONTROLLER.stop_logbook_subscriber()
self._backend.stop()
time.sleep(0.05) # give the thread a little time to finish.
diff --git a/src/leap/bitmask/gui/passwordwindow.py b/src/leap/bitmask/gui/passwordwindow.py
index 88565829..94cf25da 100644
--- a/src/leap/bitmask/gui/passwordwindow.py
+++ b/src/leap/bitmask/gui/passwordwindow.py
@@ -19,14 +19,14 @@
Change password dialog window
"""
-from PySide import QtCore, QtGui
-from leap.bitmask.util.credentials import password_checks
+from PySide import QtGui
+from leap.bitmask.logs.utils import get_logger
+from leap.bitmask.util.credentials import password_checks
from leap.bitmask.gui.ui_password_change import Ui_PasswordChange
from leap.bitmask.gui.flashable import Flashable
-import logging
-logger = logging.getLogger(__name__)
+logger = get_logger()
class PasswordWindow(QtGui.QDialog, Flashable):
diff --git a/src/leap/bitmask/gui/preferences_account_page.py b/src/leap/bitmask/gui/preferences_account_page.py
index cab90eec..da9da14d 100644
--- a/src/leap/bitmask/gui/preferences_account_page.py
+++ b/src/leap/bitmask/gui/preferences_account_page.py
@@ -16,16 +16,17 @@
"""
Widget for "account" preferences
"""
-import logging
-
from functools import partial
from PySide import QtCore, QtGui
-from leap.bitmask.gui.ui_preferences_account_page import Ui_PreferencesAccountPage
+
+from leap.bitmask.logs.utils import get_logger
+from leap.bitmask.gui import ui_preferences_account_page as ui_pref
from leap.bitmask.gui.passwordwindow import PasswordWindow
from leap.bitmask.services import get_service_display_name
+from leap.bitmask._components import HAS_EIP
-logger = logging.getLogger(__name__)
+logger = get_logger()
class PreferencesAccountPage(QtGui.QWidget):
@@ -42,7 +43,7 @@ class PreferencesAccountPage(QtGui.QWidget):
:type app: App
"""
QtGui.QWidget.__init__(self, parent)
- self.ui = Ui_PreferencesAccountPage()
+ self.ui = ui_pref.Ui_PreferencesAccountPage()
self.ui.setupUi(self)
self.account = account
@@ -120,6 +121,8 @@ class PreferencesAccountPage(QtGui.QWidget):
# add one checkbox per service and set the current value
# from what is saved in settings.
for service in services:
+ if not HAS_EIP and service == "openvpn":
+ continue
try:
checkbox = QtGui.QCheckBox(
get_service_display_name(service), self)
diff --git a/src/leap/bitmask/gui/preferences_email_page.py b/src/leap/bitmask/gui/preferences_email_page.py
index 80e8d93e..3087f343 100644
--- a/src/leap/bitmask/gui/preferences_email_page.py
+++ b/src/leap/bitmask/gui/preferences_email_page.py
@@ -16,12 +16,12 @@
"""
Widget for "email" preferences
"""
-import logging
+from PySide import QtGui
-from PySide import QtCore, QtGui
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.gui.ui_preferences_email_page import Ui_PreferencesEmailPage
-logger = logging.getLogger(__name__)
+logger = get_logger()
class PreferencesEmailPage(QtGui.QWidget):
diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py
index daad08b0..baa71252 100644
--- a/src/leap/bitmask/gui/preferenceswindow.py
+++ b/src/leap/bitmask/gui/preferenceswindow.py
@@ -18,18 +18,18 @@
"""
Preferences window
"""
-import logging
-
from PySide import QtCore, QtGui
-from leap.bitmask.services import EIP_SERVICE, MX_SERVICE
+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_account_page import PreferencesAccountPage
from leap.bitmask.gui.preferences_vpn_page import PreferencesVpnPage
from leap.bitmask.gui.preferences_email_page import PreferencesEmailPage
-logger = logging.getLogger(__name__)
+logger = get_logger()
class PreferencesWindow(QtGui.QDialog):
@@ -121,7 +121,8 @@ class PreferencesWindow(QtGui.QDialog):
"""
Adds the pages for the different configuration categories.
"""
- self._account_page = PreferencesAccountPage(self, self.account, self.app)
+ 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)
@@ -179,6 +180,7 @@ class PreferencesWindow(QtGui.QDialog):
if account != self.account:
return
- self._vpn_item.setHidden(not EIP_SERVICE in services)
+ 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.
diff --git a/src/leap/bitmask/gui/signaltracker.py b/src/leap/bitmask/gui/signaltracker.py
index 0e3b2dce..3dfcfe18 100644
--- a/src/leap/bitmask/gui/signaltracker.py
+++ b/src/leap/bitmask/gui/signaltracker.py
@@ -14,11 +14,11 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import logging
-
from PySide import QtCore
-logger = logging.getLogger(__name__)
+from leap.bitmask.logs.utils import get_logger
+
+logger = get_logger()
class SignalTracker(QtCore.QObject):
diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py
index 91f1f605..ab48b756 100644
--- a/src/leap/bitmask/gui/statemachines.py
+++ b/src/leap/bitmask/gui/statemachines.py
@@ -17,16 +17,15 @@
"""
State machines for the Bitmask app.
"""
-import logging
-
from PySide import QtCore
from PySide.QtCore import QStateMachine, QState, Signal
from PySide.QtCore import QObject
from leap.bitmask.services import connections
from leap.common.check import leap_assert_type
+from leap.bitmask.logs.utils import get_logger
-logger = logging.getLogger(__name__)
+logger = get_logger()
_tr = QObject().tr
diff --git a/src/leap/bitmask/gui/ui/wizard.ui b/src/leap/bitmask/gui/ui/wizard.ui
index 0e28ecbf..b125577e 100644
--- a/src/leap/bitmask/gui/ui/wizard.ui
+++ b/src/leap/bitmask/gui/ui/wizard.ui
@@ -43,7 +43,7 @@
<string>Welcome to Bitmask</string>
</property>
<property name="subTitle">
- <string> </string>
+ <string/>
</property>
<attribute name="pageId">
<string notr="true">0</string>
@@ -59,7 +59,7 @@
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
- <string></string>
+ <string/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
@@ -112,7 +112,7 @@
<string>Choose a provider</string>
</property>
<property name="subTitle">
- <string> </string>
+ <string/>
</property>
<attribute name="pageId">
<string notr="true">1</string>
@@ -187,15 +187,15 @@
<height>22</height>
</size>
</property>
- <property name="scaledContents">
- <bool>true</bool>
- </property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../../../../../data/resources/icons.qrc">:/images/black/22/question.png</pixmap>
</property>
+ <property name="scaledContents">
+ <bool>true</bool>
+ </property>
</widget>
</item>
<item row="2" column="1">
@@ -218,15 +218,15 @@
<height>22</height>
</size>
</property>
- <property name="scaledContents">
- <bool>true</bool>
- </property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../../../../../data/resources/icons.qrc">:/images/black/22/question.png</pixmap>
</property>
+ <property name="scaledContents">
+ <bool>true</bool>
+ </property>
</widget>
</item>
<item row="1" column="1">
@@ -249,15 +249,15 @@
<height>22</height>
</size>
</property>
- <property name="scaledContents">
- <bool>true</bool>
- </property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../../../../../data/resources/icons.qrc">:/images/black/22/question.png</pixmap>
</property>
+ <property name="scaledContents">
+ <bool>true</bool>
+ </property>
</widget>
</item>
<item row="1" column="0">
@@ -363,6 +363,12 @@
</item>
<item>
<widget class="QPushButton" name="btnCheck">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
<property name="text">
<string>Check</string>
</property>
@@ -380,7 +386,7 @@
<string>About this provider</string>
</property>
<property name="subTitle">
- <string> </string>
+ <string/>
</property>
<attribute name="pageId">
<string notr="true">2</string>
@@ -522,7 +528,7 @@
<string>Provider setup</string>
</property>
<property name="subTitle">
- <string> </string>
+ <string/>
</property>
<attribute name="pageId">
<string notr="true">3</string>
@@ -590,15 +596,15 @@
<height>22</height>
</size>
</property>
- <property name="scaledContents">
- <bool>true</bool>
- </property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../../../../../data/resources/icons.qrc">:/images/black/22/question.png</pixmap>
</property>
+ <property name="scaledContents">
+ <bool>true</bool>
+ </property>
</widget>
</item>
<item row="1" column="1">
@@ -621,15 +627,15 @@
<height>22</height>
</size>
</property>
- <property name="scaledContents">
- <bool>true</bool>
- </property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../../../../../data/resources/icons.qrc">:/images/black/22/question.png</pixmap>
</property>
+ <property name="scaledContents">
+ <bool>true</bool>
+ </property>
</widget>
</item>
<item row="1" column="0">
@@ -673,15 +679,15 @@
<height>22</height>
</size>
</property>
- <property name="scaledContents">
- <bool>true</bool>
- </property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../../../../../data/resources/icons.qrc">:/images/black/22/question.png</pixmap>
</property>
+ <property name="scaledContents">
+ <bool>true</bool>
+ </property>
</widget>
</item>
<item row="0" column="0">
@@ -720,7 +726,7 @@
<string>Register new user</string>
</property>
<property name="subTitle">
- <string> </string>
+ <string/>
</property>
<attribute name="pageId">
<string notr="true">4</string>
@@ -845,7 +851,7 @@
<string>Service selection</string>
</property>
<property name="subTitle">
- <string> </string>
+ <string/>
</property>
<attribute name="pageId">
<string notr="true">5</string>
diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py
index 5da021d1..abaf2108 100644
--- a/src/leap/bitmask/gui/wizard.py
+++ b/src/leap/bitmask/gui/wizard.py
@@ -17,7 +17,6 @@
"""
First run wizard
"""
-import logging
import random
from functools import partial
@@ -30,16 +29,18 @@ from leap.bitmask.backend.leapbackend import ERROR_KEY, PASSED_KEY
from leap.bitmask.config import flags
from leap.bitmask.config.leapsettings import LeapSettings
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.gui.signaltracker import SignalTracker
from leap.bitmask.services import get_service_display_name, get_supported
from leap.bitmask.util.credentials import password_checks, username_checks
from leap.bitmask.util.credentials import USERNAME_REGEX
from leap.bitmask.util.keyring_helpers import has_keyring
+from leap.bitmask._components import HAS_EIP
from ui_wizard import Ui_Wizard
QtDelayedCall = QtCore.QTimer.singleShot
-logger = logging.getLogger(__name__)
+logger = get_logger()
class Wizard(QtGui.QWizard, SignalTracker):
@@ -264,6 +265,20 @@ class Wizard(QtGui.QWizard, SignalTracker):
if reset:
self._reset_provider_check()
+ def _provider_widget_set_enabled(self, enabled):
+ """
+ Enable/Disable the provider widget.
+ The widget to use depends on whether the used decided to use an
+ existing provider or a new one.
+
+ :param enabled: the new state for the widget
+ :type enabled: bool
+ """
+ if self.ui.rbNewProvider.isChecked():
+ self.ui.lnProvider.setEnabled(enabled)
+ else:
+ self.ui.cbProviders.setEnabled(enabled)
+
def _focus_username(self):
"""
Focus at the username lineedit for the registration page
@@ -371,6 +386,19 @@ class Wizard(QtGui.QWizard, SignalTracker):
self._set_register_status(error_msg, error=True)
self.ui.btnRegister.setEnabled(True)
+ def _registration_disabled(self):
+ """
+ TRIGGERS:
+ self._backend.signaler.srp_registration_disabled
+
+ The registration is disabled in the current provider.
+ """
+ self._username = self._password = None
+
+ error_msg = self.tr("The registration is disabled for this provider.")
+ self._set_register_status(error_msg, error=True)
+ self.ui.btnRegister.setEnabled(True)
+
def _registration_taken(self):
"""
TRIGGERS:
@@ -439,11 +467,7 @@ class Wizard(QtGui.QWizard, SignalTracker):
self.ui.grpCheckProvider.setVisible(True)
self.ui.btnCheck.setEnabled(False)
- # Disable provider widget
- if self.ui.rbNewProvider.isChecked():
- self.ui.lnProvider.setEnabled(False)
- else:
- self.ui.cbProviders.setEnabled(False)
+ self._provider_widget_set_enabled(False)
self.button(QtGui.QWizard.BackButton).clearFocus()
@@ -510,7 +534,7 @@ class Wizard(QtGui.QWizard, SignalTracker):
self.ui.lblHTTPS.setPixmap(self.QUESTION_ICON)
self.ui.lblProviderSelectStatus.setText(status)
self.ui.btnCheck.setEnabled(not passed)
- self.ui.lnProvider.setEnabled(not passed)
+ self._provider_widget_set_enabled(not passed)
def _https_connection(self, data):
"""
@@ -529,7 +553,8 @@ class Wizard(QtGui.QWizard, SignalTracker):
else:
self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON)
self.ui.btnCheck.setEnabled(not passed)
- self.ui.lnProvider.setEnabled(not passed)
+
+ self._provider_widget_set_enabled(not passed)
def _download_provider_info(self, data):
"""
@@ -558,13 +583,9 @@ class Wizard(QtGui.QWizard, SignalTracker):
status = self.tr("<font color='red'><b>Not a valid provider"
"</b></font>")
self.ui.lblProviderSelectStatus.setText(status)
- self.ui.btnCheck.setEnabled(True)
- # Enable provider widget
- if self.ui.rbNewProvider.isChecked():
- self.ui.lnProvider.setEnabled(True)
- else:
- self.ui.cbProviders.setEnabled(True)
+ self.ui.btnCheck.setEnabled(True)
+ self._provider_widget_set_enabled(True)
def _provider_get_details(self, details):
"""
@@ -574,6 +595,22 @@ class Wizard(QtGui.QWizard, SignalTracker):
:type details: dict
"""
self._provider_details = details
+ self._check_registration_allowed()
+
+ def _check_registration_allowed(self):
+ """
+ Check whether the provider allows new users registration or not.
+ If it is not allowed we display a message and prevent the user moving
+ forward on the wizard.
+ """
+ if self._show_register: # user wants to register a new account
+ if not self._provider_details['allow_registration']:
+ logger.debug("Registration not allowed")
+ status = ("<font color='red'><b>" +
+ self.tr("The provider has disabled registration") +
+ "</b></font>")
+ self.ui.lblProviderSelectStatus.setText(status)
+ self.button(QtGui.QWizard.NextButton).setEnabled(False)
def _download_ca_cert(self, data):
"""
@@ -654,6 +691,12 @@ class Wizard(QtGui.QWizard, SignalTracker):
checkbox.stateChanged.connect(
partial(self._service_selection_changed, service))
checkbox.setChecked(True)
+
+ if service == "openvpn" and not HAS_EIP:
+ # this is a mail-only build, we disable eip.
+ checkbox.setEnabled(False)
+ checkbox.setChecked(False)
+
self._shown_services.add(service)
except ValueError:
logger.error(
@@ -675,9 +718,11 @@ class Wizard(QtGui.QWizard, SignalTracker):
skip = self.ui.rbExistingProvider.isChecked()
if not self._provider_checks_ok:
self._enable_check()
+ self.ui.btnCheck.setFocus()
self._skip_provider_checks(skip)
else:
self._enable_check(reset=False)
+ self._check_registration_allowed()
if pageId == self.SETUP_PROVIDER_PAGE:
if not self._provider_setup_ok:
@@ -757,5 +802,6 @@ class Wizard(QtGui.QWizard, SignalTracker):
conntrack(sig.prov_check_api_certificate, self._check_api_certificate)
conntrack(sig.srp_registration_finished, self._registration_finished)
+ conntrack(sig.srp_registration_disabled, self._registration_disabled)
conntrack(sig.srp_registration_failed, self._registration_failed)
conntrack(sig.srp_registration_taken, self._registration_taken)
diff --git a/src/leap/bitmask/logs/__init__.py b/src/leap/bitmask/logs/__init__.py
index 0516b304..837a5ed9 100644
--- a/src/leap/bitmask/logs/__init__.py
+++ b/src/leap/bitmask/logs/__init__.py
@@ -1,3 +1,3 @@
-# levelname length == 8, since 'CRITICAL' is the longest
-LOG_FORMAT = ('%(asctime)s - %(levelname)-8s - '
- 'L#%(lineno)-4s : %(name)s:%(funcName)s() - %(message)s')
+LOG_FORMAT = (u'[{record.time:%Y-%m-%d %H:%M:%S}] '
+ u'{record.level_name: <8} - L#{record.lineno: <4} : '
+ u'{record.module}:{record.func_name} - {record.message}')
diff --git a/src/leap/bitmask/logs/leap_log_handler.py b/src/leap/bitmask/logs/leap_log_handler.py
deleted file mode 100644
index 24141638..00000000
--- a/src/leap/bitmask/logs/leap_log_handler.py
+++ /dev/null
@@ -1,137 +0,0 @@
-# -*- coding: utf-8 -*-
-# leap_log_handler.py
-# Copyright (C) 2013 LEAP
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-"""
-Custom handler for the logger window.
-"""
-import logging
-
-from PySide import QtCore
-
-from leap.bitmask.logs import LOG_FORMAT
-
-
-class LogHandler(logging.Handler):
- """
- This is the custom handler that implements our desired formatting
- and also keeps a history of all the logged events.
- """
-
- MESSAGE_KEY = 'message'
- RECORD_KEY = 'record'
-
- def __init__(self, qtsignal):
- """
- LogHander initialization.
- Calls parent method and keeps a reference to the qtsignal
- that will be used to fire the gui update.
- """
- # TODO This is going to eat lots of memory after some time.
- # Should be pruned at some moment.
- self._log_history = []
-
- logging.Handler.__init__(self)
- self._qtsignal = qtsignal
-
- def _get_format(self, logging_level):
- """
- Sets the log format depending on the parameter.
- It uses html and css to set the colors for the logs.
-
- :param logging_level: the debug level to define the color.
- :type logging_level: str.
- """
- formatter = logging.Formatter(LOG_FORMAT)
- return formatter
-
- def emit(self, logRecord):
- """
- This method is fired every time that a record is logged by the
- logging module.
- This method reimplements logging.Handler.emit that is fired
- in every logged message.
-
- :param logRecord: the record emitted by the logging module.
- :type logRecord: logging.LogRecord.
- """
- self.setFormatter(self._get_format(logRecord.levelname))
- log = self.format(logRecord)
- log_item = {self.RECORD_KEY: logRecord, self.MESSAGE_KEY: log}
- self._log_history.append(log_item)
- self._qtsignal(log_item)
-
-
-class HandlerAdapter(object):
- """
- New style class that accesses all attributes from the LogHandler.
-
- Used as a workaround for a problem with multiple inheritance with Pyside
- that surfaced under OSX with pyside 1.1.0.
- """
- MESSAGE_KEY = 'message'
- RECORD_KEY = 'record'
-
- def __init__(self, qtsignal):
- self._handler = LogHandler(qtsignal=qtsignal)
-
- def setLevel(self, *args, **kwargs):
- return self._handler.setLevel(*args, **kwargs)
-
- def addFilter(self, *args, **kwargs):
- return self._handler.addFilter(*args, **kwargs)
-
- def handle(self, *args, **kwargs):
- return self._handler.handle(*args, **kwargs)
-
- @property
- def level(self):
- return self._handler.level
-
-
-class LeapLogHandler(QtCore.QObject, HandlerAdapter):
- """
- Custom logging handler. It emits Qt signals so it can be plugged to a gui.
-
- Its inner handler also stores an history of logs that can be fetched after
- having been connected to a gui.
- """
- # All dicts returned are of the form
- # {'record': LogRecord, 'message': str}
- new_log = QtCore.Signal(dict)
-
- def __init__(self):
- """
- LeapLogHandler initialization.
- Initializes parent classes.
- """
- QtCore.QObject.__init__(self)
- HandlerAdapter.__init__(self, qtsignal=self.qtsignal)
-
- def qtsignal(self, log_item):
- # WARNING: the new-style connection does NOT work because PySide
- # translates the emit method to self.emit, and that collides with
- # the emit method for logging.Handler
- # self.new_log.emit(log_item)
- QtCore.QObject.emit(
- self,
- QtCore.SIGNAL('new_log(PyObject)'), log_item)
-
- @property
- def log_history(self):
- """
- Returns the history of the logged messages.
- """
- return self._handler._log_history
diff --git a/src/leap/bitmask/logs/log_silencer.py b/src/leap/bitmask/logs/log_silencer.py
index 56b290e4..da95e9b1 100644
--- a/src/leap/bitmask/logs/log_silencer.py
+++ b/src/leap/bitmask/logs/log_silencer.py
@@ -17,26 +17,32 @@
"""
Filter for leap logs.
"""
-import logging
import os
-import re
from leap.bitmask.util import get_path_prefix
-class SelectiveSilencerFilter(logging.Filter):
+class SelectiveSilencerFilter(object):
"""
- Configurable filter for root leap logger.
+ Configurable log filter for a Logbook logger.
- If you want to ignore components from the logging, just add them,
- one by line, to ~/.config/leap/leap.dev.conf
+ To include certain logs add them to:
+ ~/.config/leap/leap_log_inclusion.dev.conf
+
+ To exclude certain logs add them to:
+ ~/.config/leap/leap_log_exclusion.dev.conf
+
+ The log filtering is based on how the module name starts.
+ In case of no inclusion or exclusion files are detected the default rules
+ will be used.
"""
# TODO we can augment this by properly parsing the log-silencer file
# and having different sections: ignore, levels, ...
# TODO use ConfigParser to unify sections [log-ignore] [log-debug] etc
- CONFIG_NAME = "leap.dev.conf"
+ INCLUSION_CONFIG_FILE = "leap_log_inclusion.dev.conf"
+ EXCLUSION_CONFIG_FILE = "leap_log_exclusion.dev.conf"
# Components to be completely silenced in the main bitmask logs.
# You probably should think twice before adding a component to
@@ -44,38 +50,49 @@ class SelectiveSilencerFilter(logging.Filter):
# only in those cases in which we gain more from silencing them than from
# having their logs into the main log file that the user will likely send
# to us.
- SILENCER_RULES = (
+ EXCLUSION_RULES = (
'leap.common.events',
'leap.common.decorators',
)
+ # This tuple list the module names that we want to display, any different
+ # namespace will be filtered out.
+ INCLUSION_RULES = (
+ '__main__',
+ 'leap.', # right now we just want to include logs from leap modules
+ 'twisted.',
+ )
+
def __init__(self):
"""
Tries to load silencer rules from the default path,
or load from the SILENCER_RULES tuple if not found.
"""
- self.rules = None
- if os.path.isfile(self._rules_path):
- self.rules = self._load_rules()
- if not self.rules:
- self.rules = self.SILENCER_RULES
-
- @property
- def _rules_path(self):
- """
- The configuration file for custom ignore rules.
- """
- return os.path.join(get_path_prefix(), "leap", self.CONFIG_NAME)
+ self._inclusion_path = os.path.join(get_path_prefix(), "leap",
+ self.INCLUSION_CONFIG_FILE)
+
+ self._exclusion_path = os.path.join(get_path_prefix(), "leap",
+ self.EXCLUSION_CONFIG_FILE)
+
+ self._load_rules()
def _load_rules(self):
"""
- Loads a list of paths to be ignored from the logging.
+ Load the inclusion and exclusion rules from the config files.
"""
- lines = open(self._rules_path).readlines()
- return map(lambda line: re.sub('\s', '', line),
- lines)
+ try:
+ with open(self._inclusion_path) as f:
+ self._inclusion_rules = f.read().splitlines()
+ except IOError:
+ self._inclusion_rules = self.INCLUSION_RULES
- def filter(self, record):
+ try:
+ with open(self._exclusion_path) as f:
+ self._exclusion_rules = f.read().splitlines()
+ except IOError:
+ self._exclusion_rules = self.EXCLUSION_RULES
+
+ def filter(self, record, handler):
"""
Implements the filter functionality for this Filter
@@ -84,10 +101,25 @@ class SelectiveSilencerFilter(logging.Filter):
:returns: a bool indicating whether the record should be logged or not.
:rtype: bool
"""
- if not self.rules:
- return True
- logger_path = record.name
- for path in self.rules:
+ if not self._inclusion_rules and not self._exclusion_rules:
+ return True # do not filter if there are no rules
+
+ logger_path = record.module
+ if logger_path is None:
+ return True # we can't filter if there is no module info
+
+ # exclude paths that ARE NOT listed in ANY of the inclusion rules
+ match = False
+ for path in self._inclusion_rules:
+ if logger_path.startswith(path):
+ match = True
+
+ if not match:
+ return False
+
+ # exclude paths that ARE listed in the exclusion rules
+ for path in self._exclusion_rules:
if logger_path.startswith(path):
return False
+
return True
diff --git a/src/leap/bitmask/logs/safezmqhandler.py b/src/leap/bitmask/logs/safezmqhandler.py
new file mode 100644
index 00000000..4f7aca9b
--- /dev/null
+++ b/src/leap/bitmask/logs/safezmqhandler.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+# safezmqhandler.py
+# Copyright (C) 2013, 2014, 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/>.
+"""
+A thread-safe zmq handler for LogBook.
+"""
+import json
+import threading
+
+from logbook.queues import ZeroMQHandler
+from logbook import NOTSET
+
+import zmq
+
+
+class SafeZMQHandler(ZeroMQHandler):
+ """
+ A ZMQ log handler for LogBook that is thread-safe.
+
+ This log handler makes use of the existing zmq handler and if the user
+ tries to log something from a different thread than the one used to
+ create the handler a new socket is created for that thread.
+
+ Note: In ZMQ, Contexts are threadsafe objects, but Sockets are not.
+ """
+
+ def __init__(self, uri=None, level=NOTSET, filter=None, bubble=False,
+ context=None, multi=False):
+ """
+ Safe zmq handler constructor that calls the ZeroMQHandler constructor
+ and does some extra initializations.
+ """
+ # The current `SafeZMQHandler` uses the `ZeroMQHandler` constructor
+ # which creates a socket each time.
+ # The purpose of the `self._sockets` attribute is to prevent cases in
+ # which we use the same logger in different threads. For instance when
+ # we (in the same file) `deferToThread` a method/function, we are using
+ # the same logger/socket without calling get_logger again.
+ # If we want to reuse the socket, we need to rewrite this constructor
+ # instead of calling the ZeroMQHandler's one.
+ # The best approach may be to inherit directly from `logbook.Handler`.
+
+ ZeroMQHandler.__init__(self, uri, level, filter, bubble, context,
+ multi)
+
+ current_id = self._get_caller_id()
+ # we store the socket created on the parent
+ self._sockets = {current_id: self.socket}
+
+ # store the settings for new socket creation
+ self._multi = multi
+ self._uri = uri
+
+ def _get_caller_id(self):
+ """
+ Return an id for the caller that depends on the current thread.
+ Thanks to this we can detect if we are running in a thread different
+ than the one who created the socket and create a new one for it.
+
+ :rtype: int
+ """
+ # NOTE it makes no sense to use multiprocessing id since the sockets
+ # list can't/shouldn't be shared between processes. We only use
+ # thread id. The user needs to make sure that the handler is created
+ # inside each process.
+ return threading.current_thread().ident
+
+ def _get_new_socket(self):
+ """
+ Return a new socket using the `uri` and `multi` parameters given in the
+ constructor.
+
+ :rtype: zmq.Socket
+ """
+ socket = None
+
+ if self._multi:
+ socket = self.context.socket(zmq.PUSH)
+ if self._uri is not None:
+ socket.connect(self._uri)
+ else:
+ socket = self.context.socket(zmq.PUB)
+ if self._uri is not None:
+ socket.bind(self._uri)
+
+ return socket
+
+ def emit(self, record):
+ """
+ Emit the given `record` through the socket.
+
+ :param record: the record to emit
+ :type record: Logbook.LogRecord
+ """
+ current_id = self._get_caller_id()
+ socket = None
+
+ if current_id in self._sockets:
+ socket = self._sockets[current_id]
+ else:
+ # TODO: create new socket
+ socket = self._get_new_socket()
+ self._sockets[current_id] = socket
+
+ socket.send(json.dumps(self.export_record(record)).encode("utf-8"))
+
+ def close(self, linger=-1):
+ """
+ Close all the sockets and linger `linger` time.
+
+ This reimplements the ZeroMQHandler.close method that is used by
+ context methods.
+
+ :param linger: time to linger, -1 to not to.
+ :type linger: int
+ """
+ for socket in self._sockets.values():
+ socket.close(linger)
diff --git a/src/leap/bitmask/logs/tests/test_leap_log_handler.py b/src/leap/bitmask/logs/tests/test_leap_log_handler.py
deleted file mode 100644
index 20b09aef..00000000
--- a/src/leap/bitmask/logs/tests/test_leap_log_handler.py
+++ /dev/null
@@ -1,120 +0,0 @@
-# -*- coding: utf-8 -*-
-# test_leap_log_handler.py
-# Copyright (C) 2013 LEAP
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-"""
-tests for leap_log_handler
-"""
-try:
- import unittest2 as unittest
-except ImportError:
- import unittest
-
-import logging
-
-from leap.bitmask.logs.leap_log_handler import LeapLogHandler
-from leap.bitmask.util.pyside_tests_helper import BasicPySlotCase
-from leap.common.testing.basetest import BaseLeapTest
-
-from mock import Mock
-
-
-class LeapLogHandlerTest(BaseLeapTest, BasicPySlotCase):
- """
- LeapLogHandlerTest's tests.
- """
- def _callback(self, *args):
- """
- Simple callback to track if a signal was emitted.
- """
- self.called = True
- self.emitted_msg = args[0][LeapLogHandler.MESSAGE_KEY]
-
- def setUp(self):
- BasicPySlotCase.setUp(self)
-
- # Create the logger
- level = logging.DEBUG
- self.logger = logging.getLogger(name='test')
- self.logger.setLevel(level)
-
- # Create the handler
- self.leap_handler = LeapLogHandler()
- self.leap_handler.setLevel(level)
- self.logger.addHandler(self.leap_handler)
-
- def tearDown(self):
- BasicPySlotCase.tearDown(self)
- try:
- self.leap_handler.new_log.disconnect()
- except Exception:
- pass
-
- def test_history_starts_empty(self):
- self.assertEqual(self.leap_handler.log_history, [])
-
- def test_one_log_captured(self):
- self.logger.debug('test')
- self.assertEqual(len(self.leap_handler.log_history), 1)
-
- def test_history_records_order(self):
- self.logger.debug('test 01')
- self.logger.debug('test 02')
- self.logger.debug('test 03')
-
- logs = []
- for message in self.leap_handler.log_history:
- logs.append(message[LeapLogHandler.RECORD_KEY].msg)
-
- self.assertIn('test 01', logs)
- self.assertIn('test 02', logs)
- self.assertIn('test 03', logs)
-
- def test_history_messages_order(self):
- self.logger.debug('test 01')
- self.logger.debug('test 02')
- self.logger.debug('test 03')
-
- logs = []
- for message in self.leap_handler.log_history:
- logs.append(message[LeapLogHandler.MESSAGE_KEY])
-
- self.assertIn('test 01', logs[0])
- self.assertIn('test 02', logs[1])
- self.assertIn('test 03', logs[2])
-
- def test_emits_signal(self):
- log_format = '%(name)s - %(levelname)s - %(message)s'
- formatter = logging.Formatter(log_format)
- get_format = Mock(return_value=formatter)
- self.leap_handler._handler._get_format = get_format
-
- self.leap_handler.new_log.connect(self._callback)
- self.logger.debug('test')
-
- expected_log_msg = "test - DEBUG - test"
-
- # signal emitted
- self.assertTrue(self.called)
-
- # emitted message
- self.assertEqual(self.emitted_msg, expected_log_msg)
-
- # Mock called
- self.assertTrue(get_format.called)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/bitmask/logs/utils.py b/src/leap/bitmask/logs/utils.py
index 8367937a..683fb542 100644
--- a/src/leap/bitmask/logs/utils.py
+++ b/src/leap/bitmask/logs/utils.py
@@ -1,80 +1,99 @@
-import logging
+# -*- coding: utf-8 -*-
+# utils.py
+# Copyright (C) 2013, 2014, 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/>.
+"""
+Logs utilities
+"""
+
+import os
import sys
+from leap.bitmask.config import flags
from leap.bitmask.logs import LOG_FORMAT
from leap.bitmask.logs.log_silencer import SelectiveSilencerFilter
-from leap.bitmask.logs.leap_log_handler import LeapLogHandler
-from leap.bitmask.logs.streamtologger import StreamToLogger
+from leap.bitmask.logs.safezmqhandler import SafeZMQHandler
+# from leap.bitmask.logs.streamtologger import StreamToLogger
from leap.bitmask.platform_init import IS_WIN
+from leap.bitmask.util import get_path_prefix
+from leap.common.files import mkdir_p
+
+from PySide import QtCore
+
+import logbook
+from logbook.more import ColorizedStderrHandler
+from logbook.queues import ZeroMQSubscriber
+
+
+# NOTE: make sure that the folder exists, the logger is created before saving
+# settings on the first run.
+_base = os.path.join(get_path_prefix(), "leap")
+mkdir_p(_base)
+BITMASK_LOG_FILE = os.path.join(_base, 'bitmask.log')
-def create_logger(debug=False, logfile=None, replace_stdout=True):
+def get_logger(perform_rollover=False):
"""
- Create the logger and attach the handlers.
-
- :param debug: the level of the messages that we should log
- :type debug: bool
- :param logfile: the file name of where we should to save the logs
- :type logfile: str
- :return: the new logger with the attached handlers.
- :rtype: logging.Logger
+ Push to the app stack the needed handlers and return a Logger object.
+
+ :rtype: logbook.Logger
"""
- # TODO: get severity from command line args
- if debug:
- level = logging.DEBUG
- else:
- level = logging.WARNING
-
- # Create logger and formatter
- logger = logging.getLogger(name='leap')
- logger.setLevel(level)
- formatter = logging.Formatter(LOG_FORMAT)
-
- # Console handler
- try:
- import coloredlogs
- console = coloredlogs.ColoredStreamHandler(level=level)
- except ImportError:
- console = logging.StreamHandler()
- console.setLevel(level)
- console.setFormatter(formatter)
- using_coloredlog = False
- else:
- using_coloredlog = True
-
- if using_coloredlog:
- replace_stdout = False
+ level = logbook.WARNING
+ if flags.DEBUG:
+ level = logbook.NOTSET
+
+ # This handler consumes logs not handled by the others
+ null_handler = logbook.NullHandler(bubble=False)
+ null_handler.push_application()
silencer = SelectiveSilencerFilter()
- console.addFilter(silencer)
- logger.addHandler(console)
- logger.debug('Console handler plugged!')
-
- # LEAP custom handler
- leap_handler = LeapLogHandler()
- leap_handler.setLevel(level)
- leap_handler.addFilter(silencer)
- logger.addHandler(leap_handler)
- logger.debug('Leap handler plugged!')
-
- # File handler
- if logfile is not None:
- logger.debug('Setting logfile to %s ', logfile)
- fileh = logging.FileHandler(logfile)
- fileh.setLevel(logging.DEBUG)
- fileh.setFormatter(formatter)
- fileh.addFilter(silencer)
- logger.addHandler(fileh)
- logger.debug('File handler plugged!')
-
- if replace_stdout:
- replace_stdout_stderr_with_logging(logger)
+
+ zmq_handler = SafeZMQHandler('tcp://127.0.0.1:5000', multi=True,
+ level=level, filter=silencer.filter)
+ zmq_handler.push_application()
+
+ file_handler = logbook.RotatingFileHandler(
+ BITMASK_LOG_FILE, format_string=LOG_FORMAT, bubble=True,
+ filter=silencer.filter, max_size=sys.maxint)
+
+ if perform_rollover:
+ file_handler.perform_rollover()
+
+ file_handler.push_application()
+
+ # don't use simple stream, go for colored log handler instead
+ # stream_handler = logbook.StreamHandler(sys.stdout,
+ # format_string=LOG_FORMAT,
+ # bubble=True)
+ # stream_handler.push_application()
+ stream_handler = ColorizedStderrHandler(
+ level=level, format_string=LOG_FORMAT, bubble=True,
+ filter=silencer.filter)
+ stream_handler.push_application()
+
+ logger = logbook.Logger('leap')
return logger
-def replace_stdout_stderr_with_logging(logger):
+def replace_stdout_stderr_with_logging(logger=None):
"""
+ NOTE:
+ we are not using this right now (see commented lines on app.py),
+ this needs to be reviewed since the log handler has changed.
+
Replace:
- the standard output
- the standard error
@@ -84,9 +103,119 @@ def replace_stdout_stderr_with_logging(logger):
# Disabling this on windows since it breaks ALL THE THINGS
# The issue for this is #4149
if not IS_WIN:
- sys.stdout = StreamToLogger(logger, logging.DEBUG)
- sys.stderr = StreamToLogger(logger, logging.ERROR)
+ # logger = get_logger()
+ # sys.stdout = StreamToLogger(logger, logbook.NOTSET)
+ # sys.stderr = StreamToLogger(logger, logging.ERROR)
# Replace twisted's logger to use our custom output.
from twisted.python import log
log.startLogging(sys.stdout)
+
+
+class QtLogHandler(logbook.Handler, logbook.StringFormatterHandlerMixin):
+ """
+ Custom log handler which emits a log record with the message properly
+ formatted using a Qt Signal.
+ """
+
+ class _QtSignaler(QtCore.QObject):
+ """
+ inline class used to hold the `new_log` Signal, if this is used
+ directly in the outside class it fails due how PySide works.
+
+ This is the message we get if not use this method:
+ TypeError: Error when calling the metaclass bases
+ metaclass conflict: the metaclass of a derived class must be a
+ (non-strict) subclass of the metaclasses of all its bases
+
+ """
+ new_log = QtCore.Signal(object)
+
+ def emit(self, data):
+ """
+ emit the `new_log` Signal with the given `data` parameter.
+
+ :param data: the data to emit along with the signal.
+ :type data: object
+ """
+ # WARNING: the new-style connection does NOT work because PySide
+ # translates the emit method to self.emit, and that collides with
+ # the emit method for logging.Handler
+ # self.new_log.emit(log_item)
+ QtCore.QObject.emit(self, QtCore.SIGNAL('new_log(PyObject)'), data)
+
+ def __init__(self, level=logbook.NOTSET, format_string=None,
+ encoding=None, filter=None, bubble=False):
+
+ logbook.Handler.__init__(self, level, filter, bubble)
+ logbook.StringFormatterHandlerMixin.__init__(self, format_string)
+
+ self.qt = self._QtSignaler()
+ self.logs = []
+
+ def __enter__(self):
+ return logbook.Handler.__enter__(self)
+
+ def __exit__(self, exc_type, exc_value, tb):
+ return logbook.Handler.__exit__(self, exc_type, exc_value, tb)
+
+ def emit(self, record):
+ """
+ Emit the specified logging record using a Qt Signal.
+ Also add it to the history in order to be able to access it later.
+
+ :param record: the record to emit
+ :type record: logbook.LogRecord
+ """
+ global _LOGS_HISTORY
+ record.msg = self.format(record)
+ # NOTE: not optimal approach, we may want to look at
+ # bisect.insort with a custom approach to use key or
+ # http://code.activestate.com/recipes/577197-sortedcollection/
+ # Sort logs on arrival, logs transmitted over zmq may arrive unsorted.
+ self.logs.append(record)
+ self.logs = sorted(self.logs, key=lambda r: r.time)
+
+ # XXX: emitting the record on arrival does not allow us to sort here so
+ # in the GUI the logs may arrive with with some time sort problem.
+ # We should implement a sort-on-arrive for the log window.
+ # Maybe we should switch to a tablewidget item that sort automatically
+ # by timestamp.
+ # As a user workaround you can close/open the log window
+ self.qt.emit(record)
+
+
+class _LogController(object):
+ def __init__(self):
+ self._qt_handler = QtLogHandler(format_string=LOG_FORMAT)
+ self._logbook_controller = None
+ self.new_log = self._qt_handler.qt.new_log
+
+ def start_logbook_subscriber(self):
+ """
+ Run in the background the log receiver.
+ """
+ if self._logbook_controller is None:
+ subscriber = ZeroMQSubscriber('tcp://127.0.0.1:5000', multi=True)
+ self._logbook_controller = subscriber.dispatch_in_background(
+ self._qt_handler)
+
+ def stop_logbook_subscriber(self):
+ """
+ Stop the background thread that receives messages through zmq, also
+ close the subscriber socket.
+ This allows us to re-create the subscriber when we reopen this window
+ without getting an error at trying to connect twice to the zmq port.
+ """
+ if self._logbook_controller is not None:
+ self._logbook_controller.stop()
+ self._logbook_controller.subscriber.close()
+ self._logbook_controller = None
+
+ def get_logs(self):
+ return self._qt_handler.logs
+
+# use a global variable to store received logs through different opened
+# instances of the log window as well as to containing the logbook background
+# handle.
+LOG_CONTROLLER = _LogController()
diff --git a/src/leap/bitmask/platform_init/initializers.py b/src/leap/bitmask/platform_init/initializers.py
index 086927dd..eb892cce 100644
--- a/src/leap/bitmask/platform_init/initializers.py
+++ b/src/leap/bitmask/platform_init/initializers.py
@@ -17,7 +17,6 @@
"""
Platform-dependant initialization code.
"""
-import logging
import os
import platform
import stat
@@ -32,9 +31,9 @@ from leap.bitmask.services.eip import get_vpn_launcher
from leap.bitmask.services.eip.darwinvpnlauncher import DarwinVPNLauncher
from leap.bitmask.util import first
from leap.bitmask.util.privilege_policies import LinuxPolicyChecker
+from leap.bitmask.logs.utils import get_logger
-
-logger = logging.getLogger(__name__)
+logger = get_logger()
# NOTE we could use a deferToThread here, but should
# be aware of this bug: http://www.themacaque.com/?p=1067
@@ -202,6 +201,7 @@ def check_polkit():
try:
LinuxPolicyChecker.maybe_pkexec()
+ return True
except Exception:
logger.error("No polkit agent running.")
diff --git a/src/leap/bitmask/platform_init/locks.py b/src/leap/bitmask/platform_init/locks.py
index ac45a5ce..203d367b 100644
--- a/src/leap/bitmask/platform_init/locks.py
+++ b/src/leap/bitmask/platform_init/locks.py
@@ -17,14 +17,13 @@
"""
Utilities for handling multi-platform file locking mechanisms
"""
-import logging
import errno
import os
import platform
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.platform_init import IS_WIN, IS_UNIX
-from leap.common.events import signal as signal_event
-from leap.common.events import events_pb2 as proto
+from leap.common.events import emit, catalog
if IS_UNIX:
from fcntl import flock, LOCK_EX, LOCK_NB
@@ -38,7 +37,7 @@ else: # WINDOWS
from leap.bitmask.util import get_modification_ts, update_modification_ts
-logger = logging.getLogger(__name__)
+logger = get_logger()
if IS_UNIX:
@@ -364,7 +363,7 @@ def we_are_the_one_and_only():
locker.get_lock()
we_are_the_one = locker.locked_by_us
if not we_are_the_one:
- signal_event(proto.RAISE_WINDOW)
+ emit(catalog.RAISE_WINDOW)
return we_are_the_one
elif IS_WIN:
@@ -385,7 +384,7 @@ def we_are_the_one_and_only():
# let's assume it's a stalled lock
we_are_the_one = True
- signal_event(proto.RAISE_WINDOW)
+ emit(catalog.RAISE_WINDOW)
while check_interval():
if get_modification_ts(lock_path) > ts:
diff --git a/src/leap/bitmask/provider/__init__.py b/src/leap/bitmask/provider/__init__.py
index 89ff5d95..4385a92f 100644
--- a/src/leap/bitmask/provider/__init__.py
+++ b/src/leap/bitmask/provider/__init__.py
@@ -17,6 +17,7 @@
"""
Provider utilities.
"""
+import logging
import os
from pkg_resources import parse_version
@@ -24,6 +25,8 @@ from pkg_resources import parse_version
from leap.bitmask import __short_version__ as BITMASK_VERSION
from leap.common.check import leap_assert
+logger = logging.getLogger(__name__)
+
# The currently supported API versions by the client.
SUPPORTED_APIS = ["1"]
@@ -62,4 +65,12 @@ def supports_client(minimum_version):
:returns: True if that version is supported or False otherwise.
:return type: bool
"""
- return parse_version(minimum_version) <= parse_version(BITMASK_VERSION)
+ try:
+ min_ver = parse_version(minimum_version)
+ cur_ver = parse_version(BITMASK_VERSION)
+ supported = min_ver <= cur_ver
+ except TypeError as exc:
+ logger.error("Error while parsing versions")
+ logger.exception(exc)
+ supported = False
+ return supported
diff --git a/src/leap/bitmask/provider/pinned.py b/src/leap/bitmask/provider/pinned.py
index 09fcc52c..ea1788eb 100644
--- a/src/leap/bitmask/provider/pinned.py
+++ b/src/leap/bitmask/provider/pinned.py
@@ -17,13 +17,12 @@
"""
Pinned Providers
"""
-import logging
-
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.provider import pinned_calyx
from leap.bitmask.provider import pinned_demobitmask
from leap.bitmask.provider import pinned_riseup
-logger = logging.getLogger(__name__)
+logger = get_logger()
class PinnedProviders(object):
diff --git a/src/leap/bitmask/provider/pinned_riseup.py b/src/leap/bitmask/provider/pinned_riseup.py
index 8cc51506..8cfca6ce 100644
--- a/src/leap/bitmask/provider/pinned_riseup.py
+++ b/src/leap/bitmask/provider/pinned_riseup.py
@@ -22,7 +22,7 @@ DOMAIN = "riseup.net"
PROVIDER_JSON = """
{
- "api_uri": "https://api.black.riseup.net:4430",
+ "api_uri": "https://api.black.riseup.net:443",
"api_version": "1",
"ca_cert_fingerprint": "SHA256: a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494",
"ca_cert_uri": "https://black.riseup.net/ca.crt",
diff --git a/src/leap/bitmask/provider/providerbootstrapper.py b/src/leap/bitmask/provider/providerbootstrapper.py
index efba29f9..28944d89 100644
--- a/src/leap/bitmask/provider/providerbootstrapper.py
+++ b/src/leap/bitmask/provider/providerbootstrapper.py
@@ -17,7 +17,6 @@
"""
Provider bootstrapping
"""
-import logging
import socket
import os
import sys
@@ -28,6 +27,7 @@ from leap.bitmask import provider
from leap.bitmask import util
from leap.bitmask.config import flags
from leap.bitmask.config.providerconfig import ProviderConfig, MissingCACert
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.provider import get_provider_path
from leap.bitmask.provider.pinned import PinnedProviders
from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper
@@ -38,7 +38,7 @@ from leap.common.certs import get_digest
from leap.common.check import leap_assert, leap_assert_type, leap_check
from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p
-logger = logging.getLogger(__name__)
+logger = get_logger()
class UnsupportedProviderAPI(Exception):
diff --git a/src/leap/bitmask/services/__init__.py b/src/leap/bitmask/services/__init__.py
index ba12ba4e..54426669 100644
--- a/src/leap/bitmask/services/__init__.py
+++ b/src/leap/bitmask/services/__init__.py
@@ -17,7 +17,6 @@
"""
Services module.
"""
-import logging
import os
import sys
@@ -25,6 +24,7 @@ from PySide import QtCore
from leap.bitmask.config import flags
from leap.bitmask.crypto.srpauth import SRPAuth
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.util.constants import REQUEST_TIMEOUT
from leap.bitmask.util.privilege_policies import is_missing_policy_permissions
from leap.bitmask.util.request_helpers import get_content
@@ -34,7 +34,7 @@ from leap.common.check import leap_assert
from leap.common.config.baseconfig import BaseConfig
from leap.common.files import get_mtime
-logger = logging.getLogger(__name__)
+logger = get_logger()
EIP_SERVICE = u"openvpn"
diff --git a/src/leap/bitmask/services/abstractbootstrapper.py b/src/leap/bitmask/services/abstractbootstrapper.py
index 77929b75..191309ba 100644
--- a/src/leap/bitmask/services/abstractbootstrapper.py
+++ b/src/leap/bitmask/services/abstractbootstrapper.py
@@ -18,8 +18,6 @@
"""
Abstract bootstrapper implementation
"""
-import logging
-
import requests
from functools import partial
@@ -27,12 +25,13 @@ from functools import partial
from PySide import QtCore
from twisted.python import log
-from twisted.internet import threads
+from twisted.internet import threads, reactor
from twisted.internet.defer import CancelledError
+from leap.bitmask.logs.utils import get_logger
from leap.common.check import leap_assert, leap_assert_type
-logger = logging.getLogger(__name__)
+logger = get_logger()
class AbstractBootstrapper(QtCore.QObject):
@@ -156,7 +155,8 @@ class AbstractBootstrapper(QtCore.QObject):
data = {self.PASSED_KEY: True, self.ERROR_KEY: ""}
if isinstance(signal, basestring):
if self._signaler is not None:
- self._signaler.signal(signal, data)
+ reactor.callFromThread(
+ self._signaler.signal, signal, data)
else:
logger.warning("Tried to notify but no signaler found")
else:
diff --git a/src/leap/bitmask/services/eip/conductor.py b/src/leap/bitmask/services/eip/conductor.py
index 3fc88724..3386dddf 100644
--- a/src/leap/bitmask/services/eip/conductor.py
+++ b/src/leap/bitmask/services/eip/conductor.py
@@ -20,10 +20,9 @@ EIP Conductor module.
This handles Qt Signals and triggers the calls to the backend,
where the VPNProcess has been initialized.
"""
-import logging
-
from PySide import QtCore
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.gui import statemachines
from leap.bitmask.services import EIP_SERVICE
from leap.bitmask.services import get_service_display_name
@@ -31,7 +30,7 @@ from leap.bitmask.services.eip.connection import EIPConnection
from leap.bitmask.platform_init import IS_MAC
QtDelayedCall = QtCore.QTimer.singleShot
-logger = logging.getLogger(__name__)
+logger = get_logger()
class EIPConductor(object):
diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py
index f83e0170..17fc11c2 100644
--- a/src/leap/bitmask/services/eip/darwinvpnlauncher.py
+++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py
@@ -19,15 +19,15 @@ Darwin VPN launcher implementation.
"""
import commands
import getpass
-import logging
import os
import sys
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.services.eip.vpnlauncher import VPNLauncher
from leap.bitmask.services.eip.vpnlauncher import VPNLauncherException
from leap.bitmask.util import get_path_prefix
-logger = logging.getLogger(__name__)
+logger = get_logger()
class EIPNoTunKextLoaded(VPNLauncherException):
diff --git a/src/leap/bitmask/services/eip/eipbootstrapper.py b/src/leap/bitmask/services/eip/eipbootstrapper.py
index f78113bc..7a331d71 100644
--- a/src/leap/bitmask/services/eip/eipbootstrapper.py
+++ b/src/leap/bitmask/services/eip/eipbootstrapper.py
@@ -17,11 +17,11 @@
"""
EIP bootstrapping
"""
-import logging
import os
from leap.bitmask.config.providerconfig import ProviderConfig
from leap.bitmask.crypto.certs import download_client_cert
+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.eip.eipconfig import EIPConfig
@@ -29,7 +29,7 @@ from leap.common import certs as leap_certs
from leap.common.check import leap_assert, leap_assert_type
from leap.common.files import check_and_fix_urw_only
-logger = logging.getLogger(__name__)
+logger = get_logger()
class EIPBootstrapper(AbstractBootstrapper):
diff --git a/src/leap/bitmask/services/eip/eipconfig.py b/src/leap/bitmask/services/eip/eipconfig.py
index f4d6b216..43328af9 100644
--- a/src/leap/bitmask/services/eip/eipconfig.py
+++ b/src/leap/bitmask/services/eip/eipconfig.py
@@ -26,12 +26,13 @@ import ipaddr
from leap.bitmask.config import flags
from leap.bitmask.config.providerconfig import ProviderConfig
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.services import ServiceConfig
from leap.bitmask.services.eip.eipspec import get_schema
from leap.bitmask.util import get_path_prefix
from leap.common.check import leap_assert, leap_assert_type
-logger = logging.getLogger(__name__)
+logger = get_logger()
def get_eipconfig_path(domain, relative=True):
@@ -160,12 +161,17 @@ class VPNGatewaySelector(object):
def get_gateways_country_code(self):
"""
Return a dict with ipaddress -> country code mapping.
+ Return None if there are no locations specified.
- :rtype: dict
+ :rtype: dict or None
"""
country_codes = {}
locations = self._eipconfig.get_locations()
+
+ if not locations:
+ return
+
gateways = self._eipconfig.get_gateways()
for idx, gateway in enumerate(gateways):
@@ -302,6 +308,24 @@ class EIPConfig(ServiceConfig):
logger.error("Invalid ip address in config: %s" % (ip_addr_str,))
return None
+ def get_gateway_ports(self, index=0):
+ """
+ Return the ports of the gateway.
+
+ :param index: the gateway number to get the ports from
+ :type index: int
+
+ :rtype: list of int
+ """
+ gateways = self.get_gateways()
+ leap_assert(len(gateways) > 0, "We don't have any gateway!")
+ if index > len(gateways):
+ index = 0
+ logger.warning("Provided an unknown gateway index %s, " +
+ "defaulting to 0")
+
+ return gateways[index]["capabilities"]["ports"]
+
def get_client_cert_path(self,
providerconfig=None,
about_to_download=False):
diff --git a/src/leap/bitmask/services/eip/linuxvpnlauncher.py b/src/leap/bitmask/services/eip/linuxvpnlauncher.py
index a3ab408b..cf14a8f9 100644
--- a/src/leap/bitmask/services/eip/linuxvpnlauncher.py
+++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py
@@ -18,11 +18,11 @@
Linux VPN launcher implementation.
"""
import commands
-import logging
import os
import sys
from leap.bitmask.config import flags
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.util.privilege_policies import LinuxPolicyChecker
from leap.bitmask.util.privilege_policies import NoPolkitAuthAgentAvailable
from leap.bitmask.util.privilege_policies import NoPkexecAvailable
@@ -31,7 +31,7 @@ from leap.bitmask.services.eip.vpnlauncher import VPNLauncherException
from leap.bitmask.util import get_path_prefix, force_eval
from leap.bitmask.util import first
-logger = logging.getLogger(__name__)
+logger = get_logger()
COM = commands
diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py
index 72e19413..c48f857c 100644
--- a/src/leap/bitmask/services/eip/vpnlauncher.py
+++ b/src/leap/bitmask/services/eip/vpnlauncher.py
@@ -19,7 +19,6 @@ Platform independant VPN launcher interface.
"""
import getpass
import hashlib
-import logging
import os
import stat
@@ -27,6 +26,7 @@ from abc import ABCMeta, abstractmethod
from functools import partial
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
@@ -35,7 +35,7 @@ from leap.bitmask.util import force_eval
from leap.common.check import leap_assert, leap_assert_type
-logger = logging.getLogger(__name__)
+logger = get_logger()
class VPNLauncherException(Exception):
@@ -106,12 +106,15 @@ class VPNLauncher(object):
UP_SCRIPT = None
DOWN_SCRIPT = None
+ PREFERRED_PORTS = ("443", "80", "53", "1194")
+
@classmethod
@abstractmethod
def get_gateways(kls, eipconfig, providerconfig):
"""
- Return the selected gateways for a given provider, looking at the EIP
- config file.
+ Return a list with the selected gateways for a given provider, looking
+ at the EIP config file.
+ Each item of the list is a tuple containing (gateway, port).
:param eipconfig: eip configuration object
:type eipconfig: EIPConfig
@@ -122,21 +125,37 @@ class VPNLauncher(object):
:rtype: list
"""
gateways = []
+
settings = Settings()
domain = providerconfig.get_domain()
gateway_conf = settings.get_selected_gateway(domain)
gateway_selector = VPNGatewaySelector(eipconfig)
if gateway_conf == GATEWAY_AUTOMATIC:
- gateways = gateway_selector.get_gateways()
+ gws = gateway_selector.get_gateways()
else:
- gateways = [gateway_conf]
+ gws = [gateway_conf]
- if not gateways:
+ if not gws:
logger.error('No gateway was found!')
raise VPNLauncherException('No gateway was found!')
- logger.debug("Using gateways ips: {0}".format(', '.join(gateways)))
+ for idx, gw in enumerate(gws):
+ ports = eipconfig.get_gateway_ports(idx)
+
+ the_port = "1194" # default port
+
+ # pick the port preferring this order:
+ for port in kls.PREFERRED_PORTS:
+ if port in ports:
+ the_port = port
+ break
+ else:
+ continue
+
+ gateways.append((gw, the_port))
+
+ logger.debug("Using gateways (ip, port): {0!r}".format(gateways))
return gateways
@classmethod
@@ -194,8 +213,8 @@ class VPNLauncher(object):
gateways = kls.get_gateways(eipconfig, providerconfig)
- for gw in gateways:
- args += ['--remote', gw, '1194', 'udp']
+ for ip, port in gateways:
+ args += ['--remote', ip, port, 'udp']
args += [
'--client',
diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py
index 8dc6021f..586b50f5 100644
--- a/src/leap/bitmask/services/eip/vpnprocess.py
+++ b/src/leap/bitmask/services/eip/vpnprocess.py
@@ -18,7 +18,6 @@
VPN Manager, spawned in a custom processProtocol.
"""
import commands
-import logging
import os
import shutil
import socket
@@ -39,6 +38,7 @@ except ImportError:
from leap.bitmask.config import flags
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.eipconfig import EIPConfig
@@ -47,13 +47,12 @@ from leap.bitmask.util import first, force_eval
from leap.bitmask.platform_init import IS_MAC, IS_LINUX
from leap.common.check import leap_assert, leap_assert_type
-logger = logging.getLogger(__name__)
-vpnlog = logging.getLogger('leap.openvpn')
-
from twisted.internet import defer, protocol, reactor
from twisted.internet import error as internet_error
from twisted.internet.task import LoopingCall
+logger = get_logger()
+
class VPNObserver(object):
"""
@@ -884,7 +883,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
"""
# truncate the newline
line = data[:-1]
- vpnlog.info(line)
+ logger.info(line)
self._vpn_observer.watch(line)
def processExited(self, reason):
@@ -961,9 +960,11 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
:rtype: list
"""
- gateways = self._launcher.get_gateways(
+ gateways_ports = self._launcher.get_gateways(
self._eipconfig, self._providerconfig)
- return gateways
+
+ # filter out ports since we don't need that info
+ return [gateway for gateway, port in gateways_ports]
# shutdown
diff --git a/src/leap/bitmask/services/eip/windowsvpnlauncher.py b/src/leap/bitmask/services/eip/windowsvpnlauncher.py
index 3f1ed43b..aaa3e45f 100644
--- a/src/leap/bitmask/services/eip/windowsvpnlauncher.py
+++ b/src/leap/bitmask/services/eip/windowsvpnlauncher.py
@@ -17,12 +17,11 @@
"""
Windows VPN launcher implementation.
"""
-import logging
-
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.services.eip.vpnlauncher import VPNLauncher
from leap.common.check import leap_assert
-logger = logging.getLogger(__name__)
+logger = get_logger()
class WindowsVPNLauncher(VPNLauncher):
diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py
index 0fb9f4fa..68197d9d 100644
--- a/src/leap/bitmask/services/mail/conductor.py
+++ b/src/leap/bitmask/services/mail/conductor.py
@@ -17,18 +17,17 @@
"""
Mail Services Conductor
"""
-import logging
-
from leap.bitmask.config import flags
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.gui import statemachines
from leap.bitmask.services.mail import connection as mail_connection
from leap.bitmask.services.mail.emailfirewall import get_email_firewall
-from leap.common.events import events_pb2 as leap_events
+from leap.common.events import catalog
from leap.common.events import register as leap_register
-logger = logging.getLogger(__name__)
+logger = get_logger()
class IMAPControl(object):
@@ -42,15 +41,12 @@ class IMAPControl(object):
self.imap_machine = None
self.imap_connection = None
- leap_register(signal=leap_events.IMAP_SERVICE_STARTED,
- callback=self._handle_imap_events,
- reqcbk=lambda req, resp: None)
- leap_register(signal=leap_events.IMAP_SERVICE_FAILED_TO_START,
- callback=self._handle_imap_events,
- reqcbk=lambda req, resp: None)
- leap_register(signal=leap_events.IMAP_CLIENT_LOGIN,
- callback=self._handle_imap_events,
- reqcbk=lambda req, resp: None)
+ leap_register(event=catalog.IMAP_SERVICE_STARTED,
+ callback=self._handle_imap_events)
+ leap_register(event=catalog.IMAP_SERVICE_FAILED_TO_START,
+ callback=self._handle_imap_events)
+ leap_register(event=catalog.IMAP_CLIENT_LOGIN,
+ callback=self._handle_imap_events)
def set_imap_connection(self, imap_connection):
"""
@@ -77,25 +73,29 @@ class IMAPControl(object):
self._backend.imap_stop_service()
- def _handle_imap_events(self, req):
+ def _handle_imap_events(self, event, content):
"""
Callback handler for the IMAP events
- :param req: Request type
- :type req: leap.common.events.events_pb2.SignalRequest
+ :param event: The event that triggered the callback.
+ :type event: str
+ :param content: The content of the event.
+ :type content: list
"""
- if req.event == leap_events.IMAP_SERVICE_STARTED:
+ if event == catalog.IMAP_SERVICE_STARTED:
self._on_imap_connected()
- elif req.event == leap_events.IMAP_SERVICE_FAILED_TO_START:
+ elif event == catalog.IMAP_SERVICE_FAILED_TO_START:
self._on_imap_failed()
- elif req.event == leap_events.IMAP_CLIENT_LOGIN:
+ elif event == catalog.IMAP_CLIENT_LOGIN:
self._on_mail_client_logged_in()
def _on_mail_client_logged_in(self):
"""
On mail client logged in, fetch incoming mail.
"""
- self._controller.imap_service_fetch()
+ # XXX needs to be adapted to the new-ish incoming mail service.
+ # Doing nothing for now, this could be moved to mail package itself.
+ logger.debug("A MUA has logged in, should react by forcing a fetch.")
def _on_imap_connecting(self):
"""
@@ -124,12 +124,10 @@ class SMTPControl(object):
self.smtp_connection = None
self.smtp_machine = None
- leap_register(signal=leap_events.SMTP_SERVICE_STARTED,
- callback=self._handle_smtp_events,
- reqcbk=lambda req, resp: None)
- leap_register(signal=leap_events.SMTP_SERVICE_FAILED_TO_START,
- callback=self._handle_smtp_events,
- reqcbk=lambda req, resp: None)
+ leap_register(event=catalog.SMTP_SERVICE_STARTED,
+ callback=self._handle_smtp_events)
+ leap_register(event=catalog.SMTP_SERVICE_FAILED_TO_START,
+ callback=self._handle_smtp_events)
def set_smtp_connection(self, smtp_connection):
"""
@@ -158,16 +156,18 @@ class SMTPControl(object):
self.smtp_connection.qtsigs.disconnecting_signal.emit()
self._backend.smtp_stop_service()
- def _handle_smtp_events(self, req):
+ def _handle_smtp_events(self, event, content):
"""
Callback handler for the SMTP events.
- :param req: Request type
- :type req: leap.common.events.events_pb2.SignalRequest
+ :param event: The event that triggered the callback.
+ :type event: str
+ :param content: The content of the event.
+ :type content: list
"""
- if req.event == leap_events.SMTP_SERVICE_STARTED:
+ if event == catalog.SMTP_SERVICE_STARTED:
self.on_smtp_connected()
- elif req.event == leap_events.SMTP_SERVICE_FAILED_TO_START:
+ elif event == catalog.SMTP_SERVICE_FAILED_TO_START:
self.on_smtp_failed()
def on_smtp_connecting(self):
@@ -262,7 +262,7 @@ class MailConductor(IMAPControl, SMTPControl):
if self._firewall is not None:
self._firewall.start()
if not offline:
- logger.debug("not starting smtp in offline mode")
+ logger.debug("Starting smtp service...")
self.start_smtp_service(download_if_needed=download_if_needed)
self.start_imap_service()
diff --git a/src/leap/bitmask/services/mail/imap.py b/src/leap/bitmask/services/mail/imap.py
index 5db18cb9..5934756d 100644
--- a/src/leap/bitmask/services/mail/imap.py
+++ b/src/leap/bitmask/services/mail/imap.py
@@ -17,14 +17,16 @@
"""
Initialization of imap service
"""
-import logging
import os
import sys
+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
-logger = logging.getLogger(__name__)
+logger = get_logger()
# The name of the environment variable that has to be
# set to override the default time value, in seconds.
@@ -49,6 +51,9 @@ def get_mail_check_period():
logger.warning("Unhandled error while getting %s: %r" % (
INCOMING_CHECK_PERIOD_ENV,
exc))
+
+ if period is None:
+ period = INCOMING_CHECK_PERIOD
return period
@@ -61,12 +66,34 @@ def start_imap_service(*args, **kwargs):
from leap.bitmask.config import flags
logger.debug('Launching imap service')
- override_period = get_mail_check_period()
- if override_period:
- kwargs['check_period'] = override_period
-
if flags.MAIL_LOGFILE:
log.startLogging(open(flags.MAIL_LOGFILE, 'w'))
log.startLogging(sys.stdout)
return imap.run_service(*args, **kwargs)
+
+
+def start_incoming_mail_service(keymanager, soledad, imap_factory, userid):
+ """
+ Initalizes and starts the incomming mail service.
+
+ :returns: a Deferred that will be fired with the IncomingMail instance
+ """
+ def setUpIncomingMail(inbox):
+ incoming_mail = IncomingMail(
+ keymanager,
+ soledad,
+ inbox.collection,
+ 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))
+ d.addCallback(setUpIncomingMail)
+ return d
diff --git a/src/leap/bitmask/services/mail/imapcontroller.py b/src/leap/bitmask/services/mail/imapcontroller.py
index d0bf4c34..e5313477 100644
--- a/src/leap/bitmask/services/mail/imapcontroller.py
+++ b/src/leap/bitmask/services/mail/imapcontroller.py
@@ -17,12 +17,10 @@
"""
IMAP service controller.
"""
-import logging
-
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.services.mail import imap
-
-logger = logging.getLogger(__name__)
+logger = get_logger()
class IMAPController(object):
@@ -43,9 +41,12 @@ class IMAPController(object):
self._soledad = soledad
self._keymanager = keymanager
- self.imap_service = None
+ # XXX: this should live in its own controller
+ # or, better, just be managed by a composite Mail Service in
+ # leap.mail.
self.imap_port = None
self.imap_factory = None
+ self.incoming_mail_service = None
def start_imap_service(self, userid, offline=False):
"""
@@ -58,46 +59,53 @@ class IMAPController(object):
"""
logger.debug('Starting imap service')
- self.imap_service, self.imap_port, \
- self.imap_factory = imap.start_imap_service(
- self._soledad,
- self._keymanager,
- userid=userid,
- offline=offline)
+ self.imap_port, self.imap_factory = imap.start_imap_service(
+ self._soledad,
+ userid=userid)
+
+ def start_incoming_service(incoming_mail):
+ d = incoming_mail.startService()
+ d.addCallback(lambda started: incoming_mail)
+ return d
+
+ def assign_incoming_service(incoming_mail):
+ self.incoming_mail_service = incoming_mail
+ return incoming_mail
if offline is False:
- logger.debug("Starting loop")
- self.imap_service.start_loop()
+ d = imap.start_incoming_mail_service(
+ self._keymanager,
+ self._soledad,
+ self.imap_factory,
+ userid)
+ d.addCallback(start_incoming_service)
+ d.addCallback(assign_incoming_service)
+ d.addErrback(lambda f: logger.error(f.printTraceback()))
- def stop_imap_service(self, cv):
+ def stop_imap_service(self):
"""
Stop IMAP service (fetcher, factory and port).
-
- :param cv: A condition variable to which we can signal when imap
- indeed stops.
- :type cv: threading.Condition
"""
- if self.imap_service is not None:
+ if self.incoming_mail_service is not None:
# Stop the loop call in the fetcher
- self.imap_service.stop()
- self.imap_service = None
+ # XXX BUG -- the deletion of the reference should be made
+ # after stopService() triggers its deferred (ie, cleanup has been
+ # made)
+ self.incoming_mail_service.stopService()
+ self.incoming_mail_service = None
+
+ if self.imap_port is not None:
# Stop listening on the IMAP port
self.imap_port.stopListening()
# Stop the protocol
- self.imap_factory.theAccount.closed = True
- self.imap_factory.doStop(cv)
- else:
- # Release the condition variable so the caller doesn't have to wait
- cv.acquire()
- cv.notify()
- cv.release()
+ self.imap_factory.doStop()
def fetch_incoming_mail(self):
"""
Fetch incoming mail.
"""
- if self.imap_service:
+ if self.incoming_mail_service is not None:
logger.debug('Client connected, fetching mail...')
- self.imap_service.fetch()
+ self.incoming_mail_service.fetch()
diff --git a/src/leap/bitmask/services/mail/plumber.py b/src/leap/bitmask/services/mail/plumber.py
index 1af65c5d..43203f0c 100644
--- a/src/leap/bitmask/services/mail/plumber.py
+++ b/src/leap/bitmask/services/mail/plumber.py
@@ -17,6 +17,8 @@
"""
Utils for manipulating local mailboxes.
"""
+# TODO --- this module has not yet catched up with 0.9.0
+
import getpass
import logging
import os
@@ -28,17 +30,15 @@ from twisted.internet import defer
from leap.bitmask.backend.settings import Settings
from leap.bitmask.config.providerconfig import ProviderConfig
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.provider import get_provider_path
from leap.bitmask.services.soledad.soledadbootstrapper import get_db_paths
from leap.bitmask.util import flatten, get_path_prefix
-from leap.mail.imap.account import SoledadBackedAccount
-from leap.mail.imap.memorystore import MemoryStore
-from leap.mail.imap.soledadstore import SoledadStore
+from leap.mail.imap.account import IMAPAccount
from leap.soledad.client import Soledad
-logger = logging.getLogger(__name__)
-logger.setLevel(logging.DEBUG)
+logger = get_logger()
def initialize_soledad(uuid, email, passwd,
@@ -140,11 +140,7 @@ class MBOXPlumber(object):
self.sol = initialize_soledad(
self.uuid, self.userid, self.passwd,
secrets, localdb, "/tmp", "/tmp")
- memstore = MemoryStore(
- permanent_store=SoledadStore(self.sol),
- write_period=5)
- self.acct = SoledadBackedAccount(self.userid, self.sol,
- memstore=memstore)
+ self.acct = IMAPAccount(self.userid, self.sol)
return True
#
diff --git a/src/leap/bitmask/services/mail/smtpbootstrapper.py b/src/leap/bitmask/services/mail/smtpbootstrapper.py
index 9dd61488..cd871803 100644
--- a/src/leap/bitmask/services/mail/smtpbootstrapper.py
+++ b/src/leap/bitmask/services/mail/smtpbootstrapper.py
@@ -17,11 +17,11 @@
"""
SMTP bootstrapping
"""
-import logging
import os
from leap.bitmask.config.providerconfig import ProviderConfig
from leap.bitmask.crypto.certs import download_client_cert
+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
@@ -31,7 +31,7 @@ from leap.common import certs as leap_certs
from leap.common.check import leap_assert
from leap.common.files import check_and_fix_urw_only
-logger = logging.getLogger(__name__)
+logger = get_logger()
class NoSMTPHosts(Exception):
@@ -120,6 +120,7 @@ class SMTPBootstrapper(AbstractBootstrapper):
self._provider_config, about_to_download=True)
from leap.mail.smtp import setup_smtp_gateway
+
self._smtp_service, self._smtp_port = setup_smtp_gateway(
port=2013,
userid=self._userid,
@@ -152,7 +153,7 @@ class SMTPBootstrapper(AbstractBootstrapper):
self._provider_config = ProviderConfig.get_provider_config(domain)
self._keymanager = keymanager
self._smtp_config = SMTPConfig()
- self._userid = userid
+ self._userid = str(userid)
self._download_if_needed = download_if_needed
try:
diff --git a/src/leap/bitmask/services/mail/smtpconfig.py b/src/leap/bitmask/services/mail/smtpconfig.py
index 09f90314..2d8de411 100644
--- a/src/leap/bitmask/services/mail/smtpconfig.py
+++ b/src/leap/bitmask/services/mail/smtpconfig.py
@@ -17,16 +17,16 @@
"""
SMTP configuration
"""
-import logging
import os
from leap.bitmask.config.providerconfig import ProviderConfig
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.services import ServiceConfig
from leap.bitmask.services.mail.smtpspec import get_schema
from leap.bitmask.util import get_path_prefix
from leap.common.check import leap_assert, leap_assert_type
-logger = logging.getLogger(__name__)
+logger = get_logger()
class SMTPConfig(ServiceConfig):
diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py
index 2044a27c..57ae3849 100644
--- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py
+++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py
@@ -17,37 +17,37 @@
"""
Soledad bootstrapping
"""
-import logging
import os
import socket
import sys
-import time
-from ssl import SSLError
from sqlite3 import ProgrammingError as sqlite_ProgrammingError
from u1db import errors as u1db_errors
-from twisted.internet import threads
+from twisted.internet import defer, reactor
from zope.proxy import sameProxiedObjects
from pysqlcipher.dbapi2 import ProgrammingError as sqlcipher_ProgrammingError
from leap.bitmask.config import flags
from leap.bitmask.config.providerconfig import ProviderConfig
from leap.bitmask.crypto.srpauth import SRPAuth
+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.soledad.soledadconfig import SoledadConfig
from leap.bitmask.util import first, is_file, is_empty_file, make_address
from leap.bitmask.util import get_path_prefix
-from leap.bitmask.platform_init import IS_WIN
+from leap.bitmask.util import here
+from leap.bitmask.platform_init import IS_WIN, IS_MAC
from leap.common.check import leap_assert, leap_assert_type, leap_check
from leap.common.files import which
from leap.keymanager import KeyManager, openpgp
from leap.keymanager.errors import KeyNotFound
from leap.soledad.common.errors import InvalidAuthTokenError
-from leap.soledad.client import Soledad, BootstrapSequenceError
+from leap.soledad.client import Soledad
+from leap.soledad.client.secrets import BootstrapSequenceError
-logger = logging.getLogger(__name__)
+logger = get_logger()
"""
These mocks are replicated from imap tests and the repair utility.
@@ -59,34 +59,6 @@ soledad client, and a switch to remote sync-able mode during runtime.
"""
-class Mock(object):
- """
- A generic simple mock class
- """
- def __init__(self, return_value=None):
- self._return = return_value
-
- def __call__(self, *args, **kwargs):
- return self._return
-
-
-class MockSharedDB(object):
- """
- Mocked SharedDB object to replace in soledad before
- instantiating it in offline mode.
- """
- get_doc = Mock()
- put_doc = Mock()
- lock = Mock(return_value=('atoken', 300))
- unlock = Mock(return_value=True)
-
- def __call__(self):
- return self
-
-# TODO these exceptions could be moved to soledad itself
-# after settling this down.
-
-
class SoledadSyncError(Exception):
message = "Error while syncing Soledad"
@@ -132,10 +104,6 @@ class SoledadBootstrapper(AbstractBootstrapper):
PUBKEY_KEY = "user[public_key]"
MAX_INIT_RETRIES = 10
- MAX_SYNC_RETRIES = 10
- WAIT_MAX_SECONDS = 600
- # WAIT_STEP_SECONDS = 1
- WAIT_STEP_SECONDS = 5
def __init__(self, signaler=None):
AbstractBootstrapper.__init__(self, signaler)
@@ -145,32 +113,35 @@ class SoledadBootstrapper(AbstractBootstrapper):
self._provider_config = None
self._soledad_config = None
- self._keymanager = None
self._download_if_needed = False
self._user = ""
- self._password = ""
+ self._password = u""
self._address = ""
self._uuid = ""
self._srpauth = None
+
self._soledad = None
+ self._keymanager = None
@property
- def keymanager(self):
- return self._keymanager
+ def srpauth(self):
+ if flags.OFFLINE is True:
+ return None
+ if self._srpauth is None:
+ leap_assert(self._provider_config is not None,
+ "We need a provider config")
+ self._srpauth = SRPAuth(self._provider_config)
+ return self._srpauth
@property
def soledad(self):
return self._soledad
@property
- def srpauth(self):
- if flags.OFFLINE is True:
- return None
- leap_assert(self._provider_config is not None,
- "We need a provider config")
- return SRPAuth(self._provider_config)
+ def keymanager(self):
+ return self._keymanager
# initialization
@@ -188,14 +159,19 @@ class SoledadBootstrapper(AbstractBootstrapper):
self._address = username
self._password = password
self._uuid = uuid
- try:
- self.load_and_sync_soledad(uuid, offline=True)
- self._signaler.signal(self._signaler.soledad_offline_finished)
- except Exception as e:
+
+ def error(failure):
# TODO: we should handle more specific exceptions in here
- logger.exception(e)
+ logger.exception(failure.value)
self._signaler.signal(self._signaler.soledad_offline_failed)
+ d = self.load_and_sync_soledad(uuid, offline=True)
+ d.addCallback(
+ lambda _: self._signaler.signal(
+ self._signaler.soledad_offline_finished))
+ d.addErrback(error)
+ return d
+
def _get_soledad_local_params(self, uuid, offline=False):
"""
Return the locals parameters needed for the soledad initialization.
@@ -229,25 +205,19 @@ class SoledadBootstrapper(AbstractBootstrapper):
:return: server_url, cert_file
:rtype: tuple
"""
- if uuid is None:
- uuid = self.srpauth.get_uuid()
-
if offline is True:
server_url = "http://localhost:9999/"
cert_file = ""
else:
+ if uuid is None:
+ uuid = self.srpauth.get_uuid()
server_url = self._pick_server(uuid)
cert_file = self._provider_config.get_ca_cert_path()
return server_url, cert_file
- def _soledad_sync_errback(self, failure):
- failure.trap(InvalidAuthTokenError)
- # in the case of an invalid token we have already turned off mail and
- # warned the user in _do_soledad_sync()
-
def _do_soledad_init(self, uuid, secrets_path, local_db_path,
- server_url, cert_file, token):
+ server_url, cert_file, token, syncable):
"""
Initialize soledad, retry if necessary and raise an exception if we
can't succeed.
@@ -273,7 +243,7 @@ class SoledadBootstrapper(AbstractBootstrapper):
logger.debug("Trying to init soledad....")
self._try_soledad_init(
uuid, secrets_path, local_db_path,
- server_url, cert_file, token)
+ server_url, cert_file, token, syncable)
logger.debug("Soledad has been initialized.")
return
except Exception as exc:
@@ -286,15 +256,19 @@ class SoledadBootstrapper(AbstractBootstrapper):
logger.exception(exc)
raise SoledadInitError()
- def load_and_sync_soledad(self, uuid=None, offline=False):
+ def load_and_sync_soledad(self, uuid=u"", offline=False):
"""
Once everthing is in the right place, we instantiate and sync
Soledad
:param uuid: the uuid of the user, used in offline mode.
- :type uuid: unicode, or None.
+ :type uuid: unicode.
:param offline: whether to instantiate soledad for offline use.
:type offline: bool
+
+ :return: A Deferred which fires when soledad is sync, or which fails
+ with SoledadInitError or SoledadSyncError
+ :rtype: defer.Deferred
"""
local_param = self._get_soledad_local_params(uuid, offline)
remote_param = self._get_soledad_server_params(uuid, offline)
@@ -302,34 +276,50 @@ class SoledadBootstrapper(AbstractBootstrapper):
secrets_path, local_db_path, token = local_param
server_url, cert_file = remote_param
+ if offline:
+ return self._load_soledad_nosync(
+ uuid, secrets_path, local_db_path, cert_file, token)
+
+ else:
+ return self._load_soledad_online(uuid, secrets_path, local_db_path,
+ server_url, cert_file, token)
+
+ def _load_soledad_online(self, uuid, secrets_path, local_db_path,
+ server_url, cert_file, token):
+ syncable = True
try:
self._do_soledad_init(uuid, secrets_path, local_db_path,
- server_url, cert_file, token)
- except SoledadInitError:
+ server_url, cert_file, token, syncable)
+ except SoledadInitError as e:
# re-raise the exceptions from try_init,
# we're currently handling the retries from the
# soledad-launcher in the gui.
- raise
+ return defer.fail(e)
leap_assert(not sameProxiedObjects(self._soledad, None),
"Null soledad, error while initializing")
- if flags.OFFLINE:
- self._init_keymanager(self._address, token)
- else:
- try:
- address = make_address(
- self._user, self._provider_config.get_domain())
- self._init_keymanager(address, token)
- self._keymanager.get_key(
- address, openpgp.OpenPGPKey,
- private=True, fetch_remote=False)
- d = threads.deferToThread(self._do_soledad_sync)
- d.addErrback(self._soledad_sync_errback)
- except KeyNotFound:
- logger.debug("Key not found. Generating key for %s" %
- (address,))
- self._do_soledad_sync()
+ address = make_address(
+ self._user, self._provider_config.get_domain())
+ syncer = Syncer(self._soledad, self._signaler)
+
+ d = self._init_keymanager(address, token)
+ d.addCallback(lambda _: syncer.sync())
+ d.addErrback(self._soledad_sync_errback)
+ return d
+
+ def _load_soledad_nosync(self, uuid, secrets_path, local_db_path,
+ cert_file, token):
+ syncable = False
+ self._do_soledad_init(uuid, secrets_path, local_db_path,
+ "", cert_file, token, syncable)
+ d = self._init_keymanager(self._address, token)
+ return d
+
+ def _soledad_sync_errback(self, failure):
+ failure.trap(InvalidAuthTokenError)
+ # in the case of an invalid token we have already turned off mail and
+ # warned the user
def _pick_server(self, uuid):
"""
@@ -355,54 +345,8 @@ class SoledadBootstrapper(AbstractBootstrapper):
logger.debug("Using soledad server url: %s" % (server_url,))
return server_url
- def _do_soledad_sync(self):
- """
- Do several retries to get an initial soledad sync.
- """
- # and now, let's sync
- sync_tries = self.MAX_SYNC_RETRIES
- step = self.WAIT_STEP_SECONDS
- max_wait = self.WAIT_MAX_SECONDS
- while sync_tries > 0:
- wait = 0
- try:
- logger.debug("Trying to sync soledad....")
- self._try_soledad_sync()
- while self.soledad.syncing:
- time.sleep(step)
- wait += step
- if wait >= max_wait:
- raise SoledadSyncError("timeout!")
- logger.debug("Soledad has been synced!")
- # so long, and thanks for all the fish
- return
- except SoledadSyncError:
- # maybe it's my connection, but I'm getting
- # ssl handshake timeouts and read errors quite often.
- # A particularly big sync is a disaster.
- # This deserves further investigation, maybe the
- # retry strategy can be pushed to u1db, or at least
- # it's something worthy to talk about with the
- # ubuntu folks.
- sync_tries += 1
- msg = "Sync failed, retrying... (retry {0} of {1})".format(
- sync_tries, self.MAX_SYNC_RETRIES)
- logger.warning(msg)
- continue
- except InvalidAuthTokenError:
- self._signaler.signal(
- self._signaler.soledad_invalid_auth_token)
- raise
- except Exception as e:
- # XXX release syncing lock
- logger.exception("Unhandled error while syncing "
- "soledad: %r" % (e,))
- break
-
- raise SoledadSyncError()
-
def _try_soledad_init(self, uuid, secrets_path, local_db_path,
- server_url, cert_file, auth_token):
+ server_url, cert_file, auth_token, syncable):
"""
Try to initialize soledad.
@@ -425,9 +369,6 @@ class SoledadBootstrapper(AbstractBootstrapper):
# (issue #3309)
encoding = sys.getfilesystemencoding()
- # XXX We should get a flag in soledad itself
- if flags.OFFLINE is True:
- Soledad._shared_db = MockSharedDB()
try:
self._soledad = Soledad(
uuid,
@@ -437,7 +378,8 @@ class SoledadBootstrapper(AbstractBootstrapper):
server_url=server_url,
cert_file=cert_file.encode(encoding),
auth_token=auth_token,
- defer_encryption=True)
+ defer_encryption=True,
+ syncable=syncable)
# XXX All these errors should be handled by soledad itself,
# and return a subclass of SoledadInitializationFailed
@@ -456,34 +398,6 @@ class SoledadBootstrapper(AbstractBootstrapper):
"Soledad: %r" % (exc,))
raise
- def _try_soledad_sync(self):
- """
- Try to sync soledad.
- Raises SoledadSyncError if not successful.
- """
- try:
- logger.debug("BOOTSTRAPPER: trying to sync Soledad....")
- # pass defer_decryption=False to get inline decryption
- # for debugging.
- self._soledad.sync(defer_decryption=True)
- except SSLError as exc:
- logger.error("%r" % (exc,))
- raise SoledadSyncError("Failed to sync soledad")
- except u1db_errors.InvalidGeneration as exc:
- logger.error("%r" % (exc,))
- raise SoledadSyncError("u1db: InvalidGeneration")
- except (sqlite_ProgrammingError, sqlcipher_ProgrammingError) as e:
- logger.exception("%r" % (e,))
- raise
- except InvalidAuthTokenError:
- # token is invalid, probably expired
- logger.error('Invalid auth token while trying to sync Soledad')
- raise
- except Exception as exc:
- logger.exception("Unhandled error while syncing "
- "soledad: %r" % (exc,))
- raise SoledadSyncError("Failed to sync soledad")
-
def _download_config(self):
"""
Download the Soledad config for the given provider
@@ -526,6 +440,9 @@ class SoledadBootstrapper(AbstractBootstrapper):
except IndexError as e:
logger.debug("Couldn't find the gpg binary!")
logger.exception(e)
+ if IS_MAC:
+ gpgbin = os.path.abspath(
+ os.path.join(here(), "apps", "mail", "gpg"))
leap_check(gpgbin is not None, "Could not find gpg binary")
return gpgbin
@@ -537,12 +454,12 @@ class SoledadBootstrapper(AbstractBootstrapper):
:type address: str
:param token: the auth token for accessing webapp.
:type token: str
+ :rtype: Deferred
"""
- srp_auth = self.srpauth
logger.debug('initializing keymanager...')
- if flags.OFFLINE is True:
- args = (address, "https://localhost", self._soledad)
+ if flags.OFFLINE:
+ nickserver_uri = "https://localhost"
kwargs = {
"ca_cert_path": "",
"api_uri": "",
@@ -551,45 +468,44 @@ class SoledadBootstrapper(AbstractBootstrapper):
"gpgbinary": self._get_gpg_bin_path()
}
else:
- args = (
- address,
- "https://nicknym.%s:6425" % (
- self._provider_config.get_domain(),),
- self._soledad
- )
+ nickserver_uri = "https://nicknym.%s:6425" % (
+ self._provider_config.get_domain(),)
kwargs = {
"token": token,
"ca_cert_path": self._provider_config.get_ca_cert_path(),
"api_uri": self._provider_config.get_api_uri(),
"api_version": self._provider_config.get_api_version(),
- "uid": srp_auth.get_uuid(),
+ "uid": self.srpauth.get_uuid(),
"gpgbinary": self._get_gpg_bin_path()
}
- try:
- self._keymanager = KeyManager(*args, **kwargs)
- except KeyNotFound:
- logger.debug('key for %s not found.' % address)
- except Exception as exc:
- logger.exception(exc)
- raise
+ self._keymanager = KeyManager(address, nickserver_uri, self._soledad,
+ **kwargs)
if flags.OFFLINE is False:
# make sure key is in server
logger.debug('Trying to send key to server...')
- try:
- self._keymanager.send_key(openpgp.OpenPGPKey)
- except KeyNotFound:
- logger.debug('No key found for %s, will generate soon.'
- % address)
- except Exception as exc:
- logger.error("Error sending key to server.")
- logger.exception(exc)
- # but we do not raise
+
+ def send_errback(failure):
+ if failure.check(KeyNotFound):
+ logger.debug(
+ 'No key found for %s, it might be because soledad not '
+ 'synced yet or it will generate it soon.' % address)
+ else:
+ logger.error("Error sending key to server.")
+ logger.exception(failure.value)
+ # but we do not raise
+
+ d = self._keymanager.send_key(openpgp.OpenPGPKey)
+ d.addErrback(send_errback)
+ return d
+ else:
+ return defer.succeed(None)
def _gen_key(self):
"""
Generates the key pair if needed, uploads it to the webapp and
nickserver
+ :rtype: Deferred
"""
leap_assert(self._provider_config is not None,
"We need a provider configuration!")
@@ -600,30 +516,31 @@ class SoledadBootstrapper(AbstractBootstrapper):
self._user, self._provider_config.get_domain())
logger.debug("Retrieving key for %s" % (address,))
- try:
- self._keymanager.get_key(
- address, openpgp.OpenPGPKey, private=True, fetch_remote=False)
- return
- except KeyNotFound:
- logger.debug("Key not found. Generating key for %s" % (address,))
-
- # generate key
- try:
- self._keymanager.gen_key(openpgp.OpenPGPKey)
- except Exception as exc:
- logger.error("Error while generating key!")
- logger.exception(exc)
- raise
-
- # send key
- try:
- self._keymanager.send_key(openpgp.OpenPGPKey)
- except Exception as exc:
- logger.error("Error while sending key!")
- logger.exception(exc)
- raise
-
- logger.debug("Key generated successfully.")
+ def if_not_found_generate(failure):
+ failure.trap(KeyNotFound)
+ logger.debug("Key not found. Generating key for %s"
+ % (address,))
+ d = self._keymanager.gen_key(openpgp.OpenPGPKey)
+ d.addCallbacks(send_key, log_key_error("generating"))
+ return d
+
+ def send_key(_):
+ d = self._keymanager.send_key(openpgp.OpenPGPKey)
+ d.addCallbacks(
+ lambda _: logger.debug("Key generated successfully."),
+ log_key_error("sending"))
+
+ def log_key_error(step):
+ def log_err(failure):
+ logger.error("Error while %s key!", (step,))
+ logger.exception(failure.value)
+ return failure
+ return log_err
+
+ d = self._keymanager.get_key(
+ address, openpgp.OpenPGPKey, private=True, fetch_remote=False)
+ d.addErrback(if_not_found_generate)
+ return d
def run_soledad_setup_checks(self, provider_config, user, password,
download_if_needed=False):
@@ -640,6 +557,8 @@ class SoledadBootstrapper(AbstractBootstrapper):
files if the have changed since the
time it was previously downloaded.
:type download_if_needed: bool
+
+ :return: Deferred
"""
leap_assert_type(provider_config, ProviderConfig)
@@ -651,25 +570,103 @@ class SoledadBootstrapper(AbstractBootstrapper):
if flags.OFFLINE:
signal_finished = self._signaler.soledad_offline_finished
- signal_failed = self._signaler.soledad_offline_failed
- else:
- signal_finished = self._signaler.soledad_bootstrap_finished
- signal_failed = self._signaler.soledad_bootstrap_failed
+ self._signaler.signal(signal_finished)
+ return defer.succeed(True)
+
+ signal_finished = self._signaler.soledad_bootstrap_finished
+ signal_failed = self._signaler.soledad_bootstrap_failed
try:
+ # XXX FIXME make this async too! (use txrequests)
+ # Also, why the fuck would we want to download it *every time*?
+ # We should be fine by using last-time config, or at least
+ # trying it.
self._download_config()
-
- # soledad config is ok, let's proceed to load and sync soledad
uuid = self.srpauth.get_uuid()
- self.load_and_sync_soledad(uuid)
-
- if not flags.OFFLINE:
- self._gen_key()
-
- self._signaler.signal(signal_finished)
except Exception as e:
# TODO: we should handle more specific exceptions in here
self._soledad = None
self._keymanager = None
- logger.exception("Error while bootstrapping Soledad: %r" % (e, ))
+ logger.exception("Error while bootstrapping Soledad: %r" % (e,))
self._signaler.signal(signal_failed)
+ return defer.succeed(None)
+
+ # soledad config is ok, let's proceed to load and sync soledad
+ d = self.load_and_sync_soledad(uuid)
+ d.addCallback(lambda _: self._gen_key())
+ d.addCallback(lambda _: self._signaler.signal(signal_finished))
+ return d
+
+
+class Syncer(object):
+ """
+ Takes care of retries, timeouts and other issues while syncing
+ """
+ # XXX: the timeout and proably all the stuff here should be moved to
+ # soledad
+
+ MAX_SYNC_RETRIES = 10
+ WAIT_MAX_SECONDS = 600
+
+ def __init__(self, soledad, signaler):
+ self._tries = 0
+ self._soledad = soledad
+ self._signaler = signaler
+
+ def sync(self):
+ self._callback_deferred = defer.Deferred()
+ self._try_sync()
+ return self._callback_deferred
+
+ def _try_sync(self):
+ logger.debug("BOOTSTRAPPER: trying to sync Soledad....")
+ # pass defer_decryption=False to get inline decryption
+ # for debugging.
+ self._sync_deferred = self._soledad.sync(defer_decryption=True)
+ self._sync_deferred.addCallbacks(self._success, self._error)
+ self._timeout_delayed_call = reactor.callLater(self.WAIT_MAX_SECONDS,
+ self._timeout)
+
+ def _success(self, result):
+ logger.debug("Soledad has been synced!")
+ self._timeout_delayed_call.cancel()
+ self._callback_deferred.callback(result)
+ # so long, and thanks for all the fish
+
+ def _error(self, failure):
+ self._timeout_delayed_call.cancel()
+ if failure.check(InvalidAuthTokenError):
+ logger.error('Invalid auth token while trying to sync Soledad')
+ self._signaler.signal(
+ self._signaler.soledad_invalid_auth_token)
+ self._callback_deferred.fail(failure)
+ elif failure.check(sqlite_ProgrammingError,
+ sqlcipher_ProgrammingError):
+ logger.exception("%r" % (failure.value,))
+ self._callback_deferred.fail(failure)
+ else:
+ logger.error("%r" % (failure.value,))
+ self._retry()
+
+ def _timeout(self):
+ # maybe it's my connection, but I'm getting
+ # ssl handshake timeouts and read errors quite often.
+ # A particularly big sync is a disaster.
+ # This deserves further investigation, maybe the
+ # retry strategy can be pushed to u1db, or at least
+ # it's something worthy to talk about with the
+ # ubuntu folks.
+ self._sync_deferred.cancel()
+ self._retry()
+
+ def _retry(self):
+ self._tries += 1
+ if self._tries < self.MAX_SYNC_RETRIES:
+ msg = "Sync failed, retrying... (retry {0} of {1})".format(
+ self._tries, self.MAX_SYNC_RETRIES)
+ logger.warning(msg)
+ self._try_sync()
+ else:
+ logger.error("Sync failed {0} times".format(self._tries))
+ self._callback_deferred.errback(
+ SoledadSyncError("Too many retries"))
diff --git a/src/leap/bitmask/services/soledad/soledadconfig.py b/src/leap/bitmask/services/soledad/soledadconfig.py
index d3cc7da4..8052bcdb 100644
--- a/src/leap/bitmask/services/soledad/soledadconfig.py
+++ b/src/leap/bitmask/services/soledad/soledadconfig.py
@@ -17,12 +17,11 @@
"""
Soledad configuration
"""
-import logging
-
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.services import ServiceConfig
from leap.bitmask.services.soledad.soledadspec import get_schema
-logger = logging.getLogger(__name__)
+logger = get_logger()
class SoledadConfig(ServiceConfig):
diff --git a/src/leap/bitmask/updater.py b/src/leap/bitmask/updater.py
new file mode 100644
index 00000000..c35eff5f
--- /dev/null
+++ b/src/leap/bitmask/updater.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+# updater.py
+# Copyright (C) 2014, 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/>.
+"""
+Updater check and download loop.
+"""
+import os
+import shutil
+import platform
+import time
+import threading
+import ConfigParser
+import tuf.client.updater
+
+from leap.bitmask.logs.utils import get_logger
+from leap.common.events import emit, catalog
+
+
+logger = get_logger()
+
+
+"""
+Supported platforms.
+
+Maps platform names from `platform.system() + "-" + platform.machine()` to the
+platform names we use in the repos.
+"""
+bundles_per_platform = {
+ "Linux-i386": "linux-i386",
+ "Linux-i686": "linux-i386",
+ "Linux-x86_64": "linux-x86_64",
+}
+
+CONFIG_PATH = "launcher.conf"
+GENERAL_SECTION = "General"
+DELAY_KEY = "updater_delay"
+
+
+class Updater(threading.Thread):
+ def __init__(self):
+ """
+ Initialize the list of mirrors, paths and other TUF dependencies from
+ the config file
+ """
+ config = ConfigParser.ConfigParser()
+ config.read(CONFIG_PATH)
+
+ if config.has_section(GENERAL_SECTION) and \
+ config.has_option(GENERAL_SECTION, DELAY_KEY):
+ self.delay = config.getint(GENERAL_SECTION, DELAY_KEY)
+ else:
+ self.delay = 60
+
+ self._load_mirrors(config)
+ if not self.mirrors:
+ logger.error("No updater mirrors found (missing or not well "
+ "formed launcher.conf)")
+
+ self.bundle_path = os.getcwd()
+ self.source_path = self.bundle_path
+ self.dest_path = os.path.join(self.bundle_path, 'tmp')
+ self.update_path = os.path.join(self.bundle_path, 'updates')
+
+ tuf.conf.ssl_certificates = "./lib/leap/common/cacert.pem"
+
+ threading.Thread.__init__(self)
+ self.daemon = True
+
+ def run(self):
+ """
+ Check for updates
+ """
+ if not self.mirrors:
+ return
+
+ while True:
+ try:
+ tuf.conf.repository_directory = os.path.join(self.bundle_path,
+ 'repo')
+
+ updater = tuf.client.updater.Updater('leap-updater',
+ self.mirrors)
+ updater.refresh()
+
+ targets = updater.all_targets()
+ updated_targets = updater.updated_targets(targets,
+ self.source_path)
+ if updated_targets:
+ logger.info("There is updates needed. Start downloading "
+ "updates.")
+ for target in updated_targets:
+ updater.download_target(target, self.dest_path)
+ self._set_permissions(target)
+ if os.path.isdir(self.dest_path):
+ if os.path.isdir(self.update_path):
+ shutil.rmtree(self.update_path)
+ shutil.move(self.dest_path, self.update_path)
+ filepath = sorted([f['filepath'] for f in updated_targets])
+ emit(catalog.UPDATER_NEW_UPDATES,
+ ", ".join(filepath))
+ logger.info("Updates ready: %s" % (filepath,))
+ return
+ except NotImplemented as e:
+ logger.error("NotImplemented: %s" % (e,))
+ return
+ except Exception as e:
+ logger.error("An unexpected error has occurred while "
+ "updating: %s" % (e,))
+ finally:
+ time.sleep(self.delay)
+
+ def _load_mirrors(self, config):
+ """
+ Retrieve the mirrors from config and place them in self.mirrors
+
+ :param config: parsed configuration file
+ :type config: ConfigParser
+ """
+ self.mirrors = {}
+ for section in config.sections():
+ if section[:6] != 'Mirror':
+ continue
+ url_prefix = config.get(section, 'url_prefix')
+ metadata_path = self._repo_path() + '/metadata'
+ targets_path = self._repo_path() + '/targets'
+ self.mirrors[section[7:]] = {'url_prefix': url_prefix,
+ 'metadata_path': metadata_path,
+ 'targets_path': targets_path,
+ 'confined_target_dirs': ['']}
+
+ def _set_permissions(self, target):
+ """
+ Walk over all the targets and set the rigt permissions on each file.
+ The permisions are stored in the custom field 'file_permissions' of the
+ TUF's targets.json
+
+ :param target: the already parsed target json
+ :type target: tuf.formats.TARGETFILES_SCHEMA
+ """
+ file_permissions_str = target["fileinfo"]["custom"]["file_permissions"]
+ file_permissions = int(file_permissions_str, 8)
+ filepath = target['filepath']
+ if filepath[0] == '/':
+ filepath = filepath[1:]
+ file_path = os.path.join(self.dest_path, filepath)
+ os.chmod(file_path, file_permissions)
+
+ def _repo_path(self):
+ """
+ Find the remote repo path deneding on the platform.
+
+ :return: the path to add to the remote repo url for the specific platform.
+ :rtype: str
+
+ :raises NotImplemented: When the system where bitmask is running is not
+ supported by the updater.
+ """
+ system = platform.system() + "-" + platform.machine()
+ if system not in bundles_per_platform:
+ raise NotImplementedError("Platform %s not supported" % (system,))
+ return bundles_per_platform[system]
diff --git a/src/leap/bitmask/util/__init__.py b/src/leap/bitmask/util/__init__.py
index e8eddd64..9853803a 100644
--- a/src/leap/bitmask/util/__init__.py
+++ b/src/leap/bitmask/util/__init__.py
@@ -20,6 +20,7 @@ Some small and handy functions.
import datetime
import itertools
import os
+import sys
from leap.bitmask.config import flags
from leap.common.config import get_path_prefix as common_get_path_prefix
@@ -154,3 +155,14 @@ def flags_to_dict():
values = dict((i, getattr(flags, i)) for i in items)
return values
+
+def here(module=None):
+ if getattr(sys, 'frozen', False):
+ # we are running in a |PyInstaller| bundle
+ return sys._MEIPASS
+ else:
+ dirname = os.path.dirname
+ if module:
+ return dirname(module.__file__)
+ else:
+ return dirname(__file__)
diff --git a/src/leap/bitmask/util/autostart.py b/src/leap/bitmask/util/autostart.py
index d7a8afb8..2000b9f5 100644
--- a/src/leap/bitmask/util/autostart.py
+++ b/src/leap/bitmask/util/autostart.py
@@ -17,14 +17,14 @@
"""
Helpers to enable/disable bitmask's autostart.
"""
-import logging
import os
from leap.bitmask.config import flags
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.platform_init import IS_LINUX
from leap.common.files import mkdir_p
-logger = logging.getLogger(__name__)
+logger = get_logger()
DESKTOP_ENTRY = """\
diff --git a/src/leap/bitmask/util/keyring_helpers.py b/src/leap/bitmask/util/keyring_helpers.py
index 2b729b65..d81f39b1 100644
--- a/src/leap/bitmask/util/keyring_helpers.py
+++ b/src/leap/bitmask/util/keyring_helpers.py
@@ -17,8 +17,6 @@
"""
Keyring helpers.
"""
-import logging
-
try:
import keyring
from keyring.backends.file import EncryptedKeyring, PlaintextKeyring
@@ -35,7 +33,8 @@ except Exception:
keyring = None
-logger = logging.getLogger(__name__)
+from leap.bitmask.logs.utils import get_logger
+logger = get_logger()
def _get_keyring_with_fallback():
diff --git a/src/leap/bitmask/util/leap_argparse.py b/src/leap/bitmask/util/leap_argparse.py
index 346caed5..bfff503e 100644
--- a/src/leap/bitmask/util/leap_argparse.py
+++ b/src/leap/bitmask/util/leap_argparse.py
@@ -19,8 +19,6 @@ Parses the command line arguments passed to the application.
"""
import argparse
-from leap.bitmask import IS_RELEASE_VERSION
-
def build_parser():
"""
@@ -38,9 +36,6 @@ def build_parser():
help='Displays Bitmask version and exits.')
# files
- parser.add_argument('-l', '--logfile', metavar="LOG FILE", nargs='?',
- action="store", dest="log_file",
- help='Optional log file.')
parser.add_argument('-m', '--mail-logfile',
metavar="MAIL LOG FILE", nargs='?',
action="store", dest="mail_log_file",
@@ -74,32 +69,32 @@ def build_parser():
help='Verbosity level for openvpn logs [1-6]')
# mail stuff
- # XXX Disabled right now since it's not tested after login refactor
- # parser.add_argument('-o', '--offline', action="store_true",
- # help='Starts Bitmask in offline mode: will not '
- # 'try to sync with remote replicas for email.')
-
- parser.add_argument('--acct', metavar="user@provider",
- nargs='?',
- action="store", dest="acct",
- help='Manipulate mailboxes for this account')
- parser.add_argument('-r', '--repair-mailboxes', default=False,
- action="store_true", dest="repair",
- help='Repair mailboxes for a given account. '
- 'Use when upgrading versions after a schema '
- 'change. Use with --acct')
- parser.add_argument('--import-maildir', metavar="/path/to/Maildir",
- nargs='?',
- action="store", dest="import_maildir",
- help='Import the given maildir. Use with the '
- '--to-mbox flag to import to folders other '
- 'than INBOX. Use with --acct')
-
- if not IS_RELEASE_VERSION:
- help_text = ("Bypasses the certificate check during provider "
- "bootstraping, for debugging development servers. "
- "Use at your own risk!")
- parser.add_argument('--danger', action="store_true", help=help_text)
+ parser.add_argument('-o', '--offline', action="store_true",
+ help='Starts Bitmask in offline mode: will not '
+ 'try to sync with remote replicas for email.')
+
+ # XXX not yet updated to new mail api for mail 0.4.0
+
+ # parser.add_argument('--acct', metavar="user@provider",
+ # nargs='?',
+ # action="store", dest="acct",
+ # help='Manipulate mailboxes for this account')
+ # parser.add_argument('-r', '--repair-mailboxes', default=False,
+ # action="store_true", dest="repair",
+ # help='Repair mailboxes for a given account. '
+ # 'Use when upgrading versions after a schema '
+ # 'change. Use with --acct')
+ # parser.add_argument('--import-maildir', metavar="/path/to/Maildir",
+ # nargs='?',
+ # action="store", dest="import_maildir",
+ # help='Import the given maildir. Use with the '
+ # '--to-mbox flag to import to folders other '
+ # 'than INBOX. Use with --acct')
+
+ help_text = ("INSECURE: Bypasses the certificate check during provider "
+ "bootstraping, for debugging development servers. "
+ "USE AT YOUR OWN RISK!")
+ parser.add_argument('--danger', action="store_true", help=help_text)
# optional cert file used to check domains with self signed certs.
parser.add_argument('--ca-cert-file', metavar="/path/to/cacert.pem",
@@ -134,8 +129,4 @@ def get_options():
parser = build_parser()
opts, unknown = parser.parse_known_args()
- # we add this option manually since it's not defined for 'release version'
- if IS_RELEASE_VERSION:
- opts.danger = False
-
return opts
diff --git a/src/leap/bitmask/util/polkit_agent.py b/src/leap/bitmask/util/polkit_agent.py
index e512bffa..f6c7b4ca 100644
--- a/src/leap/bitmask/util/polkit_agent.py
+++ b/src/leap/bitmask/util/polkit_agent.py
@@ -17,14 +17,14 @@
"""
Daemonizes polkit authentication agent.
"""
-import logging
import os
import subprocess
import daemon
# TODO --- logger won't work when daemoninzed. Log to syslog instead?
-logger = logging.getLogger(__name__)
+from leap.bitmask.logs.utils import get_logger
+logger = get_logger()
POLKIT_PATHS = (
'/usr/lib/lxpolkit/lxpolkit',
diff --git a/src/leap/bitmask/util/privilege_policies.py b/src/leap/bitmask/util/privilege_policies.py
index 65132133..fd8c7c8e 100644
--- a/src/leap/bitmask/util/privilege_policies.py
+++ b/src/leap/bitmask/util/privilege_policies.py
@@ -19,7 +19,6 @@ Helpers to determine if the needed policies for privilege escalation
are operative under this client run.
"""
import commands
-import logging
import os
import subprocess
import platform
@@ -28,10 +27,11 @@ import time
from abc import ABCMeta, abstractmethod
from leap.bitmask.config import flags
+from leap.bitmask.logs.utils import get_logger
from leap.common.check import leap_assert
from leap.common.files import which
-logger = logging.getLogger(__name__)
+logger = get_logger()
class NoPolkitAuthAgentAvailable(Exception):
@@ -187,6 +187,7 @@ class LinuxPolicyChecker(PolicyChecker):
'ps aux | grep "[l]xsession"',
'ps aux | grep "[g]nome-shell"',
'ps aux | grep "[f]ingerprint-polkit-agent"',
+ 'ps aux | grep "[x]fce-polkit"',
]
is_running = [commands.getoutput(cmd) for cmd in polkit_options]
diff --git a/src/leap/bitmask/util/requirement_checker.py b/src/leap/bitmask/util/requirement_checker.py
index 37e8e693..99ef81b4 100644
--- a/src/leap/bitmask/util/requirement_checker.py
+++ b/src/leap/bitmask/util/requirement_checker.py
@@ -18,9 +18,7 @@
"""
Utility to check the needed requirements.
"""
-
import os
-import logging
from pkg_resources import (DistributionNotFound,
get_distribution,
@@ -28,7 +26,9 @@ from pkg_resources import (DistributionNotFound,
resource_stream,
VersionConflict)
-logger = logging.getLogger(__name__)
+from leap.bitmask.logs.utils import get_logger
+
+logger = get_logger()
def get_requirements():