summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKali Kaneko <kali@leap.se>2016-05-18 12:13:52 -0400
committerKali Kaneko <kali@leap.se>2016-05-18 12:13:52 -0400
commit90aa7a275c41af130dcddaefd798ae35c505b6d5 (patch)
tree95441aa18d1022eea56d9348e0c32f392bb674ea
parentaecb13feed8797acaa3f91be45a82a52b64d4723 (diff)
parentfacefed990195bd89bf1064e75b171edd084f632 (diff)
Merge tag '0.9.2'
Tag leap.bitmask version 0.9.2
-rw-r--r--.gitattributes1
-rw-r--r--.gitignore2
-rw-r--r--CHANGELOG.rst36
-rw-r--r--MANIFEST.in1
-rw-r--r--Makefile97
-rw-r--r--README.rst10
-rw-r--r--changes/VERSION_COMPAT2
-rw-r--r--changes/feature-7552_mail-help-message-on-ui1
-rw-r--r--changes/next-changelog.rst3
-rw-r--r--data/ts/en_US.ts109
-rw-r--r--docker/README.rst14
-rwxr-xr-xdocker/debian/apt-bitmask.sh17
-rwxr-xr-xdocker/leap_bootstrap.sh4
-rw-r--r--docs/HISTORY.rst (renamed from HISTORY.rst)0
-rwxr-xr-xdocs/core_api_contract41
-rw-r--r--docs/dev/automating_login.rst21
-rw-r--r--docs/dev/environment.rst4
-rw-r--r--docs/dev/tests.rst2
-rw-r--r--docs/dev/translations.rst37
-rw-r--r--docs/man/bitmask-root.1.rst10
-rw-r--r--docs/man/bitmask.1.rst8
-rw-r--r--docs/pkg/debian.rst2
-rw-r--r--docs/release_checklist.wiki97
-rw-r--r--pkg/branding/bitmask-logo.svg19
-rw-r--r--pkg/branding/branding.mk2
-rw-r--r--pkg/branding/patch_pixel_logo.py57
-rw-r--r--pkg/deps.mk28
-rw-r--r--pkg/launcher/.gitignore2
-rw-r--r--pkg/launcher/Makefile15
-rw-r--r--pkg/launcher/README.rst14
-rw-r--r--pkg/launcher/bitmask-launcher.c23
-rw-r--r--pkg/leap_versions.txt8
-rwxr-xr-xpkg/linux/bitmask-launcher17
-rw-r--r--pkg/next-version1
-rwxr-xr-xpkg/osx/Bitmask.pkgproj750
-rw-r--r--pkg/osx/Makefile51
-rw-r--r--pkg/osx/README.rst48
-rw-r--r--pkg/osx/__init__.py49
-rwxr-xr-xpkg/osx/bitmask-helper430
-rwxr-xr-xpkg/osx/bitmask-wrapper3
-rw-r--r--pkg/osx/bitmask.icnsbin239193 -> 47303 bytes
-rw-r--r--pkg/osx/bitmask.pf.conf17
-rwxr-xr-xpkg/osx/client.down.sh426
-rwxr-xr-xpkg/osx/client.up.sh1521
-rw-r--r--pkg/osx/daemon/_metadata.py154
-rw-r--r--pkg/osx/daemon/daemon.py927
-rw-r--r--pkg/osx/daemon/pidfile.py67
-rw-r--r--pkg/osx/daemon/runner.py324
-rw-r--r--pkg/osx/install/ProcessNetworkChanges.plist.template16
-rwxr-xr-xpkg/osx/install/client.down.sh148
-rwxr-xr-xpkg/osx/install/client.up.sh599
-rwxr-xr-xpkg/osx/install/install-leapc.sh42
-rw-r--r--pkg/osx/install/leap-installer.platypus90
-rw-r--r--pkg/osx/install/tun.kext/Info.plist36
-rwxr-xr-xpkg/osx/post-inst.sh7
-rwxr-xr-xpkg/osx/pre-inst.sh3
-rw-r--r--pkg/osx/se.leap.bitmask-helper.plist29
-rw-r--r--pkg/pyinst/README.rst17
-rw-r--r--pkg/pyinst/bitmask.spec41
-rw-r--r--pkg/pyinst/bitmask.spec.orig38
l---------pkg/pyinst/bitmask_cli1
-rw-r--r--pkg/pyinst/bitmask_cli.spec32
l---------pkg/pyinst/bitmaskd1
-rw-r--r--pkg/pyinst/bitmaskd.spec33
-rw-r--r--pkg/pyinst/multi.spec75
-rw-r--r--pkg/pyinst/pyinst-build.mk98
-rw-r--r--pkg/pyinst/qt.conf2
-rw-r--r--pkg/pyinst/trim-notes.txt12
-rw-r--r--pkg/requirements-leap.pip8
-rw-r--r--pkg/requirements-pixelated.pip5
-rwxr-xr-xpkg/scripts/bootstrap_develop.sh2
-rw-r--r--pkg/sumo-tarballs.mk25
-rw-r--r--pkg/tools/profile.mk23
-rw-r--r--pkg/version-template8
-rw-r--r--pkg/windows/Makefile25
-rw-r--r--pkg/windows/README.rst144
-rw-r--r--pkg/windows/TODO15
-rw-r--r--pkg/windows/bitmask.nis2
-rw-r--r--pkg/windows/bitmask.nsh115
-rw-r--r--pkg/windows/bitmask_client_product.nsh8
-rw-r--r--pkg/windows/bitmask_client_registry_install.nsh17
-rw-r--r--pkg/windows/docker-compose.yml42
-rwxr-xr-xpkg/windows/installer-build.sh119
-rw-r--r--pkg/windows/installer/Dockerfile17
-rwxr-xr-xpkg/windows/openvpn-build.sh63
-rw-r--r--pkg/windows/openvpn/Dockerfile17
-rwxr-xr-xpkg/windows/pyinstaller-build.sh288
-rw-r--r--pkg/windows/pyinstaller/Dockerfile105
-rw-r--r--pkg/windows/pyinstaller/pysqlcipher_setup.py.patch14
-rw-r--r--pkg/windows/pyinstaller/zlib-mingw-shared.patch10
-rw-r--r--release-notes.rst49
-rwxr-xr-xrun_tests.sh172
-rw-r--r--setup.cfg7
-rwxr-xr-xsetup.py85
-rw-r--r--src/leap/bitmask/__init__.py57
-rw-r--r--src/leap/bitmask/_version.py576
-rw-r--r--src/leap/bitmask/app.py26
-rw-r--r--src/leap/bitmask/backend/api.py4
-rw-r--r--src/leap/bitmask/backend/components.py98
-rw-r--r--src/leap/bitmask/backend/leapbackend.py13
-rw-r--r--src/leap/bitmask/backend/leapsignaler.py1
-rw-r--r--src/leap/bitmask/cli/__init__.py0
-rwxr-xr-xsrc/leap/bitmask/cli/bitmask_cli.py341
-rw-r--r--src/leap/bitmask/config/leapsettings.py8
-rw-r--r--src/leap/bitmask/core/__init__.py2
-rw-r--r--src/leap/bitmask/core/_zmq.py68
-rw-r--r--src/leap/bitmask/core/api.py54
-rw-r--r--src/leap/bitmask/core/bitmaskd.tac11
-rw-r--r--src/leap/bitmask/core/configurable.py106
-rw-r--r--src/leap/bitmask/core/dispatcher.py273
-rw-r--r--src/leap/bitmask/core/dummy.py80
-rw-r--r--src/leap/bitmask/core/flags.py1
-rw-r--r--src/leap/bitmask/core/launcher.py47
-rw-r--r--src/leap/bitmask/core/mail_services.py688
-rw-r--r--src/leap/bitmask/core/service.py209
-rw-r--r--src/leap/bitmask/core/uuid_map.py115
-rw-r--r--src/leap/bitmask/core/web/__init__.py0
-rw-r--r--src/leap/bitmask/core/web/index.html70
-rw-r--r--src/leap/bitmask/core/web/root.py0
-rw-r--r--src/leap/bitmask/core/websocket.py98
-rw-r--r--src/leap/bitmask/gui/account.py9
-rw-r--r--src/leap/bitmask/gui/advanced_key_management.py3
-rw-r--r--src/leap/bitmask/gui/app.py34
-rw-r--r--src/leap/bitmask/gui/eip_status.py2
-rw-r--r--src/leap/bitmask/gui/logwindow.py4
-rw-r--r--src/leap/bitmask/gui/mail_status.py65
-rw-r--r--src/leap/bitmask/gui/mainwindow.py72
-rw-r--r--src/leap/bitmask/gui/passwordwindow.py2
-rw-r--r--src/leap/bitmask/gui/preferences_account_page.py30
-rw-r--r--src/leap/bitmask/gui/preferences_email_page.py204
-rw-r--r--src/leap/bitmask/gui/preferences_page.py50
-rw-r--r--src/leap/bitmask/gui/preferences_vpn_page.py31
-rw-r--r--src/leap/bitmask/gui/preferenceswindow.py144
-rw-r--r--src/leap/bitmask/gui/qt_browser.py72
-rw-r--r--src/leap/bitmask/gui/statemachines.py5
-rw-r--r--src/leap/bitmask/gui/ui/mail_status.ui235
-rw-r--r--src/leap/bitmask/gui/ui/mainwindow.ui4
-rw-r--r--src/leap/bitmask/gui/ui/preferences.ui22
-rw-r--r--src/leap/bitmask/gui/ui/preferences_email_page.ui629
-rw-r--r--src/leap/bitmask/gui/ui/wizard.ui4
-rw-r--r--src/leap/bitmask/pix.py207
-rw-r--r--src/leap/bitmask/platform_init/initializers.py82
-rw-r--r--src/leap/bitmask/provider/__init__.py2
-rw-r--r--src/leap/bitmask/services/__init__.py3
-rw-r--r--src/leap/bitmask/services/eip/darwinvpnlauncher.py65
-rw-r--r--src/leap/bitmask/services/eip/vpnlauncher.py8
-rw-r--r--src/leap/bitmask/services/eip/vpnprocess.py190
-rw-r--r--src/leap/bitmask/services/mail/conductor.py25
-rw-r--r--src/leap/bitmask/services/mail/imap.py30
-rw-r--r--src/leap/bitmask/services/mail/imapcontroller.py8
-rw-r--r--src/leap/bitmask/services/mail/plumber.py3
-rw-r--r--src/leap/bitmask/services/mail/smtpbootstrapper.py42
-rw-r--r--src/leap/bitmask/services/soledad/soledadbootstrapper.py5
-rw-r--r--src/leap/bitmask/util/keyring_helpers.py5
-rw-r--r--src/leap/bitmask/util/pastebin.py4
-rwxr-xr-xthirdparty/gnupg/build_gnupg.sh68
-rw-r--r--thirdparty/openvpn/README (renamed from openvpn/README)0
-rw-r--r--thirdparty/openvpn/Sources (renamed from openvpn/Sources)0
-rwxr-xr-xthirdparty/openvpn/build.zsh.old (renamed from openvpn/build.zsh)0
-rwxr-xr-xthirdparty/openvpn/openvpn.sh123
-rw-r--r--tox.ini12
-rw-r--r--versioneer.py2058
162 files changed, 13098 insertions, 2586 deletions
diff --git a/.gitattributes b/.gitattributes
index eb8672e0..2b5f09d8 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -17,3 +17,4 @@ share/ export-ignore
src/leap.egg-info/ export-ignore
src/leap_client.egg-info export-ignore
src/leap/_version.py export-subst
+src/leap/bitmask/_version.py export-subst
diff --git a/.gitignore b/.gitignore
index 62b21fd1..eb7280a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,7 +15,6 @@ ui_*.py
!.gitattributes
bin/
build/
-core
dist/
docs/_build
docs/covhtml
@@ -39,3 +38,4 @@ data/bitmask.pro
bitmask-resources.png
docker/data
+leap_thirdparty_build
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 9af1ebcc..3974b120 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -3,6 +3,38 @@
Changelog
---------
+0.9.2 May 13 - "panis et circenses"
++++++++++++++++++++++++++++++++++++++++++++
+
+Features
+~~~~~~~~
+- `#7552 <https://leap.se/code/issues/7552>`_: Improve UI message and add some margin above the msg box.
+- `#7656 <https://leap.se/code/issues/7656>`_: Adapt to multi-user aware events.
+- `#4469 <https://leap.se/code/issues/4469>`_: Display randomly generated service token on the Help Window.
+- `#6041 <https://leap.se/code/issues/6041>`_: Write service tokens to a file to allow email clients to read them from there.
+- Use cred-based authentication on SMTP.
+- Add email panel to preferences window.
+- Ability to launch detached bitmask.core daemon, and a simplistic bitmask_cli. Not used by the main client yet.
+- Experimental support for the Pixelated User Agent, branded as "Bitmask Mail".
+
+Bugfixes
+~~~~~~~~
+- `#7568 <https://leap.se/code/issues/7568>`_: Fix typo on signal name.
+- `#7583 <https://leap.se/code/issues/7583>`_: Fix set_soledad_auth_token event callback signature.
+- `#7585 <https://leap.se/code/issues/7585>`_: Open email help link on browser.
+- `#7598 <https://leap.se/code/issues/7598>`_: Fix errback on InvalidAuthToken.
+- `#7869 <https://leap.se/code/issues/7869>`_: Redownload smtp certificate if needed.
+- Do not translate 'https' text on QLabel.
+
+Misc
+~~~~
+- PyInstaller based new style of bundles.
+
+Known Issues
+~~~~~~~~~~~~
+- `#8057 <https://leap.se/code/issues/8057>`_: Logging out twice produces a segfault in Qt
+- `#1236 <https://leap.se/code/issues/1236>`_: Description of the known issue corresponding with issue #1236.
+
0.9.1 November 03 - "the day of the calaca"
+++++++++++++++++++++++++++++++++++++++++++
@@ -15,8 +47,8 @@ Bugfixes
- `#7563 <https://leap.se/code/issues/7563>`_: try to look for /usr/bin/gpg1
- `#7562 <https://leap.se/code/issues/7562>`_: use zmq embedded minitornado, instead of system lib.
-0.9.0 October 28
-++++++++++++++++
+0.9.0 October 28 - "not in kansas anymore"
+++++++++++++++++++++++++++++++++++++++++++
Features
~~~~~~~~
diff --git a/MANIFEST.in b/MANIFEST.in
index 73355b99..5f1fc00a 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -12,3 +12,4 @@ include src/leap/bitmask/crypto/tests/wrongcert.pem
include src/leap/bitmask/gui/ui_*.py
include src/leap/bitmask/gui/*_rc.py
+include src/leap/bitmask/_version.py
diff --git a/Makefile b/Makefile
index 1cfeecb4..84497607 100644
--- a/Makefile
+++ b/Makefile
@@ -40,6 +40,16 @@ PYRCC = pyside-rcc
PYLUP = pyside-lupdate
LRELE = lrelease
+# pyinst dist dir
+
+DIST = dist/bitmask/
+DIST_OSX = dist/Bitmask.app/
+DIST_OSX_RES = dist/Bitmask.app/Contents/Resources/
+NEXT_VERSION = $(shell cat pkg/next-version)
+DIST_VERSION = dist/bitmask-$(NEXT_VERSION)/
+GIT_COMMIT = $(shell git rev-parse HEAD)
+LEAP_BUILD_DIR = leap_thirdparty_build/
+
#################################
# DO NOT EDIT FOLLOWING
@@ -87,88 +97,11 @@ manpages:
apidocs:
@sphinx-apidoc -o docs/api src/leap/bitmask
-do_cprofile:
- python -m cProfile -o bitmask.cprofile src/leap/bitmask/app.py --debug -N
-
-view_cprofile:
- cprofilev bitmask.cprofile
-
-mailprofile:
- gprof2dot -f pstats /tmp/leap_mail_profile.pstats -n 0.2 -e 0.2 | dot -Tpdf -o /tmp/leap_mail_profile.pdf
-
-do_lineprof:
- LEAP_PROFILE_IMAPCMD=1 LEAP_MAIL_MANHOLE=1 kernprof.py -l src/leap/bitmask/app.py --debug
-
-do_lineprof_offline:
- LEAP_PROFILE_IMAPCMD=1 LEAP_MAIL_MANHOLE=1 kernprof.py -l src/leap/bitmask/app.py --offline --debug -N
-
-view_lineprof:
- @python -m line_profiler app.py.lprof | $(EDITOR) -
-
-resource_graph:
- #./pkg/scripts/monitor_resource.zsh `ps aux | grep app.py | head -1 | awk '{print $$2}'` $(RESOURCE_TIME)
- ./pkg/scripts/monitor_resource.zsh `pgrep bitmask` $(RESOURCE_TIME)
- display bitmask-resources.png
-
-get_wheels:
- pip install --upgrade setuptools
- pip install --upgrade pip
- pip install wheel
-
-gather_wheels:
- pip wheel --wheel-dir=../wheelhouse pyzmq --build-option "--zmq=bundled"
- # because fuck u1db externals, that's why...
- pip wheel --wheel-dir=../wheelhouse --allow-external dirspec --allow-unverified dirspec --allow-external u1db --allow-unverified u1db -r pkg/requirements.pip
-
-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
- git checkout develop
-
-checkout_leapdeps_release:
- pkg/scripts/checkout_leap_versions.sh
-
-setup_without_namespace:
- awk '!/namespace_packages*/' setup.py > file && mv file setup.py
-
-sumo_tarball_release: checkout_leapdeps_release setup_without_namespace
- python setup.py sdist --sumo
- git checkout -- src/leap/__init__.py
- git checkout -- src/leap/bitmask/_version.py
- rm -rf src/leap/soledad
- git checkout -- setup.py
-
-# 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 setup_without_namespace
- python setup.py sdist --sumo # --latest
- git checkout -- src/leap/__init__.py
- git checkout -- src/leap/bitmask/_version.py
- rm -rf src/leap/soledad
- git checkout -- setup.py
-
-pyinst:
- pyinstaller -y pkg/pyinst/bitmask.spec
-
-clean_pkg:
- rm -rf build dist
+include pkg/deps.mk
+include pkg/tools/profile.mk
+include pkg/sumo-tarballs.mk
+include pkg/pyinst/pyinst-build.mk
+include pkg/branding/branding.mk
clean :
$(RM) $(COMPILED_UI) $(COMPILED_RESOURCES) $(COMPILED_UI:.py=.pyc) $(COMPILED_RESOURCES:.py=.pyc)
diff --git a/README.rst b/README.rst
index 0d686350..ab30951f 100644
--- a/README.rst
+++ b/README.rst
@@ -8,6 +8,9 @@ Bitmask
.. image:: https://img.shields.io/badge/IRC-leap-blue.svg
:target: http://webchat.freenode.net/?channels=%23leap&uio=d4
:alt: IRC
+.. image:: https://img.shields.io/badge/IRC-bitmask_(es)-blue.svg
+ :target: http://webchat.freenode.net/?channels=%23bitmask-es&uio=d4
+ :alt: IRC-es
**Bitmask** is the multiplatform desktop client for the services offered by
`the LEAP Platform`_.
@@ -37,6 +40,13 @@ Bitmask is released under the terms of the `GNU GPL version 3`_ or later.
.. _`GNU GPL version 3`: http://www.gnu.org/licenses/gpl.txt
+Bugs
+====
+
+Please report any bugs `in our bug tracker`_.
+
+.. _`in our bug tracker`: https://leap.se/code/projects/report-issues
+
Contributing
============
diff --git a/changes/VERSION_COMPAT b/changes/VERSION_COMPAT
index 660f9640..cd698ae7 100644
--- a/changes/VERSION_COMPAT
+++ b/changes/VERSION_COMPAT
@@ -11,3 +11,5 @@
#
# BEGIN DEPENDENCY LIST -------------------------
# leap.foo.bar>=x.y.z
+
+leap.keymanager >= 0.5.1
diff --git a/changes/feature-7552_mail-help-message-on-ui b/changes/feature-7552_mail-help-message-on-ui
deleted file mode 100644
index 44ab8fa7..00000000
--- a/changes/feature-7552_mail-help-message-on-ui
+++ /dev/null
@@ -1 +0,0 @@
-- Add UI message to help new mail users to get started. Closes feature #7552.
diff --git a/changes/next-changelog.rst b/changes/next-changelog.rst
index 90448d57..59f68d8d 100644
--- a/changes/next-changelog.rst
+++ b/changes/next-changelog.rst
@@ -1,4 +1,4 @@
-0.9.2 - xxx
+0.10.0 - xxx
+++++++++++++++++++++++++++++++
Please add lines to this file, they will be moved to the CHANGELOG.rst during
@@ -25,4 +25,3 @@ Misc
Known Issues
~~~~~~~~~~~~
-- `#1236 <https://leap.se/code/issues/1236>`_: Description of the known issue corresponding with issue #1236.
diff --git a/data/ts/en_US.ts b/data/ts/en_US.ts
index b6fcc5da..dffb64d3 100644
--- a/data/ts/en_US.ts
+++ b/data/ts/en_US.ts
@@ -410,7 +410,7 @@ Export canceled.</source>
</message>
<message>
<location filename="../src/leap/bitmask/gui/eip_status.py" line="739"/>
- <source>We could not find any authentication agent in your system.&lt;br/&gt;Make sure you have &lt;b&gt;polkit-gnome-authentication-agent-1&lt;/b&gt; running and try again.</source>
+ <source>We could not find any authentication agent on your system.&lt;br/&gt;Make sure you have &lt;b&gt;polkit-gnome-authentication-agent-1&lt;/b&gt; running and try again.</source>
<translation type="unfinished"></translation>
</message>
</context>
@@ -490,21 +490,11 @@ Export canceled.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/logwindow.py" line="176"/>
- <source>Sending to pastebin...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<location filename="../src/leap/bitmask/gui/logwindow.py" line="191"/>
<source>Your pastebin link &lt;a href=&apos;{0}&apos;&gt;{0}&lt;/a&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/logwindow.py" line="196"/>
- <source>Pastebin OK</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<location filename="../src/leap/bitmask/gui/logwindow.py" line="211"/>
<source>Sending logs to Pastebin failed!</source>
<translation type="unfinished"></translation>
@@ -519,6 +509,16 @@ Export canceled.</source>
<source>Maximum amount of submissions reached for today.</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <location filename="../src/leap/bitmask/gui/logwindow.py" line="176"/>
+ <source>Sending to Pastebin.com&#xe2;&#x80;&#xa6;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/bitmask/gui/logwindow.py" line="196"/>
+ <source>Pastebin is OK</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LoginWidget</name>
@@ -641,165 +641,170 @@ Export canceled.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/ui/mail_status.ui" line="47"/>
+ <location filename="../src/leap/bitmask/gui/ui/mail_status.ui" line="131"/>
<source>You must login to use encrypted email.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/ui/mail_status.ui" line="79"/>
+ <location filename="../src/leap/bitmask/gui/ui/mail_status.ui" line="50"/>
<source>Email</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="183"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="187"/>
<source>There was an unexpected problem with Soledad.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="464"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="472"/>
<source>OFF</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="212"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="216"/>
<source>Mail is OFF</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="221"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="225"/>
<source>Mail is starting</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="484"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="492"/>
<source>ON</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="225"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="229"/>
<source>Mail is ON</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="228"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="232"/>
<source>Mail is disabled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="472"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="480"/>
<source>Starting...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="262"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="266"/>
<source>Soledad has started...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="265"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="269"/>
<source>Soledad is starting, please wait...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="331"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="335"/>
<source>Found key! Starting mail...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="339"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="343"/>
<source>Finished generating key!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="341"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="345"/>
<source>Starting mail...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="370"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="374"/>
<source>SMTP failed to start, check the logs.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="442"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="450"/>
<source>About to start, please wait...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="449"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="457"/>
<source>Disabled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="154"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="158"/>
<source>{0}: OFF</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="491"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="508"/>
<source>You must be logged in to use {0}.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="337"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="341"/>
<source>Generating new key, this may take a few minutes.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="429"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="437"/>
<source>{0} Unread Emails in your Inbox</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="433"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="441"/>
<source>1 Unread Email in your Inbox</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="478"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="486"/>
<source>Disconnecting...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="501"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="518"/>
<source>Invalid auth token, try logging in again.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="329"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="333"/>
<source>Initial sync in progress, please wait...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="270"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="274"/>
<source>Sync: downloading ({0:02}%)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="274"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="278"/>
<source>Sync: download completed.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="279"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="283"/>
<source>Sync: uploading ({0:02}%)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="283"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="287"/>
<source>Sync: upload complete.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="291"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="295"/>
<source>Sync: completed.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mail_status.py" line="334"/>
+ <location filename="../src/leap/bitmask/gui/mail_status.py" line="338"/>
<source>Key not found...</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <location filename="../src/leap/bitmask/gui/ui/mail_status.ui" line="93"/>
+ <source>Bitmask is ready to encrypt your email. Go to &lt;a href=&quot;https://bitmask.net/en/help/email&quot;&gt;https://bitmask.net/en/help/email&lt;/a&gt; for email application setup instructions.</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>MainWindow</name>
@@ -1060,13 +1065,13 @@ Export canceled.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1033"/>
- <source>Version: &lt;b&gt;{ver}&lt;/b&gt; ({ver_hash})&lt;br&gt;&lt;br&gt;{greet}Bitmask is the Desktop client application for the LEAP platform, supporting encrypted internet proxy.&lt;br&gt;&lt;br&gt;LEAP is a non-profit dedicated to giving all internet users access to secure communication. Our focus is on adapting encryption technology to make it easy to use and widely available.&lt;br&gt;&lt;br&gt;&lt;a href=&apos;https://leap.se&apos;&gt;More about LEAP&lt;/a&gt;</source>
+ <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1063"/>
+ <source>Alternatively, you can manually configure your mail client to use Bitmask Email with these options:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1063"/>
- <source>Alternatively, you can manually configure your mail client to use Bitmask Email with these options:</source>
+ <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1033"/>
+ <source>Version: &lt;b&gt;{ver}&lt;/b&gt; ({ver_hash})&lt;br&gt;&lt;br&gt;{greet}Bitmask is the Desktop client application for the LEAP platform, supporting Encrypted Internet Proxy and &lt;a href=&apos;https://bitmask.net/help/email&apos;&gt; Encrypted Email&lt;/a&gt;.&lt;br&gt;&lt;br&gt;LEAP is a non-profit dedicated to giving all internet users access to secure communication. Our focus is on adapting encryption technology to make it easy to use and widely available.&lt;br&gt;&lt;br&gt;&lt;a href=&apos;https://leap.se&apos;&gt;More about LEAP&lt;/a&gt;</source>
<translation type="unfinished"></translation>
</message>
</context>
@@ -1598,11 +1603,6 @@ Export canceled.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/bitmask/platform_init/initializers.py" line="468"/>
- <source>Missing Bitmask helpers</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<location filename="../src/leap/bitmask/platform_init/initializers.py" line="209"/>
<source>No polkit agent running</source>
<translation type="unfinished"></translation>
@@ -1612,6 +1612,11 @@ Export canceled.</source>
<source>There is no polkit agent running and it is needed to run the Bitmask services.&lt;br&gt;Take a look at the &lt;a href=&quot;https://leap.se/en/docs/client/known-issues&quot;&gt;known issues&lt;/a&gt; page</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <location filename="../src/leap/bitmask/platform_init/initializers.py" line="468"/>
+ <source>Missing Bitmask helper files</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>msgstr</name>
diff --git a/docker/README.rst b/docker/README.rst
index dcad0ac6..4a2d08e7 100644
--- a/docker/README.rst
+++ b/docker/README.rst
@@ -6,11 +6,9 @@ Here we have several tools that leverages docker to ease Bitmask testing.
``bitmask-docker.sh`` is a helper script to ``build`` and ``run`` the bitmask app,
here is an example usage::
- $ ./bitmask-docker build # build docker image
- $ ./bitmask-docker init ro bitmask-nightly.json # initialize all the stuff needed
- # ....
- $ ./bitmask-docker.sh run
-
+ $ ./bitmask-docker.sh build # build docker image
+ $ ./bitmask-docker.sh init bitmask-nightly.json # initialize (takes time)
+ $ ./bitmask-docker.sh run # run bitmask in docker
``bitmask-nightly.json`` is the version specifier for each bitmask component that
will be used to run bitmask.
@@ -21,10 +19,10 @@ will be used to run bitmask.
python dependencies, running bitmask, etc.
-debian/
+Debian/
-------
-``apt-bitmask.sh`` script that installs bitmask from the debian packages.
+``apt-bitmask.sh`` script that installs bitmask from the Debian packages.
``bitmask-on-docker.sh`` installs bitmask and runs it in a dummy X server,
waits a little and takes a screenshot.
@@ -32,7 +30,7 @@ waits a little and takes a screenshot.
``leap-experimental.key`` is needed by ``apt-bitmask.sh`` to ``apt-key add``
and verify apt sources.
-``run-docker-for-bitmask.sh`` is a helper script that runs an ubuntu/debian
+``run-docker-for-bitmask.sh`` is a helper script that runs an Ubuntu/Debian
container ready to run the ``apt-bitmask.sh`` command, it does (among other
stuff) X11 forwarding to display Bitmask UI on the host linux.
diff --git a/docker/debian/apt-bitmask.sh b/docker/debian/apt-bitmask.sh
index 68430aff..99c5f07e 100755
--- a/docker/debian/apt-bitmask.sh
+++ b/docker/debian/apt-bitmask.sh
@@ -20,6 +20,7 @@ distro(){
['qiana']='trusty'
['rebecca']='trusty'
['rafaela']='trusty'
+ ['sana']='jessie'
)
# if name is in the above list -> replace
@@ -30,15 +31,16 @@ distro(){
is_supported(){
distros=(
- 'wheezy' # Debian 7 - stable
+ # 'wheezy' # Debian 7 - stable
'jessie' # Debian 8 - testing
'sid' # Debian unstable
- 'quantal' # Ubuntu 12.10
- 'raring' # Ubuntu 13.04
- 'saucy' # Ubuntu 13.10
- 'trusty' # Ubuntu 14.04
- 'utopic' # Ubuntu 14.10
+ # 'quantal' # Ubuntu 12.10
+ # 'raring' # Ubuntu 13.04
+ # 'saucy' # Ubuntu 13.10
+ # 'trusty' # Ubuntu 14.04
+ # 'utopic' # Ubuntu 14.10
'vivid' # Ubuntu 15.04
+ 'wily' # Ubuntu 15.10
)
my_distro=`distro`
@@ -115,8 +117,7 @@ else # $REPO == 'experimental'
apt-key add leap-experimental.key
fi
-echo "deb http://deb.leap.se/$REPO $DISTRO main" > /etc/apt/sources.list.d/bitmask.list
-echo "deb-src http://deb.leap.se/$REPO $DISTRO main" >> /etc/apt/sources.list.d/bitmask.list
+echo "deb http://deb.bitmask.net/$REPO $DISTRO main" > /etc/apt/sources.list.d/bitmask.list
apt-get update
apt-get install -y bitmask
diff --git a/docker/leap_bootstrap.sh b/docker/leap_bootstrap.sh
index 4f553ee7..a717ab6d 100755
--- a/docker/leap_bootstrap.sh
+++ b/docker/leap_bootstrap.sh
@@ -271,6 +271,10 @@ EOF
install_dependencies
setup_develop
+ cd $REPOS_ROOT/bitmask_client/
+ make
+ cd -
+
set +x
echo "${cc_green}Status: $status done!${cc_normal}"
}
diff --git a/HISTORY.rst b/docs/HISTORY.rst
index ba0df9ef..ba0df9ef 100644
--- a/HISTORY.rst
+++ b/docs/HISTORY.rst
diff --git a/docs/core_api_contract b/docs/core_api_contract
new file mode 100755
index 00000000..b70fb8fa
--- /dev/null
+++ b/docs/core_api_contract
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# api_contract.py
+# Copyright (C) 2016 LEAP Encryption Acess Project
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Display a human-readable representation of the methods that compound the public
+api for Bitmask Core.
+
+The values are meant to be type annotations.
+"""
+
+if __name__ == "__main__":
+ from leap.bitmask.core.service import BitmaskBackend
+ from leap.bitmask.core import api
+ backend = BitmaskBackend()
+
+ print '========= Bitmask Core API =================='
+ print
+
+ for key in api.registry:
+ human_key = key.replace('do_', '').lower()
+ value = api.registry[key]
+
+ print("{}:\t\t{}".format(
+ human_key,
+ ' '.join([x for x in value])))
+ print
+ print '============================================='
diff --git a/docs/dev/automating_login.rst b/docs/dev/automating_login.rst
new file mode 100644
index 00000000..7017dd8e
--- /dev/null
+++ b/docs/dev/automating_login.rst
@@ -0,0 +1,21 @@
+.. _automating_login:
+
+Automating login
+================
+
+There's an annoying bug with python keyring module, that makes the 'remember
+login' checkbox non functional.
+
+That, and the need to script end-to-end tests with the client inside a docker
+environment, made us put a mechanism to pass credentials via environment
+variables.
+
+To automate login, set BITMASK_CREDENTIALS env var::
+
+ BITMASK_CREDENTIALS=/tmp/secrets.conf bitmask --debug
+
+where the pointed file looks like this::
+
+ [Credentials]
+ username = user@provider
+ password = mypass
diff --git a/docs/dev/environment.rst b/docs/dev/environment.rst
index a3184b01..99600ec8 100644
--- a/docs/dev/environment.rst
+++ b/docs/dev/environment.rst
@@ -34,7 +34,7 @@ Bitmask depends on these base libraries:
Debian
^^^^^^
-In debian-based systems, you can get everything you need:
+In Debian-based systems, you can get everything you need:
.. include:: quickstart.rst
:start-after: begin-debian-deps
@@ -82,7 +82,7 @@ Avoid compiling PySide inside a virtualenv
If you attempt to install PySide inside a virtualenv as part of the rest of the dependencies using pip, basically it will take ages to compile.
-As a workaround, you can run the following script after creating your virtualenv. It will symlink to your global PySide installation (*this is the recommended way if you are running a debian-based system*)::
+As a workaround, you can run the following script after creating your virtualenv. It will symlink to your global PySide installation (*this is the recommended way if you are running a Debian-based system*)::
(bitmask)$ pkg/postmkvenv.sh
diff --git a/docs/dev/tests.rst b/docs/dev/tests.rst
index d55c206a..bdefcfdc 100644
--- a/docs/dev/tests.rst
+++ b/docs/dev/tests.rst
@@ -16,7 +16,7 @@ Testing dependencies
have a look at ``pkg/test-requirements.pip``
The ``./run_tests.sh`` command should install all of them in your virtualenv for you.
-If you prefer to install them system wide, this should do in a debian system::
+If you prefer to install them system wide, this should do in a Debian system::
$ apt-get install python-nose python-mock python-coverage
diff --git a/docs/dev/translations.rst b/docs/dev/translations.rst
new file mode 100644
index 00000000..58a2a8fb
--- /dev/null
+++ b/docs/dev/translations.rst
@@ -0,0 +1,37 @@
+.. _translations:
+
+Internationalization
+====================
+
+This part of the documentation covers the localization and translation of Bitmask.
+Because we want to *bring fire to the people*, in as many countries and languages as possible.
+
+Translating Bitmask PySide Application
+--------------------------------------
+
+.. raw:: html
+
+ <div><a target="_blank" style="text-decoration:none; color:black; font-size:66%" href="https://www.transifex.com/projects/p/bitmask/resource/bitmask/" title="See more information on Transifex.com">Top translations: bitmask » bitmask</a><br/><img border="0" src="https://www.transifex.com/projects/p/bitmask/resource/bitmask/chart/image_png"/><br/><a target="_blank" href="https://www.transifex.com/"><img border="0" src="https://ds0k0en9abmn1.cloudfront.net/static/charts/images/tx-logo-micro.646b0065fce6.png"/></a></div>
+
+
+For translators
+^^^^^^^^^^^^^^^
+
+We are using `transifex <http://transifex.com/projects/p/bitmask>`_ to coordinate translation efforts. If you want to contribute, just sign up there and send us your translations.
+
+
+Translating help documents
+--------------------------
+
+There are two sets of online documents that need translation. One is
+https://leap.se/docs, and the other is https://bitmask.net/help. Both are
+maintained in git repos (`leap.se <https://github.com/leapcode/leap_se/tree/master/pages/docs>`_ and `bitmask.net <https://github.com/leapcode/bitmask_help>`_).
+
+In the repo for bitmask.net there are some explanations about how to contribute
+to those repos (using the online github editor, or using git directly).
+
+
+Human interaction
+-----------------
+We're hanging out in the #leap channel on the freenode network. There's also a
+spanish channel at #bitmask-es.
diff --git a/docs/man/bitmask-root.1.rst b/docs/man/bitmask-root.1.rst
index b2f955a8..b25a3362 100644
--- a/docs/man/bitmask-root.1.rst
+++ b/docs/man/bitmask-root.1.rst
@@ -7,9 +7,9 @@ privileged helper for bitmask, the encrypted internet access toolkit.
------------------------------------------------------------------------
:Author: LEAP Encryption Access Project https://leap.se
-:Date: 2014-06-05
+:Date: 2015-11-03
:Copyright: GPLv3+
-:Version: 0.5.2
+:Version: 0.9.1
:Manual section: 1
:Manual group: General Commands Manual
@@ -42,7 +42,7 @@ openvpn
firewall
----------
+--------
**start** [GATEWAYS] Starts the firewall. GATEWAYS is a list of EIP
gateways to allow in the firewall.
@@ -53,7 +53,7 @@ firewall
fw-email
----------
+--------
**start** UID Starts the email firewall. UID is the user name or unix
id that will have access to the email.
@@ -71,4 +71,4 @@ version
BUGS
====
-Please report any bugs to https://leap.se/code
+Please report any bugs to https://leap.se/code/projects/report-issues
diff --git a/docs/man/bitmask.1.rst b/docs/man/bitmask.1.rst
index 6eae7ff5..0970b449 100644
--- a/docs/man/bitmask.1.rst
+++ b/docs/man/bitmask.1.rst
@@ -6,10 +6,10 @@ bitmask
graphical client to control LEAP, the encrypted internet access toolkit.
------------------------------------------------------------------------
-:Author: LEAP Encryption Access Project https://leap.se
-:Date: 2014-06-05
+:Author: The LEAP Encryption Access Project https://leap.se
+:Date: 2015-11-03
:Copyright: GPLv3+
-:Version: 0.5.2
+:Version: 0.9.1
:Manual section: 1
:Manual group: General Commands Manual
@@ -108,4 +108,4 @@ GUI options
BUGS
====
-Please report any bugs to https://leap.se/code
+Please report any bugs to https://leap.se/code/projects/report-issues
diff --git a/docs/pkg/debian.rst b/docs/pkg/debian.rst
index 204d4073..13976e2b 100644
--- a/docs/pkg/debian.rst
+++ b/docs/pkg/debian.rst
@@ -3,7 +3,7 @@
Debian
======
-This section documents all related to the debian package.
+This section documents all related to the Debian package.
Dependencies
diff --git a/docs/release_checklist.wiki b/docs/release_checklist.wiki
index 075591a7..95ff2204 100644
--- a/docs/release_checklist.wiki
+++ b/docs/release_checklist.wiki
@@ -1,47 +1,90 @@
-= Bitmask Release Checklist (*) =
+= Bitmask Release Checklist =
+
+ == CI check ==
* [ ] Check that all tests are passing!
- * [ ] Check that the version in bitmask_client/pkg/linux/bitmask-root is bumped if needed.
- * [ ] Tag everything
- * Should be done for the following packages, in order:
+ * [ ] Fix any broken tests.
+
+ == Version bumps and Tagging ==
+ * [ ] Update pkg/next-release
+ * [ ] Update release-notes.rst in leap.bitmask if needed.
+ * [ ] Update version in bitmask_client/pkg/linux/bitmask-root if needed.
+
+ * [ ] Tag everything. Should be done for the following packages, in order:
* [ ] 1. leap.common
* [ ] 2. leap.keymanager
* [ ] 3. leap.soledad
* [ ] 4. leap.mail
* [ ] 5. leap.bitmask
* [ ] 6. leap.mx
+
* NOTE: It's assumed that origin is the leap.se repo
+
* [ ] git fetch origin
* [ ] git tag -l, and see the latest tagged version (unless it's not a minor version bump, in which case, just bump to it)
- * [ ] Checkout release-X.Y.Z (locally, never pushed)
- * [ ] Update relnotes.txt in leap.bitmask if needed.
+ * [ ] export version: export RELEASE=0.9.0
+ * [ ] git checkout `release/0.9.x`
+ - NOTE: the release branch is created when the first release candidate
+ is tagged, after that the bugfixes and features that are meant to be
+ shipped with the specific version that we are targetting are merged in that branch
+ * [ ] git checkout -b release/$RELEASE (this is a LOCAL branch, never published).
+ * [ ] (maybe) cherry-pick specific commits
+ * [ ] (maybe) add special fixes for this release
- * [ ] Review pkg/requirements.pip for everything and update if needed (that's why the order).
+ * [ ] Review pkg/requirements.pip for everything, update if needed (that's why the order).
- See whatever has been introduced in changes/VERSION_COMPAT
- Reset changes/VERSION_COMPAT
- * [ ] git commit -av # we should add a commit message here...
-
- * [ ] Fold in changes files into the CHANGELOG
- - NOTE: For leap.soledad, the CHANGELOG entries should be divided per package (common, client, server). See older releases for reference.
- - Helper bash line: for i in $(ls changes); do cat changes/$i; echo; done
- * [ ] git rm changes/feature*; git rm changes/bug*
- * [ ] git commit -m "Fold in changes."
-
- * [ ] git checkout master && git pull origin master && git merge --no-ff release-X.Y.Z --no-edit
- * [ ] git tag -s X.Y.Z -m "Tag <package> version X.Y.Z" # (note the -s so that it's a signed tag and -m to specify the message for the tag)
- * [ ] git push origin master; git push origin X.Y.Z
- * [ ] git checkout develop && git merge master && git push origin develop
+ - Bump all the leap-requirements altogether.
+ * [ ] git commit -am "Update requirements file"
+
+ * [ ] Merge changes/next-changelog.rst into the CHANGELOG
+ - NOTE: in leap.soledad, 3 sections (common, client, server).
+ * [ ] reset changes/next-changelog.rst
+ * [ ] git commit -S -m "[pkg] Update changelog"
+
+ * [ ] git tag --sign $RELEASE -m "Tag version $RELEASE"
+
+ * If everything went ok, push the changes, and merge back into master&develop:
+ * [ ] git checkout release/0.9.x && git merge $RELEASE
+ * [ ] git push origin release/0.9.x
+ * [ ] git push origin $RELEASE
+ * [ ] git checkout master && git pull origin master && git merge --no-edit $RELEASE
+ * [ ] git checkout develop && git merge $RELEASE && git push origin develop
+
+ == Bundles ==
* [ ] Build and upload bundles
- * [ ] Use the scripts under pkg/<os>/ to build the the bundles.
- * [ ] Sign them with gpg -a --sign --detach-sign <path/to/bundle>
+ * [ ] Use 'make pyinst-linux' to build bundles.
+ * [ ] Sign: make pyinst-sign
* [ ] Upload bundle and signature to downloads.leap.se/client/<os>/Bitmask-<os>-<ver>.(tar.bz2,dmg,zip)
+ * [ ] make pyinst-upload
* [ ] Update symbolic link for latest upload and signature:
* [ ] ~/public/client/Bitmask-<os>-latest
* [ ] ~/public/client/Bitmask-<os>-latest.asc
- * [ ] Announce
- * [ ] Mail leap@lists.riseup.net
-Notes
------
-(*) this checklist kindly borrowed from tahoe-lafs documentation =)
+ === TUF: Relese candidate bundles: RC# (skipped for now) ===
+ * [ ] Upload the TUF unstable repo
+ * [ ] Upload bundle to staging for release-candidate
+ * [ ] Sign the bundles, move it to client downloads (micah)
+ * [ ] Update symlinks for -latest
+ * [ ] Fix all show stoppers
-For a good reference look at http://nvie.com/posts/a-successful-git-branching-model/
+ === TUF: Stable bundles (skipped for now) ===
+ * [ ] Upload the TUF Stable Repo to staging
+ * [ ] Upload bundle to staging for stable
+ * [ ] move and sign the TUF repo (kwadro)
+ * [ ] Sign the bundles, move it to client downloads (micah)
+ * [ ] Update symlinks for -latest
+
+ == Debian packages ==
+ * TBD...
+
+ == Pypi upload ==
+ * [ ] python setup.py sdist upload --sign -i kali@leap.se -r pypi
+
+ == Announcing ==
+ * [ ] Announce (use release-notes.rst)
+ * [ ] Mail leap@lists.riseup.net
+ * [ ] Twitter
+ * [ ] Gnusocial
+ * [ ] Post in leap.se
+ * [ ] reddit
+ * [ ] hackernews
diff --git a/pkg/branding/bitmask-logo.svg b/pkg/branding/bitmask-logo.svg
new file mode 100644
index 00000000..0eccc057
--- /dev/null
+++ b/pkg/branding/bitmask-logo.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="30.4 627.3 612 171.1" enable-background="new 30.4 627.3 612 171.1" xml:space="preserve">
+<g>
+ <path fill="#3E3B38" d="m 233.99276,705.92697 q 3.69186,0 5.59865,-1.90576 1.90679,-1.90575 1.90679,-5.62198 0,-3.66857 -1.90679,-5.57433 -1.90679,-1.95339 -5.59865,-1.95339 l -8.64139,0 0,15.05546 8.64139,0 z m 0.52741,31.11145 q 4.70611,0 7.05917,-2.33455 2.39362,-2.33456 2.39362,-7.0513 0,-4.62145 -2.35305,-6.90836 -2.35306,-2.33455 -7.09974,-2.33455 l -9.1688,0 0,18.62876 9.1688,0 z m 14.52403,-25.58477 q 5.03067,1.71519 7.78943,6.33664 2.75875,4.62146 2.75875,11.33924 0,10.29108 -5.92321,15.34132 -5.92321,5.05026 -18.01304,5.05026 l -25.92418,0 0,-71.1323 23.44941,0 q 12.61725,0 18.25646,4.47852 5.67979,4.47853 5.67979,14.34081 0,5.19318 -2.06906,8.86176 -2.06906,3.62093 -6.00435,5.38375 z" />
+ <path fill="#3E3B38" d="m 263.627,678.38881 15.61943,0 0,71.1323 -15.61943,0 0,-71.1323 z" />
+ <path fill="#3E3B38" d="m 282.6846,678.38881 55.82421,0 0,13.86437 -20.08211,0 0,57.26793 -15.61943,0 0,-57.26793 -20.12267,0 0,-13.86437 z" />
+ <path fill="#3E3B38" d="m 341.82528,678.38881 19.87926,0 13.79377,38.06745 13.87492,-38.06745 19.83869,0 0,71.1323 -14.76745,0 0,-52.02711 -13.95605,38.35332 -9.89906,0 -13.95606,-38.35332 0,52.02711 -14.80802,0 0,-71.1323 z" />
+ <path fill="#3E3B38" d="m 456.62789,736.56198 -24.42309,0 -3.85415,12.95913 -15.70056,0 22.43518,-71.1323 18.62158,0 22.43517,71.1323 -15.70055,0 -3.81358,-12.95913 z m -20.52838,-13.19736 16.5931,0 -8.27627,-28.30046 -8.31683,28.30046 z" />
+ <path fill="#3E3B38" d="m 521.57008,680.62807 0,15.05547 q -4.9901,-2.62042 -9.73679,-3.95445 -4.74667,-1.33402 -8.96595,-1.33402 -5.59865,0 -8.27626,1.81046 -2.67762,1.81047 -2.67762,5.62198 0,2.85863 1.78509,4.47852 1.82563,1.57225 6.57232,2.71571 l 6.65347,1.57225 q 10.1019,2.38219 14.36175,7.24186 4.25984,4.85968 4.25984,13.81672 0,11.76804 -5.96378,17.53295 -5.92321,5.71726 -18.13476,5.71726 -5.76093,0 -11.56242,-1.28638 -5.8015,-1.28638 -11.603,-3.81151 l 0,-15.48426 q 5.8015,3.62093 11.1973,5.47904 5.43637,1.81047 10.46704,1.81047 5.11181,0 7.83,-2.00104 2.71818,-2.00104 2.71818,-5.71727 0,-3.33507 -1.86622,-5.14554 -1.82565,-1.81046 -7.34316,-3.23977 l -6.04491,-1.57226 q -9.08767,-2.2869 -13.30694,-7.28951 -4.1787,-5.00261 -4.1787,-13.48321 0,-10.62458 5.84207,-16.34184 5.84207,-5.71728 16.79595,-5.71728 4.9901,0 10.26419,0.90524 5.27409,0.85759 10.91331,2.62041 z" />
+ <path fill="#3E3B38" d="m 529.68237,678.38881 15.61942,0 0,25.96591 22.51631,-25.96591 18.13475,0 -29.16977,33.68421 32.17195,37.44809 -19.55471,0 -24.09853,-28.01459 0,28.01459 -15.61942,0 0,-71.1323 z" />
+ <path fill="#3E3B38" d="m 105.9,627.20077 -75.5,42.70117 0,85.79883 75.5,42.69922 75.5,-42.69922 0,-85.79883 -75.5,-42.70117 z m -32.06836,63.70508 -0.002,0.0976 c 4.21029,0.3754 7.67856,2.66948 11.22851,4.17579 5.587227,2.37079 9.140367,5.17357 14.253907,6.73437 2.529723,0.7722 4.122153,0.59681 5.978523,0.63281 l 0.004,0 c -0.008,-2e-4 0.0371,3e-4 0.0527,0 1.85774,-0.0365 3.45562,0.13969 5.98632,-0.63281 5.11354,-1.5608 8.66863,-4.36357 14.25586,-6.73437 3.80753,-1.61561 7.50206,-4.16896 12.14063,-4.25196 2.62211,-0.0472 5.77411,0.4276 7.9082,2.5918 4.02062,4.0774 5.05182,10.36502 4.81836,15.70312 -0.31466,7.1947 -3.34042,14.43571 -7.94726,19.91211 -3.21011,3.8161 -7.79539,7.87699 -13.29102,8.05469 -4.31698,0.1397 -7.55863,-3.15065 -10.25195,-5.24805 -2.24435,-1.7478 -5.32382,-3.79215 -8.14453,-4.96875 -2.7399,-1.1429 -5.50339,-1.15234 -5.49219,-1.15234 l -0.002,0 -0.0137,0 -0.004,0 c 0.0694,3e-4 -2.74684,0.01 -5.486333,1.15234 -2.8212,1.1768 -5.90485,3.22155 -8.14844,4.96875 -2.6927,2.0969 -5.929147,5.38775 -10.246087,5.24805 -5.49562,-0.1777 -10.08285,-4.23859 -13.29297,-8.05469 -4.60683,-5.4764 -7.63261,-12.71741 -7.94727,-19.91211 -0.23346,-5.3381 0.79774,-11.62572 4.81836,-15.70312 1.99925,-2.0275 4.90008,-2.56866 7.40625,-2.59766 l 1.41797,-0.0156 z m 2.22852,16.40625 c -0.40471,0.008 -0.79685,0.0358 -1.18555,0.0898 -0.93568,0.1303 -2.80387,1.0037 -4.26953,1.875 -0.62959,0.3742 -1.14469,0.71251 -1.58789,1.00781 0.35713,0.6424 0.85002,1.5059 1.42578,2.4375 1.16857,1.8908 2.74114,4.05049 3.72656,4.83789 4.74234,3.695 13.045167,4.66926 19.001957,-0.43554 0.83821,-0.6427 1.5034,-1.21185 2.08593,-1.71485 -1.06931,-0.6782 -2.15957,-1.38346 -3.21875,-2.00586 -2.49446,-1.4657 -4.73528,-3.08627 -7.113277,-3.95507 -2.99838,-1.0955 -6.05879,-2.18772 -8.86523,-2.13672 z m 58.22265,0 c -2.72231,0.0254 -5.66946,1.08042 -8.56054,2.13672 -2.37801,0.8688 -4.61882,2.48927 -7.11328,3.95507 -1.05934,0.6224 -2.15146,1.32766 -3.22071,2.00586 0.58205,0.5028 1.24645,1.07235 2.08399,1.71485 1.98552,1.5232 4.5424,3.12162 6.22851,3.51562 4.73565,1.1065 8.46108,0.36762 12.77539,-3.08008 0.98542,-0.7874 2.55996,-2.94904 3.72852,-4.83984 0.57576,-0.9316 1.06865,-1.79315 1.42578,-2.43555 -0.4432,-0.2953 -0.95829,-0.63361 -1.58789,-1.00781 -1.46565,-0.8713 -3.33385,-1.7447 -4.26953,-1.875 -0.48446,-0.067 -0.98075,-0.0898 -1.49024,-0.0898 z" />
+</g>
+<polygon id="clock1" fill="#3E3B38" points="105.8,657.8 105.8,628 105.8,627.3 181.4,669.9 152.5,683.1 " />
+<polygon id="clock2" fill="#3E3B38" points="152.5,683.1 181.4,669.9 181.4,755.7 152.5,743 " />
+<polygon id="clock3" fill="#3E3B38" points="105.9,798.3 105.9,769 152.5,743 181.4,755.7 " />
+<polygon id="clock4" fill="#3E3B38" points="58.7,743.1 105.9,769 105.9,798.3 30.4,755.7 " />
+<polygon id="clock5" fill="#3E3B38" points="30.4,669.9 58.6,683.1 58.7,743.1 30.4,755.7 " />
+<polygon id="clock6" fill="#3E3B38" points="105.8,628 105.8,657.8 58.6,683.1 30.4,669.9 105.8,627.3 " />
+</svg> \ No newline at end of file
diff --git a/pkg/branding/branding.mk b/pkg/branding/branding.mk
new file mode 100644
index 00000000..6b98d40d
--- /dev/null
+++ b/pkg/branding/branding.mk
@@ -0,0 +1,2 @@
+branding-logo:
+ python pkg/branding/patch_pixel_logo.py
diff --git a/pkg/branding/patch_pixel_logo.py b/pkg/branding/patch_pixel_logo.py
new file mode 100644
index 00000000..464bb729
--- /dev/null
+++ b/pkg/branding/patch_pixel_logo.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# patch_pixelated_logo.py
+# Copyright (C) 2016 LEAP Encryption Acess Project
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Patch the Pixelated Logo in the index.html, replacing it with a rebranded
+Bitmask Logo. To be used in the pixelated_www assets distributed with the
+Bitmask bundles.
+"""
+__author__ = 'Kali Kaneko <kali@leap.se>'
+
+import os
+import sys
+
+from BeautifulSoup import BeautifulSoup
+
+
+def patch_logo(orig_path, replacement_path):
+
+ with open(orig_path, 'r') as of:
+ orig = BeautifulSoup(of.read())
+
+ with open(replacement_path, 'r') as rf:
+ new = BeautifulSoup(rf.read())
+
+ new_svg = new.find('svg')
+ old_svg = orig.find('svg')
+ old_svg.replaceWith(new_svg)
+
+ with open(orig_path, 'w') as f:
+ f.write(str(orig))
+
+
+if __name__ == "__main__":
+ here = os.path.dirname(os.path.realpath(__file__))
+ if len(sys.argv) > 1:
+ orig_path = sys.argv[1]
+ else:
+ import pixelated_www
+ orig_path = os.path.join(pixelated_www.__path__[0],
+ 'index.html')
+ assert os.path.isfile(orig_path)
+ new_path = os.path.join(here, 'bitmask-logo.svg')
+ print('>>> patching file %s with logo in %s' % (orig_path, new_path))
+ patch_logo(orig_path, new_path)
diff --git a/pkg/deps.mk b/pkg/deps.mk
new file mode 100644
index 00000000..26bed466
--- /dev/null
+++ b/pkg/deps.mk
@@ -0,0 +1,28 @@
+get_wheels:
+ pip install --upgrade setuptools
+ pip install --upgrade pip
+ pip install wheel
+
+gather_wheels:
+ pip wheel --wheel-dir=../wheelhouse pyzmq --build-option "--zmq=bundled"
+ # because fuck u1db externals, that's why...
+ pip wheel --wheel-dir=../wheelhouse --allow-external dirspec --allow-unverified dirspec --allow-external u1db --allow-unverified u1db -r pkg/requirements.pip
+
+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
+ git checkout develop
+
diff --git a/pkg/launcher/.gitignore b/pkg/launcher/.gitignore
new file mode 100644
index 00000000..60de46d7
--- /dev/null
+++ b/pkg/launcher/.gitignore
@@ -0,0 +1,2 @@
+*.o
+bitmask
diff --git a/pkg/launcher/Makefile b/pkg/launcher/Makefile
new file mode 100644
index 00000000..8dd1013d
--- /dev/null
+++ b/pkg/launcher/Makefile
@@ -0,0 +1,15 @@
+CC = gcc
+CFLAGS = -g -Wall
+STRIP = strip
+
+default: bitmask
+
+bitmask.o: bitmask-launcher.c
+ $(CC) $(CFLAGS) -c bitmask-launcher.c -o bitmask.o
+
+bitmask: bitmask.o
+ $(CC) bitmask.o -o bitmask
+ $(STRIP) bitmask
+clean:
+ -rm -f bitmask.o
+ -rm -f bitmask
diff --git a/pkg/launcher/README.rst b/pkg/launcher/README.rst
new file mode 100644
index 00000000..9a840dec
--- /dev/null
+++ b/pkg/launcher/README.rst
@@ -0,0 +1,14 @@
+bitmask-launcher.c
+------------------
+
+A small, portable launcher for bitmask bundles.
+
+Problem that solves
+-------------------
+PyInstaller bundles leave everything (libs, data and the main binary) in a
+single folder. In a case like ours, there are too many files cluttering this
+top-most folder.
+
+We wanted to have a cleaner folder, with an obviously clickable entrypoint, that
+calls the binary that hides in an inferior folder.
+
diff --git a/pkg/launcher/bitmask-launcher.c b/pkg/launcher/bitmask-launcher.c
new file mode 100644
index 00000000..cf5c2a18
--- /dev/null
+++ b/pkg/launcher/bitmask-launcher.c
@@ -0,0 +1,23 @@
+/*
+ * bitmask-launcher.c
+ *
+ * part of the bitmask bundle.
+ * execute main entrypoint in a child folder inside the bundle.
+ *
+ * (c) LEAP Encryption Access Project, 2016.
+ * License: GPL.
+ *
+*/
+
+#include <unistd.h>
+#include <stdlib.h>
+
+char* const bitmask_path = "lib";
+char* const entrypoint = "bitmask";
+
+int main(int argc, char *argv[])
+{
+ argv[0] = entrypoint;
+ chdir(bitmask_path);
+ execv(entrypoint, argv);
+}
diff --git a/pkg/leap_versions.txt b/pkg/leap_versions.txt
index ff0dd91c..6a5a8b7a 100644
--- a/pkg/leap_versions.txt
+++ b/pkg/leap_versions.txt
@@ -1,4 +1,4 @@
-soledad 0.7.2
-keymanager 0.4.2
-leap_common 0.4.2
-leap_mail 0.4.0rc2
+soledad 0.8.0
+keymanager 0.5.0
+leap_common 0.5.1
+leap_mail 0.4.1
diff --git a/pkg/linux/bitmask-launcher b/pkg/linux/bitmask-launcher
index 550dd134..3eae57c0 100755
--- a/pkg/linux/bitmask-launcher
+++ b/pkg/linux/bitmask-launcher
@@ -4,6 +4,19 @@
[ -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
+[ -f libQtNetwork.so.4 ] || ln -s libQtNetwork.so.4.orig libQtNetwork.so.4
+[ -f libQtSvg.so.4 ] || ln -s libQtSvg.so.4.orig libQtSvg.so.4
+[ -f libQtWebKit.so.4 ] || ln -s libQtWebKit.so.4.orig libQtWebKit.so.4
+[ -f libQtXmlPatterns.so.4 ] || ln -s libQtXmlPatterns.so.4.orig libQtXmlPatterns.so.4
+[ -f libQtXml.so.4 ] || ln -s libQtXml.so.4.orig libQtXml.so.4
-./bitmask-app "$@"
+cat /etc/os-release | grep ID | grep -i ubuntu && \
+ unlink libQtCore.so.4 && \
+ unlink libQtGui.so.4 && \
+ unlink libQtNetwork.so.4 && \
+ unlink libQtSvg.so.4 && \
+ unlink libQtWebKit.so.4 && \
+ unlink libQtXmlPatterns.so.4 && \
+ unlink libQtXml.so.4
+
+./bitmask-app --standalone "$@"
diff --git a/pkg/next-version b/pkg/next-version
new file mode 100644
index 00000000..2003b639
--- /dev/null
+++ b/pkg/next-version
@@ -0,0 +1 @@
+0.9.2
diff --git a/pkg/osx/Bitmask.pkgproj b/pkg/osx/Bitmask.pkgproj
new file mode 100755
index 00000000..bf882850
--- /dev/null
+++ b/pkg/osx/Bitmask.pkgproj
@@ -0,0 +1,750 @@
+<?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>PROJECT</key>
+ <dict>
+ <key>PACKAGE_FILES</key>
+ <dict>
+ <key>DEFAULT_INSTALL_LOCATION</key>
+ <string>/Applications</string>
+ <key>HIERARCHY</key>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>/Users/user/leap/bitmask_client/dist/Bitmask.app</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>3</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Utilities</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Applications</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>509</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Application Support</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Automator</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Documentation</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Filesystems</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Frameworks</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Input Methods</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Internet Plug-Ins</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>LaunchAgents</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>LaunchDaemons</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>PreferencePanes</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Preferences</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Printers</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>PrivilegedHelperTools</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>QuickLook</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>QuickTime</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Screen Savers</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Scripts</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Services</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Widgets</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Library</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Extensions</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Library</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>System</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Shared</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>1023</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Users</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>/</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <key>PAYLOAD_TYPE</key>
+ <integer>0</integer>
+ <key>VERSION</key>
+ <integer>3</integer>
+ </dict>
+ <key>PACKAGE_SCRIPTS</key>
+ <dict>
+ <key>POSTINSTALL_PATH</key>
+ <dict>
+ <key>PATH</key>
+ <string>../pkg/osx/post-inst.sh</string>
+ <key>PATH_TYPE</key>
+ <integer>3</integer>
+ </dict>
+ <key>PREINSTALL_PATH</key>
+ <dict>
+ <key>PATH</key>
+ <string>/Users/user/leap/bitmask_client/pkg/osx/pre-inst.sh</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ </dict>
+ <key>RESOURCES</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>../pkg/osx/se.leap.bitmask-helper.plist</string>
+ <key>PATH_TYPE</key>
+ <integer>3</integer>
+ <key>PERMISSIONS</key>
+ <integer>420</integer>
+ <key>TYPE</key>
+ <integer>3</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ </dict>
+ <key>PACKAGE_SETTINGS</key>
+ <dict>
+ <key>AUTHENTICATION</key>
+ <integer>1</integer>
+ <key>CONCLUSION_ACTION</key>
+ <integer>0</integer>
+ <key>IDENTIFIER</key>
+ <string>se.leap.pkg.Bitmask</string>
+ <key>OVERWRITE_PERMISSIONS</key>
+ <false/>
+ <key>RELOCATABLE</key>
+ <true/>
+ <key>VERSION</key>
+ <string>0.9.0rc4</string>
+ </dict>
+ <key>PROJECT_COMMENTS</key>
+ <dict>
+ <key>NOTES</key>
+ <data>
+ PCFET0NUWVBFIGh0bWwgUFVCTElDICItLy9XM0MvL0RURCBIVE1M
+ IDQuMDEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvVFIvaHRtbDQv
+ c3RyaWN0LmR0ZCI+CjxodG1sPgo8aGVhZD4KPG1ldGEgaHR0cC1l
+ cXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7
+ IGNoYXJzZXQ9VVRGLTgiPgo8bWV0YSBodHRwLWVxdWl2PSJDb250
+ ZW50LVN0eWxlLVR5cGUiIGNvbnRlbnQ9InRleHQvY3NzIj4KPHRp
+ dGxlPjwvdGl0bGU+CjxtZXRhIG5hbWU9IkdlbmVyYXRvciIgY29u
+ dGVudD0iQ29jb2EgSFRNTCBXcml0ZXIiPgo8bWV0YSBuYW1lPSJD
+ b2NvYVZlcnNpb24iIGNvbnRlbnQ9IjEyNjUuMjEiPgo8c3R5bGUg
+ dHlwZT0idGV4dC9jc3MiPgo8L3N0eWxlPgo8L2hlYWQ+Cjxib2R5
+ Pgo8L2JvZHk+CjwvaHRtbD4K
+ </data>
+ </dict>
+ <key>PROJECT_SETTINGS</key>
+ <dict>
+ <key>BUILD_PATH</key>
+ <dict>
+ <key>PATH</key>
+ <string>/Users/user/Bitmask/build</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ </dict>
+ <key>CERTIFICATE</key>
+ <dict>
+ <key>NAME</key>
+ <string>Developer ID Installer: LEAP Encryption Access Project (SB5RR8K33W)</string>
+ <key>PATH</key>
+ <string>/Users/user/Library/Keychains/login.keychain</string>
+ </dict>
+ <key>EXCLUDED_FILES</key>
+ <array>
+ <dict>
+ <key>PATTERNS_ARRAY</key>
+ <array>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>.DS_Store</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>PROTECTED</key>
+ <true/>
+ <key>PROXY_NAME</key>
+ <string>Remove .DS_Store files</string>
+ <key>PROXY_TOOLTIP</key>
+ <string>Remove ".DS_Store" files created by the Finder.</string>
+ <key>STATE</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>PATTERNS_ARRAY</key>
+ <array>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>.pbdevelopment</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>PROTECTED</key>
+ <true/>
+ <key>PROXY_NAME</key>
+ <string>Remove .pbdevelopment files</string>
+ <key>PROXY_TOOLTIP</key>
+ <string>Remove ".pbdevelopment" files created by ProjectBuilder or Xcode.</string>
+ <key>STATE</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>PATTERNS_ARRAY</key>
+ <array>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>CVS</string>
+ <key>TYPE</key>
+ <integer>1</integer>
+ </dict>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>.cvsignore</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>.cvspass</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>.svn</string>
+ <key>TYPE</key>
+ <integer>1</integer>
+ </dict>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>.git</string>
+ <key>TYPE</key>
+ <integer>1</integer>
+ </dict>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>.gitignore</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>PROTECTED</key>
+ <true/>
+ <key>PROXY_NAME</key>
+ <string>Remove SCM metadata</string>
+ <key>PROXY_TOOLTIP</key>
+ <string>Remove helper files and folders used by the CVS, SVN or Git Source Code Management systems.</string>
+ <key>STATE</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>PATTERNS_ARRAY</key>
+ <array>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>classes.nib</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>designable.db</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>info.nib</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>PROTECTED</key>
+ <true/>
+ <key>PROXY_NAME</key>
+ <string>Optimize nib files</string>
+ <key>PROXY_TOOLTIP</key>
+ <string>Remove "classes.nib", "info.nib" and "designable.nib" files within .nib bundles.</string>
+ <key>STATE</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>PATTERNS_ARRAY</key>
+ <array>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>Resources Disabled</string>
+ <key>TYPE</key>
+ <integer>1</integer>
+ </dict>
+ </array>
+ <key>PROTECTED</key>
+ <true/>
+ <key>PROXY_NAME</key>
+ <string>Remove Resources Disabled folders</string>
+ <key>PROXY_TOOLTIP</key>
+ <string>Remove "Resources Disabled" folders.</string>
+ <key>STATE</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>SEPARATOR</key>
+ <true/>
+ </dict>
+ </array>
+ <key>NAME</key>
+ <string>Bitmask</string>
+ <key>REFERENCE_FOLDER_PATH</key>
+ <string>/Users/user/leap/bitmask_client/dist</string>
+ </dict>
+ </dict>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>VERSION</key>
+ <integer>2</integer>
+</dict>
+</plist>
diff --git a/pkg/osx/Makefile b/pkg/osx/Makefile
deleted file mode 100644
index 15dfb810..00000000
--- a/pkg/osx/Makefile
+++ /dev/null
@@ -1,51 +0,0 @@
-OSX = dist/LEAP\ Client.app/Contents/MacOS/
-GITC = `git rev-parse --short HEAD`
-DMG = "dist/leap-client-$(GITC).dmg"
-INST = "dist/LEAP Client installer.app"
-INSTR = "dist/LEAP Client installer.app/Contents/Resources"
-
-pkg : check-env dist tuntap installer dmg
-
-dist :
- ~/pyinstaller/pyinstaller.py -w -s leap-client.spec
- cp -r /opt/local/Library/Frameworks/QtGui.framework/Versions/4/Resources/qt_menu.nib "dist/LEAP Client.app/Contents/Resources"
- cp Info.plist "dist/LEAP Client.app/Contents/Info.plist"
- cp ../../data/images/leap-client.icns "dist/LEAP Client.app/Contents/Resources/icon-windowed.icns"
-
-trim:
- #XXX this should go properly in pyinstaller spec excludes, but going quick'n'dirty
- #XXX adapt to PySide
- rm $(OSX)QtSvg $(OSX)QtXml $(OSX)QtNetwork $(OSX)QtOpenGL $(OSX)Qt3Support $(OSX)QtSql
-
-tuntap:
- ./build_tuntaposx clean && ./build_tuntaposx
-
-installer:
- #XXX need to fix some paths there (binary, etc)
- platypus -P install/leap-installer.platypus -y $(INST)
- # build tuntaposx kernel extension
- mkdir $(INSTR)/StartupItems
- mkdir $(INSTR)/Extensions
- cp -r dist/tun.kext $(INSTR)/Extensions
- cp -r dist/tuntaposx/StartupItems/* $(INSTR)/StartupItems
- cp install/tun.kext/Info.plist $(INSTR)/Extensions/tun.kext/Contents/
- #copy the binary that we have previously built (not yet)
- cp ../../openvpn/build/openvpn.leap $(INSTR)
- #copy startup scripts
- cp install/client.up.sh $(INSTR)
- cp install/client.down.sh $(INSTR)
- cp install/ProcessNetworkChanges.plist.template $(INSTR)
- #Finally, copy application bundle...
- cp -r "dist/LEAP Client.app" $(INSTR)
-
-dmg :
- rm -f $(DMG)
- hdiutil create -format UDBZ -srcfolder $(INST) $(DMG)
-
-check-env:
-ifndef VIRTUAL_ENV
- $(error WHAT DO YOU THINK VIRTUALENV IS FOR??!! Please go get into one..)
-endif
-
-clean :
- rm -rf dist/ build/
diff --git a/pkg/osx/README.rst b/pkg/osx/README.rst
index 92799ebd..eaf04fa1 100644
--- a/pkg/osx/README.rst
+++ b/pkg/osx/README.rst
@@ -1,49 +1,19 @@
environment setup in osx
========================
-TODO:: REALLY old notes, adapting to newest flow.
-
-basically you need this to setup your environment:
-
-# check and consolidate
-
-# 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
-----------
+You need at least version 3.0.
-You need the development version. do `python setup.py develop` inside your
-virtualenv.
-
-platypus (tested with latest macports)
-
-... + install environment as usual,
- inside virtualenv.
-
-Building the package
-====================
-
-Building the binary
--------------------
-We use the scripts in openvpn/build.zsh
-The packaging Makefile is expecting the final binary in the location::
-
- ../../openvpn/build/openvpn.leap
-
-Running the build
------------------
-IMPORTANT: activate the VIRTUALENV FIRST!
-(you will get an import error otherwise)
+pyside
+----------
+use repo branch kalikaneko/PySide (has --standalone patch)
-For running all steps at once::
+python2.7 setup.py bdist_wheel --version=1.2.2 --standalone
- make pkg
+Blockers
+========
+#7321 - requests bug in merge_environment_settings
diff --git a/pkg/osx/__init__.py b/pkg/osx/__init__.py
new file mode 100644
index 00000000..77ff624b
--- /dev/null
+++ b/pkg/osx/__init__.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+
+# daemon/__init__.py
+# Part of ‘python-daemon’, an implementation of PEP 3143.
+#
+# Copyright © 2009–2015 Ben Finney <ben+python@benfinney.id.au>
+# Copyright © 2006 Robert Niederreiter
+#
+# This is free software: you may copy, modify, and/or distribute this work
+# under the terms of the Apache License, version 2.0 as published by the
+# Apache Software Foundation.
+# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details.
+
+""" Library to implement a well-behaved Unix daemon process.
+
+ This library implements the well-behaved daemon specification of
+ :pep:`3143`, “Standard daemon process library”.
+
+ A well-behaved Unix daemon process is tricky to get right, but the
+ required steps are much the same for every daemon program. A
+ `DaemonContext` instance holds the behaviour and configured
+ process environment for the program; use the instance as a context
+ manager to enter a daemon state.
+
+ Simple example of usage::
+
+ import daemon
+
+ from spam import do_main_program
+
+ with daemon.DaemonContext():
+ do_main_program()
+
+ Customisation of the steps to become a daemon is available by
+ setting options on the `DaemonContext` instance; see the
+ documentation for that class for each option.
+
+ """
+
+from __future__ import (absolute_import, unicode_literals)
+
+from .daemon import DaemonContext
+
+
+# Local variables:
+# coding: utf-8
+# mode: python
+# End:
+# vim: fileencoding=utf-8 filetype=python :
diff --git a/pkg/osx/bitmask-helper b/pkg/osx/bitmask-helper
new file mode 100755
index 00000000..a1a3e86a
--- /dev/null
+++ b/pkg/osx/bitmask-helper
@@ -0,0 +1,430 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Author: Kali Kaneko
+# Copyright (C) 2015-2016 LEAP Encryption Access Project
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+This is a privileged helper script for safely running certain commands as root
+under OSX.
+
+It should be run by launchd, and it exposes a Unix Domain Socket to where
+the following commmands can be written by the Bitmask application:
+
+ firewall_start [restart] GATEWAY1 GATEWAY2 ...
+ firewall_stop
+ openvpn_start CONFIG1 CONFIG1 ...
+ openvpn_stop
+ fw_email_start uid
+ fw_email_stop
+
+To load it manually:
+
+ sudo launchctl load /Library/LaunchDaemons/se.leap.bitmask-helper
+
+To see the loaded rules:
+
+ sudo pfctl -s rules -a bitmask
+
+"""
+import os
+import re
+import socket
+import signal
+import subprocess
+import syslog
+import threading
+
+from commands import getoutput as exec_cmd
+from functools import partial
+
+import daemon
+
+VERSION = "1"
+SCRIPT = "bitmask-helper"
+NAMESERVER = "10.42.0.1"
+BITMASK_ANCHOR = "com.apple/250.BitmaskFirewall"
+BITMASK_ANCHOR_EMAIL = "bitmask_email"
+
+OPENVPN_USER = 'nobody'
+OPENVPN_GROUP = 'nogroup'
+LEAPOPENVPN = 'LEAPOPENVPN'
+APP_PATH = '/Applications/Bitmask.app/'
+RESOURCES_PATH = APP_PATH + 'Contents/Resources/'
+OPENVPN_LEAP_BIN = RESOURCES_PATH + 'openvpn.leap'
+
+FIXED_FLAGS = [
+ "--setenv", "LEAPOPENVPN", "1",
+ "--nobind",
+ "--client",
+ "--dev", "tun",
+ "--tls-client",
+ "--remote-cert-tls", "server",
+ "--management-signal",
+ "--script-security", "1",
+ "--user", "nobody",
+ "--remap-usr1", "SIGTERM",
+ "--group", OPENVPN_GROUP,
+]
+
+ALLOWED_FLAGS = {
+ "--remote": ["IP", "NUMBER", "PROTO"],
+ "--tls-cipher": ["CIPHER"],
+ "--cipher": ["CIPHER"],
+ "--auth": ["CIPHER"],
+ "--management": ["DIR", "UNIXSOCKET"],
+ "--management-client-user": ["USER"],
+ "--cert": ["FILE"],
+ "--key": ["FILE"],
+ "--ca": ["FILE"],
+ "--fragment": ["NUMBER"]
+}
+
+PARAM_FORMATS = {
+ "NUMBER": lambda s: re.match("^\d+$", s),
+ "PROTO": lambda s: re.match("^(tcp|udp)$", s),
+ "IP": lambda s: is_valid_address(s),
+ "CIPHER": lambda s: re.match("^[A-Z0-9-]+$", s),
+ "USER": lambda s: re.match(
+ "^[a-zA-Z0-9_\.\@][a-zA-Z0-9_\-\.\@]*\$?$", s), # IEEE Std 1003.1-2001
+ "FILE": lambda s: os.path.isfile(s),
+ "DIR": lambda s: os.path.isdir(os.path.split(s)[0]),
+ "UNIXSOCKET": lambda s: s == "unix",
+ "UID": lambda s: re.match("^[a-zA-Z0-9]+$", s)
+}
+
+#
+# paths (must use absolute paths, since this script is run as root)
+#
+
+PFCTL = '/sbin/pfctl'
+ROUTE = '/sbin/route'
+AWK = '/usr/bin/awk'
+GREP = '/usr/bin/grep'
+CAT = '/bin/cat'
+
+UID = os.getuid()
+SERVER_ADDRESS = '/tmp/bitmask-helper.socket'
+
+
+#
+# COMMAND DISPATCH
+#
+
+def serve_forever():
+ try:
+ os.unlink(SERVER_ADDRESS)
+ except OSError:
+ if os.path.exists(SERVER_ADDRESS):
+ raise
+
+ syslog.syslog(syslog.LOG_WARNING, "serving forever")
+ # XXX should check permissions on the socket file
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ sock.bind(SERVER_ADDRESS)
+ sock.listen(1)
+ syslog.syslog(syslog.LOG_WARNING, "Binded to %s" % SERVER_ADDRESS)
+
+ while True:
+ connection, client_address = sock.accept()
+ thread = threading.Thread(target=handle_command, args=[connection])
+ thread.daemon = True
+ thread.start()
+
+def recv_until_marker(sock):
+ end = '/CMD'
+ total_data=[]
+ data=''
+ while True:
+ data=sock.recv(8192)
+ if end in data:
+ total_data.append(data[:data.find(end)])
+ break
+ total_data.append(data)
+ if len(total_data)>1:
+ #check if end_of_data was split
+ last_pair=total_data[-2]+total_data[-1]
+ if end in last_pair:
+ total_data[-2] = last_pair[:last_pair.find(end)]
+ total_data.pop()
+ break
+ return ''.join(total_data)
+
+
+def handle_command(sock):
+ syslog.syslog(syslog.LOG_WARNING, "handle")
+
+ received = recv_until_marker(sock)
+ syslog.syslog(syslog.LOG_WARNING, "GOT -----> %s" % received)
+ line = received.replace('\n', '').split(' ')
+
+ command, args = line[0], line[1:]
+ syslog.syslog(syslog.LOG_WARNING, 'command %s' % (command))
+
+ cmd_dict = {
+ 'firewall_start': (firewall_start, args),
+ 'firewall_stop': (firewall_stop, []),
+ 'firewall_isup': (firewall_isup, []),
+ 'openvpn_start': (openvpn_start, args),
+ 'openvpn_stop': (openvpn_stop, []),
+ 'openvpn_force_stop': (openvpn_stop, ['KILL']),
+ 'openvpn_set_watcher': (openvpn_set_watcher, args)
+ }
+
+ cmd_call = cmd_dict.get(command, None)
+ syslog.syslog(syslog.LOG_WARNING, 'call: %s' % (str(cmd_call)))
+ try:
+ if cmd_call:
+ syslog.syslog(
+ syslog.LOG_WARNING, 'GOT "%s"' % (command))
+ cmd, args = cmd_call
+ if args:
+ cmd = partial(cmd, *args)
+
+ # TODO Use a MUTEX in here
+ result = cmd()
+ syslog.syslog(syslog.LOG_WARNING, "Executed")
+ syslog.syslog(syslog.LOG_WARNING, "Result: %s" % (str(result)))
+ if result == 'YES':
+ sock.sendall("%s: YES\n" % command)
+ elif result == 'NO':
+ sock.sendall("%s: NO\n" % command)
+ else:
+ sock.sendall("%s: OK\n" % command)
+
+ else:
+ syslog.syslog(syslog.LOG_WARNING, 'invalid command: %s' % (command,))
+ sock.sendall("%s: ERROR\n" % command)
+ except Exception as exc:
+ syslog.syslog(syslog.LOG_WARNING, "error executing function %r" % (exc))
+ finally:
+ sock.close()
+
+
+
+#
+# OPENVPN
+#
+
+
+openvpn_proc = None
+openvpn_watcher_pid = None
+
+
+def openvpn_start(*args):
+ """
+ Sanitize input and run openvpn as a subprocess of this long-running daemon.
+ Keeps a reference to the subprocess Popen class instance.
+
+ :param args: arguments to be passed to openvpn
+ :type args: list
+ """
+ syslog.syslog(syslog.LOG_WARNING, "OPENVPN START")
+ opts = list(args[1:])
+
+ opts += ['--dhcp-option', 'DNS', '10.42.0.1',
+ '--up', RESOURCES_PATH + 'client.up.sh',
+ '--down', RESOURCES_PATH + 'client.down.sh']
+ binary = [RESOURCES_PATH + 'openvpn.leap']
+
+ syslog.syslog(syslog.LOG_WARNING, ' '.join(binary + opts))
+
+ # TODO sanitize options
+ global openvpn_proc
+ openvpn_proc = subprocess.Popen(binary + opts, shell=False)
+ syslog.syslog(syslog.LOG_WARNING, "OpenVPN PID: %s" % str(openvpn_proc.pid))
+
+
+def openvpn_stop(sig='TERM'):
+ """
+ Stop the openvpn that has been launched by this privileged helper.
+
+ :param args: arguments to openvpn
+ :type args: list
+ """
+ global openvpn_proc
+
+ if openvpn_proc:
+ syslog.syslog(syslog.LOG_WARNING, "OVPN PROC: %s" % str(openvpn_proc.pid))
+
+ if sig == 'KILL':
+ stop_signal = signal.SIGKILL
+ openvpn_proc.kill()
+ elif sig == 'TERM':
+ stop_signal = signal.SIGTERM
+ openvpn_proc.terminate()
+
+ returncode = openvpn_proc.wait()
+ syslog.syslog(syslog.LOG_WARNING, "openvpn return code: %s" % str(returncode))
+ syslog.syslog(syslog.LOG_WARNING, "openvpn_watcher_pid: %s" % str(openvpn_watcher_pid))
+ if openvpn_watcher_pid:
+ os.kill(openvpn_watcher_pid, stop_signal)
+
+
+def openvpn_set_watcher(pid, *args):
+ global openvpn_watcher_pid
+ openvpn_watcher_pid = int(pid)
+ syslog.syslog(syslog.LOG_WARNING, "Watcher PID: %s" % pid)
+
+
+#
+# FIREWALL
+#
+
+
+def firewall_start(*gateways):
+ """
+ Bring up the firewall.
+
+ :param gws: list of gateways, to be sanitized.
+ :type gws: list
+ """
+
+ gateways = get_gateways(gateways)
+
+ if not gateways:
+ return False
+
+ _enable_pf()
+ _reset_bitmask_gateways_table(gateways)
+
+ default_device = _get_default_device()
+ _load_bitmask_anchor(default_device)
+
+
+def firewall_stop():
+ """
+ Flush everything from anchor bitmask
+ """
+ cmd = '{pfctl} -a {anchor} -F all'.format(
+ pfctl=PFCTL, anchor=BITMASK_ANCHOR)
+ return exec_cmd(cmd)
+
+
+def firewall_isup():
+ """
+ Return YES if anchor bitmask is loaded with rules
+ """
+ syslog.syslog(syslog.LOG_WARNING, 'PID---->%s' % os.getpid())
+ cmd = '{pfctl} -s rules -a {anchor} | wc -l'.format(
+ pfctl=PFCTL, anchor=BITMASK_ANCHOR)
+ output = exec_cmd(cmd)
+ rules = output[-1]
+ if int(rules) > 0:
+ return 'YES'
+ else:
+ return 'NO'
+
+
+def _enable_pf():
+ exec_cmd('{pfctl} -e'.format(pfctl=PFCTL))
+
+
+def _reset_bitmask_gateways_table(gateways):
+ cmd = '{pfctl} -a {anchor} -t bitmask_gateways -T delete'.format(
+ pfctl=PFCTL, anchor=BITMASK_ANCHOR)
+ output = exec_cmd(cmd)
+
+ for gateway in gateways:
+ cmd = '{pfctl} -a {anchor} -t bitmask_gateways -T add {gw}'.format(
+ pfctl=PFCTL, anchor=BITMASK_ANCHOR, gw=gateway)
+ output = exec_cmd(cmd)
+ syslog.syslog(syslog.LOG_WARNING, "adding gw %s" % gateway)
+
+ #cmd = '{pfctl} -a {anchor} -t bitmask_nameservers -T delete'.format(
+ # pfctl=PFCTL, anchor=BITMASK_ANCHOR)
+ #output = exec_cmd(cmd)
+
+ cmd = '{pfctl} -a {anchor} -t bitmask_gateways -T add {ns}'.format(
+ pfctl=PFCTL, anchor=BITMASK_ANCHOR, ns=NAMESERVER)
+ output = exec_cmd(cmd)
+ syslog.syslog(syslog.LOG_WARNING, "adding ns %s" % NAMESERVER)
+
+def _load_bitmask_anchor(default_device):
+ cmd = ('{pfctl} -D default_device={defaultdevice} '
+ '-a {anchor} -f {rulefile}').format(
+ pfctl=PFCTL, defaultdevice=default_device,
+ anchor=BITMASK_ANCHOR,
+ rulefile=RESOURCES_PATH + 'bitmask-helper/bitmask.pf.conf')
+ syslog.syslog(syslog.LOG_WARNING, "LOADING CMD: %s" % cmd)
+ return exec_cmd(cmd)
+
+
+def _get_default_device():
+ """
+ Retrieve the current default network device.
+
+ :rtype: str
+ """
+ cmd_def_device = (
+ '{route} -n get -net default | '
+ '{grep} interface | {awk} "{{print $2}}"').format(
+ route=ROUTE, grep=GREP, awk=AWK)
+ iface = exec_cmd(cmd_def_device)
+ iface = iface.replace("interface: ", "").strip()
+ syslog.syslog(syslog.LOG_WARNING, "default device %s" % iface)
+ return iface
+
+
+
+#
+# UTILITY
+#
+
+
+def is_valid_address(value):
+ """
+ Validate that the passed ip is a valid IP address.
+
+ :param value: the value to be validated
+ :type value: str
+ :rtype: bool
+ """
+ try:
+ socket.inet_aton(value)
+ return True
+ except Exception:
+ syslog.syslog(syslog.LOG_WARNING, 'MALFORMED IP: %s!' % (value))
+ return False
+
+
+#
+# FIREWALL
+#
+
+
+def get_gateways(gateways):
+ """
+ Filter a passed sequence of gateways, returning only the valid ones.
+
+ :param gateways: a sequence of gateways to filter.
+ :type gateways: iterable
+ :rtype: iterable
+ """
+ syslog.syslog(syslog.LOG_WARNING, 'Filtering %s' % str(gateways))
+ result = filter(is_valid_address, gateways)
+ if not result:
+ syslog.syslog(syslog.LOG_ERR, 'No valid gateways specified')
+ return False
+ else:
+ return result
+
+
+
+if __name__ == "__main__":
+ with daemon.DaemonContext():
+ syslog.syslog(syslog.LOG_WARNING, "Serving...")
+ serve_forever()
diff --git a/pkg/osx/bitmask-wrapper b/pkg/osx/bitmask-wrapper
new file mode 100755
index 00000000..c861380b
--- /dev/null
+++ b/pkg/osx/bitmask-wrapper
@@ -0,0 +1,3 @@
+#!/bin/sh
+DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+exec $DIR/bitmask-app --debug
diff --git a/pkg/osx/bitmask.icns b/pkg/osx/bitmask.icns
index 74fa0af6..7cc3e752 100644
--- a/pkg/osx/bitmask.icns
+++ b/pkg/osx/bitmask.icns
Binary files differ
diff --git a/pkg/osx/bitmask.pf.conf b/pkg/osx/bitmask.pf.conf
new file mode 100644
index 00000000..eb0e858f
--- /dev/null
+++ b/pkg/osx/bitmask.pf.conf
@@ -0,0 +1,17 @@
+default_device = "en99"
+
+set block-policy drop
+set skip on lo0
+
+# block all traffic on default device
+block out on $default_device all
+
+# allow traffic to gateways
+pass out on $default_device to <bitmask_gateways>
+
+# allow traffic to local networks over the default device
+pass out on $default_device to $default_device:network
+
+# block all DNS, except to the gateways
+block out proto udp to any port 53
+pass out proto udp to <bitmask_gateways> port 53
diff --git a/pkg/osx/client.down.sh b/pkg/osx/client.down.sh
new file mode 100755
index 00000000..1e173bba
--- /dev/null
+++ b/pkg/osx/client.down.sh
@@ -0,0 +1,426 @@
+#!/bin/bash -e
+# Note: must be bash; uses bash-specific tricks
+#
+# ******************************************************************************************************************
+# Copyright By Tunnelblick. Redistributed with Bitmask under the GPL.
+# This Tunnelblick script does everything! It handles TUN and TAP interfaces,
+# pushed configurations and DHCP leases. :)
+#
+# This is the "Down" version of the script, executed after the connection is
+# closed.
+#
+# Created by: Nick Williams (using original code and parts of old Tblk scripts)
+#
+# ******************************************************************************************************************
+
+# @param String message - The message to log
+logMessage()
+{
+ echo "${@}"
+}
+
+# @param String message - The message to log
+logDebugMessage()
+{
+ echo "${@}" > /dev/null
+}
+
+trim()
+{
+echo ${@}
+}
+
+# @param String list - list of network service names, output from disable_ipv6()
+restore_ipv6() {
+
+ # Undoes the actions performed by the disable_ipv6() routine in client.up.tunnelblick.sh by restoring the IPv6
+ # 'automatic' setting for each network service for which that routine disabled IPv6.
+ #
+ # $1 must contain the output from disable_ipv6() -- the list of network services.
+ #
+ # This routine outputs log messages describing its activities.
+
+ if [ "$1" = "" ] ; then
+ exit
+ fi
+
+ printf %s "$1
+" | \
+ while IFS= read -r ripv6_service ; do
+ networksetup -setv6automatic "$ripv6_service"
+ logMessage "Re-enabled IPv6 (automatic) for '$ripv6_service'"
+ done
+}
+
+##########################################################################################
+flushDNSCache()
+{
+ if ${ARG_FLUSH_DNS_CACHE} ; then
+ set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
+ readonly OSVER="$(sw_vers | grep 'ProductVersion:' | grep -o '10\.[0-9]*')"
+ set -e # We instruct bash that it CAN again fail on errors
+ if [ "${OSVER}" = "10.4" ] ; then
+
+ if [ -f /usr/sbin/lookupd ] ; then
+ set +e # we will catch errors from lookupd
+ /usr/sbin/lookupd -flushcache
+ if [ $? != 0 ] ; then
+ logMessage "WARNING: Unable to flush the DNS cache via lookupd"
+ else
+ logMessage "Flushed the DNS cache via lookupd"
+ fi
+ set -e # bash should again fail on errors
+ else
+ logMessage "WARNING: /usr/sbin/lookupd not present. Not flushing the DNS cache"
+ fi
+
+ else
+
+ if [ -f /usr/bin/dscacheutil ] ; then
+ set +e # we will catch errors from dscacheutil
+ /usr/bin/dscacheutil -flushcache
+ if [ $? != 0 ] ; then
+ logMessage "WARNING: Unable to flush the DNS cache via dscacheutil"
+ else
+ logMessage "Flushed the DNS cache via dscacheutil"
+ fi
+ set -e # bash should again fail on errors
+ else
+ logMessage "WARNING: /usr/bin/dscacheutil not present. Not flushing the DNS cache via dscacheutil"
+ fi
+
+ if [ -f /usr/sbin/discoveryutil ] ; then
+ set +e # we will catch errors from discoveryutil
+ /usr/sbin/discoveryutil udnsflushcaches
+ if [ $? != 0 ] ; then
+ logMessage "WARNING: Unable to flush the DNS cache via discoveryutil udnsflushcaches"
+ else
+ logMessage "Flushed the DNS cache via discoveryutil udnsflushcaches"
+ fi
+ /usr/sbin/discoveryutil mdnsflushcache
+ if [ $? != 0 ] ; then
+ logMessage "WARNING: Unable to flush the DNS cache via discoveryutil mdnsflushcache"
+ else
+ logMessage "Flushed the DNS cache via discoveryutil mdnsflushcache"
+ fi
+ set -e # bash should again fail on errors
+ else
+ logMessage "/usr/sbin/discoveryutil not present. Not flushing the DNS cache via discoveryutil"
+ fi
+
+ set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
+ hands_off_ps="$( ps -ax | grep HandsOffDaemon | grep -v grep.HandsOffDaemon )"
+ set -e # We instruct bash that it CAN again fail on errors
+ if [ "${hands_off_ps}" = "" ] ; then
+ if [ -f /usr/bin/killall ] ; then
+ set +e # ignore errors if mDNSResponder isn't currently running
+ /usr/bin/killall -HUP mDNSResponder
+ if [ $? != 0 ] ; then
+ logMessage "mDNSResponder not running. Not notifying it that the DNS cache was flushed"
+ else
+ logMessage "Notified mDNSResponder that the DNS cache was flushed"
+ fi
+ set -e # bash should again fail on errors
+ else
+ logMessage "WARNING: /usr/bin/killall not present. Not notifying mDNSResponder that the DNS cache was flushed"
+ fi
+ else
+ logMessage "WARNING: Hands Off is running. Not notifying mDNSResponder that the DNS cache was flushed"
+ fi
+
+ fi
+ fi
+}
+
+##########################################################################################
+resetPrimaryInterface()
+{
+ set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
+ WIFI_INTERFACE="$(networksetup -listallhardwareports | awk '$3=="Wi-Fi" {getline; print $2}')"
+ if [ "${WIFI_INTERFACE}" == "" ] ; then
+ WIFI_INTERFACE="$(networksetup -listallhardwareports | awk '$3=="AirPort" {getline; print $2}')"
+ fi
+ PINTERFACE="$( scutil <<-EOF |
+ open
+ show State:/Network/Global/IPv4
+ quit
+EOF
+ grep PrimaryInterface | sed -e 's/.*PrimaryInterface : //'
+ )"
+ set -e # resume abort on error
+
+ if [ "${PINTERFACE}" != "" ] ; then
+ if [ "${PINTERFACE}" == "${WIFI_INTERFACE}" -a "${OSVER}" != "10.4" -a -f /usr/sbin/networksetup ] ; then
+ if [ "${OSVER}" == "10.5" ] ; then
+ logMessage "Resetting primary interface '${PINTERFACE}' via networksetup -setairportpower off/on..."
+ /usr/sbin/networksetup -setairportpower off
+ sleep 2
+ /usr/sbin/networksetup -setairportpower on
+ else
+ logMessage "Resetting primary interface '${PINTERFACE}' via networksetup -setairportpower ${PINTERFACE} off/on..."
+ /usr/sbin/networksetup -setairportpower "${PINTERFACE}" off
+ sleep 2
+ /usr/sbin/networksetup -setairportpower "${PINTERFACE}" on
+ fi
+ else
+ if [ -f /sbin/ifconfig ] ; then
+ logMessage "Resetting primary interface '${PINTERFACE}' via ifconfig ${PINTERFACE} down/up..."
+ /sbin/ifconfig "${PINTERFACE}" down
+ sleep 2
+ /sbin/ifconfig "${PINTERFACE}" up
+ else
+ logMessage "WARNING: Not resetting primary interface because /sbin/ifconfig does not exist."
+ fi
+ fi
+ else
+ logMessage "WARNING: Not resetting primary interface because it cannot be found."
+ fi
+}
+
+##########################################################################################
+trap "" TSTP
+trap "" HUP
+trap "" INT
+export PATH="/bin:/sbin:/usr/sbin:/usr/bin"
+
+readonly OUR_NAME=$(basename "${0}")
+
+logMessage "**********************************************"
+logMessage "Start of output from ${OUR_NAME}"
+
+# Remove the flag file that indicates we need to run the down script
+
+if [ -e "/tmp/bitmask-downscript-needs-to-be-run.txt" ] ; then
+ rm -f "/tmp/bitmask-downscript-needs-to-be-run.txt"
+fi
+
+# Test for the "-r" Bitmask option (Reset primary interface after disconnecting) because we _always_ need its value.
+# Usually we get the value for that option (and the other options) from State:/Network/OpenVPN,
+# but that key may not exist (because, for example, there were no DNS changes).
+# So we get the value from the Bitmask options passed to this script by OpenVPN.
+#
+# We do the same thing for the -f Bitmask option (Flush DNS cache after connecting or disconnecting)
+ARG_RESET_PRIMARY_INTERFACE_ON_DISCONNECT="false"
+ARG_FLUSH_DNS_CACHE="false"
+while [ {$#} ] ; do
+ if [ "${1:0:1}" != "-" ] ; then # Bitmask arguments start with "-" and come first
+ break # so if this one doesn't start with "-" we are done processing Bitmask arguments
+ fi
+ if [ "$1" = "-r" ] ; then
+ ARG_RESET_PRIMARY_INTERFACE_ON_DISCONNECT="true"
+ else
+ if [ "$1" = "-f" ] ; then
+ ARG_FLUSH_DNS_CACHE="true"
+ fi
+ fi
+ shift # Shift arguments to examine the next option (if there is one)
+done
+
+# Quick check - is the configuration there?
+if ! scutil -w State:/Network/OpenVPN &>/dev/null -t 1 ; then
+ # Configuration isn't there
+ logMessage "WARNING: Not restoring DNS settings because no saved Bitmask DNS information was found."
+
+ flushDNSCache
+
+ if ${ARG_RESET_PRIMARY_INTERFACE_ON_DISCONNECT} ; then
+ resetPrimaryInterface
+ fi
+ logMessage "End of output from ${OUR_NAME}"
+ logMessage "**********************************************"
+ exit 0
+fi
+
+# Get info saved by the up script
+TUNNELBLICK_CONFIG="$( scutil <<-EOF
+ open
+ show State:/Network/OpenVPN
+ quit
+EOF
+)"
+
+ARG_MONITOR_NETWORK_CONFIGURATION="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*MonitorNetwork :' | sed -e 's/^.*: //g')"
+LEASEWATCHER_PLIST_PATH="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*LeaseWatcherPlistPath :' | sed -e 's/^.*: //g')"
+REMOVE_LEASEWATCHER_PLIST="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*RemoveLeaseWatcherPlist :' | sed -e 's/^.*: //g')"
+PSID="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*Service :' | sed -e 's/^.*: //g')"
+# Don't need: SCRIPT_LOG_FILE="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*ScriptLogFile :' | sed -e 's/^.*: //g')"
+# Don't need: ARG_RESTORE_ON_DNS_RESET="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*RestoreOnDNSReset :' | sed -e 's/^.*: //g')"
+# Don't need: ARG_RESTORE_ON_WINS_RESET="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*RestoreOnWINSReset :' | sed -e 's/^.*: //g')"
+# Don't need: PROCESS="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*PID :' | sed -e 's/^.*: //g')"
+# Don't need: ARG_IGNORE_OPTION_FLAGS="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*IgnoreOptionFlags :' | sed -e 's/^.*: //g')"
+ARG_TAP="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*IsTapInterface :' | sed -e 's/^.*: //g')"
+ARG_FLUSH_DNS_CACHE="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*FlushDNSCache :' | sed -e 's/^.*: //g')"
+ARG_RESET_PRIMARY_INTERFACE_ON_DISCONNECT="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*ResetPrimaryInterface :' | sed -e 's/^.*: //g')"
+bRouteGatewayIsDhcp="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*RouteGatewayIsDhcp :' | sed -e 's/^.*: //g')"
+bTapDeviceHasBeenSetNone="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*TapDeviceHasBeenSetNone :' | sed -e 's/^.*: //g')"
+bAlsoUsingSetupKeys="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*bAlsoUsingSetupKeys :' | sed -e 's/^.*: //g')"
+sTunnelDevice="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*TunnelDevice :' | sed -e 's/^.*: //g')"
+
+# Note: '\n' was translated into '\t', so we translate it back (it was done because grep and sed only work with single lines)
+sRestoreIpv6Services="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*RestoreIpv6Services :' | sed -e 's/^.*: //g' | tr '\t' '\n')"
+
+# Remove leasewatcher
+if ${ARG_MONITOR_NETWORK_CONFIGURATION} ; then
+ launchctl unload "${LEASEWATCHER_PLIST_PATH}"
+ if ${REMOVE_LEASEWATCHER_PLIST} ; then
+ rm -f "${LEASEWATCHER_PLIST_PATH}"
+ fi
+ logMessage "Cancelled monitoring of system configuration changes"
+fi
+
+if ${ARG_TAP} ; then
+ if [ "$bRouteGatewayIsDhcp" == "true" ]; then
+ if [ "$bTapDeviceHasBeenSetNone" == "false" ]; then
+ if [ -z "$dev" ]; then
+ # If $dev is not defined, then use TunnelDevice, which was set from $dev by client.up.tunnelblick.sh
+ # ($def is not defined when this script is called from MenuController to clean up when exiting Bitmask)
+ if [ -n "${sTunnelDevice}" ]; then
+ logMessage "WARNING: \$dev not defined; using TunnelDevice: ${sTunnelDevice}"
+ set +e
+ ipconfig set "${sTunnelDevice}" NONE 2>/dev/null
+ set -e
+ logMessage "Released the DHCP lease via ipconfig set ${sTunnelDevice} NONE."
+ else
+ logMessage "WARNING: Cannot configure TAP interface to NONE without \$dev or State:/Network/OpenVPN/TunnelDevice being defined. Device may not have disconnected properly."
+ fi
+ else
+ set +e
+ ipconfig set "$dev" NONE 2>/dev/null
+ set -e
+ logMessage "Released the DHCP lease via ipconfig set $dev NONE."
+ fi
+ fi
+ fi
+fi
+
+# Issue warning if the primary service ID has changed
+set +e # "grep" will return error status (1) if no matches are found, so don't fail if not found
+PSID_CURRENT="$( scutil <<-EOF |
+ open
+ show State:/Network/OpenVPN
+ quit
+EOF
+grep 'Service : ' | sed -e 's/.*Service : //'
+)"
+set -e # resume abort on error
+if [ "${PSID}" != "${PSID_CURRENT}" ] ; then
+ logMessage "Ignoring change of Network Primary Service from ${PSID} to ${PSID_CURRENT}"
+fi
+
+# Restore configurations
+DNS_OLD="$( scutil <<-EOF
+ open
+ show State:/Network/OpenVPN/OldDNS
+ quit
+EOF
+)"
+SMB_OLD="$( scutil <<-EOF
+ open
+ show State:/Network/OpenVPN/OldSMB
+ quit
+EOF
+)"
+DNS_OLD_SETUP="$( scutil <<-EOF
+ open
+ show State:/Network/OpenVPN/OldDNSSetup
+ quit
+EOF
+)"
+TB_NO_SUCH_KEY="<dictionary> {
+ BitmaskNoSuchKey : true
+}"
+
+if [ "${DNS_OLD}" = "${TB_NO_SUCH_KEY}" ] ; then
+ scutil <<-EOF
+ open
+ remove State:/Network/Service/${PSID}/DNS
+ quit
+EOF
+else
+ scutil <<-EOF
+ open
+ get State:/Network/OpenVPN/OldDNS
+ set State:/Network/Service/${PSID}/DNS
+ quit
+EOF
+fi
+
+if [ "${DNS_OLD_SETUP}" = "${TB_NO_SUCH_KEY}" ] ; then
+ if ${bAlsoUsingSetupKeys} ; then
+ logDebugMessage "DEBUG: Removing 'Setup:' DNS key"
+ scutil <<-EOF
+ open
+ remove Setup:/Network/Service/${PSID}/DNS
+ quit
+EOF
+ else
+ logDebugMessage "DEBUG: Not removing 'Setup:' DNS key"
+ fi
+else
+ if ${bAlsoUsingSetupKeys} ; then
+ logDebugMessage "DEBUG: Restoring 'Setup:' DNS key"
+ scutil <<-EOF
+ open
+ get State:/Network/OpenVPN/OldDNSSetup
+ set Setup:/Network/Service/${PSID}/DNS
+ quit
+EOF
+ else
+ logDebugMessage "DEBUG: Not restoring 'Setup:' DNS key"
+ fi
+fi
+
+if [ "${SMB_OLD}" = "${TB_NO_SUCH_KEY}" ] ; then
+ scutil > /dev/null <<-EOF
+ open
+ remove State:/Network/Service/${PSID}/SMB
+ quit
+EOF
+else
+ scutil > /dev/null <<-EOF
+ open
+ get State:/Network/OpenVPN/OldSMB
+ set State:/Network/Service/${PSID}/SMB
+ quit
+EOF
+fi
+
+logMessage "Restored the DNS and SMB configurations"
+
+set +e # "grep" will return error status (1) if no matches are found, so don't fail if not found
+new_resolver_contents="$( grep -v '#' < /etc/resolv.conf )"
+set -e # resume abort on error
+logDebugMessage "DEBUG:"
+logDebugMessage "DEBUG: /etc/resolve = ${new_resolver_contents}"
+
+set +e # scutil --dns will return error status in case dns is already down, so don't fail if no dns found
+scutil_dns="$( scutil --dns)"
+set -e # resume abort on error
+logDebugMessage "DEBUG:"
+logDebugMessage "DEBUG: scutil --dns = ${scutil_dns}"
+logDebugMessage "DEBUG:"
+
+restore_ipv6 "$sRestoreIpv6Services"
+
+flushDNSCache
+
+# Remove our system configuration data
+scutil <<-EOF
+ open
+ remove State:/Network/OpenVPN/OldDNS
+ remove State:/Network/OpenVPN/OldSMB
+ remove State:/Network/OpenVPN/OldDNSSetup
+ remove State:/Network/OpenVPN/DNS
+ remove State:/Network/OpenVPN/SMB
+ remove State:/Network/OpenVPN
+ quit
+EOF
+
+if ${ARG_RESET_PRIMARY_INTERFACE_ON_DISCONNECT} ; then
+ resetPrimaryInterface
+fi
+
+logMessage "End of output from ${OUR_NAME}"
+logMessage "**********************************************"
+
+exit 0
diff --git a/pkg/osx/client.up.sh b/pkg/osx/client.up.sh
new file mode 100755
index 00000000..a713c10e
--- /dev/null
+++ b/pkg/osx/client.up.sh
@@ -0,0 +1,1521 @@
+#!/bin/bash -e
+# Note: must be bash; uses bash-specific tricks
+#
+# ******************************************************************************************************************
+# Copyright by Tunnelblick. Redistributed under GPL as part of Bitmask.
+# This Tunnelblick script does everything! It handles TUN and TAP interfaces,
+# pushed configurations, DHCP with DNS and SMB, and renewed DHCP leases. :)
+#
+# This is the "Up" version of the script, executed after the interface is
+# initialized.
+#
+# Created by: Nick Williams (using original code and parts of old Tblk scripts)
+# Modifed by: Jonathan K. Bullard for Mountain Lion
+# Adapted to use by Bitmask by: Kali Kaneko
+#
+# ******************************************************************************************************************
+
+
+##########################################################################################
+# @param String message - The message to log
+logMessage()
+{
+ echo "${@}"
+}
+
+##########################################################################################
+# @param String message - The message to log
+logDebugMessage()
+{
+ if ${ARG_EXTRA_LOGGING} ; then
+ echo "${@}"
+ fi
+}
+
+##########################################################################################
+# log a change to a setting
+# @param String filters - empty, or one or two '#' if not performing the change
+# @param String name of setting that is being changed
+# @param String new value
+# @param String old value
+logChange()
+{
+ if [ "$1" = "" ] ; then
+ if [ "$3" = "$4" ] ; then
+ echo "Did not change $2 setting of '$3' (but re-set it)"
+ else
+ echo "Changed $2 setting from '$4' to '$3'"
+ fi
+ else
+ echo "Did not change $2 setting of '$4'"
+ fi
+}
+
+##########################################################################################
+# @param String string - Content to trim
+trim()
+{
+ echo ${@}
+}
+
+##########################################################################################
+disable_ipv6() {
+
+# Disables IPv6 on each enabled (active) network service on which it is set to the OS X default "IPv6 Automatic".
+#
+# For each such service, outputs a line with the name of the service.
+# (A separate line is output for each name because a name may include spaces.)
+#
+# The 'restore_ipv6' routine in client.down.sh undoes the actions performed by this routine.
+#
+# NOTE: Done only for enabled services because some versions of OS X enable the service if this IPv6 setting is changed.
+#
+# This only works for OS X 10.5 and higher (10.4 does not implement IPv6.)
+
+ if [ "$OSVER" = "10.4" ] ; then
+ exit
+ fi
+
+ # Get list of services and remove the first line which contains a heading
+ dipv6_services="$( networksetup -listallnetworkservices | sed -e '1,1d')"
+
+ # Go through the list disabling IPv6 for enabled services, and outputting lines with the names of the services
+ printf %s "$dipv6_services
+" | \
+ while IFS= read -r dipv6_service ; do
+
+ # If first character of a line is an asterisk, the service is disabled, so we skip it
+ if [ "${dipv6_service:0:1}" != "*" ] ; then
+ dipv6_ipv6_status="$( networksetup -getinfo "$dipv6_service" | grep 'IPv6: ' | sed -e 's/IPv6: //')"
+ if [ "$dipv6_ipv6_status" = "Automatic" ] ; then
+ networksetup -setv6off "$dipv6_service"
+ echo "$dipv6_service"
+ fi
+ fi
+
+ done
+}
+
+##########################################################################################
+# @param String[] dnsServers - The name servers to use
+# @param String domainName - The domain name to use
+# @param \optional String[] winsServers - The SMB servers to use
+# @param \optional String[] searchDomains - The search domains to use
+#
+# Throughout this routine:
+# MAN_ is a prefix for manually set parameters
+# DYN_ is a prefix for dynamically set parameters (by a "push", config file, or command line option)
+# CUR_ is a prefix for the current parameters (as arbitrated by OS X between manual and DHCP data)
+# FIN_ is a prefix for the parameters we want to end up with
+# SKP_ is a prefix for an empty string or a "#" used to control execution of statements that set parameters in scutil
+#
+# DNS_SA is a suffix for the ServerAddresses value in a System Configuration DNS key
+# DNS_SD is a suffix for the SearchDomains value in a System Configuration DNS key
+# DNS_DN is a suffix for the DomainName value in a System Configuration DNS key
+#
+# SMB_NN is a suffix for the NetBIOSName value in a System Configuration SMB key
+# SMB_WG is a suffix for the Workgroup value in a System Configuration SMB key
+# SMB_WA is a suffix for the WINSAddresses value in a System Configuration SMB key
+#
+# So, for example, MAN_SMB_NN is the manually set NetBIOSName value (or the empty string if not set manually)
+
+setDnsServersAndDomainName()
+{
+ set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
+
+ PSID="$( scutil <<-EOF |
+ open
+ show State:/Network/Global/IPv4
+ quit
+EOF
+grep PrimaryService | sed -e 's/.*PrimaryService : //'
+)"
+
+ set -e # resume abort on error
+
+ MAN_DNS_CONFIG="$( scutil <<-EOF |
+ open
+ show Setup:/Network/Service/${PSID}/DNS
+ quit
+EOF
+sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' '
+)"
+
+ MAN_SMB_CONFIG="$( scutil <<-EOF |
+ open
+ show Setup:/Network/Service/${PSID}/SMB
+ quit
+EOF
+sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' '
+)"
+ CUR_DNS_CONFIG="$( scutil <<-EOF |
+ open
+ show State:/Network/Global/DNS
+ quit
+EOF
+sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' '
+)"
+
+ CUR_SMB_CONFIG="$( scutil <<-EOF |
+ open
+ show State:/Network/Global/SMB
+ quit
+EOF
+sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' '
+)"
+
+# Set up the DYN_... variables to contain what is asked for (dynamically, by a 'push' directive, for example)
+
+ declare -a vDNS=("${!1}")
+ declare -a vSMB=("${!3}")
+ declare -a vSD=("${!4}")
+
+ if [ ${#vDNS[*]} -eq 0 ] ; then
+ readonly DYN_DNS_SA=""
+ else
+ readonly DYN_DNS_SA="${!1}"
+ fi
+
+ if [ ${#vSMB[*]} -eq 0 ] ; then
+ readonly DYN_SMB_WA=""
+ else
+ readonly DYN_SMB_WA="${!3}"
+ fi
+
+ if [ ${#vSD[*]} -eq 0 ] ; then
+ readonly DYN_DNS_SD=""
+ else
+ readonly DYN_DNS_SD="${!4}"
+ fi
+
+ DYN_DNS_DN="$2"
+
+ # The variables
+ # DYN_SMB_WG
+ # DYN_SMB_NN
+ # are left empty. There isn't a way for OpenVPN to set them.
+
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: MAN_DNS_CONFIG = ${MAN_DNS_CONFIG}"
+ logDebugMessage "DEBUG: MAN_SMB_CONFIG = ${MAN_SMB_CONFIG}"
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: CUR_DNS_CONFIG = ${CUR_DNS_CONFIG}"
+ logDebugMessage "DEBUG: CUR_SMB_CONFIG = ${CUR_SMB_CONFIG}"
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: DYN_DNS_DN = ${DYN_DNS_DN}; DYN_DNS_SA = ${DYN_DNS_SA}; DYN_DNS_SD = ${DYN_DNS_SD}"
+ logDebugMessage "DEBUG: DYN_SMB_NN = ${DYN_SMB_NN}; DYN_SMB_WG = ${DYN_SMB_WG}; DYN_SMB_WA = ${DYN_SMB_WA}"
+
+# Set up the MAN_... variables to contain manual network settings
+
+ set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
+
+ if echo "${MAN_DNS_CONFIG}" | grep -q "DomainName" ; then
+ readonly MAN_DNS_DN="$( trim "$( echo "${MAN_DNS_CONFIG}" | sed -e 's/^.*DomainName[^{]*{[[:space:]]*\([^}]*\)[[:space:]]*}.*$/\1/g' )" )"
+ else
+ readonly MAN_DNS_DN="";
+ fi
+ if echo "${MAN_DNS_CONFIG}" | grep -q "ServerAddresses" ; then
+ readonly MAN_DNS_SA="$( trim "$( echo "${MAN_DNS_CONFIG}" | sed -e 's/^.*ServerAddresses[^{]*{[[:space:]]*\([^}]*\)[[:space:]]*}.*$/\1/g' )" )"
+ else
+ readonly MAN_DNS_SA="";
+ fi
+ if echo "${MAN_DNS_CONFIG}" | grep -q "SearchDomains" ; then
+ readonly MAN_DNS_SD="$( trim "$( echo "${MAN_DNS_CONFIG}" | sed -e 's/^.*SearchDomains[^{]*{[[:space:]]*\([^}]*\)[[:space:]]*}.*$/\1/g' )" )"
+ else
+ readonly MAN_DNS_SD="";
+ fi
+ if echo "${MAN_SMB_CONFIG}" | grep -q "NetBIOSName" ; then
+ readonly MAN_SMB_NN="$( trim "$( echo "${MAN_SMB_CONFIG}" | sed -e 's/^.*NetBIOSName : \([^[:space:]]*\).*$/\1/g' )" )"
+ else
+ readonly MAN_SMB_NN="";
+ fi
+ if echo "${MAN_SMB_CONFIG}" | grep -q "Workgroup" ; then
+ readonly MAN_SMB_WG="$( trim "$( echo "${MAN_SMB_CONFIG}" | sed -e 's/^.*Workgroup : \([^[:space:]]*\).*$/\1/g' )" )"
+ else
+ readonly MAN_SMB_WG="";
+ fi
+ if echo "${MAN_SMB_CONFIG}" | grep -q "WINSAddresses" ; then
+ readonly MAN_SMB_WA="$( trim "$( echo "${MAN_SMB_CONFIG}" | sed -e 's/^.*WINSAddresses[^{]*{[[:space:]]*\([^}]*\)[[:space:]]*}.*$/\1/g' )" )"
+ else
+ readonly MAN_SMB_WA="";
+ fi
+
+ set -e # resume abort on error
+
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: MAN_DNS_DN = ${MAN_DNS_DN}; MAN_DNS_SA = ${MAN_DNS_SA}; MAN_DNS_SD = ${MAN_DNS_SD}"
+ logDebugMessage "DEBUG: MAN_SMB_NN = ${MAN_SMB_NN}; MAN_SMB_WG = ${MAN_SMB_WG}; MAN_SMB_WA = ${MAN_SMB_WA}"
+
+# Set up the CUR_... variables to contain the current network settings (from manual or DHCP, as arbitrated by OS X
+
+ set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
+
+ if echo "${CUR_DNS_CONFIG}" | grep -q "DomainName" ; then
+ readonly CUR_DNS_DN="$(trim "$( echo "${CUR_DNS_CONFIG}" | sed -e 's/^.*DomainName : \([^[:space:]]*\).*$/\1/g' )")"
+ else
+ readonly CUR_DNS_DN="";
+ fi
+ if echo "${CUR_DNS_CONFIG}" | grep -q "ServerAddresses" ; then
+ readonly CUR_DNS_SA="$(trim "$( echo "${CUR_DNS_CONFIG}" | sed -e 's/^.*ServerAddresses[^{]*{[[:space:]]*\([^}]*\)[[:space:]]*}.*$/\1/g' )")"
+ else
+ readonly CUR_DNS_SA="";
+ fi
+ if echo "${CUR_DNS_CONFIG}" | grep -q "SearchDomains" ; then
+ readonly CUR_DNS_SD="$(trim "$( echo "${CUR_DNS_CONFIG}" | sed -e 's/^.*SearchDomains[^{]*{[[:space:]]*\([^}]*\)[[:space:]]*}.*$/\1/g' )")"
+ else
+ readonly CUR_DNS_SD="";
+ fi
+ if echo "${CUR_SMB_CONFIG}" | grep -q "NetBIOSName" ; then
+ readonly CUR_SMB_NN="$(trim "$( echo "${CUR_SMB_CONFIG}" | sed -e 's/^.*NetBIOSName : \([^[:space:]]*\).*$/\1/g' )")"
+ else
+ readonly CUR_SMB_NN="";
+ fi
+ if echo "${CUR_SMB_CONFIG}" | grep -q "Workgroup" ; then
+ readonly CUR_SMB_WG="$(trim "$( echo "${CUR_SMB_CONFIG}" | sed -e 's/^.*Workgroup : \([^[:space:]]*\).*$/\1/g' )")"
+ else
+ readonly CUR_SMB_WG="";
+ fi
+ if echo "${CUR_SMB_CONFIG}" | grep -q "WINSAddresses" ; then
+ readonly CUR_SMB_WA="$(trim "$( echo "${CUR_SMB_CONFIG}" | sed -e 's/^.*WINSAddresses[^{]*{[[:space:]]*\([^}]*\)[[:space:]]*}.*$/\1/g' )")"
+ else
+ readonly CUR_SMB_WA="";
+ fi
+
+ set -e # resume abort on error
+
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: CUR_DNS_DN = ${CUR_DNS_DN}; CUR_DNS_SA = ${CUR_DNS_SA}; CUR_DNS_SD = ${CUR_DNS_SD}"
+ logDebugMessage "DEBUG: CUR_SMB_NN = ${CUR_SMB_NN}; CUR_SMB_WG = ${CUR_SMB_WG}; CUR_SMB_WA = ${CUR_SMB_WA}"
+
+# set up the FIN_... variables with what we want to set things to
+
+ # Three FIN_... variables are simple -- no aggregation is done for them
+
+ if [ "${DYN_DNS_DN}" != "" ] ; then
+ if [ "${MAN_DNS_DN}" != "" ] ; then
+ logMessage "WARNING: Ignoring DomainName '$DYN_DNS_DN' because DomainName was set manually"
+ readonly FIN_DNS_DN="${MAN_DNS_DN}"
+ else
+ readonly FIN_DNS_DN="${DYN_DNS_DN}"
+ fi
+ else
+ readonly FIN_DNS_DN="${CUR_DNS_DN}"
+ fi
+
+ if [ "${DYN_SMB_NN}" != "" ] ; then
+ if [ "${MAN_SMB_NN}" != "" ] ; then
+ logMessage "WARNING: Ignoring NetBIOSName '$DYN_SMB_NN' because NetBIOSName was set manually"
+ readonly FIN_SMB_NN="${MAN_SMB_NN}"
+ else
+ readonly FIN_SMB_NN="${DYN_SMB_NN}"
+ fi
+ else
+ readonly FIN_SMB_NN="${CUR_SMB_NN}"
+ fi
+
+ if [ "${DYN_SMB_WG}" != "" ] ; then
+ if [ "${MAN_SMB_WG}" != "" ] ; then
+ logMessage "WARNING: Ignoring Workgroup '$DYN_SMB_WG' because Workgroup was set manually"
+ readonly FIN_SMB_WG="${MAN_SMB_WG}"
+ else
+ readonly FIN_SMB_WG="${DYN_SMB_WG}"
+ fi
+ else
+ readonly FIN_SMB_WG="${CUR_SMB_WG}"
+ fi
+
+ # DNS ServerAddresses (FIN_DNS_SA) are aggregated for 10.4 and 10.5
+ if [ ${#vDNS[*]} -eq 0 ] ; then
+ readonly FIN_DNS_SA="${CUR_DNS_SA}"
+ else
+ if [ "${MAN_DNS_SA}" != "" ] ; then
+ logMessage "WARNING: Ignoring ServerAddresses '$DYN_DNS_SA' because ServerAddresses was set manually"
+ readonly FIN_DNS_SA="${CUR_DNS_SA}"
+ else
+ case "${OSVER}" in
+ 10.4 | 10.5 )
+ # We need to remove duplicate DNS entries, so that our reference list matches MacOSX's
+ SDNS="$( echo "${DYN_DNS_SA}" | tr ' ' '\n' )"
+ (( i=0 ))
+ for n in "${vDNS[@]}" ; do
+ if echo "${SDNS}" | grep -q "${n}" ; then
+ unset vDNS[${i}]
+ fi
+ (( i++ ))
+ done
+ if [ ${#vDNS[*]} -gt 0 ] ; then
+ readonly FIN_DNS_SA="$( trim "${DYN_DNS_SA}" "${vDNS[*]}" )"
+ else
+ readonly FIN_DNS_SA="${DYN_DNS_SA}"
+ fi
+ logMessage "Aggregating ServerAddresses because running on OS X 10.4 or 10.5"
+ ;;
+ * )
+ # Do nothing - in 10.6 and higher -- we don't aggregate our configurations, apparently
+ readonly FIN_DNS_SA="${DYN_DNS_SA}"
+ logMessage "Not aggregating ServerAddresses because running on OS X 10.6 or higher"
+ ;;
+ esac
+ fi
+ fi
+
+ # SMB WINSAddresses (FIN_SMB_WA) are aggregated for 10.4 and 10.5
+ if [ ${#vSMB[*]} -eq 0 ] ; then
+ readonly FIN_SMB_WA="${CUR_SMB_WA}"
+ else
+ if [ "${MAN_SMB_WA}" != "" ] ; then
+ logMessage "WARNING: Ignoring WINSAddresses '$DYN_SMB_WA' because WINSAddresses was set manually"
+ readonly FIN_SMB_WA="${MAN_SMB_WA}"
+ else
+ case "${OSVER}" in
+ 10.4 | 10.5 )
+ # We need to remove duplicate SMB entries, so that our reference list matches MacOSX's
+ SSMB="$( echo "${DYN_SMB_WA}" | tr ' ' '\n' )"
+ (( i=0 ))
+ for n in "${vSMB[@]}" ; do
+ if echo "${SSMB}" | grep -q "${n}" ; then
+ unset vSMB[${i}]
+ fi
+ (( i++ ))
+ done
+ if [ ${#vSMB[*]} -gt 0 ] ; then
+ readonly FIN_SMB_WA="$( trim "${DYN_SMB_WA}" "${vSMB[*]}" )"
+ else
+ readonly FIN_SMB_WA="${DYN_SMB_WA}"
+ fi
+ logMessage "Aggregating WINSAddresses because running on OS X 10.4 or 10.5"
+ ;;
+ * )
+ # Do nothing - in 10.6 and higher -- we don't aggregate our configurations, apparently
+ readonly FIN_SMB_WA="${DYN_SMB_WA}"
+ logMessage "Not aggregating WINSAddresses because running on OS X 10.6 or higher"
+ ;;
+ esac
+ fi
+ fi
+
+ # DNS SearchDomains (FIN_DNS_SD) is treated specially
+ #
+ # OLD BEHAVIOR:
+ # if SearchDomains was not set manually, we set SearchDomains to the DomainName
+ # else
+ # In OS X 10.4-10.5, we add the DomainName to the end of any manual SearchDomains (unless it is already there)
+ # In OS X 10.6+, if SearchDomains was entered manually, we ignore the DomainName
+ # else we set SearchDomains to the DomainName
+ #
+ # NEW BEHAVIOR (done if ARG_PREPEND_DOMAIN_NAME is "true"):
+ #
+ # if SearchDomains was entered manually, we do nothing
+ # else we PREpend new SearchDomains (if any) to the existing SearchDomains (NOT replacing them)
+ # and PREpend DomainName to that
+ #
+ # (done if ARG_PREPEND_DOMAIN_NAME is "false" and there are new SearchDomains from DOMAIN-SEARCH):
+ #
+ # if SearchDomains was entered manually, we do nothing
+ # else we PREpend any new SearchDomains to the existing SearchDomains (NOT replacing them)
+ #
+ # This behavior is meant to behave like Linux with Network Manager and Windows
+
+ if "${ARG_PREPEND_DOMAIN_NAME}" ; then
+ if [ "${MAN_DNS_SD}" = "" ] ; then
+ if [ "${DYN_DNS_SD}" != "" ] ; then
+ if ! echo "${CUR_DNS_SD}" | tr ' ' '\n' | grep -q "${DYN_DNS_SD}" ; then
+ logMessage "Prepending '${DYN_DNS_SD}' to search domains '${CUR_DNS_SD}' because the search domains were not set manually and 'Prepend domain name to search domains' was selected"
+ readonly TMP_DNS_SD="$( trim "${DYN_DNS_SD}" "${CUR_DNS_SD}" )"
+ else
+ logMessage "Not prepending '${DYN_DNS_SD}' to search domains '${CUR_DNS_SD}' because it is already there"
+ readonly TMP_DNS_SD="${CUR_DNS_SD}"
+ fi
+ else
+ readonly TMP_DNS_SD="${CUR_DNS_SD}"
+ fi
+ if [ "${FIN_DNS_DN}" != "" -a "${FIN_DNS_DN}" != "localdomain" ] ; then
+ if ! echo "${TMP_DNS_SD}" | tr ' ' '\n' | grep -q "${FIN_DNS_DN}" ; then
+ logMessage "Prepending '${FIN_DNS_DN}' to search domains '${TMP_DNS_SD}' because the search domains were not set manually and 'Prepend domain name to search domains' was selected"
+ readonly FIN_DNS_SD="$( trim "${FIN_DNS_DN}" "${TMP_DNS_SD}" )"
+ else
+ logMessage "Not prepending '${FIN_DNS_DN}' to search domains '${TMP_DNS_SD}' because it is already there"
+ readonly FIN_DNS_SD="${TMP_DNS_SD}"
+ fi
+ else
+ readonly FIN_DNS_SD="${TMP_DNS_SD}"
+ fi
+ else
+ if [ "${DYN_DNS_SD}" != "" ] ; then
+ logMessage "WARNING: Not prepending '${DYN_DNS_SD}' to search domains '${CUR_DNS_SD}' because the search domains were set manually"
+ fi
+ if [ "${FIN_DNS_DN}" != "" ] ; then
+ logMessage "WARNING: Not prepending domain '${FIN_DNS_DN}' to search domains '${CUR_DNS_SD}' because the search domains were set manually"
+ fi
+ readonly FIN_DNS_SD="${CUR_DNS_SD}"
+ fi
+ else
+ if [ "${DYN_DNS_SD}" != "" ] ; then
+ if [ "${MAN_DNS_SD}" = "" ] ; then
+ logMessage "Prepending '${DYN_DNS_SD}' to search domains '${CUR_DNS_SD}' because the search domains were not set manually but were set via OpenVPN and 'Prepend domain name to search domains' was not selected"
+ readonly FIN_DNS_SD="$( trim "${DYN_DNS_SD}" "${CUR_DNS_SD}" )"
+ else
+ logMessage "WARNING: Not prepending '${DYN_DNS_SD}' to search domains '${CUR_DNS_SD}' because the search domains were set manually"
+ readonly FIN_DNS_SD="${CUR_DNS_SD}"
+ fi
+ else
+ if [ "${FIN_DNS_DN}" != "" -a "${FIN_DNS_DN}" != "localdomain" ] ; then
+ case "${OSVER}" in
+ 10.4 | 10.5 )
+ if ! echo "${MAN_DNS_SD}" | tr ' ' '\n' | grep -q "${FIN_DNS_DN}" ; then
+ logMessage "Appending '${FIN_DNS_DN}' to search domains '${CUR_DNS_SD}' that were set manually because running under OS X 10.4 or 10.5 and 'Prepend domain name to search domains' was not selected"
+ readonly FIN_DNS_SD="$( trim "${MAN_DNS_SD}" "${FIN_DNS_DN}" )"
+ else
+ logMessage "Not appending '${FIN_DNS_DN}' to search domains '${CUR_DNS_SD}' because it is already in the search domains that were set manually and 'Prepend domain name to search domains' was not selected"
+ readonly FIN_DNS_SD="${CUR_DNS_SD}"
+ fi
+ ;;
+ * )
+ if [ "${MAN_DNS_SD}" = "" ] ; then
+ logMessage "Setting search domains to '${FIN_DNS_DN}' because running under OS X 10.6 or higher and the search domains were not set manually and 'Prepend domain name to search domains' was not selected"
+ readonly FIN_DNS_SD="${FIN_DNS_DN}"
+ else
+ logMessage "Not replacing search domains '${CUR_DNS_SD}' with '${FIN_DNS_DN}' because the search domains were set manually and 'Prepend domain name to search domains' was not selected"
+ readonly FIN_DNS_SD="${CUR_DNS_SD}"
+ fi
+ ;;
+ esac
+ else
+ readonly FIN_DNS_SD="${CUR_DNS_SD}"
+ fi
+ fi
+ fi
+
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: FIN_DNS_DN = ${FIN_DNS_DN}; FIN_DNS_SA = ${FIN_DNS_SA}; FIN_DNS_SD = ${FIN_DNS_SD}"
+ logDebugMessage "DEBUG: FIN_SMB_NN = ${FIN_SMB_NN}; FIN_SMB_WG = ${FIN_SMB_WG}; FIN_SMB_WA = ${FIN_SMB_WA}"
+
+# Set up SKP_... variables to inhibit scutil from making some changes
+
+ # SKP_DNS_... and SKP_SMB_... are used to comment out individual items that are not being set
+ if [ "${FIN_DNS_DN}" = "" -o "${FIN_DNS_DN}" = "${CUR_DNS_DN}" ] ; then
+ SKP_DNS_DN="#"
+ else
+ SKP_DNS_DN=""
+ fi
+ if [ "${FIN_DNS_SA}" = "" -o "${FIN_DNS_SA}" = "${CUR_DNS_SA}" ] ; then
+ SKP_DNS_SA="#"
+ else
+ SKP_DNS_SA=""
+ fi
+ if [ "${FIN_DNS_SD}" = "" -o "${FIN_DNS_SD}" = "${CUR_DNS_SD}" ] ; then
+ SKP_DNS_SD="#"
+ else
+ SKP_DNS_SD=""
+ fi
+ if [ "${FIN_SMB_NN}" = "" -o "${FIN_SMB_NN}" = "${CUR_SMB_NN}" ] ; then
+ SKP_SMB_NN="#"
+ else
+ SKP_SMB_NN=""
+ fi
+ if [ "${FIN_SMB_WG}" = "" -o "${FIN_SMB_WG}" = "${CUR_SMB_WG}" ] ; then
+ SKP_SMB_WG="#"
+ else
+ SKP_SMB_WG=""
+ fi
+ if [ "${FIN_SMB_WA}" = "" -o "${FIN_SMB_WA}" = "${CUR_SMB_WA}" ] ; then
+ SKP_SMB_WA="#"
+ else
+ SKP_SMB_WA=""
+ fi
+
+ # if any DNS items should be set, set all that have values
+ if [ "${SKP_DNS_DN}${SKP_DNS_SA}${SKP_DNS_SD}" = "###" ] ; then
+ readonly SKP_DNS="#"
+ else
+ readonly SKP_DNS=""
+ if [ "${FIN_DNS_DN}" != "" ] ; then
+ SKP_DNS_DN=""
+ fi
+ if [ "${FIN_DNS_SA}" != "" ] ; then
+ SKP_DNS_SA=""
+ fi
+ if [ "${FIN_DNS_SD}" != "" ] ; then
+ SKP_DNS_SD=""
+ fi
+ fi
+
+ # if any SMB items should be set, set all that have values
+ if [ "${SKP_SMB_NN}${SKP_SMB_WG}${SKP_SMB_WA}" = "###" ] ; then
+ readonly SKP_SMB="#"
+ else
+ readonly SKP_SMB=""
+ if [ "${FIN_SMB_NN}" != "" ] ; then
+ SKP_SMB_NN=""
+ fi
+ if [ "${FIN_SMB_WG}" != "" ] ; then
+ SKP_SMB_WG=""
+ fi
+ if [ "${FIN_SMB_WA}" != "" ] ; then
+ SKP_SMB_WA=""
+ fi
+ fi
+
+ readonly SKP_DNS_SA SKP_DNS_SD SKP_DNS_DN
+ readonly SKP_SMB_NN SKP_SMB_WG SKP_SMB_WA
+
+# special-case fiddling:
+
+ # in 10.8 and higher, ServerAddresses and SearchDomains must be set via the Setup: key in addition to the State: key
+ # in 10.7 if ServerAddresses or SearchDomains are manually set, ServerAddresses and SearchDomains must be similarly set with the Setup: key in addition to the State: key
+ #
+ # we pass a flag indicating whether we've done that to the other scripts in 'bAlsoUsingSetupKeys'
+
+ case "${OSVER}" in
+ 10.4 | 10.5 | 10.6 )
+ logDebugMessage "DEBUG: OS X 10.4-10.6, so will modify settings using only State:"
+ readonly SKP_SETUP_DNS="#"
+ readonly bAlsoUsingSetupKeys="false"
+ ;;
+ 10.7 )
+ if [ "${MAN_DNS_SA}" = "" -a "${MAN_DNS_SD}" = "" ] ; then
+ logDebugMessage "DEBUG: OS X 10.7 and neither ServerAddresses nor SearchDomains were set manually, so will modify DNS settings using only State:"
+ readonly SKP_SETUP_DNS="#"
+ readonly bAlsoUsingSetupKeys="false"
+ else
+ logDebugMessage "DEBUG: OS X 10.7 and ServerAddresses or SearchDomains were set manually, so will modify DNS settings using Setup: in addition to State:"
+ readonly SKP_SETUP_DNS=""
+ readonly bAlsoUsingSetupKeys="true"
+ fi
+ ;;
+ * )
+ logDebugMessage "DEBUG: OS X 10.8 or higher, so will modify DNS settings using Setup: in addition to State:"
+ readonly SKP_SETUP_DNS=""
+ readonly bAlsoUsingSetupKeys="true"
+ ;;
+ esac
+
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: SKP_DNS = ${SKP_DNS}; SKP_DNS_SA = ${SKP_DNS_SA}; SKP_DNS_SD = ${SKP_DNS_SD}; SKP_DNS_DN = ${SKP_DNS_DN}"
+ logDebugMessage "DEBUG: SKP_SETUP_DNS = ${SKP_SETUP_DNS}"
+ logDebugMessage "DEBUG: SKP_SMB = ${SKP_SMB}; SKP_SMB_NN = ${SKP_SMB_NN}; SKP_SMB_WG = ${SKP_SMB_WG}; SKP_SMB_WA = ${SKP_SMB_WA}"
+
+ set +e # "grep" will return error status (1) if no matches are found, so don't fail if not found
+ original_resolver_contents="$( grep -v '#' < /etc/resolv.conf )"
+ set -e # resume abort on error
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: /etc/resolve = ${original_resolver_contents}"
+ logDebugMessage "DEBUG:"
+
+ set +e # scutil --dns will return error status in case dns is already down, so don't fail if no dns found
+ scutil_dns="$( scutil --dns)"
+ set -e # resume abort on error
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: scutil --dns BEFORE CHANGES = ${scutil_dns}"
+ logDebugMessage "DEBUG:"
+
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: Configuration changes:"
+ logDebugMessage "DEBUG: ${SKP_DNS}${SKP_DNS_SA}ADD State: ServerAddresses ${FIN_DNS_SA}"
+ logDebugMessage "DEBUG: ${SKP_DNS}${SKP_DNS_SD}ADD State: SearchDomains ${FIN_DNS_SD}"
+ logDebugMessage "DEBUG: ${SKP_DNS}${SKP_DNS_DN}ADD State: DomainName ${FIN_DNS_DN}"
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: ${SKP_SETUP_DNS}${SKP_DNS}${SKP_DNS_SA}ADD Setup: ServerAddresses ${FIN_DNS_SA}"
+ logDebugMessage "DEBUG: ${SKP_SETUP_DNS}${SKP_DNS}${SKP_DNS_SD}ADD Setup: SearchDomains ${FIN_DNS_SD}"
+ logDebugMessage "DEBUG: ${SKP_SETUP_DNS}${SKP_DNS}${SKP_DNS_DN}ADD Setup: DomainName ${FIN_DNS_DN}"
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: ${SKP_SMB}${SKP_SMB_NN}ADD State: NetBIOSName ${FIN_SMB_NN}"
+ logDebugMessage "DEBUG: ${SKP_SMB}${SKP_SMB_WG}ADD State: Workgroup ${FIN_SMB_WG}"
+ logDebugMessage "DEBUG: ${SKP_SMB}${SKP_SMB_WA}ADD State: WINSAddresses ${FIN_SMB_WA}"
+
+ # Save the openvpn process ID and the Network Primary Service ID, leasewather.plist path, logfile path, and optional arguments from Bitmask,
+ # then save old and new DNS and SMB settings
+ # PPID is a script variable (defined by bash itself) that contains the process ID of the parent of the process running the script (i.e., OpenVPN's process ID)
+ # config is an environmental variable set to the configuration path by OpenVPN prior to running this up script
+
+ scutil <<-EOF > /dev/null
+ open
+
+ # Store our variables for the other scripts (leasewatch, down, etc.) to use
+ d.init
+ # The '#' in the next line does NOT start a comment; it indicates to scutil that a number follows it (as opposed to a string or an array)
+ d.add PID # ${PPID}
+ d.add Service ${PSID}
+ d.add LeaseWatcherPlistPath "${LEASEWATCHER_PLIST_PATH}"
+ d.add RemoveLeaseWatcherPlist "${REMOVE_LEASEWATCHER_PLIST}"
+ d.add ScriptLogFile "${SCRIPT_LOG_FILE}"
+ d.add MonitorNetwork "${ARG_MONITOR_NETWORK_CONFIGURATION}"
+ d.add RestoreOnDNSReset "${ARG_RESTORE_ON_DNS_RESET}"
+ d.add RestoreOnWINSReset "${ARG_RESTORE_ON_WINS_RESET}"
+ d.add IgnoreOptionFlags "${ARG_IGNORE_OPTION_FLAGS}"
+ d.add IsTapInterface "${ARG_TAP}"
+ d.add FlushDNSCache "${ARG_FLUSH_DNS_CACHE}"
+ d.add ResetPrimaryInterface "${ARG_RESET_PRIMARY_INTERFACE_ON_DISCONNECT}"
+ d.add RouteGatewayIsDhcp "${bRouteGatewayIsDhcp}"
+ d.add bAlsoUsingSetupKeys "${bAlsoUsingSetupKeys}"
+ d.add TapDeviceHasBeenSetNone "false"
+ d.add TunnelDevice "$dev"
+ d.add RestoreIpv6Services "$ipv6_disabled_services_encoded"
+ set State:/Network/OpenVPN
+
+ # Back up the device's current DNS and SMB configurations,
+ # Indicate 'no such key' by a dictionary with a single entry: "BitmaskNoSuchKey : true"
+ # If there isn't a key, "BitmaskNoSuchKey : true" won't be removed.
+ # If there is a key, "BitmaskNoSuchKey : true" will be removed and the key's contents will be used
+
+ d.init
+ d.add BitmaskNoSuchKey true
+ get State:/Network/Service/${PSID}/DNS
+ set State:/Network/OpenVPN/OldDNS
+
+ d.init
+ d.add BitmaskNoSuchKey true
+ get Setup:/Network/Service/${PSID}/DNS
+ set State:/Network/OpenVPN/OldDNSSetup
+
+ d.init
+ d.add BitmaskNoSuchKey true
+ get State:/Network/Service/${PSID}/SMB
+ set State:/Network/OpenVPN/OldSMB
+
+ # Initialize the new DNS map via State:
+ ${SKP_DNS}d.init
+ ${SKP_DNS}${SKP_DNS_SA}d.add ServerAddresses * ${FIN_DNS_SA}
+ ${SKP_DNS}${SKP_DNS_SD}d.add SearchDomains * ${FIN_DNS_SD}
+ ${SKP_DNS}${SKP_DNS_DN}d.add DomainName ${FIN_DNS_DN}
+ ${SKP_DNS}set State:/Network/Service/${PSID}/DNS
+
+ # If necessary, initialize the new DNS map via Setup: also
+ ${SKP_SETUP_DNS}${SKP_DNS}d.init
+ ${SKP_SETUP_DNS}${SKP_DNS}${SKP_DNS_SA}d.add ServerAddresses * ${FIN_DNS_SA}
+ ${SKP_SETUP_DNS}${SKP_DNS}${SKP_DNS_SD}d.add SearchDomains * ${FIN_DNS_SD}
+ ${SKP_SETUP_DNS}${SKP_DNS}${SKP_DNS_DN}d.add DomainName ${FIN_DNS_DN}
+ ${SKP_SETUP_DNS}${SKP_DNS}set Setup:/Network/Service/${PSID}/DNS
+
+ # Initialize the SMB map
+ ${SKP_SMB}d.init
+ ${SKP_SMB}${SKP_SMB_NN}d.add NetBIOSName ${FIN_SMB_NN}
+ ${SKP_SMB}${SKP_SMB_WG}d.add Workgroup ${FIN_SMB_WG}
+ ${SKP_SMB}${SKP_SMB_WA}d.add WINSAddresses * ${FIN_SMB_WA}
+ ${SKP_SMB}set State:/Network/Service/${PSID}/SMB
+
+ quit
+EOF
+
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: Pause for configuration changes to be propagated to State:/Network/Global/DNS and .../SMB"
+ sleep 1
+
+ scutil <<-EOF > /dev/null
+ open
+
+ # Initialize the maps that will be compared when a configuration change occurs
+ d.init
+ d.add BitmaskNoSuchKey true
+ get State:/Network/Global/DNS
+ set State:/Network/OpenVPN/DNS
+
+ d.init
+ d.add BitmaskNoSuchKey true
+ get State:/Network/Global/SMB
+ set State:/Network/OpenVPN/SMB
+
+ quit
+EOF
+
+ readonly NEW_DNS_SETUP_CONFIG="$( scutil <<-EOF |
+ open
+ show Setup:/Network/Service/${PSID}/DNS
+ quit
+EOF
+sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' '
+)"
+ readonly NEW_SMB_SETUP_CONFIG="$( scutil <<-EOF |
+ open
+ show Setup:/Network/Service/${PSID}/SMB
+ quit
+EOF
+sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' '
+)"
+ readonly NEW_DNS_STATE_CONFIG="$( scutil <<-EOF |
+ open
+ show State:/Network/Service/${PSID}/DNS
+ quit
+EOF
+sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' '
+)"
+ readonly NEW_SMB_STATE_CONFIG="$( scutil <<-EOF |
+ open
+ show State:/Network/Service/${PSID}/SMB
+ quit
+EOF
+sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' '
+)"
+ readonly NEW_DNS_GLOBAL_CONFIG="$( scutil <<-EOF |
+ open
+ show State:/Network/Global/DNS
+ quit
+EOF
+sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' '
+)"
+ readonly NEW_SMB_GLOBAL_CONFIG="$( scutil <<-EOF |
+ open
+ show State:/Network/Global/SMB
+ quit
+EOF
+sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' '
+)"
+ readonly EXPECTED_NEW_DNS_GLOBAL_CONFIG="$( scutil <<-EOF |
+ open
+ show State:/Network/OpenVPN/DNS
+ quit
+EOF
+sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' '
+)"
+ readonly EXPECTED_NEW_SMB_GLOBAL_CONFIG="$( scutil <<-EOF |
+ open
+ show State:/Network/OpenVPN/SMB
+ quit
+EOF
+sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' '
+)"
+
+
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: Configurations as read back after changes:"
+ logDebugMessage "DEBUG: State:/.../DNS = ${NEW_DNS_STATE_CONFIG}"
+ logDebugMessage "DEBUG: State:/.../SMB = ${NEW_SMB_STATE_CONFIG}"
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: Setup:/.../DNS = ${NEW_DNS_SETUP_CONFIG}"
+ logDebugMessage "DEBUG: Setup:/.../SMB = ${NEW_SMB_SETUP_CONFIG}"
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: State:/Network/Global/DNS = ${NEW_DNS_GLOBAL_CONFIG}"
+ logDebugMessage "DEBUG: State:/Network/Global/SMB = ${NEW_SMB_GLOBAL_CONFIG}"
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: Expected by process-network-changes:"
+ logDebugMessage "DEBUG: State:/Network/OpenVPN/DNS = ${EXPECTED_NEW_DNS_GLOBAL_CONFIG}"
+ logDebugMessage "DEBUG: State:/Network/OpenVPN/SMB = ${EXPECTED_NEW_SMB_GLOBAL_CONFIG}"
+
+ set +e # "grep" will return error status (1) if no matches are found, so don't fail if not found
+ new_resolver_contents="$( grep -v '#' < /etc/resolv.conf )"
+ set -e # resume abort on error
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: /etc/resolve = ${new_resolver_contents}"
+ logDebugMessage "DEBUG:"
+
+ set +e # scutil --dns will return error status in case dns is already down, so don't fail if no dns found
+ scutil_dns="$( scutil --dns )"
+ set -e # resume abort on error
+ logDebugMessage "DEBUG:"
+ logDebugMessage "DEBUG: scutil --dns AFTER CHANGES = ${scutil_dns}"
+ logDebugMessage "DEBUG:"
+
+ logMessage "Saved the DNS and SMB configurations so they can be restored"
+
+ logChange "${SKP_DNS}${SKP_DNS_SA}" "DNS ServerAddresses" "${FIN_DNS_SA}" "${CUR_DNS_SA}"
+ logChange "${SKP_DNS}${SKP_DNS_SD}" "DNS SearchDomains" "${FIN_DNS_SD}" "${CUR_DNS_SD}"
+ logChange "${SKP_DNS}${SKP_DNS_DN}" "DNS DomainName" "${FIN_DNS_DN}" "${CUR_DNS_DN}"
+ logChange "${SKP_SMB}${SKP_SMB_NN}" "SMB NetBIOSName" "${FIN_SMB_SA}" "${CUR_SMB_SA}"
+ logChange "${SKP_SMB}${SKP_SMB_WG}" "SMB Workgroup" "${FIN_SMB_WG}" "${CUR_SMB_WG}"
+ logChange "${SKP_SMB}${SKP_SMB_WA}" "SMB WINSAddresses" "${FIN_SMB_WA}" "${CUR_SMB_WA}"
+
+ logDnsInfo "${MAN_DNS_SA}" "${FIN_DNS_SA}"
+
+ flushDNSCache
+
+ if ${ARG_MONITOR_NETWORK_CONFIGURATION} ; then
+ if [ "${ARG_IGNORE_OPTION_FLAGS:0:2}" = "-p" ] ; then
+ logMessage "Setting up to monitor system configuration with process-network-changes"
+ else
+ logMessage "Setting up to monitor system configuration with leasewatch"
+ fi
+ if [ "${LEASEWATCHER_TEMPLATE_PATH}" != "" ] ; then
+ sed -e "s|/Applications/Bitmask/.app/Contents/Resources|${TB_RESOURCES_PATH}|g" "${LEASEWATCHER_TEMPLATE_PATH}" > "${LEASEWATCHER_PLIST_PATH}"
+ fi
+ launchctl load "${LEASEWATCHER_PLIST_PATH}"
+ fi
+}
+
+##########################################################################################
+# Used for TAP device which does DHCP
+configureDhcpDns()
+{
+ # whilst ipconfig will have created the neccessary Network Service keys, the DNS
+ # settings won't actually be used by OS X unless the SupplementalMatchDomains key
+ # is added
+ # ref. <http://lists.apple.com/archives/Macnetworkprog/2005/Jun/msg00011.html>
+ # - is there a way to extract the domains from the SC dictionary and re-insert
+ # as SupplementalMatchDomains? i.e. not requiring the ipconfig domain_name call?
+
+ # - wait until we get a lease before extracting the DNS domain name and merging into SC
+ # - despite it's name, ipconfig waitall doesn't (but maybe one day it will :-)
+ logDebugMessage "DEBUG: About to 'ipconfig waitall'"
+ ipconfig waitall
+ logDebugMessage "DEBUG: Completed 'ipconfig waitall'"
+
+ unset test_domain_name
+ unset test_name_server
+
+ set +e # We instruct bash NOT to exit on individual command errors, because if we need to wait longer these commands will fail
+
+ # usually takes at least a few seconds to get a DHCP lease
+ sleep 3
+ n=0
+ while [ -z "$test_domain_name" -a -z "$test_name_server" -a $n -lt 5 ]
+ do
+ logMessage "Sleeping for $n seconds to wait for DHCP to finish setup."
+ sleep $n
+ n="$( expr $n + 1 )"
+
+ if [ -z "$test_domain_name" ]; then
+ test_domain_name="$( ipconfig getoption "$dev" domain_name 2>/dev/null )"
+ fi
+
+ if [ -z "$test_name_server" ]; then
+ test_name_server="$( ipconfig getoption "$dev" domain_name_server 2>/dev/null )"
+ fi
+ done
+
+ logDebugMessage "DEBUG: Finished waiting for DHCP lease: test_domain_name = '$test_domain_name', test_name_server = '$test_name_server'"
+
+ logDebugMessage "DEBUG: About to 'ipconfig getpacket $dev'"
+ sGetPacketOutput="$( ipconfig getpacket "$dev" )"
+ logDebugMessage "DEBUG: Completed 'ipconfig getpacket $dev'; sGetPacketOutput = $sGetPacketOutput"
+
+ set -e # We instruct bash that it CAN again fail on individual errors
+
+ unset aNameServers
+ unset aWinsServers
+ unset aSearchDomains
+
+ nNameServerIndex=1
+ nWinsServerIndex=1
+ nSearchDomainIndex=1
+
+ if [ "$sGetPacketOutput" ]; then
+ sGetPacketOutput_FirstLine="$( echo "$sGetPacketOutput" | head -n 1 )"
+ logDebugMessage "DEBUG: sGetPacketOutput_FirstLine = $sGetPacketOutput_FirstLine"
+
+ if [ "$sGetPacketOutput_FirstLine" == "op = BOOTREPLY" ]; then
+ set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
+
+ for tNameServer in $( echo "$sGetPacketOutput" | grep "domain_name_server" | grep -Eo "\{([0-9\.]+)(, [0-9\.]+)*\}" | grep -Eo "([0-9\.]+)" ); do
+ aNameServers[nNameServerIndex-1]="$( trim "$tNameServer" )"
+ let nNameServerIndex++
+ done
+
+ for tWINSServer in $( echo "$sGetPacketOutput" | grep "nb_over_tcpip_name_server" | grep -Eo "\{([0-9\.]+)(, [0-9\.]+)*\}" | grep -Eo "([0-9\.]+)" ); do
+ aWinsServers[nWinsServerIndex-1]="$( trim "$tWINSServer" )"
+ let nWinsServerIndex++
+ done
+
+ for tSearchDomain in $( echo "$sGetPacketOutput" | grep "search_domain" | grep -Eo "\{([-A-Za-z0-9\-\.]+)(, [-A-Za-z0-9\-\.]+)*\}" | grep -Eo "([-A-Za-z0-9\-\.]+)" ); do
+ aSearchDomains[nSearchDomainIndex-1]="$( trim "$tSearchDomain" )"
+ let nSearchDomainIndex++
+ done
+
+ sDomainName="$( echo "$sGetPacketOutput" | grep "domain_name " | grep -Eo ": [-A-Za-z0-9\-\.]+" | grep -Eo "[-A-Za-z0-9\-\.]+" )"
+ sDomainName="$( trim "$sDomainName" )"
+
+ if [ ${#aNameServers[*]} -gt 0 -a "$sDomainName" ]; then
+ logMessage "Retrieved from DHCP/BOOTP packet: name server(s) [ ${aNameServers[@]} ], domain name [ $sDomainName ], search domain(s) [ ${aSearchDomains[@]} ] and SMB server(s) [ ${aWinsServers[@]} ]"
+ setDnsServersAndDomainName aNameServers[@] "$sDomainName" aWinsServers[@] aSearchDomains[@]
+ return 0
+ elif [ ${#aNameServers[*]} -gt 0 ]; then
+ logMessage "Retrieved from DHCP/BOOTP packet: name server(s) [ ${aNameServers[@]} ], search domain(s) [ ${aSearchDomains[@]} ] and SMB server(s) [ ${aWinsServers[@]} ] and using default domain name [ $DEFAULT_DOMAIN_NAME ]"
+ setDnsServersAndDomainName aNameServers[@] "$DEFAULT_DOMAIN_NAME" aWinsServers[@] aSearchDomains[@]
+ return 0
+ else
+ # Should we return 1 here and indicate an error, or attempt the old method?
+ logMessage "No useful information extracted from DHCP/BOOTP packet. Attempting legacy configuration."
+ fi
+
+ set -e # We instruct bash that it CAN again fail on errors
+ else
+ # Should we return 1 here and indicate an error, or attempt the old method?
+ logMessage "No DHCP/BOOTP packet found on interface. Attempting legacy configuration."
+ fi
+ fi
+
+ unset sDomainName
+ unset sNameServer
+ unset aNameServers
+
+ set +e # We instruct bash NOT to exit on individual command errors, because if we need to wait longer these commands will fail
+
+ logDebugMessage "DEBUG: About to 'ipconfig getoption $dev domain_name'"
+ sDomainName="$( ipconfig getoption "$dev" domain_name 2>/dev/null )"
+ logDebugMessage "DEBUG: Completed 'ipconfig getoption $dev domain_name'"
+ logDebugMessage "DEBUG: About to 'ipconfig getoption $dev domain_name_server'"
+ sNameServer="$( ipconfig getoption "$dev" domain_name_server 2>/dev/null )"
+ logDebugMessage "DEBUG: Completed 'ipconfig getoption $dev domain_name_server'"
+
+ set -e # We instruct bash that it CAN again fail on individual errors
+
+ sDomainName="$( trim "$sDomainName" )"
+ sNameServer="$( trim "$sNameServer" )"
+
+ declare -a aWinsServers=( ) # Declare empty WINSServers array to avoid any useless error messages
+ declare -a aSearchDomains=( ) # Declare empty SearchDomains array to avoid any useless error messages
+
+ if [ "$sDomainName" -a "$sNameServer" ]; then
+ aNameServers[0]=$sNameServer
+ logMessage "Retrieved OpenVPN (DHCP): name server [ $sNameServer ], domain name [ $sDomainName ], and no SMB servers or search domains"
+ setDnsServersAndDomainName aNameServers[@] "$sDomainName" aWinsServers[@] aSearchDomains[@]
+ elif [ "$sNameServer" ]; then
+ aNameServers[0]=$sNameServer
+ logMessage "Retrieved OpenVPN (DHCP): name server [ $sNameServer ] and no SMB servers or search domains, and using default domain name [ $DEFAULT_DOMAIN_NAME ]"
+ setDnsServersAndDomainName aNameServers[@] "$DEFAULT_DOMAIN_NAME" aWinsServers[@] aSearchDomains[@]
+ elif [ "$sDomainName" ]; then
+ logMessage "WARNING: Retrieved domain name [ $sDomainName ] but no name servers from OpenVPN via DHCP, which is not sufficient to make network/DNS configuration changes."
+ if ${ARG_MONITOR_NETWORK_CONFIGURATION} ; then
+ logMessage "WARNING: Will NOT monitor for other network configuration changes."
+ fi
+ logDnsInfoNoChanges
+ flushDNSCache
+ else
+ logMessage "WARNING: No DNS information received from OpenVPN via DHCP, so no network/DNS configuration changes need to be made."
+ if ${ARG_MONITOR_NETWORK_CONFIGURATION} ; then
+ logMessage "WARNING: Will NOT monitor for other network configuration changes."
+ fi
+ logDnsInfoNoChanges
+ flushDNSCache
+ fi
+
+ return 0
+}
+
+##########################################################################################
+# Configures using OpenVPN foreign_option_* instead of DHCP
+
+configureOpenVpnDns()
+{
+# Description of foreign_option_ parameters (from OpenVPN 2.3-alpha_2 man page):
+#
+# DOMAIN name -- Set Connection-specific DNS Suffix.
+#
+# DOMAIN-SEARCH name -- Set Connection-specific DNS Search Address. Repeat this option to
+# set additional search domains. (Bitmask-specific addition.)
+#
+# DNS addr -- Set primary domain name server address. Repeat this option to set
+# secondary DNS server addresses.
+#
+# WINS addr -- Set primary WINS server address (NetBIOS over TCP/IP Name Server).
+# Repeat this option to set secondary WINS server addresses.
+#
+# NBDD addr -- Set primary NBDD server address (NetBIOS over TCP/IP Datagram Distribution Server)
+# Repeat this option to set secondary NBDD server addresses.
+#
+# NTP addr -- Set primary NTP server address (Network Time Protocol). Repeat this option
+# to set secondary NTP server addresses.
+#
+# NBT type -- Set NetBIOS over TCP/IP Node type. Possible options: 1 = b-node
+# (broadcasts), 2 = p-node (point-to-point name queries to a WINS server), 4 = m-
+# node (broadcast then query name server), and 8 = h-node (query name server, then
+# broadcast).
+#
+# NBS scope-id -- Set NetBIOS over TCP/IP Scope. A NetBIOS Scope ID provides an
+# extended naming service for the NetBIOS over TCP/IP (Known as NBT) module. The
+# primary purpose of a NetBIOS scope ID is to isolate NetBIOS traffic on a single
+# network to only those nodes with the same NetBIOS scope ID. The NetBIOS scope ID
+# is a character string that is appended to the NetBIOS name. The NetBIOS scope ID
+# on two hosts must match, or the two hosts will not be able to communicate. The
+# NetBIOS Scope ID also allows computers to use the same computer name, as they have
+# different scope IDs. The Scope ID becomes a part of the NetBIOS name, making the
+# name unique. (This description of NetBIOS scopes courtesy of NeonSurge@abyss.com)
+#
+#DISABLE-NBT -- Disable Netbios-over-TCP/IP.
+
+ unset vForOptions
+ unset vOptions
+ unset aNameServers
+ unset aWinsServers
+ unset aSearchDomains
+
+ nOptionIndex=1
+ nNameServerIndex=1
+ nWinsServerIndex=1
+ nSearchDomainIndex=1
+
+ while vForOptions=foreign_option_$nOptionIndex; [ -n "${!vForOptions}" ]; do
+ vOptions[nOptionIndex-1]=${!vForOptions}
+ case ${vOptions[nOptionIndex-1]} in
+ *DOMAIN-SEARCH* )
+ aSearchDomains[nSearchDomainIndex-1]="$( trim "${vOptions[nOptionIndex-1]//dhcp-option DOMAIN-SEARCH /}" )"
+ let nSearchDomainIndex++
+ ;;
+ *DOMAIN* )
+ sDomainName="$( trim "${vOptions[nOptionIndex-1]//dhcp-option DOMAIN /}" )"
+ ;;
+ *DNS* )
+ aNameServers[nNameServerIndex-1]="$( trim "${vOptions[nOptionIndex-1]//dhcp-option DNS /}" )"
+ let nNameServerIndex++
+ ;;
+ *WINS* )
+ aWinsServers[nWinsServerIndex-1]="$( trim "${vOptions[nOptionIndex-1]//dhcp-option WINS /}" )"
+ let nWinsServerIndex++
+ ;;
+ * )
+ logMessage "WARNING: 'foreign_option_${nOptionIndex}' = '${vOptions[nOptionIndex-1]}' ignored"
+ ;;
+ esac
+ let nOptionIndex++
+ done
+
+ if [ ${#aNameServers[*]} -gt 0 -a "$sDomainName" ]; then
+ logMessage "Retrieved from OpenVPN: name server(s) [ ${aNameServers[@]} ], domain name [ $sDomainName ], search domain(s) [ ${aSearchDomains[@]} ], and SMB server(s) [ ${aWinsServers[@]} ]"
+ setDnsServersAndDomainName aNameServers[@] "$sDomainName" aWinsServers[@] aSearchDomains[@]
+ elif [ ${#aNameServers[*]} -gt 0 ]; then
+ logMessage "Retrieved from OpenVPN: name server(s) [ ${aNameServers[@]} ], search domain(s) [ ${aSearchDomains[@]} ] and SMB server(s) [ ${aWinsServers[@]} ] and using default domain name [ $DEFAULT_DOMAIN_NAME ]"
+ setDnsServersAndDomainName aNameServers[@] "$DEFAULT_DOMAIN_NAME" aWinsServers[@] aSearchDomains[@]
+ else
+ logMessage "WARNING: No DNS information received from OpenVPN, so no network configuration changes need to be made."
+ if ${ARG_MONITOR_NETWORK_CONFIGURATION} ; then
+ logMessage "WARNING: Will NOT monitor for other network configuration changes."
+ fi
+ logDnsInfoNoChanges
+ flushDNSCache
+ fi
+
+ return 0
+}
+
+##########################################################################################
+flushDNSCache()
+{
+ if ${ARG_FLUSH_DNS_CACHE} ; then
+ if [ "${OSVER}" = "10.4" ] ; then
+
+ if [ -f /usr/sbin/lookupd ] ; then
+ set +e # we will catch errors from lookupd
+ /usr/sbin/lookupd -flushcache
+ if [ $? != 0 ] ; then
+ logMessage "WARNING: Unable to flush the DNS cache via lookupd"
+ else
+ logMessage "Flushed the DNS cache via lookupd"
+ fi
+ set -e # bash should again fail on errors
+ else
+ logMessage "WARNING: /usr/sbin/lookupd not present. Not flushing the DNS cache"
+ fi
+
+ else
+
+ if [ -f /usr/bin/dscacheutil ] ; then
+ set +e # we will catch errors from dscacheutil
+ /usr/bin/dscacheutil -flushcache
+ if [ $? != 0 ] ; then
+ logMessage "WARNING: Unable to flush the DNS cache via dscacheutil"
+ else
+ logMessage "Flushed the DNS cache via dscacheutil"
+ fi
+ set -e # bash should again fail on errors
+ else
+ logMessage "WARNING: /usr/bin/dscacheutil not present. Not flushing the DNS cache via dscacheutil"
+ fi
+
+ if [ -f /usr/sbin/discoveryutil ] ; then
+ set +e # we will catch errors from discoveryutil
+ /usr/sbin/discoveryutil udnsflushcaches
+ if [ $? != 0 ] ; then
+ logMessage "WARNING: Unable to flush the DNS cache via discoveryutil udnsflushcaches"
+ else
+ logMessage "Flushed the DNS cache via discoveryutil udnsflushcaches"
+ fi
+ /usr/sbin/discoveryutil mdnsflushcache
+ if [ $? != 0 ] ; then
+ logMessage "WARNING: Unable to flush the DNS cache via discoveryutil mdnsflushcache"
+ else
+ logMessage "Flushed the DNS cache via discoveryutil mdnsflushcache"
+ fi
+ set -e # bash should again fail on errors
+ else
+ logMessage "/usr/sbin/discoveryutil not present. Not flushing the DNS cache via discoveryutil"
+ fi
+
+ set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
+ hands_off_ps="$( ps -ax | grep HandsOffDaemon | grep -v grep.HandsOffDaemon )"
+ set -e # We instruct bash that it CAN again fail on errors
+ if [ "${hands_off_ps}" = "" ] ; then
+ if [ -f /usr/bin/killall ] ; then
+ set +e # ignore errors if mDNSResponder isn't currently running
+ /usr/bin/killall -HUP mDNSResponder
+ if [ $? != 0 ] ; then
+ logMessage "mDNSResponder not running. Not notifying it that the DNS cache was flushed"
+ else
+ logMessage "Notified mDNSResponder that the DNS cache was flushed"
+ fi
+ set -e # bash should again fail on errors
+ else
+ logMessage "WARNING: /usr/bin/killall not present. Not notifying mDNSResponder that the DNS cache was flushed"
+ fi
+ else
+ logMessage "WARNING: Hands Off is running. Not notifying mDNSResponder that the DNS cache was flushed"
+ fi
+
+ fi
+ fi
+}
+
+
+##########################################################################################
+# log information about the DNS settings
+# @param String Manual DNS_SA
+# @param String New DNS_SA
+logDnsInfo() {
+
+ log_dns_info_manual_dns_sa="$1"
+ log_dns_info_new_dns_sa="$2"
+
+ if [ "${log_dns_info_manual_dns_sa}" != "" ] ; then
+ logMessage "DNS servers '${log_dns_info_manual_dns_sa}' were set manually"
+ if [ "${log_dns_info_manual_dns_sa}" != "${log_dns_info_new_dns_sa}" ] ; then
+ logMessage "WARNING: that setting is being ignored by OS X; '${log_dns_info_new_dns_sa}' is being used."
+ fi
+ fi
+
+ if [ "${log_dns_info_new_dns_sa}" != "" ] ; then
+ logMessage "DNS servers '${log_dns_info_new_dns_sa}' will be used for DNS queries when the VPN is active"
+ if [ "${log_dns_info_new_dns_sa}" == "127.0.0.1" ] ; then
+ logMessage "NOTE: DNS server 127.0.0.1 often is used inside virtual machines (e.g., 'VirtualBox', 'Parallels', or 'VMWare'). The actual VPN server may be specified by the host machine. This DNS server setting may cause DNS queries to fail or be intercepted or falsified. Specify only known public DNS servers or DNS servers located on the VPN network to avoid such problems."
+ else
+ set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
+ serversContainLoopback="$( echo "${log_dns_info_new_dns_sa}" | grep "127.0.0.1" )"
+ set -e # We instruct bash that it CAN again fail on errors
+ if [ "${serversContainLoopback}" != "" ] ; then
+ logMessage "NOTE: DNS server 127.0.0.1 often is used inside virtual machines (e.g., 'VirtualBox', 'Parallels', or 'VMWare'). The actual VPN server may be specified by the host machine. If used, 127.0.0.1 may cause DNS queries to fail or be intercepted or falsified. Specify only known public DNS servers or DNS servers located on the VPN network to avoid such problems."
+ else
+ readonly knownPublicDnsServers="$( cat "${FREE_PUBLIC_DNS_SERVERS_LIST_PATH}" )"
+ knownDnsServerNotFound="true"
+ unknownDnsServerFound="false"
+ for server in ${log_dns_info_new_dns_sa} ; do
+ set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
+ serverIsKnown="$( echo "${knownPublicDnsServers}" | grep "${server}" )"
+ set -e # We instruct bash that it CAN again fail on errors
+ if [ "${serverIsKnown}" != "" ] ; then
+ knownDnsServerNotFound="false"
+ else
+ unknownDnsServerFound="true"
+ fi
+ done
+ if ${knownDnsServerNotFound} ; then
+ logMessage "NOTE: The DNS servers do not include any free public DNS servers known to Bitmask. This may cause DNS queries to fail or be intercepted or falsified even if they are directed through the VPN. Specify only known public DNS servers or DNS servers located on the VPN network to avoid such problems."
+ else
+ if ${unknownDnsServerFound} ; then
+ logMessage "NOTE: The DNS servers include one or more free public DNS servers known to Bitmask and one or more DNS servers not known to Bitmask. If used, the DNS servers not known to Bitmask may cause DNS queries to fail or be intercepted or falsified even if they are directed through the VPN. Specify only known public DNS servers or DNS servers located on the VPN network to avoid such problems."
+ else
+ logMessage "The DNS servers include only free public DNS servers known to Bitmask."
+ fi
+ fi
+ fi
+ fi
+ else
+ logMessage "WARNING: There are no DNS servers in this computer's new network configuration. This computer or a DHCP server that this computer uses may be configured incorrectly."
+ fi
+}
+
+logDnsInfoNoChanges() {
+# log information about DNS settings if they are not changing
+
+ set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
+
+ PSID="$( scutil <<-EOF |
+ open
+ show State:/Network/Global/IPv4
+ quit
+EOF
+grep PrimaryService | sed -e 's/.*PrimaryService : //'
+)"
+
+ readonly LOGDNSINFO_MAN_DNS_CONFIG="$( scutil <<-EOF |
+ open
+ show Setup:/Network/Service/${PSID}/DNS
+ quit
+EOF
+sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' '
+)"
+
+ readonly LOGDNSINFO_CUR_DNS_CONFIG="$( scutil <<-EOF |
+ open
+ show State:/Network/Global/DNS
+ quit
+EOF
+sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' '
+)"
+
+ if echo "${LOGDNSINFO_MAN_DNS_CONFIG}" | grep -q "ServerAddresses" ; then
+ readonly LOGDNSINFO_MAN_DNS_SA="$( trim "$( echo "${LOGDNSINFO_MAN_DNS_CONFIG}" | sed -e 's/^.*ServerAddresses[^{]*{[[:space:]]*\([^}]*\)[[:space:]]*}.*$/\1/g' )" )"
+ else
+ readonly LOGDNSINFO_MAN_DNS_SA="";
+ fi
+
+ if echo "${LOGDNSINFO_CUR_DNS_CONFIG}" | grep -q "ServerAddresses" ; then
+ readonly LOGDNSINFO_CUR_DNS_SA="$( trim "$( echo "${LOGDNSINFO_CUR_DNS_CONFIG}" | sed -e 's/^.*ServerAddresses[^{]*{[[:space:]]*\([^}]*\)[[:space:]]*}.*$/\1/g' )" )"
+ else
+ readonly LOGDNSINFO_CUR_DNS_SA="";
+ fi
+
+ set -e # resume abort on error
+
+ logDnsInfo "${LOGDNSINFO_MAN_DNS_SA}" "${LOGDNSINFO_CUR_DNS_SA}"
+}
+
+##########################################################################################
+#
+# START OF SCRIPT
+#
+##########################################################################################
+
+trap "" TSTP
+trap "" HUP
+trap "" INT
+export PATH="/bin:/sbin:/usr/sbin:/usr/bin"
+
+readonly OUR_NAME="$( basename "${0}" )"
+
+logMessage "**********************************************"
+logMessage "Start of output from ${OUR_NAME}"
+
+# Process optional arguments (if any) for the script
+# Each one begins with a "-"
+# They come from Bitmask, and come first, before the OpenVPN arguments
+# So we set ARG_ script variables to their values and shift them out of the argument list
+# When we're done, only the OpenVPN arguments remain for the rest of the script to use
+ARG_TAP="false"
+ARG_WAIT_FOR_DHCP_IF_TAP="false"
+ARG_RESTORE_ON_DNS_RESET="false"
+ARG_FLUSH_DNS_CACHE="false"
+ARG_IGNORE_OPTION_FLAGS=""
+ARG_EXTRA_LOGGING="false"
+ARG_MONITOR_NETWORK_CONFIGURATION="false"
+ARG_DO_NO_USE_DEFAULT_DOMAIN="false"
+ARG_PREPEND_DOMAIN_NAME="false"
+ARG_RESET_PRIMARY_INTERFACE_ON_DISCONNECT="false"
+ARG_TB_PATH="/Applications/Bitmask.app"
+ARG_RESTORE_ON_WINS_RESET="false"
+ARG_DISABLE_IPV6_ON_TUN="false"
+ARG_ENABLE_IPV6_ON_TAP="false"
+
+# Handle the arguments we know about by setting ARG_ script variables to their values, then shift them out
+while [ {$#} ] ; do
+ if [ "$1" = "-6" ] ; then # -6 = ARG_ENABLE_IPV6_ON_TAP (for TAP connections only)
+ ARG_ENABLE_IPV6_ON_TAP="true"
+ shift
+ elif [ "$1" = "-9" ] ; then # -9 = ARG_DISABLE_IPV6_ON_TUN (for TUN connections only)
+ ARG_DISABLE_IPV6_ON_TUN="true"
+ shift
+ elif [ "$1" = "-a" ] ; then # -a = ARG_TAP
+ ARG_TAP="true"
+ shift
+ elif [ "$1" = "-b" ] ; then # -b = ARG_WAIT_FOR_DHCP_IF_TAP
+ ARG_WAIT_FOR_DHCP_IF_TAP="true"
+ shift
+ elif [ "$1" = "-d" ] ; then # -d = ARG_RESTORE_ON_DNS_RESET
+ ARG_RESTORE_ON_DNS_RESET="true"
+ shift
+ elif [ "$1" = "-f" ] ; then # -f = ARG_FLUSH_DNS_CACHE
+ ARG_FLUSH_DNS_CACHE="true"
+ shift
+ elif [ "${1:0:2}" = "-i" ] ; then # -i arguments are for leasewatcher
+ ARG_IGNORE_OPTION_FLAGS="${1}"
+ shift
+ elif [ "$1" = "-l" ] ; then # -l = ARG_EXTRA_LOGGING
+ ARG_EXTRA_LOGGING="true"
+ shift
+ elif [ "$1" = "-m" ] ; then # -m = ARG_MONITOR_NETWORK_CONFIGURATION
+ ARG_MONITOR_NETWORK_CONFIGURATION="true"
+ shift
+ elif [ "$1" = "-n" ] ; then # -n = ARG_DO_NO_USE_DEFAULT_DOMAIN
+ ARG_DO_NO_USE_DEFAULT_DOMAIN="true"
+ shift
+ elif [ "$1" = "-p" ] ; then # -p = ARG_PREPEND_DOMAIN_NAME
+ ARG_PREPEND_DOMAIN_NAME="true"
+ shift
+ elif [ "${1:0:2}" = "-p" ] ; then # -p arguments are for process-network-changes
+ ARG_IGNORE_OPTION_FLAGS="${1}"
+ shift
+ elif [ "$1" = "-r" ] ; then # -r = ARG_RESET_PRIMARY_INTERFACE_ON_DISCONNECT
+ ARG_RESET_PRIMARY_INTERFACE_ON_DISCONNECT="true"
+ shift
+ elif [ "${1:0:2}" = "-t" ] ; then
+ ARG_TB_PATH="${1:2}" # -t path of Bitmask.app
+ shift
+ elif [ "$1" = "-w" ] ; then # -w = ARG_RESTORE_ON_WINS_RESET
+ ARG_RESTORE_ON_WINS_RESET="true"
+ shift
+ else
+ if [ "${1:0:1}" = "-" ] ; then # Shift out Bitmask arguments (they start with "-") that we don't understand
+ shift # so the rest of the script sees only the OpenVPN arguments
+ else
+ break
+ fi
+ fi
+done
+
+readonly ARG_MONITOR_NETWORK_CONFIGURATION ARG_RESTORE_ON_DNS_RESET ARG_RESTORE_ON_WINS_RESET ARG_TAP ARG_PREPEND_DOMAIN_NAME ARG_FLUSH_DNS_CACHE ARG_RESET_PRIMARY_INTERFACE_ON_DISCONNECT ARG_IGNORE_OPTION_FLAGS
+
+# Note: The script log path name is constructed from the path of the regular config file, not the shadow copy
+# if the config is shadow copy, e.g. /Library/Application Support/Bitmask/Users/Jonathan/Folder/Subfolder/config.ovpn
+# then convert to regular config /Users/Jonathan/Library/Application Support/Bitmask/Configurations/Folder/Subfolder/config.ovpn
+# to get the script log path
+# Note: "/Users/..." works even if the home directory has a different path; it is used in the name of the log file, and is not used as a path to get to anything.
+readonly TBALTPREFIX="/Library/Application Support/Bitmask/Users/"
+readonly TBALTPREFIXLEN="${#TBALTPREFIX}"
+readonly TBCONFIGSTART="${config:0:$TBALTPREFIXLEN}"
+if [ "$TBCONFIGSTART" = "$TBALTPREFIX" ] ; then
+ readonly TBBASE="${config:$TBALTPREFIXLEN}"
+ readonly TBSUFFIX="${TBBASE#*/}"
+ readonly TBUSERNAME="${TBBASE%%/*}"
+ readonly TBCONFIG="/Users/$TBUSERNAME/Library/Application Support/Bitmask/Configurations/$TBSUFFIX"
+else
+ readonly TBCONFIG="${config}"
+fi
+
+readonly CONFIG_PATH_DASHES_SLASHES="$( echo "${TBCONFIG}" | sed -e 's/-/--/g' | sed -e 's/\//-S/g' )"
+readonly SCRIPT_LOG_FILE="/Library/Application Support/Bitmask/Logs/${CONFIG_PATH_DASHES_SLASHES}.script.log"
+
+readonly TB_RESOURCES_PATH="${ARG_TB_PATH}/Contents/Resources"
+readonly FREE_PUBLIC_DNS_SERVERS_LIST_PATH="${TB_RESOURCES_PATH}/FreePublicDnsServersList.txt"
+
+# These scripts use a launchd .plist to set up to monitor the network configuration.
+#
+# If Bitmask.app is located in /Applications, we load the launchd .plist directly from within the .app.
+#
+# If Bitmask.app is not located in /Applications (i.e., we are debugging), we create a modified version of the launchd .plist and use
+# that modified copy in the 'launchctl load' command. (The modification is that the path to process-network-changes or leasewatch program
+# in the .plist is changed to point to the copy of the program that is inside the running Bitmask.)
+#
+# The variables involved in this are set up here:
+#
+# LEASEWATCHER_PLIST_PATH is the path of the .plist to use in the 'launchctl load' command
+# LEASEWATCHER_TEMPLATE_PATH is an empty string if we load the .plist directly from within the .app,
+# or it is the path to the original .plist inside the .app which we copy and modify
+# REMOVE_LEASEWATCHER_PLIST is "true" if a modified .plist was used and should be deleted after it is unloaded
+# or "false' if the plist was loaded directly from the .app
+#
+# LEASEWATCHER_PLIST_PATH and REMOVE_LEASEWATCHER_PLIST are passed to the other scripts via the scutil State:/Network/OpenVPN mechanism
+
+if [ "${ARG_IGNORE_OPTION_FLAGS:0:2}" = "-p" ] ; then
+ readonly LEASEWATCHER_PLIST="ProcessNetworkChanges.plist"
+else
+ readonly LEASEWATCHER_PLIST="LeaseWatch.plist"
+fi
+if [ "${ARG_TB_PATH}" = "/Applications/Bitmask.app" ] ; then
+ readonly LEASEWATCHER_PLIST_PATH="${TB_RESOURCES_PATH}/${LEASEWATCHER_PLIST}"
+ readonly LEASEWATCHER_TEMPLATE_PATH=""
+ readonly REMOVE_LEASEWATCHER_PLIST="false"
+else
+ readonly LEASEWATCHER_PLIST_PATH="/Library/Application Support/Bitmask/${LEASEWATCHER_PLIST}"
+ readonly LEASEWATCHER_TEMPLATE_PATH="${TB_RESOURCES_PATH}/${LEASEWATCHER_PLIST}"
+ readonly REMOVE_LEASEWATCHER_PLIST="true"
+fi
+
+set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
+readonly OSVER="$( sw_vers | grep 'ProductVersion:' | grep -o '10\.[0-9]*' )"
+set -e # We instruct bash that it CAN again fail on errors
+
+if ${ARG_DO_NO_USE_DEFAULT_DOMAIN} ; then
+ readonly DEFAULT_DOMAIN_NAME=""
+else
+ readonly DEFAULT_DOMAIN_NAME="openvpn"
+fi
+
+bRouteGatewayIsDhcp="false"
+
+# We sleep to allow time for OS X to process network settings
+sleep 2
+
+EXIT_CODE=0
+
+if ${ARG_TAP} ; then
+
+ # IPv6 should be re-enabled only for TUN, not TAP
+ readonly ipv6_disabled_services=""
+ readonly ipv6_disabled_services_encoded=""
+
+ # Still need to do: Look for route-gateway dhcp (TAP isn't always DHCP)
+ bRouteGatewayIsDhcp="false"
+ if [ -z "${route_vpn_gateway}" -o "$route_vpn_gateway" == "dhcp" -o "$route_vpn_gateway" == "DHCP" ]; then
+ bRouteGatewayIsDhcp="true"
+ fi
+
+ if [ "$bRouteGatewayIsDhcp" == "true" ]; then
+ logDebugMessage "DEBUG: bRouteGatewayIsDhcp is TRUE"
+ if [ -z "$dev" ]; then
+ logMessage "ERROR: Cannot configure TAP interface for DHCP without \$dev being defined. Exiting."
+ # We don't create the "/tmp/bitmask-downscript-needs-to-be-run.txt" file, because the down script does NOT need to be run since we didn't do anything
+ logMessage "End of output from ${OUR_NAME}"
+ logMessage "**********************************************"
+ exit 1
+ fi
+
+ logDebugMessage "DEBUG: About to 'ipconfig set \"$dev\" DHCP"
+ ipconfig set "$dev" DHCP
+ logMessage "Did 'ipconfig set \"$dev\" DHCP'"
+
+ if ${ARG_ENABLE_IPV6_ON_TAP} ; then
+ ipconfig set "$dev" AUTOMATIC-V6
+ logMessage "Did 'ipconfig set \"$dev\" AUTOMATIC-V6'"
+ fi
+
+ if ${ARG_WAIT_FOR_DHCP_IF_TAP} ; then
+ logMessage "Configuring tap DNS via DHCP synchronously"
+ configureDhcpDns
+ else
+ logMessage "Configuring tap DNS via DHCP asynchronously"
+ configureDhcpDns & # This must be run asynchronously; the DHCP lease will not complete until this script exits
+ EXIT_CODE=0
+ fi
+ elif [ "$foreign_option_1" == "" ]; then
+ logMessage "NOTE: No network configuration changes need to be made."
+ if ${ARG_MONITOR_NETWORK_CONFIGURATION} ; then
+ logMessage "WARNING: Will NOT monitor for other network configuration changes."
+ fi
+ if ${ARG_ENABLE_IPV6_ON_TAP} ; then
+ logMessage "WARNING: Will NOT set up IPv6 on TAP device because it does not use DHCP."
+ fi
+ logDnsInfoNoChanges
+ flushDNSCache
+ else
+ if ${ARG_ENABLE_IPV6_ON_TAP} ; then
+ logMessage "WARNING: Will NOT set up IPv6 on TAP device because it does not use DHCP."
+ fi
+ logMessage "Configuring tap DNS via OpenVPN"
+ configureOpenVpnDns
+ EXIT_CODE=$?
+ fi
+else
+ if [ "$foreign_option_1" == "" ]; then
+ logMessage "NOTE: No network configuration changes need to be made."
+ if ${ARG_MONITOR_NETWORK_CONFIGURATION} ; then
+ logMessage "WARNING: Will NOT monitor for other network configuration changes."
+ fi
+ if ${ARG_DISABLE_IPV6_ON_TUN} ; then
+ logMessage "WARNING: Will NOT disable IPv6 settings."
+ fi
+ logDnsInfoNoChanges
+ flushDNSCache
+ else
+
+ ipv6_disabled_services=""
+ if ${ARG_DISABLE_IPV6_ON_TUN} ; then
+ ipv6_disabled_services="$( disable_ipv6 )"
+ if [ "$ipv6_disabled_services" != "" ] ; then
+ printf %s "$ipv6_disabled_services
+" | \
+ while IFS= read -r dipv6_service ; do
+ logMessage "Disabled IPv6 for '$dipv6_service'"
+ done
+ fi
+ fi
+ readonly ipv6_disabled_services
+ # Note '\n' is translated into '\t' so it is all on one line, because grep and sed only work with single lines
+ readonly ipv6_disabled_services_encoded="$( echo "$ipv6_disabled_services" | tr '\n' '\t' )"
+
+ configureOpenVpnDns
+ EXIT_CODE=$?
+ fi
+fi
+
+touch "/tmp/bitmask-downscript-needs-to-be-run.txt"
+
+logMessage "End of output from ${OUR_NAME}"
+logMessage "**********************************************"
+
+exit $EXIT_CODE
diff --git a/pkg/osx/daemon/_metadata.py b/pkg/osx/daemon/_metadata.py
new file mode 100644
index 00000000..88843df7
--- /dev/null
+++ b/pkg/osx/daemon/_metadata.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+
+# daemon/_metadata.py
+# Part of ‘python-daemon’, an implementation of PEP 3143.
+#
+# Copyright © 2008–2015 Ben Finney <ben+python@benfinney.id.au>
+#
+# This is free software: you may copy, modify, and/or distribute this work
+# under the terms of the Apache License, version 2.0 as published by the
+# Apache Software Foundation.
+# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details.
+
+""" Package metadata for the ‘python-daemon’ distribution. """
+
+from __future__ import (absolute_import, unicode_literals)
+
+import json
+import re
+import collections
+import datetime
+
+import pkg_resources
+
+
+distribution_name = "python-daemon"
+version_info_filename = "version_info.json"
+
+
+def get_distribution_version_info(filename=version_info_filename):
+ """ Get the version info from the installed distribution.
+
+ :param filename: Base filename of the version info resource.
+ :return: The version info as a mapping of fields. If the
+ distribution is not available, the mapping is empty.
+
+ The version info is stored as a metadata file in the
+ distribution.
+
+ """
+ version_info = {
+ 'release_date': "UNKNOWN",
+ 'version': "UNKNOWN",
+ 'maintainer': "UNKNOWN",
+ }
+
+ try:
+ distribution = pkg_resources.get_distribution(distribution_name)
+ except pkg_resources.DistributionNotFound:
+ distribution = None
+
+ if distribution is not None:
+ if distribution.has_metadata(version_info_filename):
+ content = distribution.get_metadata(version_info_filename)
+ version_info = json.loads(content)
+
+ return version_info
+
+version_info = get_distribution_version_info()
+
+version_installed = version_info['version']
+
+
+rfc822_person_regex = re.compile(
+ "^(?P<name>[^<]+) <(?P<email>[^>]+)>$")
+
+ParsedPerson = collections.namedtuple('ParsedPerson', ['name', 'email'])
+
+
+def parse_person_field(value):
+ """ Parse a person field into name and email address.
+
+ :param value: The text value specifying a person.
+ :return: A 2-tuple (name, email) for the person's details.
+
+ If the `value` does not match a standard person with email
+ address, the `email` item is ``None``.
+
+ """
+ result = (None, None)
+
+ match = rfc822_person_regex.match(value)
+ if len(value):
+ if match is not None:
+ result = ParsedPerson(
+ name=match.group('name'),
+ email=match.group('email'))
+ else:
+ result = ParsedPerson(name=value, email=None)
+
+ return result
+
+author_name = "Ben Finney"
+author_email = "ben+python@benfinney.id.au"
+author = "{name} <{email}>".format(name=author_name, email=author_email)
+
+
+class YearRange:
+ """ A range of years spanning a period. """
+
+ def __init__(self, begin, end=None):
+ self.begin = begin
+ self.end = end
+
+ def __unicode__(self):
+ text = "{range.begin:04d}".format(range=self)
+ if self.end is not None:
+ if self.end > self.begin:
+ text = "{range.begin:04d}–{range.end:04d}".format(range=self)
+ return text
+
+ __str__ = __unicode__
+
+
+def make_year_range(begin_year, end_date=None):
+ """ Construct the year range given a start and possible end date.
+
+ :param begin_date: The beginning year (text) for the range.
+ :param end_date: The end date (text, ISO-8601 format) for the
+ range, or a non-date token string.
+ :return: The range of years as a `YearRange` instance.
+
+ If the `end_date` is not a valid ISO-8601 date string, the
+ range has ``None`` for the end year.
+
+ """
+ begin_year = int(begin_year)
+
+ try:
+ end_date = datetime.datetime.strptime(end_date, "%Y-%m-%d")
+ except (TypeError, ValueError):
+ # Specified end_date value is not a valid date.
+ end_year = None
+ else:
+ end_year = end_date.year
+
+ year_range = YearRange(begin=begin_year, end=end_year)
+
+ return year_range
+
+copyright_year_begin = "2001"
+build_date = version_info['release_date']
+copyright_year_range = make_year_range(copyright_year_begin, build_date)
+
+copyright = "Copyright © {year_range} {author} and others".format(
+ year_range=copyright_year_range, author=author)
+license = "Apache-2"
+url = "https://alioth.debian.org/projects/python-daemon/"
+
+
+# Local variables:
+# coding: utf-8
+# mode: python
+# End:
+# vim: fileencoding=utf-8 filetype=python :
diff --git a/pkg/osx/daemon/daemon.py b/pkg/osx/daemon/daemon.py
new file mode 100644
index 00000000..7ca8770e
--- /dev/null
+++ b/pkg/osx/daemon/daemon.py
@@ -0,0 +1,927 @@
+# -*- coding: utf-8 -*-
+
+# daemon/daemon.py
+# Part of ‘python-daemon’, an implementation of PEP 3143.
+#
+# Copyright © 2008–2015 Ben Finney <ben+python@benfinney.id.au>
+# Copyright © 2007–2008 Robert Niederreiter, Jens Klein
+# Copyright © 2004–2005 Chad J. Schroeder
+# Copyright © 2003 Clark Evans
+# Copyright © 2002 Noah Spurrier
+# Copyright © 2001 Jürgen Hermann
+#
+# This is free software: you may copy, modify, and/or distribute this work
+# under the terms of the Apache License, version 2.0 as published by the
+# Apache Software Foundation.
+# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details.
+
+""" Daemon process behaviour.
+ """
+
+from __future__ import (absolute_import, unicode_literals)
+
+import os
+import sys
+import resource
+import errno
+import signal
+import socket
+import atexit
+try:
+ # Python 2 has both ‘str’ (bytes) and ‘unicode’ (text).
+ basestring = basestring
+ unicode = unicode
+except NameError:
+ # Python 3 names the Unicode data type ‘str’.
+ basestring = str
+ unicode = str
+
+
+class DaemonError(Exception):
+ """ Base exception class for errors from this module. """
+
+ def __init__(self, *args, **kwargs):
+ self._chain_from_context()
+
+ super(DaemonError, self).__init__(*args, **kwargs)
+
+ def _chain_from_context(self):
+ _chain_exception_from_existing_exception_context(self, as_cause=True)
+
+
+class DaemonOSEnvironmentError(DaemonError, OSError):
+ """ Exception raised when daemon OS environment setup receives error. """
+
+
+class DaemonProcessDetachError(DaemonError, OSError):
+ """ Exception raised when process detach fails. """
+
+
+class DaemonContext:
+ """ Context for turning the current program into a daemon process.
+
+ A `DaemonContext` instance represents the behaviour settings and
+ process context for the program when it becomes a daemon. The
+ behaviour and environment is customised by setting options on the
+ instance, before calling the `open` method.
+
+ Each option can be passed as a keyword argument to the `DaemonContext`
+ constructor, or subsequently altered by assigning to an attribute on
+ the instance at any time prior to calling `open`. That is, for
+ options named `wibble` and `wubble`, the following invocation::
+
+ foo = daemon.DaemonContext(wibble=bar, wubble=baz)
+ foo.open()
+
+ is equivalent to::
+
+ foo = daemon.DaemonContext()
+ foo.wibble = bar
+ foo.wubble = baz
+ foo.open()
+
+ The following options are defined.
+
+ `files_preserve`
+ :Default: ``None``
+
+ List of files that should *not* be closed when starting the
+ daemon. If ``None``, all open file descriptors will be closed.
+
+ Elements of the list are file descriptors (as returned by a file
+ object's `fileno()` method) or Python `file` objects. Each
+ specifies a file that is not to be closed during daemon start.
+
+ `chroot_directory`
+ :Default: ``None``
+
+ Full path to a directory to set as the effective root directory of
+ the process. If ``None``, specifies that the root directory is not
+ to be changed.
+
+ `working_directory`
+ :Default: ``'/'``
+
+ Full path of the working directory to which the process should
+ change on daemon start.
+
+ Since a filesystem cannot be unmounted if a process has its
+ current working directory on that filesystem, this should either
+ be left at default or set to a directory that is a sensible “home
+ directory” for the daemon while it is running.
+
+ `umask`
+ :Default: ``0``
+
+ File access creation mask (“umask”) to set for the process on
+ daemon start.
+
+ A daemon should not rely on the parent process's umask value,
+ which is beyond its control and may prevent creating a file with
+ the required access mode. So when the daemon context opens, the
+ umask is set to an explicit known value.
+
+ If the conventional value of 0 is too open, consider setting a
+ value such as 0o022, 0o027, 0o077, or another specific value.
+ Otherwise, ensure the daemon creates every file with an
+ explicit access mode for the purpose.
+
+ `pidfile`
+ :Default: ``None``
+
+ Context manager for a PID lock file. When the daemon context opens
+ and closes, it enters and exits the `pidfile` context manager.
+
+ `detach_process`
+ :Default: ``None``
+
+ If ``True``, detach the process context when opening the daemon
+ context; if ``False``, do not detach.
+
+ If unspecified (``None``) during initialisation of the instance,
+ this will be set to ``True`` by default, and ``False`` only if
+ detaching the process is determined to be redundant; for example,
+ in the case when the process was started by `init`, by `initd`, or
+ by `inetd`.
+
+ `signal_map`
+ :Default: system-dependent
+
+ Mapping from operating system signals to callback actions.
+
+ The mapping is used when the daemon context opens, and determines
+ the action for each signal's signal handler:
+
+ * A value of ``None`` will ignore the signal (by setting the
+ signal action to ``signal.SIG_IGN``).
+
+ * A string value will be used as the name of an attribute on the
+ ``DaemonContext`` instance. The attribute's value will be used
+ as the action for the signal handler.
+
+ * Any other value will be used as the action for the
+ signal handler. See the ``signal.signal`` documentation
+ for details of the signal handler interface.
+
+ The default value depends on which signals are defined on the
+ running system. Each item from the list below whose signal is
+ actually defined in the ``signal`` module will appear in the
+ default map:
+
+ * ``signal.SIGTTIN``: ``None``
+
+ * ``signal.SIGTTOU``: ``None``
+
+ * ``signal.SIGTSTP``: ``None``
+
+ * ``signal.SIGTERM``: ``'terminate'``
+
+ Depending on how the program will interact with its child
+ processes, it may need to specify a signal map that
+ includes the ``signal.SIGCHLD`` signal (received when a
+ child process exits). See the specific operating system's
+ documentation for more detail on how to determine what
+ circumstances dictate the need for signal handlers.
+
+ `uid`
+ :Default: ``os.getuid()``
+
+ `gid`
+ :Default: ``os.getgid()``
+
+ The user ID (“UID”) value and group ID (“GID”) value to switch
+ the process to on daemon start.
+
+ The default values, the real UID and GID of the process, will
+ relinquish any effective privilege elevation inherited by the
+ process.
+
+ `prevent_core`
+ :Default: ``True``
+
+ If true, prevents the generation of core files, in order to avoid
+ leaking sensitive information from daemons run as `root`.
+
+ `stdin`
+ :Default: ``None``
+
+ `stdout`
+ :Default: ``None``
+
+ `stderr`
+ :Default: ``None``
+
+ Each of `stdin`, `stdout`, and `stderr` is a file-like object
+ which will be used as the new file for the standard I/O stream
+ `sys.stdin`, `sys.stdout`, and `sys.stderr` respectively. The file
+ should therefore be open, with a minimum of mode 'r' in the case
+ of `stdin`, and mimimum of mode 'w+' in the case of `stdout` and
+ `stderr`.
+
+ If the object has a `fileno()` method that returns a file
+ descriptor, the corresponding file will be excluded from being
+ closed during daemon start (that is, it will be treated as though
+ it were listed in `files_preserve`).
+
+ If ``None``, the corresponding system stream is re-bound to the
+ file named by `os.devnull`.
+
+ """
+
+ __metaclass__ = type
+
+ def __init__(
+ self,
+ chroot_directory=None,
+ working_directory="/",
+ umask=0,
+ uid=None,
+ gid=None,
+ prevent_core=True,
+ detach_process=None,
+ files_preserve=None,
+ pidfile=None,
+ stdin=None,
+ stdout=None,
+ stderr=None,
+ signal_map=None,
+ ):
+ """ Set up a new instance. """
+ self.chroot_directory = chroot_directory
+ self.working_directory = working_directory
+ self.umask = umask
+ self.prevent_core = prevent_core
+ self.files_preserve = files_preserve
+ self.pidfile = pidfile
+ self.stdin = stdin
+ self.stdout = stdout
+ self.stderr = stderr
+
+ if uid is None:
+ uid = os.getuid()
+ self.uid = uid
+ if gid is None:
+ gid = os.getgid()
+ self.gid = gid
+
+ if detach_process is None:
+ detach_process = is_detach_process_context_required()
+ self.detach_process = detach_process
+
+ if signal_map is None:
+ signal_map = make_default_signal_map()
+ self.signal_map = signal_map
+
+ self._is_open = False
+
+ @property
+ def is_open(self):
+ """ ``True`` if the instance is currently open. """
+ return self._is_open
+
+ def open(self):
+ """ Become a daemon process.
+
+ :return: ``None``.
+
+ Open the daemon context, turning the current program into a daemon
+ process. This performs the following steps:
+
+ * If this instance's `is_open` property is true, return
+ immediately. This makes it safe to call `open` multiple times on
+ an instance.
+
+ * If the `prevent_core` attribute is true, set the resource limits
+ for the process to prevent any core dump from the process.
+
+ * If the `chroot_directory` attribute is not ``None``, set the
+ effective root directory of the process to that directory (via
+ `os.chroot`).
+
+ This allows running the daemon process inside a “chroot gaol”
+ as a means of limiting the system's exposure to rogue behaviour
+ by the process. Note that the specified directory needs to
+ already be set up for this purpose.
+
+ * Set the process UID and GID to the `uid` and `gid` attribute
+ values.
+
+ * Close all open file descriptors. This excludes those listed in
+ the `files_preserve` attribute, and those that correspond to the
+ `stdin`, `stdout`, or `stderr` attributes.
+
+ * Change current working directory to the path specified by the
+ `working_directory` attribute.
+
+ * Reset the file access creation mask to the value specified by
+ the `umask` attribute.
+
+ * If the `detach_process` option is true, detach the current
+ process into its own process group, and disassociate from any
+ controlling terminal.
+
+ * Set signal handlers as specified by the `signal_map` attribute.
+
+ * If any of the attributes `stdin`, `stdout`, `stderr` are not
+ ``None``, bind the system streams `sys.stdin`, `sys.stdout`,
+ and/or `sys.stderr` to the files represented by the
+ corresponding attributes. Where the attribute has a file
+ descriptor, the descriptor is duplicated (instead of re-binding
+ the name).
+
+ * If the `pidfile` attribute is not ``None``, enter its context
+ manager.
+
+ * Mark this instance as open (for the purpose of future `open` and
+ `close` calls).
+
+ * Register the `close` method to be called during Python's exit
+ processing.
+
+ When the function returns, the running program is a daemon
+ process.
+
+ """
+ if self.is_open:
+ return
+
+ if self.chroot_directory is not None:
+ change_root_directory(self.chroot_directory)
+
+ if self.prevent_core:
+ prevent_core_dump()
+
+ change_file_creation_mask(self.umask)
+ change_working_directory(self.working_directory)
+ change_process_owner(self.uid, self.gid)
+
+ if self.detach_process:
+ detach_process_context()
+
+ signal_handler_map = self._make_signal_handler_map()
+ set_signal_handlers(signal_handler_map)
+
+ exclude_fds = self._get_exclude_file_descriptors()
+ close_all_open_files(exclude=exclude_fds)
+
+ redirect_stream(sys.stdin, self.stdin)
+ redirect_stream(sys.stdout, self.stdout)
+ redirect_stream(sys.stderr, self.stderr)
+
+ if self.pidfile is not None:
+ self.pidfile.__enter__()
+
+ self._is_open = True
+
+ register_atexit_function(self.close)
+
+ def __enter__(self):
+ """ Context manager entry point. """
+ self.open()
+ return self
+
+ def close(self):
+ """ Exit the daemon process context.
+
+ :return: ``None``.
+
+ Close the daemon context. This performs the following steps:
+
+ * If this instance's `is_open` property is false, return
+ immediately. This makes it safe to call `close` multiple times
+ on an instance.
+
+ * If the `pidfile` attribute is not ``None``, exit its context
+ manager.
+
+ * Mark this instance as closed (for the purpose of future `open`
+ and `close` calls).
+
+ """
+ if not self.is_open:
+ return
+
+ if self.pidfile is not None:
+ # Follow the interface for telling a context manager to exit,
+ # <URL:http://docs.python.org/library/stdtypes.html#typecontextmanager>.
+ self.pidfile.__exit__(None, None, None)
+
+ self._is_open = False
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ """ Context manager exit point. """
+ self.close()
+
+ def terminate(self, signal_number, stack_frame):
+ """ Signal handler for end-process signals.
+
+ :param signal_number: The OS signal number received.
+ :param stack_frame: The frame object at the point the
+ signal was received.
+ :return: ``None``.
+
+ Signal handler for the ``signal.SIGTERM`` signal. Performs the
+ following step:
+
+ * Raise a ``SystemExit`` exception explaining the signal.
+
+ """
+ exception = SystemExit(
+ "Terminating on signal {signal_number!r}".format(
+ signal_number=signal_number))
+ raise exception
+
+ def _get_exclude_file_descriptors(self):
+ """ Get the set of file descriptors to exclude closing.
+
+ :return: A set containing the file descriptors for the
+ files to be preserved.
+
+ The file descriptors to be preserved are those from the
+ items in `files_preserve`, and also each of `stdin`,
+ `stdout`, and `stderr`. For each item:
+
+ * If the item is ``None``, it is omitted from the return
+ set.
+
+ * If the item's ``fileno()`` method returns a value, that
+ value is in the return set.
+
+ * Otherwise, the item is in the return set verbatim.
+
+ """
+ files_preserve = self.files_preserve
+ if files_preserve is None:
+ files_preserve = []
+ files_preserve.extend(
+ item for item in [self.stdin, self.stdout, self.stderr]
+ if hasattr(item, 'fileno'))
+
+ exclude_descriptors = set()
+ for item in files_preserve:
+ if item is None:
+ continue
+ file_descriptor = _get_file_descriptor(item)
+ if file_descriptor is not None:
+ exclude_descriptors.add(file_descriptor)
+ else:
+ exclude_descriptors.add(item)
+
+ return exclude_descriptors
+
+ def _make_signal_handler(self, target):
+ """ Make the signal handler for a specified target object.
+
+ :param target: A specification of the target for the
+ handler; see below.
+ :return: The value for use by `signal.signal()`.
+
+ If `target` is ``None``, return ``signal.SIG_IGN``. If `target`
+ is a text string, return the attribute of this instance named
+ by that string. Otherwise, return `target` itself.
+
+ """
+ if target is None:
+ result = signal.SIG_IGN
+ elif isinstance(target, unicode):
+ name = target
+ result = getattr(self, name)
+ else:
+ result = target
+
+ return result
+
+ def _make_signal_handler_map(self):
+ """ Make the map from signals to handlers for this instance.
+
+ :return: The constructed signal map for this instance.
+
+ Construct a map from signal numbers to handlers for this
+ context instance, suitable for passing to
+ `set_signal_handlers`.
+
+ """
+ signal_handler_map = dict(
+ (signal_number, self._make_signal_handler(target))
+ for (signal_number, target) in self.signal_map.items())
+ return signal_handler_map
+
+
+def _get_file_descriptor(obj):
+ """ Get the file descriptor, if the object has one.
+
+ :param obj: The object expected to be a file-like object.
+ :return: The file descriptor iff the file supports it; otherwise
+ ``None``.
+
+ The object may be a non-file object. It may also be a
+ file-like object with no support for a file descriptor. In
+ either case, return ``None``.
+
+ """
+ file_descriptor = None
+ if hasattr(obj, 'fileno'):
+ try:
+ file_descriptor = obj.fileno()
+ except ValueError:
+ # The item doesn't support a file descriptor.
+ pass
+
+ return file_descriptor
+
+
+def change_working_directory(directory):
+ """ Change the working directory of this process.
+
+ :param directory: The target directory path.
+ :return: ``None``.
+
+ """
+ try:
+ os.chdir(directory)
+ except Exception as exc:
+ error = DaemonOSEnvironmentError(
+ "Unable to change working directory ({exc})".format(exc=exc))
+ raise error
+
+
+def change_root_directory(directory):
+ """ Change the root directory of this process.
+
+ :param directory: The target directory path.
+ :return: ``None``.
+
+ Set the current working directory, then the process root directory,
+ to the specified `directory`. Requires appropriate OS privileges
+ for this process.
+
+ """
+ try:
+ os.chdir(directory)
+ os.chroot(directory)
+ except Exception as exc:
+ error = DaemonOSEnvironmentError(
+ "Unable to change root directory ({exc})".format(exc=exc))
+ raise error
+
+
+def change_file_creation_mask(mask):
+ """ Change the file creation mask for this process.
+
+ :param mask: The numeric file creation mask to set.
+ :return: ``None``.
+
+ """
+ try:
+ os.umask(mask)
+ except Exception as exc:
+ error = DaemonOSEnvironmentError(
+ "Unable to change file creation mask ({exc})".format(exc=exc))
+ raise error
+
+
+def change_process_owner(uid, gid):
+ """ Change the owning UID and GID of this process.
+
+ :param uid: The target UID for the daemon process.
+ :param gid: The target GID for the daemon process.
+ :return: ``None``.
+
+ Set the GID then the UID of the process (in that order, to avoid
+ permission errors) to the specified `gid` and `uid` values.
+ Requires appropriate OS privileges for this process.
+
+ """
+ try:
+ os.setgid(gid)
+ os.setuid(uid)
+ except Exception as exc:
+ error = DaemonOSEnvironmentError(
+ "Unable to change process owner ({exc})".format(exc=exc))
+ raise error
+
+
+def prevent_core_dump():
+ """ Prevent this process from generating a core dump.
+
+ :return: ``None``.
+
+ Set the soft and hard limits for core dump size to zero. On Unix,
+ this entirely prevents the process from creating core dump.
+
+ """
+ core_resource = resource.RLIMIT_CORE
+
+ try:
+ # Ensure the resource limit exists on this platform, by requesting
+ # its current value.
+ core_limit_prev = resource.getrlimit(core_resource)
+ except ValueError as exc:
+ error = DaemonOSEnvironmentError(
+ "System does not support RLIMIT_CORE resource limit"
+ " ({exc})".format(exc=exc))
+ raise error
+
+ # Set hard and soft limits to zero, i.e. no core dump at all.
+ core_limit = (0, 0)
+ resource.setrlimit(core_resource, core_limit)
+
+
+def detach_process_context():
+ """ Detach the process context from parent and session.
+
+ :return: ``None``.
+
+ Detach from the parent process and session group, allowing the
+ parent to exit while this process continues running.
+
+ Reference: “Advanced Programming in the Unix Environment”,
+ section 13.3, by W. Richard Stevens, published 1993 by
+ Addison-Wesley.
+
+ """
+
+ def fork_then_exit_parent(error_message):
+ """ Fork a child process, then exit the parent process.
+
+ :param error_message: Message for the exception in case of a
+ detach failure.
+ :return: ``None``.
+ :raise DaemonProcessDetachError: If the fork fails.
+
+ """
+ try:
+ pid = os.fork()
+ if pid > 0:
+ os._exit(0)
+ except OSError as exc:
+ error = DaemonProcessDetachError(
+ "{message}: [{exc.errno:d}] {exc.strerror}".format(
+ message=error_message, exc=exc))
+ raise error
+
+ fork_then_exit_parent(error_message="Failed first fork")
+ os.setsid()
+ fork_then_exit_parent(error_message="Failed second fork")
+
+
+def is_process_started_by_init():
+ """ Determine whether the current process is started by `init`.
+
+ :return: ``True`` iff the parent process is `init`; otherwise
+ ``False``.
+
+ The `init` process is the one with process ID of 1.
+
+ """
+ result = False
+
+ init_pid = 1
+ if os.getppid() == init_pid:
+ result = True
+
+ return result
+
+
+def is_socket(fd):
+ """ Determine whether the file descriptor is a socket.
+
+ :param fd: The file descriptor to interrogate.
+ :return: ``True`` iff the file descriptor is a socket; otherwise
+ ``False``.
+
+ Query the socket type of `fd`. If there is no error, the file is a
+ socket.
+
+ """
+ result = False
+
+ file_socket = socket.fromfd(fd, socket.AF_INET, socket.SOCK_RAW)
+
+ try:
+ socket_type = file_socket.getsockopt(
+ socket.SOL_SOCKET, socket.SO_TYPE)
+ except socket.error as exc:
+ exc_errno = exc.args[0]
+ if exc_errno == errno.ENOTSOCK:
+ # Socket operation on non-socket.
+ pass
+ else:
+ # Some other socket error.
+ result = True
+ else:
+ # No error getting socket type.
+ result = True
+
+ return result
+
+
+def is_process_started_by_superserver():
+ """ Determine whether the current process is started by the superserver.
+
+ :return: ``True`` if this process was started by the internet
+ superserver; otherwise ``False``.
+
+ The internet superserver creates a network socket, and
+ attaches it to the standard streams of the child process. If
+ that is the case for this process, return ``True``, otherwise
+ ``False``.
+
+ """
+ result = False
+
+ stdin_fd = sys.__stdin__.fileno()
+ if is_socket(stdin_fd):
+ result = True
+
+ return result
+
+
+def is_detach_process_context_required():
+ """ Determine whether detaching the process context is required.
+
+ :return: ``True`` iff the process is already detached; otherwise
+ ``False``.
+
+ The process environment is interrogated for the following:
+
+ * Process was started by `init`; or
+
+ * Process was started by `inetd`.
+
+ If any of the above are true, the process is deemed to be already
+ detached.
+
+ """
+ result = True
+ if is_process_started_by_init() or is_process_started_by_superserver():
+ result = False
+
+ return result
+
+
+def close_file_descriptor_if_open(fd):
+ """ Close a file descriptor if already open.
+
+ :param fd: The file descriptor to close.
+ :return: ``None``.
+
+ Close the file descriptor `fd`, suppressing an error in the
+ case the file was not open.
+
+ """
+ try:
+ os.close(fd)
+ except EnvironmentError as exc:
+ if exc.errno == errno.EBADF:
+ # File descriptor was not open.
+ pass
+ else:
+ error = DaemonOSEnvironmentError(
+ "Failed to close file descriptor {fd:d} ({exc})".format(
+ fd=fd, exc=exc))
+ raise error
+
+
+MAXFD = 2048
+
+
+def get_maximum_file_descriptors():
+ """ Get the maximum number of open file descriptors for this process.
+
+ :return: The number (integer) to use as the maximum number of open
+ files for this process.
+
+ The maximum is the process hard resource limit of maximum number of
+ open file descriptors. If the limit is “infinity”, a default value
+ of ``MAXFD`` is returned.
+
+ """
+ limits = resource.getrlimit(resource.RLIMIT_NOFILE)
+ result = limits[1]
+ if result == resource.RLIM_INFINITY:
+ result = MAXFD
+ return result
+
+
+def close_all_open_files(exclude=set()):
+ """ Close all open file descriptors.
+
+ :param exclude: Collection of file descriptors to skip when closing
+ files.
+ :return: ``None``.
+
+ Closes every file descriptor (if open) of this process. If
+ specified, `exclude` is a set of file descriptors to *not*
+ close.
+
+ """
+ maxfd = get_maximum_file_descriptors()
+ for fd in reversed(range(maxfd)):
+ if fd not in exclude:
+ close_file_descriptor_if_open(fd)
+
+
+def redirect_stream(system_stream, target_stream):
+ """ Redirect a system stream to a specified file.
+
+ :param standard_stream: A file object representing a standard I/O
+ stream.
+ :param target_stream: The target file object for the redirected
+ stream, or ``None`` to specify the null device.
+ :return: ``None``.
+
+ `system_stream` is a standard system stream such as
+ ``sys.stdout``. `target_stream` is an open file object that
+ should replace the corresponding system stream object.
+
+ If `target_stream` is ``None``, defaults to opening the
+ operating system's null device and using its file descriptor.
+
+ """
+ if target_stream is None:
+ target_fd = os.open(os.devnull, os.O_RDWR)
+ else:
+ target_fd = target_stream.fileno()
+ os.dup2(target_fd, system_stream.fileno())
+
+
+def make_default_signal_map():
+ """ Make the default signal map for this system.
+
+ :return: A mapping from signal number to handler object.
+
+ The signals available differ by system. The map will not contain
+ any signals not defined on the running system.
+
+ """
+ name_map = {
+ 'SIGTSTP': None,
+ 'SIGTTIN': None,
+ 'SIGTTOU': None,
+ 'SIGTERM': 'terminate',
+ }
+ signal_map = dict(
+ (getattr(signal, name), target)
+ for (name, target) in name_map.items()
+ if hasattr(signal, name))
+
+ return signal_map
+
+
+def set_signal_handlers(signal_handler_map):
+ """ Set the signal handlers as specified.
+
+ :param signal_handler_map: A map from signal number to handler
+ object.
+ :return: ``None``.
+
+ See the `signal` module for details on signal numbers and signal
+ handlers.
+
+ """
+ for (signal_number, handler) in signal_handler_map.items():
+ signal.signal(signal_number, handler)
+
+
+def register_atexit_function(func):
+ """ Register a function for processing at program exit.
+
+ :param func: A callable function expecting no arguments.
+ :return: ``None``.
+
+ The function `func` is registered for a call with no arguments
+ at program exit.
+
+ """
+ atexit.register(func)
+
+
+def _chain_exception_from_existing_exception_context(exc, as_cause=False):
+ """ Decorate the specified exception with the existing exception context.
+
+ :param exc: The exception instance to decorate.
+ :param as_cause: If true, the existing context is declared to be
+ the cause of the exception.
+ :return: ``None``.
+
+ :PEP:`344` describes syntax and attributes (`__traceback__`,
+ `__context__`, `__cause__`) for use in exception chaining.
+
+ Python 2 does not have that syntax, so this function decorates
+ the exception with values from the current exception context.
+
+ """
+ (existing_exc_type, existing_exc, existing_traceback) = sys.exc_info()
+ if as_cause:
+ exc.__cause__ = existing_exc
+ else:
+ exc.__context__ = existing_exc
+ exc.__traceback__ = existing_traceback
+
+
+# Local variables:
+# coding: utf-8
+# mode: python
+# End:
+# vim: fileencoding=utf-8 filetype=python :
diff --git a/pkg/osx/daemon/pidfile.py b/pkg/osx/daemon/pidfile.py
new file mode 100644
index 00000000..68f7b2ac
--- /dev/null
+++ b/pkg/osx/daemon/pidfile.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+
+# daemon/pidfile.py
+# Part of ‘python-daemon’, an implementation of PEP 3143.
+#
+# Copyright © 2008–2015 Ben Finney <ben+python@benfinney.id.au>
+#
+# This is free software: you may copy, modify, and/or distribute this work
+# under the terms of the Apache License, version 2.0 as published by the
+# Apache Software Foundation.
+# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details.
+
+""" Lockfile behaviour implemented via Unix PID files.
+ """
+
+from __future__ import (absolute_import, unicode_literals)
+
+from lockfile.pidlockfile import PIDLockFile
+
+
+class TimeoutPIDLockFile(PIDLockFile, object):
+ """ Lockfile with default timeout, implemented as a Unix PID file.
+
+ This uses the ``PIDLockFile`` implementation, with the
+ following changes:
+
+ * The `acquire_timeout` parameter to the initialiser will be
+ used as the default `timeout` parameter for the `acquire`
+ method.
+
+ """
+
+ def __init__(self, path, acquire_timeout=None, *args, **kwargs):
+ """ Set up the parameters of a TimeoutPIDLockFile.
+
+ :param path: Filesystem path to the PID file.
+ :param acquire_timeout: Value to use by default for the
+ `acquire` call.
+ :return: ``None``.
+
+ """
+ self.acquire_timeout = acquire_timeout
+ super(TimeoutPIDLockFile, self).__init__(path, *args, **kwargs)
+
+ def acquire(self, timeout=None, *args, **kwargs):
+ """ Acquire the lock.
+
+ :param timeout: Specifies the timeout; see below for valid
+ values.
+ :return: ``None``.
+
+ The `timeout` defaults to the value set during
+ initialisation with the `acquire_timeout` parameter. It is
+ passed to `PIDLockFile.acquire`; see that method for
+ details.
+
+ """
+ if timeout is None:
+ timeout = self.acquire_timeout
+ super(TimeoutPIDLockFile, self).acquire(timeout, *args, **kwargs)
+
+
+# Local variables:
+# coding: utf-8
+# mode: python
+# End:
+# vim: fileencoding=utf-8 filetype=python :
diff --git a/pkg/osx/daemon/runner.py b/pkg/osx/daemon/runner.py
new file mode 100644
index 00000000..de9025d3
--- /dev/null
+++ b/pkg/osx/daemon/runner.py
@@ -0,0 +1,324 @@
+# -*- coding: utf-8 -*-
+
+# daemon/runner.py
+# Part of ‘python-daemon’, an implementation of PEP 3143.
+#
+# Copyright © 2009–2015 Ben Finney <ben+python@benfinney.id.au>
+# Copyright © 2007–2008 Robert Niederreiter, Jens Klein
+# Copyright © 2003 Clark Evans
+# Copyright © 2002 Noah Spurrier
+# Copyright © 2001 Jürgen Hermann
+#
+# This is free software: you may copy, modify, and/or distribute this work
+# under the terms of the Apache License, version 2.0 as published by the
+# Apache Software Foundation.
+# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details.
+
+""" Daemon runner library.
+ """
+
+from __future__ import (absolute_import, unicode_literals)
+
+import sys
+import os
+import signal
+import errno
+try:
+ # Python 3 standard library.
+ ProcessLookupError
+except NameError:
+ # No such class in Python 2.
+ ProcessLookupError = NotImplemented
+
+import lockfile
+
+from . import pidfile
+from .daemon import (basestring, unicode)
+from .daemon import DaemonContext
+from .daemon import _chain_exception_from_existing_exception_context
+
+
+class DaemonRunnerError(Exception):
+ """ Abstract base class for errors from DaemonRunner. """
+
+ def __init__(self, *args, **kwargs):
+ self._chain_from_context()
+
+ super(DaemonRunnerError, self).__init__(*args, **kwargs)
+
+ def _chain_from_context(self):
+ _chain_exception_from_existing_exception_context(self, as_cause=True)
+
+
+class DaemonRunnerInvalidActionError(DaemonRunnerError, ValueError):
+ """ Raised when specified action for DaemonRunner is invalid. """
+
+ def _chain_from_context(self):
+ # This exception is normally not caused by another.
+ _chain_exception_from_existing_exception_context(self, as_cause=False)
+
+
+class DaemonRunnerStartFailureError(DaemonRunnerError, RuntimeError):
+ """ Raised when failure starting DaemonRunner. """
+
+
+class DaemonRunnerStopFailureError(DaemonRunnerError, RuntimeError):
+ """ Raised when failure stopping DaemonRunner. """
+
+
+class DaemonRunner:
+ """ Controller for a callable running in a separate background process.
+
+ The first command-line argument is the action to take:
+
+ * 'start': Become a daemon and call `app.run()`.
+ * 'stop': Exit the daemon process specified in the PID file.
+ * 'restart': Stop, then start.
+
+ """
+
+ __metaclass__ = type
+
+ start_message = "started with pid {pid:d}"
+
+ def __init__(self, app):
+ """ Set up the parameters of a new runner.
+
+ :param app: The application instance; see below.
+ :return: ``None``.
+
+ The `app` argument must have the following attributes:
+
+ * `stdin_path`, `stdout_path`, `stderr_path`: Filesystem paths
+ to open and replace the existing `sys.stdin`, `sys.stdout`,
+ `sys.stderr`.
+
+ * `pidfile_path`: Absolute filesystem path to a file that will
+ be used as the PID file for the daemon. If ``None``, no PID
+ file will be used.
+
+ * `pidfile_timeout`: Used as the default acquisition timeout
+ value supplied to the runner's PID lock file.
+
+ * `run`: Callable that will be invoked when the daemon is
+ started.
+
+ """
+ self.parse_args()
+ self.app = app
+ self.daemon_context = DaemonContext()
+ self.daemon_context.stdin = open(app.stdin_path, 'rt')
+ self.daemon_context.stdout = open(app.stdout_path, 'w+t')
+ self.daemon_context.stderr = open(
+ app.stderr_path, 'w+t', buffering=0)
+
+ self.pidfile = None
+ if app.pidfile_path is not None:
+ self.pidfile = make_pidlockfile(
+ app.pidfile_path, app.pidfile_timeout)
+ self.daemon_context.pidfile = self.pidfile
+
+ def _usage_exit(self, argv):
+ """ Emit a usage message, then exit.
+
+ :param argv: The command-line arguments used to invoke the
+ program, as a sequence of strings.
+ :return: ``None``.
+
+ """
+ progname = os.path.basename(argv[0])
+ usage_exit_code = 2
+ action_usage = "|".join(self.action_funcs.keys())
+ message = "usage: {progname} {usage}".format(
+ progname=progname, usage=action_usage)
+ emit_message(message)
+ sys.exit(usage_exit_code)
+
+ def parse_args(self, argv=None):
+ """ Parse command-line arguments.
+
+ :param argv: The command-line arguments used to invoke the
+ program, as a sequence of strings.
+
+ :return: ``None``.
+
+ The parser expects the first argument as the program name, the
+ second argument as the action to perform.
+
+ If the parser fails to parse the arguments, emit a usage
+ message and exit the program.
+
+ """
+ if argv is None:
+ argv = sys.argv
+
+ min_args = 2
+ if len(argv) < min_args:
+ self._usage_exit(argv)
+
+ self.action = unicode(argv[1])
+ if self.action not in self.action_funcs:
+ self._usage_exit(argv)
+
+ def _start(self):
+ """ Open the daemon context and run the application.
+
+ :return: ``None``.
+ :raises DaemonRunnerStartFailureError: If the PID file cannot
+ be locked by this process.
+
+ """
+ if is_pidfile_stale(self.pidfile):
+ self.pidfile.break_lock()
+
+ try:
+ self.daemon_context.open()
+ except lockfile.AlreadyLocked:
+ error = DaemonRunnerStartFailureError(
+ "PID file {pidfile.path!r} already locked".format(
+ pidfile=self.pidfile))
+ raise error
+
+ pid = os.getpid()
+ message = self.start_message.format(pid=pid)
+ emit_message(message)
+
+ self.app.run()
+
+ def _terminate_daemon_process(self):
+ """ Terminate the daemon process specified in the current PID file.
+
+ :return: ``None``.
+ :raises DaemonRunnerStopFailureError: If terminating the daemon
+ fails with an OS error.
+
+ """
+ pid = self.pidfile.read_pid()
+ try:
+ os.kill(pid, signal.SIGTERM)
+ except OSError as exc:
+ error = DaemonRunnerStopFailureError(
+ "Failed to terminate {pid:d}: {exc}".format(
+ pid=pid, exc=exc))
+ raise error
+
+ def _stop(self):
+ """ Exit the daemon process specified in the current PID file.
+
+ :return: ``None``.
+ :raises DaemonRunnerStopFailureError: If the PID file is not
+ already locked.
+
+ """
+ if not self.pidfile.is_locked():
+ error = DaemonRunnerStopFailureError(
+ "PID file {pidfile.path!r} not locked".format(
+ pidfile=self.pidfile))
+ raise error
+
+ if is_pidfile_stale(self.pidfile):
+ self.pidfile.break_lock()
+ else:
+ self._terminate_daemon_process()
+
+ def _restart(self):
+ """ Stop, then start.
+ """
+ self._stop()
+ self._start()
+
+ action_funcs = {
+ 'start': _start,
+ 'stop': _stop,
+ 'restart': _restart,
+ }
+
+ def _get_action_func(self):
+ """ Get the function for the specified action.
+
+ :return: The function object corresponding to the specified
+ action.
+ :raises DaemonRunnerInvalidActionError: if the action is
+ unknown.
+
+ The action is specified by the `action` attribute, which is set
+ during `parse_args`.
+
+ """
+ try:
+ func = self.action_funcs[self.action]
+ except KeyError:
+ error = DaemonRunnerInvalidActionError(
+ "Unknown action: {action!r}".format(
+ action=self.action))
+ raise error
+ return func
+
+ def do_action(self):
+ """ Perform the requested action.
+
+ :return: ``None``.
+
+ The action is specified by the `action` attribute, which is set
+ during `parse_args`.
+
+ """
+ func = self._get_action_func()
+ func(self)
+
+
+def emit_message(message, stream=None):
+ """ Emit a message to the specified stream (default `sys.stderr`). """
+ if stream is None:
+ stream = sys.stderr
+ stream.write("{message}\n".format(message=message))
+ stream.flush()
+
+
+def make_pidlockfile(path, acquire_timeout):
+ """ Make a PIDLockFile instance with the given filesystem path. """
+ if not isinstance(path, basestring):
+ error = ValueError("Not a filesystem path: {path!r}".format(
+ path=path))
+ raise error
+ if not os.path.isabs(path):
+ error = ValueError("Not an absolute path: {path!r}".format(
+ path=path))
+ raise error
+ lockfile = pidfile.TimeoutPIDLockFile(path, acquire_timeout)
+
+ return lockfile
+
+
+def is_pidfile_stale(pidfile):
+ """ Determine whether a PID file is stale.
+
+ :return: ``True`` iff the PID file is stale; otherwise ``False``.
+
+ The PID file is “stale” if its contents are valid but do not
+ match the PID of a currently-running process.
+
+ """
+ result = False
+
+ pidfile_pid = pidfile.read_pid()
+ if pidfile_pid is not None:
+ try:
+ os.kill(pidfile_pid, signal.SIG_DFL)
+ except ProcessLookupError:
+ # The specified PID does not exist.
+ result = True
+ except OSError as exc:
+ if exc.errno == errno.ESRCH:
+ # Under Python 2, process lookup error is an OSError.
+ # The specified PID does not exist.
+ result = True
+
+ return result
+
+
+# Local variables:
+# coding: utf-8
+# mode: python
+# End:
+# vim: fileencoding=utf-8 filetype=python :
diff --git a/pkg/osx/install/ProcessNetworkChanges.plist.template b/pkg/osx/install/ProcessNetworkChanges.plist.template
deleted file mode 100644
index eaf54fcf..00000000
--- a/pkg/osx/install/ProcessNetworkChanges.plist.template
+++ /dev/null
@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
- <dict>
- <key>Label</key>
- <string>se.leap.openvpn.process-network-changes</string>
- <key>ProgramArguments</key>
- <array>
- <string>${DIR}/process-network-changes</string>
- </array>
- <key>WatchPaths</key>
- <array>
- <string>/Library/Preferences/SystemConfiguration</string>
- </array>
- </dict>
-</plist>
diff --git a/pkg/osx/install/client.down.sh b/pkg/osx/install/client.down.sh
deleted file mode 100755
index 52ba4de6..00000000
--- a/pkg/osx/install/client.down.sh
+++ /dev/null
@@ -1,148 +0,0 @@
-#!/bin/bash -e
-# Note: must be bash; uses bash-specific tricks
-#
-# ******************************************************************************************************************
-# Based on the Tunnelblick script that just "does everything!"
-# It handles TUN and TAP interfaces,
-# pushed configurations and DHCP leases. :)
-#
-# This is the "Down" version of the script, executed after the connection is
-# closed.
-#
-# Created by: Nick Williams (using original code and parts of old Tblk scripts)
-#
-# ******************************************************************************************************************
-# TODO: review and adapt version 3 of the clientX.down.sh
-
-trap "" TSTP
-trap "" HUP
-trap "" INT
-export PATH="/bin:/sbin:/usr/sbin:/usr/bin"
-
-readonly LOG_MESSAGE_COMMAND=$(basename "${0}")
-
-# Quick check - is the configuration there?
-if ! scutil -w State:/Network/OpenVPN &>/dev/null -t 1 ; then
- # Configuration isn't there, so we forget it
- echo "$(date '+%a %b %e %T %Y') *LEAPClient $LOG_MESSAGE_COMMAND: WARNING: No existing OpenVPN DNS configuration found; not tearing down anything; exiting."
- exit 0
-fi
-
-# NOTE: This script does not use any arguments passed to it by OpenVPN, so it doesn't shift LEAPClient options out of the argument list
-
-# Get info saved by the up script
-LEAPCLIENT_CONFIG="$(/usr/sbin/scutil <<-EOF
- open
- show State:/Network/OpenVPN
- quit
-EOF)"
-
-ARG_MONITOR_NETWORK_CONFIGURATION="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*MonitorNetwork :' | sed -e 's/^.*: //g')"
-LEASEWATCHER_PLIST_PATH="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*LeaseWatcherPlistPath :' | sed -e 's/^.*: //g')"
-PSID="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*Service :' | sed -e 's/^.*: //g')"
-SCRIPT_LOG_FILE="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*ScriptLogFile :' | sed -e 's/^.*: //g')"
-# Don't need: ARG_RESTORE_ON_DNS_RESET="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*RestoreOnDNSReset :' | sed -e 's/^.*: //g')"
-# Don't need: ARG_RESTORE_ON_WINS_RESET="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*RestoreOnWINSReset :' | sed -e 's/^.*: //g')"
-# Don't need: PROCESS="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*PID :' | sed -e 's/^.*: //g')"
-# Don't need: ARG_IGNORE_OPTION_FLAGS="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*IgnoreOptionFlags :' | sed -e 's/^.*: //g')"
-ARG_TAP="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*IsTapInterface :' | sed -e 's/^.*: //g')"
-bRouteGatewayIsDhcp="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*RouteGatewayIsDhcp :' | sed -e 's/^.*: //g')"
-
-# @param String message - The message to log
-logMessage()
-{
- echo "$(date '+%a %b %e %T %Y') *LEAP CLient $LOG_MESSAGE_COMMAND: "${@} >> "${SCRIPT_LOG_FILE}"
-}
-
-trim()
-{
- echo ${@}
-}
-
-if ${ARG_TAP} ; then
- if [ "$bRouteGatewayIsDhcp" == "true" ]; then
- if [ -z "$dev" ]; then
- logMessage "Cannot configure TAP interface for DHCP without \$dev being defined. Device may not have disconnected properly."
- else
- set +e
- ipconfig set "$dev" NONE 2>/dev/null
- set -e
- fi
- fi
-fi
-
-# Issue warning if the primary service ID has changed
-PSID_CURRENT="$( (scutil | grep Service | sed -e 's/.*Service : //')<<- EOF
- open
- show State:/Network/OpenVPN
- quit
-EOF)"
-if [ "${PSID}" != "${PSID_CURRENT}" ] ; then
- logMessage "Ignoring change of Network Primary Service from ${PSID} to ${PSID_CURRENT}"
-fi
-
-# Remove leasewatcher
-if ${ARG_MONITOR_NETWORK_CONFIGURATION} ; then
- launchctl unload "${LEASEWATCHER_PLIST_PATH}"
- logMessage "Cancelled monitoring of system configuration changes"
-fi
-
-# Restore configurations
-DNS_OLD="$(/usr/sbin/scutil <<-EOF
- open
- show State:/Network/OpenVPN/OldDNS
- quit
-EOF)"
-WINS_OLD="$(/usr/sbin/scutil <<-EOF
- open
- show State:/Network/OpenVPN/OldSMB
- quit
-EOF)"
-TB_NO_SUCH_KEY="<dictionary> {
- LEAPClientNoSuchKey : true
-}"
-
-if [ "${DNS_OLD}" = "${TB_NO_SUCH_KEY}" ] ; then
- scutil <<- EOF
- open
- remove State:/Network/Service/${PSID}/DNS
- quit
-EOF
-else
- scutil <<- EOF
- open
- get State:/Network/OpenVPN/OldDNS
- set State:/Network/Service/${PSID}/DNS
- quit
-EOF
-fi
-
-if [ "${WINS_OLD}" = "${TB_NO_SUCH_KEY}" ] ; then
- scutil <<- EOF
- open
- remove State:/Network/Service/${PSID}/SMB
- quit
-EOF
-else
- scutil <<- EOF
- open
- get State:/Network/OpenVPN/OldSMB
- set State:/Network/Service/${PSID}/SMB
- quit
-EOF
-fi
-
-logMessage "Restored the DNS and WINS configurations"
-
-# Remove our system configuration data
-scutil <<- EOF
- open
- remove State:/Network/OpenVPN/SMB
- remove State:/Network/OpenVPN/DNS
- remove State:/Network/OpenVPN/OldSMB
- remove State:/Network/OpenVPN/OldDNS
- remove State:/Network/OpenVPN
- quit
-EOF
-
-exit 0
diff --git a/pkg/osx/install/client.up.sh b/pkg/osx/install/client.up.sh
deleted file mode 100755
index be9814c2..00000000
--- a/pkg/osx/install/client.up.sh
+++ /dev/null
@@ -1,599 +0,0 @@
-#!/bin/bash -e
-# Note: must be bash; uses bash-specific tricks
-#
-# ******************************************************************************************************************
-# Taken from the Tunnelblick script that "just does everything!"
-# It handles TUN and TAP interfaces,
-# pushed configurations, DHCP with DNS and WINS, and renewed DHCP leases. :)
-#
-# This is the "Up" version of the script, executed after the interface is
-# initialized.
-#
-# Created by: Nick Williams (using original code and parts of old Tblk scripts)
-#
-# ******************************************************************************************************************
-# TODO: review and adapt revision 3 of the clientX-up.sh instead
-
-trap "" TSTP
-trap "" HUP
-trap "" INT
-export PATH="/bin:/sbin:/usr/sbin:/usr/bin"
-
-# Process optional arguments (if any) for the script
-# Each one begins with a "-"
-# They come from the leap-client invocation, and come first, before the OpenVPN arguments
-# So we set ARG_ script variables to their values and shift them out of the argument list
-# When we're done, only the OpenVPN arguments remain for the rest of the script to use
-ARG_MONITOR_NETWORK_CONFIGURATION="false"
-ARG_RESTORE_ON_DNS_RESET="false"
-ARG_RESTORE_ON_WINS_RESET="false"
-ARG_TAP="false"
-ARG_IGNORE_OPTION_FLAGS=""
-
-while [ {$#} ] ; do
- if [ "$1" = "-m" ] ; then # Handle the arguments we know about
- ARG_MONITOR_NETWORK_CONFIGURATION="true" # by setting ARG_ script variables to their values
- shift # Then shift them out
- elif [ "$1" = "-d" ] ; then
- ARG_RESTORE_ON_DNS_RESET="true"
- shift
- elif [ "$1" = "-w" ] ; then
- ARG_RESTORE_ON_WINS_RESET="true"
- shift
- elif [ "$1" = "-a" ] ; then
- ARG_TAP="true"
- shift
- elif [ "${1:0:2}" = "-i" ] ; then
- ARG_IGNORE_OPTION_FLAGS="${1}"
- shift
- elif [ "${1:0:2}" = "-a" ] ; then
- ARG_IGNORE_OPTION_FLAGS="${1}"
- shift
- else
- if [ "${1:0:1}" = "-" ] ; then # Shift out Tunnelblick arguments (they start with "-") that we don't understand
- shift # so the rest of the script sees only the OpenVPN arguments
- else
- break
- fi
- fi
-done
-
-readonly ARG_MONITOR_NETWORK_CONFIGURATION ARG_RESTORE_ON_DNS_RESET ARG_RESTORE_ON_WINS_RESET ARG_TAP ARG_IGNORE_OPTION_FLAGS
-
-# Note: The script log path name is constructed from the path of the regular config file, not the shadow copy
-# if the config is shadow copy, e.g. /Library/Application Support/Tunnelblick/Users/Jonathan/Folder/Subfolder/config.ovpn
-# then convert to regular config /Users/Jonathan/Library/Application Support/Tunnelblick/Configurations/Folder/Subfolder/config.ovpn
-# to get the script log path
-# Note: "/Users/..." works even if the home directory has a different path; it is used in the name of the log file, and is not used as a path to get to anything.
-readonly TBALTPREFIX="/Library/Application Support/LEAP Client/Users/"
-readonly TBALTPREFIXLEN="${#TBALTPREFIX}"
-readonly TBCONFIGSTART="${config:0:$TBALTPREFIXLEN}"
-if [ "$TBCONFIGSTART" = "$TBALTPREFIX" ] ; then
- readonly TBBASE="${config:$TBALTPREFIXLEN}"
- readonly TBSUFFIX="${TBBASE#*/}"
- readonly TBUSERNAME="${TBBASE%%/*}"
- readonly TBCONFIG="/Users/$TBUSERNAME/Library/Application Support/LEAP Client/Configurations/$TBSUFFIX"
-else
- readonly TBCONFIG="${config}"
-fi
-
-readonly CONFIG_PATH_DASHES_SLASHES="$(echo "${TBCONFIG}" | sed -e 's/-/--/g' | sed -e 's/\//-S/g')"
-
-# XXX PUT LOGS SOMEWHERE BETTER
-readonly SCRIPT_LOG_FILE="/Users/$LEAPUSER/.config/leap/logs/${CONFIG_PATH_DASHES_SLASHES}.script.log"
-readonly TB_RESOURCE_PATH=$(dirname "${0}")
-
-LEASEWATCHER_PLIST_PATH="/Users/$LEAPUSER/.config/leap/logs/LeaseWatch.plist"
-
-readonly OSVER="$(sw_vers | grep 'ProductVersion:' | grep -o '10\.[0-9]*')"
-
-readonly DEFAULT_DOMAIN_NAME="openvpn"
-
-bRouteGatewayIsDhcp="false"
-
-# @param String message - The message to log
-readonly LOG_MESSAGE_COMMAND=$(basename "${0}")
-logMessage()
-{
- echo "$(date '+%a %b %e %T %Y') *LEAP Client $LOG_MESSAGE_COMMAND: "${@} >> "${SCRIPT_LOG_FILE}"
-}
-
-# @param String string - Content to trim
-trim()
-{
- echo ${@}
-}
-
-# @param String[] dnsServers - The name servers to use
-# @param String domainName - The domain name to use
-# @param \optional String[] winsServers - The WINS servers to use
-setDnsServersAndDomainName()
-{
- declare -a vDNS=("${!1}")
- domain=$2
- declare -a vWINS=("${!3}")
-
- set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
-
- PSID=$( (scutil | grep PrimaryService | sed -e 's/.*PrimaryService : //')<<- EOF
- open
- show State:/Network/Global/IPv4
- quit
-EOF )
-
- STATIC_DNS_CONFIG="$( (scutil | sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' ')<<- EOF
- open
- show Setup:/Network/Service/${PSID}/DNS
- quit
-EOF )"
- if echo "${STATIC_DNS_CONFIG}" | grep -q "ServerAddresses" ; then
- readonly STATIC_DNS="$(trim "$( echo "${STATIC_DNS_CONFIG}" | sed -e 's/^.*ServerAddresses[^{]*{[[:space:]]*\([^}]*\)[[:space:]]*}.*$/\1/g' )")"
- fi
- if echo "${STATIC_DNS_CONFIG}" | grep -q "SearchDomains" ; then
- readonly STATIC_SEARCH="$(trim "$( echo "${STATIC_DNS_CONFIG}" | sed -e 's/^.*SearchDomains[^{]*{[[:space:]]*\([^}]*\)[[:space:]]*}.*$/\1/g' )")"
- fi
-
- STATIC_WINS_CONFIG="$( (scutil | sed -e 's/^[[:space:]]*[[:digit:]]* : //g' | tr '\n' ' ')<<- EOF
- open
- show Setup:/Network/Service/${PSID}/SMB
- quit
-EOF )"
- STATIC_WINS_SERVERS=""
- STATIC_WORKGROUP=""
- STATIC_NETBIOSNAME=""
- if echo "${STATIC_WINS_CONFIG}" | grep -q "WINSAddresses" ; then
- STATIC_WINS_SERVERS="$(trim "$( echo "${STATIC_WINS_CONFIG}" | sed -e 's/^.*WINSAddresses[^{]*{[[:space:]]*\([^}]*\)[[:space:]]*}.*$/\1/g' )")"
- fi
- if echo "${STATIC_WINS_CONFIG}" | grep -q "Workgroup" ; then
- STATIC_WORKGROUP="$(trim "$( echo "${STATIC_WINS_CONFIG}" | sed -e 's/^.*Workgroup : \([^[:space:]]*\).*$/\1/g' )")"
- fi
- if echo "${STATIC_WINS_CONFIG}" | grep -q "NetBIOSName" ; then
- STATIC_NETBIOSNAME="$(trim "$( echo "${STATIC_WINS_CONFIG}" | sed -e 's/^.*NetBIOSName : \([^[:space:]]*\).*$/\1/g' )")"
- fi
- readonly STATIC_WINS_SERVERS STATIC_WORKGROUP STATIC_NETBIOSNAME
-
- if [ ${#vDNS[*]} -eq 0 ] ; then
- DYN_DNS="false"
- ALL_DNS="${STATIC_DNS}"
- elif [ -n "${STATIC_DNS}" ] ; then
- case "${OSVER}" in
- 10.6 | 10.7 )
- # Do nothing - in 10.6 we don't aggregate our configurations, apparently
- DYN_DNS="false"
- ALL_DNS="${STATIC_DNS}"
- ;;
- 10.4 | 10.5 )
- DYN_DNS="true"
- # We need to remove duplicate DNS entries, so that our reference list matches MacOSX's
- SDNS="$(echo "${STATIC_DNS}" | tr ' ' '\n')"
- (( i=0 ))
- for n in "${vDNS[@]}" ; do
- if echo "${SDNS}" | grep -q "${n}" ; then
- unset vDNS[${i}]
- fi
- (( i++ ))
- done
- if [ ${#vDNS[*]} -gt 0 ] ; then
- ALL_DNS="$(trim "${STATIC_DNS}" "${vDNS[*]}")"
- else
- DYN_DNS="false"
- ALL_DNS="${STATIC_DNS}"
- fi
- ;;
- esac
- else
- DYN_DNS="true"
- ALL_DNS="$(trim "${vDNS[*]}")"
- fi
- readonly DYN_DNS ALL_DNS
-
- if [ ${#vWINS[*]} -eq 0 ] ; then
- DYN_WINS="false"
- ALL_WINS_SERVERS="${STATIC_WINS_SERVERS}"
- elif [ -n "${STATIC_WINS_SERVERS}" ] ; then
- case "${OSVER}" in
- 10.6 | 10.7 )
- # Do nothing - in 10.6 we don't aggregate our configurations, apparently
- DYN_WINS="false"
- ALL_WINS_SERVERS="${STATIC_WINS_SERVERS}"
- ;;
- 10.4 | 10.5 )
- DYN_WINS="true"
- # We need to remove duplicate WINS entries, so that our reference list matches MacOSX's
- SWINS="$(echo "${STATIC_WINS_SERVERS}" | tr ' ' '\n')"
- (( i=0 ))
- for n in "${vWINS[@]}" ; do
- if echo "${SWINS}" | grep -q "${n}" ; then
- unset vWINS[${i}]
- fi
- (( i++ ))
- done
- if [ ${#vWINS[*]} -gt 0 ] ; then
- ALL_WINS_SERVERS="$(trim "${STATIC_WINS_SERVERS}" "${vWINS[*]}")"
- else
- DYN_WINS="false"
- ALL_WINS_SERVERS="${STATIC_WINS_SERVERS}"
- fi
- ;;
- esac
- else
- DYN_WINS="true"
- ALL_WINS_SERVERS="$(trim "${vWINS[*]}")"
- fi
- readonly DYN_WINS ALL_WINS_SERVERS
-
- # We double-check that our search domain isn't already on the list
- SEARCH_DOMAIN="${domain}"
- case "${OSVER}" in
- 10.6 | 10.7 )
- # Do nothing - in 10.6 we don't aggregate our configurations, apparently
- if [ -n "${STATIC_SEARCH}" ] ; then
- ALL_SEARCH="${STATIC_SEARCH}"
- SEARCH_DOMAIN=""
- else
- ALL_SEARCH="${SEARCH_DOMAIN}"
- fi
- ;;
- 10.4 | 10.5 )
- if echo "${STATIC_SEARCH}" | tr ' ' '\n' | grep -q "${SEARCH_DOMAIN}" ; then
- SEARCH_DOMAIN=""
- fi
- if [ -z "${SEARCH_DOMAIN}" ] ; then
- ALL_SEARCH="${STATIC_SEARCH}"
- else
- ALL_SEARCH="$(trim "${STATIC_SEARCH}" "${SEARCH_DOMAIN}")"
- fi
- ;;
- esac
- readonly SEARCH_DOMAIN ALL_SEARCH
-
- if ! ${DYN_DNS} ; then
- NO_DNS="#"
- fi
- if ! ${DYN_WINS} ; then
- NO_WS="#"
- fi
- if [ -z "${SEARCH_DOMAIN}" ] ; then
- NO_SEARCH="#"
- fi
- if [ -z "${STATIC_WORKGROUP}" ] ; then
- NO_WG="#"
- fi
- if [ -z "${STATIC_NETBIOSNAME}" ] ; then
- NO_NB="#"
- fi
- if [ -z "${ALL_DNS}" ] ; then
- AGG_DNS="#"
- fi
- if [ -z "${ALL_SEARCH}" ] ; then
- AGG_SEARCH="#"
- fi
- if [ -z "${ALL_WINS_SERVERS}" ] ; then
- AGG_WINS="#"
- fi
-
- # Now, do the aggregation
- # Save the openvpn process ID and the Network Primary Service ID, leasewather.plist path, logfile path, and optional arguments from LEAP Client,
- # then save old and new DNS and WINS settings
- # PPID is a bash-script variable that contains the process ID of the parent of the process running the script (i.e., OpenVPN's process ID)
- # config is an environmental variable set to the configuration path by OpenVPN prior to running this up script
- logMessage "Up to two 'No such key' warnings are normal and may be ignored"
-
- # If DNS is manually set, it overrides the DHCP setting, which isn't reflected in 'State:/Network/Service/${PSID}/DNS'
- if echo "${STATIC_DNS_CONFIG}" | grep -q "ServerAddresses" ; then
- CORRECT_OLD_DNS_KEY="Setup:"
- else
- CORRECT_OLD_DNS_KEY="State:"
- fi
-
- # If WINS is manually set, it overrides the DHCP setting, which isn't reflected in 'State:/Network/Service/${PSID}/DNS'
- if echo "${STATIC_WINS_CONFIG}" | grep -q "WINSAddresses" ; then
- CORRECT_OLD_WINS_KEY="Setup:"
- else
- CORRECT_OLD_WINS_KEY="State:"
- fi
-
- # If we are not expecting any WINS value, add <LEAPClientNoSuchKey : true> to the expected WINS setup
- NO_NOSUCH_KEY_WINS="#"
- if [ "${NO_NB}" = "#" -a "${AGG_WINS}" = "#" -a "${NO_WG}" = "#" ] ; then
- NO_NOSUCH_KEY_WINS=""
- fi
- readonly NO_NOSUCH_KEY_WINS
-
- set -e # We instruct bash that it CAN again fail on errors
-
- scutil <<- EOF
- open
- d.init
- d.add PID # ${PPID}
- d.add Service ${PSID}
- d.add LeaseWatcherPlistPath "${LEASEWATCHER_PLIST_PATH}"
- d.add ScriptLogFile "${SCRIPT_LOG_FILE}"
- d.add MonitorNetwork "${ARG_MONITOR_NETWORK_CONFIGURATION}"
- d.add RestoreOnDNSReset "${ARG_RESTORE_ON_DNS_RESET}"
- d.add RestoreOnWINSReset "${ARG_RESTORE_ON_WINS_RESET}"
- d.add IgnoreOptionFlags "${ARG_IGNORE_OPTION_FLAGS}"
- d.add IsTapInterface "${ARG_TAP}"
- d.add RouteGatewayIsDhcp "${bRouteGatewayIsDhcp}"
- set State:/Network/OpenVPN
-
- # First, back up the device's current DNS and WINS configurations
- # Indicate 'no such key' by a dictionary with a single entry: "LEAPClientNoSuchKey : true"
- d.init
- d.add LEAPClientNoSuchKey true
- get ${CORRECT_OLD_DNS_KEY}/Network/Service/${PSID}/DNS
- set State:/Network/OpenVPN/OldDNS
-
- d.init
- d.add LEAPClientNoSuchKey true
- get ${CORRECT_OLD_WINS_KEY}/Network/Service/${PSID}/SMB
- set State:/Network/OpenVPN/OldSMB
-
- # Second, initialize the new DNS map
- d.init
- ${NO_DNS}d.add ServerAddresses * ${vDNS[*]}
- ${NO_SEARCH}d.add SearchDomains * ${SEARCH_DOMAIN}
- d.add DomainName ${domain}
- set State:/Network/Service/${PSID}/DNS
-
- # Third, initialize the WINS map
- d.init
- ${NO_NB}d.add NetBIOSName ${STATIC_NETBIOSNAME}
- ${NO_WS}d.add WINSAddresses * ${vWINS[*]}
- ${NO_WG}d.add Workgroup ${STATIC_WORKGROUP}
- set State:/Network/Service/${PSID}/SMB
-
- # Now, initialize the maps that will be compared against the system-generated map
- # which means that we will have to aggregate configurations of statically-configured
- # nameservers, and statically-configured search domains
- d.init
- ${AGG_DNS}d.add ServerAddresses * ${ALL_DNS}
- ${AGG_SEARCH}d.add SearchDomains * ${ALL_SEARCH}
- d.add DomainName ${domain}
- set State:/Network/OpenVPN/DNS
-
- d.init
- ${NO_NB}d.add NetBIOSName ${STATIC_NETBIOSNAME}
- ${AGG_WINS}d.add WINSAddresses * ${ALL_WINS_SERVERS}
- ${NO_WG}d.add Workgroup ${STATIC_WORKGROUP}
- ${NO_NOSUCH_KEY_WINS}d.add LEAPClientNoSuchKey true
- set State:/Network/OpenVPN/SMB
-
- # We are done
- quit
-EOF
-
- logMessage "Saved the DNS and WINS configurations for later use"
-
- if ${ARG_MONITOR_NETWORK_CONFIGURATION} ; then
- if [ "${ARG_IGNORE_OPTION_FLAGS:0:2}" = "-a" ] ; then
- # Generate an updated plist with the path for process-network-changes
- readonly LEASEWATCHER_TEMPLATE_PATH="$(dirname "${0}")/ProcessNetworkChanges.plist.template"
- sed -e "s|\${DIR}|$(dirname "${0}")|g" "${LEASEWATCHER_TEMPLATE_PATH}" > "${LEASEWATCHER_PLIST_PATH}"
- launchctl load "${LEASEWATCHER_PLIST_PATH}"
- logMessage "Set up to monitor system configuration with process-network-changes"
- else
- # Generate an updated plist with the path for leasewatch
- readonly LEASEWATCHER_TEMPLATE_PATH="$(dirname "${0}")/LeaseWatch.plist.template"
- sed -e "s|\${DIR}|$(dirname "${0}")|g" "${LEASEWATCHER_TEMPLATE_PATH}" > "${LEASEWATCHER_PLIST_PATH}"
- launchctl load "${LEASEWATCHER_PLIST_PATH}"
- logMessage "Set up to monitor system configuration with leasewatch"
- fi
- fi
-}
-
-configureDhcpDns()
-{
- # whilst ipconfig will have created the neccessary Network Service keys, the DNS
- # settings won't actually be used by OS X unless the SupplementalMatchDomains key
- # is added
- # ref. <http://lists.apple.com/archives/Macnetworkprog/2005/Jun/msg00011.html>
- # - is there a way to extract the domains from the SC dictionary and re-insert
- # as SupplementalMatchDomains? i.e. not requiring the ipconfig domain_name call?
-
- # - wait until we get a lease before extracting the DNS domain name and merging into SC
- # - despite it's name, ipconfig waitall doesn't (but maybe one day it will :-)
- ipconfig waitall
-
- unset test_domain_name
- unset test_name_server
-
- set +e # We instruct bash NOT to exit on individual command errors, because if we need to wait longer these commands will fail
-
- # usually takes at least a few seconds to get a DHCP lease
- sleep 3
- n=0
- while [ -z "$test_domain_name" -a -z "$test_name_server" -a $n -lt 5 ]
- do
- logMessage "Sleeping for $n seconds to wait for DHCP to finish setup."
- sleep $n
- n=`expr $n + 1`
-
- if [ -z "$test_domain_name" ]; then
- test_domain_name=`ipconfig getoption $dev domain_name 2>/dev/null`
- fi
-
- if [ -z "$test_name_server" ]; then
- test_name_server=`ipconfig getoption $dev domain_name_server 2>/dev/null`
- fi
- done
-
- sGetPacketOutput=`ipconfig getpacket $dev`
-
- set -e # We instruct bash that it CAN again fail on individual errors
-
- #echo "`date` test_domain_name = $test_domain_name, test_name_server = $test_name_server, sGetPacketOutput = $sGetPacketOutput"
-
- unset aNameServers
- unset aWinsServers
-
- nNameServerIndex=1
- nWinsServerIndex=1
-
- if [ "$sGetPacketOutput" ]; then
- sGetPacketOutput_FirstLine=`echo "$sGetPacketOutput"|head -n 1`
- #echo $sGetPacketOutput_FirstLine
-
- if [ "$sGetPacketOutput_FirstLine" == "op = BOOTREPLY" ]; then
- set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
-
- for tNameServer in `echo "$sGetPacketOutput"|grep "domain_name_server"|grep -Eo "\{([0-9\.]+)(, [0-9\.]+)*\}"|grep -Eo "([0-9\.]+)"`; do
- aNameServers[nNameServerIndex-1]="$(trim "$tNameServer")"
- let nNameServerIndex++
- done
-
- for tWINSServer in `echo "$sGetPacketOutput"|grep "nb_over_tcpip_name_server"|grep -Eo "\{([0-9\.]+)(, [0-9\.]+)*\}"|grep -Eo "([0-9\.]+)"`; do
- aWinsServers[nWinsServerIndex-1]="$(trim "$tWINSServer")"
- let nWinsServerIndex++
- done
-
- sDomainName=`echo "$sGetPacketOutput"|grep "domain_name "|grep -Eo ": [-A-Za-z0-9\-\.]+"|grep -Eo "[-A-Za-z0-9\-\.]+"`
- sDomainName="$(trim "$sDomainName")"
-
- if [ ${#aNameServers[*]} -gt 0 -a "$sDomainName" ]; then
- logMessage "Retrieved name server(s) [ ${aNameServers[@]} ], domain name [ $sDomainName ], and WINS server(s) [ ${aWinsServers[@]} ]"
- setDnsServersAndDomainName aNameServers[@] "$sDomainName" aWinsServers[@]
- return 0
- elif [ ${#aNameServers[*]} -gt 0 ]; then
- logMessage "Retrieved name server(s) [ ${aNameServers[@]} ] and WINS server(s) [ ${aWinsServers[@]} ] and using default domain name [ $DEFAULT_DOMAIN_NAME ]"
- setDnsServersAndDomainName aNameServers[@] "$DEFAULT_DOMAIN_NAME" aWinsServers[@]
- return 0
- else
- # Should we return 1 here and indicate an error, or attempt the old method?
- logMessage "No useful information extracted from DHCP/BOOTP packet. Attempting legacy configuration."
- fi
-
- set -e # We instruct bash that it CAN again fail on errors
- else
- # Should we return 1 here and indicate an error, or attempt the old method?
- logMessage "No DHCP/BOOTP packet found on interface. Attempting legacy configuration."
- fi
- fi
-
- unset sDomainName
- unset sNameServer
- unset aNameServers
-
- sDomainName=`ipconfig getoption $dev domain_name 2>/dev/null`
- sNameServer=`ipconfig getoption $dev domain_name_server 2>/dev/null`
-
- sDomainName="$(trim "$sDomainName")"
- sNameServer="$(trim "$sNameServer")"
-
- declare -a aWinsServers=( ) # Declare empty WINS array to avoid any useless error messages
-
- if [ "$sDomainName" -a "$sNameServer" ]; then
- aNameServers[0]=$sNameServer
- logMessage "Retrieved name server [ $sNameServer ], domain name [ $sDomainName ], and no WINS servers"
- setDnsServersAndDomainName aNameServers[@] "$sDomainName" aWinsServers[@]
- elif [ "$sNameServer" ]; then
- aNameServers[0]=$sNameServer
- logMessage "Retrieved name server [ $sNameServer ] and no WINS servers, and using default domain name [ $DEFAULT_DOMAIN_NAME ]"
- setDnsServersAndDomainName aNameServers[@] "$DEFAULT_DOMAIN_NAME" aWinsServers[@]
- elif [ "$sDomainName" ]; then
- logMessage "WARNING: Retrieved domain name [ $sDomainName ] but no name servers from OpenVPN (DHCP), which is not sufficient to make network/DNS configuration changes."
- if ${ARG_MONITOR_NETWORK_CONFIGURATION} ; then
- logMessage "Will NOT monitor for other network configuration changes."
- fi
- else
- logMessage "WARNING: No DNS information received from OpenVPN (DHCP), so no network/DNS configuration changes need to be made."
- if ${ARG_MONITOR_NETWORK_CONFIGURATION} ; then
- logMessage "Will NOT monitor for other network configuration changes."
- fi
- fi
-
- return 0
-}
-
-configureOpenVpnDns()
-{
- unset vForOptions
- unset vOptions
- unset aNameServers
- unset aWinsServers
-
- nOptionIndex=1
- nNameServerIndex=1
- nWinsServerIndex=1
-
- while vForOptions=foreign_option_$nOptionIndex; [ -n "${!vForOptions}" ]; do
- vOptions[nOptionIndex-1]=${!vForOptions}
- case ${vOptions[nOptionIndex-1]} in
- *DOMAIN* )
- sDomainName="$(trim "${vOptions[nOptionIndex-1]//dhcp-option DOMAIN /}")"
- ;;
- *DNS* )
- aNameServers[nNameServerIndex-1]="$(trim "${vOptions[nOptionIndex-1]//dhcp-option DNS /}")"
- let nNameServerIndex++
- ;;
- *WINS* )
- aWinsServers[nWinsServerIndex-1]="$(trim "${vOptions[nOptionIndex-1]//dhcp-option WINS /}")"
- let nWinsServerIndex++
- ;;
- * )
- logMessage "Unknown: 'foreign_option_${nOptionIndex}' = '${vOptions[nOptionIndex-1]}'"
- ;;
- esac
- let nOptionIndex++
- done
-
- if [ ${#aNameServers[*]} -gt 0 -a "$sDomainName" ]; then
- logMessage "Retrieved name server(s) [ ${aNameServers[@]} ], domain name [ $sDomainName ], and WINS server(s) [ ${aWinsServers[@]} ]"
- setDnsServersAndDomainName aNameServers[@] "$sDomainName" aWinsServers[@]
- elif [ ${#aNameServers[*]} -gt 0 ]; then
- logMessage "Retrieved name server(s) [ ${aNameServers[@]} ] and WINS server(s) [ ${aWinsServers[@]} ] and using default domain name [ $DEFAULT_DOMAIN_NAME ]"
- setDnsServersAndDomainName aNameServers[@] "$DEFAULT_DOMAIN_NAME" aWinsServers[@]
- else
- # Should we maybe just return 1 here to indicate an error? Does this mean that something bad has happened?
- logMessage "No DNS information recieved from OpenVPN, so no network configuration changes need to be made."
- if ${ARG_MONITOR_NETWORK_CONFIGURATION} ; then
- logMessage "Will NOT monitor for other network configuration changes."
- fi
- fi
-
- return 0
-}
-
-# We sleep here to allow time for OS X to process network settings
-sleep 2
-
-EXIT_CODE=0
-
-if ${ARG_TAP} ; then
- # Still need to do: Look for route-gateway dhcp (TAP isn't always DHCP)
- bRouteGatewayIsDhcp="false"
- if [ -z "${route_vpn_gateway}" -o "$route_vpn_gateway" == "dhcp" -o "$route_vpn_gateway" == "DHCP" ]; then
- bRouteGatewayIsDhcp="true"
- fi
-
- if [ "$bRouteGatewayIsDhcp" == "true" ]; then
- if [ -z "$dev" ]; then
- logMessage "Cannot configure TAP interface for DHCP without \$dev being defined. Exiting."
- exit 1
- fi
-
- ipconfig set "$dev" DHCP
-
- configureDhcpDns &
- elif [ "$foreign_option_1" == "" ]; then
- logMessage "No network configuration changes need to be made."
- if ${ARG_MONITOR_NETWORK_CONFIGURATION} ; then
- logMessage "Will NOT monitor for other network configuration changes."
- fi
- else
- configureOpenVpnDns
- EXIT_CODE=$?
- fi
-else
- if [ "$foreign_option_1" == "" ]; then
- logMessage "No network configuration changes need to be made."
- if ${ARG_MONITOR_NETWORK_CONFIGURATION} ; then
- logMessage "Will NOT monitor for other network configuration changes."
- fi
- else
- configureOpenVpnDns
- EXIT_CODE=$?
- fi
-fi
-
-exit $EXIT_CODE
diff --git a/pkg/osx/install/install-leapc.sh b/pkg/osx/install/install-leapc.sh
deleted file mode 100755
index e47abb7c..00000000
--- a/pkg/osx/install/install-leapc.sh
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/bin/bash
-
-# Bitmask Installer Script.
-#
-# Copyright (C) 2013 LEAP Encryption Access Project
-#
-# This file is part of LEAP Client, as
-# available from http://leap.se/. This file is free software;
-# you can redistribute it and/or modify it under the terms of the GNU
-# General Public License (GPL) as published by the Free Software
-# Foundation, in version 2 as it comes in the "COPYING" file of the
-# LEAP Client distribution. LEAP Client is distributed in the
-# hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
-#
-
-set -e
-
-destlibs=/opt/local/lib
-leapdir=/Applications/LEAP\ Client.app
-leaplibs=${leapdir}/Contents/MacOS
-tunstartup=/Library/StartupItems/tun/tun
-
-echo "Installing Bitmask in /Applications..."
-cp -r "LEAP Client.app" /Applications
-
-echo "Copying openvpn binary..."
-cp -r openvpn.leap /usr/bin
-
-echo "Installing tun/tap drivers..."
-test -f $tunstartup && $tunstartup stop
-
-test -d /Library/Extensions || mkdir -p /Library/Extensions
-test -d /Library/StartupItems || mkdir -p /Library/StartupItems
-
-cp -r Extensions/* /Library/Extensions
-cp -r StartupItems/* /Library/StartupItems
-
-echo "Loading tun/tap kernel extension..."
-
-$tunstartup start
-
-echo "Installation Finished!"
diff --git a/pkg/osx/install/leap-installer.platypus b/pkg/osx/install/leap-installer.platypus
deleted file mode 100644
index 9150961e..00000000
--- a/pkg/osx/install/leap-installer.platypus
+++ /dev/null
@@ -1,90 +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>AcceptsFiles</key>
- <true/>
- <key>AcceptsText</key>
- <false/>
- <key>Authentication</key>
- <true/>
- <key>Author</key>
- <string>Kali Yuga</string>
- <key>BundledFiles</key>
- <array/>
- <key>Creator</key>
- <string>Platypus-4.7</string>
- <key>DeclareService</key>
- <false/>
- <key>Destination</key>
- <string>MyPlatypusApp.app</string>
- <key>DestinationOverride</key>
- <false/>
- <key>DevelopmentVersion</key>
- <false/>
- <key>DocIcon</key>
- <string></string>
- <key>Droppable</key>
- <false/>
- <key>ExecutablePath</key>
- <string>/opt/local/share/platypus/ScriptExec</string>
- <key>FileTypes</key>
- <array>
- <string>****</string>
- <string>fold</string>
- </array>
- <key>IconPath</key>
- <string></string>
- <key>Identifier</key>
- <string>se.leap.LEAPClientInstaller</string>
- <key>Interpreter</key>
- <string>/bin/sh</string>
- <key>InterpreterArgs</key>
- <array/>
- <key>Name</key>
- <string>LEAPClient Installer</string>
- <key>NibPath</key>
- <string>/opt/local/share/platypus/MainMenu.nib</string>
- <key>OptimizeApplication</key>
- <true/>
- <key>Output</key>
- <string>Progress Bar</string>
- <key>RemainRunning</key>
- <true/>
- <key>Role</key>
- <string>Viewer</string>
- <key>ScriptArgs</key>
- <array/>
- <key>ScriptPath</key>
- <string>./install/install-leapc.sh</string>
- <key>Secure</key>
- <false/>
- <key>ShowInDock</key>
- <false/>
- <key>StatusItemDisplayType</key>
- <string>Text</string>
- <key>StatusItemIcon</key>
- <data>
- </data>
- <key>StatusItemTitle</key>
- <string>MyPlatypusApp</string>
- <key>Suffixes</key>
- <array>
- <string>*</string>
- </array>
- <key>TextBackground</key>
- <string>#ffffff</string>
- <key>TextEncoding</key>
- <integer>4</integer>
- <key>TextFont</key>
- <string>Monaco</string>
- <key>TextForeground</key>
- <string>#000000</string>
- <key>TextSize</key>
- <real>10</real>
- <key>UseXMLPlistFormat</key>
- <true/>
- <key>Version</key>
- <string>1.0</string>
-</dict>
-</plist>
diff --git a/pkg/osx/install/tun.kext/Info.plist b/pkg/osx/install/tun.kext/Info.plist
deleted file mode 100644
index fb69ba85..00000000
--- a/pkg/osx/install/tun.kext/Info.plist
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>CFBundleDevelopmentRegion</key>
- <string>English</string>
- <key>CFBundleExecutable</key>
- <string>tun</string>
- <key>CFBundleIdentifier</key>
- <string>leap.tun</string>
- <key>CFBundleInfoDictionaryVersion</key>
- <string>6.0</string>
- <key>CFBundleName</key>
- <string>tun</string>
- <key>CFBundlePackageType</key>
- <string>KEXT</string>
- <key>CFBundleShortVersionString</key>
- <string>20120120</string>
- <key>CFBundleSignature</key>
- <string>????</string>
- <key>CFBundleVersion</key>
- <string>1.0</string>
- <key>OSBundleLibraries</key>
- <dict>
- <key>com.apple.kpi.mach</key>
- <string>8.0</string>
- <key>com.apple.kpi.bsd</key>
- <string>8.0</string>
- <key>com.apple.kpi.libkern</key>
- <string>8.0</string>
- <key>com.apple.kpi.unsupported</key>
- <string>8.0</string>
- </dict>
-</dict>
-</plist>
-
diff --git a/pkg/osx/post-inst.sh b/pkg/osx/post-inst.sh
new file mode 100755
index 00000000..f88ea97a
--- /dev/null
+++ b/pkg/osx/post-inst.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+# Bitmask Post-Instalation script
+
+cp se.leap.bitmask-helper.plist /Library/LaunchDaemons/
+launchctl load /Library/LaunchDaemons/se.leap.bitmask-helper.plist
+cp tuntap_20150118.pkg /tmp/
+open /tmp/tuntap_20150118.pkg
diff --git a/pkg/osx/pre-inst.sh b/pkg/osx/pre-inst.sh
new file mode 100755
index 00000000..1651a221
--- /dev/null
+++ b/pkg/osx/pre-inst.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+# Bitmask Post-Instalation script
+[[ -f /Library/LaunchDaemons/se.leap.bitmask-helper.plist ]] && launchctl unload /Library/LaunchDaemons/se.leap.bitmask-helper.plist
diff --git a/pkg/osx/se.leap.bitmask-helper.plist b/pkg/osx/se.leap.bitmask-helper.plist
new file mode 100644
index 00000000..4428f131
--- /dev/null
+++ b/pkg/osx/se.leap.bitmask-helper.plist
@@ -0,0 +1,29 @@
+<?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>StandardOutPath</key>
+ <string>bitmask-helper.log</string>
+ <key>StandardErrorPath</key>
+ <string>bitmask-helper-err.log</string>
+ <key>GroupName</key>
+ <string>daemon</string>
+ <key>KeepAlive</key>
+ <dict>
+ <key>SuccessfulExit</key>
+ <false/>
+ </dict>
+ <key>Label</key>
+ <string>se.leap.bitmask-helper</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>/Applications/Bitmask.app/Contents/Resources/bitmask-helper/bitmask-helper</string>
+ </array>
+ <key>RunAtLoad</key>
+ <true/>
+ <key>WorkingDirectory</key>
+ <string>/Applications/Bitmask.app/Contents/Resources/bitmask-helper/</string>
+ <key>SessionCreate</key>
+ <true/>
+</dict>
+</plist>
diff --git a/pkg/pyinst/README.rst b/pkg/pyinst/README.rst
new file mode 100644
index 00000000..b6784a52
--- /dev/null
+++ b/pkg/pyinst/README.rst
@@ -0,0 +1,17 @@
+Building da bundles
+--------------------
+Because, you know, bundles are cool. Who needs a decent package manager nowadays? </rant>.
+
+You need a couple of things in your virtualenv:
+
+- All the dependencies. A sumo tarball is probably a good idea.
+- PyInstaller. Version 3 or higher.
+- A PySide build. While the postmkenv.sh hack is good enough for
+ developing, you will need a wheel built with --standalone flag.
+ See
+ http://pyside.readthedocs.org/en/latest/building/linux.html#building-pyside-distribution::
+
+ $ python2.7 setup.py bdist_wheel --qmake=/usr/bin/qmake-qt4 --standalone
+
+ (since this takes a while, you can probably grab the already built wheel from
+ the leap servers).
diff --git a/pkg/pyinst/bitmask.spec b/pkg/pyinst/bitmask.spec
index 2bc2f9d2..8c6561cf 100644
--- a/pkg/pyinst/bitmask.spec
+++ b/pkg/pyinst/bitmask.spec
@@ -1,31 +1,49 @@
# -*- mode: python -*-
+import sys
block_cipher = None
-
-a = Analysis([os.path.join('pkg', 'pyinst', 'bitmask.py')],
+# TODO remove QtWebKit for bundles that don't ship pixelated???
+a = Analysis(['bitmask.py'],
hiddenimports=[
- 'zope.interface', 'zope.proxy',
- 'PySide.QtCore', 'PySide.QtGui'],
+ 'zope.interface', 'zope.proxy',
+ 'PySide.QtCore', 'PySide.QtGui', 'PySide.QtWebKit',
+ 'pysqlcipher', 'service_identity',
+ 'leap.common', 'leap.bitmask'
+ ],
+ binaries=None,
+ datas=None,
hookspath=None,
runtime_hooks=None,
excludes=None,
+ win_no_prefer_redirects=None,
+ win_private_assemblies=None,
cipher=block_cipher)
-pyz = PYZ(a.pure,
+pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
+
+# Binary files you need to include in the form of:
+# (<destination>, <source>, '<TYPE>')
+
+# Data files you want to include, in the form of:
+# (<destination>, <source>, '<TYPE>')
+data = [
+ ('qt.conf', 'qt.conf', 'DATA')
+]
exe = EXE(pyz,
a.scripts,
exclude_binaries=True,
name='bitmask',
- debug=False,
- strip=False,
+ debug=True,
+ strip=None,
upx=True,
- console=False )
+ console=False,
+ icon='../../data/images/mask-icon.ico')
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
- strip=False,
+ strip=None,
upx=True,
name='bitmask')
if sys.platform.startswith("darwin"):
@@ -33,6 +51,7 @@ if sys.platform.startswith("darwin"):
name=os.path.join(
'dist', 'Bitmask.app'),
appname='Bitmask',
- version='0.9.0rc2',
+ # TODO get this from ../next-version.txt
+ version='0.9.0.rc1',
icon='pkg/osx/bitmask.icns',
- bundle_identifier='bitmask-0.9.0rc2')
+ bundle_identifier='bitmask-0.9.0alpha7')
diff --git a/pkg/pyinst/bitmask.spec.orig b/pkg/pyinst/bitmask.spec.orig
new file mode 100644
index 00000000..617104b9
--- /dev/null
+++ b/pkg/pyinst/bitmask.spec.orig
@@ -0,0 +1,38 @@
+# -*- mode: python -*-
+
+block_cipher = None
+
+
+a = Analysis([os.path.join('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/pyinst/bitmask_cli b/pkg/pyinst/bitmask_cli
new file mode 120000
index 00000000..7842f03b
--- /dev/null
+++ b/pkg/pyinst/bitmask_cli
@@ -0,0 +1 @@
+../../src/leap/bitmask/cli/bitmask_cli.py \ No newline at end of file
diff --git a/pkg/pyinst/bitmask_cli.spec b/pkg/pyinst/bitmask_cli.spec
new file mode 100644
index 00000000..5157e179
--- /dev/null
+++ b/pkg/pyinst/bitmask_cli.spec
@@ -0,0 +1,32 @@
+# -*- mode: python -*-
+
+block_cipher = None
+
+
+a = Analysis(['bitmask_cli'],
+ pathex=['/home/kali/leap/bitmask_client/pkg/pyinst'],
+ binaries=None,
+ datas=None,
+ hiddenimports=[],
+ hookspath=[],
+ runtime_hooks=[],
+ excludes=[],
+ win_no_prefer_redirects=False,
+ win_private_assemblies=False,
+ cipher=block_cipher)
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+exe = EXE(pyz,
+ a.scripts,
+ exclude_binaries=True,
+ name='bitmask_cli',
+ debug=False,
+ strip=False,
+ upx=True,
+ console=True )
+coll = COLLECT(exe,
+ a.binaries,
+ a.zipfiles,
+ a.datas,
+ strip=False,
+ upx=True,
+ name='bitmask_cli')
diff --git a/pkg/pyinst/bitmaskd b/pkg/pyinst/bitmaskd
new file mode 120000
index 00000000..c5b08597
--- /dev/null
+++ b/pkg/pyinst/bitmaskd
@@ -0,0 +1 @@
+../../src/leap/bitmask/core/launcher.py \ No newline at end of file
diff --git a/pkg/pyinst/bitmaskd.spec b/pkg/pyinst/bitmaskd.spec
new file mode 100644
index 00000000..4e2e2d6a
--- /dev/null
+++ b/pkg/pyinst/bitmaskd.spec
@@ -0,0 +1,33 @@
+# -*- mode: python -*-
+
+block_cipher = None
+
+
+a = Analysis(['bitmaskd'],
+ pathex=['/home/kali/leap/bitmask_client/pkg/pyinst'],
+ binaries=None,
+ datas=None,
+ hiddenimports=[],
+ hookspath=[],
+ runtime_hooks=[],
+ excludes=[],
+ win_no_prefer_redirects=False,
+ win_private_assemblies=False,
+ cipher=block_cipher)
+pyz = PYZ(a.pure, a.zipped_data,
+ cipher=block_cipher)
+exe = EXE(pyz,
+ a.scripts,
+ exclude_binaries=True,
+ name='bitmaskd',
+ debug=False,
+ strip=False,
+ upx=True,
+ console=True )
+coll = COLLECT(exe,
+ a.binaries,
+ a.zipfiles,
+ a.datas,
+ strip=False,
+ upx=True,
+ name='bitmaskd')
diff --git a/pkg/pyinst/multi.spec b/pkg/pyinst/multi.spec
new file mode 100644
index 00000000..aa5c83c0
--- /dev/null
+++ b/pkg/pyinst/multi.spec
@@ -0,0 +1,75 @@
+
+# -*- mode: python -*-
+
+block_cipher = None
+
+
+gui_a = Analysis(['bitmask.py'],
+ hiddenimports=[
+ 'zope.interface', 'zope.proxy',
+ 'PySide.QtCore', 'PySide.QtGui'],
+ hookspath=None,
+ runtime_hooks=None,
+ excludes=None,
+ cipher=block_cipher)
+cli_a = Analysis(['bitmask_cli'],
+ binaries=None,
+ datas=None,
+ hiddenimports=[
+ 'zope.interface', 'zope.proxy'],
+ hookspath=[],
+ runtime_hooks=[],
+ excludes=[],
+ win_no_prefer_redirects=False,
+ win_private_assemblies=False,
+ cipher=block_cipher)
+daemon_a = Analysis(['bitmaskd'],
+ binaries=None,
+ datas=None,
+ hiddenimports=[
+ 'leap.bitmask.core.service'],
+ hookspath=[],
+ runtime_hooks=[],
+ excludes=[],
+ win_no_prefer_redirects=False,
+ win_private_assemblies=False,
+ cipher=block_cipher)
+
+MERGE( (gui_a, 'bitmask', 'bitmask'),
+ (cli_a, 'bitmask_cli', 'bitmask'),
+ (daemon_a, 'bitmaskd', 'bitmaskd'))
+
+gui_pyz = PYZ(gui_a.pure, gui_a.zipped_data, cipher=block_cipher)
+gui_exe = EXE(gui_pyz,
+ gui_a.scripts,
+ exclude_binaries=True,
+ name='bitmask', debug=False, strip=False,
+ upx=True, console=False )
+
+cli_pyz = PYZ(cli_a.pure, cli_a.zipped_data, cipher=block_cipher)
+cli_exe = EXE(cli_pyz,
+ cli_a.scripts,
+ exclude_binaries=True,
+ name='bitmask_cli', debug=False, strip=False,
+ upx=True, console=True)
+daemon_pyz = PYZ(daemon_a.pure, daemon_a.zipped_data, cipher=block_cipher)
+daemon_exe = EXE(daemon_pyz,
+ daemon_a.scripts,
+ exclude_binaries=True,
+ name='bitmaskd', debug=False, strip=False, upx=True, console=True )
+
+gui_coll = COLLECT(gui_exe,
+ gui_a.binaries,
+ gui_a.zipfiles,
+ gui_a.datas,
+ strip=False, upx=True, name='bitmask')
+cli_coll = COLLECT(cli_exe,
+ cli_a.binaries,
+ cli_a.zipfiles,
+ cli_a.datas,
+ strip=False, upx=True, name='bitmask_cli')
+daemon_coll = COLLECT(daemon_exe,
+ daemon_a.binaries,
+ daemon_a.zipfiles,
+ daemon_a.datas,
+ strip=False, upx=True, name='bitmaskd')
diff --git a/pkg/pyinst/pyinst-build.mk b/pkg/pyinst/pyinst-build.mk
new file mode 100644
index 00000000..32239f8e
--- /dev/null
+++ b/pkg/pyinst/pyinst-build.mk
@@ -0,0 +1,98 @@
+freeze-ver:
+ cp pkg/version-template src/leap/bitmask/_version.py
+ sed -i 's/^version_version\(.*\)/version_version = "$(NEXT_VERSION)"/' src/leap/bitmask/_version.py
+ sed -i "s/^full_revisionid\(.*\)/full_revisionid='$(GIT_COMMIT)'/" src/leap/bitmask/_version.py
+
+freeze-ver-osx:
+ cp pkg/version-template src/leap/bitmask/_version.py
+ sed -i ' ' 's/^version_version\(.*\)/version_version = "$(NEXT_VERSION)"/' src/leap/bitmask/_version.py
+ sed -i ' ' "s/^full_revisionid\(.*\)/full_revisionid='$(GIT_COMMIT)'/" src/leap/bitmask/_version.py
+
+hash-binaries:
+ OPENVPN_BIN=$(LEAP_BUILD_DIR)openvpn BITMASK_ROOT=pkg/linux/bitmask-root python setup.py hash_binaries
+
+pyinst: freeze-ver hash-binaries
+ pyinstaller -y pkg/pyinst/bitmask.spec
+
+pyinst_osx: freeze-ver-osx hash-binaries
+ pyinstaller -y pkg/pyinst/bitmask.spec
+
+reset-ver:
+ git checkout -- src/leap/bitmask/_version.py
+
+pyinst-hacks-linux:
+ # XXX this should be taken care of by pyinstaller data collector
+ cp ../leap_common/src/leap/common/cacert.pem $(DIST)
+ mkdir -p $(DIST)pysqlcipher
+ mkdir -p $(DIST)pixelated
+ mkdir -p $(DIST)twisted/web
+ cp $(VIRTUAL_ENV)/lib/python2.7/site-packages/pysqlcipher/_sqlite.so $(DIST)pysqlcipher
+ cp -r $(VIRTUAL_ENV)/lib/python2.7/site-packages/pixelated_www $(DIST)
+ cp -r $(VIRTUAL_ENV)/lib/python2.7/site-packages/pixelated/assets/ $(DIST)pixelated
+ cp -r $(VIRTUAL_ENV)/lib/python2.7/site-packages/twisted/web/failure.xhtml $(DIST)twisted/web/
+
+pyinst-hacks-osx:
+ # XXX this should be taken care of by pyinstaller data collector
+ cp $(VIRTUAL_ENV)/lib/python2.7/site-packages/leap/common/cacert.pem $(DIST)
+ cp $(VIRTUAL_ENV)/lib/python2.7/site-packages/leap/common/cacert.pem $(DIST_OSX)Contents/MacOS/
+ mv $(DIST_OSX)Contents/MacOS/bitmask $(DIST_OSX)Contents/MacOS/bitmask-app
+ cp pkg/osx/bitmask-wrapper $(DIST_OSX)Contents/MacOS/bitmask
+ # XXX need the rest???
+
+pyinst-trim:
+ rm -f $(DIST)libQtOpenGL.so.4
+ rm -f $(DIST)libQtSql.so.4
+ rm -f $(DIST)libQt3Support.so.4
+ rm -f $(DIST)libaudio.so.2
+ rm -f $(DIST)libnvidia-*
+
+pyinst-cleanup:
+ rm -rf $(DIST)config
+ mkdir -p $(DIST_VERSION)
+ mv $(DIST) $(DIST_VERSION)lib
+ cd pkg/launcher && make
+ mv pkg/launcher/bitmask $(DIST_VERSION)
+
+pyinst-distribution-data:
+ cp release-notes.rst $(DIST_VERSION)
+ cp LICENSE $(DIST_VERSION)
+
+pyinst-helpers-linux:
+ mkdir -p $(DIST_VERSION)apps/eip/files
+ cp $(LEAP_BUILD_DIR)openvpn $(DIST_VERSION)apps/eip/files/leap-openvpn
+ cp pkg/linux/bitmask-root $(DIST_VERSION)apps/eip/files/
+ cp pkg/linux/leap-install-helper.sh $(DIST_VERSION)apps/eip/files/
+ cp pkg/linux/polkit/se.leap.bitmask.bundle.policy $(DIST_VERSION)apps/eip/files/
+ mkdir -p $(DIST_VERSION)apps/mail
+ cp $(LEAP_BUILD_DIR)gpg $(DIST_VERSION)apps/mail
+
+pyinst-helpers-osx:
+ mkdir -p $(DIST_OSX_RES)bitmask-helper
+ mkdir -p $(DIST_OSX)Contents/MacOS/apps/mail
+ cp pkg/osx/client.up.sh $(DIST_OSX_RES)
+ cp pkg/osx/client.down.sh $(DIST_OSX_RES)
+ cp pkg/osx/bitmask-helper $(DIST_OSX_RES)bitmask-helper/
+ cp pkg/osx/bitmask.pf.conf $(DIST_OSX_RES)bitmask-helper/
+ cp pkg/osx/se.leap.bitmask-helper.plist $(DIST_OSX_RES)bitmask-helper/
+ cp pkg/osx/post-inst.sh $(DIST_OSX_RES)bitmask-helper/
+ cp pkg/osx/daemon/daemon.py $(DIST_OSX_RES)bitmask-helper/
+ cp /opt/homebrew-cask/Caskroom/tuntap/20150118/tuntap_20150118.pkg $(DIST_OSX_RES)
+ # TODO make the build script put it there
+ cp $(LEAP_BUILD_DIR)openvpn.leap.polarssl $(DIST_OSX_RES)openvpn.leap
+ cp $(LEAP_BUILD_DIR)gpg $(DIST_OSX)Contents/MacOS/apps/mail/
+
+pyinst-tar:
+ cd dist/ && tar cvzf Bitmask.$(NEXT_VERSION).tar.gz bitmask-$(NEXT_VERSION)
+
+pyinst-sign:
+ gpg2 -a --sign --detach-sign dist/Bitmask.$(NEXT_VERSION).tar.gz
+
+pyinst-upload:
+ rsync --rsh='ssh' -avztlpog --progress --partial dist/Bitmask.$(NEXT_VERSION).* salmon.leap.se:./
+
+pyinst-linux: pyinst reset-ver pyinst-hacks-linux pyinst-trim pyinst-cleanup pyinst-distribution-data pyinst-helpers-linux pyinst-tar
+
+pyinst-osx: pyinst_osx reset-ver pyinst-hacks-osx pyinst-helpers-osx
+
+clean_pkg:
+ rm -rf build dist
diff --git a/pkg/pyinst/qt.conf b/pkg/pyinst/qt.conf
new file mode 100644
index 00000000..7eecd342
--- /dev/null
+++ b/pkg/pyinst/qt.conf
@@ -0,0 +1,2 @@
+[Paths]
+Plugins=qt4_plugins \ No newline at end of file
diff --git a/pkg/pyinst/trim-notes.txt b/pkg/pyinst/trim-notes.txt
new file mode 100644
index 00000000..2d825760
--- /dev/null
+++ b/pkg/pyinst/trim-notes.txt
@@ -0,0 +1,12 @@
+88M
+-------------
+libaudio.so.2
+libQt3Support.so.4
+libQtNetwork.so.4
+libtiff.so.5
+libQtSvg.so.4
+libQtXml.so.4
+libQtSql.so.4
+libQtOpenGL.so.4
+libnvidia-glcore.so.352.79
+PySide.QtNetwork.x86_64-linux-gnu.so
diff --git a/pkg/requirements-leap.pip b/pkg/requirements-leap.pip
index 8e353c33..abaeb8fc 100644
--- a/pkg/requirements-leap.pip
+++ b/pkg/requirements-leap.pip
@@ -1,4 +1,4 @@
-leap.soledad.client>=0.6.0
-leap.keymanager>=0.4.0
-leap.mail>=0.4.0
-leap.common>=0.4.0
+leap.soledad.client>=0.8.0
+leap.keymanager>=0.5.0
+leap.mail>=0.4.1
+leap.common>=0.5.1
diff --git a/pkg/requirements-pixelated.pip b/pkg/requirements-pixelated.pip
new file mode 100644
index 00000000..b3e57a1b
--- /dev/null
+++ b/pkg/requirements-pixelated.pip
@@ -0,0 +1,5 @@
+--find-links https://downloads.leap.se/libs/pixelated/
+pixelated-user-agent
+pixelated-www
+whoosh
+leap.auth \ No newline at end of file
diff --git a/pkg/scripts/bootstrap_develop.sh b/pkg/scripts/bootstrap_develop.sh
index 68edcd43..af3dce20 100755
--- a/pkg/scripts/bootstrap_develop.sh
+++ b/pkg/scripts/bootstrap_develop.sh
@@ -118,6 +118,8 @@ setup_develop() {
# hack to solve gnupg version problem
pip uninstall -y gnupg && pip install gnupg
+ # XXX this fails in trusty; see #8009
+ # pip install -r pkg/requirements-pixelated.pip
set +x
echo "${cc_green}Status: $status done.${cc_normal}"
}
diff --git a/pkg/sumo-tarballs.mk b/pkg/sumo-tarballs.mk
new file mode 100644
index 00000000..3bd5fa77
--- /dev/null
+++ b/pkg/sumo-tarballs.mk
@@ -0,0 +1,25 @@
+checkout_leapdeps_release:
+ pkg/scripts/checkout_leap_versions.sh
+
+setup_without_namespace:
+ awk '!/namespace_packages*/' setup.py > file && mv file setup.py
+
+sumo_tarball_release: checkout_leapdeps_release setup_without_namespace
+ python setup.py sdist --sumo
+ git checkout -- src/leap/__init__.py
+ git checkout -- src/leap/bitmask/_version.py
+ rm -rf src/leap/soledad
+ git checkout -- setup.py
+
+# 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 setup_without_namespace
+ python setup.py sdist --sumo # --latest
+ git checkout -- src/leap/__init__.py
+ git checkout -- src/leap/bitmask/_version.py
+ rm -rf src/leap/soledad
+ git checkout -- setup.py
diff --git a/pkg/tools/profile.mk b/pkg/tools/profile.mk
new file mode 100644
index 00000000..8d45c01a
--- /dev/null
+++ b/pkg/tools/profile.mk
@@ -0,0 +1,23 @@
+do_cprofile:
+ python -m cProfile -o bitmask.cprofile src/leap/bitmask/app.py --debug -N
+
+view_cprofile:
+ cprofilev bitmask.cprofile
+
+mailprofile:
+ gprof2dot -f pstats /tmp/leap_mail_profile.pstats -n 0.2 -e 0.2 | dot -Tpdf -o /tmp/leap_mail_profile.pdf
+
+do_lineprof:
+ LEAP_PROFILE_IMAPCMD=1 LEAP_MAIL_MANHOLE=1 kernprof.py -l src/leap/bitmask/app.py --debug
+
+do_lineprof_offline:
+ LEAP_PROFILE_IMAPCMD=1 LEAP_MAIL_MANHOLE=1 kernprof.py -l src/leap/bitmask/app.py --offline --debug -N
+
+view_lineprof:
+ @python -m line_profiler app.py.lprof | $(EDITOR) -
+
+resource_graph:
+ #./pkg/scripts/monitor_resource.zsh `ps aux | grep app.py | head -1 | awk '{print $$2}'` $(RESOURCE_TIME)
+ ./pkg/scripts/monitor_resource.zsh `pgrep bitmask` $(RESOURCE_TIME)
+ display bitmask-resources.png
+
diff --git a/pkg/version-template b/pkg/version-template
new file mode 100644
index 00000000..ecb5b987
--- /dev/null
+++ b/pkg/version-template
@@ -0,0 +1,8 @@
+version_version = "xxx"
+full_revisionid = 'deadbeef'
+
+
+def get_versions(default={}, verbose=False):
+ return {'version': version_version,
+ 'full-revisionid': full_revisionid}
+
diff --git a/pkg/windows/Makefile b/pkg/windows/Makefile
new file mode 100644
index 00000000..5d19353e
--- /dev/null
+++ b/pkg/windows/Makefile
@@ -0,0 +1,25 @@
+.PHONY: all pkg installer openvpn pyinstaller
+all:
+ docker-compose build
+ $(MAKE) pkg
+
+pkg:
+ $(MAKE) openvpn
+ $(MAKE) pyinstaller
+ $(MAKE) installer
+
+pyinstaller:
+ docker-compose run --rm pyinstaller
+
+openvpn:
+ docker-compose run --rm openvpn
+
+installer:
+ docker-compose run --rm installer
+
+clean:
+ docker rmi windows_pyinstaller
+ docker rmi windows_openvpn
+ docker rmi windows_installer
+ rm -rf ../../dist/*.exe
+ rm -rf ../../build/* \ No newline at end of file
diff --git a/pkg/windows/README.rst b/pkg/windows/README.rst
new file mode 100644
index 00000000..0bdfb1d1
--- /dev/null
+++ b/pkg/windows/README.rst
@@ -0,0 +1,144 @@
+Environment setup in debian:jessie
+==================================
+
+basically you need this to setup your environment:
+
+# apt-get install mingw-w64
+# apt-get install wine
+# apt-get install nsis
+
+this is a incomplete list of dependencies, review the pyinstaller/Dockerfile
+to get a understanding of what needs to be setup in order to have a
+environment that builds the installer
+
+Requirements
+============
+
+docker-compose
+
+Building the package
+====================
+
+make pkg
+
+
+Reproducible builds
+===================
+
+please run the binary and installer builds on a clean machine eg
+using docker or any virtual environment that can easily be prepared
+by a third party to verify that the binaries are actually what the
+sourcecode suggests.
+
+to use reproducible build you need to install docker which then installs
+a clean debian:jessie to install nsis or the mingw environment
+
+
+Installer
+=========
+
+NSIS was choosen because it provided a out of the box toolchain to build
+installers for the windows platform with minimal dependencies. The downside
+of nsis is that it does not produce msi binaries
+
+to build the binary dependencies run:
+
+```
+docker-compose run --rm openvpn
+docker-compose run --rm pyinstaller
+```
+
+the produced binaries will be stored in ${ROOT}/build
+
+to build the installer run:
+
+```
+docker-compose run --rm installer
+```
+
+the produced installer will be stored in ${ROOT}/dist
+
+
+Pyinstaller
+===========
+
+Pyinstaller is a docker image based on debian:jessie with a cross-compile
+toolchain (gcc) for building zlib and openssl in linux and wine (staging)
+with installed python and mingw32 for pip/wheel compiling.
+All pip installed dependencies are
+part of the pyinstaller-build.sh script so they can be re-executed when the
+dependencies of the project change. The image should be rebuild when openssl,
+python or pyinstaller is updated:
+
+```
+docker-compose build pyinstaller
+```
+
+To debug or fine-tune the compile process it may be useful to setup the
+following software on the development machine:
+
+```
+X :1 -listen tcp
+DISPLAY=:1 xhost +
+docker-compose run --rm pyinstaller /bin/bash
+root@0fa19215321f:/# export DISPLAY=${YOUR_LOCAL_IP}:1
+root@0fa19215321f:/# wine cmd
+Z:\>python
+>>>
+```
+
+the configured volumes are:
+
+- the (read-only) sourcecode of the bitmask project in /var/src/bitmask
+- the result of the builds in /var/build
+
+pyinstaller-build.sh
+====================
+
+Contains all steps to build the win32 executables. The project relies on
+a read-write source tree which will pollute the development environment and
+make it hard to reproduce 'clean' builds. therefore it expects that the source
+is freshly checked out and not used to run in the host-environment. Otherwise
+pyc and ui elements will mess up the binary in unpredictable ways.
+
+* copy the /var/src/bitmask sources to a read-write location (/var/build)
+* execute ```make all``` in wine to build the qt ui and other resources
+* execute ```pip install $dependencies``` to have all dependencies available
+* execute ```pyinstaller``` in wine to compile the executable for
+** bitmask (src/leap/bitmask/app.py)
+* cleanup
+** remove the read-write copy
+** remove wine-dlls from the installer
+
+As the step 'install dependencies' may take long on slow internet connections
+during development it is advised to recycle the container and share the
+build/executables path with a windows-vm to test the result in short cycles
+instead of make pkg, uninstall, install.
+
+```
+docker-compose run --rm --entrypoint=/bin/bash pyinstalle
+root@0fa19215321f:/# cd /var/src/bitmask/pkg/windows
+root@0fa19215321f:/var/src/bitmask/pkg/windows# ./pyinstaller-build.sh
+root@0fa19215321f:/var/src/bitmask/pkg/windows# ./pyinstaller-build.sh
+root@0fa19215321f:/var/src/bitmask/pkg/windows# ./pyinstaller-build.sh
+....
+```
+
+and test the result binary (accessible in bitmask/build in a separate vm.
+
+OpenVPN
+=======
+
+OpenVPN is a straight forward cross compile image that builds the openvpn
+sourcecode from the git-repository to a windows executable that can be
+used by bitmask_root to launch eip.
+It needs to be rebuild regulary as openssl gets a new version about every
+month. PyInstaller uses the openssl that is compiled by this image
+
+Installer
+=========
+
+Installer is a straight forward debian image with makensis installed. The
+installer-build script lists the previously built files from pyinstaller and
+openvpn to pass it as nsh file to makensis. bitmask.nis controls what will
+be displayed to the user and how the components are installed and uninstalled \ No newline at end of file
diff --git a/pkg/windows/TODO b/pkg/windows/TODO
new file mode 100644
index 00000000..49c6bac1
--- /dev/null
+++ b/pkg/windows/TODO
@@ -0,0 +1,15 @@
+TODO
+====
+
+fix python-code (0.9.1) that fails on windows:
+- fix the race condition for the backend/frontend startup.
+ spawning the backend takes time. with a introduced 15s timeout
+ it was possible to ensure the backend is up. It would be ideal
+ if the backend could signal the app to continue loading the frontend
+- fix the ~/leap/events/zmq_certificates/public_keys/server.key
+- fix logger (& remove hack in pyinstaller-build.sh:228)
+- fix pysqlcipher (/LIB:,https get not working in setup.py, remove hack
+ in pyinstaller-build:164)
+- merge bitmask_root from https://github.com/alirezamirzaeiyan/bitmask-root
+
+create similar infrastructure for osx dmg, preferably run on osx for compressed dmg \ No newline at end of file
diff --git a/pkg/windows/bitmask.nis b/pkg/windows/bitmask.nis
new file mode 100644
index 00000000..8705c058
--- /dev/null
+++ b/pkg/windows/bitmask.nis
@@ -0,0 +1,2 @@
+!define PKGNAME bitmask
+!include .\bitmask.nsh \ No newline at end of file
diff --git a/pkg/windows/bitmask.nsh b/pkg/windows/bitmask.nsh
new file mode 100644
index 00000000..fe02f84e
--- /dev/null
+++ b/pkg/windows/bitmask.nsh
@@ -0,0 +1,115 @@
+# pwd is a ro-mounted source-tree that had all dependencies build into
+# package-name directories
+!define PKGNAMEPATH ..\..\build\executables\${PKGNAME}
+!include ${PKGNAMEPATH}_version.nsh
+!include .\bitmask_client_product.nsh
+!include ${PKGNAMEPATH}_install_files_size.nsh
+
+RequestExecutionLevel admin ;Require admin rights on NT6+ (When UAC is turned on)
+
+InstallDir "$PROGRAMFILES\${APPNAME}"
+
+LicenseData "..\..\LICENSE"
+Name "${COMPANYNAME} - ${APPNAME}"
+Icon "..\..\build\executables\mask-icon.ico"
+
+# /var/dist is a rw mounted volume
+outFile "/var/dist/${PKGNAME}-${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}${VERSIONSUFFIX}.exe"
+!include LogicLib.nsh
+
+# Just three pages - license agreement, install location, and installation
+page license
+page directory
+Page instfiles
+
+!macro VerifyUserIsAdmin
+UserInfo::GetAccountType
+pop $0
+${If} $0 != "admin" ;Require admin rights on NT4+
+ messageBox mb_iconstop "Administrator rights required!"
+ setErrorLevel 740 ;ERROR_ELEVATION_REQUIRED
+ quit
+${EndIf}
+!macroend
+
+function .onInit
+ setShellVarContext all
+ !insertmacro VerifyUserIsAdmin
+functionEnd
+
+section "TAP Virtual Ethernet Adapter" SecTAP
+ SetOverwrite on
+ SetOutPath "$TEMP"
+ File /oname=tap-windows.exe "..\..\build\executables\openvpn\tap-windows.exe"
+
+ DetailPrint "Installing TAP (may need confirmation)..."
+ nsExec::ExecToLog '"$TEMP\tap-windows.exe" /S /SELECT_UTILITIES=1'
+ Pop $R0 # return value/error/timeout
+
+ Delete "$TEMP\tap-windows.exe"
+ WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "tap" "installed"
+sectionEnd
+
+section "install"
+ setOutPath $INSTDIR
+
+ !include ${PKGNAMEPATH}_install_files.nsh
+
+ # Uninstaller - See function un.onInit and section "uninstall" for configuration
+ writeUninstaller "$INSTDIR\uninstall.exe"
+
+ # Start Menu
+ createDirectory "$SMPROGRAMS\${COMPANYNAME}"
+ createShortCut "$SMPROGRAMS\${COMPANYNAME}\${APPNAME}.lnk" "$INSTDIR\bitmask.exe" "" "$INSTDIR\bitmask.exe"
+
+ !include bitmask_client_registry_install.nsh
+sectionEnd
+
+# Uninstaller
+
+function un.onInit
+ SetShellVarContext all
+ !insertmacro VerifyUserIsAdmin
+functionEnd
+
+section "uninstall"
+
+ delete "$SMPROGRAMS\${COMPANYNAME}\${APPNAME}.lnk"
+ # Try to remove the Start Menu folder - this will only happen if it is empty
+ rmDir "$SMPROGRAMS\${COMPANYNAME}"
+
+ # Remove files
+ !include ${PKGNAMEPATH}_uninstall_files.nsh
+
+ # Remove TAP Drivers
+ ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "tap"
+ ${If} $R0 == "installed"
+ DetailPrint "Uninstalling TAP as we installed it..."
+ ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\TAP-Windows" "UninstallString"
+ ${If} $R0 != ""
+ DetailPrint "Uninstalling TAP..."
+ nsExec::ExecToLog '"$R0" /S'
+ Pop $R0 # return value/error/timeout
+ ${Else}
+ # on x64 windows the uninstall location needs to be accessed using WOW
+ SetRegView 64
+ ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\TAP-Windows" "UninstallString"
+ SetRegView 32
+ ${If} $R0 != ""
+ DetailPrint "Uninstalling TAP 64..."
+ nsExec::ExecToLog '"$R0" /S'
+ Pop $R0 # return value/error/timeout
+ ${EndIf}
+ ${EndIf}
+ DeleteRegValue HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "tap"
+ ${EndIf}
+
+ # Always delete uninstaller as the last action
+ delete $INSTDIR\uninstall.exe
+
+ # Try to remove the install directory - this will only happen if it is empty
+ rmDir $INSTDIR
+
+ # Remove uninstaller information from the registry
+ DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}"
+sectionEnd \ No newline at end of file
diff --git a/pkg/windows/bitmask_client_product.nsh b/pkg/windows/bitmask_client_product.nsh
new file mode 100644
index 00000000..64f59ac7
--- /dev/null
+++ b/pkg/windows/bitmask_client_product.nsh
@@ -0,0 +1,8 @@
+!define APPNAME "Bitmask"
+!define COMPANYNAME "leap.se"
+!define DESCRIPTION "With Bitmask VPN, all your traffic is securely routed through your provider before it is decrypted and sent on to the open internet."
+# These will be displayed by the "Click here for support information" link in "Add/Remove Programs"
+# It is possible to use "mailto:" links in here to open the email client
+!define HELPURL "https://bitmask.net/en/help" # "Support Information" link
+!define UPDATEURL "https://bitmask.net/en/install" # "Product Updates" link
+!define ABOUTURL "https://bitmask.net/" # "Publisher" link \ No newline at end of file
diff --git a/pkg/windows/bitmask_client_registry_install.nsh b/pkg/windows/bitmask_client_registry_install.nsh
new file mode 100644
index 00000000..5bf04045
--- /dev/null
+++ b/pkg/windows/bitmask_client_registry_install.nsh
@@ -0,0 +1,17 @@
+WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "DisplayName" "${COMPANYNAME} - ${APPNAME} - ${DESCRIPTION}"
+WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
+WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
+WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "InstallLocation" "$\"$INSTDIR$\""
+WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "DisplayIcon" "$\"$INSTDIR\bitmask.exe$\""
+WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "Publisher" "${COMPANYNAME}"
+WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "HelpLink" "${HELPURL}"
+WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "URLUpdateInfo" "$\"${UPDATEURL}$\""
+WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "URLInfoAbout" "$\"${ABOUTURL}$\""
+WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "DisplayVersion" "${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}${VERSIONSUFFIX}"
+WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "VersionMajor" ${VERSIONMAJOR}
+WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "VersionMinor" ${VERSIONMINOR}
+# There is no option for modifying or repairing the install
+WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "NoModify" 1
+WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "NoRepair" 1
+# Set the INSTALLSIZE constant (!defined at the top of this script) so Add/Remove Programs can accurately report the size
+WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "EstimatedSize" ${INSTALLSIZE} \ No newline at end of file
diff --git a/pkg/windows/docker-compose.yml b/pkg/windows/docker-compose.yml
new file mode 100644
index 00000000..92b310c8
--- /dev/null
+++ b/pkg/windows/docker-compose.yml
@@ -0,0 +1,42 @@
+# mingw environment to build dependency binaries in a reproducible environment
+# https://wiki.debian.org/ReproducibleBuilds
+# service to build a windows executable using pyinstaller
+# utilizes wine and pyinstaller-build.sh to produce
+# build/executables/pyinstaller/bitmask/*
+# usage: docker-compose run --rm pyinstaller
+# non-zero exit code on failure
+pyinstaller:
+ build: pyinstaller
+ volumes:
+# bitmask sources
+ - ../../:/var/src/bitmask:ro
+# produced binaries
+ - ../../build:/var/build
+# service to build a windows-executable from openvpn sources
+# uses the openvpn-build infrastructure to produce
+# build/executables/openvpn/*
+# produces the openvpn.exe and provides openssl that is to be
+# used by pyinsaller
+# usage: docker-compose run --rm openvpn
+# non-zero exit code on failure
+openvpn:
+ build: openvpn
+ volumes:
+# bitmask sources
+ - ../../:/var/src/bitmask:ro
+# produced binaries
+ - ../../build:/var/build
+# service to compile a installer using nullsoft installer
+# nsis environment to build installer (exe) that contains all required binaries
+# for a clean, just installed windows machine
+# utilizes the debian makensis and installer-build to produce
+# dist/bitmask-VERSION.exe
+# usage: docker-compose run --rm installer
+# non-zero exit code on failure
+installer:
+ build: installer
+ volumes:
+# bitmask sources
+ - ../../:/var/src/bitmask:ro
+# produced installers - configured in bitmask.nsh
+ - ../../dist:/var/dist
diff --git a/pkg/windows/installer-build.sh b/pkg/windows/installer-build.sh
new file mode 100755
index 00000000..cf664200
--- /dev/null
+++ b/pkg/windows/installer-build.sh
@@ -0,0 +1,119 @@
+#!/bin/bash
+
+# build installer
+# ===============
+#
+# builds several installers from previously compiled binaries
+
+product=bitmask
+# the location of the nsis installer nis files dictates the path of the files
+relative_executable_path=../../build/executables
+source_ro_path=/var/src/${product}
+temporary_build_path=/var/tmp/installer
+
+setups=($(ls -1 ${source_ro_path}/pkg/windows | grep '.nis$' | sed 's|.nis$||'))
+
+# generate nsis file references for installer for single directory
+# appends File and Remove to files that are later included by makensis
+# separate files for install and uninstall statements
+#
+# directory_root: the tree root that is currently generated
+# subdir: any directory in the tree
+# setup_name: the name of the setup this nsh entries are generated for
+function generateDirectoryNSISStatements() {
+ directory_root=$1
+ subdir=$2
+ setup_name=$3
+ find ${subdir} -maxdepth 1 -type f -exec echo 'File "'${relative_executable_path}'/{}"' \;>> ${setup_name}_install_files.nsh
+ find ${subdir} -maxdepth 1 -type f -exec echo 'Delete "$INSTDIR/{}"' \; >> ${setup_name}_uninstall_files.nsh
+}
+# generate a tree of files into nsis installer definitions
+# directory_root: the tree root that is currently generated
+# setup_name: the name of the setup this nsh entries are generated for
+function generateDirectoryNSISStatementsTree() {
+ directory_root=$1
+ setup_name=$2
+ subdirs=$(find ${directory_root} -type d | sort)
+ for subdir in ${subdirs[@]}
+ do
+ if [ "${directory_root}" != "${subdir}" ]; then
+ echo 'SetOutPath "$INSTDIR/'${subdir}'"' >> ${setup_name}_install_files.nsh
+ fi
+ generateDirectoryNSISStatements ${directory_root} ${subdir} ${setup_name}
+ done
+ # again to remove emptied directories on uninstall so reverse
+ subdirs=$(find ${directory_root} -type d | sort | tac)
+ for subdir in ${subdirs[@]}
+ do
+ if [ "${directory_root}" != "${subdir}" ]; then
+ echo 'RMDir "$INSTDIR/'${subdir}'"' >> ${setup_name}_uninstall_files.nsh
+ fi
+ done
+}
+# generate installer files for the available setups
+# those files include install and uninstall statements and are
+# modified (backslashes/source_path) to generate a sane target
+# structure
+function generateNSISStatements() {
+ pushd ${temporary_build_path}/build/executables
+ for setup in "${setups[@]}"
+ do
+ echo "setup:" ${setup}
+ echo "# auto generated by pkg/windows/installer-build.sh please do not modify" > ${setup}_install_files.nsh
+ echo "# auto generated by pkg/windows/installer-build.sh please do not modify" > ${setup}_uninstall_files.nsh
+ setup_source_path=${setup}
+ generateDirectoryNSISStatementsTree ${setup_source_path} ${setup}
+ # remove the setup_source_path from the nsh files
+ sed -i "s|INSTDIR/${setup_source_path}/|INSTDIR/|" ${setup}_install_files.nsh
+ sed -i "s|/${setup_source_path}/|/|" ${setup}_uninstall_files.nsh
+ # make backslashes
+ sed -i "s|/|\\\\|g" ${setup}_install_files.nsh ${setup}_uninstall_files.nsh
+ # make install size
+ installed_size=$(du -s --block-size=1000 ${setup} | awk '{print $1}')
+ echo "!define INSTALLSIZE ${installed_size}" > ${setup}_install_files_size.nsh
+ done
+ popd
+}
+# makensis to produce a installer.exe
+# the result is placed in /var/dist
+function buildInstaller() {
+ pushd ${temporary_build_path}/pkg/windows
+ for setup in ${setups[@]}
+ do
+ makensis ${setup}.nis || die 'build setup "'${setup}'" failed'
+ done
+ popd
+}
+# prepare build path
+# copies files that have been produced by other containers
+# merges the product so the nsis files are correct
+function prepareBuildPath() {
+ mkdir -p ${temporary_build_path}/pkg/windows
+ mkdir -p ${temporary_build_path}/build
+ cp -r ${source_ro_path}/pkg/windows/* ${temporary_build_path}/pkg/windows
+ cp -r ${source_ro_path}/build/* ${temporary_build_path}/build
+ cp -r ${source_ro_path}/LICENSE ${temporary_build_path}/LICENSE
+
+ test -d ${temporary_build_path}/build/executables/bitmask || die 'bitmask not available run docker-compose run --rm pyinstaller'
+ test -d ${temporary_build_path}/build/executables/openvpn || die 'openvpn not available run docker-compose run --rm openvpn'
+ pushd ${temporary_build_path}/build/executables
+ cp openvpn/bin/openvpn.exe bitmask
+ cp openvpn/bin/*.dll bitmask
+ popd
+}
+# remove build files to ensure subsequent builds
+function cleanup() {
+ rm -r ${temporary_build_path}
+}
+# display failure message and emit non-zero exit code
+function die() {
+ echo "die:" $@
+ exit 1
+}
+function main() {
+ prepareBuildPath
+ generateNSISStatements
+ buildInstaller
+ cleanup
+}
+main $@ \ No newline at end of file
diff --git a/pkg/windows/installer/Dockerfile b/pkg/windows/installer/Dockerfile
new file mode 100644
index 00000000..ae46acb6
--- /dev/null
+++ b/pkg/windows/installer/Dockerfile
@@ -0,0 +1,17 @@
+FROM debian:jessie
+MAINTAINER paixu@0xn0.de
+RUN apt-get update
+
+######
+# install packages required to build
+
+RUN apt-get -y install \
+ nsis
+WORKDIR /var/src/bitmask/pkg/windows
+
+######
+# set a specific user
+# needs external tuning of the /var/dist rights!
+# RUN useradd installer
+# USER installer
+ENTRYPOINT ["/var/src/bitmask/pkg/windows/installer-build.sh"] \ No newline at end of file
diff --git a/pkg/windows/openvpn-build.sh b/pkg/windows/openvpn-build.sh
new file mode 100755
index 00000000..7f8ed84f
--- /dev/null
+++ b/pkg/windows/openvpn-build.sh
@@ -0,0 +1,63 @@
+#!/bin/bash
+
+# render openvpn prepared for installer
+# ================================================
+#
+# requires
+# - a linux host with mingw installed
+# - a rw directory mounted to /var/build
+# returns nonzero exit code when failed
+#
+# clone openvpn-build repository
+# runs cross-compile build
+# - downloads openvpn dependencies
+# - compiles
+# copy files to executables so they can be installed
+# cleans up (remove read-write copy)
+
+# the location where the openvpn binaries are placed
+absolute_executable_path=/var/build/executables
+temporary_build_path=/var/build/openvpn
+
+# cleanup the temporary build path for subsequent executes
+function cleanup() {
+ rm -r ${temporary_build_path} 2>/dev/null
+}
+# build openvpn source
+function buildSource() {
+ pushd ${temporary_build_path}/openvpn-build/generic
+ CHOST=i686-w64-mingw32 \
+ CBUILD=i686-pc-linux-gnu \
+ ./build \
+ || die 'build openvpn from source failed'
+ mkdir -p ${absolute_executable_path}
+ cp -r image/openvpn ${absolute_executable_path}/openvpn
+ popd
+}
+# fetch tap-windows.exe as defined in the openvpn vars
+function fetchTapWindows() {
+ pushd ${temporary_build_path}/openvpn-build
+ source windows-nsis/build-complete.vars
+ wget ${TAP_WINDOWS_INSTALLER_URL} -O ${absolute_executable_path}/openvpn/tap-windows.exe || die 'tap-windows.exe could not be fetched'
+ popd
+}
+# prepare read-write copy
+function prepareBuildPath() {
+ cleanup
+ mkdir -p ${temporary_build_path}
+ pushd ${temporary_build_path}
+ git clone https://github.com/OpenVPN/openvpn-build || die 'openvpn-build could not be cloned'
+ popd
+}
+# display failure message and emit non-zero exit code
+function die() {
+ echo "die:" $@
+ exit 1
+}
+function main() {
+ prepareBuildPath
+ buildSource
+ fetchTapWindows
+ cleanup
+}
+main $@
diff --git a/pkg/windows/openvpn/Dockerfile b/pkg/windows/openvpn/Dockerfile
new file mode 100644
index 00000000..471685a1
--- /dev/null
+++ b/pkg/windows/openvpn/Dockerfile
@@ -0,0 +1,17 @@
+FROM debian:jessie
+MAINTAINER paixu@0xn0.de
+
+######
+# install packages required to build
+# https-transport: winehq deb
+# winbind: pip install keyring (requirements.pip) needs this somehow
+# git-core: clone rw copy of repo and build specific commit
+# imagemagick: convert png to ico-files
+RUN apt-get update && apt-get -y install \
+ unzip bzip2 \
+ curl wget \
+ apt-transport-https \
+ man2html \
+ git-core \
+ build-essential autoconf mingw-w64
+ENTRYPOINT ["/var/src/bitmask/pkg/windows/openvpn-build.sh"] \ No newline at end of file
diff --git a/pkg/windows/pyinstaller-build.sh b/pkg/windows/pyinstaller-build.sh
new file mode 100755
index 00000000..522fc10f
--- /dev/null
+++ b/pkg/windows/pyinstaller-build.sh
@@ -0,0 +1,288 @@
+#!/bin/bash
+
+# render dependencies into separate subdirectories
+# ================================================
+#
+# requires
+# - a linux host with wine, wine with python and mingw installed
+# - the sourcecode mounted to /var/src/
+# - a rw directory mounted to /var/build
+# returns nonzero exit code when pyinstaller failed
+#
+# prepares a read-write copy of the sourcecode
+# executes qt-uic and qt-rcc for gui dialogs
+# installs dependencies from pkg/dependencies-windows.pip
+# runs pyinstaller
+# cleans up (remove wine-dlls, remove read-write copy)
+# creates nsis install/uninstall scripts for the files for each package
+# if $1 is set it is expected to be a branch/git-tag
+
+product=bitmask
+# the location where the pyinstaller results are placed
+absolute_executable_path=/var/build/executables
+# the location of the nsis installer nis files dictates the path of the files
+relative_executable_path=../../build/executables
+source_ro_path=/var/src/${product}
+temporary_build_path=/var/build/pyinstaller
+git_tag=HEAD
+version_prefix=leap.bitmask
+git_version=unknown
+# option that is changed when a dependency-cache is found
+install_dependencies=true
+# default options for components
+with_eip=false
+with_mail=true
+
+setups=($(ls -1 ${source_ro_path}/pkg/windows | grep '.nis$' | sed 's|.nis$||'))
+# add mingw dlls that are build in other steps
+function addMingwDlls() {
+ root=$1
+ cp /usr/lib/gcc/i686-w64-mingw32/4.9-win32/libgcc_s_sjlj-1.dll ${root}
+ cp /root/.wine/drive_c/Python27/Lib/site-packages/zmq/libzmq.pyd ${root}
+ cp /root/.wine/drive_c/Python27/Lib/site-packages/zmq/libzmq.pyd ${root}
+ mkdir -p ${root}/pysqlcipher
+ cp /var/build/pyinstaller/pkg/pyinst/build/bitmask/pysqlcipher-2.6.4-py2.7-win32.egg/pysqlcipher/_sqlite.pyd ${root}/pysqlcipher
+ cp ~/.wine/drive_c/openssl/bin/*.dll ${root}
+}
+# cleanup the temporary build path for subsequent executes
+function cleanup() {
+ rm -rf ${temporary_build_path} 2>/dev/null
+}
+# create files that are not part of the repository but are needed
+# in the windows environment:
+# - license with \r\n
+# - ico from png (multiple sizes for best results on high-res displays)
+function createInstallablesDependencies() {
+ pushd ${temporary_build_path} > /dev/null
+ cat LICENSE | sed 's|\n|\r\n|g' > LICENSE.txt
+ convert data/images/mask-icon.png -filter Cubic -scale 256x256! data/images/mask-icon-256.png
+ convert data/images/mask-icon-256.png -define icon:auto-resize data/images/mask-icon.ico
+ # execute qt-uic / qt-rcc
+ wine mingw32-make all || die 'qt-uic / qt-rcc failed'
+ # get version using git (only available in host)
+ git_version=$(python setup.py version| grep 'Version is currently' | awk -F': ' '{print $2}')
+ # run setup.py in a path with the version contained so versioneer can
+ # find the information and put it into the egg
+ versioned_build_path=/var/tmp/${version_prefix}-${git_version}
+ mkdir -p ${versioned_build_path}
+ cp -r ${temporary_build_path}/* ${versioned_build_path}
+ # apply patches to the source that are required for working code
+ # should not be required in the future as it introduces possible
+ # hacks that are hard to debug
+ applyPatches ${versioned_build_path}
+ pushd ${versioned_build_path} > /dev/null
+ # XXX what's this update_files command?
+ #wine python setup.py update_files || die 'setup.py update_files failed'
+ wine python setup.py build || die 'setup.py build failed'
+ wine python setup.py install || die 'setup.py install failed'
+ popd
+ rm -rf ${versioned_build_path}
+ popd
+}
+# create installer version that may be used by installer-build.sh / makensis
+# greps the version-parts from the previously extracted git_version and stores
+# the result in a setup_version.nsh
+# when the git_version does provide a suffix it is prefixed with a dash so the
+# installer output needs no conditional for this
+function createInstallerVersion() {
+ setup=$1
+ # [0-9]*.[0-9]*.[0-9]*-[0-9]*_g[0-9a-f]*_dirty
+ VERSIONMAJOR=$(echo ${git_version} | sed 's|^\([0-9]*\)\..*$|\1|')
+ VERSIONMINOR=$(echo ${git_version} | sed 's|^[0-9]*\.\([0-9]*\).*$|\1|')
+ VERSIONBUILD=$(echo ${git_version} | sed 's|^[0-9]*\.[0-9]*\.\([0-9]*\).*$|\1|')
+ VERSIONSUFFIX=$(echo ${git_version} | sed 's|^[0-9]*\.[0-9]*\.[0-9]*-\(.*\)$|\1|')
+ echo "!define VERSIONMAJOR ${VERSIONMAJOR}" > ${absolute_executable_path}/${setup}_version.nsh
+ echo "!define VERSIONMINOR ${VERSIONMINOR}" >> ${absolute_executable_path}/${setup}_version.nsh
+ echo "!define VERSIONBUILD ${VERSIONBUILD}" >> ${absolute_executable_path}/${setup}_version.nsh
+ if [ ${VERSIONSUFFIX} != "" ]; then
+ VERSIONSUFFIX="-${VERSIONSUFFIX}"
+ fi
+ echo "!define VERSIONSUFFIX ${VERSIONSUFFIX}" >> ${absolute_executable_path}/${setup}_version.nsh
+}
+# create installable binaries with dlls
+function createInstallables() {
+ mkdir -p ${absolute_executable_path}
+ pushd ${temporary_build_path}/pkg/pyinst
+ # build install directories (contains multiple files with pyd,dll, some of
+ # them look like windows WS_32.dll but are from wine)
+ for setup in ${setups[@]}
+ do
+ # --clean do not cache anything and overwrite everything --noconfirm
+ # --distpath to place on correct location
+ # --debug to see what may be wrong with the result
+ # --paths=c:\python\lib\site-packages;c:\python27\lib\site-packages
+ wine pyinstaller \
+ --clean \
+ --noconfirm \
+ --distpath=.\\installables \
+ --paths=Z:\\var\\build\\pyinstaller\\src\\ \
+ --paths=C:\\Python27\\Lib\\site-packages\\ \
+ --debug \
+ ${setup}.spec \
+ || die 'pyinstaller for "'${setup}'" failed'
+ removeWineDlls installables/${setup}
+ addMingwDlls installables/${setup}
+ rm -r ${absolute_executable_path}/${setup}
+ cp -r installables/${setup} ${absolute_executable_path}
+ cp ${absolute_executable_path}/cacert.pem ${absolute_executable_path}/${setup}
+ rm -r installables
+ createInstallerVersion ${setup}
+ done
+ popd
+ pushd ${temporary_build_path}
+ cp data/images/mask-icon.ico ${absolute_executable_path}/
+ popd
+}
+# install (windows)dependencies of project
+function installProjectDependencies() {
+ pushd ${temporary_build_path} > /dev/null
+ unsupported_packages="dirspec"
+ pip_flags="--find-links=Z:${temporary_build_path}/wheels"
+ for unsupported_package in ${unsupported_packages}
+ do
+ pip_flags="${pip_flags} --allow-external ${unsupported_package} --allow-unverified ${unsupported_package}"
+ done
+ pip_flags="${pip_flags} -r"
+
+ # install dependencies
+ mkdir -p ${temporary_build_path}/wheels
+ wine pip install ${pip_flags} pkg/requirements-leap.pip || die 'requirements-leap.pip could not be installed'
+ # fix requirements
+ # python-daemon breaks windows build
+ sed -i 's|^python-daemon|#python-daemon|' pkg/requirements.pip
+ wine pip install ${pip_flags} pkg/requirements.pip || die 'requirements.pip could not be installed'
+ git checkout pkg/requirements.pip
+ popd
+ cp -r /root/.wine/drive_c/Python27/Lib/site-packages ${absolute_executable_path}
+ curl https://curl.haxx.se/ca/cacert.pem > ${absolute_executable_path}/cacert.pem || die 'cacert.pem could not be fetched - would result in bad ssl in installer'
+}
+# workaround for broken dependencies
+# runs before pip install requirements
+# fixes failure for pysqlcipher as this requests a https file that the
+# windows-python fails to request
+function installProjectDependenciesBroken() {
+ pushd ${temporary_build_path} > /dev/null
+ curl https://pypi.python.org/packages/source/p/pysqlcipher/pysqlcipher-2.6.4.tar.gz \
+ > pysqlcipher-2.6.4.tar.gz \
+ || die 'fetch pysqlcipher failed'
+ tar xzf pysqlcipher-2.6.4.tar.gz
+ pushd pysqlcipher-2.6.4
+ curl https://downloads.leap.se/libs/pysqlcipher/amalgamation-sqlcipher-2.1.0.zip \
+ > amalgamation-sqlcipher-2.1.0.zip \
+ || die 'fetch amalgamation for pysqlcipher failed'
+ unzip -o amalgamation-sqlcipher-2.1.0.zip || die 'unzip amalgamation failed'
+ mv sqlcipher amalgamation
+ patch -p0 < ${source_ro_path}/pkg/windows/pyinstaller/pysqlcipher_setup.py.patch \
+ || die 'patch pysqlcipher setup.py failed'
+ wine python setup.py build install || die 'setup.py for pysqlcipher failed'
+ popd
+ popd # temporary_build_path
+}
+# prepare read-write copy
+function prepareBuildPath() {
+ cleanup
+ # ensure shared openssl for all pip builds
+ test -d ${absolute_executable_path}/openvpn || die 'openvpn not available run docker-compose run --rm openvpn'
+ cp -r ${absolute_executable_path}/openvpn /root/.wine/drive_c/openssl
+ if [ -d ${absolute_executable_path}/site-packages ]; then
+ # use pip install cache for slow connections
+ rm -r /root/.wine/drive_c/Python27/Lib/site-packages
+ cp -r ${absolute_executable_path}/site-packages /root/.wine/drive_c/Python27/Lib/
+ install_dependencies=false
+ fi
+ if [ ! -z $1 ]; then
+ git_tag=$1
+ fi
+ if [ ${git_tag} != "HEAD" ]; then
+ echo "using ${git_tag} as source for the project"
+ git clone ${source_ro_path} ${temporary_build_path}
+ pushd ${temporary_build_path}
+ git checkout ${git_tag} || die 'checkout "'${git_tag}'" failed'
+ popd
+ else
+ echo "using current source tree for build"
+ mkdir -p ${temporary_build_path}/data
+ mkdir -p ${temporary_build_path}/docs
+ mkdir -p ${temporary_build_path}/pkg
+ mkdir -p ${temporary_build_path}/src
+ mkdir -p ${temporary_build_path}/.git
+ cp -r ${source_ro_path}/data/* ${temporary_build_path}/data
+ cp -r ${source_ro_path}/data/* ${temporary_build_path}/docs
+ cp -r ${source_ro_path}/pkg/* ${temporary_build_path}/pkg
+ cp -r ${source_ro_path}/src/* ${temporary_build_path}/src
+ cp -r ${source_ro_path}/.git/* ${temporary_build_path}/.git
+ cp ${source_ro_path}/* ${temporary_build_path}/
+ fi
+}
+# add patches to the sourcetree
+# this function should do nothing some day and should be run after
+# the version has been evaluated
+function applyPatches() {
+ root_path=$1
+ # disable eip
+ if [ !${with_eip} ]; then
+ sed -i "s|HAS_EIP = True|HAS_EIP = False|" ${root_path}/src/leap/bitmask/_components.py
+ fi
+ # disable mail
+ if [ !${with_mail} ]; then
+ sed -i "s|HAS_MAIL = True|HAS_MAIL = False|" ${root_path}/src/leap/bitmask/_components.py
+ fi
+ # hack the logger
+ sed -i "s|'bitmask.log'|str(random.random()) + '_bitmask.log'|;s|import sys|import sys\nimport random|" ${root_path}/src/leap/bitmask/logs/utils.py
+ sed -i "s|perform_rollover=True|perform_rollover=False|" ${root_path}/src/leap/bitmask/app.py
+ # fix requirements
+ # python-daemon breaks windows build
+ sed -i 's|^python-daemon|#python-daemon|' ${root_path}/pkg/requirements.pip
+}
+# remove wine dlls that should not be in the installer
+# root: path that should be cleaned from dlls
+function removeWineDlls() {
+ root=$1
+ declare -a wine_dlls=(\
+ advapi32.dll \
+ comctl32.dll \
+ comdlg32.dll \
+ gdi32.dll \
+ imm32.dll \
+ iphlpapi.dll \
+ ktmw32.dll \
+ msvcp90.dll \
+ msvcrt.dll \
+ mswsock.dll \
+ mpr.dll \
+ netapi32.dll \
+ ole32.dll \
+ oleaut32.dll \
+ opengl32.dll \
+ psapi.dll \
+ rpcrt4.dll \
+ shell32.dll \
+ user32.dll \
+ version.dll \
+ winmm.dll \
+ winspool.drv \
+ ws2_32.dll \
+ wtsapi32.dll \
+ )
+ for wine_dll in "${wine_dlls[@]}"
+ do
+ # not all of the listed dlls are in all directories
+ rm ${root}/${wine_dll} 2>/dev/null
+ done
+}
+# display failure message and emit non-zero exit code
+function die() {
+ echo "die:" $@
+ exit 1
+}
+function main() {
+ prepareBuildPath $@
+ if [ ${install_dependencies} == true ]; then
+ installProjectDependenciesBroken
+ installProjectDependencies
+ fi
+ createInstallablesDependencies
+ createInstallables
+ cleanup
+}
+main $@
diff --git a/pkg/windows/pyinstaller/Dockerfile b/pkg/windows/pyinstaller/Dockerfile
new file mode 100644
index 00000000..2da0da3a
--- /dev/null
+++ b/pkg/windows/pyinstaller/Dockerfile
@@ -0,0 +1,105 @@
+FROM debian:jessie
+MAINTAINER paixu@0xn0.de
+
+ENV PYTHON_VERSION=2.7.11
+ENV OPENSSL_VERSION=1.0.2f
+ENV ZLIB_VERSION=1.2.8
+ENV MINGW_VERSION=0.6.2-beta-20131004-1
+ENV MINGW_BIN_VERSION=0.6.2-mingw32-beta-20131004-1-bin
+ENV WINEDEBUG=fixme-all
+
+######
+# install packages required to build
+# https-transport: winehq deb
+# winbind: pip install keyring (requirements.pip) needs this somehow
+# git-core: clone rw copy of repo and build specific commit
+# imagemagick: convert png to ico-files
+RUN apt-get update && apt-get -y install \
+ unzip curl apt-transport-https \
+ winbind \
+ build-essential autoconf bison gperf flex libtool mingw-w64 \
+ git-core \
+ imagemagick \
+ pkg-config
+
+# install wine > 1.6.2 (debian:jessie version fails with pip)
+RUN dpkg --add-architecture i386 \
+ && curl https://dl.winehq.org/wine-builds/Release.key | apt-key add - \
+ && echo 'deb https://dl.winehq.org/wine-builds/debian/ jessie main' >> /etc/apt/sources.list.d/wine.list \
+ && apt-get update
+
+RUN curl https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}.msi > /tmp/python-${PYTHON_VERSION}.msi
+RUN curl -L http://sourceforge.net/projects/mingw/files/Installer/mingw-get/mingw-get-${MINGW_VERSION}/mingw-get-${MINGW_BIN_VERSION}.zip/download > /tmp/mingw-get.zip
+
+# alternative with messy python afterwards
+# RUN curl -L http://download.microsoft.com/download/7/9/6/796EF2E4-801B-4FC4-AB28-B59FBF6D907B/VCForPython27.msi > /tmp/msvcforpython27.msi
+
+RUN curl -L http://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz > /tmp/openssl-${OPENSSL_VERSION}.tar.gz
+RUN apt-get install -y winehq-staging
+
+RUN curl -L http://sourceforge.net/projects/mingw/files/Installer/mingw-get/mingw-get-${MINGW_VERSION}/mingw-get-${MINGW_BIN_VERSION}.zip/download > /tmp/mingw-get.zip
+RUN mkdir -p /root/.wine/drive_c/mingw \
+ && unzip -d /root/.wine/drive_c/mingw /tmp/mingw-get.zip
+
+#######
+# Build python dependency
+# using the 'host' (linux) xcompiler instead of fiddeling in wine
+# zlib - needs a update every 5 years
+# adds a patch that makes a shared lib - default is static
+RUN curl -L http://zlib.net/zlib-${ZLIB_VERSION}.tar.gz > /tmp/zlib-${ZLIB_VERSION}.tar.gz
+ADD zlib-mingw-shared.patch /zlib-mingw-shared.patch
+RUN mkdir -p /root/.wine/drive_c/zlib/src \
+ && mv /tmp/zlib-${ZLIB_VERSION}.tar.gz /root/.wine/drive_c/zlib/src \
+ && cd /root/.wine/drive_c/zlib/src \
+ && tar xzf zlib-${ZLIB_VERSION}.tar.gz \
+ && cd zlib-${ZLIB_VERSION} \
+ && patch -p0 < /zlib-mingw-shared.patch \
+ && make -f win32/Makefile.gcc PREFIX=/usr/bin/i686-w64-mingw32- \
+ && make -f win32/Makefile.gcc INCLUDE_PATH=/root/.wine/drive_c/zlib/include LIBRARY_PATH=/root/.wine/drive_c/zlib/lib BINARY_PATH=/root/.wine/drive_c/zlib/bin install
+
+######
+# install gcc for most pip builds
+# install g++ for pycryptopp
+# this is mingw in wine, not to get confused with mingw-w64 in container-host
+RUN wine msiexec -i /tmp/python-${PYTHON_VERSION}.msi -q \
+ && wine c:/mingw/bin/mingw-get.exe install gcc g++ mingw32-make \
+ && rm -r /tmp/.wine-0
+
+####
+# pip configuration
+# set wine mingw compiler to be used by "python setup build"
+# set default include dirs, libraries and library paths
+# the libraries=crypto is added because srp will only link using -lssl but links to BN_* (libcrypto) code
+# 'install' zlib to mingw so python may find its dlls
+# pyside-rcc fix (https://srinikom.github.io/pyside-bz-archive/670.html)
+RUN printf "[build]\ncompiler=mingw32\n\n[build_ext]\ninclude_dirs=c:\\openssl\\include;c:\\zlib\\include\nlibraries=crypto\nlibrary_dirs=c:\\openssl\\lib;c:\\openssl\\bin;c:\\zlib\\lib;c:\\zlib\\bin" > /root/.wine/drive_c/Python27/Lib/distutils/distutils.cfg \
+ && printf 'REGEDIT4\n\n[HKEY_CURRENT_USER\\Environment]\n"PATH"="C:\\\\python27;C:\\\\python27\\\\Scripts;C:\\\\python27\\\\Lib\\\\site-packages\\\\PySide;C:\\\\mingw\\\\bin;c:\\\\windows;c:\\\\windows\\\\system"' > /root/.wine/drive_c/path.reg \
+ && printf 'REGEDIT4\n\n[HKEY_CURRENT_USER\\Environment]\n"OPENSSL_CONF"="C:\\\\openssl"' > /root/.wine/drive_c/openssl_conf.reg \
+ && printf 'REGEDIT4\n\n[HKEY_CURRENT_USER\\Environment]\n"PYTHONPATH"="C:\\\\python27\\\\lib\\\\site-packages;Z:\\\\var\\\\build\\\\bitmask_rw\\\\src"' > /root/.wine/drive_c/pythonpath.reg \
+ && cp /root/.wine/drive_c/zlib/bin/zlib1.dll /root/.wine/drive_c/mingw/bin \
+ && cp /root/.wine/drive_c/zlib/lib/libz.dll.a /root/.wine/drive_c/mingw/lib
+
+####
+# prepare the environment with all python dependencies installed
+# inject dirspec from distribution
+#
+RUN apt-get install -y python-dirspec \
+ && cp -r /usr/lib/python2.7/dist-packages/dirspec* /root/.wine/drive_c/Python27/Lib/site-packages/
+RUN apt-get install -y python-setuptools
+RUN wine regedit /root/.wine/drive_c/path.reg \
+ && wine regedit /root/.wine/drive_c/openssl_conf.reg \
+ && wine regedit /root/.wine/drive_c/pythonpath.reg \
+ && wine pip install virtualenv pyinstaller \
+ && wine pip install wheel \
+ && wine pip install -U setuptools-scm \
+ && wine pip install -U setuptools_scm \
+ && wine pip install -U pyside python-qt \
+ && wine pip install -I psutil==3.4.2 \
+ && rm -r /tmp/.wine-0
+
+# alternative msvc: after python is installed (or before?)
+# && wine msiexec -i /tmp/msvcforpython27.msi -q \
+
+RUN apt-get -y install \
+ mc
+ENTRYPOINT ["/var/src/bitmask/pkg/windows/pyinstaller-build.sh"] \ No newline at end of file
diff --git a/pkg/windows/pyinstaller/pysqlcipher_setup.py.patch b/pkg/windows/pyinstaller/pysqlcipher_setup.py.patch
new file mode 100644
index 00000000..dcec54fa
--- /dev/null
+++ b/pkg/windows/pyinstaller/pysqlcipher_setup.py.patch
@@ -0,0 +1,14 @@
+--- setup.py.org 2014-11-12 16:38:07.000000000 +0000
++++ setup.py 2016-01-23 14:08:13.255261595 +0000
+@@ -192,10 +192,7 @@
+ ext.define_macros.append(("inline", "__inline"))
+
+ # Configure the linker
+- ext.extra_link_args.append("libeay32.lib")
+- ext.extra_link_args.append(
+- "/LIBPATH:" + os.path.join(openssl, "lib")
+- )
++ ext.extra_link_args.append("-lcrypto")
+ else:
+ ext.extra_link_args.append("-lcrypto")
+
diff --git a/pkg/windows/pyinstaller/zlib-mingw-shared.patch b/pkg/windows/pyinstaller/zlib-mingw-shared.patch
new file mode 100644
index 00000000..1b980cb8
--- /dev/null
+++ b/pkg/windows/pyinstaller/zlib-mingw-shared.patch
@@ -0,0 +1,10 @@
+diff -Naur ../zlib-1.2.8-org/win32/Makefile.gcc ./win32/Makefile.gcc
+--- ../zlib-1.2.8-org/win32/Makefile.gcc 2008-10-23 17:44:36.000000000 +0000
++++ ./win32/Makefile.gcc 2015-12-06 19:20:00.449471787 +0000
+@@ -37,6 +37,6 @@
+ # Set to 1 if shared object needs to be installed
+ #
+-SHARED_MODE=0
++SHARED_MODE=1
+
+ #LOC = -DASMV \ No newline at end of file
diff --git a/release-notes.rst b/release-notes.rst
index 7a63901e..2fc1f9ea 100644
--- a/release-notes.rst
+++ b/release-notes.rst
@@ -1,25 +1,38 @@
-0.9.1 November 03 - "the day of the calaca"
-+++++++++++++++++++++++++++++++++++++++++++
+0.9.2 April XY - "Panis et Circenses"
++++++++++++++++++++++++++++++++++++++
-We were very pleased to announce Bitmask stable 0.9.1 :tada:.
+We are pleased to announce the Bitmask 0.9.2 release. You can refer to `the
+changelog`_ for the whole changeset.
-This is a minor release and you can see changes on `the changelog`_.
+In addition to the Encrypted Internet Proxy, the Encrypted Email service is now
+available, in Beta state in the Linux bundles. An alpha version for OSX is also
+ready. This service can be tested against any LEAP provider that has made it
+publicly available.
-The complete list of things that have changed since 0.8.x series can be seen on
-`0.9.0 release notes`_
-Don't miss the big pile of changes we brought you on 0.9.0 :)
+The "Beta" in the Email service means that things are getting more stable, but
+unforeseen issues are still expected to be found, so please use it accordingly.
+We need you to test it as hard as you can, and report any bugs that you find,
+but don't trust it yet in situations where data loss, delivery problems or any
+other errors can put you at risk or otherwise cause major trouble.
-Using the latest Bitmask, Linux users will be able to use our encrypted email
-service, now in beta state! A Mac release is imminent and a windows release is
-underway.
+Currently we maintain a demo provider for the Mail service at
+https://mail.bitmask.net. This provider is already pinned in Bitmask for easy
+access when creating a new account from within the wizard.
-Currently we have a test provider for mail @ https://mail.bitmask.net This
-provider is already bundled with Bitmask for easy access on the wizard. Please
-help us test this and file bug reports here:
-https://leap.se/code/projects/report-issues
+In the standalone bundles beginning with 0.9.2, we are shipping the Pixelated
+Webmail interface. Access via Thunderbird or any other mail client of your
+choice is still supported, but we hope that this modern web interface will make
+the delights of our users.
-NOTE: beta means that we expect things not to break but we don't promise you
-won't get any headaches or lose some email, so please be careful.
+The bundles for 0.9.2 include statically compiled binaries for OpenVPN 2.3.10
+(compiled against PolarSSL 1.3.9) and GnuPG 1.4.20.
-.. _`the changelog`: https://github.com/leapcode/bitmask_client/blob/0.9.1/CHANGELOG.rst
-.. _`0.9.0 release notes`: https://github.com/leapcode/bitmask_client/blob/0.9.0/release-notes.rst
+Please help us test Bitmask and file any bug reports here:
+https://leap.se/code/projects/report-issues, that will be a great contribution
+towards future improvement!
+
+Until the next release, see you on the intertubes, and stay safe.
+
+The Bitmask team.
+
+.. _`the changelog`: https://github.com/leapcode/bitmask_client/blob/0.9.2/CHANGELOG.rst
diff --git a/run_tests.sh b/run_tests.sh
deleted file mode 100755
index 0d7e7463..00000000
--- a/run_tests.sh
+++ /dev/null
@@ -1,172 +0,0 @@
-#!/bin/bash
-
-set -e
-
-function usage {
- echo "Usage: $0 [OPTION]...[@virtualenv-name]"
- echo "Run leap-client test suite"
- echo ""
- echo " -V, --virtual-env Always use virtualenv. Install automatically if not present"
- echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment"
- echo " -s, --no-site-packages Isolate the virtualenv from the global Python environment"
- echo " -x, --stop Stop running tests after the first error or failure."
- echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
- echo " -p, --pep8 Just run pep8"
- echo " -P, --no-pep8 Don't run pep8"
- echo " -c, --coverage Generate coverage report"
- echo " -h, --help Print this usage message"
- echo " -A, --all Run all tests, without excluding any"
- echo " -i, --progressive Run with nose-progressive plugin"
- echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list"
- echo ""
- echo "Note: with no options specified, the script will try to run the tests in a virtual environment,"
- echo " If no virtualenv is found, the script will ask if you would like to create one. If you "
- echo " prefer to run tests NOT in a virtual environment, simply pass the -N option."
- echo " If you pass @virtualenv-name, the given virtualenv will be used as long as "
- echo " virtualenvwrapper.sh can be found in the PATH."
- exit
-}
-
-function process_option {
- case "$1" in
- -h|--help) usage;;
- -V|--virtual-env) always_venv=1; never_venv=0;;
- -N|--no-virtual-env) always_venv=0; never_venv=1;;
- -s|--no-site-packages) no_site_packages=1;;
- -f|--force) force=1;;
- -p|--pep8) just_pep8=1;;
- -P|--no-pep8) no_pep8=1;;
- -c|--coverage) coverage=1;;
- -A|--all) alltests=1;;
- -i|--progressive) progressive=1;;
- @*) venvwrapper=1; source_venv=`echo $1 | cut -c 2-`;;
- -*) noseopts="$noseopts $1";;
- *) noseargs="$noseargs $1"
- esac
-}
-
-venv=.venv
-with_venv=pkg/tools/with_venv.sh
-with_venvwrapper=pkg/tools/with_venvwrapper.sh
-always_venv=0
-never_venv=0
-force=0
-no_site_packages=0
-installvenvopts=
-noseargs=
-noseopts=
-venvwrapper=0
-source_venv=
-wrapper=""
-just_pep8=0
-no_pep8=0
-coverage=0
-alltests=0
-progressive=0
-
-for arg in "$@"; do
- process_option $arg
-done
-
-# If enabled, tell nose to collect coverage data
-if [ $coverage -eq 1 ]; then
- noseopts="$noseopts --with-coverage --cover-package=leap --cover-html --cover-html-dir=docs/covhtml/ --cover-erase"
-fi
-
-if [ $no_site_packages -eq 1 ]; then
- installvenvopts="--no-site-packages"
-fi
-
-# If alltests flag is not set, let's exclude some dirs that are troublesome.
-if [ $alltests -eq 0 ]; then
- echo "[+] Running ALL tests..."
- #noseopts="$noseopts --exclude-dir=leap/soledad"
-fi
-
-# If progressive flag enabled, run with this nice plugin :)
-if [ $progressive -eq 1 ]; then
- noseopts="$noseopts --with-progressive"
-fi
-
-function run_tests {
- echo "running tests..."
-
- if [ $venvwrapper -eq 1 ]; then
- VIRTUAL_ENV=$WORKON_HOME/$source_venv
- wrapper="$with_venvwrapper $source_venv"
-
- fi
-
- #NOSETESTS="nosetests leap --exclude=soledad* $noseopts $noseargs"
- NOSETESTS="$VIRTUAL_ENV/bin/nosetests . $noseopts $noseargs"
- #--with-coverage --cover-package=leap"
-
- # Just run the test suites in current environment
- echo "NOSETESTS=$NOSETESTS"
- ${wrapper} $NOSETESTS
- # If we get some short import error right away, print the error log directly
- RESULT=$?
- return $RESULT
-}
-
-function run_pep8 {
- echo "Running pep8 ..."
- srcfiles="src/leap"
- # Just run PEP8 in current environment
- ${wrapper} flake8 ${pep8_opts} ${srcfiles}
-}
-
-# XXX we cannot run tests that need X server
-# in the current debhelper build process,
-# so I exclude the topmost tests
-
-
-if [ $never_venv -eq 0 ]
-then
- # Remove the virtual environment if --force used
- if [ $force -eq 1 ]; then
- echo "Cleaning virtualenv..."
- rm -rf ${venv}
- fi
- if [ -e ${venv} ]; then
- wrapper="${with_venv}"
- else
- if [ $always_venv -eq 1 ]; then
- # Automatically install the virtualenv
- python pkg/install_venv.py $installvenvopts
- wrapper="${with_venv}"
- else
- echo -e "No virtual environment found...create one? (Y/n) \c"
- read use_ve
- if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then
- # Install the virtualenv and run the test suite in it
- python pkg/install_venv.py $installvenvopts
- wrapper=${with_venv}
- fi
- fi
- fi
-fi
-
-# Delete old coverage data from previous runs
-if [ $coverage -eq 1 ]; then
- ${wrapper} coverage erase
-fi
-
-if [ $just_pep8 -eq 1 ]; then
- run_pep8
- exit
-fi
-
-run_tests
-
-if [ -z "$noseargs" ]; then
- if [ $no_pep8 -eq 0 ]; then
- run_pep8
- fi
-fi
-
-if [ $coverage -eq 1 ]; then
- echo "Generating coverage report in docs/covhtml/"
- echo "now point your browser at docs/covhtml/index.html"
- exit
-fi
diff --git a/setup.cfg b/setup.cfg
index 1a05d2c3..7732c1cc 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -14,3 +14,10 @@ exclude = *_rc.py,ui_*,_version.py,docker,build,docs,pkg,versioneer.py,_binaries
[flake8]
ignore = E731
exclude = *_rc.py,ui_*,_version.py,docker,build,docs,pkg,versioneer.py,_binaries.py
+
+[versioneer]
+VCS = git
+style = pep440
+versionfile_source = src/leap/bitmask/_version.py
+versionfile_build = leap/bitmask/_version.py
+tag_prefix =
diff --git a/setup.py b/setup.py
index 8463c172..24ed98aa 100755
--- a/setup.py
+++ b/setup.py
@@ -20,32 +20,21 @@ Setup file for bitmask.
"""
from __future__ import print_function
+import glob
import hashlib
-import sys
import os
+import platform
import re
+import shutil
import sys
-if not sys.version_info[0] == 2:
- print("[ERROR] Sorry, Python 3 is not supported (yet). "
- "Try running with python2: python2 setup.py ...")
- exit()
-
-try:
- from setuptools import setup, find_packages
-except ImportError:
- from pkg import distribute_setup
- distribute_setup.use_setuptools()
- from setuptools import setup, find_packages
+from distutils.command.build import build as _build
+from setuptools import Command
+from setuptools.command.develop import develop as _develop
from pkg import utils
import versioneer
-versioneer.versionfile_source = 'src/leap/bitmask/_version.py'
-versioneer.versionfile_build = 'leap/bitmask/_version.py'
-versioneer.tag_prefix = '' # tags are like 1.2.0
-versioneer.parentdir_prefix = 'leap.bitmask-'
-
# The following import avoids the premature unloading of the `util` submodule
# when running tests, which would cause an error when nose finishes tests and
@@ -53,6 +42,20 @@ versioneer.parentdir_prefix = 'leap.bitmask-'
from multiprocessing import util
assert(util)
+
+try:
+ from setuptools import setup, find_packages
+except ImportError:
+ from pkg import distribute_setup
+ distribute_setup.use_setuptools()
+ from setuptools import setup, find_packages
+
+if not sys.version_info[0] == 2:
+ print("[ERROR] Sorry, Python 3 is not supported (yet). "
+ "Try running with python2: python2 setup.py ...")
+ exit()
+
+
setup_root = os.path.dirname(__file__)
sys.path.insert(0, os.path.join(setup_root, "src"))
@@ -78,7 +81,7 @@ DOWNLOAD_BASE = ('https://github.com/leapcode/bitmask_client/'
'archive/%s.tar.gz')
_versions = versioneer.get_versions()
VERSION = _versions['version']
-VERSION_FULL = _versions['full']
+VERSION_REVISION = _versions['full-revisionid']
DOWNLOAD_URL = ""
# get the short version for the download url
@@ -87,11 +90,6 @@ if len(_version_short) > 0:
VERSION_SHORT = _version_short[0]
DOWNLOAD_URL = DOWNLOAD_BASE % VERSION_SHORT
-cmdclass = versioneer.get_cmdclass()
-
-
-from setuptools import Command
-
class freeze_debianver(Command):
@@ -102,18 +100,19 @@ class freeze_debianver(Command):
user_options = []
template = r"""
# This file was generated by the `freeze_debianver` command in setup.py
-# Using 'versioneer.py' (0.7+) from
+# Using 'versioneer.py' (0.16) from
# revision-control system data, or from the parent directory name of an
# unpacked source archive. Distribution tarballs contain a pre-generated copy
# of this file.
version_version = '{version}'
-version_full = '{version_full}'
+full_revisionid = '{full_revisionid}'
"""
templatefun = r"""
def get_versions(default={}, verbose=False):
- return {'version': version_version, 'full': version_full}
+ return {'version': version_version,
+ 'full-revisionid': full_revisionid}
"""
def initialize_options(self):
@@ -130,8 +129,9 @@ def get_versions(default={}, verbose=False):
return
subst_template = self.template.format(
version=VERSION_SHORT,
- version_full=VERSION_FULL) + self.templatefun
- with open(versioneer.versionfile_source, 'w') as f:
+ full_revisionid=VERSION_REVISION) + self.templatefun
+ versioneer_cfg = versioneer.get_config_from_root('.')
+ with open(versioneer_cfg.versionfile_source, 'w') as f:
f.write(subst_template)
@@ -142,16 +142,16 @@ def freeze_pkg_ver(path, version_short, version_full):
"""
subst_template = freeze_debianver.template.format(
version=version_short,
- version_full=version_full) + freeze_debianver.templatefun
+ full_revisionid=version_full) + freeze_debianver.templatefun
with open(path, 'w') as f:
f.write(subst_template)
-
if sys.argv[:1] == '--sumo':
IS_SUMO = True
else:
IS_SUMO = False
+cmdclass = versioneer.get_cmdclass()
cmdclass["freeze_debianver"] = freeze_debianver
parsed_reqs = utils.parse_requirements()
@@ -170,11 +170,6 @@ else:
reqfiles=["pkg/requirements-leap.pip"])
-leap_launcher = 'bitmask=leap.bitmask.app:start_app'
-
-from setuptools.command.develop import develop as _develop
-
-
def copy_reqs(path, withsrc=False):
# add a copy of the processed requirements to the package
_reqpath = ('leap', 'bitmask', 'util', 'reqs.txt')
@@ -220,6 +215,7 @@ class cmd_binary_hash(Command):
pass
def run(self, *args):
+ # TODO check gnupg binary too.
OPENVPN_BIN = os.environ.get('OPENVPN_BIN', None)
BITMASK_ROOT = os.environ.get('BITMASK_ROOT', None)
@@ -262,16 +258,15 @@ BITMASK_ROOT = "{bitmask}"
cmdclass["hash_binaries"] = cmd_binary_hash
-# next two classes need to augment the versioneer modified ones
+# sdist class need to augment the versioneer modified ones
-versioneer_build = cmdclass['build']
versioneer_sdist = cmdclass['sdist']
-class cmd_build(versioneer_build):
+class cmd_build(_build):
def run(self):
- versioneer_build.run(self)
+ _build.run(self)
copy_reqs(self.build_lib)
@@ -371,10 +366,6 @@ class cmd_sdist(versioneer_sdist):
pass
-import shutil
-import glob
-
-
def _get_leap_versions():
versions = {}
with open("pkg/leap_versions.txt") as vf:
@@ -463,7 +454,6 @@ def copy_recursively(source_folder, destination_folder):
cmdclass["build"] = cmd_build
cmdclass["sdist"] = cmd_sdist
-import platform
_system = platform.system()
IS_LINUX = _system == "Linux"
IS_MAC = _system == "Darwin"
@@ -487,6 +477,11 @@ if IS_LINUX:
extra_options = {}
+gui_launcher = 'bitmask=leap.bitmask.app:start_app'
+bitmask_cli = 'bitmask_cli=leap.bitmask.cli.bitmask_cli:main'
+bitmaskd = 'bitmaskd=leap.bitmask.core.launcher:run_bitmaskd'
+
+
setup(
name="leap.bitmask",
package_dir={"": "src"},
@@ -522,7 +517,7 @@ setup(
zip_safe=False,
platforms="all",
entry_points={
- 'console_scripts': [leap_launcher]
+ 'console_scripts': [gui_launcher, bitmask_cli, bitmaskd]
},
**extra_options
)
diff --git a/src/leap/bitmask/__init__.py b/src/leap/bitmask/__init__.py
index 9ec5aae7..c25ae999 100644
--- a/src/leap/bitmask/__init__.py
+++ b/src/leap/bitmask/__init__.py
@@ -19,11 +19,7 @@ Init file for leap.bitmask
Initializes version and app info.
"""
-import re
-
-from pkg_resources import parse_version
-
-from leap.bitmask.util import first
+from ._version import get_versions
# HACK: This is a hack so that py2app copies _scrypt.so to the right
# place, it can't be technically imported, but that doesn't matter
@@ -32,7 +28,7 @@ if False:
import _scrypt # noqa - skip 'not used' warning
-def _is_release_version(version):
+def _is_release_version(version_str):
"""
Helper to determine whether a version is a final release or not.
The release needs to be of the form: w.x.y.z containing only numbers
@@ -43,40 +39,15 @@ def _is_release_version(version):
:returns: if the version is a release version or not.
:rtype: bool
"""
- parsed_version = parse_version(version)
- not_number = 0
- for x in parsed_version:
- try:
- int(x)
- except:
- not_number += 1
-
- return not_number == 1
-
-
-__version__ = "unknown"
-IS_RELEASE_VERSION = False
-
-__short_version__ = "unknown"
-
-try:
- from leap.bitmask._version import get_versions
- __version__ = get_versions()['version']
- __version_hash__ = get_versions()['full']
- IS_RELEASE_VERSION = _is_release_version(__version__)
- del get_versions
-except ImportError:
- # running on a tree that has not run
- # the setup.py setver
- pass
-
-__appname__ = "unknown"
-try:
- from leap.bitmask._appname import __appname__
-except ImportError:
- # running on a tree that has not run
- # the setup.py setver
- pass
-
-__short_version__ = first(re.findall('\d+\.\d+\.\d+', __version__))
-__full_version__ = __appname__ + '/' + str(__version__)
+ parts = __version__.split('.')
+ try:
+ patch = parts[2]
+ except IndexError:
+ return False
+ return patch.isdigit()
+
+
+__version__ = get_versions()['version']
+__version_hash__ = get_versions()['full-revisionid']
+IS_RELEASE_VERSION = _is_release_version(__version__)
+del get_versions
diff --git a/src/leap/bitmask/_version.py b/src/leap/bitmask/_version.py
index 412b0c9e..d64032a7 100644
--- a/src/leap/bitmask/_version.py
+++ b/src/leap/bitmask/_version.py
@@ -1,201 +1,483 @@
-
-IN_LONG_VERSION_PY = True
# This file helps to compute a version number in source trees obtained from
# git-archive tarball (such as those provided by githubs download-from-tag
-# feature). Distribution tarballs (build by setup.py sdist) and build
+# feature). Distribution tarballs (built by setup.py sdist) and build
# directories (produced by setup.py build) will contain a much shorter file
# that just contains the computed version number.
# This file is released into the public domain. Generated by
-# versioneer-0.7+ (https://github.com/warner/python-versioneer)
-
-# these strings will be replaced by git during git-archive
-git_refnames = "$Format:%d$"
-git_full = "$Format:%H$"
+# versioneer-0.16 (https://github.com/warner/python-versioneer)
+"""Git implementation of _version.py."""
+import errno
+import os
+import re
import subprocess
import sys
-import re
-import os.path
-def run_command(args, cwd=None, verbose=False):
- try:
- # remember shell=False, so use git.cmd on windows, not just git
- p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd)
- except EnvironmentError:
- e = sys.exc_info()[1]
+def get_keywords():
+ """Get the keywords needed to look up the version information."""
+ # these strings will be replaced by git during git-archive.
+ # setup.py/versioneer.py will grep for the variable names, so they must
+ # each be defined on a line of their own. _version.py will just call
+ # get_keywords().
+ git_refnames = "$Format:%d$"
+ git_full = "$Format:%H$"
+ keywords = {"refnames": git_refnames, "full": git_full}
+ return keywords
+
+
+class VersioneerConfig:
+ """Container for Versioneer configuration parameters."""
+
+
+def get_config():
+ """Create, populate and return the VersioneerConfig() object."""
+ # these strings are filled in when 'setup.py versioneer' creates
+ # _version.py
+ cfg = VersioneerConfig()
+ cfg.VCS = "git"
+ cfg.style = "pep440"
+ cfg.tag_prefix = ""
+ cfg.parentdir_prefix = "None"
+ cfg.versionfile_source = "src/leap/bitmask/_version.py"
+ cfg.verbose = False
+ return cfg
+
+
+class NotThisMethod(Exception):
+ """Exception raised if a method is not valid for the current scenario."""
+
+
+LONG_VERSION_PY = {}
+HANDLERS = {}
+
+
+def register_vcs_handler(vcs, method): # decorator
+ """Decorator to mark a method as the handler for a particular VCS."""
+ def decorate(f):
+ """Store f in HANDLERS[vcs][method]."""
+ if vcs not in HANDLERS:
+ HANDLERS[vcs] = {}
+ HANDLERS[vcs][method] = f
+ return f
+ return decorate
+
+
+def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False):
+ """Call the given command(s)."""
+ assert isinstance(commands, list)
+ p = None
+ for c in commands:
+ try:
+ dispcmd = str([c] + args)
+ # remember shell=False, so use git.cmd on windows, not just git
+ p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE,
+ stderr=(subprocess.PIPE if hide_stderr
+ else None))
+ break
+ except EnvironmentError:
+ e = sys.exc_info()[1]
+ if e.errno == errno.ENOENT:
+ continue
+ if verbose:
+ print("unable to run %s" % dispcmd)
+ print(e)
+ return None
+ else:
if verbose:
- print("unable to run %s" % args[0])
- print(e)
+ print("unable to find command, tried %s" % (commands,))
return None
stdout = p.communicate()[0].strip()
- if sys.version >= '3':
+ if sys.version_info[0] >= 3:
stdout = stdout.decode()
if p.returncode != 0:
if verbose:
- print("unable to run %s (error)" % args[0])
+ print("unable to run %s (error)" % dispcmd)
return None
return stdout
-def get_expanded_variables(versionfile_source):
+def versions_from_parentdir(parentdir_prefix, root, verbose):
+ """Try to determine the version from the parent directory name.
+
+ Source tarballs conventionally unpack into a directory that includes
+ both the project name and a version string.
+ """
+ dirname = os.path.basename(root)
+ if not dirname.startswith(parentdir_prefix):
+ if verbose:
+ print("guessing rootdir is '%s', but '%s' doesn't start with "
+ "prefix '%s'" % (root, dirname, parentdir_prefix))
+ raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
+ return {"version": dirname[len(parentdir_prefix):],
+ "full-revisionid": None,
+ "dirty": False, "error": None}
+
+
+@register_vcs_handler("git", "get_keywords")
+def git_get_keywords(versionfile_abs):
+ """Extract version information from the given file."""
# the code embedded in _version.py can just fetch the value of these
- # variables. When used from setup.py, we don't want to import
- # _version.py, so we do it with a regexp instead. This function is not
- # used from _version.py.
- variables = {}
+ # keywords. When used from setup.py, we don't want to import _version.py,
+ # so we do it with a regexp instead. This function is not used from
+ # _version.py.
+ keywords = {}
try:
- for line in open(versionfile_source, "r").readlines():
+ f = open(versionfile_abs, "r")
+ for line in f.readlines():
if line.strip().startswith("git_refnames ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
- variables["refnames"] = mo.group(1)
+ keywords["refnames"] = mo.group(1)
if line.strip().startswith("git_full ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
- variables["full"] = mo.group(1)
+ keywords["full"] = mo.group(1)
+ f.close()
except EnvironmentError:
pass
- return variables
+ return keywords
-def versions_from_expanded_variables(variables, tag_prefix, verbose=False):
- refnames = variables["refnames"].strip()
+@register_vcs_handler("git", "keywords")
+def git_versions_from_keywords(keywords, tag_prefix, verbose):
+ """Get version information from git keywords."""
+ if not keywords:
+ raise NotThisMethod("no keywords at all, weird")
+ refnames = keywords["refnames"].strip()
if refnames.startswith("$Format"):
if verbose:
- print("variables are unexpanded, not using")
- return {} # unexpanded, so not in an unpacked git-archive tarball
+ print("keywords are unexpanded, not using")
+ raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
refs = set([r.strip() for r in refnames.strip("()").split(",")])
- for ref in list(refs):
- if not re.search(r'\d', ref):
- if verbose:
- print("discarding '%s', no digits" % ref)
- refs.discard(ref)
- # Assume all version tags have a digit. git's %d expansion
- # behaves like git log --decorate=short and strips out the
- # refs/heads/ and refs/tags/ prefixes that would let us
- # distinguish between branches and tags. By ignoring refnames
- # without digits, we filter out many common branch names like
- # "release" and "stabilization", as well as "HEAD" and "master".
+ # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
+ # just "foo-1.0". If we see a "tag: " prefix, prefer those.
+ TAG = "tag: "
+ tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
+ if not tags:
+ # Either we're using git < 1.8.3, or there really are no tags. We use
+ # a heuristic: assume all version tags have a digit. The old git %d
+ # expansion behaves like git log --decorate=short and strips out the
+ # refs/heads/ and refs/tags/ prefixes that would let us distinguish
+ # between branches and tags. By ignoring refnames without digits, we
+ # filter out many common branch names like "release" and
+ # "stabilization", as well as "HEAD" and "master".
+ tags = set([r for r in refs if re.search(r'\d', r)])
+ if verbose:
+ print("discarding '%s', no digits" % ",".join(refs - tags))
if verbose:
- print("remaining refs: %s" % ",".join(sorted(refs)))
- for ref in sorted(refs):
+ print("likely tags: %s" % ",".join(sorted(tags)))
+ for ref in sorted(tags):
# sorting will prefer e.g. "2.0" over "2.0rc1"
if ref.startswith(tag_prefix):
r = ref[len(tag_prefix):]
if verbose:
print("picking %s" % r)
return {"version": r,
- "full": variables["full"].strip()}
- # no suitable tags, so we use the full revision id
+ "full-revisionid": keywords["full"].strip(),
+ "dirty": False, "error": None
+ }
+ # no suitable tags, so version is "0+unknown", but full hex is still there
if verbose:
- print("no suitable tags, using full revision id")
- return {"version": variables["full"].strip(),
- "full": variables["full"].strip()}
-
-
-def versions_from_vcs(tag_prefix, versionfile_source, verbose=False):
- # this runs 'git' from the root of the source tree. That either means
- # someone ran a setup.py command (and this code is in versioneer.py, so
- # IN_LONG_VERSION_PY=False, thus the containing directory is the root of
- # the source tree), or someone ran a project-specific entry point (and
- # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the
- # containing directory is somewhere deeper in the source tree). This only
- # gets called if the git-archive 'subst' variables were *not* expanded,
- # and _version.py hasn't already been rewritten with a short version
- # string, meaning we're inside a checked out source tree.
+ print("no suitable tags, using unknown + full revision id")
+ return {"version": "0+unknown",
+ "full-revisionid": keywords["full"].strip(),
+ "dirty": False, "error": "no suitable tags"}
- try:
- here = os.path.abspath(__file__)
- except NameError:
- # some py2exe/bbfreeze/non-CPython implementations don't do __file__
- return {} # not always correct
-
- # versionfile_source is the relative path from the top of the source tree
- # (where the .git directory might live) to this file. Invert this to find
- # the root from __file__.
- root = here
- if IN_LONG_VERSION_PY:
- for i in range(len(versionfile_source.split("/"))):
- root = os.path.dirname(root)
- else:
- root = os.path.dirname(here)
+
+@register_vcs_handler("git", "pieces_from_vcs")
+def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
+ """Get version from 'git describe' in the root of the source tree.
+
+ This only gets called if the git-archive 'subst' keywords were *not*
+ expanded, and _version.py hasn't already been rewritten with a short
+ version string, meaning we're inside a checked out source tree.
+ """
if not os.path.exists(os.path.join(root, ".git")):
if verbose:
print("no .git in %s" % root)
- return {}
+ raise NotThisMethod("no .git directory")
- GIT = "git"
+ GITS = ["git"]
if sys.platform == "win32":
- GIT = "git.cmd"
- stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"],
- cwd=root)
- if stdout is None:
- return {}
- if not stdout.startswith(tag_prefix):
- if verbose:
- print("tag '%s' doesn't start with prefix '%s'" % (
- stdout, tag_prefix))
- return {}
- tag = stdout[len(tag_prefix):]
- stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root)
- if stdout is None:
- return {}
- full = stdout.strip()
- if tag.endswith("-dirty"):
- full += "-dirty"
- return {"version": tag, "full": full}
-
-
-def versions_from_parentdir(parentdir_prefix, versionfile_source,
- verbose=False):
- if IN_LONG_VERSION_PY:
- # We're running from _version.py. If it's from a source tree
- # (execute-in-place), we can work upwards to find the root of the
- # tree, and then check the parent directory for a version string. If
- # it's in an installed application, there's no hope.
- try:
- here = os.path.abspath(__file__)
- except NameError:
- # py2exe/bbfreeze/non-CPython don't have __file__
- return {} # without __file__, we have no hope
+ GITS = ["git.cmd", "git.exe"]
+ # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
+ # if there isn't one, this yields HEX[-dirty] (no NUM)
+ describe_out = run_command(GITS, ["describe", "--tags", "--dirty",
+ "--always", "--long",
+ "--match", "%s*" % tag_prefix],
+ cwd=root)
+ # --long was added in git-1.5.5
+ if describe_out is None:
+ raise NotThisMethod("'git describe' failed")
+ describe_out = describe_out.strip()
+ full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
+ if full_out is None:
+ raise NotThisMethod("'git rev-parse' failed")
+ full_out = full_out.strip()
+
+ pieces = {}
+ pieces["long"] = full_out
+ pieces["short"] = full_out[:7] # maybe improved later
+ pieces["error"] = None
+
+ # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
+ # TAG might have hyphens.
+ git_describe = describe_out
+
+ # look for -dirty suffix
+ dirty = git_describe.endswith("-dirty")
+ pieces["dirty"] = dirty
+ if dirty:
+ git_describe = git_describe[:git_describe.rindex("-dirty")]
+
+ # now we have TAG-NUM-gHEX or HEX
+
+ if "-" in git_describe:
+ # TAG-NUM-gHEX
+ mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
+ if not mo:
+ # unparseable. Maybe git-describe is misbehaving?
+ pieces["error"] = ("unable to parse git-describe output: '%s'"
+ % describe_out)
+ return pieces
+
+ # tag
+ full_tag = mo.group(1)
+ if not full_tag.startswith(tag_prefix):
+ if verbose:
+ fmt = "tag '%s' doesn't start with prefix '%s'"
+ print(fmt % (full_tag, tag_prefix))
+ pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
+ % (full_tag, tag_prefix))
+ return pieces
+ pieces["closest-tag"] = full_tag[len(tag_prefix):]
+
+ # distance: number of commits since tag
+ pieces["distance"] = int(mo.group(2))
+
+ # commit: short hex revision ID
+ pieces["short"] = mo.group(3)
+
+ else:
+ # HEX: no tags
+ pieces["closest-tag"] = None
+ count_out = run_command(GITS, ["rev-list", "HEAD", "--count"],
+ cwd=root)
+ pieces["distance"] = int(count_out) # total number of commits
+
+ return pieces
+
+
+def plus_or_dot(pieces):
+ """Return a + if we don't already have one, else return a ."""
+ if "+" in pieces.get("closest-tag", ""):
+ return "."
+ return "+"
+
+
+def render_pep440(pieces):
+ """Build up version string, with post-release "local version identifier".
+
+ Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
+ get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
+
+ Exceptions:
+ 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"] or pieces["dirty"]:
+ rendered += plus_or_dot(pieces)
+ rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
+ if pieces["dirty"]:
+ rendered += ".dirty"
+ else:
+ # exception #1
+ rendered = "0+untagged.%d.g%s" % (pieces["distance"],
+ pieces["short"])
+ if pieces["dirty"]:
+ rendered += ".dirty"
+ return rendered
+
+
+def render_pep440_pre(pieces):
+ """TAG[.post.devDISTANCE] -- No -dirty.
+
+ Exceptions:
+ 1: no tags. 0.post.devDISTANCE
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"]:
+ rendered += ".post.dev%d" % pieces["distance"]
+ else:
+ # exception #1
+ rendered = "0.post.dev%d" % pieces["distance"]
+ return rendered
+
+
+def render_pep440_post(pieces):
+ """TAG[.postDISTANCE[.dev0]+gHEX] .
+
+ The ".dev0" means dirty. Note that .dev0 sorts backwards
+ (a dirty tree will appear "older" than the corresponding clean one),
+ but you shouldn't be releasing software with -dirty anyways.
+
+ Exceptions:
+ 1: no tags. 0.postDISTANCE[.dev0]
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"] or pieces["dirty"]:
+ rendered += ".post%d" % pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ rendered += plus_or_dot(pieces)
+ rendered += "g%s" % pieces["short"]
+ else:
+ # exception #1
+ rendered = "0.post%d" % pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ rendered += "+g%s" % pieces["short"]
+ return rendered
+
+
+def render_pep440_old(pieces):
+ """TAG[.postDISTANCE[.dev0]] .
+
+ The ".dev0" means dirty.
+
+ Eexceptions:
+ 1: no tags. 0.postDISTANCE[.dev0]
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"] or pieces["dirty"]:
+ rendered += ".post%d" % pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ else:
+ # exception #1
+ rendered = "0.post%d" % pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ return rendered
+
+
+def render_git_describe(pieces):
+ """TAG[-DISTANCE-gHEX][-dirty].
+
+ Like 'git describe --tags --dirty --always'.
+
+ Exceptions:
+ 1: no tags. HEX[-dirty] (note: no 'g' prefix)
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"]:
+ rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
+ else:
+ # exception #1
+ rendered = pieces["short"]
+ if pieces["dirty"]:
+ rendered += "-dirty"
+ return rendered
+
+
+def render_git_describe_long(pieces):
+ """TAG-DISTANCE-gHEX[-dirty].
+
+ Like 'git describe --tags --dirty --always -long'.
+ The distance/hash is unconditional.
+
+ Exceptions:
+ 1: no tags. HEX[-dirty] (note: no 'g' prefix)
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
+ else:
+ # exception #1
+ rendered = pieces["short"]
+ if pieces["dirty"]:
+ rendered += "-dirty"
+ return rendered
+
+
+def render(pieces, style):
+ """Render the given version pieces into the requested style."""
+ if pieces["error"]:
+ return {"version": "unknown",
+ "full-revisionid": pieces.get("long"),
+ "dirty": None,
+ "error": pieces["error"]}
+
+ if not style or style == "default":
+ style = "pep440" # the default
+
+ if style == "pep440":
+ rendered = render_pep440(pieces)
+ elif style == "pep440-pre":
+ rendered = render_pep440_pre(pieces)
+ elif style == "pep440-post":
+ rendered = render_pep440_post(pieces)
+ elif style == "pep440-old":
+ rendered = render_pep440_old(pieces)
+ elif style == "git-describe":
+ rendered = render_git_describe(pieces)
+ elif style == "git-describe-long":
+ rendered = render_git_describe_long(pieces)
+ else:
+ raise ValueError("unknown style '%s'" % style)
+
+ return {"version": rendered, "full-revisionid": pieces["long"],
+ "dirty": pieces["dirty"], "error": None}
+
+
+def get_versions():
+ """Get version information or return default if unable to do so."""
+ # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
+ # __file__, we can work backwards from there to the root. Some
+ # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
+ # case we can only use expanded keywords.
+
+ cfg = get_config()
+ verbose = cfg.verbose
+
+ try:
+ return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
+ verbose)
+ except NotThisMethod:
+ pass
+
+ try:
+ root = os.path.realpath(__file__)
# versionfile_source is the relative path from the top of the source
- # tree to _version.py. Invert this to find the root from __file__.
- root = here
- for i in range(len(versionfile_source.split("/"))):
+ # tree (where the .git directory might live) to this file. Invert
+ # this to find the root from __file__.
+ for i in cfg.versionfile_source.split('/'):
root = os.path.dirname(root)
- else:
- # we're running from versioneer.py, which means we're running from
- # the setup.py in a source tree. sys.argv[0] is setup.py in the root.
- here = os.path.abspath(sys.argv[0])
- root = os.path.dirname(here)
+ except NameError:
+ return {"version": "0+unknown", "full-revisionid": None,
+ "dirty": None,
+ "error": "unable to find root of source tree"}
- # Source tarballs conventionally unpack into a directory that includes
- # both the project name and a version string.
- dirname = os.path.basename(root)
- if not dirname.startswith(parentdir_prefix):
- if verbose:
- print("guessing rootdir is '%s', but '%s' "
- "doesn't start with prefix '%s'" %
- (root, dirname, parentdir_prefix))
- return None
- return {"version": dirname[len(parentdir_prefix):], "full": ""}
-
-tag_prefix = ""
-parentdir_prefix = "bitmask-"
-versionfile_source = "src/leap/bitmask/_version.py"
-
-
-def get_versions(default={"version": "unknown", "full": ""}, verbose=False):
- variables = {"refnames": git_refnames, "full": git_full}
- ver = versions_from_expanded_variables(variables, tag_prefix, verbose)
- if not ver:
- ver = versions_from_vcs(tag_prefix, versionfile_source, verbose)
- if not ver:
- ver = versions_from_parentdir(parentdir_prefix, versionfile_source,
- verbose)
- if not ver:
- ver = default
- return ver
+ try:
+ pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
+ return render(pieces, cfg.style)
+ except NotThisMethod:
+ pass
+
+ try:
+ if cfg.parentdir_prefix:
+ return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
+ except NotThisMethod:
+ pass
+
+ return {"version": "0+unknown", "full-revisionid": None,
+ "dirty": None,
+ "error": "unable to compute version"}
diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py
index a1b7481a..e5189e23 100644
--- a/src/leap/bitmask/app.py
+++ b/src/leap/bitmask/app.py
@@ -46,12 +46,16 @@ import os
import platform
import sys
+
if platform.system() == "Darwin":
+ # XXX please ignore pep8 complains, this needs to be executed
+ # early.
# We need to tune maximum number of files, due to zmq usage
# we hit the limit.
import resource
resource.setrlimit(resource.RLIMIT_NOFILE, (4096, 10240))
+
from leap.bitmask import __version__ as VERSION
from leap.bitmask.backend.backend_proxy import BackendProxy
from leap.bitmask.backend_app import run_backend
@@ -73,6 +77,12 @@ codecs.register(lambda name: codecs.lookup('utf-8')
import psutil
+def qt_hack_ubuntu():
+ """Export two env vars to avoid gui corruption, see #8028"""
+ os.environ['QT_GRAPHICSSYSTEM'] = 'native'
+ os.environ['LIBOVERLAY_SCROLLBAR'] = '0'
+
+
def kill_the_children():
"""
Make sure no lingering subprocesses are left in case of a bad termination.
@@ -150,6 +160,8 @@ def start_app():
"""
Starts the main event loop and launches the main window.
"""
+ qt_hack_ubuntu()
+
# Ignore the signals since we handle them in the subprocesses
# signal.signal(signal.SIGINT, signal.SIG_IGN)
@@ -163,6 +175,12 @@ def start_app():
}
flags.STANDALONE = opts.standalone
+
+ if platform.system() != 'Darwin':
+ # XXX this hangs the OSX bundles.
+ if getattr(sys, 'frozen', False):
+ flags.STANDALONE = True
+
flags.OFFLINE = opts.offline
flags.MAIL_LOGFILE = opts.mail_log_file
flags.APP_VERSION_CHECK = opts.app_version_check
@@ -223,9 +241,10 @@ def start_app():
backend_pid = None
if not backend_running:
frontend_pid = os.getpid()
- backend = lambda: run_backend(opts.danger, flags_dict, frontend_pid)
- backend_process = multiprocessing.Process(target=backend,
- name='Backend')
+ backend_process = multiprocessing.Process(
+ target=run_backend,
+ name='Backend',
+ args=(opts.danger, flags_dict, frontend_pid))
# we don't set the 'daemon mode' since we need to start child processes
# in the backend
# backend_process.daemon = True
@@ -237,4 +256,5 @@ def start_app():
if __name__ == "__main__":
+ multiprocessing.freeze_support()
start_app()
diff --git a/src/leap/bitmask/backend/api.py b/src/leap/bitmask/backend/api.py
index 48aa2090..2fd983ae 100644
--- a/src/leap/bitmask/backend/api.py
+++ b/src/leap/bitmask/backend/api.py
@@ -42,6 +42,8 @@ API = (
"keymanager_export_keys",
"keymanager_get_key_details",
"keymanager_list_keys",
+ "pixelated_start_service",
+ "pixelated_stop_service",
"provider_bootstrap",
"provider_cancel_setup",
"provider_get_all_services",
@@ -57,6 +59,7 @@ API = (
"soledad_change_password",
"soledad_close",
"soledad_load_offline",
+ "soledad_get_service_token",
"tear_fw_down",
"bitmask_root_vpn_down",
"user_cancel_login",
@@ -135,6 +138,7 @@ SIGNALS = (
"soledad_offline_finished",
"soledad_password_change_error",
"soledad_password_change_ok",
+ "soledad_got_service_token",
"srp_auth_bad_user_or_password",
"srp_auth_connection_error",
"srp_auth_error",
diff --git a/src/leap/bitmask/backend/components.py b/src/leap/bitmask/backend/components.py
index 5f34d290..ba64fd65 100644
--- a/src/leap/bitmask/backend/components.py
+++ b/src/leap/bitmask/backend/components.py
@@ -20,13 +20,16 @@ Backend components
# TODO [ ] Get rid of all this deferToThread mess, or at least contain
# all of it into its own threadpool.
+import json
import os
+import shutil
import socket
+import tempfile
import time
from functools import partial
-from twisted.internet import threads, defer
+from twisted.internet import threads, defer, reactor
from twisted.python import log
import zope.interface
@@ -38,9 +41,10 @@ from leap.bitmask.crypto.srpauth import SRPAuth
from leap.bitmask.crypto.srpregister import SRPRegister
from leap.bitmask.logs.utils import get_logger
from leap.bitmask.platform_init import IS_LINUX
+from leap.bitmask import pix
from leap.bitmask.provider.pinned import PinnedProviders
from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper
-from leap.bitmask.services import get_supported
+from leap.bitmask.services import get_supported, EIP_SERVICE
from leap.bitmask.services.eip import eipconfig
from leap.bitmask.services.eip import get_openvpn_management
from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper
@@ -572,8 +576,10 @@ class EIP(object):
self._signaler.eip_uninitialized_provider)
return
- eip_config = eipconfig.EIPConfig()
provider_config = ProviderConfig.get_provider_config(domain)
+ if EIP_SERVICE not in provider_config.get_services():
+ return
+ eip_config = eipconfig.EIPConfig()
api_version = provider_config.get_api_version()
eip_config.set_api_version(api_version)
@@ -643,12 +649,14 @@ class EIP(object):
:param domain: the domain for the provider to check
:type domain: str
"""
- if not LinuxPolicyChecker.is_up():
+ if IS_LINUX and not LinuxPolicyChecker.is_up():
logger.error("No polkit agent running.")
return False
- eip_config = eipconfig.EIPConfig()
provider_config = ProviderConfig.get_provider_config(domain)
+ if EIP_SERVICE not in provider_config.get_services():
+ return False
+ eip_config = eipconfig.EIPConfig()
api_version = provider_config.get_api_version()
eip_config.set_api_version(api_version)
@@ -763,6 +771,7 @@ class Soledad(object):
self._signaler = signaler
self._soledad_bootstrapper = SoledadBootstrapper(signaler)
self._soledad_defer = None
+ self._service_tokens = {}
def bootstrap(self, username, domain, password):
"""
@@ -786,6 +795,9 @@ class Soledad(object):
provider_config, username, password,
download_if_needed=True)
self._soledad_defer.addCallback(self._set_proxies_cb)
+ self._soledad_defer.addCallback(self._set_service_tokens_cb)
+ self._soledad_defer.addCallback(self._write_tokens_file,
+ username, domain)
else:
if self._signaler is not None:
self._signaler.signal(self._signaler.soledad_bootstrap_failed)
@@ -793,6 +805,38 @@ class Soledad(object):
return self._soledad_defer
+ def _set_service_tokens_cb(self, result):
+
+ def register_service_token(token, service):
+ self._service_tokens[service] = token
+ if self._signaler is not None:
+ self._signaler.signal(
+ self._signaler.soledad_got_service_token,
+ (service, token))
+
+ sol = self._soledad_bootstrapper.soledad
+ d = sol.get_or_create_service_token('mail_auth')
+ d.addCallback(register_service_token, 'mail_auth')
+ d.addCallback(lambda _: result)
+ return d
+
+ def _write_tokens_file(self, result, username, domain):
+ tokens_folder = os.path.join(tempfile.gettempdir(), "bitmask_tokens")
+ if os.path.exists(tokens_folder):
+ try:
+ shutil.rmtree(tokens_folder)
+ except OSError as e:
+ logger.error("Can't remove tokens folder %s: %s"
+ % (tokens_folder, e))
+ return
+ os.mkdir(tokens_folder, 0700)
+
+ tokens_path = os.path.join(tokens_folder,
+ "%s@%s.json" % (username, domain))
+ with open(tokens_path, 'w') as ftokens:
+ json.dump(self._service_tokens, ftokens)
+ return result
+
def _set_proxies_cb(self, _):
"""
Update the soledad and keymanager proxies to reference the ones created
@@ -803,6 +847,12 @@ class Soledad(object):
zope.proxy.setProxiedObject(self._keymanager_proxy,
self._soledad_bootstrapper.keymanager)
+ def get_service_token(self, service):
+ """
+ Get an authentication token for a given service.
+ """
+ return self._service_tokens.get(service, '')
+
def load_offline(self, username, password, uuid):
"""
Load the soledad database in offline mode.
@@ -944,23 +994,22 @@ class Keymanager(object):
d.addCallback(export)
d.addErrback(log_error)
+ @defer.inlineCallbacks
def list_keys(self):
"""
List all the keys stored in the local DB.
"""
- d = self._keymanager_proxy.get_all_keys()
- d.addCallback(
- lambda keys:
- self._signaler.signal(self._signaler.keymanager_keys_list, keys))
+ keys = yield self._keymanager_proxy.get_all_keys()
+ keydicts = [dict(key) for key in keys]
+ self._signaler.signal(self._signaler.keymanager_keys_list, keydicts)
def get_key_details(self, username):
"""
- List all the keys stored in the local DB.
+ Get information on our primary key pair
"""
def signal_details(public_key):
- details = (public_key.key_id, public_key.fingerprint)
self._signaler.signal(self._signaler.keymanager_key_details,
- details)
+ dict(public_key))
d = self._keymanager_proxy.get_key(username,
openpgp.OpenPGPKey)
@@ -1012,7 +1061,8 @@ class Mail(object):
"""
return threads.deferToThread(
self._smtp_bootstrapper.start_smtp_service,
- self._keymanager_proxy, full_user_id, download_if_needed)
+ self._soledad_proxy, self._keymanager_proxy, full_user_id,
+ download_if_needed)
def start_imap_service(self, full_user_id, offline=False):
"""
@@ -1058,6 +1108,18 @@ class Mail(object):
"""
return threads.deferToThread(self._stop_imap_service)
+ def start_pixelated_service(self, full_user_id):
+ if pix.HAS_PIXELATED:
+ reactor.callFromThread(
+ pix.start_pixelated_user_agent,
+ full_user_id,
+ self._soledad_proxy,
+ self._keymanager_proxy)
+
+ def stop_pixelated_service(self):
+ # TODO stop it, somehow
+ pass
+
class Authenticate(object):
"""
@@ -1153,15 +1215,13 @@ class Authenticate(object):
def get_logged_in_status(self):
"""
- Signal if the user is currently logged in or not.
+ Signal if the user is currently logged in or not. If logged in,
+ authenticated username is passed as argument to the signal.
"""
if self._signaler is None:
return
- signal = None
if self._is_logged_in():
- signal = self._signaler.srp_status_logged_in
+ self._signaler.signal(self._signaler.srp_status_logged_in)
else:
- signal = self._signaler.srp_status_not_logged_in
-
- self._signaler.signal(signal)
+ self._signaler.signal(self._signaler.srp_status_not_logged_in)
diff --git a/src/leap/bitmask/backend/leapbackend.py b/src/leap/bitmask/backend/leapbackend.py
index cf45c4f8..56b1597c 100644
--- a/src/leap/bitmask/backend/leapbackend.py
+++ b/src/leap/bitmask/backend/leapbackend.py
@@ -35,6 +35,7 @@ class LeapBackend(Backend):
"""
Backend server subclass, used to implement the API methods.
"""
+
def __init__(self, bypass_checks=False, frontend_pid=None):
"""
Constructor for the backend.
@@ -438,6 +439,12 @@ class LeapBackend(Backend):
"""
self._soledad.load_offline(username, password, uuid)
+ def soledad_get_service_token(self, service):
+ """
+ Attempt to get an authentication token for a given service.
+ """
+ self._soledad.get_service_token(service)
+
def soledad_cancel_bootstrap(self):
"""
Cancel the ongoing soledad bootstrapping process (if any).
@@ -524,6 +531,12 @@ class LeapBackend(Backend):
"""
self._mail.stop_imap_service()
+ def pixelated_start_service(self, full_user_id):
+ self._mail.start_pixelated_service(full_user_id)
+
+ def pixelated_stop_service(self):
+ self._mail.stop_pixelated_service()
+
def settings_set_selected_gateway(self, provider, gateway):
"""
Set the selected gateway for a given provider.
diff --git a/src/leap/bitmask/backend/leapsignaler.py b/src/leap/bitmask/backend/leapsignaler.py
index 1ac51f5e..13a9fa5f 100644
--- a/src/leap/bitmask/backend/leapsignaler.py
+++ b/src/leap/bitmask/backend/leapsignaler.py
@@ -97,6 +97,7 @@ class LeapSignaler(SignalerQt):
soledad_offline_finished = QtCore.Signal()
soledad_password_change_error = QtCore.Signal()
soledad_password_change_ok = QtCore.Signal()
+ soledad_got_service_token = QtCore.Signal(object)
srp_auth_bad_user_or_password = QtCore.Signal()
srp_auth_connection_error = QtCore.Signal()
diff --git a/src/leap/bitmask/cli/__init__.py b/src/leap/bitmask/cli/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/leap/bitmask/cli/__init__.py
diff --git a/src/leap/bitmask/cli/bitmask_cli.py b/src/leap/bitmask/cli/bitmask_cli.py
new file mode 100755
index 00000000..c2b1ba71
--- /dev/null
+++ b/src/leap/bitmask/cli/bitmask_cli.py
@@ -0,0 +1,341 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# bitmask_cli
+# Copyright (C) 2015, 2016 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Bitmask Command Line interface: zmq client.
+"""
+import json
+import sys
+import getpass
+import argparse
+
+from colorama import init as color_init
+from colorama import Fore
+from twisted.internet import reactor
+from txzmq import ZmqEndpoint, ZmqEndpointType
+from txzmq import ZmqFactory, ZmqREQConnection
+from txzmq import ZmqRequestTimeoutError
+
+from leap.bitmask.core import ENDPOINT
+
+
+class BitmaskCLI(object):
+
+ def __init__(self):
+ parser = argparse.ArgumentParser(
+ usage='''bitmask_cli <command> [<args>]
+
+Controls the Bitmask application.
+
+SERVICE COMMANDS:
+
+ user Handles Bitmask accounts
+ mail Bitmask Encrypted Mail
+ eip Encrypted Internet Proxy
+ keys Bitmask Keymanager
+
+GENERAL COMMANDS:
+
+ version prints version number and exit
+ launch launch the Bitmask backend daemon
+ shutdown shutdown Bitmask backend daemon
+ status displays general status about the running Bitmask services
+ debug show some debug info about bitmask-core
+
+
+''', epilog=("Use 'bitmask_cli <command> --help' to learn more "
+ "about each command."))
+ parser.add_argument('command', help='Subcommand to run')
+
+ # parse_args defaults to [1:] for args, but you need to
+ # exclude the rest of the args too, or validation will fail
+ args = parser.parse_args(sys.argv[1:2])
+ self.args = args
+ self.subargs = None
+
+ if not hasattr(self, args.command):
+ print 'Unrecognized command'
+ parser.print_help()
+ exit(1)
+
+ # use dispatch pattern to invoke method with same name
+ getattr(self, args.command)()
+
+ def user(self):
+ parser = argparse.ArgumentParser(
+ description=('Handles Bitmask accounts: creation, authentication '
+ 'and modification'),
+ prog='bitmask_cli user')
+ parser.add_argument('username', nargs='?',
+ help='username ID, in the form <user@example.org>')
+ parser.add_argument('--create', action='store_true',
+ help='register a new user, if possible')
+ parser.add_argument('--authenticate', action='store_true',
+ help='logs in against the provider')
+ parser.add_argument('--logout', action='store_true',
+ help='ends any active session with the provider')
+ parser.add_argument('--active', action='store_true',
+ help='shows the active user, if any')
+ # now that we're inside a subcommand, ignore the first
+ # TWO argvs, ie the command (bitmask_cli) and the subcommand (user)
+ args = parser.parse_args(sys.argv[2:])
+ self.subargs = args
+
+ def mail(self):
+ parser = argparse.ArgumentParser(
+ description='Bitmask Encrypted Mail service',
+ prog='bitmask_cli mail')
+ parser.add_argument('--start', action='store_true',
+ help='tries to start the mail service')
+ parser.add_argument('--stop', action='store_true',
+ help='stops the mail service if running')
+ parser.add_argument('--status', action='store_true',
+ help='displays status about the mail service')
+ parser.add_argument('--enable', action='store_true')
+ parser.add_argument('--disable', action='store_true')
+ parser.add_argument('--get-imap-token', action='store_true',
+ help='returns token for the IMAP service')
+ parser.add_argument('--get-smtp-token', action='store_true',
+ help='returns token for the SMTP service')
+ parser.add_argument('--get-smtp-certificate', action='store_true',
+ help='downloads a new smtp certificate')
+ parser.add_argument('--check-smtp-certificate', action='store_true',
+ help='downloads a new smtp certificate '
+ '(NOT IMPLEMENTED)')
+
+ args = parser.parse_args(sys.argv[2:])
+ self.subargs = args
+
+ def eip(self):
+ parser = argparse.ArgumentParser(
+ description='Encrypted Internet Proxy service',
+ prog='bitmask_cli eip')
+ parser.add_argument('--start', action='store_true',
+ help='Start service')
+ parser.add_argument('--stop', action='store_true', help='Stop service')
+ parser.add_argument('--status', action='store_true',
+ help='Display status about service')
+ parser.add_argument('--enable', action='store_true')
+ parser.add_argument('--disable', action='store_true')
+ args = parser.parse_args(sys.argv[2:])
+ self.subargs = args
+
+ def keys(self):
+ parser = argparse.ArgumentParser(
+ description='Bitmask Keymanager management service',
+ prog='bitmask_cli keys')
+ parser.add_argument('--status', action='store_true',
+ help='Display status about service')
+ parser.add_argument('--list-keys', action='store_true',
+ help='List all known keys')
+ parser.add_argument('--export-key', action='store_true',
+ help='Export the given key')
+ args = parser.parse_args(sys.argv[2:])
+ self.subargs = args
+
+ # Single commands
+
+ def launch(self):
+ pass
+
+ def shutdown(self):
+ pass
+
+ def status(self):
+ pass
+
+ def version(self):
+ pass
+
+ def debug(self):
+ pass
+
+
+def get_zmq_connection():
+ zf = ZmqFactory()
+ e = ZmqEndpoint(ZmqEndpointType.connect, ENDPOINT)
+ return ZmqREQConnection(zf, e)
+
+
+def error(msg, stop=False):
+ print Fore.RED + "[!] %s" % msg + Fore.RESET
+ if stop:
+ reactor.stop()
+ else:
+ sys.exit(1)
+
+
+def timeout_handler(failure, stop_reactor=True):
+ # TODO ---- could try to launch the bitmask daemon here and retry
+
+ if failure.trap(ZmqRequestTimeoutError) == ZmqRequestTimeoutError:
+ print (Fore.RED + "[ERROR] Timeout contacting the bitmask daemon. "
+ "Is it running?" + Fore.RESET)
+ reactor.stop()
+
+
+def do_print_result(stuff):
+ obj = json.loads(stuff[0])
+ if not obj['error']:
+ print Fore.GREEN + '%s' % obj['result'] + Fore.RESET
+ else:
+ print Fore.RED + 'ERROR:' + '%s' % obj['error'] + Fore.RESET
+
+
+def send_command(cli):
+
+ args = cli.args
+ subargs = cli.subargs
+ cb = do_print_result
+
+ cmd = args.command
+
+ if cmd == 'launch':
+ # XXX careful! Should see if the process in PID is running,
+ # avoid launching again.
+ import commands
+ commands.getoutput('bitmaskd')
+ reactor.stop()
+ return
+
+ elif cmd == 'version':
+ do_print_result([json.dumps(
+ {'result': 'bitmask_cli: 0.0.1',
+ 'error': None})])
+ data = ('version',)
+
+ elif cmd == 'status':
+ data = ('status',)
+
+ elif cmd == 'shutdown':
+ data = ('shutdown',)
+
+ elif cmd == 'debug':
+ data = ('stats',)
+
+ elif cmd == 'user':
+ username = subargs.username
+ if username and '@' not in username:
+ error("Username ID must be in the form <user@example.org>",
+ stop=True)
+ return
+ if not username:
+ username = ''
+
+ # TODO check that ONLY ONE FLAG is True
+ # TODO check that AT LEAST ONE FLAG is True
+
+ passwd = getpass.getpass()
+ data = ['user']
+
+ if subargs.active:
+ data += ['active', '', '']
+
+ elif subargs.create:
+ data += ['signup', username, passwd]
+
+ elif subargs.authenticate:
+ data += ['authenticate', username, passwd]
+
+ elif subargs.logout:
+ data += ['logout', username, passwd]
+
+ else:
+ error('Use bitmask_cli user --help to see available subcommands')
+ return
+
+ elif cmd == 'mail':
+ data = ['mail']
+
+ if subargs.status:
+ data += ['status']
+
+ elif subargs.enable:
+ data += ['enable']
+
+ elif subargs.disable:
+ data += ['disable']
+
+ elif subargs.get_imap_token:
+ data += ['get_imap_token']
+
+ elif subargs.get_smtp_token:
+ data += ['get_smtp_token']
+
+ elif subargs.get_smtp_certificate:
+ data += ['get_smtp_certificate']
+
+ else:
+ error('Use bitmask_cli mail --help to see available subcommands')
+ return
+
+ elif cmd == 'eip':
+ data = ['eip']
+
+ if subargs.status:
+ data += ['status']
+
+ elif subargs.enable:
+ data += ['enable']
+
+ elif subargs.disable:
+ data += ['disable']
+
+ elif subargs.start:
+ data += ['start']
+
+ elif subargs.stop:
+ data += ['stop']
+
+ else:
+ error('Use bitmask_cli eip --help to see available subcommands',
+ stop=True)
+ return
+
+ elif cmd == 'keys':
+ data = ['keys']
+
+ if subargs.status:
+ data += ['status']
+
+ elif subargs.list_keys:
+ data += ['list_keys']
+
+ elif subargs.export_key:
+ data += ['export_keys']
+
+ else:
+ error('Use bitmask_cli keys --help to see available subcommands',
+ stop=True)
+ return
+
+ s = get_zmq_connection()
+
+ d = s.sendMsg(*data, timeout=60)
+ d.addCallback(cb)
+ d.addCallback(lambda x: reactor.stop())
+ d.addErrback(timeout_handler)
+
+
+def main():
+ color_init()
+ cli = BitmaskCLI()
+ reactor.callWhenRunning(reactor.callLater, 0, send_command, cli)
+ reactor.run()
+
+if __name__ == "__main__":
+ main()
diff --git a/src/leap/bitmask/config/leapsettings.py b/src/leap/bitmask/config/leapsettings.py
index 484a8a25..075be8a7 100644
--- a/src/leap/bitmask/config/leapsettings.py
+++ b/src/leap/bitmask/config/leapsettings.py
@@ -70,6 +70,7 @@ class LeapSettings(object):
PINNED_KEY = "Pinned"
SKIPFIRSTRUN_KEY = "SkipFirstRun"
UUIDFORUSER_KEY = "%s/%s_uuid"
+ PIXELMAIL_KEY = "Pixmail"
# values
GATEWAY_AUTOMATIC = "Automatic"
@@ -353,3 +354,10 @@ class LeapSettings(object):
"""
leap_assert_type(skip, bool)
self._settings.setValue(self.SKIPFIRSTRUN_KEY, skip)
+
+ def get_pixelmail_enabled(self):
+ return to_bool(self._settings.value(self.PIXELMAIL_KEY, False))
+
+ def set_pixelmail_enabled(self, enabled):
+ leap_assert_type(enabled, bool)
+ self._settings.setValue(self.PIXELMAIL_KEY, enabled)
diff --git a/src/leap/bitmask/core/__init__.py b/src/leap/bitmask/core/__init__.py
new file mode 100644
index 00000000..bda4b8d0
--- /dev/null
+++ b/src/leap/bitmask/core/__init__.py
@@ -0,0 +1,2 @@
+APPNAME = "bitmask.core"
+ENDPOINT = "ipc:///tmp/%s.sock" % APPNAME
diff --git a/src/leap/bitmask/core/_zmq.py b/src/leap/bitmask/core/_zmq.py
new file mode 100644
index 00000000..a656fc65
--- /dev/null
+++ b/src/leap/bitmask/core/_zmq.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# _zmq.py
+# Copyright (C) 2015 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+ZMQ REQ-REP Dispatcher.
+"""
+
+from twisted.application import service
+from twisted.internet import reactor
+from twisted.python import log
+
+from txzmq import ZmqEndpoint, ZmqEndpointType
+from txzmq import ZmqFactory, ZmqREPConnection
+
+from leap.bitmask.core import ENDPOINT
+from leap.bitmask.core.dispatcher import CommandDispatcher
+
+
+class ZMQServerService(service.Service):
+
+ def __init__(self, core):
+ self._core = core
+
+ def startService(self):
+ zf = ZmqFactory()
+ e = ZmqEndpoint(ZmqEndpointType.bind, ENDPOINT)
+
+ self._conn = _DispatcherREPConnection(zf, e, self._core)
+ reactor.callWhenRunning(self._conn.do_greet)
+ service.Service.startService(self)
+
+ def stopService(self):
+ service.Service.stopService(self)
+
+
+class _DispatcherREPConnection(ZmqREPConnection):
+
+ def __init__(self, zf, e, core):
+ ZmqREPConnection.__init__(self, zf, e)
+ self.dispatcher = CommandDispatcher(core)
+
+ def gotMessage(self, msgId, *parts):
+
+ r = self.dispatcher.dispatch(parts)
+ r.addCallback(self.defer_reply, msgId)
+
+ def defer_reply(self, response, msgId):
+ reactor.callLater(0, self.reply, msgId, str(response))
+
+ def log_err(self, failure, msgId):
+ log.err(failure)
+ self.defer_reply("ERROR: %r" % failure, msgId)
+
+ def do_greet(self):
+ log.msg('starting ZMQ dispatcher')
diff --git a/src/leap/bitmask/core/api.py b/src/leap/bitmask/core/api.py
new file mode 100644
index 00000000..9f3725dc
--- /dev/null
+++ b/src/leap/bitmask/core/api.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# api.py
+# Copyright (C) 2016 LEAP Encryption Acess Project
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Registry for the public API for the Bitmask Backend.
+"""
+from collections import OrderedDict
+
+registry = OrderedDict()
+
+
+class APICommand(type):
+ """
+ A metaclass to keep a global registry of all the methods that compose the
+ public API for the Bitmask Backend.
+ """
+ def __init__(cls, name, bases, attrs):
+ for key, val in attrs.iteritems():
+ properties = getattr(val, 'register', None)
+ label = getattr(cls, 'label', None)
+ if label:
+ name = label
+ if properties is not None:
+ registry['%s.%s' % (name, key)] = properties
+
+
+def register_method(*args):
+ """
+ This method gathers info about all the methods that are supposed to
+ compose the public API to communicate with the backend.
+
+ It sets up a register property for any method that uses it.
+ A type annotation is supposed to be in this property.
+ The APICommand metaclass collects these properties of the methods and
+ stores them in the global api_registry object, where they can be
+ introspected at runtime.
+ """
+ def decorator(f):
+ f.register = tuple(args)
+ return f
+ return decorator
diff --git a/src/leap/bitmask/core/bitmaskd.tac b/src/leap/bitmask/core/bitmaskd.tac
new file mode 100644
index 00000000..3c9b1d8b
--- /dev/null
+++ b/src/leap/bitmask/core/bitmaskd.tac
@@ -0,0 +1,11 @@
+# Service composition for bitmask-core.
+# Run as: twistd -n -y bitmaskd.tac
+#
+from twisted.application import service
+
+from leap.bitmask.core.service import BitmaskBackend
+
+
+bb = BitmaskBackend()
+application = service.Application("bitmaskd")
+bb.setServiceParent(application)
diff --git a/src/leap/bitmask/core/configurable.py b/src/leap/bitmask/core/configurable.py
new file mode 100644
index 00000000..8e33de95
--- /dev/null
+++ b/src/leap/bitmask/core/configurable.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+# configurable.py
+# Copyright (C) 2015, 2016 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Configurable Backend for Bitmask Service.
+"""
+import ConfigParser
+import os
+
+from twisted.application import service
+
+from leap.common import files
+from leap.common.config import get_path_prefix
+
+
+DEFAULT_BASEDIR = os.path.join(get_path_prefix(), 'leap')
+
+
+class MissingConfigEntry(Exception):
+ """
+ A required config entry was not found.
+ """
+
+
+class ConfigurableService(service.MultiService):
+
+ config_file = u"bitmaskd.cfg"
+ service_names = ('mail', 'eip', 'zmq', 'web')
+
+ def __init__(self, basedir=DEFAULT_BASEDIR):
+ service.MultiService.__init__(self)
+
+ path = os.path.abspath(os.path.expanduser(basedir))
+ if not os.path.isdir(path):
+ files.mkdir_p(path)
+ self.basedir = path
+
+ # creates self.config
+ self.read_config()
+
+ def get_config(self, section, option, default=None, boolean=False):
+ try:
+ if boolean:
+ return self.config.getboolean(section, option)
+
+ item = self.config.get(section, option)
+ return item
+
+ except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
+ if default is None:
+ fn = self._get_config_path()
+ raise MissingConfigEntry("%s is missing the [%s]%s entry"
+ % fn, section, option)
+ return default
+
+ def set_config(self, section, option, value):
+ if not self.config.has_section(section):
+ self.config.add_section(section)
+ self.config.set(section, option, value)
+ self.save_config()
+ self.read_config()
+ assert self.config.get(section, option) == value
+
+ def read_config(self):
+ self.config = ConfigParser.SafeConfigParser()
+ bitmaskd_cfg = self._get_config_path()
+
+ if not os.path.isfile(bitmaskd_cfg):
+ self._create_default_config(bitmaskd_cfg)
+
+ with open(bitmaskd_cfg, "rb") as f:
+ self.config.readfp(f)
+
+ def save_config(self):
+ bitmaskd_cfg = self._get_config_path()
+ with open(bitmaskd_cfg, 'wb') as f:
+ self.config.write(f)
+
+ def _create_default_config(self, path):
+ with open(path, 'w') as outf:
+ outf.write(DEFAULT_CONFIG)
+
+ def _get_config_path(self):
+ return os.path.join(self.basedir, self.config_file)
+
+
+DEFAULT_CONFIG = """
+[services]
+mail = True
+eip = True
+zmq = True
+web = False
+"""
diff --git a/src/leap/bitmask/core/dispatcher.py b/src/leap/bitmask/core/dispatcher.py
new file mode 100644
index 00000000..e7c961fd
--- /dev/null
+++ b/src/leap/bitmask/core/dispatcher.py
@@ -0,0 +1,273 @@
+# -*- coding: utf-8 -*-
+# dispatcher.py
+# Copyright (C) 2016 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Command dispatcher.
+"""
+import json
+
+from twisted.internet import defer
+from twisted.python import failure, log
+
+from .api import APICommand, register_method
+
+
+class SubCommand(object):
+
+ __metaclass__ = APICommand
+
+ def dispatch(self, service, *parts, **kw):
+ subcmd = parts[1]
+
+ _method = getattr(self, 'do_' + subcmd.upper(), None)
+ if not _method:
+ raise RuntimeError('No such subcommand')
+ return _method(service, *parts, **kw)
+
+
+class UserCmd(SubCommand):
+
+ label = 'user'
+
+ @register_method("{'srp_token': unicode, 'uuid': unicode}")
+ def do_AUTHENTICATE(self, bonafide, *parts):
+ user, password = parts[2], parts[3]
+ d = defer.maybeDeferred(bonafide.do_authenticate, user, password)
+ return d
+
+ @register_method("{'signup': 'ok', 'user': str}")
+ def do_SIGNUP(self, bonafide, *parts):
+ user, password = parts[2], parts[3]
+ d = defer.maybeDeferred(bonafide.do_signup, user, password)
+ return d
+
+ @register_method("{'logout': 'ok'}")
+ def do_LOGOUT(self, bonafide, *parts):
+ user, password = parts[2], parts[3]
+ d = defer.maybeDeferred(bonafide.do_logout, user, password)
+ return d
+
+ @register_method('str')
+ def do_ACTIVE(self, bonafide, *parts):
+ d = defer.maybeDeferred(bonafide.do_get_active_user)
+ return d
+
+
+class EIPCmd(SubCommand):
+
+ label = 'eip'
+
+ @register_method('dict')
+ def do_ENABLE(self, service, *parts):
+ d = service.do_enable_service(self.label)
+ return d
+
+ @register_method('dict')
+ def do_DISABLE(self, service, *parts):
+ d = service.do_disable_service(self.label)
+ return d
+
+ @register_method('dict')
+ def do_STATUS(self, eip, *parts):
+ d = eip.do_status()
+ return d
+
+ @register_method('dict')
+ def do_START(self, eip, *parts):
+ # TODO --- attempt to get active provider
+ # TODO or catch the exception and send error
+ provider = parts[2]
+ d = eip.do_start(provider)
+ return d
+
+ @register_method('dict')
+ def do_STOP(self, eip, *parts):
+ d = eip.do_stop()
+ return d
+
+
+class MailCmd(SubCommand):
+
+ label = 'mail'
+
+ @register_method('dict')
+ def do_ENABLE(self, service, *parts):
+ d = service.do_enable_service(self.label)
+ return d
+
+ @register_method('dict')
+ def do_DISABLE(self, service, *parts):
+ d = service.do_disable_service(self.label)
+ return d
+
+ @register_method('dict')
+ def do_STATUS(self, mail, *parts):
+ d = mail.do_status()
+ return d
+
+ @register_method('dict')
+ def do_GET_IMAP_TOKEN(self, mail, *parts):
+ d = mail.get_imap_token()
+ return d
+
+ @register_method('dict')
+ def do_GET_SMTP_TOKEN(self, mail, *parts):
+ d = mail.get_smtp_token()
+ return d
+
+ @register_method('dict')
+ def do_GET_SMTP_CERTIFICATE(self, mail, *parts, **kw):
+ # TODO move to mail service
+ # TODO should ask for confirmation? like --force or something,
+ # if we already have a valid one. or better just refuse if cert
+ # exists.
+ # TODO how should we pass the userid??
+ # - Keep an 'active' user in bonafide (last authenticated)
+ # (doing it now)
+ # - Get active user from Mail Service (maybe preferred?)
+ # - Have a command/method to set 'active' user.
+
+ @defer.inlineCallbacks
+ def save_cert(cert_data):
+ userid, cert_str = cert_data
+ cert_path = yield mail.do_get_smtp_cert_path(userid)
+ with open(cert_path, 'w') as outf:
+ outf.write(cert_str)
+ defer.returnValue('certificate saved to %s' % cert_path)
+
+ bonafide = kw['bonafide']
+ d = bonafide.do_get_smtp_cert()
+ d.addCallback(save_cert)
+ return d
+
+
+class CommandDispatcher(object):
+
+ __metaclass__ = APICommand
+
+ label = 'core'
+
+ def __init__(self, core):
+
+ self.core = core
+ self.subcommand_user = UserCmd()
+ self.subcommand_eip = EIPCmd()
+ self.subcommand_mail = MailCmd()
+
+ # XXX --------------------------------------------
+ # TODO move general services to another subclass
+
+ @register_method("{'mem_usage': str}")
+ def do_STATS(self, *parts):
+ return _format_result(self.core.do_stats())
+
+ @register_method("{version_core': '0.0.0'}")
+ def do_VERSION(self, *parts):
+ return _format_result(self.core.do_version())
+
+ @register_method("{'mail': 'running'}")
+ def do_STATUS(self, *parts):
+ return _format_result(self.core.do_status())
+
+ @register_method("{'shutdown': 'ok'}")
+ def do_SHUTDOWN(self, *parts):
+ return _format_result(self.core.do_shutdown())
+
+ # -----------------------------------------------
+
+ def do_USER(self, *parts):
+ bonafide = self._get_service('bonafide')
+ d = self.subcommand_user.dispatch(bonafide, *parts)
+ d.addCallbacks(_format_result, _format_error)
+ return d
+
+ def do_EIP(self, *parts):
+ eip = self._get_service(self.subcommand_eip.label)
+ if not eip:
+ return _format_result('eip: disabled')
+ subcmd = parts[1]
+
+ dispatch = self._subcommand_eip.dispatch
+ if subcmd in ('enable', 'disable'):
+ d = dispatch(self.core, *parts)
+ else:
+ d = dispatch(eip, *parts)
+
+ d.addCallbacks(_format_result, _format_error)
+ return d
+
+ def do_MAIL(self, *parts):
+ subcmd = parts[1]
+ dispatch = self.subcommand_mail.dispatch
+
+ if subcmd == 'enable':
+ d = dispatch(self.core, *parts)
+
+ mail = self._get_service(self.subcommand_mail.label)
+ bonafide = self._get_service('bonafide')
+ kw = {'bonafide': bonafide}
+
+ if not mail:
+ return _format_result('mail: disabled')
+
+ if subcmd == 'disable':
+ d = dispatch(self.core)
+ else:
+ d = dispatch(mail, *parts, **kw)
+
+ d.addCallbacks(_format_result, _format_error)
+ return d
+
+ def do_KEYS(self, *parts):
+ subcmd = parts[1]
+
+ keymanager_label = 'keymanager'
+ km = self._get_service(keymanager_label)
+ bf = self._get_service('bonafide')
+
+ if not km:
+ return _format_result('keymanager: disabled')
+
+ if subcmd == 'list_keys':
+ d = bf.do_get_active_user()
+ d.addCallback(km.do_list_keys)
+ d.addCallbacks(_format_result, _format_error)
+ return d
+
+ def dispatch(self, msg):
+ cmd = msg[0]
+
+ _method = getattr(self, 'do_' + cmd.upper(), None)
+
+ if not _method:
+ return defer.fail(failure.Failure(RuntimeError('No such command')))
+
+ return defer.maybeDeferred(_method, *msg)
+
+ def _get_service(self, name):
+ try:
+ return self.core.getServiceNamed(name)
+ except KeyError:
+ return None
+
+
+def _format_result(result):
+ return json.dumps({'error': None, 'result': result})
+
+
+def _format_error(failure):
+ log.err(failure)
+ return json.dumps({'error': failure.value.message, 'result': None})
diff --git a/src/leap/bitmask/core/dummy.py b/src/leap/bitmask/core/dummy.py
new file mode 100644
index 00000000..99dfafa5
--- /dev/null
+++ b/src/leap/bitmask/core/dummy.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+# dummy.py
+# Copyright (C) 2016 LEAP Encryption Acess Project
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+An authoritative dummy backend for tests.
+"""
+import json
+
+from leap.common.service_hooks import HookableService
+
+
+class BackendCommands(object):
+
+ """
+ General commands for the BitmaskBackend Core Service.
+ """
+
+ def __init__(self, core):
+ self.core = core
+
+ def do_status(self):
+ return json.dumps(
+ {'soledad': 'running',
+ 'keymanager': 'running',
+ 'mail': 'running',
+ 'eip': 'stopped',
+ 'backend': 'dummy'})
+
+ def do_version(self):
+ return {'version_core': '0.0.1'}
+
+ def do_stats(self):
+ return {'mem_usage': '01 KB'}
+
+ def do_shutdown(self):
+ return {'shutdown': 'ok'}
+
+
+class mail_services(object):
+
+ class SoledadService(HookableService):
+ pass
+
+ class KeymanagerService(HookableService):
+ pass
+
+ class StandardMailService(HookableService):
+ pass
+
+
+class BonafideService(HookableService):
+
+ def __init__(self, basedir):
+ pass
+
+ def do_authenticate(self, user, password):
+ return {u'srp_token': u'deadbeef123456789012345678901234567890123',
+ u'uuid': u'01234567890abcde01234567890abcde'}
+
+ def do_signup(self, user, password):
+ return {'signup': 'ok', 'user': 'dummyuser@provider.example.org'}
+
+ def do_logout(self, user, password):
+ return {'logout': 'ok'}
+
+ def do_get_active_user(self):
+ return 'dummyuser@provider.example.org'
diff --git a/src/leap/bitmask/core/flags.py b/src/leap/bitmask/core/flags.py
new file mode 100644
index 00000000..9a40c70c
--- /dev/null
+++ b/src/leap/bitmask/core/flags.py
@@ -0,0 +1 @@
+BACKEND = 'default'
diff --git a/src/leap/bitmask/core/launcher.py b/src/leap/bitmask/core/launcher.py
new file mode 100644
index 00000000..b8916a1e
--- /dev/null
+++ b/src/leap/bitmask/core/launcher.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# launcher.py
+# Copyright (C) 2016 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Run bitmask daemon.
+"""
+from os.path import join
+from sys import argv
+
+from twisted.scripts.twistd import run
+
+from leap.bitmask.util import here
+from leap.bitmask import core
+from leap.bitmask.core import flags
+
+
+def run_bitmaskd():
+ # TODO --- configure where to put the logs... (get --logfile, --logdir
+ # from the bitmask_cli
+ for (index, arg) in enumerate(argv):
+ if arg == '--backend':
+ flags.BACKEND = argv[index + 1]
+ argv[1:] = [
+ '-y', join(here(core), "bitmaskd.tac"),
+ '--pidfile', '/tmp/bitmaskd.pid',
+ '--logfile', '/tmp/bitmaskd.log',
+ '--umask=0022',
+ ]
+ print '[+] launching bitmaskd...'
+ run()
+
+
+if __name__ == "__main__":
+ run_bitmaskd()
diff --git a/src/leap/bitmask/core/mail_services.py b/src/leap/bitmask/core/mail_services.py
new file mode 100644
index 00000000..fb9ee698
--- /dev/null
+++ b/src/leap/bitmask/core/mail_services.py
@@ -0,0 +1,688 @@
+# -*- coding: utf-8 -*-
+# mail_services.py
+# Copyright (C) 2016 LEAP Encryption Acess Project
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Mail services.
+
+This is quite moving work still.
+This should be moved to the different packages when it stabilizes.
+"""
+import json
+import os
+from collections import defaultdict
+from collections import namedtuple
+
+from twisted.application import service
+from twisted.internet import defer
+from twisted.python import log
+
+from leap.bonafide import config
+from leap.common.service_hooks import HookableService
+from leap.keymanager import KeyManager, openpgp
+from leap.keymanager.errors import KeyNotFound
+from leap.soledad.client.api import Soledad
+from leap.mail.constants import INBOX_NAME
+from leap.mail.mail import Account
+from leap.mail.imap.service import imap
+from leap.mail.incoming.service import IncomingMail, INCOMING_CHECK_PERIOD
+from leap.mail import smtp
+
+from leap.bitmask.core.uuid_map import UserMap
+from leap.bitmask.core.configurable import DEFAULT_BASEDIR
+
+
+class Container(object):
+
+ def __init__(self, service=None):
+ self._instances = defaultdict(None)
+ if service is not None:
+ self.service = service
+
+ def get_instance(self, key):
+ return self._instances.get(key, None)
+
+ def add_instance(self, key, data):
+ self._instances[key] = data
+
+
+class ImproperlyConfigured(Exception):
+ pass
+
+
+class SoledadContainer(Container):
+
+ def __init__(self, service=None, basedir=DEFAULT_BASEDIR):
+ self._basedir = os.path.expanduser(basedir)
+ self._usermap = UserMap()
+ super(SoledadContainer, self).__init__(service=service)
+
+ def add_instance(self, userid, passphrase, uuid=None, token=None):
+
+ if not uuid:
+ bootstrapped_uuid = self._usermap.lookup_uuid(userid, passphrase)
+ uuid = bootstrapped_uuid
+ if not uuid:
+ return
+ else:
+ self._usermap.add(userid, uuid, passphrase)
+
+ user, provider = userid.split('@')
+
+ soledad_path = os.path.join(self._basedir, 'soledad')
+ soledad_url = _get_soledad_uri(self._basedir, provider)
+ cert_path = _get_ca_cert_path(self._basedir, provider)
+
+ soledad = self._create_soledad_instance(
+ uuid, passphrase, soledad_path, soledad_url,
+ cert_path, token)
+
+ super(SoledadContainer, self).add_instance(userid, soledad)
+
+ data = {'user': userid, 'uuid': uuid, 'token': token,
+ 'soledad': soledad}
+ self.service.trigger_hook('on_new_soledad_instance', **data)
+
+ def _create_soledad_instance(self, uuid, passphrase, soledad_path,
+ server_url, cert_file, token):
+ # setup soledad info
+ secrets_path = os.path.join(soledad_path, '%s.secret' % uuid)
+ local_db_path = os.path.join(soledad_path, '%s.db' % uuid)
+
+ if token is None:
+ syncable = False
+ token = ''
+ else:
+ syncable = True
+
+ return Soledad(
+ uuid,
+ unicode(passphrase),
+ secrets_path=secrets_path,
+ local_db_path=local_db_path,
+ server_url=server_url,
+ cert_file=cert_file,
+ auth_token=token,
+ defer_encryption=True,
+ syncable=syncable)
+
+ def set_remote_auth_token(self, userid, token):
+ self.get_instance(userid).token = token
+
+ def set_syncable(self, userid, state):
+ # TODO should check that there's a token!
+ self.get_instance(userid).set_syncable(bool(state))
+
+ def sync(self, userid):
+ self.get_instance(userid).sync()
+
+
+def _get_provider_from_full_userid(userid):
+ _, provider_id = config.get_username_and_provider(userid)
+ return config.Provider(provider_id)
+
+
+def is_service_ready(service, provider):
+ """
+ Returns True when the following conditions are met:
+ - Provider offers that service.
+ - We have the config files for the service.
+ - The service is enabled.
+ """
+ has_service = provider.offers_service(service)
+ has_config = provider.has_config_for_service(service)
+ is_enabled = provider.is_service_enabled(service)
+ return has_service and has_config and is_enabled
+
+
+class SoledadService(HookableService):
+
+ def __init__(self, basedir):
+ service.Service.__init__(self)
+ self._basedir = basedir
+
+ def startService(self):
+ log.msg('Starting Soledad Service')
+ self._container = SoledadContainer(service=self)
+ super(SoledadService, self).startService()
+
+ # hooks
+
+ def hook_on_passphrase_entry(self, **kw):
+ userid = kw.get('username')
+ provider = _get_provider_from_full_userid(userid)
+ provider.callWhenReady(self._hook_on_passphrase_entry, provider, **kw)
+
+ def _hook_on_passphrase_entry(self, provider, **kw):
+ if is_service_ready('mx', provider):
+ userid = kw.get('username')
+ password = kw.get('password')
+ uuid = kw.get('uuid')
+ container = self._container
+ log.msg("on_passphrase_entry: New Soledad Instance: %s" % userid)
+ if not container.get_instance(userid):
+ container.add_instance(userid, password, uuid=uuid, token=None)
+ else:
+ log.msg('Service MX is not ready...')
+
+ def hook_on_bonafide_auth(self, **kw):
+ userid = kw['username']
+ provider = _get_provider_from_full_userid(userid)
+ provider.callWhenReady(self._hook_on_bonafide_auth, provider, **kw)
+
+ def _hook_on_bonafide_auth(self, provider, **kw):
+ if provider.offers_service('mx'):
+ userid = kw['username']
+ password = kw['password']
+ token = kw['token']
+ uuid = kw['uuid']
+
+ container = self._container
+ if container.get_instance(userid):
+ log.msg("Passing a new SRP Token to Soledad: %s" % userid)
+ container.set_remote_auth_token(userid, token)
+ container.set_syncable(userid, True)
+ else:
+ log.msg("Adding a new Soledad Instance: %s" % userid)
+ container.add_instance(
+ userid, password, uuid=uuid, token=token)
+
+
+class KeymanagerContainer(Container):
+
+ def __init__(self, service=None, basedir=DEFAULT_BASEDIR):
+ self._basedir = os.path.expanduser(basedir)
+ super(KeymanagerContainer, self).__init__(service=service)
+
+ def add_instance(self, userid, token, uuid, soledad):
+
+ keymanager = self._create_keymanager_instance(
+ userid, token, uuid, soledad)
+
+ d = self._get_or_generate_keys(keymanager, userid)
+ d.addCallback(self._on_keymanager_ready_cb, userid, soledad)
+ return d
+
+ def set_remote_auth_token(self, userid, token):
+ self.get_instance(userid)._token = token
+
+ def _on_keymanager_ready_cb(self, keymanager, userid, soledad):
+ # TODO use onready-deferreds instead
+ self.add_instance(userid, keymanager)
+
+ log.msg("Adding Keymanager instance for: %s" % userid)
+ data = {'userid': userid, 'soledad': soledad, 'keymanager': keymanager}
+ self.service.trigger_hook('on_new_keymanager_instance', **data)
+
+ def _get_or_generate_keys(self, keymanager, userid):
+
+ def if_not_found_generate(failure):
+ # TODO -------------- should ONLY generate if INITIAL_SYNC_DONE.
+ # ie: put callback on_soledad_first_sync_ready -----------------
+ # --------------------------------------------------------------
+ failure.trap(KeyNotFound)
+ log.msg("Core: Key not found. Generating key for %s" % (userid,))
+ d = keymanager.gen_key(openpgp.OpenPGPKey)
+ d.addCallbacks(send_key, log_key_error("generating"))
+ return d
+
+ def send_key(ignored):
+ # ----------------------------------------------------------------
+ # It might be the case that we have generated a key-pair
+ # but this hasn't been successfully uploaded. How do we know that?
+ # XXX Should this be a method of bonafide instead?
+ # -----------------------------------------------------------------
+ d = keymanager.send_key(openpgp.OpenPGPKey)
+ d.addCallbacks(
+ lambda _: log.msg(
+ "Key generated successfully for %s" % userid),
+ log_key_error("sending"))
+ return d
+
+ def log_key_error(step):
+ def log_error(failure):
+ log.err("Error while %s key!" % step)
+ log.err(failure)
+ return failure
+ return log_error
+
+ d = keymanager.get_key(
+ userid, openpgp.OpenPGPKey, private=True, fetch_remote=False)
+ d.addErrback(if_not_found_generate)
+ d.addCallback(lambda _: keymanager)
+ return d
+
+ def _create_keymanager_instance(self, userid, token, uuid, soledad):
+ user, provider = userid.split('@')
+ nickserver_uri = self._get_nicknym_uri(provider)
+
+ cert_path = _get_ca_cert_path(self._basedir, provider)
+ api_uri = self._get_api_uri(provider)
+
+ if not token:
+ token = self.service.tokens.get(userid)
+
+ km_args = (userid, nickserver_uri, soledad)
+
+ # TODO use the method in
+ # services.soledadbootstrapper._get_gpg_bin_path.
+ # That should probably live in keymanager package.
+
+ km_kwargs = {
+ "token": token, "uid": uuid,
+ "api_uri": api_uri, "api_version": "1",
+ "ca_cert_path": cert_path,
+ "gpgbinary": "/usr/bin/gpg"
+ }
+ keymanager = KeyManager(*km_args, **km_kwargs)
+ return keymanager
+
+ def _get_api_uri(self, provider):
+ # TODO get this from service.json (use bonafide service)
+ api_uri = "https://api.{provider}:4430".format(
+ provider=provider)
+ return api_uri
+
+ def _get_nicknym_uri(self, provider):
+ return 'https://nicknym.{provider}:6425'.format(
+ provider=provider)
+
+
+class KeymanagerService(HookableService):
+
+ def __init__(self, basedir=DEFAULT_BASEDIR):
+ service.Service.__init__(self)
+ self._basedir = basedir
+
+ def startService(self):
+ log.msg('Starting Keymanager Service')
+ self._container = KeymanagerContainer(self._basedir)
+ self._container.service = self
+ self.tokens = {}
+ super(KeymanagerService, self).startService()
+
+ # hooks
+
+ def hook_on_new_soledad_instance(self, **kw):
+ container = self._container
+ user = kw['user']
+ token = kw['token']
+ uuid = kw['uuid']
+ soledad = kw['soledad']
+ if not container.get_instance(user):
+ log.msg('Adding a new Keymanager instance for %s' % user)
+ if not token:
+ token = self.tokens.get(user)
+ container.add_instance(user, token, uuid, soledad)
+
+ def hook_on_bonafide_auth(self, **kw):
+ userid = kw['username']
+ provider = _get_provider_from_full_userid(userid)
+ provider.callWhenReady(self._hook_on_bonafide_auth, provider, **kw)
+
+ def _hook_on_bonafide_auth(self, provider, **kw):
+ if provider.offers_service('mx'):
+ userid = kw['username']
+ token = kw['token']
+
+ container = self._container
+ if container.get_instance(userid):
+ log.msg('Passing a new SRP Token to Keymanager: %s' % userid)
+ container.set_remote_auth_token(userid, token)
+ else:
+ log.msg('storing the keymanager token... %s ' % token)
+ self.tokens[userid] = token
+
+ # commands
+
+ def do_list_keys(self, userid):
+ km = self._container.get_instance(userid)
+ d = km.get_all_keys()
+ d.addCallback(
+ lambda keys: [
+ (key.uids, key.fingerprint) for key in keys])
+ return d
+
+
+class StandardMailService(service.MultiService, HookableService):
+ """
+ A collection of Services.
+
+ This is the parent service, that launches 3 different services that expose
+ Encrypted Mail Capabilities on specific ports:
+
+ - SMTP service, on port 2013
+ - IMAP service, on port 1984
+ - The IncomingMail Service, which doesn't listen on any port, but
+ watches and processes the Incoming Queue and saves the processed mail
+ into the matching INBOX.
+ """
+
+ name = 'mail'
+
+ # TODO factor out Mail Service to inside mail package.
+
+ subscribed_to_hooks = ('on_new_keymanager_instance',)
+
+ def __init__(self, basedir):
+ self._basedir = basedir
+ self._soledad_sessions = {}
+ self._keymanager_sessions = {}
+ self._sendmail_opts = {}
+ self._imap_tokens = {}
+ self._smtp_tokens = {}
+ self._active_user = None
+ super(StandardMailService, self).__init__()
+ self.initializeChildrenServices()
+
+ def initializeChildrenServices(self):
+ self.addService(IMAPService(self._soledad_sessions))
+ self.addService(SMTPService(
+ self._soledad_sessions, self._keymanager_sessions,
+ self._sendmail_opts))
+ # TODO adapt the service to receive soledad/keymanager sessions object.
+ # See also the TODO before IncomingMailService.startInstance
+ self.addService(IncomingMailService(self))
+
+ def startService(self):
+ log.msg('Starting Mail Service...')
+ super(StandardMailService, self).startService()
+
+ def stopService(self):
+ super(StandardMailService, self).stopService()
+
+ def startInstance(self, userid, soledad, keymanager):
+ username, provider = userid.split('@')
+
+ self._soledad_sessions[userid] = soledad
+ self._keymanager_sessions[userid] = keymanager
+
+ sendmail_opts = _get_sendmail_opts(self._basedir, provider, username)
+ self._sendmail_opts[userid] = sendmail_opts
+
+ incoming = self.getServiceNamed('incoming_mail')
+ incoming.startInstance(userid)
+
+ def registerIMAPToken(token):
+ self._imap_tokens[userid] = token
+ self._active_user = userid
+ return token
+
+ def registerSMTPToken(token):
+ self._smtp_tokens[userid] = token
+ return token
+
+ d = soledad.get_or_create_service_token('imap')
+ d.addCallback(registerIMAPToken)
+ d.addCallback(
+ lambda _: soledad.get_or_create_service_token('smtp'))
+ d.addCallback(registerSMTPToken)
+ return d
+
+ def stopInstance(self):
+ pass
+
+ # hooks
+
+ def hook_on_new_keymanager_instance(self, **kw):
+ # XXX we can specify this as a waterfall, or just AND the two
+ # conditions.
+ userid = kw['userid']
+ soledad = kw['soledad']
+ keymanager = kw['keymanager']
+
+ # TODO --- only start instance if "autostart" is True.
+ self.startInstance(userid, soledad, keymanager)
+
+ # commands
+
+ def do_status(self):
+ return 'mail: %s' % 'running' if self.running else 'disabled'
+
+ def get_imap_token(self):
+ active_user = self._active_user
+ if not active_user:
+ return defer.succeed('NO ACTIVE USER')
+ token = self._imap_tokens.get(active_user)
+ # TODO return just the tuple, no format.
+ return defer.succeed("IMAP TOKEN (%s): %s" % (active_user, token))
+
+ def get_smtp_token(self):
+ active_user = self._active_user
+ if not active_user:
+ return defer.succeed('NO ACTIVE USER')
+ token = self._smtp_tokens.get(active_user)
+ # TODO return just the tuple, no format.
+ return defer.succeed("SMTP TOKEN (%s): %s" % (active_user, token))
+
+ def do_get_smtp_cert_path(self, userid):
+ username, provider = userid.split('@')
+ return _get_smtp_client_cert_path(self._basedir, provider, username)
+
+ # access to containers
+
+ def get_soledad_session(self, userid):
+ return self._soledad_sessions.get(userid)
+
+ def get_keymanager_session(self, userid):
+ return self._keymanager_sessions.get(userid)
+
+
+class IMAPService(service.Service):
+
+ name = 'imap'
+
+ def __init__(self, soledad_sessions):
+ port, factory = imap.run_service(soledad_sessions)
+
+ self._port = port
+ self._factory = factory
+ self._soledad_sessions = soledad_sessions
+ super(IMAPService, self).__init__()
+
+ def startService(self):
+ log.msg('Starting IMAP Service')
+ super(IMAPService, self).startService()
+
+ def stopService(self):
+ self._port.stopListening()
+ self._factory.doStop()
+ super(IMAPService, self).stopService()
+
+
+class SMTPService(service.Service):
+
+ name = 'smtp'
+
+ def __init__(self, soledad_sessions, keymanager_sessions, sendmail_opts,
+ basedir=DEFAULT_BASEDIR):
+
+ self._basedir = os.path.expanduser(basedir)
+ port, factory = smtp.run_service(
+ soledad_sessions, keymanager_sessions, sendmail_opts)
+ self._port = port
+ self._factory = factory
+ self._soledad_sessions = soledad_sessions
+ self._keymanager_sessions = keymanager_sessions
+ self._sendmail_opts = sendmail_opts
+ super(SMTPService, self).__init__()
+
+ def startService(self):
+ log.msg('Starting SMTP Service')
+ super(SMTPService, self).startService()
+
+ def stopService(self):
+ # TODO cleanup all instances
+ super(SMTPService, self).stopService()
+
+
+class IncomingMailService(service.Service):
+
+ name = 'incoming_mail'
+
+ def __init__(self, mail_service):
+ super(IncomingMailService, self).__init__()
+ self._mail = mail_service
+ self._instances = {}
+
+ def startService(self):
+ log.msg('Starting IncomingMail Service')
+ super(IncomingMailService, self).startService()
+
+ def stopService(self):
+ super(IncomingMailService, self).stopService()
+
+ # Individual accounts
+
+ # TODO IncomingMail *IS* already a service.
+ # I think we should better model the current Service
+ # as a startInstance inside a container, and get this
+ # multi-tenant service inside the leap.mail.incoming.service.
+ # ... or just simply make it a multiService and set per-user
+ # instances as Child of this parent.
+
+ def startInstance(self, userid):
+ soledad = self._mail.get_soledad_session(userid)
+ keymanager = self._mail.get_keymanager_session(userid)
+
+ log.msg('Starting Incoming Mail instance for %s' % userid)
+ self._start_incoming_mail_instance(
+ keymanager, soledad, userid)
+
+ def stopInstance(self, userid):
+ # TODO toggle offline!
+ pass
+
+ def _start_incoming_mail_instance(self, keymanager, soledad,
+ userid, start_sync=True):
+
+ def setUpIncomingMail(inbox):
+ incoming_mail = IncomingMail(
+ keymanager, soledad,
+ inbox, userid,
+ check_period=INCOMING_CHECK_PERIOD)
+ return incoming_mail
+
+ def registerInstance(incoming_instance):
+ self._instances[userid] = incoming_instance
+ if start_sync:
+ incoming_instance.startService()
+
+ acc = Account(soledad, userid)
+ d = acc.callWhenReady(
+ lambda _: acc.get_collection_by_mailbox(INBOX_NAME))
+ d.addCallback(setUpIncomingMail)
+ d.addCallback(registerInstance)
+ d.addErrback(log.err)
+ return d
+
+# --------------------------------------------------------------------
+#
+# config utilities. should be moved to bonafide
+#
+
+SERVICES = ('soledad', 'smtp', 'eip')
+
+
+Provider = namedtuple(
+ 'Provider', ['hostname', 'ip_address', 'location', 'port'])
+
+SendmailOpts = namedtuple(
+ 'SendmailOpts', ['cert', 'key', 'hostname', 'port'])
+
+
+def _get_ca_cert_path(basedir, provider):
+ path = os.path.join(
+ basedir, 'providers', provider, 'keys', 'ca', 'cacert.pem')
+ return path
+
+
+def _get_sendmail_opts(basedir, provider, username):
+ cert = _get_smtp_client_cert_path(basedir, provider, username)
+ key = cert
+ prov = _get_provider_for_service('smtp', basedir, provider)
+ hostname = prov.hostname
+ port = prov.port
+ opts = SendmailOpts(cert, key, hostname, port)
+ return opts
+
+
+def _get_smtp_client_cert_path(basedir, provider, username):
+ path = os.path.join(
+ basedir, 'providers', provider, 'keys', 'client', 'stmp_%s.pem' %
+ username)
+ return path
+
+
+def _get_config_for_service(service, basedir, provider):
+ if service not in SERVICES:
+ raise ImproperlyConfigured('Tried to use an unknown service')
+
+ config_path = os.path.join(
+ basedir, 'providers', provider, '%s-service.json' % service)
+ try:
+ with open(config_path) as config:
+ config = json.loads(config.read())
+ except IOError:
+ # FIXME might be that the provider DOES NOT offer this service!
+ raise ImproperlyConfigured(
+ 'could not open config file %s' % config_path)
+ else:
+ return config
+
+
+def first(xs):
+ return xs[0]
+
+
+def _pick_server(config, strategy=first):
+ """
+ Picks a server from a list of possible choices.
+ The service files have a <describe>.
+ This implementation just picks the FIRST available server.
+ """
+ servers = config['hosts'].keys()
+ choice = config['hosts'][strategy(servers)]
+ return choice
+
+
+def _get_subdict(d, keys):
+ return {key: d.get(key) for key in keys}
+
+
+def _get_provider_for_service(service, basedir, provider):
+
+ if service not in SERVICES:
+ raise ImproperlyConfigured('Tried to use an unknown service')
+
+ config = _get_config_for_service(service, basedir, provider)
+ p = _pick_server(config)
+ attrs = _get_subdict(p, ('hostname', 'ip_address', 'location', 'port'))
+ provider = Provider(**attrs)
+ return provider
+
+
+def _get_smtp_uri(basedir, provider):
+ prov = _get_provider_for_service('smtp', basedir, provider)
+ url = 'https://{hostname}:{port}'.format(
+ hostname=prov.hostname, port=prov.port)
+ return url
+
+
+def _get_soledad_uri(basedir, provider):
+ prov = _get_provider_for_service('soledad', basedir, provider)
+ url = 'https://{hostname}:{port}'.format(
+ hostname=prov.hostname, port=prov.port)
+ return url
diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py
new file mode 100644
index 00000000..99132c2d
--- /dev/null
+++ b/src/leap/bitmask/core/service.py
@@ -0,0 +1,209 @@
+# -*- coding: utf-8 -*-
+# service.py
+# Copyright (C) 2015 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Bitmask-core Service.
+"""
+import json
+import resource
+
+from twisted.internet import reactor
+from twisted.python import log
+
+from leap.bitmask import __version__
+from leap.bitmask.core import configurable
+from leap.bitmask.core import _zmq
+from leap.bitmask.core import flags
+from leap.common.events import server as event_server
+# from leap.vpn import EIPService
+
+
+backend = flags.BACKEND
+
+if backend == 'default':
+ from leap.bitmask.core import mail_services
+ from leap.bonafide.service import BonafideService
+elif backend == 'dummy':
+ from leap.bitmask.core.dummy import mail_services
+ from leap.bitmask.core.dummy import BonafideService
+else:
+ raise RuntimeError('Backend not supported')
+
+
+class BitmaskBackend(configurable.ConfigurableService):
+
+ def __init__(self, basedir=configurable.DEFAULT_BASEDIR):
+
+ configurable.ConfigurableService.__init__(self, basedir)
+ self.core_commands = BackendCommands(self)
+
+ def enabled(service):
+ return self.get_config('services', service, False, boolean=True)
+
+ on_start = reactor.callWhenRunning
+
+ on_start(self.init_events)
+ on_start(self.init_bonafide)
+
+ if enabled('mail'):
+ on_start(self.init_soledad)
+ on_start(self.init_keymanager)
+ on_start(self.init_mail)
+
+ if enabled('eip'):
+ on_start(self.init_eip)
+
+ if enabled('zmq'):
+ on_start(self.init_zmq)
+
+ if enabled('web'):
+ on_start(self.init_web)
+
+ def init_events(self):
+ event_server.ensure_server()
+
+ def init_bonafide(self):
+ bf = BonafideService(self.basedir)
+ bf.setName("bonafide")
+ bf.setServiceParent(self)
+ # TODO ---- these hooks should be activated only if
+ # (1) we have enabled that service
+ # (2) provider offers this service
+ bf.register_hook('on_passphrase_entry', listener='soledad')
+ bf.register_hook('on_bonafide_auth', listener='soledad')
+ bf.register_hook('on_bonafide_auth', listener='keymanager')
+
+ def init_soledad(self):
+ service = mail_services.SoledadService
+ sol = self._maybe_start_service(
+ 'soledad', service, self.basedir)
+ if sol:
+ sol.register_hook(
+ 'on_new_soledad_instance', listener='keymanager')
+
+ def init_keymanager(self):
+ service = mail_services.KeymanagerService
+ km = self._maybe_start_service(
+ 'keymanager', service, self.basedir)
+ if km:
+ km.register_hook('on_new_keymanager_instance', listener='mail')
+
+ def init_mail(self):
+ service = mail_services.StandardMailService
+ self._maybe_start_service('mail', service, self.basedir)
+
+ def init_eip(self):
+ # FIXME -- land EIP into leap.vpn
+ pass
+ # self._maybe_start_service('eip', EIPService)
+
+ def init_zmq(self):
+ zs = _zmq.ZMQServerService(self)
+ zs.setServiceParent(self)
+
+ def init_web(self):
+ from leap.bitmask.core import websocket
+ ws = websocket.WebSocketsDispatcherService(self)
+ ws.setServiceParent(self)
+
+ def _maybe_start_service(self, label, klass, *args, **kw):
+ try:
+ self.getServiceNamed(label)
+ except KeyError:
+ service = klass(*args, **kw)
+ service.setName(label)
+ service.setServiceParent(self)
+ return service
+
+ def do_stats(self):
+ return self.core_commands.do_stats()
+
+ def do_status(self):
+ return self.core_commands.do_status()
+
+ def do_version(self):
+ return self.core_commands.do_version()
+
+ def do_shutdown(self):
+ return self.core_commands.do_shutdown()
+
+ # Service Toggling
+
+ def do_enable_service(self, service):
+ assert service in self.service_names
+ self.set_config('services', service, 'True')
+
+ if service == 'mail':
+ self.init_soledad()
+ self.init_keymanager()
+ self.init_mail()
+
+ elif service == 'eip':
+ self.init_eip()
+
+ elif service == 'zmq':
+ self.init_zmq()
+
+ elif service == 'web':
+ self.init_web()
+
+ return 'ok'
+
+ def do_disable_service(self, service):
+ assert service in self.service_names
+ # TODO -- should stop also?
+ self.set_config('services', service, 'False')
+ return 'ok'
+
+
+class BackendCommands(object):
+
+ """
+ General commands for the BitmaskBackend Core Service.
+ """
+
+ def __init__(self, core):
+ self.core = core
+
+ def do_status(self):
+ # we may want to make this tuple a class member
+ services = ('soledad', 'keymanager', 'mail', 'eip')
+
+ status = {}
+ for name in services:
+ _status = 'stopped'
+ try:
+ if self.core.getServiceNamed(name).running:
+ _status = 'running'
+ except KeyError:
+ pass
+ status[name] = _status
+ status['backend'] = flags.BACKEND
+
+ return json.dumps(status)
+
+ def do_version(self):
+ return {'version_core': __version__}
+
+ def do_stats(self):
+ log.msg('BitmaskCore Service STATS')
+ mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
+ return {'mem_usage': '%s KB' % (mem / 1024)}
+
+ def do_shutdown(self):
+ self.core.stopService()
+ reactor.callLater(1, reactor.stop)
+ return {'shutdown': 'ok'}
diff --git a/src/leap/bitmask/core/uuid_map.py b/src/leap/bitmask/core/uuid_map.py
new file mode 100644
index 00000000..5edc7216
--- /dev/null
+++ b/src/leap/bitmask/core/uuid_map.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+# uuid_map.py
+# Copyright (C) 2015,2016 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+UUID Map: a persistent mapping between user-ids and uuids.
+"""
+
+import base64
+import os
+import re
+
+import scrypt
+
+from leap.common.config import get_path_prefix
+
+
+MAP_PATH = os.path.join(get_path_prefix(), 'leap', 'uuids')
+
+
+class UserMap(object):
+
+ """
+ A persistent mapping between user-ids and uuids.
+ """
+
+ # TODO Add padding to the encrypted string
+
+ def __init__(self):
+ self._d = {}
+ self._lines = set([])
+ if os.path.isfile(MAP_PATH):
+ self.load()
+
+ def add(self, userid, uuid, passwd):
+ """
+ Add a new userid-uuid mapping, and encrypt the record with the user
+ password.
+ """
+ self._add_to_cache(userid, uuid)
+ self._lines.add(_encode_uuid_map(userid, uuid, passwd))
+ self.dump()
+
+ def _add_to_cache(self, userid, uuid):
+ self._d[userid] = uuid
+
+ def load(self):
+ """
+ Load a mapping from a default file.
+ """
+ with open(MAP_PATH, 'r') as infile:
+ lines = infile.readlines()
+ self._lines = set(lines)
+
+ def dump(self):
+ """
+ Dump the mapping to a default file.
+ """
+ with open(MAP_PATH, 'w') as out:
+ out.write('\n'.join(self._lines))
+
+ def lookup_uuid(self, userid, passwd=None):
+ """
+ Lookup the uuid for a given userid.
+
+ If no password is given, try to lookup on cache.
+ Else, try to decrypt all the records that we know about with the
+ passed password.
+ """
+ if not passwd:
+ return self._d.get(userid)
+
+ for line in self._lines:
+ guess = _decode_uuid_line(line, passwd)
+ if guess:
+ record_userid, uuid = guess
+ if record_userid == userid:
+ self._add_to_cache(userid, uuid)
+ return uuid
+
+ def lookup_userid(self, uuid):
+ """
+ Get the userid for the given uuid from cache.
+ """
+ rev_d = {v: k for (k, v) in self._d.items()}
+ return rev_d.get(uuid)
+
+
+def _encode_uuid_map(userid, uuid, passwd):
+ data = 'userid:%s:uuid:%s' % (userid, uuid)
+ encrypted = scrypt.encrypt(data, passwd, maxtime=0.05)
+ return base64.encodestring(encrypted).replace('\n', '')
+
+
+def _decode_uuid_line(line, passwd):
+ decoded = base64.decodestring(line)
+ try:
+ maybe_decrypted = scrypt.decrypt(decoded, passwd, maxtime=0.1)
+ except scrypt.error:
+ return None
+ match = re.findall("userid\:(.+)\:uuid\:(.+)", maybe_decrypted)
+ if match:
+ return match[0]
diff --git a/src/leap/bitmask/core/web/__init__.py b/src/leap/bitmask/core/web/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/leap/bitmask/core/web/__init__.py
diff --git a/src/leap/bitmask/core/web/index.html b/src/leap/bitmask/core/web/index.html
new file mode 100644
index 00000000..9490eca8
--- /dev/null
+++ b/src/leap/bitmask/core/web/index.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Bitmask WebSockets Endpoint</title>
+ <script type="text/javascript">
+ var sock = null;
+ var ellog = null;
+
+ window.onload = function() {
+
+ ellog = document.getElementById('log');
+
+ var wsuri;
+ if (window.location.protocol === "file:") {
+ wsuri = "ws://127.0.0.1:8080/ws";
+ } else {
+ wsuri = "ws://" + window.location.hostname + ":8080/bitmask";
+ }
+
+ if ("WebSocket" in window) {
+ sock = new WebSocket(wsuri);
+ } else if ("MozWebSocket" in window) {
+ sock = new MozWebSocket(wsuri);
+ } else {
+ log("Browser does not support WebSocket!");
+ window.location = "http://autobahn.ws/unsupportedbrowser";
+ }
+
+ if (sock) {
+ sock.onopen = function() {
+ log("Connected to " + wsuri);
+ }
+
+ sock.onclose = function(e) {
+ log("Connection closed (wasClean = " + e.wasClean + ", code = " + e.code + ", reason = '" + e.reason + "')");
+ sock = null;
+ }
+
+ sock.onmessage = function(e) {
+ log("[res] " + e.data + '\n');
+ }
+ }
+ };
+
+ function send() {
+ var msg = document.getElementById('message').value;
+ if (sock) {
+ sock.send(msg);
+ log("[cmd] " + msg);
+ } else {
+ log("Not connected.");
+ }
+ };
+
+ function log(m) {
+ ellog.innerHTML += m + '\n';
+ ellog.scrollTop = ellog.scrollHeight;
+ };
+ </script>
+ </head>
+ <body>
+ <h1>Bitmask Control Panel</h1>
+ <noscript>You must enable JavaScript</noscript>
+ <form>
+ <p>Command: <input id="message" type="text" size="50" maxlength="50" value="status"></p>
+ </form>
+ <button onclick='send();'>Send Command</button>
+ <pre id="log" style="height: 20em; overflow-y: scroll; background-color: #faa;"></pre>
+ </body>
+</html>
diff --git a/src/leap/bitmask/core/web/root.py b/src/leap/bitmask/core/web/root.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/leap/bitmask/core/web/root.py
diff --git a/src/leap/bitmask/core/websocket.py b/src/leap/bitmask/core/websocket.py
new file mode 100644
index 00000000..5569c6c7
--- /dev/null
+++ b/src/leap/bitmask/core/websocket.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+# websocket.py
+# Copyright (C) 2015, 2016 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+WebSockets Dispatcher Service.
+"""
+
+import os
+import pkg_resources
+
+from twisted.internet import reactor
+from twisted.application import service
+
+from twisted.web.server import Site
+from twisted.web.static import File
+
+from autobahn.twisted.resource import WebSocketResource
+from autobahn.twisted.websocket import WebSocketServerFactory
+from autobahn.twisted.websocket import WebSocketServerProtocol
+
+from leap.bitmask.core.dispatcher import CommandDispatcher
+
+
+class WebSocketsDispatcherService(service.Service):
+
+ """
+ A Dispatcher for BitmaskCore exposing a WebSockets Endpoint.
+ """
+
+ def __init__(self, core, port=8080, debug=False):
+ self._core = core
+ self.port = port
+ self.debug = debug
+
+ def startService(self):
+
+ factory = WebSocketServerFactory(u"ws://127.0.0.1:%d" % self.port,
+ debug=self.debug)
+ factory.protocol = DispatcherProtocol
+ factory.protocol.dispatcher = CommandDispatcher(self._core)
+
+ # FIXME: Site.start/stopFactory should start/stop factories wrapped as
+ # Resources
+ factory.startFactory()
+
+ resource = WebSocketResource(factory)
+
+ # we server static files under "/" ..
+ webdir = os.path.abspath(
+ pkg_resources.resource_filename("leap.bitmask.core", "web"))
+ root = File(webdir)
+
+ # and our WebSocket server under "/ws"
+ root.putChild(u"bitmask", resource)
+
+ # both under one Twisted Web Site
+ site = Site(root)
+
+ self.site = site
+ self.factory = factory
+
+ self.listener = reactor.listenTCP(self.port, site)
+
+ def stopService(self):
+ self.factory.stopFactory()
+ self.site.stopFactory()
+ self.listener.stopListening()
+
+
+class DispatcherProtocol(WebSocketServerProtocol):
+
+ def onMessage(self, msg, binary):
+ parts = msg.split()
+ r = self.dispatcher.dispatch(parts)
+ r.addCallback(self.defer_reply, binary)
+
+ def reply(self, response, binary):
+ self.sendMessage(response, binary)
+
+ def defer_reply(self, response, binary):
+ reactor.callLater(0, self.reply, response, binary)
+
+ def _get_service(self, name):
+ return self.core.getServiceNamed(name)
diff --git a/src/leap/bitmask/gui/account.py b/src/leap/bitmask/gui/account.py
index 81f96389..b8b9509a 100644
--- a/src/leap/bitmask/gui/account.py
+++ b/src/leap/bitmask/gui/account.py
@@ -20,6 +20,7 @@ A frontend GUI object to hold the current username and domain.
from leap.bitmask.util import make_address
from leap.bitmask.config.leapsettings import LeapSettings
from leap.bitmask.services import EIP_SERVICE, MX_SERVICE
+from leap.bitmask._components import HAS_EIP, HAS_MAIL
class Account():
@@ -42,8 +43,8 @@ class Account():
"""
return self._settings.get_enabled_services(self.domain)
- def is_email_enabled(self):
- return MX_SERVICE in self.services()
+ def has_email(self):
+ return HAS_MAIL and MX_SERVICE in self.services()
- def is_eip_enabled(self):
- return EIP_SERVICE in self.services()
+ def has_eip(self):
+ return HAS_EIP and EIP_SERVICE in self.services()
diff --git a/src/leap/bitmask/gui/advanced_key_management.py b/src/leap/bitmask/gui/advanced_key_management.py
index 2e315d18..bc496a57 100644
--- a/src/leap/bitmask/gui/advanced_key_management.py
+++ b/src/leap/bitmask/gui/advanced_key_management.py
@@ -94,6 +94,7 @@ class AdvancedKeyManagement(QtGui.QDialog):
"""
Set the current user's key details into the gui.
"""
+ # XXX: We should avoid the key-id
self.ui.leKeyID.setText(details[0])
self.ui.leFingerprint.setText(details[1])
@@ -246,7 +247,7 @@ class AdvancedKeyManagement(QtGui.QDialog):
row = keys_table.rowCount()
keys_table.insertRow(row)
keys_table.setItem(row, 0, QtGui.QTableWidgetItem(key.address))
- keys_table.setItem(row, 1, QtGui.QTableWidgetItem(key.key_id))
+ keys_table.setItem(row, 1, QtGui.QTableWidgetItem(key.fingerprint))
def _backend_connect(self):
"""
diff --git a/src/leap/bitmask/gui/app.py b/src/leap/bitmask/gui/app.py
index 97fd0549..1011454e 100644
--- a/src/leap/bitmask/gui/app.py
+++ b/src/leap/bitmask/gui/app.py
@@ -20,6 +20,7 @@ and the signaler get signals from the backend.
"""
from PySide import QtCore, QtGui
+from leap.bitmask.gui.account import Account
from leap.bitmask.config.leapsettings import LeapSettings
from leap.bitmask.backend.backend_proxy import BackendProxy
from leap.bitmask.backend.leapsignaler import LeapSignaler
@@ -44,12 +45,37 @@ class App(QtGui.QWidget):
self.signaler.start()
self.soledad_started = False
+ self.service_tokens = {}
+ self.login_state = None
+ self.providers_widget = None
# periodically check if the backend is alive
self._backend_checker = QtCore.QTimer(self)
self._backend_checker.timeout.connect(self._check_backend_status)
self._backend_checker.start(2000)
+ # store the service tokens for later use, once they are known.
+ self.signaler.soledad_got_service_token.connect(
+ self._set_service_tokens)
+
+ def current_account(self):
+ """
+ Alas, the only definitive account information is buried in the memory
+ of QT widgets.
+
+ :returns: an object representing the current user account.
+ :rtype: Account
+ """
+ if self.login_state is None or self.providers_widget is None:
+ return None
+
+ if self.login_state.full_logged_username is not None:
+ username, domain = self.login_state.full_logged_username.split('@')
+ return Account(username, domain)
+ else:
+ domain = self.providers_widget.get_selected_provider()
+ return Account(None, domain)
+
def _check_backend_status(self):
"""
TRIGGERS:
@@ -64,3 +90,11 @@ class App(QtGui.QWidget):
self.tr("There is a problem contacting the backend, please "
"restart Bitmask."))
self._backend_checker.stop()
+
+ def _set_service_tokens(self, data):
+ """
+ Triggered by signal soledad_got_service_token.
+ Saves the service tokens.
+ """
+ service, token = data
+ self.service_tokens[service] = token
diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py
index 64a408c4..470ef88a 100644
--- a/src/leap/bitmask/gui/eip_status.py
+++ b/src/leap/bitmask/gui/eip_status.py
@@ -733,7 +733,7 @@ class EIPStatusWidget(QtGui.QWidget):
self.set_eip_status(
# XXX this should change to polkit-kde where
# applicable.
- self.tr("We could not find any authentication agent in your "
+ self.tr("We could not find any authentication agent on your "
"system.<br/>Make sure you have "
"<b>polkit-gnome-authentication-agent-1</b> running and "
"try again."),
diff --git a/src/leap/bitmask/gui/logwindow.py b/src/leap/bitmask/gui/logwindow.py
index 718269c9..5d8c99fc 100644
--- a/src/leap/bitmask/gui/logwindow.py
+++ b/src/leap/bitmask/gui/logwindow.py
@@ -173,7 +173,7 @@ class LoggerWindow(QtGui.QDialog):
:type sending: bool
"""
if sending:
- self.ui.btnPastebin.setText(self.tr("Sending to pastebin..."))
+ self.ui.btnPastebin.setText(self.tr("Sending to Pastebin.com…"))
self.ui.btnPastebin.setEnabled(False)
else:
self.ui.btnPastebin.setText(self.tr("Send to Pastebin.com"))
@@ -193,7 +193,7 @@ class LoggerWindow(QtGui.QDialog):
# We save the dialog in an instance member to avoid dialog being
# deleted right after we exit this method
self._msgBox = msgBox = QtGui.QMessageBox(
- QtGui.QMessageBox.Information, self.tr("Pastebin OK"), msg)
+ QtGui.QMessageBox.Information, self.tr("Pastebin is OK"), msg)
msgBox.setWindowModality(QtCore.Qt.NonModal)
msgBox.show()
diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py
index 8b4329d7..cb0314b5 100644
--- a/src/leap/bitmask/gui/mail_status.py
+++ b/src/leap/bitmask/gui/mail_status.py
@@ -26,7 +26,9 @@ from leap.common.check import leap_assert, leap_assert_type
from leap.common.events import register
from leap.common.events import catalog
+from leap.bitmask.gui.preferenceswindow import PreferencesWindow
from ui_mail_status import Ui_MailStatusWidget
+from .qt_browser import PixelatedWindow
logger = get_logger()
@@ -52,13 +54,21 @@ class MailStatusWidget(QtGui.QWidget):
self._systray = None
self._disabled = True
self._started = False
+ self._mainwindow = parent
self._unread_mails = 0
self.ui = Ui_MailStatusWidget()
self.ui.setupUi(self)
- self.ui.lblMailReadyHelp.setVisible(False)
+ self.ui.email_ready.setVisible(False)
+ self.ui.configure_button.clicked.connect(
+ self._show_configure)
+ self.ui.open_mail_button.clicked.connect(
+ self._show_pix_ua)
+ if not self._mainwindow._settings.get_pixelmail_enabled():
+ self.ui.open_mail_button.setVisible(False)
+ self.ui.or_label.setVisible(False)
# set systray tooltip status
self._mx_status = ""
@@ -144,7 +154,23 @@ class MailStatusWidget(QtGui.QWidget):
self.CONNECTED_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[1])
self.ERROR_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[2])
- # Systray and actions
+ #
+ # Button actions
+ #
+
+ def _show_configure(self):
+ pref_win = PreferencesWindow(self._mainwindow, self._mainwindow.app)
+ pref_win.set_page("email")
+ pref_win.show()
+
+ def _show_pix_ua(self):
+ win = PixelatedWindow(self._mainwindow)
+ win.show()
+ win.load_app()
+
+ #
+ # Systray
+ #
def set_systray(self, systray):
"""
@@ -166,6 +192,10 @@ class MailStatusWidget(QtGui.QWidget):
mx_status = u"{0}: {1}".format(self._service_name, self._mx_status)
self._systray.set_service_tooltip(MX_SERVICE, mx_status)
+ #
+ # Status
+ #
+
def set_action_mail_status(self, action_mail_status):
"""
Sets the action_mail_status to use.
@@ -186,7 +216,7 @@ class MailStatusWidget(QtGui.QWidget):
msg = self.tr("There was an unexpected problem with Soledad.")
self._set_mail_status(msg, ready=-1)
- def set_soledad_invalid_auth_token(self, event, content):
+ def set_soledad_invalid_auth_token(self, event, content=None):
"""
This method is called when the auth token is invalid
@@ -229,16 +259,22 @@ class MailStatusWidget(QtGui.QWidget):
elif ready < 0:
tray_status = self.tr("Mail is disabled")
+ if ready < 1:
+ self._hide_mail_ready()
+
self.ui.lblMailStatusIcon.setPixmap(icon)
self._action_mail_status.setText(tray_status)
self._update_systray_tooltip()
- def _mail_handle_soledad_events(self, event, content):
+ def _mail_handle_soledad_events(self, event, user_data, content=""):
"""
Callback for handling events that are emitted from Soledad
:param event: The event that triggered the callback.
:type event: str
+ :param user_id: The user_data of the soledad user. Ignored right now,
+ since we're only contemplating single-user in soledad.
+ :type user_id: dict
:param content: The content of the event.
:type content: dict
"""
@@ -346,7 +382,7 @@ class MailStatusWidget(QtGui.QWidget):
logger.warning("don't know to to handle %s" % (event,))
self._set_mail_status(ext_status, ready=1)
- def _mail_handle_smtp_events(self, event):
+ def _mail_handle_smtp_events(self, event, content=""):
"""
Callback for the SMTP events
@@ -380,12 +416,14 @@ class MailStatusWidget(QtGui.QWidget):
# ----- XXX deprecate (move to mail conductor)
- def _mail_handle_imap_events(self, event, content):
+ def _mail_handle_imap_events(self, event, uuid, content=""):
"""
Callback for the IMAP events
:param event: The event that triggered the callback.
:type event: str
+ :param uuid: The UUID for the user. Ignored right now.
+ :type uuid: str
:param content: The content of the event.
:type content: list
"""
@@ -419,9 +457,10 @@ class MailStatusWidget(QtGui.QWidget):
self._show_unread_mails()
elif event == catalog.IMAP_SERVICE_STARTED:
self._imap_started = True
- elif event == catalog.IMAP_CLIENT_LOGIN:
- # If a MUA has logged in then we don't need to show this.
- self._hide_mail_ready_help()
+ # this is disabled for now, because this event was being
+ # triggered at weird times.
+ # elif event == catalog.IMAP_CLIENT_LOGIN:
+ # self._hide_mail_ready()
if ext_status is not None:
self._set_mail_status(ext_status, ready=1)
@@ -490,15 +529,13 @@ class MailStatusWidget(QtGui.QWidget):
Display the correct UI for the connected state.
"""
self._set_mail_status(self.tr("ON"), 2)
+ self.ui.email_ready.setVisible(True)
- # this help message will hide when the MUA connects
- self.ui.lblMailReadyHelp.setVisible(True)
-
- def _hide_mail_ready_help(self):
+ def _hide_mail_ready(self):
"""
Hide the mail help message on the UI.
"""
- self.ui.lblMailReadyHelp.setVisible(False)
+ self.ui.email_ready.setVisible(False)
def mail_state_disabled(self):
"""
diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py
index a8a4e41d..6637f170 100644
--- a/src/leap/bitmask/gui/mainwindow.py
+++ b/src/leap/bitmask/gui/mainwindow.py
@@ -46,7 +46,6 @@ from leap.bitmask.gui.signaltracker import SignalTracker
from leap.bitmask.gui.systray import SysTray
from leap.bitmask.gui.wizard import Wizard
from leap.bitmask.gui.providers import Providers
-from leap.bitmask.gui.account import Account
from leap.bitmask.gui.app import App
from leap.bitmask.platform_init import IS_WIN, IS_MAC, IS_LINUX
@@ -152,6 +151,14 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
# Provider List
self._providers = Providers(self.ui.cmbProviders)
+ ##
+ # tmphack: important state information about the application is stored
+ # in widgets. Rather than rewrite the UI, for now we simulate this
+ # info being stored in an application object:
+ ##
+ self.app.login_state = self._login_widget._state
+ self.app.providers_widget = self._providers
+
# Qt Signal Connections #####################################
# TODO separate logic from ui signals.
@@ -218,6 +225,7 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
self._backend_connect()
self.ui.action_preferences.triggered.connect(self._show_preferences)
+
self.ui.action_about_leap.triggered.connect(self._about)
self.ui.action_quit.triggered.connect(self.quit)
self.ui.action_wizard.triggered.connect(self._show_wizard)
@@ -294,6 +302,7 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
self._mail_conductor.connect_mail_signals(self._mail_status)
if not init_platform():
+ logger.critical('init_platform failed, quitting application.')
self.quit()
return
@@ -436,6 +445,9 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
# Refer to http://www.themacaque.com/?p=1067 for funny details.
self._wizard.show()
if IS_MAC:
+ # XXX hack. For some reason, there's a signal that doesn't arrive
+ # on time, so that the next button is disabled. See #8041
+ self._wizard.page(self._wizard.INTRO_PAGE).set_completed()
self._wizard.raise_()
self._settings.set_skip_first_run(True)
@@ -553,15 +565,7 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
Display the preferences window.
"""
- logged_user = self._login_widget.get_logged_user()
- if logged_user is not None:
- user, domain = logged_user.split('@')
- else:
- user = None
- domain = self._providers.get_selected_provider()
-
- account = Account(user, domain)
- pref_win = PreferencesWindow(self, account, self.app)
+ pref_win = PreferencesWindow(self, self.app)
pref_win.show()
def _update_eip_enabled_status(self, account=None, services=None):
@@ -1014,12 +1018,9 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
Display the About Bitmask dialog
"""
today = datetime.now().date()
- greet = ("Happy New 1984!... or not ;)<br><br>"
- if today.month == 1 and today.day < 15 else "")
title = self.tr("About Bitmask - %s") % (VERSION,)
msg = self.tr(
"Version: <b>{ver}</b> ({ver_hash})<br>"
- "<br>{greet}"
"Bitmask is the Desktop client application for the LEAP "
"platform, supporting Encrypted Internet Proxy "
"and <a href='https://bitmask.net/help/email'> "
@@ -1030,7 +1031,7 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
"available.<br>"
"<br>"
"<a href='https://leap.se'>More about LEAP</a>")
- msg = msg.format(ver=VERSION, ver_hash=VERSION_HASH[:10], greet=greet)
+ msg = msg.format(ver=VERSION, ver_hash=VERSION_HASH[:10])
QtGui.QMessageBox.about(self, title, msg)
def _help(self):
@@ -1038,46 +1039,9 @@ class MainWindow(QtGui.QMainWindow, SignalTracker):
TRIGGERS:
self.ui.action_help.triggered
- Display the Bitmask help dialog.
- """
- # TODO: don't hardcode!
- smtp_port = 2013
-
- help_url = "<p><a href='https://{0}'>{0}</a></p>".format(
- self.tr("bitmask.net/help"))
-
- lang = QtCore.QLocale.system().name().replace('_', '-')
- thunderbird_extension_url = \
- "https://addons.mozilla.org/{0}/" \
- "thunderbird/addon/bitmask/".format(lang)
-
- email_quick_reference = self.tr("Email quick reference")
- thunderbird_text = self.tr(
- "For Thunderbird, you can use the "
- "Bitmask extension. Search for \"Bitmask\" in the add-on "
- "manager or download it from <a href='{0}'>"
- "addons.mozilla.org</a>.".format(thunderbird_extension_url))
- manual_text = self.tr(
- "Alternatively, you can manually configure "
- "your mail client to use Bitmask Email with these options:")
- manual_imap = self.tr("IMAP: localhost, port {0}".format(IMAP_PORT))
- manual_smtp = self.tr("SMTP: localhost, port {0}".format(smtp_port))
- manual_username = self.tr("Username: your full email address")
- manual_password = self.tr("Password: any non-empty text")
-
- msg = help_url + self.tr(
- "<p><strong>{0}</strong></p>"
- "<p>{1}</p>"
- "<p>{2}"
- "<ul>"
- "<li>&nbsp;{3}</li>"
- "<li>&nbsp;{4}</li>"
- "<li>&nbsp;{5}</li>"
- "<li>&nbsp;{6}</li>"
- "</ul></p>").format(email_quick_reference, thunderbird_text,
- manual_text, manual_imap, manual_smtp,
- manual_username, manual_password)
- QtGui.QMessageBox.about(self, self.tr("Bitmask Help"), msg)
+ Open bitmask.net/help
+ """
+ QtGui.QDesktopServices.openUrl("https://bitmask.net/help")
def _needs_update(self):
"""
diff --git a/src/leap/bitmask/gui/passwordwindow.py b/src/leap/bitmask/gui/passwordwindow.py
index dedfcb10..fe70b250 100644
--- a/src/leap/bitmask/gui/passwordwindow.py
+++ b/src/leap/bitmask/gui/passwordwindow.py
@@ -83,7 +83,7 @@ class PasswordWindow(QtGui.QDialog, Flashable):
Returns true if the current account needs to change the soledad
password as well as the SRP password.
"""
- return self.account.is_email_enabled()
+ return self.account.has_email()
#
# MANAGE WIDGETS
diff --git a/src/leap/bitmask/gui/preferences_account_page.py b/src/leap/bitmask/gui/preferences_account_page.py
index da9da14d..141523c8 100644
--- a/src/leap/bitmask/gui/preferences_account_page.py
+++ b/src/leap/bitmask/gui/preferences_account_page.py
@@ -22,6 +22,7 @@ from PySide import QtCore, QtGui
from leap.bitmask.logs.utils import get_logger
from leap.bitmask.gui import ui_preferences_account_page as ui_pref
+from leap.bitmask.gui.preferences_page import PreferencesPage
from leap.bitmask.gui.passwordwindow import PasswordWindow
from leap.bitmask.services import get_service_display_name
from leap.bitmask._components import HAS_EIP
@@ -29,7 +30,7 @@ from leap.bitmask._components import HAS_EIP
logger = get_logger()
-class PreferencesAccountPage(QtGui.QWidget):
+class PreferencesAccountPage(PreferencesPage):
def __init__(self, parent, account, app):
"""
@@ -42,20 +43,15 @@ class PreferencesAccountPage(QtGui.QWidget):
:param app: the current App object
:type app: App
"""
- QtGui.QWidget.__init__(self, parent)
+ PreferencesPage.__init__(self, parent, account, app)
self.ui = ui_pref.Ui_PreferencesAccountPage()
self.ui.setupUi(self)
- self.account = account
- self.app = app
-
self._selected_services = set()
self.ui.change_password_label.setVisible(False)
self.ui.provider_services_label.setVisible(False)
- self.ui.change_password_button.clicked.connect(
- self._show_change_password)
- app.signaler.prov_get_supported_services.connect(self._load_services)
+ self.setup_connections()
app.backend.provider_get_supported_services(domain=account.domain)
if account.username is None:
@@ -64,6 +60,24 @@ class PreferencesAccountPage(QtGui.QWidget):
self.ui.change_password_label.setVisible(True)
self.ui.change_password_button.setEnabled(False)
+ def setup_connections(self):
+ """
+ connect signals
+ """
+ self.ui.change_password_button.clicked.connect(
+ self._show_change_password)
+ self.app.signaler.prov_get_supported_services.connect(
+ self._load_services)
+
+ def teardown_connections(self):
+ """
+ disconnect signals
+ """
+ self.ui.change_password_button.clicked.disconnect(
+ self._show_change_password)
+ self.app.signaler.prov_get_supported_services.disconnect(
+ self._load_services)
+
def _service_selection_changed(self, service, state):
"""
TRIGGERS:
diff --git a/src/leap/bitmask/gui/preferences_email_page.py b/src/leap/bitmask/gui/preferences_email_page.py
index 3087f343..93c77df1 100644
--- a/src/leap/bitmask/gui/preferences_email_page.py
+++ b/src/leap/bitmask/gui/preferences_email_page.py
@@ -16,20 +16,214 @@
"""
Widget for "email" preferences
"""
-from PySide import QtGui
+from PySide import QtCore, QtGui
+from leap.bitmask.config.leapsettings import LeapSettings
from leap.bitmask.logs.utils import get_logger
from leap.bitmask.gui.ui_preferences_email_page import Ui_PreferencesEmailPage
+from leap.bitmask.gui.preferences_page import PreferencesPage
+from leap.bitmask.pix import HAS_PIXELATED
+from leap.mail.imap.service.imap import IMAP_PORT
+
logger = get_logger()
-class PreferencesEmailPage(QtGui.QWidget):
+class PreferencesEmailPage(PreferencesPage):
def __init__(self, parent, account, app):
- QtGui.QWidget.__init__(self, parent)
+ """
+ :param parent: parent object of the PreferencesWindow.
+ :parent type: QWidget
+
+ :param account: user account (user + provider or just provider)
+ :type account: Account
+
+ :param app: the current App object
+ :type app: App
+ """
+ PreferencesPage.__init__(self, parent, account, app)
+ self.settings = LeapSettings()
self.ui = Ui_PreferencesEmailPage()
self.ui.setupUi(self)
- self.account = account
- self.app = app
+ # the only way to set the tab titles is to re-add them:
+ self.ui.email_tabs.addTab(self.ui.config_tab,
+ self.tr("Mail Client"))
+ self.ui.email_tabs.addTab(self.ui.my_key_tab,
+ self.tr("My Key"))
+ self.ui.email_tabs.addTab(self.ui.other_keys_tab,
+ self.tr("Other Keys"))
+
+ # set mail client configuration help text
+ lang = QtCore.QLocale.system().name().replace('_', '-')
+ thunderbird_extension_url = \
+ "https://addons.mozilla.org/{0}/" \
+ "thunderbird/addon/bitmask/".format(lang)
+ self.ui.thunderbird_label.setText(self.tr(
+ "For Thunderbird, you can use the Bitmask extension. "
+ "Search for \"Bitmask\" in the add-on manager or "
+ "download it from <a href='{0}'>addons.mozilla.org</a>.".format(
+ thunderbird_extension_url)))
+
+ self.ui.mail_client_label.setText(self.tr(
+ "Alternatively, you can manually configure your mail client to "
+ "use Bitmask with these options:"))
+
+ self.ui.webmail_label.setText(self.tr(
+ "Bitmask Mail is an integrated mail client based "
+ "on <a href='https://pixelated-project.org/'>Pixelated "
+ "User Agent</a>. If enabled, any user on your device "
+ "can read your mail by opening http://localhost:9090"))
+
+ self.ui.keys_table.horizontalHeader().setResizeMode(
+ 0, QtGui.QHeaderView.Stretch)
+
+ self.setup_connections()
+
+ def setup_connections(self):
+ """
+ connect signals
+ """
+ self.app.signaler.keymanager_key_details.connect(self._key_details)
+ self.app.signaler.keymanager_keys_list.connect(
+ self._keymanager_keys_list)
+ self.app.signaler.keymanager_export_ok.connect(
+ self._keymanager_export_ok)
+ self.app.signaler.keymanager_export_error.connect(
+ self._keymanager_export_error)
+ self.ui.import_button.clicked.connect(self._import_keys)
+ self.ui.export_button.clicked.connect(self._export_keys)
+ self.ui.webmail_checkbox.stateChanged.connect(self._toggle_webmail)
+
+ def teardown_connections(self):
+ """
+ disconnect signals
+ """
+ self.app.signaler.keymanager_key_details.disconnect(self._key_details)
+ self.app.signaler.keymanager_export_ok.disconnect(
+ self._keymanager_export_ok)
+ self.app.signaler.keymanager_export_error.disconnect(
+ self._keymanager_export_error)
+
+ def showEvent(self, event):
+ """
+ called whenever this widget is shown
+ """
+ self.ui.keys_table.clearContents()
+
+ if self.account.username is None:
+ self.ui.email_tabs.setVisible(False)
+ self.ui.message_label.setVisible(True)
+ self.ui.message_label.setText(
+ self.tr('You must be logged in to edit email settings.'))
+ else:
+ webmail_enabled = self.settings.get_pixelmail_enabled()
+ self.ui.webmail_checkbox.setChecked(webmail_enabled)
+ if not HAS_PIXELATED:
+ self.ui.webmail_box.setVisible(False)
+ self.ui.import_button.setVisible(False) # hide this until working
+ self.ui.message_label.setVisible(False)
+ self.ui.email_tabs.setVisible(True)
+ smtp_port = 2013
+ self.ui.imap_port_edit.setText(str(IMAP_PORT))
+ self.ui.imap_host_edit.setText("127.0.0.1")
+ self.ui.smtp_port_edit.setText(str(smtp_port))
+ self.ui.smtp_host_edit.setText("127.0.0.1")
+ self.ui.username_edit.setText(self.account.address)
+ self.ui.password_edit.setText(
+ self.app.service_tokens.get('mail_auth', ''))
+
+ self.app.backend.keymanager_list_keys()
+ self.app.backend.keymanager_get_key_details(
+ username=self.account.address)
+
+ def _key_details(self, details):
+ """
+ Trigger by signal: keymanager_key_details
+ Set the current user's key details into the gui.
+ """
+ self.ui.fingerprint_edit.setPlainText(
+ self._format_fingerprint(details["fingerprint"]))
+ self.ui.expiration_edit.setText(details["expiry_date"])
+ self.ui.uid_edit.setText(" ".join(details["uids"]))
+ self.ui.public_key_edit.setPlainText(details["key_data"])
+
+ def _format_fingerprint(self, fingerprint):
+ """
+ formats an openpgp fingerprint in a manner similar to what gpg
+ produces, wrapped to two lines.
+ """
+ fp = fingerprint.upper()
+ fp_list = [fp[i:i + 4] for i in range(0, len(fp), 4)]
+ fp_wrapped = " ".join(fp_list[0:5]) + "\n" + " ".join(fp_list[5:10])
+ return fp_wrapped
+
+ def _export_keys(self):
+ """
+ Exports the user's key pair.
+ """
+ file_name, filtr = QtGui.QFileDialog.getSaveFileName(
+ self, self.tr("Save private key file"),
+ filter="*.pem",
+ options=QtGui.QFileDialog.DontUseNativeDialog)
+
+ if file_name:
+ if not file_name.endswith('.pem'):
+ file_name += '.pem'
+ self.app.backend.keymanager_export_keys(
+ username=self.account.address,
+ filename=file_name)
+ else:
+ logger.debug('Export canceled by the user.')
+
+ def _keymanager_export_ok(self):
+ """
+ TRIGGERS:
+ Signaler.keymanager_export_ok
+
+ Notify the user that the key export went OK.
+ """
+ QtGui.QMessageBox.information(
+ self, self.tr("Export Successful"),
+ self.tr("The key pair was exported successfully.\n"
+ "Please, store your private key in a safe place."))
+
+ def _keymanager_export_error(self):
+ """
+ TRIGGERS:
+ Signaler.keymanager_export_error
+
+ Notify the user that the key export didn't go well.
+ """
+ QtGui.QMessageBox.critical(
+ self, self.tr("Input/Output error"),
+ self.tr("There was an error accessing the file.\n"
+ "Export canceled."))
+
+ def _import_keys(self):
+ """
+ not yet supported
+ """
+
+ def _keymanager_keys_list(self, keys):
+ """
+ TRIGGERS:
+ Signaler.keymanager_keys_list
+
+ Load the keys given as parameter in the table.
+
+ :param keys: the list of keys to load.
+ :type keys: list
+ """
+ for key in keys:
+ row = self.ui.keys_table.rowCount()
+ self.ui.keys_table.insertRow(row)
+ self.ui.keys_table.setItem(
+ row, 0, QtGui.QTableWidgetItem(" ".join(key["uids"])))
+ self.ui.keys_table.setItem(
+ row, 1, QtGui.QTableWidgetItem(key["fingerprint"]))
+
+ def _toggle_webmail(self, state):
+ value = True if state == QtCore.Qt.Checked else False
+ self.settings.set_pixelmail_enabled(value)
diff --git a/src/leap/bitmask/gui/preferences_page.py b/src/leap/bitmask/gui/preferences_page.py
new file mode 100644
index 00000000..a5d811f9
--- /dev/null
+++ b/src/leap/bitmask/gui/preferences_page.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Base class for preference pages
+"""
+
+from PySide import QtGui
+
+
+class PreferencesPage(QtGui.QWidget):
+
+ def __init__(self, parent, account=None, app=None):
+ """
+ :param parent: parent object of the EIPPreferencesWindow.
+ :type parent: QWidget
+
+ :param account: the currently active account
+ :type account: Account
+
+ :param app: shared App instance
+ :type app: App
+ """
+ QtGui.QWidget.__init__(self, parent)
+ self.app = app
+ self.account = account
+
+ def setup_connections(self):
+ """
+ connect signals
+ must be overridden by subclass
+ """
+
+ def teardown_connections(self):
+ """
+ disconnect signals
+ must be overridden by subclass
+ """
diff --git a/src/leap/bitmask/gui/preferences_vpn_page.py b/src/leap/bitmask/gui/preferences_vpn_page.py
index 5b5c9604..87b86c4e 100644
--- a/src/leap/bitmask/gui/preferences_vpn_page.py
+++ b/src/leap/bitmask/gui/preferences_vpn_page.py
@@ -20,11 +20,11 @@ Widget for "vpn" preferences
from PySide import QtCore, QtGui
from leap.bitmask.gui.ui_preferences_vpn_page import Ui_PreferencesVpnPage
-from leap.bitmask.config.leapsettings import LeapSettings
from leap.bitmask.gui.flashable import Flashable
+from leap.bitmask.gui.preferences_page import PreferencesPage
-class PreferencesVpnPage(QtGui.QWidget, Flashable):
+class PreferencesVpnPage(PreferencesPage, Flashable):
"""
Page in the preferences window that shows VPN settings
@@ -41,19 +41,24 @@ class PreferencesVpnPage(QtGui.QWidget, Flashable):
:param app: shared App instance
:type app: App
"""
- QtGui.QWidget.__init__(self, parent)
+ PreferencesPage.__init__(self, parent, account, app)
self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic")
- self.account = account
- self.app = app
-
# Load UI
self.ui = Ui_PreferencesVpnPage()
self.ui.setupUi(self)
self.ui.flash_label.setVisible(False)
self.hide_flash()
- # Connections
+ self.setup_connections()
+
+ # Trigger update
+ self.app.backend.eip_get_gateways_list(domain=self.account.domain)
+
+ def setup_connections(self):
+ """
+ connect signals
+ """
self.ui.gateways_list.clicked.connect(self._save_selected_gateway)
sig = self.app.signaler
sig.eip_get_gateways_list.connect(self._update_gateways_list)
@@ -61,8 +66,16 @@ class PreferencesVpnPage(QtGui.QWidget, Flashable):
sig.eip_uninitialized_provider.connect(
self._gateways_list_uninitialized)
- # Trigger update
- self.app.backend.eip_get_gateways_list(domain=self.account.domain)
+ def teardown_connections(self):
+ """
+ disconnect signals
+ """
+ self.ui.gateways_list.clicked.disconnect(self._save_selected_gateway)
+ sig = self.app.signaler
+ sig.eip_get_gateways_list.disconnect(self._update_gateways_list)
+ sig.eip_get_gateways_list_error.disconnect(self._gateways_list_error)
+ sig.eip_uninitialized_provider.disconnect(
+ self._gateways_list_uninitialized)
def _save_selected_gateway(self, index):
"""
diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py
index 44c4641c..50a972e1 100644
--- a/src/leap/bitmask/gui/preferenceswindow.py
+++ b/src/leap/bitmask/gui/preferenceswindow.py
@@ -20,11 +20,9 @@ Preferences window
"""
from PySide import QtCore, QtGui
-from leap.bitmask.services import EIP_SERVICE
-from leap.bitmask._components import HAS_EIP
-
from leap.bitmask.logs.utils import get_logger
from leap.bitmask.gui.ui_preferences import Ui_Preferences
+from leap.bitmask.gui.preferences_page import PreferencesPage
from leap.bitmask.gui.preferences_account_page import PreferencesAccountPage
from leap.bitmask.gui.preferences_vpn_page import PreferencesVpnPage
from leap.bitmask.gui.preferences_email_page import PreferencesEmailPage
@@ -40,39 +38,52 @@ class PreferencesWindow(QtGui.QDialog):
_current_window = None # currently visible preferences window
- def __init__(self, parent, account, app):
+ _panels = {
+ "account": 0,
+ "vpn": 1,
+ "email": 2
+ }
+
+ def __init__(self, parent, app):
"""
:param parent: parent object of the PreferencesWindow.
:parent type: QWidget
- :param account: the user or provider
- :type account: Account
-
:param app: the current App object
:type app: App
"""
QtGui.QDialog.__init__(self, parent)
- self.account = account
self.app = app
self.ui = Ui_Preferences()
self.ui.setupUi(self)
- self.ui.close_button.clicked.connect(self.close)
- self.ui.account_label.setText(account.address)
-
- self.app.service_selection_changed.connect(self._update_icons)
+ self._account_page = None
+ self._vpn_page = None
+ self._email_page = None
self._add_icons()
- self._add_pages()
- self._update_icons(self.account, self.account.services())
+ self._set_account(app.current_account())
+ self._setup_connections()
# only allow a single preferences window at a time.
if PreferencesWindow._current_window is not None:
PreferencesWindow._current_window.close()
PreferencesWindow._current_window = self
+ def _set_account(self, account):
+ """
+ Initially sets, or resets, the currently viewed account.
+ The account might not represent an authenticated user, but
+ just a domain.
+ """
+ self.ui.account_label.setText(account.address)
+ self._add_pages(account)
+ self._update_icons(account)
+ self.ui.pages_widget.setCurrentIndex(0)
+ self.ui.nav_widget.setCurrentRow(0)
+
def _add_icons(self):
"""
Adds all the icons for the different configuration categories.
@@ -114,22 +125,71 @@ class PreferencesWindow(QtGui.QDialog):
email_item.setSizeHint(QtCore.QSize(98, 56))
self._email_item = email_item
- self.ui.nav_widget.currentItemChanged.connect(self._change_page)
- self.ui.nav_widget.setCurrentRow(0)
-
- def _add_pages(self):
+ def _add_pages(self, account):
"""
Adds the pages for the different configuration categories.
"""
- self._account_page = PreferencesAccountPage(
- self, self.account, self.app)
- self._vpn_page = PreferencesVpnPage(self, self.account, self.app)
- self._email_page = PreferencesEmailPage(self, self.account, self.app)
-
+ self._remove_pages() # in case different account was loaded.
+
+ # load placeholder widgets if the page should not be loaded.
+ # the order of the pages is important, and must match the order
+ # of the nav_widget icons.
+ self._account_page = PreferencesAccountPage(self, account, self.app)
+ if account.has_eip():
+ self._vpn_page = PreferencesVpnPage(self, account, self.app)
+ else:
+ self._vpn_page = PreferencesPage(self)
+ if account.has_email():
+ self._email_page = PreferencesEmailPage(self, account, self.app)
+ else:
+ self._email_page = PreferencesPage(self)
self.ui.pages_widget.addWidget(self._account_page)
self.ui.pages_widget.addWidget(self._vpn_page)
self.ui.pages_widget.addWidget(self._email_page)
+ def _remove_pages(self):
+ # deleteLater does not seem to cascade to items in stackLayout
+ # (even with QtCore.Qt.WA_DeleteOnClose attribute).
+ # so, here we call deleteLater() explicitly.
+ if self._account_page is not None:
+ self.ui.pages_widget.removeWidget(self._account_page)
+ self._account_page.teardown_connections()
+ self._account_page.deleteLater()
+ if self._vpn_page is not None:
+ self.ui.pages_widget.removeWidget(self._vpn_page)
+ self._vpn_page.teardown_connections()
+ self._vpn_page.deleteLater()
+ if self._email_page is not None:
+ self.ui.pages_widget.removeWidget(self._email_page)
+ self._email_page.teardown_connections()
+ self._email_page.deleteLater()
+
+ def _setup_connections(self):
+ """
+ setup signal connections
+ """
+ self.ui.nav_widget.currentItemChanged.connect(self._change_page)
+ self.ui.close_button.clicked.connect(self.close)
+ self.app.service_selection_changed.connect(self._update_icons)
+ sig = self.app.signaler
+ sig.srp_auth_ok.connect(self._login_status_changed)
+ sig.srp_logout_ok.connect(self._login_status_changed)
+ sig.srp_status_logged_in.connect(self._update_account)
+ sig.srp_status_not_logged_in.connect(self._update_account)
+
+ def _teardown_connections(self):
+ """
+ clean up signal connections
+ """
+ self.ui.nav_widget.currentItemChanged.disconnect(self._change_page)
+ self.ui.close_button.clicked.disconnect(self.close)
+ self.app.service_selection_changed.disconnect(self._update_icons)
+ sig = self.app.signaler
+ sig.srp_auth_ok.disconnect(self._login_status_changed)
+ sig.srp_logout_ok.disconnect(self._login_status_changed)
+ sig.srp_status_logged_in.disconnect(self._update_account)
+ sig.srp_status_not_logged_in.disconnect(self._update_account)
+
#
# Slots
#
@@ -144,13 +204,8 @@ class PreferencesWindow(QtGui.QDialog):
Close this dialog and destroy it.
"""
PreferencesWindow._current_window = None
-
- # deleteLater does not seem to cascade to items in stackLayout
- # (even with QtCore.Qt.WA_DeleteOnClose attribute).
- # so, here we call deleteLater() explicitly:
- self._account_page.deleteLater()
- self._vpn_page.deleteLater()
- self._email_page.deleteLater()
+ self._teardown_connections()
+ self._remove_pages()
self.deleteLater()
def _change_page(self, current, previous):
@@ -170,17 +225,32 @@ class PreferencesWindow(QtGui.QDialog):
current = previous
self.ui.pages_widget.setCurrentIndex(self.ui.nav_widget.row(current))
- def _update_icons(self, account, services):
+ def _update_icons(self, account):
"""
TRIGGERS:
self.app.service_selection_changed
Change which icons are visible.
"""
- if account != self.account:
- return
+ self._vpn_item.setHidden(not account.has_eip())
+ self._email_item.setHidden(not account.has_email())
- if HAS_EIP:
- self._vpn_item.setHidden(EIP_SERVICE not in services)
- # self._email_item.setHidden(not MX_SERVICE in services)
- # ^^ disable email for now, there is nothing there yet.
+ def _login_status_changed(self):
+ """
+ Triggered by signal srp_auth_ok, srp_logout_ok
+ """
+ self.app.backend.user_get_logged_in_status()
+
+ def _update_account(self):
+ """
+ Triggered by get srp_status_logged_in, srp_status_not_logged_in
+ """
+ self._set_account(self.app.current_account())
+
+ def set_page(self, page):
+ """
+ Jump to a particular page
+ """
+ index = PreferencesWindow._panels[page]
+ self.ui.nav_widget.setCurrentRow(index)
+ self.ui.pages_widget.setCurrentIndex(index)
diff --git a/src/leap/bitmask/gui/qt_browser.py b/src/leap/bitmask/gui/qt_browser.py
new file mode 100644
index 00000000..2d9e20e6
--- /dev/null
+++ b/src/leap/bitmask/gui/qt_browser.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+# qt_browser.py
+# Copyright (C) 2016 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+QtWebKit-based browser to display Pixelated User Agent
+"""
+import os
+import urlparse
+
+from PySide import QtCore, QtWebKit, QtGui, QtNetwork
+
+PIXELATED_URI = 'http://localhost:9090'
+
+
+class PixelatedWindow(QtGui.QDialog):
+
+ def __init__(self, parent):
+ super(PixelatedWindow, self).__init__(parent)
+ self.view = QtWebKit.QWebView(self)
+
+ layout = QtGui.QGridLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(self.view)
+ self.setLayout(layout)
+ self.setWindowTitle('Bitmask Mail')
+
+ # For the moment, we need to resize to a sensible default to avoid the
+ # "send" button to be out of view in the compose pane. This should be
+ # removed as soon as pixelated becomes size-responsive.
+ self.resize(800, 700)
+
+ def load_app(self):
+ self.view.load(QtCore.QUrl(PIXELATED_URI))
+ self.view.page().setForwardUnsupportedContent(True)
+ self.view.page().unsupportedContent.connect(self.download)
+
+ self.manager = QtNetwork.QNetworkAccessManager()
+ self.manager.finished.connect(self.finished)
+
+ def download(self, reply):
+ self.request = QtNetwork.QNetworkRequest(reply.url())
+ self.reply = self.manager.get(self.request)
+
+ def finished(self):
+ url = self.reply.url().toString()
+
+ filename = urlparse.parse_qs(url).get('filename', None)
+ if filename:
+ filename = filename[0]
+ name = filename or url
+
+ path = os.path.expanduser(os.path.join(
+ '~', unicode(name).split('/')[-1]))
+ destination = QtGui.QFileDialog.getSaveFileName(self, "Save", path)
+ if destination:
+ filename = destination[0]
+ with open(filename, 'wb') as f:
+ f.write(str(self.reply.readAll()))
+ f.close()
diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py
index ab48b756..92c5431d 100644
--- a/src/leap/bitmask/gui/statemachines.py
+++ b/src/leap/bitmask/gui/statemachines.py
@@ -40,6 +40,7 @@ class SignallingState(QState):
"""
A state that emits a custom signal on entry.
"""
+
def __init__(self, signal, parent=None, name=None):
"""
Initializer.
@@ -134,6 +135,7 @@ class States(object):
class CompositeEvent(QtCore.QEvent):
+
def __init__(self):
super(CompositeEvent, self).__init__(
QtCore.QEvent.Type(self.ID))
@@ -174,6 +176,7 @@ class Events(QtCore.QObject):
A Wrapper object for containing the events that will be
posted to a composite state machine.
"""
+
def __init__(self, parent=None):
"""
Initializes the QObject with the given parent.
@@ -289,6 +292,7 @@ class ConnectionMachineBuilder(object):
"""
Builder class for state machines made from LEAPConnections.
"""
+
def __init__(self, connection):
"""
:param connection: an instance of a concrete LEAPConnection
@@ -352,7 +356,6 @@ class ConnectionMachineBuilder(object):
:rtype: QStateMachine
"""
# TODO split this method in smaller utility functions.
- parent = kwargs.get('parent', None)
# 1. create machine
machine = CompositeMachine()
diff --git a/src/leap/bitmask/gui/ui/mail_status.ui b/src/leap/bitmask/gui/ui/mail_status.ui
index 8e8f1848..f8ebb5a8 100644
--- a/src/leap/bitmask/gui/ui/mail_status.ui
+++ b/src/leap/bitmask/gui/ui/mail_status.ui
@@ -6,12 +6,12 @@
<rect>
<x>0</x>
<y>0</y>
- <width>417</width>
- <height>185</height>
+ <width>427</width>
+ <height>157</height>
</rect>
</property>
<property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
@@ -20,26 +20,131 @@
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
- <property name="topMargin">
- <number>0</number>
- </property>
+ <item row="1" column="1">
+ <widget class="QLabel" name="lblMailStatus">
+ <property name="styleSheet">
+ <string notr="true">color: rgb(80, 80, 80);</string>
+ </property>
+ <property name="text">
+ <string>You must login to use encrypted email.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="3">
+ <widget class="QLabel" name="lblMailStatusIcon">
+ <property name="minimumSize">
+ <size>
+ <width>22</width>
+ <height>22</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>22</width>
+ <height>22</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="pixmap">
+ <pixmap resource="../../../../../data/resources/icons.qrc">:/images/black/22/off.png</pixmap>
+ </property>
+ <property name="scaledContents">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QWidget" name="email_ready" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>6</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="configure_button">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Configure Client</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="or_label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>or</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="open_mail_button">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Open Bitmask Mail</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="email_ready_spacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Expanding</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>10</width>
+ <height>10</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
<item row="0" column="0">
- <layout class="QGridLayout" name="gridLayout_3">
- <item row="0" column="2">
- <spacer name="horizontalSpacer_4">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item row="0" column="1">
- <widget class="QLabel" name="label_4">
+ <widget class="QLabel" name="mail_icon">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="pixmap">
+ <pixmap resource="../../../../../data/resources/icons.qrc">:/images/black/32/email.png</pixmap>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <layout class="QHBoxLayout" name="email_line">
+ <item>
+ <widget class="QLabel" name="email_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
@@ -51,85 +156,18 @@
</property>
</widget>
</item>
- <item row="0" column="4">
- <widget class="QLabel" name="lblMailStatusIcon">
- <property name="maximumSize">
+ <item>
+ <spacer name="horizontal1_spacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
<size>
- <width>24</width>
- <height>24</height>
+ <width>1</width>
+ <height>1</height>
</size>
</property>
- <property name="text">
- <string/>
- </property>
- <property name="pixmap">
- <pixmap resource="../../../../../data/resources/icons.qrc">:/images/black/22/off.png</pixmap>
- </property>
- <property name="scaledContents">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="3" column="1" colspan="2">
- <widget class="QLabel" name="lblMailReadyHelp">
- <property name="autoFillBackground">
- <bool>false</bool>
- </property>
- <property name="styleSheet">
- <string notr="true">background-color: #e0efd8;
-padding: 10px;</string>
- </property>
- <property name="frameShape">
- <enum>QFrame::NoFrame</enum>
- </property>
- <property name="frameShadow">
- <enum>QFrame::Plain</enum>
- </property>
- <property name="lineWidth">
- <number>0</number>
- </property>
- <property name="text">
- <string>Congratulations! You are ready to use Bitmask to encrypt your email. Go to &lt;a href=&quot;https://bitmask.net/en/help/email&quot;&gt;https://bitmask.net/en/help/email&lt;/a&gt; for instructions on how to set up your mail client.</string>
- </property>
- <property name="textFormat">
- <enum>Qt::AutoText</enum>
- </property>
- <property name="scaledContents">
- <bool>false</bool>
- </property>
- <property name="alignment">
- <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- <property name="margin">
- <number>0</number>
- </property>
- <property name="indent">
- <number>-1</number>
- </property>
- </widget>
- </item>
- <item row="0" column="0">
- <widget class="QLabel" name="label">
- <property name="text">
- <string/>
- </property>
- <property name="pixmap">
- <pixmap resource="../../../../../data/resources/icons.qrc">:/images/black/32/email.png</pixmap>
- </property>
- </widget>
- </item>
- <item row="2" column="1" colspan="2">
- <widget class="QLabel" name="lblMailStatus">
- <property name="styleSheet">
- <string notr="true">color: rgb(80, 80, 80);</string>
- </property>
- <property name="text">
- <string>You must login to use encrypted email.</string>
- </property>
- </widget>
+ </spacer>
</item>
</layout>
</item>
@@ -137,6 +175,7 @@ padding: 10px;</string>
</widget>
<resources>
<include location="../../../../../data/resources/icons.qrc"/>
+ <include location="dev/leap/client/bitmask_client/data/resources/icons.qrc"/>
</resources>
<connections/>
</ui>
diff --git a/src/leap/bitmask/gui/ui/mainwindow.ui b/src/leap/bitmask/gui/ui/mainwindow.ui
index b1d68c4a..5d8e0f35 100644
--- a/src/leap/bitmask/gui/ui/mainwindow.ui
+++ b/src/leap/bitmask/gui/ui/mainwindow.ui
@@ -75,7 +75,7 @@
<x>0</x>
<y>0</y>
<width>524</width>
- <height>549</height>
+ <height>541</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@@ -306,7 +306,7 @@
<x>0</x>
<y>0</y>
<width>524</width>
- <height>21</height>
+ <height>25</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
diff --git a/src/leap/bitmask/gui/ui/preferences.ui b/src/leap/bitmask/gui/ui/preferences.ui
index 5e30ea57..8e884a63 100644
--- a/src/leap/bitmask/gui/ui/preferences.ui
+++ b/src/leap/bitmask/gui/ui/preferences.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>520</width>
- <height>439</height>
+ <width>630</width>
+ <height>560</height>
</rect>
</property>
<property name="windowTitle">
@@ -60,6 +60,24 @@
<height>16777215</height>
</size>
</property>
+ <property name="styleSheet">
+ <string notr="true">background: palette(base); border: 1px solid palette(dark); border-radius: 2px;</string>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Plain</enum>
+ </property>
+ <property name="lineWidth">
+ <number>1</number>
+ </property>
+ <property name="midLineWidth">
+ <number>0</number>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
<property name="iconSize">
<size>
<width>32</width>
diff --git a/src/leap/bitmask/gui/ui/preferences_email_page.ui b/src/leap/bitmask/gui/ui/preferences_email_page.ui
index 7cc5bb3c..610a43c7 100644
--- a/src/leap/bitmask/gui/ui/preferences_email_page.ui
+++ b/src/leap/bitmask/gui/ui/preferences_email_page.ui
@@ -6,13 +6,638 @@
<rect>
<x>0</x>
<y>0</y>
- <width>400</width>
- <height>300</height>
+ <width>526</width>
+ <height>605</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="margin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="email_tabs">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="config_tab">
+ <property name="accessibleName">
+ <string/>
+ </property>
+ <property name="accessibleDescription">
+ <string/>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">background: palette(base);</string>
+ </property>
+ <attribute name="title">
+ <string>Tab 1</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <item>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="scrollAreaWidgetContents">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>504</width>
+ <height>537</height>
+ </rect>
+ </property>
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">background: palette(base);</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <property name="spacing">
+ <number>6</number>
+ </property>
+ <property name="margin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QGroupBox" name="webmail_box">
+ <property name="title">
+ <string>Bitmask Mail</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QCheckBox" name="webmail_checkbox">
+ <property name="text">
+ <string>Enable Bitmask Mail (needs restart)</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="webmail_label">
+ <property name="text">
+ <string>webmail info</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="thunderbird_box">
+ <property name="title">
+ <string>Thunderbird Configuration</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QLabel" name="thunderbird_label">
+ <property name="text">
+ <string>thunderbird information</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="imap_box">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="title">
+ <string>Other Mail Clients Configuration</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QLabel" name="mail_client_label">
+ <property name="text">
+ <string>mail client information</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="1" column="1">
+ <layout class="QGridLayout" name="gridLayout_5">
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_8">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Host</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QLabel" name="label_9">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Port</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="smtp_host_edit">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>100</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="4">
+ <widget class="QLabel" name="label_10">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>TLS: off</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="3">
+ <widget class="QLineEdit" name="smtp_port_edit">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>50</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="5">
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>IMAP</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Username</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>SMTP</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <layout class="QGridLayout" name="gridLayout_4">
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_5">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Host</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="imap_host_edit">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>100</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QLabel" name="label_6">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Port</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="6">
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="1" column="5">
+ <widget class="QLabel" name="label_7">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>TLS: off</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="3">
+ <widget class="QLineEdit" name="imap_port_edit">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>50</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="username_edit">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Password</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLineEdit" name="password_edit">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>4</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="my_key_tab">
+ <attribute name="title">
+ <string>Tab 2</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QWidget" name="mykey_box" native="true">
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="4" column="0">
+ <widget class="QLabel" name="pub_label">
+ <property name="text">
+ <string>Public Key</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <widget class="QPlainTextEdit" name="public_key_edit">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="font">
+ <font>
+ <family>Courier</family>
+ </font>
+ </property>
+ <property name="lineWrapMode">
+ <enum>QPlainTextEdit::NoWrap</enum>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QPlainTextEdit" name="fingerprint_edit">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>42</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>48</height>
+ </size>
+ </property>
+ <property name="font">
+ <font>
+ <family>Courier</family>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="acceptDrops">
+ <bool>false</bool>
+ </property>
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <property name="lineWrapMode">
+ <enum>QPlainTextEdit::NoWrap</enum>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="plainText">
+ <string notr="true"/>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="expiration_label">
+ <property name="text">
+ <string>Expiration</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="fp_label">
+ <property name="text">
+ <string>Fingerprint</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="uid_edit">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="uid_label">
+ <property name="text">
+ <string>Address</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="expiration_edit">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="2" column="1">
+ <widget class="QPushButton" name="export_button">
+ <property name="text">
+ <string>Export Private Key</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QPushButton" name="import_button">
+ <property name="text">
+ <string>Import Private Key</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="3">
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ <zorder>uid_edit</zorder>
+ <zorder>fp_label</zorder>
+ <zorder>uid_label</zorder>
+ <zorder>expiration_edit</zorder>
+ <zorder>expiration_label</zorder>
+ <zorder>fingerprint_edit</zorder>
+ <zorder>public_key_edit</zorder>
+ <zorder>pub_label</zorder>
+ <zorder></zorder>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="other_keys_tab">
+ <attribute name="title">
+ <string>Page</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <widget class="QTableWidget" name="keys_table">
+ <property name="editTriggers">
+ <set>QAbstractItemView::NoEditTriggers</set>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::SingleSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="textElideMode">
+ <enum>Qt::ElideRight</enum>
+ </property>
+ <attribute name="horizontalHeaderStretchLastSection">
+ <bool>true</bool>
+ </attribute>
+ <column>
+ <property name="text">
+ <string>Email</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>Fingerprint</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="message_label">
+ <property name="text">
+ <string>this message should be hidden</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
</widget>
<resources/>
<connections/>
diff --git a/src/leap/bitmask/gui/ui/wizard.ui b/src/leap/bitmask/gui/ui/wizard.ui
index b125577e..37226d13 100644
--- a/src/leap/bitmask/gui/ui/wizard.ui
+++ b/src/leap/bitmask/gui/ui/wizard.ui
@@ -316,7 +316,7 @@
<item row="1" column="1">
<widget class="QLabel" name="label">
<property name="text">
- <string>https://</string>
+ <string notr="true">https://</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
@@ -339,7 +339,7 @@
<item row="0" column="1">
<widget class="QLabel" name="label_8">
<property name="text">
- <string>https://</string>
+ <string notr="true">https://</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
diff --git a/src/leap/bitmask/pix.py b/src/leap/bitmask/pix.py
new file mode 100644
index 00000000..b510106d
--- /dev/null
+++ b/src/leap/bitmask/pix.py
@@ -0,0 +1,207 @@
+# -*- coding: utf-8 -*-
+# pix.py
+# Copyright (C) 2016 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Pixelated plugin integration.
+"""
+import json
+import os
+import sys
+
+from twisted.internet import defer
+from twisted.python import log
+
+from leap.bitmask.util import get_path_prefix
+from leap.mail.mail import Account
+from leap.keymanager import openpgp, KeyNotFound
+
+try:
+ from pixelated.adapter.mailstore import LeapMailStore
+ from pixelated.adapter.welcome_mail import add_welcome_mail
+ from pixelated.application import SingleUserServicesFactory
+ from pixelated.application import UserAgentMode
+ from pixelated.application import start_site
+ from pixelated.bitmask_libraries.smtp import LeapSMTPConfig
+ from pixelated.bitmask_libraries.session import SessionCache
+ from pixelated.config import services
+ from pixelated.resources.root_resource import RootResource
+ import pixelated_www
+ HAS_PIXELATED = True
+except ImportError:
+ HAS_PIXELATED = False
+
+
+def start_pixelated_user_agent(userid, soledad, keymanager):
+
+ leap_session = LeapSessionAdapter(
+ userid, soledad, keymanager)
+
+ config = Config()
+ leap_home = os.path.join(get_path_prefix(), 'leap')
+ config.leap_home = leap_home
+ leap_session.config = config
+
+ services_factory = SingleUserServicesFactory(
+ UserAgentMode(is_single_user=True))
+
+ if getattr(sys, 'frozen', False):
+ # we are running in a |PyInstaller| bundle
+ static_folder = os.path.join(sys._MEIPASS, 'pixelated_www')
+ else:
+ static_folder = os.path.abspath(pixelated_www.__path__[0])
+
+ resource = RootResource(services_factory, static_folder=static_folder)
+
+ config.host = 'localhost'
+ config.port = 9090
+ config.sslkey = None
+ config.sslcert = None
+
+ d = leap_session.account.callWhenReady(
+ lambda _: _start_in_single_user_mode(
+ leap_session, config,
+ resource, services_factory))
+ return d
+
+
+def get_smtp_config(provider):
+ config_path = os.path.join(
+ get_path_prefix(), 'leap', 'providers', provider, 'smtp-service.json')
+ json_config = json.loads(open(config_path).read())
+ chosen_host = json_config['hosts'].keys()[0]
+ hostname = json_config['hosts'][chosen_host]['hostname']
+ port = json_config['hosts'][chosen_host]['port']
+
+ config = Config()
+ config.host = hostname
+ config.port = port
+ return config
+
+
+class NickNym(object):
+
+ def __init__(self, keymanager, userid):
+ self._email = userid
+ self.keymanager = keymanager
+
+ @defer.inlineCallbacks
+ def generate_openpgp_key(self):
+ key_present = yield self._key_exists(self._email)
+ if not key_present:
+ yield self._gen_key()
+ yield self._send_key_to_leap()
+
+ @defer.inlineCallbacks
+ def _key_exists(self, email):
+ try:
+ yield self.fetch_key(email, private=True, fetch_remote=False)
+ defer.returnValue(True)
+ except KeyNotFound:
+ defer.returnValue(False)
+
+ def fetch_key(self, email, private=False, fetch_remote=True):
+ return self.keymanager.get_key(
+ email, openpgp.OpenPGPKey,
+ private=private, fetch_remote=fetch_remote)
+
+ def _gen_key(self):
+ return self.keymanager.gen_key(openpgp.OpenPGPKey)
+
+ def _send_key_to_leap(self):
+ return self.keymanager.send_key(openpgp.OpenPGPKey)
+
+
+class LeapSessionAdapter(object):
+
+ def __init__(self, userid, soledad, keymanager):
+ self.userid = userid
+
+ self.soledad = soledad
+
+ # XXX this needs to be converged with our public apis.
+ self.nicknym = NickNym(keymanager, userid)
+ self.mail_store = LeapMailStore(soledad)
+
+ self.user_auth = Config()
+ self.user_auth.uuid = soledad.uuid
+
+ self.fresh_account = False
+ self.incoming_mail_fetcher = None
+ self.account = Account(soledad, userid)
+
+ username, provider = userid.split('@')
+ smtp_client_cert = os.path.join(
+ get_path_prefix(),
+ 'leap', 'providers', provider, 'keys',
+ 'client',
+ 'smtp_{username}.pem'.format(
+ username=username))
+
+ assert(os.path.isfile(smtp_client_cert))
+
+ smtp_config = get_smtp_config(provider)
+ smtp_host = smtp_config.host
+ smtp_port = smtp_config.port
+
+ self.smtp_config = LeapSMTPConfig(
+ userid,
+ smtp_client_cert, smtp_host, smtp_port)
+
+ def account_email(self):
+ return self.userid
+
+ def close(self):
+ pass
+
+ @property
+ def is_closed(self):
+ return self._is_closed
+
+ def remove_from_cache(self):
+ key = SessionCache.session_key(self.provider, self.userid)
+ SessionCache.remove_session(key)
+
+ def sync(self):
+ return self.soledad.sync()
+
+
+class Config(object):
+ pass
+
+
+def _start_in_single_user_mode(leap_session, config, resource,
+ services_factory):
+ start_site(config, resource)
+ return start_user_agent_in_single_user_mode(
+ resource, services_factory,
+ leap_session.config.leap_home, leap_session)
+
+
+@defer.inlineCallbacks
+def start_user_agent_in_single_user_mode(
+ root_resource, services_factory, leap_home, leap_session):
+ log.msg('Bootstrap done, loading services for user %s'
+ % leap_session.userid)
+
+ _services = services.Services(leap_session)
+ yield _services.setup()
+
+ if leap_session.fresh_account:
+ yield add_welcome_mail(leap_session.mail_store)
+
+ services_factory.add_session(leap_session.user_auth.uuid, _services)
+ root_resource.initialize()
+ log.msg('Done, the user agent is ready to be used')
diff --git a/src/leap/bitmask/platform_init/initializers.py b/src/leap/bitmask/platform_init/initializers.py
index eb892cce..751762df 100644
--- a/src/leap/bitmask/platform_init/initializers.py
+++ b/src/leap/bitmask/platform_init/initializers.py
@@ -126,7 +126,7 @@ def check_missing():
if alert_missing and not flags.STANDALONE:
# We refuse to install missing stuff if not running with standalone
# flag. Right now we rely on the flag alone, but we can disable this
- # by overwriting some constant from within the debian package.
+ # by overwriting some constant from within the Debian package.
alert_missing = False
complain_missing = True
@@ -257,36 +257,19 @@ def WindowsInitializer():
if not _windows_has_tap_device():
msg = QtGui.QMessageBox()
msg.setWindowTitle(msg.tr("TAP Driver"))
- msg.setText(msg.tr("Bitmask needs to install the necessary drivers "
- "for Encrypted Internet to work. Would you like to "
- "proceed?"))
+ msg.setText(msg.tr("Bitmask needs a TAP Driver installed "
+ "for Encrypted Internet to work. Please reinstall "
+ "bitmask-client to proceed."))
msg.setInformativeText(msg.tr("Encrypted Internet uses VPN, which "
"needs a TAP device installed and none "
- "has been found. This will ask for "
- "administrative privileges."))
- msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)
- msg.setDefaultButton(QtGui.QMessageBox.Yes)
- ret = msg.exec_()
-
- if ret == QtGui.QMessageBox.Yes:
- # XXX should do this only if executed inside bundle.
- # Let's assume it's the only way it's gonna be executed under win
- # by now.
- driver_path = os.path.join(os.getcwd(),
- "apps",
- "eip",
- "tap_driver")
- dev_installer = os.path.join(driver_path,
- "devcon.exe")
- if os.path.isfile(dev_installer) and \
- stat.S_IXUSR & os.stat(dev_installer)[stat.ST_MODE] != 0:
- inf_path = os.path.join(driver_path,
- "OemWin2k.inf")
- cmd = [dev_installer, "install", inf_path, "tap0901"]
- ret = subprocess.call(cmd, stdout=subprocess.PIPE, shell=True)
- else:
- logger.error("Tried to install TAP driver, but the installer "
- "is not found or not executable")
+ "has been found. The bitmask-client "
+ "installer prompts for installing the "
+ "TAP Driver."))
+ msg.setStandardButtons(QtGui.QMessageBox.Ok)
+ logger.error('TAP Drivers not installed')
+ msg.exec_()
+ return False
+ return True
#
# Darwin initializer functions
@@ -368,6 +351,9 @@ def DarwinInitializer():
Raise a dialog in case that the osx tuntap driver has not been found
in the registry, asking the user for permission to install the driver
"""
+ logger.debug("Skipping darwin initialization, only-mail build")
+ return True
+
# XXX split this function into several
TUNTAP_NOTFOUND_MSG = NOTFOUND_MSG % (
@@ -440,32 +426,32 @@ def _get_missing_complain_dialog(stuff):
class ComplainDialog(QtGui.QDialog):
def __init__(self, parent=None):
- super(ComplainDialog, self).__init__(parent)
+ super(ComplainDialog, self).__init__(parent)
- label = QtGui.QLabel(msgstr.NO_HELPERS)
- label.setAlignment(QtCore.Qt.AlignLeft)
+ label = QtGui.QLabel(msgstr.NO_HELPERS)
+ label.setAlignment(QtCore.Qt.AlignLeft)
- label2 = QtGui.QLabel(msgstr.EXPLAIN)
- label2.setAlignment(QtCore.Qt.AlignLeft)
+ label2 = QtGui.QLabel(msgstr.EXPLAIN)
+ label2.setAlignment(QtCore.Qt.AlignLeft)
- textedit = QtGui.QTextEdit()
- textedit.setText("\n".join(stuff))
+ textedit = QtGui.QTextEdit()
+ textedit.setText("\n".join(stuff))
- ok = QtGui.QPushButton()
- ok.setText(self.tr("Ok, thanks"))
- self.ok = ok
- self.ok.clicked.connect(self.close)
+ ok = QtGui.QPushButton()
+ ok.setText(self.tr("Ok, thanks"))
+ self.ok = ok
+ self.ok.clicked.connect(self.close)
- mainLayout = QtGui.QGridLayout()
- mainLayout.addWidget(label, 0, 0)
- mainLayout.addWidget(label2, 1, 0)
- mainLayout.addWidget(textedit, 2, 0)
- mainLayout.addWidget(ok, 3, 0)
+ mainLayout = QtGui.QGridLayout()
+ mainLayout.addWidget(label, 0, 0)
+ mainLayout.addWidget(label2, 1, 0)
+ mainLayout.addWidget(textedit, 2, 0)
+ mainLayout.addWidget(ok, 3, 0)
- self.setLayout(mainLayout)
+ self.setLayout(mainLayout)
msg = ComplainDialog()
- msg.setWindowTitle(msg.tr("Missing Bitmask helpers"))
+ msg.setWindowTitle(msg.tr("Missing Bitmask helper files"))
return msg
@@ -482,7 +468,7 @@ def _linux_install_missing_scripts(badexec, notfound):
"""
success = False
installer_path = os.path.abspath(
- os.path.join(os.getcwd(), "apps", "eip", "files"))
+ os.path.join(os.getcwd(), "..", "apps", "eip", "files"))
install_helper = "leap-install-helper.sh"
install_helper_path = os.path.join(installer_path, install_helper)
diff --git a/src/leap/bitmask/provider/__init__.py b/src/leap/bitmask/provider/__init__.py
index 4385a92f..60a41181 100644
--- a/src/leap/bitmask/provider/__init__.py
+++ b/src/leap/bitmask/provider/__init__.py
@@ -22,7 +22,7 @@ import os
from pkg_resources import parse_version
-from leap.bitmask import __short_version__ as BITMASK_VERSION
+from leap.bitmask import __version__ as BITMASK_VERSION
from leap.common.check import leap_assert
logger = logging.getLogger(__name__)
diff --git a/src/leap/bitmask/services/__init__.py b/src/leap/bitmask/services/__init__.py
index 54426669..d86f8aa4 100644
--- a/src/leap/bitmask/services/__init__.py
+++ b/src/leap/bitmask/services/__init__.py
@@ -154,6 +154,9 @@ def download_service_config(provider_config, service_config,
# Not modified
service_path = ("leap", "providers", provider_config.get_domain(),
service_json)
+
+ service_config.__class__.standalone = flags.STANDALONE
+
if res.status_code == 304:
logger.debug(
"{0} definition has not been modified".format(
diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py
index 17fc11c2..e697b118 100644
--- a/src/leap/bitmask/services/eip/darwinvpnlauncher.py
+++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py
@@ -20,6 +20,7 @@ Darwin VPN launcher implementation.
import commands
import getpass
import os
+import socket
import sys
from leap.bitmask.logs.utils import get_logger
@@ -34,17 +35,46 @@ class EIPNoTunKextLoaded(VPNLauncherException):
pass
+class DarwinHelperCommand(object):
+
+ SOCKET_ADDR = '/tmp/bitmask-helper.socket'
+
+ def __init__(self):
+ pass
+
+ def _connect(self):
+ self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ try:
+ self._sock.connect(self.SOCKET_ADDR)
+ except socket.error, msg:
+ raise RuntimeError(msg)
+
+ def send(self, cmd, args=''):
+ # TODO check cmd is in allowed list
+ self._connect()
+ sock = self._sock
+ data = ""
+
+ command = cmd + ' ' + args + '/CMD'
+
+ try:
+ sock.sendall(command)
+ while '\n' not in data:
+ data += sock.recv(32)
+ finally:
+ sock.close()
+
+ return data
+
+
class DarwinVPNLauncher(VPNLauncher):
"""
VPN launcher for the Darwin Platform
"""
- COCOASUDO = "cocoasudo"
- # XXX need the good old magic translate for these strings
- # (look for magic in 0.2.0 release)
- SUDO_MSG = ("Bitmask needs administrative privileges to run "
- "Encrypted Internet.")
- INSTALL_MSG = ("\"Bitmask needs administrative privileges to install "
- "missing scripts and fix permissions.\"")
+ UP_SCRIPT = None
+ DOWN_SCRIPT = None
+
+ # TODO -- move this to bitmask-helper
# Hardcode the installation path for OSX for security, openvpn is
# run as root
@@ -56,14 +86,9 @@ class DarwinVPNLauncher(VPNLauncher):
INSTALL_PATH_ESCAPED,)
OPENVPN_BIN_PATH = "%s/Contents/Resources/%s" % (INSTALL_PATH,
OPENVPN_BIN)
-
- UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,)
- DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,)
- OPENVPN_DOWN_PLUGIN = '%s/openvpn-down-root.so' % (OPENVPN_PATH,)
-
- UPDOWN_FILES = (UP_SCRIPT, DOWN_SCRIPT, OPENVPN_DOWN_PLUGIN)
OTHER_FILES = []
+ # TODO deprecate ------------------------------------------------
@classmethod
def cmd_for_missing_scripts(kls, frompath):
"""
@@ -87,7 +112,11 @@ class DarwinVPNLauncher(VPNLauncher):
:returns: True if kext is loaded, False otherwise.
:rtype: bool
"""
- return bool(commands.getoutput('kextstat | grep "leap.tun"'))
+ loaded = bool(commands.getoutput(
+ 'kextstat | grep "net.sf.tuntaposx.tun"'))
+ if not loaded:
+ logger.error("tuntaposx extension not loaded!")
+ return loaded
@classmethod
def _get_icon_path(kls):
@@ -101,6 +130,7 @@ class DarwinVPNLauncher(VPNLauncher):
return os.path.join(resources_path, "bitmask.tiff")
+ # TODO deprecate ---------------------------------------------------------
@classmethod
def get_cocoasudo_ovpn_cmd(kls):
"""
@@ -120,6 +150,7 @@ class DarwinVPNLauncher(VPNLauncher):
return kls.COCOASUDO, args
+ # TODO deprecate ---------------------------------------------------------
@classmethod
def get_cocoasudo_installmissing_cmd(kls):
"""
@@ -171,12 +202,6 @@ class DarwinVPNLauncher(VPNLauncher):
# we use `super` in order to send the class to use
command = super(DarwinVPNLauncher, kls).get_vpn_command(
eipconfig, providerconfig, socket_host, socket_port, openvpn_verb)
-
- cocoa, cargs = kls.get_cocoasudo_ovpn_cmd()
- cargs.extend(command)
- command = cargs
- command.insert(0, cocoa)
-
command.extend(['--setenv', "LEAPUSER", getpass.getuser()])
return command
diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py
index c48f857c..16dfd9cf 100644
--- a/src/leap/bitmask/services/eip/vpnlauncher.py
+++ b/src/leap/bitmask/services/eip/vpnlauncher.py
@@ -29,7 +29,7 @@ from leap.bitmask.config import flags
from leap.bitmask.logs.utils import get_logger
from leap.bitmask.backend.settings import Settings, GATEWAY_AUTOMATIC
from leap.bitmask.config.providerconfig import ProviderConfig
-from leap.bitmask.platform_init import IS_LINUX
+from leap.bitmask.platform_init import IS_LINUX, IS_MAC
from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector
from leap.bitmask.util import force_eval
from leap.common.check import leap_assert, leap_assert_type
@@ -286,8 +286,8 @@ class VPNLauncher(object):
:rtype: list
"""
# FIXME
- # XXX remove method when we ditch UPDOWN in osx and win too
- if IS_LINUX:
+ # XXX remove method when we ditch UPDOWN in win too
+ if IS_LINUX or IS_MAC:
return []
else:
leap_assert(kls.UPDOWN_FILES is not None,
@@ -308,7 +308,7 @@ class VPNLauncher(object):
"""
leap_assert(kls.OTHER_FILES is not None,
"Need to define OTHER_FILES for this particular "
- "auncher before calling this method")
+ "launcher before calling this method")
other = force_eval(kls.OTHER_FILES)
file_exist = partial(_has_other_files, warn=False)
diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py
index 586b50f5..580bd572 100644
--- a/src/leap/bitmask/services/eip/vpnprocess.py
+++ b/src/leap/bitmask/services/eip/vpnprocess.py
@@ -41,6 +41,7 @@ from leap.bitmask.config.providerconfig import ProviderConfig
from leap.bitmask.logs.utils import get_logger
from leap.bitmask.services.eip import get_vpn_launcher
from leap.bitmask.services.eip import linuxvpnlauncher
+from leap.bitmask.services.eip import darwinvpnlauncher
from leap.bitmask.services.eip.eipconfig import EIPConfig
from leap.bitmask.services.eip.udstelnet import UDSTelnet
from leap.bitmask.util import first, force_eval
@@ -145,7 +146,7 @@ class VPN(object):
demand.
"""
TERMINATE_MAXTRIES = 10
- TERMINATE_WAIT = 1 # secs
+ TERMINATE_WAIT = 2 # secs
OPENVPN_VERB = "openvpn_verb"
@@ -173,7 +174,8 @@ class VPN(object):
:param kwargs: kwargs to be passed to the VPNProcess
:type kwargs: dict
"""
- logger.debug('VPN: start')
+ logger.debug(
+ 'VPN: start ---------------------------------------------------')
self._user_stopped = False
self._stop_pollers()
kwargs['openvpn_verb'] = self._openvpn_verb
@@ -181,23 +183,6 @@ class VPN(object):
restart = kwargs.pop('restart', False)
- # start the main vpn subprocess
- vpnproc = VPNProcess(*args, **kwargs)
-
- if vpnproc.get_openvpn_process():
- logger.info("Another vpn process is running. Will try to stop it.")
- vpnproc.stop_if_already_running()
-
- # we try to bring the firewall up
- if IS_LINUX:
- gateways = vpnproc.getGateways()
- firewall_up = self._launch_firewall(gateways,
- restart=restart)
- if not restart and not firewall_up:
- logger.error("Could not bring firewall up, "
- "aborting openvpn launch.")
- return
-
# FIXME it would be good to document where the
# errors here are catched, since we currently handle them
# at the frontend layer. This *should* move to be handled entirely
@@ -211,17 +196,54 @@ class VPN(object):
# the ping-pong to the frontend, and without adding any logical checks
# in the frontend. We should just communicate UI changes to frontend,
# and abstract us away from anything else.
- try:
+
+ # TODO factor this out to the platform-launchers
+
+ if IS_LINUX:
+ # start the main vpn subprocess
+ vpnproc = VPNProcess(*args, **kwargs)
cmd = vpnproc.getCommand()
- except Exception as e:
- logger.error("Error while getting vpn command... {0!r}".format(e))
- raise
+
+ if vpnproc.get_openvpn_process():
+ logger.info(
+ "Another vpn process is running. Will try to stop it.")
+ vpnproc.stop_if_already_running()
+
+ # we try to bring the firewall up
+ gateways = vpnproc.getGateways()
+ firewall_up = self._launch_firewall_linux(
+ gateways, restart=restart)
+ if not restart and not firewall_up:
+ logger.error("Could not bring firewall up, "
+ "aborting openvpn launch.")
+ return
+
+ if IS_MAC:
+ # start the main vpn subprocess
+ vpnproc = VPNCanary(*args, **kwargs)
+
+ # we try to bring the firewall up
+ gateways = vpnproc.getGateways()
+ firewall_up = self._launch_firewall_osx(
+ gateways, restart=restart)
+ if not restart and not firewall_up:
+ logger.error("Could not bring firewall up, "
+ "aborting openvpn launch.")
+ return
+
+ helper = darwinvpnlauncher.DarwinHelperCommand()
+ cmd = vpnproc.getVPNCommand()
+ result = helper.send('openvpn_start %s' % ' '.join(cmd))
+
+ # TODO Windows version -- should be similar to osx.
env = os.environ
for key, val in vpnproc.vpn_env.items():
env[key] = val
- reactor.spawnProcess(vpnproc, cmd[0], cmd, env)
+ cmd = vpnproc.getCommand()
+ running_proc = reactor.spawnProcess(vpnproc, cmd[0], cmd, env)
+ vpnproc.pid = running_proc.pid
self._vpnproc = vpnproc
# add pollers for status and state
@@ -233,9 +255,9 @@ class VPN(object):
self._pollers.extend(poll_list)
self._start_pollers()
- def _launch_firewall(self, gateways, restart=False):
+ def _launch_firewall_linux(self, gateways, restart=False):
"""
- Launch the firewall using the privileged wrapper.
+ Launch the firewall using the privileged wrapper (linux).
:param gateways:
:type gateways: list
@@ -254,40 +276,65 @@ class VPN(object):
exitCode = subprocess.call(cmd + gateways)
return True if exitCode is 0 else False
+ def _launch_firewall_osx(self, gateways, restart=False):
+ cmd = 'firewall_start %s' % ' '.join(gateways)
+ helper = darwinvpnlauncher.DarwinHelperCommand()
+ result = helper.send(cmd)
+ return True
+
+ # TODO -- write LINUX/OSX VERSION too ------------------------------------
def is_fw_down(self):
"""
Return whether the firewall is down or not.
:rtype: bool
"""
- BM_ROOT = force_eval(linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT)
- fw_up_cmd = "pkexec {0} firewall isup".format(BM_ROOT)
- fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256
- return fw_is_down()
+ if IS_LINUX:
+ BM_ROOT = force_eval(
+ linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT)
+ fw_up_cmd = "pkexec {0} firewall isup".format(BM_ROOT)
+ fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256
+ return fw_is_down()
+
+ if IS_MAC:
+ cmd = 'firewall_isup'
+ helper = darwinvpnlauncher.DarwinHelperCommand()
+ result = helper.send(cmd)
+ return True
def tear_down_firewall(self):
"""
Tear the firewall down using the privileged wrapper.
"""
if IS_MAC:
- # We don't support Mac so far
+ cmd = 'firewall_stop'
+ helper = darwinvpnlauncher.DarwinHelperCommand()
+ result = helper.send(cmd)
return True
- BM_ROOT = force_eval(linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT)
- exitCode = subprocess.call(["pkexec",
- BM_ROOT, "firewall", "stop"])
- return True if exitCode is 0 else False
+
+ if IS_LINUX:
+ BM_ROOT = force_eval(
+ linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT)
+ exitCode = subprocess.call(["pkexec",
+ BM_ROOT, "firewall", "stop"])
+ return True if exitCode is 0 else False
def bitmask_root_vpn_down(self):
"""
Bring openvpn down using the privileged wrapper.
"""
if IS_MAC:
- # We don't support Mac so far
+ cmd = 'openvpn_stop'
+ helper = darwinvpnlauncher.DarwinHelperCommand()
+ result = helper.send(cmd)
return True
- BM_ROOT = force_eval(linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT)
- exitCode = subprocess.call(["pkexec",
- BM_ROOT, "openvpn", "stop"])
- return True if exitCode is 0 else False
+
+ if IS_LINUX:
+ BM_ROOT = force_eval(
+ linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT)
+ exitCode = subprocess.call(["pkexec",
+ BM_ROOT, "openvpn", "stop"])
+ return True if exitCode is 0 else False
def _kill_if_left_alive(self, tries=0):
"""
@@ -297,18 +344,18 @@ class VPN(object):
:param tries: counter of tries, used in recursion
:type tries: int
"""
+ # we try to tear the firewall down
+ if (IS_LINUX or IS_MAC) and self._user_stopped:
+ logger.debug('trying to bring firewall down...')
+ firewall_down = self.tear_down_firewall()
+ if firewall_down:
+ logger.debug("Firewall down")
+ else:
+ logger.warning("Could not tear firewall down")
+
while tries < self.TERMINATE_MAXTRIES:
if self._vpnproc.transport.pid is None:
logger.debug("Process has been happily terminated.")
-
- # we try to tear the firewall down
- if IS_LINUX and self._user_stopped:
- firewall_down = self.tear_down_firewall()
- if firewall_down:
- logger.debug("Firewall down")
- else:
- logger.warning("Could not tear firewall down")
-
return
else:
logger.debug("Process did not die, waiting...")
@@ -813,6 +860,8 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
programmatically.
"""
+ pid = None
+
def __init__(self, eipconfig, providerconfig, socket_host, socket_port,
signaler, openvpn_verb):
"""
@@ -861,7 +910,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
self._vpn_observer = VPNObserver(signaler)
self.is_restart = False
- # processProtocol methods
+ # ProcessProtocol methods
def connectionMade(self):
"""
@@ -893,8 +942,11 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
.. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa
"""
exit_code = reason.value.exitCode
+
if isinstance(exit_code, int):
logger.debug("processExited, status %d" % (exit_code,))
+ else:
+ exit_code = 0
self._signaler.signal(
self._signaler.eip_process_finished, exit_code)
self._alive = False
@@ -976,3 +1028,41 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
self.transport.signalProcess('KILL')
except internet_error.ProcessExitedAlready:
logger.debug('Process Exited Already')
+
+
+class VPNCanary(VPNProcess):
+
+ """
+ This is a Canary Process that does not run openvpn itself, but it's
+ notified by the privileged process when the process dies.
+
+ This is an ugly workaround to allow the qt signals and the processprotocol
+ to live happily together until we refactor EIP out of the qt model
+ completely.
+ """
+
+ def connectionMade(self):
+ VPNProcess.connectionMade(self)
+ reactor.callLater(2, self.registerPID)
+
+ def registerPID(self):
+ helper = darwinvpnlauncher.DarwinHelperCommand()
+ cmd = 'openvpn_set_watcher %s' % self.pid
+ result = helper.send(cmd)
+
+ def killProcess(self):
+ helper = darwinvpnlauncher.DarwinHelperCommand()
+ cmd = 'openvpn_force_stop'
+ result = helper.send(cmd)
+
+ def getVPNCommand(self):
+ return VPNProcess.getCommand(self)
+
+ def getCommand(self):
+ canary = '''import sys, signal, time
+def receive_signal(signum, stack):
+ sys.exit()
+signal.signal(signal.SIGTERM, receive_signal)
+while True:
+ time.sleep(60)'''
+ return ['python', '-c', '%s' % canary]
diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py
index 68197d9d..cccbcf14 100644
--- a/src/leap/bitmask/services/mail/conductor.py
+++ b/src/leap/bitmask/services/mail/conductor.py
@@ -18,6 +18,7 @@
Mail Services Conductor
"""
from leap.bitmask.config import flags
+from leap.bitmask.config.leapsettings import LeapSettings
from leap.bitmask.logs.utils import get_logger
from leap.bitmask.gui import statemachines
from leap.bitmask.services.mail import connection as mail_connection
@@ -34,6 +35,7 @@ class IMAPControl(object):
"""
Methods related to IMAP control.
"""
+
def __init__(self):
"""
Initializes smtp variables.
@@ -73,12 +75,13 @@ class IMAPControl(object):
self._backend.imap_stop_service()
- def _handle_imap_events(self, event, content):
+ def _handle_imap_events(self, event, userid=None, content=None):
"""
Callback handler for the IMAP events
:param event: The event that triggered the callback.
:type event: str
+ :param userid: The user id of the logged in user. Ignored.
:param content: The content of the event.
:type content: list
"""
@@ -113,10 +116,11 @@ class IMAPControl(object):
"""
Callback for IMAP failed state.
"""
- self.imap_connection.qtsigs.connetion_aborted_signal.emit()
+ self.imap_connection.qtsigs.connection_aborted_signal.emit()
class SMTPControl(object):
+
def __init__(self):
"""
Initializes smtp variables.
@@ -189,7 +193,17 @@ class SMTPControl(object):
self.smtp_connection.qtsigs.connection_aborted_signal.emit()
-class MailConductor(IMAPControl, SMTPControl):
+class PixelatedControl(object):
+
+ def start_pixelated_service(self):
+ self._backend.pixelated_start_service(
+ full_user_id=self.userid)
+
+ def stop_pixelated_service(self):
+ pass
+
+
+class MailConductor(IMAPControl, SMTPControl, PixelatedControl):
"""
This class encapsulates everything related to the initialization and
process control for the mail services.
@@ -266,6 +280,11 @@ class MailConductor(IMAPControl, SMTPControl):
self.start_smtp_service(download_if_needed=download_if_needed)
self.start_imap_service()
+ settings = LeapSettings()
+ pixelmail = settings.get_pixelmail_enabled()
+ if pixelmail:
+ self.start_pixelated_service()
+
self._mail_services_started = True
def stop_mail_services(self):
diff --git a/src/leap/bitmask/services/mail/imap.py b/src/leap/bitmask/services/mail/imap.py
index 5934756d..7875a4af 100644
--- a/src/leap/bitmask/services/mail/imap.py
+++ b/src/leap/bitmask/services/mail/imap.py
@@ -20,11 +20,14 @@ Initialization of imap service
import os
import sys
+from twisted.python import log
+
from leap.bitmask.logs.utils import get_logger
from leap.mail.constants import INBOX_NAME
from leap.mail.imap.service import imap
from leap.mail.incoming.service import IncomingMail, INCOMING_CHECK_PERIOD
-from twisted.python import log
+from leap.mail.mail import Account
+
logger = get_logger()
@@ -57,11 +60,13 @@ def get_mail_check_period():
return period
-def start_imap_service(*args, **kwargs):
+def start_imap_service(soledad_sessions):
"""
Initializes and run imap service.
- :returns: twisted.internet.task.LoopingCall instance
+ :returns: the port as returned by the reactor when starts listening, and
+ the factory for the protocol.
+ :rtype: tuple
"""
from leap.bitmask.config import flags
logger.debug('Launching imap service')
@@ -70,10 +75,10 @@ def start_imap_service(*args, **kwargs):
log.startLogging(open(flags.MAIL_LOGFILE, 'w'))
log.startLogging(sys.stdout)
- return imap.run_service(*args, **kwargs)
+ return imap.run_service(soledad_sessions)
-def start_incoming_mail_service(keymanager, soledad, imap_factory, userid):
+def start_incoming_mail_service(keymanager, soledad, userid):
"""
Initalizes and starts the incomming mail service.
@@ -81,19 +86,12 @@ def start_incoming_mail_service(keymanager, soledad, imap_factory, userid):
"""
def setUpIncomingMail(inbox):
incoming_mail = IncomingMail(
- keymanager,
- soledad,
- inbox.collection,
- userid,
+ keymanager, soledad,
+ inbox, userid,
check_period=get_mail_check_period())
return incoming_mail
- # XXX: do I really need to know here how to get a mailbox??
- # XXX: ideally, the parent service in mail would take care of initializing
- # the account, and passing the mailbox to the incoming service.
- # In an even better world, we just would subscribe to a channel that would
- # pass us the serialized object to be inserted.
- acc = imap_factory.theAccount
- d = acc.callWhenReady(lambda _: acc.getMailbox(INBOX_NAME))
+ acc = Account(soledad, userid)
+ d = acc.callWhenReady(lambda _: acc.get_collection_by_mailbox(INBOX_NAME))
d.addCallback(setUpIncomingMail)
return d
diff --git a/src/leap/bitmask/services/mail/imapcontroller.py b/src/leap/bitmask/services/mail/imapcontroller.py
index 5053d897..855fb74b 100644
--- a/src/leap/bitmask/services/mail/imapcontroller.py
+++ b/src/leap/bitmask/services/mail/imapcontroller.py
@@ -60,9 +60,9 @@ class IMAPController(object):
"""
logger.debug('Starting imap service')
+ soledad_sessions = {userid: self._soledad}
self.imap_port, self.imap_factory = imap.start_imap_service(
- self._soledad,
- userid=userid)
+ soledad_sessions)
def start_and_assign_incoming_service(incoming_mail):
# this returns a deferred that will be called when the looping call
@@ -74,9 +74,7 @@ class IMAPController(object):
if offline is False:
d = imap.start_incoming_mail_service(
- self._keymanager,
- self._soledad,
- self.imap_factory,
+ self._keymanager, self._soledad,
userid)
d.addCallback(start_and_assign_incoming_service)
d.addErrback(lambda f: logger.error(f.printTraceback()))
diff --git a/src/leap/bitmask/services/mail/plumber.py b/src/leap/bitmask/services/mail/plumber.py
index 43203f0c..cd1f06bb 100644
--- a/src/leap/bitmask/services/mail/plumber.py
+++ b/src/leap/bitmask/services/mail/plumber.py
@@ -60,6 +60,7 @@ def initialize_soledad(uuid, email, passwd,
cert_file = ""
class Mock(object):
+
def __init__(self, return_value=None):
self._return = return_value
@@ -140,7 +141,7 @@ class MBOXPlumber(object):
self.sol = initialize_soledad(
self.uuid, self.userid, self.passwd,
secrets, localdb, "/tmp", "/tmp")
- self.acct = IMAPAccount(self.userid, self.sol)
+ self.acct = IMAPAccount(self.sol, self.userid)
return True
#
diff --git a/src/leap/bitmask/services/mail/smtpbootstrapper.py b/src/leap/bitmask/services/mail/smtpbootstrapper.py
index a577509e..f73687a7 100644
--- a/src/leap/bitmask/services/mail/smtpbootstrapper.py
+++ b/src/leap/bitmask/services/mail/smtpbootstrapper.py
@@ -19,6 +19,7 @@ SMTP bootstrapping
"""
import os
import warnings
+from collections import namedtuple
from requests.exceptions import HTTPError
@@ -28,7 +29,6 @@ from leap.bitmask.logs.utils import get_logger
from leap.bitmask.services import download_service_config
from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper
from leap.bitmask.services.mail.smtpconfig import SMTPConfig
-from leap.bitmask.util import is_file
from leap.common import certs as leap_certs
from leap.common.check import leap_assert
@@ -92,11 +92,13 @@ class SMTPBootstrapper(AbstractBootstrapper):
client_cert_path = self._smtp_config.get_client_cert_path(
self._userid, self._provider_config, about_to_download=True)
- if not is_file(client_cert_path):
+ needs_download = leap_certs.should_redownload(client_cert_path)
+
+ if needs_download:
# For re-download if something is wrong with the cert
+ # FIXME this doesn't read well. should reword the logic here.
self._download_if_needed = (
- self._download_if_needed and
- not leap_certs.should_redownload(client_cert_path))
+ self._download_if_needed and not needs_download)
if self._download_if_needed and os.path.isfile(client_cert_path):
check_and_fix_urw_only(client_cert_path)
@@ -127,9 +129,6 @@ class SMTPBootstrapper(AbstractBootstrapper):
Start the smtp service using the downloaded configurations.
"""
# TODO Make the encrypted_only configurable
- # TODO pick local smtp port in a better way
- # TODO remove hard-coded port and let leap.mail set
- # the specific default.
# TODO handle more than one host and define how to choose
hosts = self._smtp_config.get_hosts()
hostname = hosts.keys()[0]
@@ -138,19 +137,25 @@ class SMTPBootstrapper(AbstractBootstrapper):
client_cert_path = self._smtp_config.get_client_cert_path(
self._userid, self._provider_config, about_to_download=True)
- from leap.mail.smtp import setup_smtp_gateway
+ # XXX this should be defined in leap.mail.smtp, it's in bitmask.core
+ # right now.
+ SendmailOpts = namedtuple(
+ 'SendmailOpts', ['cert', 'key', 'hostname', 'port'])
+
+ userid = self._userid
+ soledad_sessions = {userid: self._soledad}
+ keymanager_sessions = {userid: self._keymanager}
+
+ key = cert = client_cert_path
+ opts = SendmailOpts(cert, key, host, port)
+ sendmail_opts = {userid: opts}
- self._smtp_service, self._smtp_port = setup_smtp_gateway(
- port=2013,
- userid=self._userid,
- keymanager=self._keymanager,
- smtp_host=host,
- smtp_port=port,
- smtp_cert=client_cert_path,
- smtp_key=client_cert_path,
- encrypted_only=False)
+ from leap.mail.smtp import run_service
+ self._smtp_service, self._smtp_port = run_service(
+ soledad_sessions, keymanager_sessions, sendmail_opts)
- def start_smtp_service(self, keymanager, userid, download_if_needed=False):
+ def start_smtp_service(self, soledad, keymanager, userid,
+ download_if_needed=False):
"""
Starts the SMTP service.
@@ -170,6 +175,7 @@ class SMTPBootstrapper(AbstractBootstrapper):
raise MalformedUserId()
self._provider_config = ProviderConfig.get_provider_config(domain)
+ self._soledad = soledad
self._keymanager = keymanager
self._smtp_config = SMTPConfig()
self._userid = str(userid)
diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py
index 60a2130b..21cdee31 100644
--- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py
+++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py
@@ -331,6 +331,9 @@ class SoledadBootstrapper(AbstractBootstrapper):
:returns: the server url
:rtype: unicode
"""
+ if not self._soledad_config:
+ self._soledad_config = SoledadConfig()
+
# TODO: Select server based on timezone (issue #3308)
server_dict = self._soledad_config.get_hosts()
@@ -654,7 +657,7 @@ class Syncer(object):
logger.error('Invalid auth token while trying to sync Soledad')
self._signaler.signal(
self._signaler.soledad_invalid_auth_token)
- self._callback_deferred.fail(failure)
+ self._callback_deferred.errback(failure)
elif failure.check(sqlite_ProgrammingError,
sqlcipher_ProgrammingError):
logger.exception("%r" % (failure.value,))
diff --git a/src/leap/bitmask/util/keyring_helpers.py b/src/leap/bitmask/util/keyring_helpers.py
index d81f39b1..c5181348 100644
--- a/src/leap/bitmask/util/keyring_helpers.py
+++ b/src/leap/bitmask/util/keyring_helpers.py
@@ -24,8 +24,9 @@ try:
EncryptedKeyring,
PlaintextKeyring
]
- canuse = lambda kr: (kr is not None
- and kr.__class__ not in OBSOLETE_KEYRINGS)
+ canuse = lambda kr: (
+ kr is not None and
+ kr.__class__ not in OBSOLETE_KEYRINGS)
except Exception:
# Problems when importing keyring! It might be a problem binding to the
diff --git a/src/leap/bitmask/util/pastebin.py b/src/leap/bitmask/util/pastebin.py
index 6d50ac9e..b476ad88 100644
--- a/src/leap/bitmask/util/pastebin.py
+++ b/src/leap/bitmask/util/pastebin.py
@@ -25,13 +25,13 @@
#############################################################################
+import urllib
+
__ALL__ = ['delete_paste', 'user_details', 'trending', 'pastes_by_user',
'generate_user_key', 'paste', 'Pastebin', 'PastebinError',
'PostLimitError']
-import urllib
-
class PastebinError(RuntimeError):
"""Pastebin API error.
diff --git a/thirdparty/gnupg/build_gnupg.sh b/thirdparty/gnupg/build_gnupg.sh
new file mode 100755
index 00000000..e125b684
--- /dev/null
+++ b/thirdparty/gnupg/build_gnupg.sh
@@ -0,0 +1,68 @@
+#!/usr/bin/env sh
+
+# ----------------------------------------------------------
+# Compile gnupg binary, to distribute with Bitmask bundles.
+# ----------------------------------------------------------
+# You will need to import the keys for the gnupg developers into your keyring,
+# see https://www.gnupg.org/download/integrity_check.html
+# and https://www.gnupg.org/signature_key.html
+
+# For osx specific details, see:
+# http://macgpg.sourceforge.net/docs/howto-build-gpg-osx.txt.asc
+# osx doesn't allow to build static binaries, see:
+# http://stackoverflow.com/questions/5259249/creating-static-mac-os-x-c-build
+
+set -e
+set -x
+
+gnupg_version="gnupg-1.4.20"
+url="ftp://ftp.gnupg.org/gcrypt/gnupg/$gnupg_version.tar.bz2"
+
+platform='unknown'
+unamestr=`uname`
+if [[ "$unamestr" == 'Linux' ]]; then
+ platform='linux'
+elif [[ "$unamestr" == 'Darwin' ]]; then
+ platform='osx'
+fi
+
+function prepare_source()
+{
+ wget -c $url -O $gnupg_version.tar.bz2;
+ wget -c $url.sig -O $gnupg_version.tar.bz2.sig;
+ #gpg --verify $gnupg_version.tar.bz2.sig $gnupg_version.tar.bz2;
+ tar -xjf $gnupg_version.tar.bz2;
+ cd $gnupg_version;
+}
+
+
+function build_static_gpg()
+{
+ ./configure CFLAGS="-static";
+ make;
+}
+
+function build_gpg()
+{
+ ./configure;
+ make;
+}
+
+function copy_to_builddir()
+{
+ mkdir -p ~/leap_thirdparty_build
+ cp g10/gpg ~/leap_thirdparty_build
+}
+
+function main()
+{
+ if [[ $platform == 'linux' ]]; then
+ (prepare_source; build_static_gpg; copy_to_builddir)
+ elif [[ $platform == 'osx' ]]; then
+ (prepare_source; build_gpg; copy_to_builddir)
+ fi
+
+}
+
+main "$@"
+
diff --git a/openvpn/README b/thirdparty/openvpn/README
index bf2205c2..bf2205c2 100644
--- a/openvpn/README
+++ b/thirdparty/openvpn/README
diff --git a/openvpn/Sources b/thirdparty/openvpn/Sources
index e2fe7bb3..e2fe7bb3 100644
--- a/openvpn/Sources
+++ b/thirdparty/openvpn/Sources
diff --git a/openvpn/build.zsh b/thirdparty/openvpn/build.zsh.old
index b36717c1..b36717c1 100755
--- a/openvpn/build.zsh
+++ b/thirdparty/openvpn/build.zsh.old
diff --git a/thirdparty/openvpn/openvpn.sh b/thirdparty/openvpn/openvpn.sh
new file mode 100755
index 00000000..db63c987
--- /dev/null
+++ b/thirdparty/openvpn/openvpn.sh
@@ -0,0 +1,123 @@
+#!/bin/bash
+
+set -e
+set -x
+
+mkdir -p ~/openvpn && cd ~/openvpn
+
+BASE=`pwd`
+SRC=$BASE/src
+WGET="wget --prefer-family=IPv4"
+DEST=$BASE/stuff
+LDFLAGS="-L$DEST/lib -Wl"
+CPPFLAGS="-I$DEST/include"
+CFLAGS="-O3"
+CXXFLAGS=$CFLAGS
+CONFIGURE="./configure --prefix=/stuff"
+MAKE="make -j2"
+mkdir -p $SRC
+
+######## ####################################################################
+# ZLIB # ####################################################################
+######## ####################################################################
+
+mkdir $SRC/zlib && cd $SRC/zlib
+
+if [ ! -f zlib-1.2.8.tar.gz ]; then
+ $WGET http://zlib.net/zlib-1.2.8.tar.gz
+fi
+tar zxvf zlib-1.2.8.tar.gz
+cd zlib-1.2.8
+
+LDFLAGS=$LDFLAGS \
+CPPFLAGS=$CPPFLAGS \
+CFLAGS=$CFLAGS \
+CXXFLAGS=$CXXFLAGS \
+./configure \
+--prefix=/stuff
+
+$MAKE
+make install DESTDIR=$BASE
+
+########### #################################################################
+# OPENSSL # #################################################################
+########### #################################################################
+
+#mkdir -p $SRC/openssl && cd $SRC/openssl
+#if [ ! -f openssl-1.0.2f.tar.gz ]; then
+# $WGET https://www.openssl.org/source/openssl-1.0.2f.tar.gz
+#fi
+#tar zxvf openssl-1.0.2f.tar.gz
+#cd openssl-1.0.2f
+
+#./Configure darwin64-x86_64-cc \
+#-Wl \
+#--prefix=/opts zlib \
+#--with-zlib-lib=$DEST/lib \
+#--with-zlib-include=$DEST/include
+
+#$MAKE
+#make install INSTALLTOP=$DEST OPENSSLDIR=$DEST/ssl
+
+############ #################################################################
+# POLARSSL # #################################################################
+############ #################################################################
+
+mkdir -p $SRC/polarssl && cd $SRC/polarssl
+if [ ! -f polarssl-1.3.9-gpl.tgz ]; then
+ $WGET https://tls.mbed.org/download/polarssl-1.3.9-gpl.tgz
+fi
+tar zxvf polarssl-1.3.9-gpl.tgz
+cd polarssl-1.3.9
+mkdir build
+cd build
+cmake ..
+$MAKE
+make install DESTDIR=$BASE
+
+######## ####################################################################
+# LZO2 # ####################################################################
+######## ####################################################################
+
+mkdir $SRC/lzo2 && cd $SRC/lzo2
+if [ ! -f lzo-2.09.tar.gz ]; then
+ $WGET http://www.oberhumer.com/opensource/lzo/download/lzo-2.09.tar.gz
+fi
+tar zxvf lzo-2.09.tar.gz
+cd lzo-2.09
+
+LDFLAGS=$LDFLAGS \
+CPPFLAGS=$CPPFLAGS \
+CFLAGS=$CFLAGS \
+CXXFLAGS=$CXXFLAGS \
+$CONFIGURE
+
+$MAKE
+make install DESTDIR=$BASE
+
+########### #################################################################
+# OPENVPN # #################################################################
+########### #################################################################
+
+mkdir $SRC/openvpn && cd $SRC/openvpn
+if [ ! -f openvpn-2.3.10.tar.gz ]; then
+ $WGET http://swupdate.openvpn.org/community/releases/openvpn-2.3.10.tar.gz
+fi
+tar zxvf openvpn-2.3.10.tar.gz
+cd openvpn-2.3.10
+
+# OPENSSL_SSL_LIBS=$DEST/lib/
+
+POLARSSL_CFLAGS=-I$DEST/usr/local/include \
+POLARSSL_LIBS=$DEST/lib/libpolarssl.a \
+LDFLAGS=$LDFLAGS \
+CPPFLAGS=$CPPFLAGS \
+CFLAGS=$CFLAGS \
+CXXFLAGS=$CXXFLAGS \
+$CONFIGURE \
+--disable-plugin-auth-pam \
+--enable-password-save \
+--with-crypto-library=polarssl
+
+$MAKE LIBS="-all-static -lssl -lcrypto -lz -llzo2"
+make install DESTDIR=$BASE/openvpn
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index e041515d..00000000
--- a/tox.ini
+++ /dev/null
@@ -1,12 +0,0 @@
-[tox]
-envlist = py26,py27
-
-[testenv]
-deps = -r{toxinidir}/pkg/requirements.pip
- -r{toxinidir}/pkg/requirements-testing.pip
-sitepackages = True
-commands = xvfb-run nosetests leap --first-package-wins --exclude=soledad*
-
-[testenv:pep8]
-deps = pep8==1.1
-commands = pep8 --repeat --show-source src/leap setup.py --ignore=E202,W602 --exclude=*_rc.py --repeat
diff --git a/versioneer.py b/versioneer.py
index 885ebcfd..24a69025 100644
--- a/versioneer.py
+++ b/versioneer.py
@@ -1,170 +1,641 @@
-#! /usr/bin/python
-"""versioneer.py
+# Version: 0.16
-(like a rocketeer, but for versions)
+"""The Versioneer - like a rocketeer, but for versions.
+The Versioneer
+==============
+
+* like a rocketeer, but for versions!
* https://github.com/warner/python-versioneer
* Brian Warner
* License: Public Domain
-* Version: 0.7+
-
-This file helps distutils-based projects manage their version number by just
-creating version-control tags.
-
-For developers who work from a VCS-generated tree (e.g. 'git clone' etc),
-each 'setup.py version', 'setup.py build', 'setup.py sdist' will compute a
-version number by asking your version-control tool about the current
-checkout. The version number will be written into a generated _version.py
-file of your choosing, where it can be included by your __init__.py
-
-For users who work from a VCS-generated tarball (e.g. 'git archive'), it will
-compute a version number by looking at the name of the directory created when
-te tarball is unpacked. This conventionally includes both the name of the
-project and a version number.
-
-For users who work from a tarball built by 'setup.py sdist', it will get a
-version number from a previously-generated _version.py file.
-
-As a result, loading code directly from the source tree will not result in a
-real version. If you want real versions from VCS trees (where you frequently
-update from the upstream repository, or do new development), you will need to
-do a 'setup.py version' after each update, and load code from the build/
-directory.
-
-You need to provide this code with a few configuration values:
-
- versionfile_source:
- A project-relative pathname into which the generated version strings
- should be written. This is usually a _version.py next to your project's
- main __init__.py file. If your project uses src/myproject/__init__.py,
- this should be 'src/myproject/_version.py'. This file should be checked
- in to your VCS as usual: the copy created below by 'setup.py
- update_files' will include code that parses expanded VCS keywords in
- generated tarballs. The 'build' and 'sdist' commands will replace it with
- a copy that has just the calculated version string.
-
- versionfile_build:
- Like versionfile_source, but relative to the build directory instead of
- the source directory. These will differ when your setup.py uses
- 'package_dir='. If you have package_dir={'myproject': 'src/myproject'},
- then you will probably have versionfile_build='myproject/_version.py' and
- versionfile_source='src/myproject/_version.py'.
-
- tag_prefix: a string, like 'PROJECTNAME-', which appears at the start of all
- VCS tags. If your tags look like 'myproject-1.2.0', then you
- should use tag_prefix='myproject-'. If you use unprefixed tags
- like '1.2.0', this should be an empty string.
-
- parentdir_prefix: a string, frequently the same as tag_prefix, which
- appears at the start of all unpacked tarball filenames. If
- your tarball unpacks into 'myproject-1.2.0', this should
- be 'myproject-'.
-
-To use it:
-
- 1: include this file in the top level of your project
- 2: make the following changes to the top of your setup.py:
- import versioneer
- versioneer.versionfile_source = 'src/myproject/_version.py'
- versioneer.versionfile_build = 'myproject/_version.py'
- versioneer.tag_prefix = '' # tags are like 1.2.0
- versioneer.parentdir_prefix = 'myproject-' # dirname like 'myproject-1.2.0'
- 3: add the following arguments to the setup() call in your setup.py:
- version=versioneer.get_version(),
- cmdclass=versioneer.get_cmdclass(),
- 4: run 'setup.py update_files', which will create _version.py, and will
- modify your __init__.py to define __version__ (by calling a function
- from _version.py)
- 5: modify your MANIFEST.in to include versioneer.py
- 6: add both versioneer.py and the generated _version.py to your VCS
-"""
+* Compatible With: python2.6, 2.7, 3.3, 3.4, 3.5, and pypy
+* [![Latest Version]
+(https://pypip.in/version/versioneer/badge.svg?style=flat)
+](https://pypi.python.org/pypi/versioneer/)
+* [![Build Status]
+(https://travis-ci.org/warner/python-versioneer.png?branch=master)
+](https://travis-ci.org/warner/python-versioneer)
+
+This is a tool for managing a recorded version number in distutils-based
+python projects. The goal is to remove the tedious and error-prone "update
+the embedded version string" step from your release process. Making a new
+release should be as easy as recording a new tag in your version-control
+system, and maybe making new tarballs.
+
+
+## Quick Install
+
+* `pip install versioneer` to somewhere to your $PATH
+* add a `[versioneer]` section to your setup.cfg (see below)
+* run `versioneer install` in your source tree, commit the results
+
+## Version Identifiers
+
+Source trees come from a variety of places:
+
+* a version-control system checkout (mostly used by developers)
+* a nightly tarball, produced by build automation
+* a snapshot tarball, produced by a web-based VCS browser, like github's
+ "tarball from tag" feature
+* a release tarball, produced by "setup.py sdist", distributed through PyPI
+
+Within each source tree, the version identifier (either a string or a number,
+this tool is format-agnostic) can come from a variety of places:
+
+* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows
+ about recent "tags" and an absolute revision-id
+* the name of the directory into which the tarball was unpacked
+* an expanded VCS keyword ($Id$, etc)
+* a `_version.py` created by some earlier build step
+
+For released software, the version identifier is closely related to a VCS
+tag. Some projects use tag names that include more than just the version
+string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool
+needs to strip the tag prefix to extract the version identifier. For
+unreleased software (between tags), the version identifier should provide
+enough information to help developers recreate the same tree, while also
+giving them an idea of roughly how old the tree is (after version 1.2, before
+version 1.3). Many VCS systems can report a description that captures this,
+for example `git describe --tags --dirty --always` reports things like
+"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the
+0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has
+uncommitted changes.
+
+The version identifier is used for multiple purposes:
+
+* to allow the module to self-identify its version: `myproject.__version__`
+* to choose a name and prefix for a 'setup.py sdist' tarball
+
+## Theory of Operation
+
+Versioneer works by adding a special `_version.py` file into your source
+tree, where your `__init__.py` can import it. This `_version.py` knows how to
+dynamically ask the VCS tool for version information at import time.
+
+`_version.py` also contains `$Revision$` markers, and the installation
+process marks `_version.py` to have this marker rewritten with a tag name
+during the `git archive` command. As a result, generated tarballs will
+contain enough information to get the proper version.
+
+To allow `setup.py` to compute a version too, a `versioneer.py` is added to
+the top level of your source tree, next to `setup.py` and the `setup.cfg`
+that configures it. This overrides several distutils/setuptools commands to
+compute the version when invoked, and changes `setup.py build` and `setup.py
+sdist` to replace `_version.py` with a small static file that contains just
+the generated version data.
+
+## Installation
+
+First, decide on values for the following configuration variables:
+
+* `VCS`: the version control system you use. Currently accepts "git".
+
+* `style`: the style of version string to be produced. See "Styles" below for
+ details. Defaults to "pep440", which looks like
+ `TAG[+DISTANCE.gSHORTHASH[.dirty]]`.
+
+* `versionfile_source`:
+
+ A project-relative pathname into which the generated version strings should
+ be written. This is usually a `_version.py` next to your project's main
+ `__init__.py` file, so it can be imported at runtime. If your project uses
+ `src/myproject/__init__.py`, this should be `src/myproject/_version.py`.
+ This file should be checked in to your VCS as usual: the copy created below
+ by `setup.py setup_versioneer` will include code that parses expanded VCS
+ keywords in generated tarballs. The 'build' and 'sdist' commands will
+ replace it with a copy that has just the calculated version string.
+
+ This must be set even if your project does not have any modules (and will
+ therefore never import `_version.py`), since "setup.py sdist" -based trees
+ still need somewhere to record the pre-calculated version strings. Anywhere
+ in the source tree should do. If there is a `__init__.py` next to your
+ `_version.py`, the `setup.py setup_versioneer` command (described below)
+ will append some `__version__`-setting assignments, if they aren't already
+ present.
+
+* `versionfile_build`:
+
+ Like `versionfile_source`, but relative to the build directory instead of
+ the source directory. These will differ when your setup.py uses
+ 'package_dir='. If you have `package_dir={'myproject': 'src/myproject'}`,
+ then you will probably have `versionfile_build='myproject/_version.py'` and
+ `versionfile_source='src/myproject/_version.py'`.
+
+ If this is set to None, then `setup.py build` will not attempt to rewrite
+ any `_version.py` in the built tree. If your project does not have any
+ libraries (e.g. if it only builds a script), then you should use
+ `versionfile_build = None`. To actually use the computed version string,
+ your `setup.py` will need to override `distutils.command.build_scripts`
+ with a subclass that explicitly inserts a copy of
+ `versioneer.get_version()` into your script file. See
+ `test/demoapp-script-only/setup.py` for an example.
+
+* `tag_prefix`:
+
+ a string, like 'PROJECTNAME-', which appears at the start of all VCS tags.
+ If your tags look like 'myproject-1.2.0', then you should use
+ tag_prefix='myproject-'. If you use unprefixed tags like '1.2.0', this
+ should be an empty string, using either `tag_prefix=` or `tag_prefix=''`.
+
+* `parentdir_prefix`:
+
+ a optional string, frequently the same as tag_prefix, which appears at the
+ start of all unpacked tarball filenames. If your tarball unpacks into
+ 'myproject-1.2.0', this should be 'myproject-'. To disable this feature,
+ just omit the field from your `setup.cfg`.
+
+This tool provides one script, named `versioneer`. That script has one mode,
+"install", which writes a copy of `versioneer.py` into the current directory
+and runs `versioneer.py setup` to finish the installation.
+
+To versioneer-enable your project:
+
+* 1: Modify your `setup.cfg`, adding a section named `[versioneer]` and
+ populating it with the configuration values you decided earlier (note that
+ the option names are not case-sensitive):
+
+ ````
+ [versioneer]
+ VCS = git
+ style = pep440
+ versionfile_source = src/myproject/_version.py
+ versionfile_build = myproject/_version.py
+ tag_prefix =
+ parentdir_prefix = myproject-
+ ````
+
+* 2: Run `versioneer install`. This will do the following:
+
+ * copy `versioneer.py` into the top of your source tree
+ * create `_version.py` in the right place (`versionfile_source`)
+ * modify your `__init__.py` (if one exists next to `_version.py`) to define
+ `__version__` (by calling a function from `_version.py`)
+ * modify your `MANIFEST.in` to include both `versioneer.py` and the
+ generated `_version.py` in sdist tarballs
+
+ `versioneer install` will complain about any problems it finds with your
+ `setup.py` or `setup.cfg`. Run it multiple times until you have fixed all
+ the problems.
+
+* 3: add a `import versioneer` to your setup.py, and add the following
+ arguments to the setup() call:
+
+ version=versioneer.get_version(),
+ cmdclass=versioneer.get_cmdclass(),
+
+* 4: commit these changes to your VCS. To make sure you won't forget,
+ `versioneer install` will mark everything it touched for addition using
+ `git add`. Don't forget to add `setup.py` and `setup.cfg` too.
+
+## Post-Installation Usage
+
+Once established, all uses of your tree from a VCS checkout should get the
+current version string. All generated tarballs should include an embedded
+version string (so users who unpack them will not need a VCS tool installed).
+
+If you distribute your project through PyPI, then the release process should
+boil down to two steps:
+
+* 1: git tag 1.0
+* 2: python setup.py register sdist upload
+
+If you distribute it through github (i.e. users use github to generate
+tarballs with `git archive`), the process is:
+
+* 1: git tag 1.0
+* 2: git push; git push --tags
+
+Versioneer will report "0+untagged.NUMCOMMITS.gHASH" until your tree has at
+least one tag in its history.
+
+## Version-String Flavors
+
+Code which uses Versioneer can learn about its version string at runtime by
+importing `_version` from your main `__init__.py` file and running the
+`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can
+import the top-level `versioneer.py` and run `get_versions()`.
+
+Both functions return a dictionary with different flavors of version
+information:
+
+* `['version']`: A condensed version string, rendered using the selected
+ style. This is the most commonly used value for the project's version
+ string. The default "pep440" style yields strings like `0.11`,
+ `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section
+ below for alternative styles.
+
+* `['full-revisionid']`: detailed revision identifier. For Git, this is the
+ full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac".
+
+* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that
+ this is only accurate if run in a VCS checkout, otherwise it is likely to
+ be False or None
+
+* `['error']`: if the version string could not be computed, this will be set
+ to a string describing the problem, otherwise it will be None. It may be
+ useful to throw an exception in setup.py if this is set, to avoid e.g.
+ creating tarballs with a version string of "unknown".
+
+Some variants are more useful than others. Including `full-revisionid` in a
+bug report should allow developers to reconstruct the exact code being tested
+(or indicate the presence of local changes that should be shared with the
+developers). `version` is suitable for display in an "about" box or a CLI
+`--version` output: it can be easily compared against release notes and lists
+of bugs fixed in various releases.
+
+The installer adds the following text to your `__init__.py` to place a basic
+version in `YOURPROJECT.__version__`:
+
+ from ._version import get_versions
+ __version__ = get_versions()['version']
+ del get_versions
+
+## Styles
+
+The setup.cfg `style=` configuration controls how the VCS information is
+rendered into a version string.
+
+The default style, "pep440", produces a PEP440-compliant string, equal to the
+un-prefixed tag name for actual releases, and containing an additional "local
+version" section with more detail for in-between builds. For Git, this is
+TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags
+--dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the
+tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and
+that this commit is two revisions ("+2") beyond the "0.11" tag. For released
+software (exactly equal to a known tag), the identifier will only contain the
+stripped tag, e.g. "0.11".
+
+Other styles are available. See details.md in the Versioneer source tree for
+descriptions.
+
+## Debugging
+
+Versioneer tries to avoid fatal errors: if something goes wrong, it will tend
+to return a version of "0+unknown". To investigate the problem, run `setup.py
+version`, which will run the version-lookup code in a verbose mode, and will
+display the full contents of `get_versions()` (including the `error` string,
+which may help identify what went wrong).
+
+## Updating Versioneer
+
+To upgrade your project to a new release of Versioneer, do the following:
+
+* install the new Versioneer (`pip install -U versioneer` or equivalent)
+* edit `setup.cfg`, if necessary, to include any new configuration settings
+ indicated by the release notes
+* re-run `versioneer install` in your source tree, to replace
+ `SRC/_version.py`
+* commit any changed files
+
+### Upgrading to 0.16
+
+Nothing special.
+
+### Upgrading to 0.15
+
+Starting with this version, Versioneer is configured with a `[versioneer]`
+section in your `setup.cfg` file. Earlier versions required the `setup.py` to
+set attributes on the `versioneer` module immediately after import. The new
+version will refuse to run (raising an exception during import) until you
+have provided the necessary `setup.cfg` section.
+
+In addition, the Versioneer package provides an executable named
+`versioneer`, and the installation process is driven by running `versioneer
+install`. In 0.14 and earlier, the executable was named
+`versioneer-installer` and was run without an argument.
+
+### Upgrading to 0.14
-import os, sys, re
-from distutils.core import Command
-from distutils.command.sdist import sdist as _sdist
-from distutils.command.build import build as _build
+0.14 changes the format of the version string. 0.13 and earlier used
+hyphen-separated strings like "0.11-2-g1076c97-dirty". 0.14 and beyond use a
+plus-separated "local version" section strings, with dot-separated
+components, like "0.11+2.g1076c97". PEP440-strict tools did not like the old
+format, but should be ok with the new one.
-versionfile_source = None
-versionfile_build = None
-tag_prefix = None
-parentdir_prefix = None
+### Upgrading from 0.11 to 0.12
-VCS = "git"
-IN_LONG_VERSION_PY = False
+Nothing special.
+### Upgrading from 0.10 to 0.11
-LONG_VERSION_PY = '''
-IN_LONG_VERSION_PY = True
+You must add a `versioneer.VCS = "git"` to your `setup.py` before re-running
+`setup.py setup_versioneer`. This will enable the use of additional
+version-control systems (SVN, etc) in the future.
+
+## Future Directions
+
+This tool is designed to make it easily extended to other version-control
+systems: all VCS-specific components are in separate directories like
+src/git/ . The top-level `versioneer.py` script is assembled from these
+components by running make-versioneer.py . In the future, make-versioneer.py
+will take a VCS name as an argument, and will construct a version of
+`versioneer.py` that is specific to the given VCS. It might also take the
+configuration arguments that are currently provided manually during
+installation by editing setup.py . Alternatively, it might go the other
+direction and include code from all supported VCS systems, reducing the
+number of intermediate scripts.
+
+
+## License
+
+To make Versioneer easier to embed, all its code is dedicated to the public
+domain. The `_version.py` that it creates is also in the public domain.
+Specifically, both are released under the Creative Commons "Public Domain
+Dedication" license (CC0-1.0), as described in
+https://creativecommons.org/publicdomain/zero/1.0/ .
+
+"""
+
+from __future__ import print_function
+try:
+ import configparser
+except ImportError:
+ import ConfigParser as configparser
+import errno
+import json
+import os
+import re
+import subprocess
+import sys
+
+
+class VersioneerConfig:
+ """Container for Versioneer configuration parameters."""
+
+
+def get_root():
+ """Get the project root directory.
+
+ We require that all commands are run from the project root, i.e. the
+ directory that contains setup.py, setup.cfg, and versioneer.py .
+ """
+ root = os.path.realpath(os.path.abspath(os.getcwd()))
+ setup_py = os.path.join(root, "setup.py")
+ versioneer_py = os.path.join(root, "versioneer.py")
+ if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)):
+ # allow 'python path/to/setup.py COMMAND'
+ root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0])))
+ setup_py = os.path.join(root, "setup.py")
+ versioneer_py = os.path.join(root, "versioneer.py")
+ if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)):
+ err = ("Versioneer was unable to run the project root directory. "
+ "Versioneer requires setup.py to be executed from "
+ "its immediate directory (like 'python setup.py COMMAND'), "
+ "or in a way that lets it use sys.argv[0] to find the root "
+ "(like 'python path/to/setup.py COMMAND').")
+ raise VersioneerBadRootError(err)
+ try:
+ # Certain runtime workflows (setup.py install/develop in a setuptools
+ # tree) execute all dependencies in a single python process, so
+ # "versioneer" may be imported multiple times, and python's shared
+ # module-import table will cache the first one. So we can't use
+ # os.path.dirname(__file__), as that will find whichever
+ # versioneer.py was first imported, even in later projects.
+ me = os.path.realpath(os.path.abspath(__file__))
+ if os.path.splitext(me)[0] != os.path.splitext(versioneer_py)[0]:
+ print("Warning: build in %s is using versioneer.py from %s"
+ % (os.path.dirname(me), versioneer_py))
+ except NameError:
+ pass
+ return root
+
+
+def get_config_from_root(root):
+ """Read the project setup.cfg file to determine Versioneer config."""
+ # This might raise EnvironmentError (if setup.cfg is missing), or
+ # configparser.NoSectionError (if it lacks a [versioneer] section), or
+ # configparser.NoOptionError (if it lacks "VCS="). See the docstring at
+ # the top of versioneer.py for instructions on writing your setup.cfg .
+ setup_cfg = os.path.join(root, "setup.cfg")
+ parser = configparser.SafeConfigParser()
+ with open(setup_cfg, "r") as f:
+ parser.readfp(f)
+ VCS = parser.get("versioneer", "VCS") # mandatory
+
+ def get(parser, name):
+ if parser.has_option("versioneer", name):
+ return parser.get("versioneer", name)
+ return None
+ cfg = VersioneerConfig()
+ cfg.VCS = VCS
+ cfg.style = get(parser, "style") or ""
+ cfg.versionfile_source = get(parser, "versionfile_source")
+ cfg.versionfile_build = get(parser, "versionfile_build")
+ cfg.tag_prefix = get(parser, "tag_prefix")
+ if cfg.tag_prefix in ("''", '""'):
+ cfg.tag_prefix = ""
+ cfg.parentdir_prefix = get(parser, "parentdir_prefix")
+ cfg.verbose = get(parser, "verbose")
+ return cfg
+
+
+class NotThisMethod(Exception):
+ """Exception raised if a method is not valid for the current scenario."""
+
+# these dictionaries contain VCS-specific tools
+LONG_VERSION_PY = {}
+HANDLERS = {}
+
+
+def register_vcs_handler(vcs, method): # decorator
+ """Decorator to mark a method as the handler for a particular VCS."""
+ def decorate(f):
+ """Store f in HANDLERS[vcs][method]."""
+ if vcs not in HANDLERS:
+ HANDLERS[vcs] = {}
+ HANDLERS[vcs][method] = f
+ return f
+ return decorate
+
+
+def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False):
+ """Call the given command(s)."""
+ assert isinstance(commands, list)
+ p = None
+ for c in commands:
+ try:
+ dispcmd = str([c] + args)
+ # remember shell=False, so use git.cmd on windows, not just git
+ p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE,
+ stderr=(subprocess.PIPE if hide_stderr
+ else None))
+ break
+ except EnvironmentError:
+ e = sys.exc_info()[1]
+ if e.errno == errno.ENOENT:
+ continue
+ if verbose:
+ print("unable to run %s" % dispcmd)
+ print(e)
+ return None
+ else:
+ if verbose:
+ print("unable to find command, tried %s" % (commands,))
+ return None
+ stdout = p.communicate()[0].strip()
+ if sys.version_info[0] >= 3:
+ stdout = stdout.decode()
+ if p.returncode != 0:
+ if verbose:
+ print("unable to run %s (error)" % dispcmd)
+ return None
+ return stdout
+LONG_VERSION_PY['git'] = '''
# This file helps to compute a version number in source trees obtained from
# git-archive tarball (such as those provided by githubs download-from-tag
-# feature). Distribution tarballs (build by setup.py sdist) and build
+# feature). Distribution tarballs (built by setup.py sdist) and build
# directories (produced by setup.py build) will contain a much shorter file
# that just contains the computed version number.
# This file is released into the public domain. Generated by
-# versioneer-0.7+ (https://github.com/warner/python-versioneer)
-
-# these strings will be replaced by git during git-archive
-git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s"
-git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s"
+# versioneer-0.16 (https://github.com/warner/python-versioneer)
+"""Git implementation of _version.py."""
+import errno
+import os
+import re
import subprocess
import sys
-def run_command(args, cwd=None, verbose=False):
- try:
- # remember shell=False, so use git.cmd on windows, not just git
- p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd)
- except EnvironmentError:
- e = sys.exc_info()[1]
+
+def get_keywords():
+ """Get the keywords needed to look up the version information."""
+ # these strings will be replaced by git during git-archive.
+ # setup.py/versioneer.py will grep for the variable names, so they must
+ # each be defined on a line of their own. _version.py will just call
+ # get_keywords().
+ git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s"
+ git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s"
+ keywords = {"refnames": git_refnames, "full": git_full}
+ return keywords
+
+
+class VersioneerConfig:
+ """Container for Versioneer configuration parameters."""
+
+
+def get_config():
+ """Create, populate and return the VersioneerConfig() object."""
+ # these strings are filled in when 'setup.py versioneer' creates
+ # _version.py
+ cfg = VersioneerConfig()
+ cfg.VCS = "git"
+ cfg.style = "%(STYLE)s"
+ cfg.tag_prefix = "%(TAG_PREFIX)s"
+ cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s"
+ cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s"
+ cfg.verbose = False
+ return cfg
+
+
+class NotThisMethod(Exception):
+ """Exception raised if a method is not valid for the current scenario."""
+
+
+LONG_VERSION_PY = {}
+HANDLERS = {}
+
+
+def register_vcs_handler(vcs, method): # decorator
+ """Decorator to mark a method as the handler for a particular VCS."""
+ def decorate(f):
+ """Store f in HANDLERS[vcs][method]."""
+ if vcs not in HANDLERS:
+ HANDLERS[vcs] = {}
+ HANDLERS[vcs][method] = f
+ return f
+ return decorate
+
+
+def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False):
+ """Call the given command(s)."""
+ assert isinstance(commands, list)
+ p = None
+ for c in commands:
+ try:
+ dispcmd = str([c] + args)
+ # remember shell=False, so use git.cmd on windows, not just git
+ p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE,
+ stderr=(subprocess.PIPE if hide_stderr
+ else None))
+ break
+ except EnvironmentError:
+ e = sys.exc_info()[1]
+ if e.errno == errno.ENOENT:
+ continue
+ if verbose:
+ print("unable to run %%s" %% dispcmd)
+ print(e)
+ return None
+ else:
if verbose:
- print("unable to run %%s" %% args[0])
- print(e)
+ print("unable to find command, tried %%s" %% (commands,))
return None
stdout = p.communicate()[0].strip()
- if sys.version >= '3':
+ if sys.version_info[0] >= 3:
stdout = stdout.decode()
if p.returncode != 0:
if verbose:
- print("unable to run %%s (error)" %% args[0])
+ print("unable to run %%s (error)" %% dispcmd)
return None
return stdout
-import sys
-import re
-import os.path
+def versions_from_parentdir(parentdir_prefix, root, verbose):
+ """Try to determine the version from the parent directory name.
+
+ Source tarballs conventionally unpack into a directory that includes
+ both the project name and a version string.
+ """
+ dirname = os.path.basename(root)
+ if not dirname.startswith(parentdir_prefix):
+ if verbose:
+ print("guessing rootdir is '%%s', but '%%s' doesn't start with "
+ "prefix '%%s'" %% (root, dirname, parentdir_prefix))
+ raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
+ return {"version": dirname[len(parentdir_prefix):],
+ "full-revisionid": None,
+ "dirty": False, "error": None}
-def get_expanded_variables(versionfile_source):
+
+@register_vcs_handler("git", "get_keywords")
+def git_get_keywords(versionfile_abs):
+ """Extract version information from the given file."""
# the code embedded in _version.py can just fetch the value of these
- # variables. When used from setup.py, we don't want to import
- # _version.py, so we do it with a regexp instead. This function is not
- # used from _version.py.
- variables = {}
+ # keywords. When used from setup.py, we don't want to import _version.py,
+ # so we do it with a regexp instead. This function is not used from
+ # _version.py.
+ keywords = {}
try:
- f = open(versionfile_source,"r")
+ f = open(versionfile_abs, "r")
for line in f.readlines():
if line.strip().startswith("git_refnames ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
- variables["refnames"] = mo.group(1)
+ keywords["refnames"] = mo.group(1)
if line.strip().startswith("git_full ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
- variables["full"] = mo.group(1)
+ keywords["full"] = mo.group(1)
f.close()
except EnvironmentError:
pass
- return variables
+ return keywords
+
-def versions_from_expanded_variables(variables, tag_prefix, verbose=False):
- refnames = variables["refnames"].strip()
+@register_vcs_handler("git", "keywords")
+def git_versions_from_keywords(keywords, tag_prefix, verbose):
+ """Get version information from git keywords."""
+ if not keywords:
+ raise NotThisMethod("no keywords at all, weird")
+ refnames = keywords["refnames"].strip()
if refnames.startswith("$Format"):
if verbose:
- print("variables are unexpanded, not using")
- return {} # unexpanded, so not in an unpacked git-archive tarball
+ print("keywords are unexpanded, not using")
+ raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
refs = set([r.strip() for r in refnames.strip("()").split(",")])
# starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
# just "foo-1.0". If we see a "tag: " prefix, prefer those.
@@ -189,172 +660,350 @@ def versions_from_expanded_variables(variables, tag_prefix, verbose=False):
r = ref[len(tag_prefix):]
if verbose:
print("picking %%s" %% r)
- return { "version": r,
- "full": variables["full"].strip() }
- # no suitable tags, so we use the full revision id
+ return {"version": r,
+ "full-revisionid": keywords["full"].strip(),
+ "dirty": False, "error": None
+ }
+ # no suitable tags, so version is "0+unknown", but full hex is still there
if verbose:
- print("no suitable tags, using full revision id")
- return { "version": variables["full"].strip(),
- "full": variables["full"].strip() }
-
-def versions_from_vcs(tag_prefix, versionfile_source, verbose=False):
- # this runs 'git' from the root of the source tree. That either means
- # someone ran a setup.py command (and this code is in versioneer.py, so
- # IN_LONG_VERSION_PY=False, thus the containing directory is the root of
- # the source tree), or someone ran a project-specific entry point (and
- # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the
- # containing directory is somewhere deeper in the source tree). This only
- # gets called if the git-archive 'subst' variables were *not* expanded,
- # and _version.py hasn't already been rewritten with a short version
- # string, meaning we're inside a checked out source tree.
+ print("no suitable tags, using unknown + full revision id")
+ return {"version": "0+unknown",
+ "full-revisionid": keywords["full"].strip(),
+ "dirty": False, "error": "no suitable tags"}
- try:
- here = os.path.abspath(__file__)
- except NameError:
- # some py2exe/bbfreeze/non-CPython implementations don't do __file__
- return {} # not always correct
-
- # versionfile_source is the relative path from the top of the source tree
- # (where the .git directory might live) to this file. Invert this to find
- # the root from __file__.
- root = here
- if IN_LONG_VERSION_PY:
- for i in range(len(versionfile_source.split("/"))):
- root = os.path.dirname(root)
- else:
- root = os.path.dirname(here)
+
+@register_vcs_handler("git", "pieces_from_vcs")
+def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
+ """Get version from 'git describe' in the root of the source tree.
+
+ This only gets called if the git-archive 'subst' keywords were *not*
+ expanded, and _version.py hasn't already been rewritten with a short
+ version string, meaning we're inside a checked out source tree.
+ """
if not os.path.exists(os.path.join(root, ".git")):
if verbose:
print("no .git in %%s" %% root)
- return {}
+ raise NotThisMethod("no .git directory")
- GIT = "git"
+ GITS = ["git"]
if sys.platform == "win32":
- GIT = "git.exe"
- stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"],
- cwd=root)
- if stdout is None:
- return {}
- if not stdout.startswith(tag_prefix):
- if verbose:
- print("tag '%%s' doesn't start with prefix '%%s'" %% (stdout, tag_prefix))
- return {}
- tag = stdout[len(tag_prefix):]
- stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root)
- if stdout is None:
- return {}
- full = stdout.strip()
- if tag.endswith("-dirty"):
- full += "-dirty"
- return {"version": tag, "full": full}
-
-
-def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False):
- if IN_LONG_VERSION_PY:
- # We're running from _version.py. If it's from a source tree
- # (execute-in-place), we can work upwards to find the root of the
- # tree, and then check the parent directory for a version string. If
- # it's in an installed application, there's no hope.
- try:
- here = os.path.abspath(__file__)
- except NameError:
- # py2exe/bbfreeze/non-CPython don't have __file__
- return {} # without __file__, we have no hope
- # versionfile_source is the relative path from the top of the source
- # tree to _version.py. Invert this to find the root from __file__.
- root = here
- for i in range(len(versionfile_source.split("/"))):
- root = os.path.dirname(root)
+ GITS = ["git.cmd", "git.exe"]
+ # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
+ # if there isn't one, this yields HEX[-dirty] (no NUM)
+ describe_out = run_command(GITS, ["describe", "--tags", "--dirty",
+ "--always", "--long",
+ "--match", "%%s*" %% tag_prefix],
+ cwd=root)
+ # --long was added in git-1.5.5
+ if describe_out is None:
+ raise NotThisMethod("'git describe' failed")
+ describe_out = describe_out.strip()
+ full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
+ if full_out is None:
+ raise NotThisMethod("'git rev-parse' failed")
+ full_out = full_out.strip()
+
+ pieces = {}
+ pieces["long"] = full_out
+ pieces["short"] = full_out[:7] # maybe improved later
+ pieces["error"] = None
+
+ # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
+ # TAG might have hyphens.
+ git_describe = describe_out
+
+ # look for -dirty suffix
+ dirty = git_describe.endswith("-dirty")
+ pieces["dirty"] = dirty
+ if dirty:
+ git_describe = git_describe[:git_describe.rindex("-dirty")]
+
+ # now we have TAG-NUM-gHEX or HEX
+
+ if "-" in git_describe:
+ # TAG-NUM-gHEX
+ mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
+ if not mo:
+ # unparseable. Maybe git-describe is misbehaving?
+ pieces["error"] = ("unable to parse git-describe output: '%%s'"
+ %% describe_out)
+ return pieces
+
+ # tag
+ full_tag = mo.group(1)
+ if not full_tag.startswith(tag_prefix):
+ if verbose:
+ fmt = "tag '%%s' doesn't start with prefix '%%s'"
+ print(fmt %% (full_tag, tag_prefix))
+ pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'"
+ %% (full_tag, tag_prefix))
+ return pieces
+ pieces["closest-tag"] = full_tag[len(tag_prefix):]
+
+ # distance: number of commits since tag
+ pieces["distance"] = int(mo.group(2))
+
+ # commit: short hex revision ID
+ pieces["short"] = mo.group(3)
+
+ else:
+ # HEX: no tags
+ pieces["closest-tag"] = None
+ count_out = run_command(GITS, ["rev-list", "HEAD", "--count"],
+ cwd=root)
+ pieces["distance"] = int(count_out) # total number of commits
+
+ return pieces
+
+
+def plus_or_dot(pieces):
+ """Return a + if we don't already have one, else return a ."""
+ if "+" in pieces.get("closest-tag", ""):
+ return "."
+ return "+"
+
+
+def render_pep440(pieces):
+ """Build up version string, with post-release "local version identifier".
+
+ Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
+ get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
+
+ Exceptions:
+ 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"] or pieces["dirty"]:
+ rendered += plus_or_dot(pieces)
+ rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"])
+ if pieces["dirty"]:
+ rendered += ".dirty"
+ else:
+ # exception #1
+ rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"],
+ pieces["short"])
+ if pieces["dirty"]:
+ rendered += ".dirty"
+ return rendered
+
+
+def render_pep440_pre(pieces):
+ """TAG[.post.devDISTANCE] -- No -dirty.
+
+ Exceptions:
+ 1: no tags. 0.post.devDISTANCE
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"]:
+ rendered += ".post.dev%%d" %% pieces["distance"]
else:
- # we're running from versioneer.py, which means we're running from
- # the setup.py in a source tree. sys.argv[0] is setup.py in the root.
- here = os.path.abspath(sys.argv[0])
- root = os.path.dirname(here)
+ # exception #1
+ rendered = "0.post.dev%%d" %% pieces["distance"]
+ return rendered
+
+
+def render_pep440_post(pieces):
+ """TAG[.postDISTANCE[.dev0]+gHEX] .
+
+ The ".dev0" means dirty. Note that .dev0 sorts backwards
+ (a dirty tree will appear "older" than the corresponding clean one),
+ but you shouldn't be releasing software with -dirty anyways.
+
+ Exceptions:
+ 1: no tags. 0.postDISTANCE[.dev0]
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"] or pieces["dirty"]:
+ rendered += ".post%%d" %% pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ rendered += plus_or_dot(pieces)
+ rendered += "g%%s" %% pieces["short"]
+ else:
+ # exception #1
+ rendered = "0.post%%d" %% pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ rendered += "+g%%s" %% pieces["short"]
+ return rendered
+
+
+def render_pep440_old(pieces):
+ """TAG[.postDISTANCE[.dev0]] .
+
+ The ".dev0" means dirty.
+
+ Eexceptions:
+ 1: no tags. 0.postDISTANCE[.dev0]
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"] or pieces["dirty"]:
+ rendered += ".post%%d" %% pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ else:
+ # exception #1
+ rendered = "0.post%%d" %% pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ return rendered
- # Source tarballs conventionally unpack into a directory that includes
- # both the project name and a version string.
- dirname = os.path.basename(root)
- if not dirname.startswith(parentdir_prefix):
- if verbose:
- print("guessing rootdir is '%%s', but '%%s' doesn't start with prefix '%%s'" %%
- (root, dirname, parentdir_prefix))
- return None
- return {"version": dirname[len(parentdir_prefix):], "full": ""}
-
-tag_prefix = "%(TAG_PREFIX)s"
-parentdir_prefix = "%(PARENTDIR_PREFIX)s"
-versionfile_source = "%(VERSIONFILE_SOURCE)s"
-
-def get_versions(default={"version": "unknown", "full": ""}, verbose=False):
- variables = { "refnames": git_refnames, "full": git_full }
- ver = versions_from_expanded_variables(variables, tag_prefix, verbose)
- if not ver:
- ver = versions_from_vcs(tag_prefix, versionfile_source, verbose)
- if not ver:
- ver = versions_from_parentdir(parentdir_prefix, versionfile_source,
- verbose)
- if not ver:
- ver = default
- return ver
-'''
+def render_git_describe(pieces):
+ """TAG[-DISTANCE-gHEX][-dirty].
+ Like 'git describe --tags --dirty --always'.
-import subprocess
-import sys
+ Exceptions:
+ 1: no tags. HEX[-dirty] (note: no 'g' prefix)
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"]:
+ rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"])
+ else:
+ # exception #1
+ rendered = pieces["short"]
+ if pieces["dirty"]:
+ rendered += "-dirty"
+ return rendered
+
+
+def render_git_describe_long(pieces):
+ """TAG-DISTANCE-gHEX[-dirty].
+
+ Like 'git describe --tags --dirty --always -long'.
+ The distance/hash is unconditional.
+
+ Exceptions:
+ 1: no tags. HEX[-dirty] (note: no 'g' prefix)
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"])
+ else:
+ # exception #1
+ rendered = pieces["short"]
+ if pieces["dirty"]:
+ rendered += "-dirty"
+ return rendered
+
+
+def render(pieces, style):
+ """Render the given version pieces into the requested style."""
+ if pieces["error"]:
+ return {"version": "unknown",
+ "full-revisionid": pieces.get("long"),
+ "dirty": None,
+ "error": pieces["error"]}
+
+ if not style or style == "default":
+ style = "pep440" # the default
+
+ if style == "pep440":
+ rendered = render_pep440(pieces)
+ elif style == "pep440-pre":
+ rendered = render_pep440_pre(pieces)
+ elif style == "pep440-post":
+ rendered = render_pep440_post(pieces)
+ elif style == "pep440-old":
+ rendered = render_pep440_old(pieces)
+ elif style == "git-describe":
+ rendered = render_git_describe(pieces)
+ elif style == "git-describe-long":
+ rendered = render_git_describe_long(pieces)
+ else:
+ raise ValueError("unknown style '%%s'" %% style)
+
+ return {"version": rendered, "full-revisionid": pieces["long"],
+ "dirty": pieces["dirty"], "error": None}
+
+
+def get_versions():
+ """Get version information or return default if unable to do so."""
+ # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
+ # __file__, we can work backwards from there to the root. Some
+ # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
+ # case we can only use expanded keywords.
+
+ cfg = get_config()
+ verbose = cfg.verbose
-def run_command(args, cwd=None, verbose=False):
try:
- # remember shell=False, so use git.cmd on windows, not just git
- p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd)
- except EnvironmentError:
- e = sys.exc_info()[1]
- if verbose:
- print("unable to run %s" % args[0])
- print(e)
- return None
- stdout = p.communicate()[0].strip()
- if sys.version >= '3':
- stdout = stdout.decode()
- if p.returncode != 0:
- if verbose:
- print("unable to run %s (error)" % args[0])
- return None
- return stdout
+ return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
+ verbose)
+ except NotThisMethod:
+ pass
+ try:
+ root = os.path.realpath(__file__)
+ # versionfile_source is the relative path from the top of the source
+ # tree (where the .git directory might live) to this file. Invert
+ # this to find the root from __file__.
+ for i in cfg.versionfile_source.split('/'):
+ root = os.path.dirname(root)
+ except NameError:
+ return {"version": "0+unknown", "full-revisionid": None,
+ "dirty": None,
+ "error": "unable to find root of source tree"}
-import sys
-import re
-import os.path
+ try:
+ pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
+ return render(pieces, cfg.style)
+ except NotThisMethod:
+ pass
+
+ try:
+ if cfg.parentdir_prefix:
+ return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
+ except NotThisMethod:
+ pass
-def get_expanded_variables(versionfile_source):
+ return {"version": "0+unknown", "full-revisionid": None,
+ "dirty": None,
+ "error": "unable to compute version"}
+'''
+
+
+@register_vcs_handler("git", "get_keywords")
+def git_get_keywords(versionfile_abs):
+ """Extract version information from the given file."""
# the code embedded in _version.py can just fetch the value of these
- # variables. When used from setup.py, we don't want to import
- # _version.py, so we do it with a regexp instead. This function is not
- # used from _version.py.
- variables = {}
+ # keywords. When used from setup.py, we don't want to import _version.py,
+ # so we do it with a regexp instead. This function is not used from
+ # _version.py.
+ keywords = {}
try:
- f = open(versionfile_source,"r")
+ f = open(versionfile_abs, "r")
for line in f.readlines():
if line.strip().startswith("git_refnames ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
- variables["refnames"] = mo.group(1)
+ keywords["refnames"] = mo.group(1)
if line.strip().startswith("git_full ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
- variables["full"] = mo.group(1)
+ keywords["full"] = mo.group(1)
f.close()
except EnvironmentError:
pass
- return variables
+ return keywords
+
-def versions_from_expanded_variables(variables, tag_prefix, verbose=False):
- refnames = variables["refnames"].strip()
+@register_vcs_handler("git", "keywords")
+def git_versions_from_keywords(keywords, tag_prefix, verbose):
+ """Get version information from git keywords."""
+ if not keywords:
+ raise NotThisMethod("no keywords at all, weird")
+ refnames = keywords["refnames"].strip()
if refnames.startswith("$Format"):
if verbose:
- print("variables are unexpanded, not using")
- return {} # unexpanded, so not in an unpacked git-archive tarball
+ print("keywords are unexpanded, not using")
+ raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
refs = set([r.strip() for r in refnames.strip("()").split(",")])
# starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
# just "foo-1.0". If we see a "tag: " prefix, prefer those.
@@ -370,7 +1019,7 @@ def versions_from_expanded_variables(variables, tag_prefix, verbose=False):
# "stabilization", as well as "HEAD" and "master".
tags = set([r for r in refs if re.search(r'\d', r)])
if verbose:
- print("discarding '%s', no digits" % ",".join(refs-tags))
+ print("discarding '%s', no digits" % ",".join(refs - tags))
if verbose:
print("likely tags: %s" % ",".join(sorted(tags)))
for ref in sorted(tags):
@@ -379,107 +1028,122 @@ def versions_from_expanded_variables(variables, tag_prefix, verbose=False):
r = ref[len(tag_prefix):]
if verbose:
print("picking %s" % r)
- return { "version": r,
- "full": variables["full"].strip() }
- # no suitable tags, so we use the full revision id
+ return {"version": r,
+ "full-revisionid": keywords["full"].strip(),
+ "dirty": False, "error": None
+ }
+ # no suitable tags, so version is "0+unknown", but full hex is still there
if verbose:
- print("no suitable tags, using full revision id")
- return { "version": variables["full"].strip(),
- "full": variables["full"].strip() }
-
-def versions_from_vcs(tag_prefix, versionfile_source, verbose=False):
- # this runs 'git' from the root of the source tree. That either means
- # someone ran a setup.py command (and this code is in versioneer.py, so
- # IN_LONG_VERSION_PY=False, thus the containing directory is the root of
- # the source tree), or someone ran a project-specific entry point (and
- # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the
- # containing directory is somewhere deeper in the source tree). This only
- # gets called if the git-archive 'subst' variables were *not* expanded,
- # and _version.py hasn't already been rewritten with a short version
- # string, meaning we're inside a checked out source tree.
+ print("no suitable tags, using unknown + full revision id")
+ return {"version": "0+unknown",
+ "full-revisionid": keywords["full"].strip(),
+ "dirty": False, "error": "no suitable tags"}
- try:
- here = os.path.abspath(__file__)
- except NameError:
- # some py2exe/bbfreeze/non-CPython implementations don't do __file__
- return {} # not always correct
-
- # versionfile_source is the relative path from the top of the source tree
- # (where the .git directory might live) to this file. Invert this to find
- # the root from __file__.
- root = here
- if IN_LONG_VERSION_PY:
- for i in range(len(versionfile_source.split("/"))):
- root = os.path.dirname(root)
- else:
- root = os.path.dirname(here)
+
+@register_vcs_handler("git", "pieces_from_vcs")
+def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
+ """Get version from 'git describe' in the root of the source tree.
+
+ This only gets called if the git-archive 'subst' keywords were *not*
+ expanded, and _version.py hasn't already been rewritten with a short
+ version string, meaning we're inside a checked out source tree.
+ """
if not os.path.exists(os.path.join(root, ".git")):
if verbose:
print("no .git in %s" % root)
- return {}
+ raise NotThisMethod("no .git directory")
- GIT = "git"
+ GITS = ["git"]
if sys.platform == "win32":
- GIT = "git.exe"
- stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"],
- cwd=root)
- if stdout is None:
- return {}
- if not stdout.startswith(tag_prefix):
- if verbose:
- print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix))
- return {}
- tag = stdout[len(tag_prefix):]
- stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root)
- if stdout is None:
- return {}
- full = stdout.strip()
- if tag.endswith("-dirty"):
- full += "-dirty"
- return {"version": tag, "full": full}
-
-
-def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False):
- if IN_LONG_VERSION_PY:
- # We're running from _version.py. If it's from a source tree
- # (execute-in-place), we can work upwards to find the root of the
- # tree, and then check the parent directory for a version string. If
- # it's in an installed application, there's no hope.
- try:
- here = os.path.abspath(__file__)
- except NameError:
- # py2exe/bbfreeze/non-CPython don't have __file__
- return {} # without __file__, we have no hope
- # versionfile_source is the relative path from the top of the source
- # tree to _version.py. Invert this to find the root from __file__.
- root = here
- for i in range(len(versionfile_source.split("/"))):
- root = os.path.dirname(root)
+ GITS = ["git.cmd", "git.exe"]
+ # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
+ # if there isn't one, this yields HEX[-dirty] (no NUM)
+ describe_out = run_command(GITS, ["describe", "--tags", "--dirty",
+ "--always", "--long",
+ "--match", "%s*" % tag_prefix],
+ cwd=root)
+ # --long was added in git-1.5.5
+ if describe_out is None:
+ raise NotThisMethod("'git describe' failed")
+ describe_out = describe_out.strip()
+ full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
+ if full_out is None:
+ raise NotThisMethod("'git rev-parse' failed")
+ full_out = full_out.strip()
+
+ pieces = {}
+ pieces["long"] = full_out
+ pieces["short"] = full_out[:7] # maybe improved later
+ pieces["error"] = None
+
+ # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
+ # TAG might have hyphens.
+ git_describe = describe_out
+
+ # look for -dirty suffix
+ dirty = git_describe.endswith("-dirty")
+ pieces["dirty"] = dirty
+ if dirty:
+ git_describe = git_describe[:git_describe.rindex("-dirty")]
+
+ # now we have TAG-NUM-gHEX or HEX
+
+ if "-" in git_describe:
+ # TAG-NUM-gHEX
+ mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
+ if not mo:
+ # unparseable. Maybe git-describe is misbehaving?
+ pieces["error"] = ("unable to parse git-describe output: '%s'"
+ % describe_out)
+ return pieces
+
+ # tag
+ full_tag = mo.group(1)
+ if not full_tag.startswith(tag_prefix):
+ if verbose:
+ fmt = "tag '%s' doesn't start with prefix '%s'"
+ print(fmt % (full_tag, tag_prefix))
+ pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
+ % (full_tag, tag_prefix))
+ return pieces
+ pieces["closest-tag"] = full_tag[len(tag_prefix):]
+
+ # distance: number of commits since tag
+ pieces["distance"] = int(mo.group(2))
+
+ # commit: short hex revision ID
+ pieces["short"] = mo.group(3)
+
else:
- # we're running from versioneer.py, which means we're running from
- # the setup.py in a source tree. sys.argv[0] is setup.py in the root.
- here = os.path.abspath(sys.argv[0])
- root = os.path.dirname(here)
+ # HEX: no tags
+ pieces["closest-tag"] = None
+ count_out = run_command(GITS, ["rev-list", "HEAD", "--count"],
+ cwd=root)
+ pieces["distance"] = int(count_out) # total number of commits
- # Source tarballs conventionally unpack into a directory that includes
- # both the project name and a version string.
- dirname = os.path.basename(root)
- if not dirname.startswith(parentdir_prefix):
- if verbose:
- print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" %
- (root, dirname, parentdir_prefix))
- return None
- return {"version": dirname[len(parentdir_prefix):], "full": ""}
+ return pieces
-import sys
-def do_vcs_install(versionfile_source, ipy):
- GIT = "git"
+def do_vcs_install(manifest_in, versionfile_source, ipy):
+ """Git-specific installation logic for Versioneer.
+
+ For Git, this means creating/changing .gitattributes to mark _version.py
+ for export-time keyword substitution.
+ """
+ GITS = ["git"]
if sys.platform == "win32":
- GIT = "git.exe"
- run_command([GIT, "add", "versioneer.py"])
- run_command([GIT, "add", versionfile_source])
- run_command([GIT, "add", ipy])
+ GITS = ["git.cmd", "git.exe"]
+ files = [manifest_in, versionfile_source]
+ if ipy:
+ files.append(ipy)
+ try:
+ me = __file__
+ if me.endswith(".pyc") or me.endswith(".pyo"):
+ me = os.path.splitext(me)[0] + ".py"
+ versioneer_file = os.path.relpath(me)
+ except NameError:
+ versioneer_file = "versioneer.py"
+ files.append(versioneer_file)
present = False
try:
f = open(".gitattributes", "r")
@@ -494,135 +1158,490 @@ def do_vcs_install(versionfile_source, ipy):
f = open(".gitattributes", "a+")
f.write("%s export-subst\n" % versionfile_source)
f.close()
- run_command([GIT, "add", ".gitattributes"])
+ files.append(".gitattributes")
+ run_command(GITS, ["add", "--"] + files)
+
+def versions_from_parentdir(parentdir_prefix, root, verbose):
+ """Try to determine the version from the parent directory name.
+
+ Source tarballs conventionally unpack into a directory that includes
+ both the project name and a version string.
+ """
+ dirname = os.path.basename(root)
+ if not dirname.startswith(parentdir_prefix):
+ if verbose:
+ print("guessing rootdir is '%s', but '%s' doesn't start with "
+ "prefix '%s'" % (root, dirname, parentdir_prefix))
+ raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
+ return {"version": dirname[len(parentdir_prefix):],
+ "full-revisionid": None,
+ "dirty": False, "error": None}
SHORT_VERSION_PY = """
-# This file was generated by 'versioneer.py' (0.7+) from
+# This file was generated by 'versioneer.py' (0.16) from
# revision-control system data, or from the parent directory name of an
# unpacked source archive. Distribution tarballs contain a pre-generated copy
# of this file.
-version_version = '%(version)s'
-version_full = '%(full)s'
-def get_versions(default={}, verbose=False):
- return {'version': version_version, 'full': version_full}
+import json
+import sys
+
+version_json = '''
+%s
+''' # END VERSION_JSON
+
+def get_versions():
+ return json.loads(version_json)
"""
-DEFAULT = {"version": "unknown", "full": "unknown"}
def versions_from_file(filename):
- versions = {}
+ """Try to determine the version from _version.py if present."""
try:
- f = open(filename)
+ with open(filename) as f:
+ contents = f.read()
except EnvironmentError:
- return versions
- for line in f.readlines():
- mo = re.match("version_version = '([^']+)'", line)
- if mo:
- versions["version"] = mo.group(1)
- mo = re.match("version_full = '([^']+)'", line)
- if mo:
- versions["full"] = mo.group(1)
- f.close()
- return versions
+ raise NotThisMethod("unable to read _version.py")
+ mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON",
+ contents, re.M | re.S)
+ if not mo:
+ raise NotThisMethod("no version_json in _version.py")
+ return json.loads(mo.group(1))
+
def write_to_version_file(filename, versions):
- f = open(filename, "w")
- f.write(SHORT_VERSION_PY % versions)
- f.close()
+ """Write the given version number to the given _version.py file."""
+ os.unlink(filename)
+ contents = json.dumps(versions, sort_keys=True,
+ indent=1, separators=(",", ": "))
+ with open(filename, "w") as f:
+ f.write(SHORT_VERSION_PY % contents)
+
print("set %s to '%s'" % (filename, versions["version"]))
-def get_best_versions(versionfile, tag_prefix, parentdir_prefix,
- default=DEFAULT, verbose=False):
- # returns dict with two keys: 'version' and 'full'
- #
- # extract version from first of _version.py, 'git describe', parentdir.
- # This is meant to work for developers using a source checkout, for users
- # of a tarball created by 'setup.py sdist', and for users of a
- # tarball/zipball created by 'git archive' or github's download-from-tag
- # feature.
-
- variables = get_expanded_variables(versionfile_source)
- if variables:
- ver = versions_from_expanded_variables(variables, tag_prefix)
- if ver:
- if verbose: print("got version from expanded variable %s" % ver)
- return ver
+def plus_or_dot(pieces):
+ """Return a + if we don't already have one, else return a ."""
+ if "+" in pieces.get("closest-tag", ""):
+ return "."
+ return "+"
- ver = versions_from_file(versionfile)
- if ver:
- if verbose: print("got version from file %s %s" % (versionfile, ver))
- return ver
- ver = versions_from_vcs(tag_prefix, versionfile_source, verbose)
- if ver:
- if verbose: print("got version from git %s" % ver)
- return ver
+def render_pep440(pieces):
+ """Build up version string, with post-release "local version identifier".
- ver = versions_from_parentdir(parentdir_prefix, versionfile_source, verbose)
- if ver:
- if verbose: print("got version from parentdir %s" % ver)
- return ver
+ Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
+ get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
+
+ Exceptions:
+ 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"] or pieces["dirty"]:
+ rendered += plus_or_dot(pieces)
+ rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
+ if pieces["dirty"]:
+ rendered += ".dirty"
+ else:
+ # exception #1
+ rendered = "0+untagged.%d.g%s" % (pieces["distance"],
+ pieces["short"])
+ if pieces["dirty"]:
+ rendered += ".dirty"
+ return rendered
+
+
+def render_pep440_pre(pieces):
+ """TAG[.post.devDISTANCE] -- No -dirty.
+
+ Exceptions:
+ 1: no tags. 0.post.devDISTANCE
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"]:
+ rendered += ".post.dev%d" % pieces["distance"]
+ else:
+ # exception #1
+ rendered = "0.post.dev%d" % pieces["distance"]
+ return rendered
+
+
+def render_pep440_post(pieces):
+ """TAG[.postDISTANCE[.dev0]+gHEX] .
+
+ The ".dev0" means dirty. Note that .dev0 sorts backwards
+ (a dirty tree will appear "older" than the corresponding clean one),
+ but you shouldn't be releasing software with -dirty anyways.
+
+ Exceptions:
+ 1: no tags. 0.postDISTANCE[.dev0]
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"] or pieces["dirty"]:
+ rendered += ".post%d" % pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ rendered += plus_or_dot(pieces)
+ rendered += "g%s" % pieces["short"]
+ else:
+ # exception #1
+ rendered = "0.post%d" % pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ rendered += "+g%s" % pieces["short"]
+ return rendered
+
+
+def render_pep440_old(pieces):
+ """TAG[.postDISTANCE[.dev0]] .
+
+ The ".dev0" means dirty.
+
+ Eexceptions:
+ 1: no tags. 0.postDISTANCE[.dev0]
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"] or pieces["dirty"]:
+ rendered += ".post%d" % pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ else:
+ # exception #1
+ rendered = "0.post%d" % pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ return rendered
+
+
+def render_git_describe(pieces):
+ """TAG[-DISTANCE-gHEX][-dirty].
+
+ Like 'git describe --tags --dirty --always'.
- if verbose: print("got version from default %s" % ver)
- return default
-
-def get_versions(default=DEFAULT, verbose=False):
- assert versionfile_source is not None, "please set versioneer.versionfile_source"
- assert tag_prefix is not None, "please set versioneer.tag_prefix"
- assert parentdir_prefix is not None, "please set versioneer.parentdir_prefix"
- return get_best_versions(versionfile_source, tag_prefix, parentdir_prefix,
- default=default, verbose=verbose)
-def get_version(verbose=False):
- return get_versions(verbose=verbose)["version"]
-
-class cmd_version(Command):
- description = "report generated version string"
- user_options = []
- boolean_options = []
- def initialize_options(self):
+ Exceptions:
+ 1: no tags. HEX[-dirty] (note: no 'g' prefix)
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"]:
+ rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
+ else:
+ # exception #1
+ rendered = pieces["short"]
+ if pieces["dirty"]:
+ rendered += "-dirty"
+ return rendered
+
+
+def render_git_describe_long(pieces):
+ """TAG-DISTANCE-gHEX[-dirty].
+
+ Like 'git describe --tags --dirty --always -long'.
+ The distance/hash is unconditional.
+
+ Exceptions:
+ 1: no tags. HEX[-dirty] (note: no 'g' prefix)
+ """
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
+ else:
+ # exception #1
+ rendered = pieces["short"]
+ if pieces["dirty"]:
+ rendered += "-dirty"
+ return rendered
+
+
+def render(pieces, style):
+ """Render the given version pieces into the requested style."""
+ if pieces["error"]:
+ return {"version": "unknown",
+ "full-revisionid": pieces.get("long"),
+ "dirty": None,
+ "error": pieces["error"]}
+
+ if not style or style == "default":
+ style = "pep440" # the default
+
+ if style == "pep440":
+ rendered = render_pep440(pieces)
+ elif style == "pep440-pre":
+ rendered = render_pep440_pre(pieces)
+ elif style == "pep440-post":
+ rendered = render_pep440_post(pieces)
+ elif style == "pep440-old":
+ rendered = render_pep440_old(pieces)
+ elif style == "git-describe":
+ rendered = render_git_describe(pieces)
+ elif style == "git-describe-long":
+ rendered = render_git_describe_long(pieces)
+ else:
+ raise ValueError("unknown style '%s'" % style)
+
+ return {"version": rendered, "full-revisionid": pieces["long"],
+ "dirty": pieces["dirty"], "error": None}
+
+
+class VersioneerBadRootError(Exception):
+ """The project root directory is unknown or missing key files."""
+
+
+def get_versions(verbose=False):
+ """Get the project version from whatever source is available.
+
+ Returns dict with two keys: 'version' and 'full'.
+ """
+ if "versioneer" in sys.modules:
+ # see the discussion in cmdclass.py:get_cmdclass()
+ del sys.modules["versioneer"]
+
+ root = get_root()
+ cfg = get_config_from_root(root)
+
+ assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg"
+ handlers = HANDLERS.get(cfg.VCS)
+ assert handlers, "unrecognized VCS '%s'" % cfg.VCS
+ verbose = verbose or cfg.verbose
+ assert cfg.versionfile_source is not None, \
+ "please set versioneer.versionfile_source"
+ assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix"
+
+ versionfile_abs = os.path.join(root, cfg.versionfile_source)
+
+ # extract version from first of: _version.py, VCS command (e.g. 'git
+ # describe'), parentdir. This is meant to work for developers using a
+ # source checkout, for users of a tarball created by 'setup.py sdist',
+ # and for users of a tarball/zipball created by 'git archive' or github's
+ # download-from-tag feature or the equivalent in other VCSes.
+
+ get_keywords_f = handlers.get("get_keywords")
+ from_keywords_f = handlers.get("keywords")
+ if get_keywords_f and from_keywords_f:
+ try:
+ keywords = get_keywords_f(versionfile_abs)
+ ver = from_keywords_f(keywords, cfg.tag_prefix, verbose)
+ if verbose:
+ print("got version from expanded keyword %s" % ver)
+ return ver
+ except NotThisMethod:
+ pass
+
+ try:
+ ver = versions_from_file(versionfile_abs)
+ if verbose:
+ print("got version from file %s %s" % (versionfile_abs, ver))
+ return ver
+ except NotThisMethod:
pass
- def finalize_options(self):
+
+ from_vcs_f = handlers.get("pieces_from_vcs")
+ if from_vcs_f:
+ try:
+ pieces = from_vcs_f(cfg.tag_prefix, root, verbose)
+ ver = render(pieces, cfg.style)
+ if verbose:
+ print("got version from VCS %s" % ver)
+ return ver
+ except NotThisMethod:
+ pass
+
+ try:
+ if cfg.parentdir_prefix:
+ ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
+ if verbose:
+ print("got version from parentdir %s" % ver)
+ return ver
+ except NotThisMethod:
pass
- def run(self):
- ver = get_version(verbose=True)
- print("Version is currently: %s" % ver)
-
-
-class cmd_build(_build):
- def run(self):
- versions = get_versions(verbose=True)
- _build.run(self)
- # now locate _version.py in the new build/ directory and replace it
- # with an updated value
- target_versionfile = os.path.join(self.build_lib, versionfile_build)
- print("UPDATING %s" % target_versionfile)
- os.unlink(target_versionfile)
- f = open(target_versionfile, "w")
- f.write(SHORT_VERSION_PY % versions)
- f.close()
-class cmd_sdist(_sdist):
- def run(self):
- versions = get_versions(verbose=True)
- self._versioneer_generated_versions = versions
- # unless we update this, the command will keep using the old version
- self.distribution.metadata.version = versions["version"]
- return _sdist.run(self)
-
- def make_release_tree(self, base_dir, files):
- _sdist.make_release_tree(self, base_dir, files)
- # now locate _version.py in the new base_dir directory (remembering
- # that it may be a hardlink) and replace it with an updated value
- target_versionfile = os.path.join(base_dir, versionfile_source)
- print("UPDATING %s" % target_versionfile)
- os.unlink(target_versionfile)
- f = open(target_versionfile, "w")
- f.write(SHORT_VERSION_PY % self._versioneer_generated_versions)
- f.close()
+ if verbose:
+ print("unable to compute version")
+
+ return {"version": "0+unknown", "full-revisionid": None,
+ "dirty": None, "error": "unable to compute version"}
+
+
+def get_version():
+ """Get the short version string for this project."""
+ return get_versions()["version"]
+
+
+def get_cmdclass():
+ """Get the custom setuptools/distutils subclasses used by Versioneer."""
+ if "versioneer" in sys.modules:
+ del sys.modules["versioneer"]
+ # this fixes the "python setup.py develop" case (also 'install' and
+ # 'easy_install .'), in which subdependencies of the main project are
+ # built (using setup.py bdist_egg) in the same python process. Assume
+ # a main project A and a dependency B, which use different versions
+ # of Versioneer. A's setup.py imports A's Versioneer, leaving it in
+ # sys.modules by the time B's setup.py is executed, causing B to run
+ # with the wrong versioneer. Setuptools wraps the sub-dep builds in a
+ # sandbox that restores sys.modules to it's pre-build state, so the
+ # parent is protected against the child's "import versioneer". By
+ # removing ourselves from sys.modules here, before the child build
+ # happens, we protect the child from the parent's versioneer too.
+ # Also see https://github.com/warner/python-versioneer/issues/52
+
+ cmds = {}
+
+ # we add "version" to both distutils and setuptools
+ from distutils.core import Command
+
+ class cmd_version(Command):
+ description = "report generated version string"
+ user_options = []
+ boolean_options = []
+
+ def initialize_options(self):
+ pass
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ vers = get_versions(verbose=True)
+ print("Version: %s" % vers["version"])
+ print(" full-revisionid: %s" % vers.get("full-revisionid"))
+ print(" dirty: %s" % vers.get("dirty"))
+ if vers["error"]:
+ print(" error: %s" % vers["error"])
+ cmds["version"] = cmd_version
+
+ # we override "build_py" in both distutils and setuptools
+ #
+ # most invocation pathways end up running build_py:
+ # distutils/build -> build_py
+ # distutils/install -> distutils/build ->..
+ # setuptools/bdist_wheel -> distutils/install ->..
+ # setuptools/bdist_egg -> distutils/install_lib -> build_py
+ # setuptools/install -> bdist_egg ->..
+ # setuptools/develop -> ?
+
+ # we override different "build_py" commands for both environments
+ if "setuptools" in sys.modules:
+ from setuptools.command.build_py import build_py as _build_py
+ else:
+ from distutils.command.build_py import build_py as _build_py
+
+ class cmd_build_py(_build_py):
+
+ def run(self):
+ root = get_root()
+ cfg = get_config_from_root(root)
+ versions = get_versions()
+ _build_py.run(self)
+ # now locate _version.py in the new build/ directory and replace
+ # it with an updated value
+ if cfg.versionfile_build:
+ target_versionfile = os.path.join(self.build_lib,
+ cfg.versionfile_build)
+ print("UPDATING %s" % target_versionfile)
+ write_to_version_file(target_versionfile, versions)
+ cmds["build_py"] = cmd_build_py
+
+ if "cx_Freeze" in sys.modules: # cx_freeze enabled?
+ from cx_Freeze.dist import build_exe as _build_exe
+
+ class cmd_build_exe(_build_exe):
+
+ def run(self):
+ root = get_root()
+ cfg = get_config_from_root(root)
+ versions = get_versions()
+ target_versionfile = cfg.versionfile_source
+ print("UPDATING %s" % target_versionfile)
+ write_to_version_file(target_versionfile, versions)
+
+ _build_exe.run(self)
+ os.unlink(target_versionfile)
+ with open(cfg.versionfile_source, "w") as f:
+ LONG = LONG_VERSION_PY[cfg.VCS]
+ f.write(LONG %
+ {"DOLLAR": "$",
+ "STYLE": cfg.style,
+ "TAG_PREFIX": cfg.tag_prefix,
+ "PARENTDIR_PREFIX": cfg.parentdir_prefix,
+ "VERSIONFILE_SOURCE": cfg.versionfile_source,
+ })
+ cmds["build_exe"] = cmd_build_exe
+ del cmds["build_py"]
+
+ # we override different "sdist" commands for both environments
+ if "setuptools" in sys.modules:
+ from setuptools.command.sdist import sdist as _sdist
+ else:
+ from distutils.command.sdist import sdist as _sdist
+
+ class cmd_sdist(_sdist):
+
+ def run(self):
+ versions = get_versions()
+ self._versioneer_generated_versions = versions
+ # unless we update this, the command will keep using the old
+ # version
+ self.distribution.metadata.version = versions["version"]
+ return _sdist.run(self)
+
+ def make_release_tree(self, base_dir, files):
+ root = get_root()
+ cfg = get_config_from_root(root)
+ _sdist.make_release_tree(self, base_dir, files)
+ # now locate _version.py in the new base_dir directory
+ # (remembering that it may be a hardlink) and replace it with an
+ # updated value
+ target_versionfile = os.path.join(base_dir, cfg.versionfile_source)
+ print("UPDATING %s" % target_versionfile)
+ write_to_version_file(target_versionfile,
+ self._versioneer_generated_versions)
+ cmds["sdist"] = cmd_sdist
+
+ return cmds
+
+
+CONFIG_ERROR = """
+setup.cfg is missing the necessary Versioneer configuration. You need
+a section like:
+
+ [versioneer]
+ VCS = git
+ style = pep440
+ versionfile_source = src/myproject/_version.py
+ versionfile_build = myproject/_version.py
+ tag_prefix =
+ parentdir_prefix = myproject-
+
+You will also need to edit your setup.py to use the results:
+
+ import versioneer
+ setup(version=versioneer.get_version(),
+ cmdclass=versioneer.get_cmdclass(), ...)
+
+Please read the docstring in ./versioneer.py for configuration instructions,
+edit setup.cfg, and re-run the installer or 'python versioneer.py setup'.
+"""
+
+SAMPLE_CONFIG = """
+# See the docstring in versioneer.py for instructions. Note that you must
+# re-run 'versioneer.py setup' after changing this section, and commit the
+# resulting files.
+
+[versioneer]
+#VCS = git
+#style = pep440
+#versionfile_source =
+#versionfile_build =
+#tag_prefix =
+#parentdir_prefix =
+
+"""
INIT_PY_SNIPPET = """
from ._version import get_versions
@@ -630,40 +1649,129 @@ __version__ = get_versions()['version']
del get_versions
"""
-class cmd_update_files(Command):
- description = "modify __init__.py and create _version.py"
- user_options = []
- boolean_options = []
- def initialize_options(self):
- pass
- def finalize_options(self):
- pass
- def run(self):
- ipy = os.path.join(os.path.dirname(versionfile_source), "__init__.py")
- print(" creating %s" % versionfile_source)
- f = open(versionfile_source, "w")
- f.write(LONG_VERSION_PY % {"DOLLAR": "$",
- "TAG_PREFIX": tag_prefix,
- "PARENTDIR_PREFIX": parentdir_prefix,
- "VERSIONFILE_SOURCE": versionfile_source,
- })
- f.close()
+
+def do_setup():
+ """Main VCS-independent setup function for installing Versioneer."""
+ root = get_root()
+ try:
+ cfg = get_config_from_root(root)
+ except (EnvironmentError, configparser.NoSectionError,
+ configparser.NoOptionError) as e:
+ if isinstance(e, (EnvironmentError, configparser.NoSectionError)):
+ print("Adding sample versioneer config to setup.cfg",
+ file=sys.stderr)
+ with open(os.path.join(root, "setup.cfg"), "a") as f:
+ f.write(SAMPLE_CONFIG)
+ print(CONFIG_ERROR, file=sys.stderr)
+ return 1
+
+ print(" creating %s" % cfg.versionfile_source)
+ with open(cfg.versionfile_source, "w") as f:
+ LONG = LONG_VERSION_PY[cfg.VCS]
+ f.write(LONG % {"DOLLAR": "$",
+ "STYLE": cfg.style,
+ "TAG_PREFIX": cfg.tag_prefix,
+ "PARENTDIR_PREFIX": cfg.parentdir_prefix,
+ "VERSIONFILE_SOURCE": cfg.versionfile_source,
+ })
+
+ ipy = os.path.join(os.path.dirname(cfg.versionfile_source),
+ "__init__.py")
+ if os.path.exists(ipy):
try:
- old = open(ipy, "r").read()
+ with open(ipy, "r") as f:
+ old = f.read()
except EnvironmentError:
old = ""
if INIT_PY_SNIPPET not in old:
print(" appending to %s" % ipy)
- f = open(ipy, "a")
- f.write(INIT_PY_SNIPPET)
- f.close()
+ with open(ipy, "a") as f:
+ f.write(INIT_PY_SNIPPET)
else:
print(" %s unmodified" % ipy)
- do_vcs_install(versionfile_source, ipy)
+ else:
+ print(" %s doesn't exist, ok" % ipy)
+ ipy = None
+
+ # Make sure both the top-level "versioneer.py" and versionfile_source
+ # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so
+ # they'll be copied into source distributions. Pip won't be able to
+ # install the package without this.
+ manifest_in = os.path.join(root, "MANIFEST.in")
+ simple_includes = set()
+ try:
+ with open(manifest_in, "r") as f:
+ for line in f:
+ if line.startswith("include "):
+ for include in line.split()[1:]:
+ simple_includes.add(include)
+ except EnvironmentError:
+ pass
+ # That doesn't cover everything MANIFEST.in can do
+ # (http://docs.python.org/2/distutils/sourcedist.html#commands), so
+ # it might give some false negatives. Appending redundant 'include'
+ # lines is safe, though.
+ if "versioneer.py" not in simple_includes:
+ print(" appending 'versioneer.py' to MANIFEST.in")
+ with open(manifest_in, "a") as f:
+ f.write("include versioneer.py\n")
+ else:
+ print(" 'versioneer.py' already in MANIFEST.in")
+ if cfg.versionfile_source not in simple_includes:
+ print(" appending versionfile_source ('%s') to MANIFEST.in" %
+ cfg.versionfile_source)
+ with open(manifest_in, "a") as f:
+ f.write("include %s\n" % cfg.versionfile_source)
+ else:
+ print(" versionfile_source already in MANIFEST.in")
-def get_cmdclass():
- return {'version': cmd_version,
- 'update_files': cmd_update_files,
- 'build': cmd_build,
- 'sdist': cmd_sdist,
- }
+ # Make VCS-specific changes. For git, this means creating/changing
+ # .gitattributes to mark _version.py for export-time keyword
+ # substitution.
+ do_vcs_install(manifest_in, cfg.versionfile_source, ipy)
+ return 0
+
+
+def scan_setup_py():
+ """Validate the contents of setup.py against Versioneer's expectations."""
+ found = set()
+ setters = False
+ errors = 0
+ with open("setup.py", "r") as f:
+ for line in f.readlines():
+ if "import versioneer" in line:
+ found.add("import")
+ if "versioneer.get_cmdclass()" in line:
+ found.add("cmdclass")
+ if "versioneer.get_version()" in line:
+ found.add("get_version")
+ if "versioneer.VCS" in line:
+ setters = True
+ if "versioneer.versionfile_source" in line:
+ setters = True
+ if len(found) != 3:
+ print("")
+ print("Your setup.py appears to be missing some important items")
+ print("(but I might be wrong). Please make sure it has something")
+ print("roughly like the following:")
+ print("")
+ print(" import versioneer")
+ print(" setup( version=versioneer.get_version(),")
+ print(" cmdclass=versioneer.get_cmdclass(), ...)")
+ print("")
+ errors += 1
+ if setters:
+ print("You should remove lines like 'versioneer.VCS = ' and")
+ print("'versioneer.versionfile_source = ' . This configuration")
+ print("now lives in setup.cfg, and should be removed from setup.py")
+ print("")
+ errors += 1
+ return errors
+
+if __name__ == "__main__":
+ cmd = sys.argv[1]
+ if cmd == "setup":
+ errors = do_setup()
+ errors += scan_setup_py()
+ if errors:
+ sys.exit(1)