diff options
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 +++++++++++++++++ @@ -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) @@ -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 Binary files differnew file mode 100644 index 00000000..a478cc96 --- /dev/null +++ b/data/images/menubar-mask-icon.png 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 Binary files differindex 7cc3e752..74fa0af6 100644 --- a/pkg/osx/bitmask.icns +++ b/pkg/osx/bitmask.icns 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 @@ -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 @@ -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(): |