summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.coveragerc25
-rw-r--r--.gitignore17
-rw-r--r--.tx/config10
-rw-r--r--CHANGELOG210
-rw-r--r--LICENSE (renamed from COPYING)24
-rw-r--r--MANIFEST.in9
-rw-r--r--Makefile66
-rw-r--r--NEWS.rst76
-rw-r--r--README.rst91
-rw-r--r--data/TODO1
-rw-r--r--data/bitmask.pro56
-rw-r--r--data/images/Arrow-Down-32.pngbin0 -> 1458 bytes
-rw-r--r--data/images/Arrow-Up-32.pngbin0 -> 1400 bytes
-rw-r--r--data/images/Blue-Arrow-Right-32.pngbin0 -> 1432 bytes
-rw-r--r--data/images/Globe.pngbin0 -> 22470 bytes
-rw-r--r--data/images/conn_connected-light.pngbin0 -> 426 bytes
-rw-r--r--data/images/conn_connecting-light.pngbin0 -> 791 bytes
-rw-r--r--data/images/conn_error-light.pngbin0 -> 1457 bytes
-rw-r--r--data/images/leap-client.tiffbin0 -> 3598 bytes
-rw-r--r--data/images/leap-color-big.pngbin0 -> 30322 bytes
-rw-r--r--data/images/leap-gray-big.pngbin0 -> 17246 bytes
-rw-r--r--data/images/leapfrog.jpgbin1767 -> 0 bytes
-rw-r--r--data/images/light/16/accept.pngbin0 -> 285 bytes
-rw-r--r--data/images/light/16/down-arrow.pngbin0 -> 356 bytes
-rw-r--r--data/images/light/16/error.pngbin0 -> 355 bytes
-rw-r--r--data/images/light/16/refresh.pngbin0 -> 472 bytes
-rw-r--r--data/images/light/16/up-arrow.pngbin0 -> 357 bytes
-rw-r--r--data/images/light/64/network-eip-acquiring.pngbin0 -> 791 bytes
-rw-r--r--data/images/light/64/network-eip-down.pngbin0 -> 1429 bytes
-rw-r--r--data/images/light/64/network-eip-up.pngbin0 -> 426 bytes
-rw-r--r--data/images/mask-icon.pngbin0 -> 16575 bytes
-rw-r--r--data/images/mask-launcher.pngbin0 -> 37759 bytes
-rw-r--r--data/images/oxygen-icons/dialog-error.pngbin0 -> 1527 bytes
-rw-r--r--data/images/oxygen-icons/dialog-information.pngbin0 -> 1636 bytes
-rw-r--r--data/images/oxygen-icons/dialog-warning.pngbin0 -> 1354 bytes
-rw-r--r--data/images/oxygen-icons/document-save-as.pngbin0 -> 2152 bytes
-rw-r--r--data/images/oxygen-icons/edit-bomb.pngbin0 -> 1635 bytes
-rw-r--r--data/images/oxygen-icons/script-error.pngbin0 -> 1709 bytes
-rw-r--r--data/leap_client.pro25
-rwxr-xr-xdata/mkpyqt.py271
-rw-r--r--data/resources/icons.qrc9
-rw-r--r--data/resources/loggerwindow.qrc11
-rw-r--r--data/resources/mainwindow.qrc33
-rw-r--r--data/translations/de.qmbin6036 -> 0 bytes
-rw-r--r--data/translations/de.ts218
-rw-r--r--data/translations/es.qmbin13360 -> 12111 bytes
-rw-r--r--data/translations/es.ts608
-rw-r--r--data/ts/en_US.ts752
-rw-r--r--docs/COPYING674
-rw-r--r--docs/api/leap.base.rst98
-rw-r--r--docs/api/leap.base.tests.rst43
-rw-r--r--docs/api/leap.baseapp.rst75
-rw-r--r--docs/api/leap.certs.rst11
-rw-r--r--docs/api/leap.config.rst27
-rw-r--r--docs/api/leap.crypto.rst18
-rw-r--r--docs/api/leap.crypto.tests.rst22
-rw-r--r--docs/api/leap.eip.rst74
-rw-r--r--docs/api/leap.eip.tests.rst43
-rw-r--r--docs/api/leap.gui.firstrun.rst91
-rw-r--r--docs/api/leap.gui.rst68
-rw-r--r--docs/api/leap.gui.tests.rst59
-rw-r--r--docs/api/leap.keymanager.rst50
-rw-r--r--docs/api/leap.keymanager.tests.rst11
-rw-r--r--docs/api/leap.platform_init.rst27
-rw-r--r--docs/api/leap.rst9
-rw-r--r--docs/api/leap.services.eip.rst66
-rw-r--r--docs/api/leap.services.eip.tests.rst11
-rw-r--r--docs/api/leap.services.mail.rst27
-rw-r--r--docs/api/leap.services.rst28
-rw-r--r--docs/api/leap.services.soledad.rst27
-rw-r--r--docs/api/leap.testing.rst43
-rw-r--r--docs/api/leap.util.rst49
-rw-r--r--docs/api/leap.util.tests.rst19
-rw-r--r--docs/checklist_for_leap_client_release.wiki38
-rw-r--r--docs/conf.py26
-rw-r--r--docs/config/files.rst2
-rw-r--r--docs/dev/environment.rst61
-rw-r--r--docs/dev/internals.rst2
-rw-r--r--docs/dev/internationalization.rst40
-rw-r--r--docs/dev/resources.rst4
-rw-r--r--docs/dev/tests.rst4
-rw-r--r--docs/dev/workflow.rst8
-rw-r--r--docs/index.rst7
-rw-r--r--docs/known_issues.rst4
-rw-r--r--docs/man/bitmask.1.rst (renamed from docs/man/leap-client.1.rst)14
-rw-r--r--docs/pkg/debian.rst4
-rw-r--r--docs/testers/howto.rst34
-rw-r--r--docs/user/install.rst61
-rw-r--r--docs/user/intro.rst16
-rw-r--r--docs/user/running.rst34
-rw-r--r--ez_setup.py284
-rw-r--r--openvpn/README6
-rw-r--r--openvpn/Sources4
-rwxr-xr-xopenvpn/build.zsh191
-rw-r--r--pkg/branding/__init__.py15
-rw-r--r--pkg/branding/config.py11
-rw-r--r--pkg/dev-reqs.pip4
-rw-r--r--pkg/distribute_setup.py515
-rwxr-xr-xpkg/install_pyqt.sh10
-rw-r--r--pkg/install_venv.py4
-rw-r--r--pkg/osx/Makefile25
-rw-r--r--pkg/osx/README.rst28
-rwxr-xr-xpkg/osx/build_tuntaposx45
-rw-r--r--pkg/osx/install/ProcessNetworkChanges.plist.template2
-rwxr-xr-xpkg/osx/install/client.down.sh34
-rwxr-xr-xpkg/osx/install/client.up.sh29
-rwxr-xr-xpkg/osx/install/install-leapc.sh43
-rw-r--r--pkg/osx/install/tun.kext/Info.plist36
-rw-r--r--pkg/osx/leap-client.spec6
m---------pkg/osx/tuntaposx0
-rwxr-xr-xpkg/postmkvenv.sh8
-rw-r--r--pkg/requirements-dev.pip17
-rw-r--r--pkg/requirements-testing.pip18
-rw-r--r--pkg/requirements.pip35
-rw-r--r--pkg/scripts/leap_client_bootstrap.sh2
-rw-r--r--pkg/test-requirements.pip8
-rw-r--r--pkg/utils.py52
-rw-r--r--pkg/windows/openvpn_manifest19
-rw-r--r--relnotes.txt31
-rwxr-xr-xrun_tests.sh45
-rw-r--r--setup.cfg3
-rwxr-xr-xsetup.py196
-rw-r--r--setup/install_venv.py242
-rw-r--r--setup/linux/leap.desktop13
-rw-r--r--setup/linux/polkit/net.openvpn.gui.leap.policy23
-rw-r--r--setup/requirements.pip1
-rwxr-xr-xsetup/scripts/leap6
-rw-r--r--setup/test-requires5
-rwxr-xr-xsetup/tools/with_venv.sh4
-rw-r--r--src/leap/__init__.py37
-rw-r--r--src/leap/_version.py9
-rw-r--r--src/leap/app.py242
-rw-r--r--src/leap/base/auth.py355
-rw-r--r--src/leap/base/authentication.py11
-rw-r--r--src/leap/base/checks.py214
-rw-r--r--src/leap/base/config.py348
-rw-r--r--src/leap/base/connection.py115
-rw-r--r--src/leap/base/constants.py42
-rw-r--r--src/leap/base/exceptions.py97
-rw-r--r--src/leap/base/network.py107
-rw-r--r--src/leap/base/pluggableconfig.py462
-rw-r--r--src/leap/base/providers.py29
-rw-r--r--src/leap/base/tests/test_auth.py58
-rw-r--r--src/leap/base/tests/test_checks.py189
-rw-r--r--src/leap/base/tests/test_config.py247
-rw-r--r--src/leap/base/tests/test_providers.py148
-rw-r--r--src/leap/base/tests/test_validation.py93
-rw-r--r--src/leap/baseapp/constants.py6
-rw-r--r--src/leap/baseapp/dialogs.py61
-rw-r--r--src/leap/baseapp/eip.py243
-rw-r--r--src/leap/baseapp/leap_app.py153
-rw-r--r--src/leap/baseapp/log.py69
-rw-r--r--src/leap/baseapp/mainwindow.py191
-rw-r--r--src/leap/baseapp/network.py63
-rw-r--r--src/leap/baseapp/permcheck.py17
-rw-r--r--src/leap/baseapp/systray.py268
-rw-r--r--src/leap/certs/__init__.py7
-rw-r--r--src/leap/config/__init__.py (renamed from src/leap/base/__init__.py)0
-rw-r--r--src/leap/config/leapsettings.py253
-rw-r--r--src/leap/config/provider_spec.py (renamed from src/leap/base/specs.py)51
-rw-r--r--src/leap/config/providerconfig.py218
-rw-r--r--src/leap/config/tests/test_providerconfig.py279
-rw-r--r--src/leap/crypto/certs.py112
-rw-r--r--src/leap/crypto/certs_gnutls.py112
-rw-r--r--src/leap/crypto/leapkeyring.py70
-rw-r--r--src/leap/crypto/srpauth.py606
-rw-r--r--src/leap/crypto/srpregister.py168
-rw-r--r--src/leap/crypto/tests/__init__.py16
-rw-r--r--src/leap/crypto/tests/eip-service.json43
-rwxr-xr-xsrc/leap/crypto/tests/fake_provider.py376
-rw-r--r--src/leap/crypto/tests/openvpn.pem33
-rw-r--r--src/leap/crypto/tests/test_certs.py22
-rw-r--r--src/leap/crypto/tests/test_provider.json15
-rw-r--r--src/leap/crypto/tests/test_srpauth.py790
-rw-r--r--src/leap/crypto/tests/test_srpregister.py201
-rw-r--r--src/leap/crypto/tests/wrongcert.pem33
-rw-r--r--src/leap/eip/checks.py542
-rw-r--r--src/leap/eip/config.py398
-rw-r--r--src/leap/eip/constants.py3
-rw-r--r--src/leap/eip/eipconnection.py405
-rw-r--r--src/leap/eip/exceptions.py175
-rw-r--r--src/leap/eip/openvpnconnection.py410
-rw-r--r--src/leap/eip/specs.py136
-rw-r--r--src/leap/eip/tests/data.py51
-rw-r--r--src/leap/eip/tests/test_checks.py372
-rw-r--r--src/leap/eip/tests/test_config.py298
-rw-r--r--src/leap/eip/tests/test_eipconnection.py216
-rw-r--r--src/leap/eip/tests/test_openvpnconnection.py161
-rw-r--r--src/leap/eip/udstelnet.py38
-rw-r--r--src/leap/gui/__init__.py32
-rw-r--r--src/leap/gui/constants.py14
-rw-r--r--src/leap/gui/firstrun/__init__.py28
-rw-r--r--src/leap/gui/firstrun/connect.py218
-rw-r--r--src/leap/gui/firstrun/constants.py0
-rw-r--r--src/leap/gui/firstrun/intro.py68
-rw-r--r--src/leap/gui/firstrun/last.py119
-rw-r--r--src/leap/gui/firstrun/login.py341
-rw-r--r--src/leap/gui/firstrun/mixins.py18
-rw-r--r--src/leap/gui/firstrun/providerinfo.py110
-rw-r--r--src/leap/gui/firstrun/providerselect.py475
-rw-r--r--src/leap/gui/firstrun/providersetup.py161
-rw-r--r--src/leap/gui/firstrun/register.py391
-rwxr-xr-xsrc/leap/gui/firstrun/tests/integration/fake_provider.py302
-rwxr-xr-xsrc/leap/gui/firstrun/wizard.py309
-rw-r--r--src/leap/gui/locale_rc.py1260
-rw-r--r--src/leap/gui/loggerwindow.py137
-rw-r--r--src/leap/gui/login.py245
-rw-r--r--src/leap/gui/mainwindow.py1537
-rw-r--r--src/leap/gui/mainwindow_rc.py2564
-rw-r--r--src/leap/gui/progress.py488
-rw-r--r--src/leap/gui/statuspanel.py459
-rw-r--r--src/leap/gui/styles.py16
-rw-r--r--src/leap/gui/tests/integration/fake_user_signup.py84
-rw-r--r--src/leap/gui/tests/test_firstrun_login.py212
-rw-r--r--src/leap/gui/tests/test_firstrun_providerselect.py203
-rw-r--r--src/leap/gui/tests/test_firstrun_register.py244
-rw-r--r--src/leap/gui/tests/test_firstrun_wizard.py137
-rw-r--r--src/leap/gui/tests/test_mainwindow_rc.py32
-rw-r--r--src/leap/gui/tests/test_progress.py449
-rw-r--r--src/leap/gui/tests/test_threads.py27
-rw-r--r--src/leap/gui/threads.py21
-rw-r--r--src/leap/gui/twisted_main.py60
-rw-r--r--src/leap/gui/ui/loggerwindow.ui155
-rw-r--r--src/leap/gui/ui/login.ui132
-rw-r--r--src/leap/gui/ui/mainwindow.ui315
-rw-r--r--src/leap/gui/ui/statuspanel.ui289
-rw-r--r--src/leap/gui/ui/wizard.ui846
-rw-r--r--src/leap/gui/utils.py34
-rw-r--r--src/leap/gui/wizard.py626
-rw-r--r--src/leap/gui/wizardpage.py40
-rw-r--r--src/leap/platform_init/__init__.py28
-rw-r--r--src/leap/platform_init/initializers.py409
-rw-r--r--src/leap/platform_init/locks.py405
-rw-r--r--src/leap/provider/__init__.py (renamed from src/leap/base/tests/__init__.py)0
-rw-r--r--src/leap/provider/supportedapis.py38
-rw-r--r--src/leap/services/__init__.py33
-rw-r--r--src/leap/services/abstractbootstrapper.py163
-rw-r--r--src/leap/services/eip/__init__.py (renamed from src/leap/baseapp/__init__.py)0
-rw-r--r--src/leap/services/eip/eipbootstrapper.py183
-rw-r--r--src/leap/services/eip/eipconfig.py263
-rw-r--r--src/leap/services/eip/eipspec.py85
-rw-r--r--src/leap/services/eip/providerbootstrapper.py340
-rw-r--r--src/leap/services/eip/tests/__init__.py (renamed from src/leap/eip/__init__.py)0
-rw-r--r--src/leap/services/eip/tests/test_eipbootstrapper.py347
-rw-r--r--src/leap/services/eip/tests/test_eipconfig.py324
-rw-r--r--src/leap/services/eip/tests/test_providerbootstrapper.py531
-rw-r--r--src/leap/services/eip/tests/test_vpngatewayselector.py131
-rw-r--r--src/leap/services/eip/tests/wrongcert.pem33
-rw-r--r--src/leap/services/eip/udstelnet.py60
-rw-r--r--src/leap/services/eip/vpnlaunchers.py927
-rw-r--r--src/leap/services/eip/vpnprocess.py791
-rw-r--r--src/leap/services/mail/__init__.py (renamed from src/leap/eip/tests/__init__.py)0
-rw-r--r--src/leap/services/mail/imap.py42
-rw-r--r--src/leap/services/mail/smtpbootstrapper.py139
-rw-r--r--src/leap/services/mail/smtpconfig.py49
-rw-r--r--src/leap/services/mail/smtpspec.py70
-rw-r--r--src/leap/services/soledad/__init__.py (renamed from src/leap/gui/tests/__init__.py)0
-rw-r--r--src/leap/services/soledad/soledadbootstrapper.py265
-rw-r--r--src/leap/services/soledad/soledadconfig.py49
-rw-r--r--src/leap/services/soledad/soledadspec.py76
-rw-r--r--src/leap/services/tests/__init__.py (renamed from src/leap/testing/__init__.py)0
-rw-r--r--src/leap/services/tests/test_abstractbootstrapper.py196
-rw-r--r--src/leap/services/tx.py46
-rw-r--r--src/leap/testing/basetest.py85
-rw-r--r--src/leap/testing/cacert.pem23
-rw-r--r--src/leap/testing/https_server.py68
-rw-r--r--src/leap/testing/leaptestscert.pem84
-rw-r--r--src/leap/testing/leaptestskey.pem27
-rw-r--r--src/leap/testing/pyqt.py52
-rw-r--r--src/leap/testing/qunittest.py302
-rw-r--r--src/leap/testing/test_basetest.py91
-rw-r--r--src/leap/util/__init__.py108
-rw-r--r--src/leap/util/certs.py18
-rw-r--r--src/leap/util/constants.py19
-rw-r--r--src/leap/util/coroutines.py109
-rw-r--r--src/leap/util/dicts.py268
-rw-r--r--src/leap/util/fileutil.py120
-rw-r--r--src/leap/util/geo.py32
-rw-r--r--src/leap/util/keyring_helpers.py37
-rw-r--r--src/leap/util/leap_argparse.py34
-rw-r--r--src/leap/util/leap_log_handler.py134
-rw-r--r--src/leap/util/misc.py37
-rw-r--r--src/leap/util/polkit.py36
-rw-r--r--src/leap/util/privilege_policies.py169
-rw-r--r--src/leap/util/pyside_tests_helper.py136
-rw-r--r--src/leap/util/request_helpers.py58
-rw-r--r--src/leap/util/requirement_checker.py101
-rw-r--r--src/leap/util/streamtologger.py59
-rw-r--r--src/leap/util/tests/test_fileutil.py100
-rw-r--r--src/leap/util/tests/test_is_release_version.py57
-rw-r--r--src/leap/util/tests/test_leap_argparse.py35
-rw-r--r--src/leap/util/tests/test_leap_log_handler.py118
-rw-r--r--src/leap/util/tests/test_streamtologger.py122
-rw-r--r--src/leap/util/tests/test_translations.py22
-rw-r--r--src/leap/util/translations.py82
-rw-r--r--src/leap/util/web.py40
-rw-r--r--tests/README1
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/test_qt_environment.py39
-rw-r--r--tox.ini12
300 files changed, 17809 insertions, 21292 deletions
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 00000000..8ecf46a6
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,25 @@
+# .coveragerc to control coverage.py
+[run]
+branch = True
+omit=src/leap/gui/ui_*,src/leap/*/tests/*,src/leap/*/*/tests/*,src/leap/gui/*_rc*,src/leap/_version*
+
+[report]
+# Regexes for lines to exclude from consideration
+exclude_lines =
+ # Have to re-enable the standard pragma
+ pragma: no cover
+
+ # Don't complain about missing debug-only code:
+ def __repr__
+ if self\.debug
+
+ # Don't complain if tests don't hit defensive assertion code:
+ raise AssertionError
+ raise NotImplementedError
+
+ # Don't complain if non-runnable code isn't run:
+ if 0:
+ if __name__ == .__main__.:
+
+ignore_errors = True
+omit=src/leap/gui/ui_*,src/leap/*/tests/*,src/leap/*/*/tests/*,src/leap/gui/*_rc*,src/leap/_version* \ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 26838e5c..7712b0d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,13 @@
*.swp
*.swo
*.pyc
+*.log
+*.*~
.*
+*_rc.py
+ui_*.py
+!.coveragerc
+!.tx
bin/
build/
core
@@ -14,5 +20,16 @@ lib/
local/
man/
share/
+src/leap/util/reqs.txt
src/leap.egg-info/
src/leap_client.egg-info
+src/leap/_branding.py
+src/leap/certs/*.pem
+src/*.egg-info
+src/pysqlcipher
+pkg/osx/dist
+pkg/osx/build
+MANIFEST
+_trial_temp*
+config/*
+CHANGELOG~
diff --git a/.tx/config b/.tx/config
new file mode 100644
index 00000000..31ea5b1c
--- /dev/null
+++ b/.tx/config
@@ -0,0 +1,10 @@
+[main]
+host = https://www.transifex.com
+
+[bitmask.bitmask]
+
+file_filter = data/translations/<lang>.ts
+source_file = data/ts/en_US.ts
+source_lang = en
+type = QT
+#minimum_perc = 90 # minimum percentage completed before pulling
diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 00000000..8713b220
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,210 @@
+0.3.0 Aug 9:
+ o Add missing scripts does not stop if a command fails, also warns
+ the user if there was an error. Closes #3294.
+ o Replace 'Sign Out' with 'Log Out' and 'User' with
+ 'Username'. Closes #3319.
+ o Verify cacert existence before using it. Closes bug #3362.
+ o Properly handle login failures. Closes bug #3401.
+ o Bugfix, avoid getting negative rates. Closes #3274.
+ o Raise window when setting it as visible. Fixes #3374
+ o Fail gracefully when the events port 8090 is in use by something
+ else. Fixes #3276.
+ o Validate the username in the login form against the same regexp as
+ the wizard registration form. Fixes #3214.
+ o Update text from the tray menu based on the visibility of the
+ window. Fixes #3400.
+ o Add check for outdated polkit file. Closes #3209.
+ o Add support for multiple schemas so we can support multiples api
+ versions. Closes #3310.
+ o Rebrand the client to be named Bitmask. Feature #3313.
+ o Add cancel button to login. Closes #3318.
+ o Add multiple schema support for SMTP. Closes #3403.
+ o Add multiple schema support for Soledad. Closes #3404.
+ o Update Transifex project name and translators'
+ documentation. Closes #3418.
+ o Add check for tuntap kext before launching openvpn. Closes: #2906
+ o Accept flag for changing openvpn verbosity in logs. Closes: #3305
+ o Add imap service to the client. Closes: #2579
+ o Add pyside-uic support inside the virtualenv. This way it won't
+ fail to 'make' if the virtualenv is activated. Closes #3411.
+ o Reintegrate SMTP relay module. Closes #3375
+ o Reintegrate Soledad into the client. Closes #3307.
+ o Support bundled gpg. Related to #3397.
+ o Set the default port for SMTP to be 2013.
+ o Display a more generic error message in the main window, and leave
+ the detailed one for the log. Closes #3373.
+
+0.2.4 Jul 26:
+ o Use the provider CA cert for every request once we have it
+ bootstrapped (TOFU). Closes #3227.
+ o Make calls to leap.common.events asynchronous. Closes #2937.
+ o Always logout when closing the app if the user previously signed
+ in. Fixes #3245.
+ o Make sure the domain field in provider.json is escaped to avoid
+ potential problems. Fixes #3244.
+ o Fix incorrect handling of locks in Windows so that stalled locks
+ do not avoid raising the first instance of the app. Closes: #2910
+ o Use traffic rates instead of totals. Closes #2913
+ o Allow to alternate between rates and total throughput for the
+ virtual interface. Closes: #3232
+ o Reset rates/totals when terminating connection. Closes #3249
+ o Fix a bug in the displayed magnitude for the up/down traffic rates
+ and totals.
+ o Force Cleanlooks style if we are running in a KDE environment, so
+ that it doesn't load potentially incompatible Qt libs. Fixes
+ #3194.
+ o Wrap long login status messages to 40 characters. Fixes #3124
+ o Workaround a segmentation fault when emitting a signal with its
+ last parameter being None. Fixes #3083.
+ o Added IS_RELEASE_VERSION flag that allows us to use code only in
+ develop versions. Closes #3224.
+ o Try to terminate already running openvpn instances. Closes #2916
+ o Linux: Dynamically generate policy file for polkit. Closes #3208
+ o Workaround some OpenVPN problems with priviledge dropping and
+ routing. Fixes #3178 #3135 #3207 #3203
+
+0.2.3 Jul 12:
+ o Adapt code to Soledad 0.2.1 api.
+ o Fix Main Window briefly display before the wizard on first
+ start. Closes Bug #2954.
+ o Bugfix: Remember should not be automatically set to
+ checked. Closes #2955.
+ o Bugfix: reload config if switching to a different provider. Closes
+ #3067.
+ o Bugfix: logger window's toggle button reflects window
+ state. Closes #3152.
+ o Set timeout for requests to 10 seconds globally, configurable from
+ leap.util.constants. Fixes #2878.
+ o Bugfix: display error message on registration problem. Closes
+ #3039.
+ o Make wizard use the main event loop, ensuring clean termination.
+ o Use cocoasudo for installing missing updown scripts.
+ o Bugfix: Systray Turn ON action fails because is not correctly
+ enabled/disabled. Closes #3125.
+ o Bugfix: wrong systray icon on startup. Closes #3147.
+ o Bugfix: parse line return in the logger window. Closes #3151.
+ o Do not log user data on registration. Fixes #3168.
+ o Add --log-append eip.log to windows EIP launcher options to save
+ the logs in case of any problems. Fixes #2054.
+ o OSX: Make the install_path relative to the launcher path instead
+ of absolute.
+ o OSX: Fix icon display in cocoasudo.
+ o OSX: Raise window when showing if running on OSX.
+ o Bugfix: EIP status button moved to status panel.
+ o Check if there is no gateway to use and display correct
+ message. Close #2921.
+ o Reorder tray icons according new design. Closes #2919.
+ o Redirect stdout/stderr and twisted log to the logger. Closes
+ #3134.
+ o Improve LoggerWindow colors for easier debugging.
+ o Move the key manager to its own repository/package.
+
+0.2.2 Jun 28:
+ o Add support for the kde polkit daemon
+ o Handle 'Incorrect Password' exception (keyring)
+ o Select the configured domain in the providers combo box. Closes
+ #2693.
+ o Remember provider along with the username and password. Closes
+ #2755.
+ o Close the app on rejected wizard. Closes bug #2905.
+ o Only use the Keyring when it's using a known good backend. Closes
+ #2960
+ o Update implementation and semantics of the supported and available
+ services by a provider. Closes bug #3032.
+ o Only show the question mark for a check being done if the previous
+ one passed. Fixes #2569.
+ o Fix main client window not restoring after minimized into
+ systray. Closes #2574
+ o Set EIP different status icons depending on OS. Closes #2643.
+ o Reimplement openvpn invocation to use twisted ProcessProtocol
+ o Add runtime requirements checker, verifies that the requirements
+ are installed and in its correct versions. Closes #2563
+ o Add centraliced logging facility, log history in a window. Closes
+ #2566
+ o Improve wizard, hide registration widgets (labels, inputs, button)
+ and only display a message. Closes #2694
+ o Clarify labels through the app (use of EIP)
+ o Check if the provider api version is supported. Closes feature
+ #2774.
+ o Autoselect VPN gateway based on timezone. Closes #2790.
+ o Disable vpn disconnect on logout. Closes #2795.
+ o Improve gateway selector based on timezone. It allows to use
+ multiple gateways in openvpn for redundancy. Closes #2894.
+ o Use cocoasudo in place of osascript for osx privilege escalation
+ during openvpn launch.
+ o Clicking in the tray icon will always show the context menu
+ instead of activating the window under certain
+ circumstances. Closes #2788
+ o Autostart EIP whenever possible. Closes #2815
+ o Update test suite, run_scripts and requirements to run smoothly
+ with buildbot.
+ o Add a copy of the processed requirements to util/
+ o Display the default provider configured in the systray menu. Close
+ #2813
+ o Make the login steps be a chain of defers in order to be able to
+ have more cancel points for the whole procedure. Closes #2571
+ o Linux: check for up/down scripts and policy files and ask user for
+ permission to install them in a root-writeable location. Used from
+ within bundle or for broken installations.
+ o Integrate SMTP-Relay into the client.
+ o Integrate Soledad and KeyManager.
+ o Move the KeyManager from leap.common to leap-client.
+ o Only use one systray icon, repesenting the status for EIP. Closes
+ #2762
+ o Properly set the binary manifest to the windows openvpn
+ binary. Closes #203
+ o OSX: Add dialog with suggestion to install up/down scripts if
+ these not found. Closes: #1264, #2759, #2249
+ o Workaround for PySide breaking with multiple inheritance. Closes
+ #2827
+ o Refactor login to its own widget and remove Utils menu. Closes
+ #2789
+ o Refactor the status bits out of the MainWindow to its own
+ StatusPanelWidget. Closes #2792
+ o Save the default provider to be used for autostart EIP as
+ DefaultProvider in leap.conf. Closes #2793
+ o Cleanly terminate openvpn process, sending SIGTERM and SIGKILL
+ after a while. Closes #2753
+ o Use twisted's deferToThread and Deferreds to handle parallel tasks
+ o Use a qt4 reactor for twisted, for launching leap twisted
+ services.
+
+0.2.1 May 15:
+ o Rewrite most of the client based on the insight gained so far.
+ o Deselecting the remember checkbox makes the app not populate
+ user/password values on the login widget. Closes #2059
+ o Rewording of setup steps in wizard, to make them more meaningful
+ to the non-technical user. Closes #2061
+ o Fix typo in wizard.
+ o Fix multiple drawing of services if going back.
+ o Make registration errors show in red.
+ o Add a warning if EIP service needs admin password. Addresses part
+ of #2062
+ o Make traffic indicators display fixed precision. Closes #2114
+ o Do not hide the application if the user right clicked the system
+ tray icon.
+ o Sanitize network-fetched content that is used to build openvpn
+ command.
+ o Avoids multiple instances of leap-client. Each new one just raises
+ the existing instance and quits.
+ o Use dark eip icons os osx. Closes #2130
+ o Moves BaseConfig to leap.common.config. Closes #2164
+ o Add handling for ASSIGN_IP state from OpenVPN in the mainwindow.
+ o Emit events notifying of the session_id and uid after
+ authentication so other services can make use of it. Closes #1957
+ o Working packaging workflow with rewritten client, using
+ pyinstaller and platypus.
+ o Remove network checks temporarily until we find a good way of
+ doing it, and a good way to deal with them.
+ o Saves the token to allow token authenticated queries.
+ o Turn "leap" into namespace package, move common files to
+ leap_common package that can be shared by other LEAP projects.
+ o Support standalone configurations for distribution in thumbdrives
+ and the like.
+ o Add support for requests < 1.0.0
+ o Tests infrastructure, and tests for crypto/srpauth and crypto/srpregister.
+ o Documentation updated for 0.2.1 release.
+ o Docstrings style changed to fit sphinx autodoc format.
+ o Add a simple UI to notify of pending updates.
+ o Add Windows support.
+ o Try to install TAP driver on Windows if no tap device is preset.
diff --git a/COPYING b/LICENSE
index 94a9ed02..5f7cfbae 100644
--- a/COPYING
+++ b/LICENSE
@@ -672,3 +672,27 @@ may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+
+--------------------------------------------------------------------------------
+
+Leap-client also uses third party icons:
+
+---
+data/images/Arrow-Up-32.png
+data/images/Arrow-Down-32.png
+
+Author: Liam McKay
+License: GNU General Public License - http://en.wikipedia.org/wiki/GNU_General_Public_License
+WebSite: http://wefunction.com/
+IconPackage: WooFunction icon pack - http://www.iconspedia.com/pack/woofunction-icons-4136/
+---
+data/images/Globe.png
+
+Author: Everaldo Coelho
+License: LGPL - http://www.gnu.org/licenses/lgpl.html
+WebSite: http://www.everaldo.com/
+---
+data/images/oxygen-icons/
+
+License: LGPL - http://www.gnu.org/licenses/lgpl.html
+Website: http://www.oxygen-icons.org/
diff --git a/MANIFEST.in b/MANIFEST.in
index 3ce64e45..a4d18e0b 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,8 @@
-# ??? not needed from win
-include setup/linux/polkit/*
+include pkg/*
include docs/*
+recursive-include docs api/* config/* dev/* man/* pkg/* testers/* user/*
+prune docs/_build
+prune docs/covhtml
+include versioneer.py
+include src/leap/util/reqs.txt
+include src/leap/crypto/tests/wrongcert.pem
diff --git a/Makefile b/Makefile
index fc4a172a..8a6e87e1 100644
--- a/Makefile
+++ b/Makefile
@@ -7,40 +7,52 @@
###### EDIT ######################
#Directory with ui and resource files
RESOURCE_DIR = data/resources
-
+UI_DIR = src/leap/gui/ui
+
#Directory for compiled resources
COMPILED_DIR = src/leap/gui
-
+
+#Directory for (finished) translations
+TRANSLAT_DIR = data/translations
+
+#Project file, used for translations
+PROJFILE = data/bitmask.pro
+
#UI files to compile
-# UI_FILES = foo.ui
-UI_FILES =
+UI_FILES = loggerwindow.ui mainwindow.ui wizard.ui login.ui statuspanel.ui
#Qt resource files to compile
-#images.qrc
-RESOURCES = mainwindow.qrc
-
+RESOURCES = locale.qrc loggerwindow.qrc mainwindow.qrc icons.qrc
+
#pyuic4 and pyrcc4 binaries
-PYUIC = pyuic4
-PYRCC = pyrcc4
-
+PYUIC = pyside-uic
+PYRCC = pyside-rcc
+PYLUP = pyside-lupdate
+LRELE = lrelease
+
+
#################################
# DO NOT EDIT FOLLOWING
-
+
COMPILED_UI = $(UI_FILES:%.ui=$(COMPILED_DIR)/ui_%.py)
COMPILED_RESOURCES = $(RESOURCES:%.qrc=$(COMPILED_DIR)/%_rc.py)
DEBVER = $(shell dpkg-parsechangelog | sed -ne 's,Version: ,,p')
#
-
-all : resources ui
-
-resources : $(COMPILED_RESOURCES)
-
+
+all : resources ui
+
+resources : $(COMPILED_RESOURCES)
+
ui : $(COMPILED_UI)
-
-$(COMPILED_DIR)/ui_%.py : $(RESOURCE_DIR)/%.ui
+
+translations:
+ $(PYLUP) $(PROJFILE)
+ $(LRELE) $(TRANSLAT_DIR)/*.ts
+
+$(COMPILED_DIR)/ui_%.py : $(UI_DIR)/%.ui
$(PYUIC) $< -o $@
-
+
$(COMPILED_DIR)/%_rc.py : $(RESOURCE_DIR)/%.qrc
$(PYRCC) $< -o $@
@@ -51,20 +63,10 @@ deb:
@git-buildpackage --git-ignore-new --git-builder="debuild -us -uc -i'.*|bin|share|lib|local|include|\.git'" --git-upstream-branch=upstream --git-upstream-tree=branch --git-debian-branch=debian
manpages:
- rst2man docs/man/leap-client.1.rst docs/man/leap-client.1
+ rst2man docs/man/bitmask.1.rst docs/man/bitmask.1
apidocs:
@sphinx-apidoc -o docs/api src/leap
-cleandebinstall:
- rm debian/files
- rm debian/leap-client.debhelper.log
- rm debian/leap-client.postinst.debhelper
- rm debian/leap-client.postrm.debhelper
- rm debian/leap-client.prerm.debhelper
- rm debian/leap-client.substvars
- rm -rf debian/leap-client/
-
-
-clean :
- $(RM) $(COMPILED_UI) $(COMPILED_RESOURCES) $(COMPILED_UI:.py=.pyc) $(COMPILED_RESOURCES:.py=.pyc)
+clean :
+ $(RM) $(COMPILED_UI) $(COMPILED_RESOURCES) $(COMPILED_UI:.py=.pyc) $(COMPILED_RESOURCES:.py=.pyc)
diff --git a/NEWS.rst b/NEWS.rst
deleted file mode 100644
index b3a3e40a..00000000
--- a/NEWS.rst
+++ /dev/null
@@ -1,76 +0,0 @@
-==================================
-User-facing changes in LEAP Client
-==================================
-
-Release 0.2.0 "The Eternal Friday Release" (2013-2-15)
-------------------------------------------------------
-
-This release is the first functionally working version released for the
-LEAP Client.
-
-It can be installed from pypi, or using the official leap repository at
-``deb.leap.se``. Packages are built for:
-
-* debian: wheezy and sid
-* ubuntu: precise, quantal and raring
-
-On first use, the first connection wizard guides user through steps needed
-to stablish a Encrypted Internet Proxy (EIP) connection with the chosen provider.
-
-Python Support
-''''''''''''''
-This release supports Python2.6 and Python2.7
-
-New Features
-''''''''''''
-- First run wizard: allows to register an user with the selected provider. It also downloads all
- the config files needed to connect to the eip service on this provider.
-- Network checks: we do some basic network testing and warn user in case we cannot find a
- suitable network interface, or if the virtual interface dissapears after a successful eip connection.
-- Debug mode and logfiles: the leap-client script allows to be invoked with the --debug flag.
- It also accepts a --logfile option that is useful for documenting bug reports.
-
-Dependencies
-''''''''''''
-See the ``README.rst`` for a step-to-step install guide.
-
-The following libraries are needed:
-
-- PyQt4
-- openssl
-- openvpn
-
-for building the package dependencies, you will need also:
-
-- python-setuptools
-- python-dev
-
-LEAP-Client depends also on the following python packages:
-
-- requests
-- psutil
-- netifaces
-- pyopenssl
-- jsonschema
-- srp
-- pycrypto
-- keyring
-- python-dateutil
-- sh
-- pyxdg
-
-
-Configuration files
-'''''''''''''''''''
-
-Config files are created under ``~/.config/leap``
-
-Currently user should be able to completely remove this folder and have it auto-generated in the first run.
-
-- Current eip service config is stored in ``eip.json``
-- Under ``.config/leap/providers``, there is a per-provider folder that contains:
- - ``provider.json``, with all options for connecting to this provider.
- - ``eip-service.json``, with eip-specific configuration options,
- - ``keys/ca``, for a copy of the ca certificates used in the tls connections to provider.
- - ``keys/client``, for a local copy of leap user certificates used in the eip connection.
-- ``leap.conf`` for general application configurations (gui windows geometry, ...).
diff --git a/README.rst b/README.rst
index 08c8e38d..d5d6c946 100644
--- a/README.rst
+++ b/README.rst
@@ -1,29 +1,19 @@
-=========================================
The LEAP Encryption Access Project Client
=========================================
*your internet encryption toolkit*
-Read the docs!
-==============
-
-You can read the documentation online at `http://leap-client.readthedocs.org <http://leap-client.readthedocs.org/en/latest/>`_. If you prefer to build it locally, run::
-
- $ cd docs
- $ make html
-
-Quick Start
-==============
-
-At the current development stage we still do not have any versioned release. Instead, you might want to have a look at the `testers guide <http://leap-client.readthedocs.org/en/latest/testers/howto.html>`_ for a quick howto on fetching and testing latest development code.
+.. image:: https://pypip.in/v/bitmask/badge.png
+ :target: https://crate.io/packages/bitmask
Dependencies
------------------
-LEAP Client depends on these libraries:
+Bitmask depends on these libraries:
* ``python 2.6`` or ``2.7``
* ``qt4 libraries``
+* ``libopenssl``
* ``openvpn``
Python packages are listed in ``pkg/requirements.pip`` and ``pkg/test-requirements.pip``
@@ -31,52 +21,41 @@ Python packages are listed in ``pkg/requirements.pip`` and ``pkg/test-requiremen
Debian
^^^^^^
-Under a debian-based system, you can run::
-
- $ apt-get install openvpn python-qt4 python-crypto python-requests python-gnutls
-
-For *testing*::
-
- $ apt-get install python-nose python-mock python-coverage
-
-For *building* the package you will need to install also::
-
- $ apt-get install pyqt4-dev-tools libgnutls-dev python-setuptools python-all-dev
-
-
-pip
-^^^
-
-Use pip to install the required python packages::
-
- $ apt-get install python-pip python-dev libgnutls-dev
- $ pip install -r pkg/requirements.pip
+With a Debian based system, to be able to run Bitmask you need to run the following command::
+ $ sudo apt-get install openvpn python-pyside pyside-tools python-setuptools python-all-dev python-pip python-dev python-openssl
Installing
-----------
-After getting the source and installing all the dependencies, proceed to install ``leap-client`` package::
-
- $ python setup.py install
+After getting the source and installing all the dependencies, proceed to install ``bitmask`` package::
+ $ make
+ $ sudo LEAP_VENV_SKIP_PYSIDE=1 python setup.py install
Running
-------
-After a successful installation, there should be a launcher called ``leap-client`` somewhere in your path::
+After a successful installation, there should be a launcher called ``bitmask`` somewhere in your path::
+
+ $ bitmask
+
+If you are testing a new provider and do not have a CA certificate chain tied to your SSL certificate, you should execute Bitmask in the following way::
- $ leap-client
+ $ bitmask --danger
+But **DO NOT use it on a regular bases**.
+
+**WARNING**: If you use the --danger flag you may be victim to a MITM_ attack without noticing. Use at your own risk.
+
+.. _MITM: http://en.wikipedia.org/wiki/Man-in-the-middle_attack
Hacking
=======
-See the `hackers guide <http://leap-client.readthedocs.org/en/latest/dev/environment.html>`_.
-
-The LEAP client git repository is available at::
+The Bitmask git repository is available at::
- git://leap.se/leap_client
+ git://leap.se/bitmask
Some steps need to be run when setting a development environment for the first time.
@@ -87,18 +66,28 @@ Enable a **virtualenv** to isolate your libraries. (Current *.gitignore* knows a
Make sure you are in the development branch::
- (leap_client)$ git checkout develop
+ (bitmask)$ git checkout develop
-Symlink your global pyqt libraries::
+Symlink your global pyside libraries::
- (leap_client)$ pkg/postmkvenv.sh
+ (bitmask)$ pkg/postmkvenv.sh
And make your working tree available to your pythonpath::
- (leap_client)$ python setup.py develop
+ (bitmask)$ python setup.py develop
+Run Bitmask::
-Testing
+ (bitmask)$ python src/leap/app.py -d
+
+
+If you are testing a new provider that doesn't have the proper certificates yet, you can use --danger flag, but **DO NOT use it on a regular bases**.
+
+**WARNING**: If you use the --danger flag you may be victim to a MITM_ attack without noticing. Use at your own risk.
+
+.. _MITM: http://en.wikipedia.org/wiki/Man-in-the-middle_attack
+
+Testing
=======
Have a look at ``pkg/test-requirements.pip`` for the tests dependencies.
@@ -106,14 +95,14 @@ Have a look at ``pkg/test-requirements.pip`` for the tests dependencies.
To run the test suite::
$ ./run_tests.sh
-
+
which the first time should automagically install all the needed dependencies in your virtualenv for you.
License
=======
-.. image:: https://raw.github.com/leapcode/leap_client/develop/docs/user/gpl.png
+.. image:: https://raw.github.com/leapcode/bitmask/develop/docs/user/gpl.png
-The LEAP Client is released under the terms of the `GNU GPL version 3`_ or later.
+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
diff --git a/data/TODO b/data/TODO
deleted file mode 100644
index 580227ac..00000000
--- a/data/TODO
+++ /dev/null
@@ -1 +0,0 @@
-icons file and stuff should be moved here at some point!
diff --git a/data/bitmask.pro b/data/bitmask.pro
new file mode 100644
index 00000000..ff26ad1a
--- /dev/null
+++ b/data/bitmask.pro
@@ -0,0 +1,56 @@
+# qmake file
+
+# is not there a f*** way of expanding this? other to template with python I mean...
+
+# to get a list of python files we can use:
+# find . -iname "*.py" | grep -Ev "__init__.py|/build/|/docs/|/gui/ui_[a-z]*.py|/gui/[a-z]*_rc.py|./.venv/|/tests/"
+# and remove by hand the few files that we do not want.
+
+SOURCES += ../src/leap/app.py \
+ ../src/leap/config/leapsettings.py \
+ ../src/leap/config/providerconfig.py \
+ ../src/leap/crypto/srpauth.py \
+ ../src/leap/crypto/srpregister.py \
+ ../src/leap/gui/loggerwindow.py \
+ ../src/leap/gui/login.py \
+ ../src/leap/gui/mainwindow.py \
+ ../src/leap/gui/statuspanel.py \
+ ../src/leap/gui/twisted_main.py \
+ ../src/leap/gui/wizardpage.py \
+ ../src/leap/gui/wizard.py \
+ ../src/leap/platform_init/initializers.py \
+ ../src/leap/platform_init/locks.py \
+ ../src/leap/provider/supportedapis.py \
+ ../src/leap/services/abstractbootstrapper.py \
+ ../src/leap/services/eip/eipbootstrapper.py \
+ ../src/leap/services/eip/eipconfig.py \
+ ../src/leap/services/eip/providerbootstrapper.py \
+ ../src/leap/services/eip/udstelnet.py \
+ ../src/leap/services/eip/vpnlaunchers.py \
+ ../src/leap/services/eip/vpnprocess.py \
+ ../src/leap/services/mail/smtpbootstrapper.py \
+ ../src/leap/services/mail/smtpconfig.py \
+ ../src/leap/services/soledad/soledadbootstrapper.py \
+ ../src/leap/services/soledad/soledadconfig.py \
+ ../src/leap/services/tx.py \
+ ../src/leap/util/constants.py \
+ ../src/leap/util/keyring_helpers.py \
+ ../src/leap/util/leap_argparse.py \
+ ../src/leap/util/leap_log_handler.py \
+ ../src/leap/util/privilege_policies.py \
+ ../src/leap/util/pyside_tests_helper.py \
+ ../src/leap/util/request_helpers.py \
+ ../src/leap/util/requirement_checker.py
+
+FORMS += ../src/leap/gui/ui/loggerwindow.ui \
+ ../src/leap/gui/ui/login.ui \
+ ../src/leap/gui/ui/mainwindow.ui \
+ ../src/leap/gui/ui/statuspanel.ui \
+ ../src/leap/gui/ui/wizard.ui \
+
+# where to generate ts files -- tx will pick from here
+
+# original file, english
+
+TRANSLATIONS += ts/en_US.ts
+
diff --git a/data/images/Arrow-Down-32.png b/data/images/Arrow-Down-32.png
new file mode 100644
index 00000000..c5c607a1
--- /dev/null
+++ b/data/images/Arrow-Down-32.png
Binary files differ
diff --git a/data/images/Arrow-Up-32.png b/data/images/Arrow-Up-32.png
new file mode 100644
index 00000000..85370ac5
--- /dev/null
+++ b/data/images/Arrow-Up-32.png
Binary files differ
diff --git a/data/images/Blue-Arrow-Right-32.png b/data/images/Blue-Arrow-Right-32.png
new file mode 100644
index 00000000..66e50b0d
--- /dev/null
+++ b/data/images/Blue-Arrow-Right-32.png
Binary files differ
diff --git a/data/images/Globe.png b/data/images/Globe.png
new file mode 100644
index 00000000..7549433b
--- /dev/null
+++ b/data/images/Globe.png
Binary files differ
diff --git a/data/images/conn_connected-light.png b/data/images/conn_connected-light.png
new file mode 100644
index 00000000..a0db2a22
--- /dev/null
+++ b/data/images/conn_connected-light.png
Binary files differ
diff --git a/data/images/conn_connecting-light.png b/data/images/conn_connecting-light.png
new file mode 100644
index 00000000..83e3ea75
--- /dev/null
+++ b/data/images/conn_connecting-light.png
Binary files differ
diff --git a/data/images/conn_error-light.png b/data/images/conn_error-light.png
new file mode 100644
index 00000000..8cee20fe
--- /dev/null
+++ b/data/images/conn_error-light.png
Binary files differ
diff --git a/data/images/leap-client.tiff b/data/images/leap-client.tiff
new file mode 100644
index 00000000..71c95b0d
--- /dev/null
+++ b/data/images/leap-client.tiff
Binary files differ
diff --git a/data/images/leap-color-big.png b/data/images/leap-color-big.png
new file mode 100644
index 00000000..eafacdcd
--- /dev/null
+++ b/data/images/leap-color-big.png
Binary files differ
diff --git a/data/images/leap-gray-big.png b/data/images/leap-gray-big.png
new file mode 100644
index 00000000..1529a74f
--- /dev/null
+++ b/data/images/leap-gray-big.png
Binary files differ
diff --git a/data/images/leapfrog.jpg b/data/images/leapfrog.jpg
deleted file mode 100644
index a1ddf4bb..00000000
--- a/data/images/leapfrog.jpg
+++ /dev/null
Binary files differ
diff --git a/data/images/light/16/accept.png b/data/images/light/16/accept.png
new file mode 100644
index 00000000..917a68c4
--- /dev/null
+++ b/data/images/light/16/accept.png
Binary files differ
diff --git a/data/images/light/16/down-arrow.png b/data/images/light/16/down-arrow.png
new file mode 100644
index 00000000..9beb5de3
--- /dev/null
+++ b/data/images/light/16/down-arrow.png
Binary files differ
diff --git a/data/images/light/16/error.png b/data/images/light/16/error.png
new file mode 100644
index 00000000..edc20985
--- /dev/null
+++ b/data/images/light/16/error.png
Binary files differ
diff --git a/data/images/light/16/refresh.png b/data/images/light/16/refresh.png
new file mode 100644
index 00000000..a948a071
--- /dev/null
+++ b/data/images/light/16/refresh.png
Binary files differ
diff --git a/data/images/light/16/up-arrow.png b/data/images/light/16/up-arrow.png
new file mode 100644
index 00000000..4e0ec404
--- /dev/null
+++ b/data/images/light/16/up-arrow.png
Binary files differ
diff --git a/data/images/light/64/network-eip-acquiring.png b/data/images/light/64/network-eip-acquiring.png
new file mode 100644
index 00000000..83e3ea75
--- /dev/null
+++ b/data/images/light/64/network-eip-acquiring.png
Binary files differ
diff --git a/data/images/light/64/network-eip-down.png b/data/images/light/64/network-eip-down.png
new file mode 100644
index 00000000..85669af6
--- /dev/null
+++ b/data/images/light/64/network-eip-down.png
Binary files differ
diff --git a/data/images/light/64/network-eip-up.png b/data/images/light/64/network-eip-up.png
new file mode 100644
index 00000000..a0db2a22
--- /dev/null
+++ b/data/images/light/64/network-eip-up.png
Binary files differ
diff --git a/data/images/mask-icon.png b/data/images/mask-icon.png
new file mode 100644
index 00000000..3504bae9
--- /dev/null
+++ b/data/images/mask-icon.png
Binary files differ
diff --git a/data/images/mask-launcher.png b/data/images/mask-launcher.png
new file mode 100644
index 00000000..cb27f8d3
--- /dev/null
+++ b/data/images/mask-launcher.png
Binary files differ
diff --git a/data/images/oxygen-icons/dialog-error.png b/data/images/oxygen-icons/dialog-error.png
new file mode 100644
index 00000000..01bc922d
--- /dev/null
+++ b/data/images/oxygen-icons/dialog-error.png
Binary files differ
diff --git a/data/images/oxygen-icons/dialog-information.png b/data/images/oxygen-icons/dialog-information.png
new file mode 100644
index 00000000..ee59e170
--- /dev/null
+++ b/data/images/oxygen-icons/dialog-information.png
Binary files differ
diff --git a/data/images/oxygen-icons/dialog-warning.png b/data/images/oxygen-icons/dialog-warning.png
new file mode 100644
index 00000000..8e913378
--- /dev/null
+++ b/data/images/oxygen-icons/dialog-warning.png
Binary files differ
diff --git a/data/images/oxygen-icons/document-save-as.png b/data/images/oxygen-icons/document-save-as.png
new file mode 100644
index 00000000..9695a564
--- /dev/null
+++ b/data/images/oxygen-icons/document-save-as.png
Binary files differ
diff --git a/data/images/oxygen-icons/edit-bomb.png b/data/images/oxygen-icons/edit-bomb.png
new file mode 100644
index 00000000..2b36224c
--- /dev/null
+++ b/data/images/oxygen-icons/edit-bomb.png
Binary files differ
diff --git a/data/images/oxygen-icons/script-error.png b/data/images/oxygen-icons/script-error.png
new file mode 100644
index 00000000..c7ace707
--- /dev/null
+++ b/data/images/oxygen-icons/script-error.png
Binary files differ
diff --git a/data/leap_client.pro b/data/leap_client.pro
deleted file mode 100644
index 57764a23..00000000
--- a/data/leap_client.pro
+++ /dev/null
@@ -1,25 +0,0 @@
-# qmake file
-
-# is not there a f*** way of expanding this? other to template with python I mean...
-
-SOURCES += ../src/leap/base/exceptions.py
-SOURCES += ../src/leap/eip/exceptions.py
-SOURCES += ../src/leap/baseapp/eip.py
-SOURCES += ../src/leap/baseapp/log.py
-SOURCES += ../src/leap/baseapp/systray.py
-SOURCES += ../src/leap/gui/firstrun/intro.py
-SOURCES += ../src/leap/gui/firstrun/last.py
-SOURCES += ../src/leap/gui/firstrun/login.py
-SOURCES += ../src/leap/gui/firstrun/providerinfo.py
-SOURCES += ../src/leap/gui/firstrun/providerselect.py
-SOURCES += ../src/leap/gui/firstrun/providersetup.py
-SOURCES += ../src/leap/gui/firstrun/register.py
-SOURCES += ../src/leap/gui/firstrun/connect.py
-SOURCES += ../src/leap/gui/firstrun/wizard.py
-
-# where to generate ts files -- tx will pick from here
-
-# original file, english
-
-TRANSLATIONS += ts/en_US.ts
-
diff --git a/data/mkpyqt.py b/data/mkpyqt.py
deleted file mode 100755
index 1ce2cd28..00000000
--- a/data/mkpyqt.py
+++ /dev/null
@@ -1,271 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2007-10 Qtrac Ltd. All rights reserved.
-# This program or module 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 2 of the License, or
-# version 3 of the License, or (at your option) any later version. It is
-# provided for educational purposes and 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.
-
-import os
-import platform
-import stat
-import subprocess
-import sys
-import PyQt4.QtCore
-
-__version__ = "1.0.4"
-
-Windows = sys.platform.lower().startswith(("win", "microsoft"))
-if Windows:
- PATH = os.path.join(os.path.dirname(sys.executable),
- "Lib/site-packages/PyQt4")
- if os.access(os.path.join(PATH, "bin"), os.R_OK):
- PATH = os.path.join(PATH, "bin")
-else:
- app = PyQt4.QtCore.QCoreApplication([])
- PATH = unicode(app.applicationDirPath())
- del app
-if sys.platform.startswith("darwin"):
- i = PATH.find("Resources")
- if i > -1:
- PATH = PATH[:i] + "bin"
-PYUIC4 = os.path.join(PATH, "pyuic4") # e.g. PYUIC4 = "/usr/bin/pyuic4"
-if sys.platform.startswith("darwin"):
- PYUIC4 = os.path.dirname(sys.executable)
- i = PYUIC4.find("Resources")
- if i > -1:
- PYUIC4 = PYUIC4[:i] + "Lib/python2.5/site-packages/PyQt4/uic/pyuic.py"
-PYRCC4 = os.path.join(PATH, "pyrcc4")
-PYLUPDATE4 = os.path.join(PATH, "pylupdate4")
-LRELEASE = "lrelease"
-if Windows:
- PYUIC4 = PYUIC4.replace("/", "\\") + ".bat"
- PYRCC4 = PYRCC4.replace("/", "\\") + ".exe"
- PYLUPDATE4 = PYLUPDATE4.replace("/", "\\") + ".exe"
-
-msg = []
-shell = lambda command: subprocess.Popen(['which', command],
- stdout=subprocess.PIPE).communicate()
-
-if not os.access(PYUIC4, os.F_OK):
- PYUIC4 = shell('pyuic4')[0].strip('\n')
- if not os.access(PYUIC4, os.F_OK):
- msg.append("failed to find pyuic4; tried %s" % PYUIC4)
-
-if not os.access(PYRCC4, os.F_OK):
- PYRCC4 = shell('pyrcc4')[0].strip('\n')
- if not os.access(PYRCC4, os.F_OK):
- msg.append("failed to find pyrcc4; tried %s" % PYRCC4)
-
-if not os.access(PYLUPDATE4, os.F_OK):
- PYLUPDATE4 = shell('pylupdate4')[0].strip('\n')
- if not os.access(PYLUPDATE4, os.F_OK):
- msg.append("failed to find pylupdate4; tried %s" % PYLUPDATE4)
-
-if msg:
- print "\n".join(msg)
- print "try manually editing this program to put the correct " + \
- "paths in place"
- sys.exit()
-
-Debug = False
-Verbose = False
-
-def usage():
- print """usage: mkpyqt.py [options] [path]
-
-Options (which can be given in any of the forms shown):
--b --build build [default]
--c --clean clean
--f --force force
--t --translate translate
--r --recurse recurse
--v --verbose verbose
--D --debug debug
-path defaults to .
-
-If executed with no arguments (or with a build argument) it does a
-build, i.e., it looks for all *.ui and *.qrc files and makes sure that
-the corresponding ui_*.py and qrc_*.py files exist and are up-to-date.
-
-If executed with clean, deletes all ui_*.py and qrc_*.py files that have
-corresponding *.ui and *.qrc files, and all *.pyc and *.pyo files.
-
-If executed with force, it does a clean followed by a build.
-
-If building and the translate option is given, after building, it runs
-pylupdate4 on all .py and .pyw files it encounters, and then runs lrelease
-on all .ts files it encounters. It does not use a .pro file so the .ts
-files must be created in the first place, e.g., using pylupdate4 on one
-of the source files and using its -ts option.
-
-WARNING: Do not give any hand-coded files names that match ui_*.py or
-qrc_*.py since these will be deleted by mkpyqt.py clean!
-
-NOTE: If any tool fails to run, e.g., pyuic4, then edit this program and
-hard-code the path; the variables with the tool paths are near the top
-of the file.
-
-mkpyqt.py v %s. Copyright (c) 2007-9 Qtrac Ltd. All rights reserved.
-""" % __version__
- sys.exit()
-
-
-def report_failure(command, args, process):
- msg = ""
- ba = process.readAllStandardError()
- if not ba.isEmpty():
- msg = ": " + str(QString(ba))
- print "failed", command, " ".join(args), msg
-
-
-def build(path):
- for name in os.listdir(path):
- source = os.path.join(path, name)
- target = None
- if source.endswith(".ui"):
- target = os.path.join(path,
- "ui_" + name.replace(".ui", ".py"))
- command = PYUIC4
- elif source.endswith(".qrc"):
- target = os.path.join(path,
- "qrc_" + name.replace(".qrc", ".py"))
- command = PYRCC4
- process = PyQt4.QtCore.QProcess()
- if target is not None:
- if not os.access(target, os.F_OK) or (
- os.stat(source)[stat.ST_MTIME] > \
- os.stat(target)[stat.ST_MTIME]):
- args = ["-o", target, source]
- if sys.platform.startswith("darwin") and command == PYUIC4:
- command = sys.executable
- args = [PYUIC4] + args
- if Debug:
- print "# %s %s" % (command, " ".join(args))
- else:
- process.start(command, args)
- if not process.waitForFinished(2 * 60 * 1000):
- report_failure(command, args, process)
- else:
- print source, "->", target
- elif Verbose:
- print source, "is up-to-date"
-
-
-def clean(path):
- deletelist = []
- for name in os.listdir(path):
- target = os.path.join(path, name)
- source = None
- if target.endswith(".py") or target.endswith(".pyc") or \
- target.endswith(".pyo"):
- if name.startswith("ui_") and not name[-1] in "oc":
- source = os.path.join(path, name[3:-3] + ".ui")
- elif name.startswith("qrc_"):
- if target[-1] in "oc":
- source = os.path.join(path, name[4:-4] + ".qrc")
- else:
- source = os.path.join(path, name[4:-3] + ".qrc")
- elif target[-1] in "oc":
- source = target[:-1]
- if source is not None:
- if os.access(source, os.F_OK):
- if Debug:
- print "# delete ", target
- else:
- deletelist.append(target)
- else:
- print "will not remove '%s' since `%s' not found" % (
- target, source)
- if not Debug:
- for target in deletelist:
- if Verbose:
- print "deleted", target
- os.remove(target)
-
-
-def translate(path):
- files = []
- tsfiles = []
- for name in os.listdir(path):
- if name.endswith((".py", ".pyw")):
- files.append(os.path.join(path, name))
- elif name.endswith(".ts"):
- tsfiles.append(os.path.join(path, name))
- if not tsfiles:
- return
- verbose = "-verbose" if Verbose else ""
- silent = "-silent" if not Verbose else ""
- process = PyQt4.QtCore.QProcess()
- for ts in tsfiles:
- qm = ts[:-3] + ".qm"
- command1 = PYLUPDATE4
- args1 = [verbose] + files + ["-ts", ts]
- command2 = LRELEASE
- args2 = [silent, ts, "-qm", qm]
- if Debug:
- print "updated", ts
- print "generated", qm
- else:
- process.start(command1, args1)
- if not process.waitForFinished(2 * 60 * 1000):
- report_failure(command1, args1, process)
- process.start(command2, args2)
- if not process.waitForFinished(2 * 60 * 1000):
- report_failure(command2, args2, process)
-
-
-def apply(recurse, function, path):
- if not recurse:
- function(path)
- else:
- for root, dirs, files in os.walk(path):
- for dir in dirs:
- function(os.path.join(root, dir))
-
-
-def main():
- global Debug, Verbose
- function = build
- recurse = False
- trans = False
- force = False
- path = "."
- args = sys.argv[1:]
- while args:
- arg = args.pop(0)
- if arg in ("-D", "--debug", "debug"):
- Debug = True
- elif arg in ("-b", "--build", "build"):
- pass # This is the default
- elif arg in ("-c", "--clean", "clean"):
- function = clean
- elif arg in ("-f", "--force", "force"):
- force = True
- elif arg in ("-t", "--translate", "translate"):
- trans = True
- elif arg in ("-r", "--recurse", "recurse"):
- recurse = True
- elif arg in ("-v", "--verbose", "verbose"):
- Verbose = True
- elif arg in ("-h", "--help", "help"):
- usage()
- else:
- path = arg
- if not force:
- apply(recurse, function, path)
- else:
- apply(recurse, clean, path)
- apply(recurse, build, path)
- if trans and (function == build or force):
- apply(recurse, translate, path)
-
-main()
-
-# 1.0.1 Fixed bug reported by Brian Downing where paths that contained
-# spaces were not handled correctly.
-# 1.0.2 Changed default path on Windows to match PyQt 4.4
-# 1.0.3 Tried to make the paths work on Mac OS X
diff --git a/data/resources/icons.qrc b/data/resources/icons.qrc
new file mode 100644
index 00000000..6a6de9e6
--- /dev/null
+++ b/data/resources/icons.qrc
@@ -0,0 +1,9 @@
+<RCC>
+ <qresource prefix="/">
+ <file>../images/light/16/down-arrow.png</file>
+ <file>../images/light/16/up-arrow.png</file>
+ <file>../images/light/64/network-eip-acquiring.png</file>
+ <file>../images/light/64/network-eip-down.png</file>
+ <file>../images/light/64/network-eip-up.png</file>
+ </qresource>
+</RCC>
diff --git a/data/resources/loggerwindow.qrc b/data/resources/loggerwindow.qrc
new file mode 100644
index 00000000..847ca9a1
--- /dev/null
+++ b/data/resources/loggerwindow.qrc
@@ -0,0 +1,11 @@
+<RCC>
+ <qresource prefix="/">
+ <file>../images/oxygen-icons/edit-bomb.png</file>
+ <file>../images/oxygen-icons/document-save-as.png</file>
+ <file>../images/oxygen-icons/dialog-information.png</file>
+ <file>../images/oxygen-icons/script-error.png</file>
+ <file>../images/oxygen-icons/dialog-error.png</file>
+ <file>../images/oxygen-icons/dialog-information.png</file>
+ <file>../images/oxygen-icons/dialog-warning.png</file>
+ </qresource>
+</RCC>
diff --git a/data/resources/mainwindow.qrc b/data/resources/mainwindow.qrc
index 293988e1..d1268186 100644
--- a/data/resources/mainwindow.qrc
+++ b/data/resources/mainwindow.qrc
@@ -1,12 +1,23 @@
-<!DOCTYPE RCC><RCC version="1.0">
-<qresource prefix="/">
- <file>../images/conn_error.png</file>
- <file>../images/conn_connecting.png</file>
- <file>../images/conn_connected.png</file>
- <file>../images/leap-color-small.png</file>
- <file>../images/watermark.png</file>
- <file>../images/Dialog-accept.png</file>
- <file>../images/Dialog-error.png</file>
- <file>../images/Emblem-question.png</file>
-</qresource>
+<RCC>
+ <qresource prefix="/">
+ <file>../images/mask-launcher.png</file>
+ <file>../images/mask-icon.png</file>
+ <file>../images/watermark.png</file>
+ <file>../images/leap-gray-big.png</file>
+ <file>../images/Blue-Arrow-Right-32.png</file>
+ <file>../images/Globe.png</file>
+ <file>../images/leap-color-big.png</file>
+ <file>../images/Arrow-Down-32.png</file>
+ <file>../images/Arrow-Up-32.png</file>
+ <file>../images/conn_connecting.png</file>
+ <file>../images/conn_connected.png</file>
+ <file>../images/conn_error.png</file>
+ <file>../images/conn_connecting-light.png</file>
+ <file>../images/conn_connected-light.png</file>
+ <file>../images/conn_error-light.png</file>
+ <file>../images/leap-color-small.png</file>
+ <file>../images/Dialog-accept.png</file>
+ <file>../images/Dialog-error.png</file>
+ <file>../images/Emblem-question.png</file>
+ </qresource>
</RCC>
diff --git a/data/translations/de.qm b/data/translations/de.qm
deleted file mode 100644
index b2f04f22..00000000
--- a/data/translations/de.qm
+++ /dev/null
Binary files differ
diff --git a/data/translations/de.ts b/data/translations/de.ts
deleted file mode 100644
index f2ab6fa4..00000000
--- a/data/translations/de.ts
+++ /dev/null
@@ -1,218 +0,0 @@
-<?xml version="1.0" ?><!DOCTYPE TS><TS language="de" version="2.0">
-<context>
- <name>IntroPage</name>
- <message>
- <location filename="../src/leap/gui/firstrun/intro.py" line="14"/>
- <source>First run wizard.</source>
- <translation>Assistent für erstmaligen Start</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/intro.py" line="24"/>
- <source>Now we will guide you through some configuration that is needed before you can connect for the first time.&lt;br&gt;&lt;br&gt;If you ever need to modify these options again, you can find the wizard in the &apos;&lt;i&gt;Settings&lt;/i&gt;&apos; menu from the main window.&lt;br&gt;&lt;br&gt;Do you want to &lt;b&gt;sign up&lt;/b&gt; for a new account, or &lt;b&gt;log in&lt;/b&gt; with an already existing username?&lt;br&gt;</source>
- <translation>Wir werden dich nun durch einige Konfigurationen führen, die du für den ersten Start benötigst.&lt;br&gt;&lt;br&gt;Wenn du diese Konfigurationen jemals ändern musst, findest du den Assistenten im &apos;&lt;i&gt;Einstellungen&lt;/i&gt;&apos;-Menü des Haupfensters.&lt;br&gt;&lt;br&gt;Möchtest du dich für einen neuen Account &lt;b&gt;anmelden&lt;/b&gt; oder mit einem bestehenden Usernamen &lt;b&gt;einloggen&lt;/b&gt;?</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/intro.py" line="37"/>
- <source>Sign up for a new account.</source>
- <translation>Für einen neuen Account anmelden.</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/intro.py" line="40"/>
- <source>Log In with my credentials.</source>
- <translation>Mit bestehenden Daten einloggen.</translation>
- </message>
-</context>
-<context>
- <name>LogInPage</name>
- <message>
- <location filename="../src/leap/gui/firstrun/login.py" line="96"/>
- <source>Username must be in the username@provider form.</source>
- <translation>Der Username muss in der Form username@provider sein.</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/login.py" line="140"/>
- <source>Resolving domain name</source>
- <translation>Auflösen des Domain-Namens</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/login.py" line="163"/>
- <source>Authentication error: %s</source>
- <translation>Authentifizierungsfehler: %s</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/login.py" line="179"/>
- <source>Credentials validated.</source>
- <translation>Anmeldedaten korrekt.</translation>
- </message>
-</context>
-<context>
- <name>ProviderInfoPage</name>
- <message>
- <location filename="../src/leap/gui/firstrun/providerinfo.py" line="18"/>
- <source>Provider Info</source>
- <translation>Provider-info</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/providerinfo.py" line="19"/>
- <source>This is what provider says.</source>
- <translation>Das ist, was der Provider sagt.</translation>
- </message>
-</context>
-<context>
- <name>ProviderSetupValidationPage</name>
- <message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="26"/>
- <source>Provider setup</source>
- <translation>Provider-Setup</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="27"/>
- <source>Doing autoconfig.</source>
- <translation>Führe autoconfig durch.</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="83"/>
- <source>Fetching CA certificate</source>
- <translation>Hole CA-Zertifikat</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="105"/>
- <source>Checking CA fingerprint</source>
- <translation>Überprüfe CA-Fingerprint</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="134"/>
- <source>Validating api certificate</source>
- <translation>Überprüfe API-Zertifikat</translation>
- </message>
-</context>
-<context>
- <name>RegisterUserPage</name>
- <message>
- <location filename="../src/leap/gui/firstrun/register.py" line="31"/>
- <source>Sign Up</source>
- <translation>Anmelden</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/register.py" line="208"/>
- <source>Registration succeeded!</source>
- <translation>Registrierung erfolgreich!</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/register.py" line="238"/>
- <source>Password does not match..</source>
- <translation>Passwort stimmt nicht überien.</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/register.py" line="242"/>
- <source>Password too short.</source>
- <translation>Passwort zu kurz</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/register.py" line="247"/>
- <source>Password too obvious.</source>
- <translation>Passwort zu simpel.</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/register.py" line="279"/>
- <source>Error connecting to provider (timeout)</source>
- <translation>Verbindungsfehler zu Provider (timeout)</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/register.py" line="284"/>
- <source>Error Connecting to provider (connerr).</source>
- <translation>Verbindungsfehler zu Provider (connerr)</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/register.py" line="292"/>
- <source>Error during registration (%s)</source>
- <translation>Fehler während der Registrierung (%s)</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/register.py" line="302"/>
- <source>Could not register (bad response)</source>
- <translation>Konnte nicht registrieren (bad response)</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/register.py" line="309"/>
- <source>Username not available.</source>
- <translation>Username nicht verfügbar.</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/register.py" line="375"/>
- <source>Register a new user with provider %s.</source>
- <translation>Registriere einen neuen User bei Provider %s</translation>
- </message>
-</context>
-<context>
- <name>RegisterUserValidationPage</name>
- <message>
- <location filename="../src/leap/gui/firstrun/regvalidation.py" line="95"/>
- <source>Fetching provider config...</source>
- <translation>Hole Provider-Konfiguration...</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/regvalidation.py" line="112"/>
- <source>Authentication error: %s</source>
- <translation>Authentifizierungsfehler: %s</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/regvalidation.py" line="117"/>
- <source>Fetching eip certificate</source>
- <translation>Hole EIP-Zertifikat</translation>
- </message>
-</context>
-<context>
- <name>SelectProviderPage</name>
- <message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="32"/>
- <source>Enter Provider</source>
- <translation>Gib Provider ein</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="33"/>
- <source>Please enter the domain of the provider you want to use for your connection.</source>
- <translation>Bitte gib die Domain des Providers an, den du für deine Verbindung nutzen möchtest.</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="87"/>
- <source>chec&amp;k!</source>
- <translation type="unfinished"/>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="97"/>
- <source>Server certificate could not be verified.</source>
- <translation>Server-Zertifikat konnte nicht bestätigt werden.</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="136"/>
- <source>Certificate validation</source>
- <translation>Zertifikatsüberprüfung</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="222"/>
- <source>checking domain name</source>
- <translation>Prüfe Domain-Name</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="276"/>
- <source>checking https connection</source>
- <translation>Prüfe HTTPS-Verbindung</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="292"/>
- <source>Could not get info from provider.</source>
- <translation>Konnte keine Information vom Provider bekommen.</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="295"/>
- <source>Could not download provider info (refused conn.).</source>
- <translation>Konnte Provider-Info nicht herunterladen (refused conn.).</translation>
- </message>
- <message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="305"/>
- <source>fetching provider info</source>
- <translation>Hole Provider-Info</translation>
- </message>
-</context>
-</TS> \ No newline at end of file
diff --git a/data/translations/es.qm b/data/translations/es.qm
index 0edb22dc..d75644ba 100644
--- a/data/translations/es.qm
+++ b/data/translations/es.qm
Binary files differ
diff --git a/data/translations/es.ts b/data/translations/es.ts
index 994269dc..f6102bfa 100644
--- a/data/translations/es.ts
+++ b/data/translations/es.ts
@@ -1,476 +1,496 @@
<?xml version="1.0" ?><!DOCTYPE TS><TS language="es" version="2.0">
<context>
- <name>ConnectionPage</name>
+ <name>MainWindow</name>
<message>
- <location filename="../src/leap/gui/firstrun/connect.py" line="26"/>
- <source>Connecting...</source>
- <translation>Conectando...</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="592"/>
+ <source>Encryption is OFF</source>
+ <translation>Cifrado APAGADO</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/connect.py" line="27"/>
- <source>Setting up a encrypted connection with the provider</source>
- <translation>Estableciendo una conexion cifrada con el proveedor</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="155"/>
+ <source>Stop</source>
+ <translation>Detener</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/connect.py" line="89"/>
- <source>Getting EIP configuration files</source>
- <translation>Obteniendo los archivos de configuracion para EIP</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="257"/>
+ <source>Hide</source>
+ <translation>Ocultar</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/connect.py" line="105"/>
- <source>Authentication error: %s</source>
- <translation>Error de autenticacion: %s</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="259"/>
+ <source>Show</source>
+ <translation>Mostrar</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/connect.py" line="113"/>
- <source>Getting EIP certificate</source>
- <translation>Obteniendo certificado EIP</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="288"/>
+ <source>About LEAP</source>
+ <translation>Acerca de LEAP</translation>
</message>
-</context>
-<context>
- <name>EIPConductorAppMixin</name>
<message>
- <location filename="../src/leap/baseapp/eip.py" line="225"/>
- <source>&amp;Disconnect</source>
- <translation>&amp;Desconectar</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="288"/>
+ <source>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;a href=&quot;https://leap.se&quot;&gt;More about LEAP&lt;/a&gt;</source>
+ <translation>LEAP es una empresa sin fines de lucro dedicada a proveer comunicaciones seguras a todos sus usuarios. Nuestro objetivo es la adatación de tecnologías de cifrado para que sean fáciles de utilizar, y estén ampliamente disponibles. &lt;a href=&quot;https://leap.se&quot;&gt;Más acerca de LEAP&lt;/a&gt;</translation>
</message>
<message>
- <location filename="../src/leap/baseapp/eip.py" line="239"/>
- <source>&amp;Connect</source>
- <translation>&amp;Conectar</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="433"/>
+ <source>Could not load provider configuration</source>
+ <translation>No fue posible cargar la configuración del proveedor</translation>
</message>
-</context>
-<context>
- <name>EIPErrors</name>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="66"/>
- <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>
- <translation>No hemos podido encontrar ningun agente de autenticacion en el sistema.&lt;br/&gt;Asegurate de que &lt;b&gt;polkit-gnome-authentication-agent-1&lt;/b&gt; esta ejecutandose e intentalo de nuevo.</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="459"/>
+ <source>Please select a valid provider</source>
+ <translation>Por favor, selectione un proveedor válido</translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="77"/>
- <source>We could not find &lt;b&gt;pkexec&lt;/b&gt; in your system.&lt;br/&gt; Do you want to try &lt;b&gt;setuid workaround&lt;/b&gt;? (&lt;i&gt;DOES NOTHING YET&lt;/i&gt;)</source>
- <translation>No hemos encontrado &lt;b&gt;pkexec&lt;/b&gt; en el sistema. &lt;br/&gt; Quieres probar a intentar establecer setuid? (No hace nada todavia).</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="463"/>
+ <source>Please provide a valid username</source>
+ <translation>Por favor, provea un nombre de usuario válido</translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="88"/>
- <source>No suitable openvpn command found. &lt;br/&gt;(Might be a permissions problem)</source>
- <translation>No se ha podido encontrar el comando openvpn. &lt;br/&gt; (podria ser un problema de permisos)</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="467"/>
+ <source>Please provide a valid Password</source>
+ <translation>Por favor, provea una contraseña válida</translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="97"/>
- <source>there is a problem with provider certificate</source>
- <translation>hay un problema con el certificado del proveedor</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="470"/>
+ <source>Logging in...</source>
+ <translation>Ingresando...</translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="104"/>
- <source>an error occurred during configuratio of leap services</source>
- <translation>ocurrio un error con la configuracion de los servicios LEAP</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="549"/>
+ <source>Stop EIP</source>
+ <translation>Detener EIP</translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="119"/>
- <source>Server does not allow secure connections</source>
- <translation>El servidor no permite conexiones seguras</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="558"/>
+ <source>EIP has stopped</source>
+ <translation>EIP se ha detenido</translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="126"/>
- <source>Server certificate could not be verified</source>
- <translation>El certificado del servidor no se ha podido verificar</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="560"/>
+ <source>Start EIP</source>
+ <translation>Comenzar EIP</translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="137"/>
- <source>We could not find your eip certs in the expected path</source>
- <translation>No se han podido encontrar los certificados eip en el path esperado</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="573"/>
+ <source>Checking configuration, please wait...</source>
+ <translation>Corroborando la configuración, por favor espere...</translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="111"/>
- <source>Another OpenVPN Process has been detected. Please close it before starting leap-client</source>
- <translation>Se ha detectado otro proceso OpenVPN. Acaba con el antes de iniciar leap-client</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="581"/>
+ <source>%s does not support EIP</source>
+ <translation>%s no soporta EIP</translation>
</message>
-</context>
-<context>
- <name>Errors</name>
<message>
- <location filename="../src/leap/base/exceptions.py" line="57"/>
- <source>Interface not found</source>
- <translation>No se encontro la interfaz</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="596"/>
+ <source>Encryption is ON</source>
+ <translation>Cifrado ENCENDIDO</translation>
</message>
<message>
- <location filename="../src/leap/base/exceptions.py" line="64"/>
- <source>Looks like your computer is not connected to the internet</source>
- <translation>Parece que no estas conectado a internet</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="614"/>
+ <source>VPN: Authenticating...</source>
+ <translation>VPN: Autorizando...</translation>
</message>
<message>
- <location filename="../src/leap/base/exceptions.py" line="72"/>
- <source>Looks like there are problems with your internet connection</source>
- <translation>Oh oh... parece que hay problemas con la conexion a internet</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="616"/>
+ <source>VPN: Retrieving configuration...</source>
+ <translation>VPN: Obteniendo configuración...</translation>
</message>
<message>
- <location filename="../src/leap/base/exceptions.py" line="80"/>
- <source>It looks like there is no internet connection.</source>
- <translation>Parece que no hay conexion a internet</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="618"/>
+ <source>VPN: Connected!</source>
+ <translation>VPN: Conectado!</translation>
</message>
<message>
- <location filename="../src/leap/base/exceptions.py" line="88"/>
- <source>Domain cannot be found</source>
- <translation>Dominio no encontrado</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="672"/>
+ <source>Signing out...</source>
+ <translation>Saliendo...</translation>
</message>
<message>
- <location filename="../src/leap/base/exceptions.py" line="95"/>
- <source>The Encrypted Connection was lost.</source>
- <translation>La Conexion Cifrada se perdio</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="14"/>
+ <source>LEAP</source>
+ <translation>LEAP</translation>
</message>
-</context>
-<context>
- <name>IntroPage</name>
<message>
- <location filename="../src/leap/gui/firstrun/intro.py" line="14"/>
- <source>First run wizard</source>
- <translation>Asistente para la primera conexion</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="80"/>
+ <source>Remember</source>
+ <translation>Recordar</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/intro.py" line="37"/>
- <source>Sign up for a new account</source>
- <translation>Registrar una nueva cuenta</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="103"/>
+ <source>Login</source>
+ <translation>Acceder</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/intro.py" line="40"/>
- <source>Log In with my credentials</source>
- <translation>Loguearme con mis credenciales</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="123"/>
+ <source>&lt;b&gt;Provider:&lt;/b&gt;</source>
+ <translation>&lt;b&gt;Proveedor:&lt;/b&gt;</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/intro.py" line="24"/>
- <source>Now we will guide you through some configuration that is needed before you can connect for the first time.&lt;br&gt;&lt;br&gt;If you ever need to modify these options again, you can find the wizard in the &apos;&lt;i&gt;Settings&lt;/i&gt;&apos; menu from the main window.&lt;br&gt;&lt;br&gt;Do you want to &lt;b&gt;sign up&lt;/b&gt; for a new account, or &lt;b&gt;log in&lt;/b&gt; with an already existing username?&lt;br&gt;</source>
- <translation>Vamos a configurar algunas cosas antes de que te puedas conectar por primera vez.&lt;br&gt;&lt;br&gt;Si necesitas modificar estas opciones de nuevo, puedes encontrar este asistente en el menu de &apos;&lt;i&gt;Opciones&lt;/i&gt;&apos; en la ventana principal.&lt;br&gt;&lt;br&gt;Quieres &lt;b&gt;registrar&lt;/b&gt; una nueva cuenta, o &lt;b&gt;loguearte&lt;/b&gt; con tu usuario?&lt;br&gt; </translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="133"/>
+ <source>&lt;b&gt;Password:&lt;/b&gt;</source>
+ <translation>&lt;b&gt;Contraseña:&lt;/b&gt;</translation>
</message>
-</context>
-<context>
- <name>LastPage</name>
<message>
- <location filename="../src/leap/gui/firstrun/last.py" line="18"/>
- <source>Connecting to Encrypted Internet Proxy service...</source>
- <translation>Conectando al servicio de Proxy Cifrado de Internet (EIP)</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="153"/>
+ <source>&lt;b&gt;User:&lt;/b&gt;</source>
+ <translation>&lt;b&gt;Usuario:&lt;/b&gt;</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/last.py" line="66"/>
- <source>Click &apos;&lt;i&gt;%s&lt;/i&gt;&apos; to end the wizard and save your settings.</source>
- <translation>Haz click en &apos;&lt;i&gt;%s&lt;/i&gt;&apos; para terminar el asistente y guardar tu configuracion.</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="219"/>
+ <source>0.0 Kb</source>
+ <translation>0.0 Kb</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/last.py" line="62"/>
- <source>You are now using an encrypted connection!</source>
- <translation>Estas usando una conexion cifrada!</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="272"/>
+ <source>Disconnected</source>
+ <translation>Desconectado</translation>
</message>
-</context>
-<context>
- <name>LogInPage</name>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="104"/>
- <source>Username must be in the username@provider form.</source>
- <translation>El usuario tiene que ser usuario@tu.proveedor</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="332"/>
+ <source>&amp;Session</source>
+ <translation>&amp;Sesión</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="150"/>
- <source>Resolving domain name</source>
- <translation>Resolviendo nombre de dominio</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="340"/>
+ <source>Help</source>
+ <translation>Ayuda</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="171"/>
- <source>Authentication error: %s</source>
- <translation>Error de autenticacion: %s</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="348"/>
+ <source>S&amp;ettings</source>
+ <translation>C&amp;onfiguración</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="188"/>
- <source>Credentials validated.</source>
- <translation>Credenciales validadas.</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="359"/>
+ <source>&amp;Sign out</source>
+ <translation>&amp;Desconectarse</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="28"/>
- <source>Log In</source>
- <translation>Log in</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="364"/>
+ <source>&amp;Quit</source>
+ <translation>&amp;Salir</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="369"/>
+ <source>About &amp;LEAP</source>
+ <translation>Acerca de &amp;LEAP</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="46"/>
- <source>User &amp;name:</source>
- <translation>&amp;Nombre:</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="374"/>
+ <source>&amp;Help</source>
+ <translation>&amp;Ayuda</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="61"/>
- <source>&amp;Password:</source>
- <translation>&amp;Contrasena:</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="379"/>
+ <source>&amp;Wizard</source>
+ <translation>&amp;Asistente</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="88"/>
- <source>Log in</source>
- <translation>Log in</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="620"/>
+ <source>VPN: Waiting to start...</source>
+ <translation>VPN: Esperando para comenzar...</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="29"/>
- <source>Log in with your credentials</source>
- <translation>Loguearte con tus credenciales</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="176"/>
+ <source>Automatically login</source>
+ <translation>Ingresar automáticamente</translation>
</message>
</context>
<context>
- <name>LogPaneMixin</name>
+ <name>Wizard</name>
<message>
- <location filename="../src/leap/baseapp/log.py" line="25"/>
- <source>&amp;Connect</source>
- <translation>&amp;Conectar</translation>
+ <location filename="../src/leap/gui/wizard.py" line="152"/>
+ <source>Passwords don&apos;t match</source>
+ <translation>Las contraseñas no son iguales</translation>
</message>
<message>
- <location filename="../src/leap/baseapp/log.py" line="38"/>
- <source>Disconnected</source>
- <translation>&amp;Desconectado</translation>
+ <location filename="../src/leap/gui/wizard.py" line="155"/>
+ <source>Password too short</source>
+ <translation>Contraseña muy corta</translation>
</message>
-</context>
-<context>
- <name>ProviderInfoPage</name>
<message>
- <location filename="../src/leap/gui/firstrun/providerinfo.py" line="19"/>
- <source>Provider Information</source>
- <translation>Informacion del Proveedor</translation>
+ <location filename="../src/leap/gui/wizard.py" line="158"/>
+ <source>Password too easy</source>
+ <translation>Contraseña demsiado simple</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerinfo.py" line="20"/>
- <source>Services offered by this provider</source>
- <translation>Servicios ofrecidos por este proveedor</translation>
+ <location filename="../src/leap/gui/wizard.py" line="161"/>
+ <source>Password equal to username</source>
+ <translation>Contraseña es igual al usuario</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerinfo.py" line="99"/>
- <source>enrollment policy</source>
- <translation>politica de registro</translation>
+ <location filename="../src/leap/gui/wizard.py" line="189"/>
+ <source>Starting registration...</source>
+ <translation>Comenzando el registro...</translation>
</message>
-</context>
-<context>
- <name>ProviderSetupValidationPage</name>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="28"/>
- <source>Provider setup</source>
- <translation>Configuracion del Proveedor</translation>
+ <location filename="../src/leap/gui/wizard.py" line="195"/>
+ <source>&lt;font color=&apos;green&apos;&gt;&lt;b&gt;User registration OK&lt;/b&gt;&lt;/font&gt;</source>
+ <translation>&lt;font color=&apos;green&apos;&gt;&lt;b&gt;Registro realizado con éxito!&lt;/b&gt;&lt;/font&gt;</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="89"/>
- <source>Fetching CA certificate</source>
- <translation>Obteniendo certificado de la CA</translation>
+ <location filename="../src/leap/gui/wizard.py" line="202"/>
+ <source>Unknown error</source>
+ <translation>Error desconocido</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="111"/>
- <source>Checking CA fingerprint</source>
- <translation>Comprobando el fingerprint de la CA</translation>
+ <location filename="../src/leap/gui/wizard.py" line="303"/>
+ <source>Unable to load provider configuration</source>
+ <translation>No fue posible carga la configuración del proveedor</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="129"/>
- <source>Validating api certificate</source>
- <translation>Validando certificado de la api</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="14"/>
+ <source>LEAP First run</source>
+ <translation>Primera ejecución de LEAP</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="29"/>
- <source>Gathering configuration options for this provider</source>
- <translation>Obteniendo opciones para este proveedor</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="31"/>
+ <source>Welcome</source>
+ <translation>Bienvenido</translation>
</message>
-</context>
-<context>
- <name>RegisterUserPage</name>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="31"/>
- <source>Sign Up</source>
- <translation>Nueva Cuenta</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="34"/>
+ <source>This is the LEAP Client first run wizard</source>
+ <translation>Este es el asistente de primera ejecución de LEAP</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="212"/>
- <source>Registration succeeded!</source>
- <translation>Cuenta creada con exito!</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="43"/>
+ <source>Log In with my credentials</source>
+ <translation>Acceder con mis credenciales</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="248"/>
- <source>Password does not match..</source>
- <translation>Las contrasenas no son iguales..</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="50"/>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Now we will guide you through some configuration that is needed before you can connect for the first time.&lt;/p&gt;&lt;p&gt;If you ever need to modify these options again, you can find the wizard in the &lt;span style=&quot; font-style:italic;&quot;&gt;&apos;Settings&apos;&lt;/span&gt; menu from the main window.&lt;/p&gt;&lt;p&gt;Do you want to &lt;span style=&quot; font-weight:600;&quot;&gt;sign up&lt;/span&gt; for a new account, or &lt;span style=&quot; font-weight:600;&quot;&gt;log in&lt;/span&gt; with an already existing username?&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Ahora le guiaremos para configurar lo necesario para que ud. pueda ingresar por primera vez.&lt;/p&gt;&lt;p&gt;Si necesita alguna vez modificar estas opciones, podrá ejecutar este asistente desde el menú &lt;span style=&quot; font-style:italic;&quot;&gt;&apos;Configuración&apos;&lt;/span&gt; desde la ventana principal.&lt;/p&gt;&lt;p&gt;Desea &lt;span style=&quot; font-weight:600;&quot;&gt;crear&lt;/span&gt; una nueva cuenta, o &lt;span style=&quot; font-weight:600;&quot;&gt;acceder&lt;/span&gt; con un usuario existente?&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="252"/>
- <source>Password too short.</source>
- <translation>Contrasena demasiado corta.</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="63"/>
+ <source>Sign up for a new account</source>
+ <translation>Crear una nueva cuenta</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="257"/>
- <source>Password too obvious.</source>
- <translation>Contrasena demasiado obvia.</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="100"/>
+ <source>Provider selection</source>
+ <translation>Selección de proveedor</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="286"/>
- <source>Error connecting to provider (timeout)</source>
- <translation>Error conectandose al proveedor (timeout)</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="103"/>
+ <source>Please enter the domain of the provider you want to user for your connection</source>
+ <translation>Por favor, ingrese el dominio del proveedor que desea utilizar</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="291"/>
- <source>Error Connecting to provider (connerr).</source>
- <translation>Error conectandose al proveedor (connerr).</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="128"/>
+ <source>https://</source>
+ <translation>https://</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="299"/>
- <source>Error during registration (%s)</source>
- <translation>Error durante el registro (%s)</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="368"/>
+ <source>Checking provider</source>
+ <translation>Corroborando proveedor</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="309"/>
- <source>Could not register (bad response)</source>
- <translation>No se pudo registrar (bad response)</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="147"/>
+ <source>Download provider information</source>
+ <translation>Descargando información del proveedor</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="316"/>
- <source>Username not available.</source>
- <translation>Usuario no disponible.</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="154"/>
+ <source>HTTPS Connection</source>
+ <translation>Conexión HTTPS</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="52"/>
- <source>User &amp;name:</source>
- <translation>&amp;Nombre:</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="209"/>
+ <source>Name resolution</source>
+ <translation>Resolución de nombre</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="64"/>
- <source>&amp;Password:</source>
- <translation>&amp;Contrasena:</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="232"/>
+ <source>Check</source>
+ <translation>Corroborar</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="70"/>
- <source>Password (again):</source>
- <translation>Contrasena (de nuevo):</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="253"/>
+ <source>Provider Information</source>
+ <translation>Información de proveedor</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="76"/>
- <source>&amp;Remember username and password.</source>
- <translation>&amp;Recordar usuario y contrasena</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="256"/>
+ <source>Services offered by this provider</source>
+ <translation>Servicios ofrecidos por este proveedor</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="382"/>
- <source>Register a new user with provider &lt;em&gt;%s&lt;/em&gt;</source>
- <translation>Registrar un nuevo usuario con el proveedor &lt;em&gt;%s&lt;/em&gt;</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="278"/>
+ <source>&lt;b&gt;Enrollment policy:&lt;/b&gt;</source>
+ <translation>&lt;b&gt;Política de ingreso:&lt;/b&gt;</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="285"/>
+ <source>URL</source>
+ <translation>URL</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="292"/>
+ <source>Name</source>
+ <translation>Nombre</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="325"/>
+ <source>policy</source>
+ <translation>política</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="332"/>
+ <source>Desc</source>
+ <translation>Desc</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="356"/>
+ <source>Provider setup</source>
+ <translation>Configuración de proveedor</translation>
</message>
-</context>
-<context>
- <name>SelectProviderPage</name>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="32"/>
- <source>Enter Provider</source>
- <translation>Entra tu Proveedor</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="359"/>
+ <source>Gathering configuration options for this provider</source>
+ <translation>Obteniendo opciones de configuración para este proveedor</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="406"/>
+ <source>Download CA Certificate</source>
+ <translation>Descargando certificado de autoridad</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="91"/>
- <source>chec&amp;k!</source>
- <translation>compro&amp;bar!</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="413"/>
+ <source>Check CA Certificate Fingerprint</source>
+ <translation>Corroborando el fingerprint del certificado</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="101"/>
- <source>Server certificate could not be verified.</source>
- <translation>No se pudo verificar el certificado del servidor.</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="420"/>
+ <source>Check API Certificate</source>
+ <translation>Corroborando certificado de API</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="140"/>
- <source>Certificate validation</source>
- <translation>Validacion del certificado</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="486"/>
+ <source>Register new user</source>
+ <translation>Registrar un nuevo usuario</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="294"/>
- <source>Could not get info from provider.</source>
- <translation>no se pudo obtener info del proveedor</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="489"/>
+ <source>Register a new user with provider</source>
+ <translation>Registrar un nuevo usuario con el proveedor</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="297"/>
- <source>Could not download provider info (refused conn.).</source>
- <translation>no se pudo obtener info del proveedor (refused conn.).</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="524"/>
+ <source>&lt;b&gt;User:&lt;/b&gt;</source>
+ <translation>&lt;b&gt;Usuario:&lt;/b&gt;</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="107"/>
- <source>&amp;Trust this provider certificate.</source>
- <translation>Con&amp;fio en este certificado del proveedor</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="534"/>
+ <source>&lt;b&gt;Password:&lt;/b&gt;</source>
+ <translation>&lt;b&gt;Contraseña:&lt;/b&gt;</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="348"/>
- <source>Do you want to &lt;b&gt;trust this provider certificate?&lt;/b&gt;</source>
- <translation>Confias en este certificado del proveedor?</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="553"/>
+ <source>&lt;b&gt;Re-enter password:&lt;/b&gt;</source>
+ <translation>&lt;b&gt;Re-introduzca contraseña:&lt;/b&gt;</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="351"/>
- <source>SHA-256 fingerprint: &lt;i&gt;%s&lt;/i&gt;&lt;br&gt;</source>
- <translation>Fingerprint SHA-256: &lt;i&gt;%s&lt;/i&gt;&lt;br&gt;</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="563"/>
+ <source>Register</source>
+ <translation>Registrar</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="33"/>
- <source>Please enter the domain of the provider you want to use for your connection</source>
- <translation>Introduce el dominio del proveedor que quieres usar para tu conexion</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="584"/>
+ <source>EIP Setup</source>
+ <translation>Configuración de EIP</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="226"/>
- <source>Checking if it is a valid provider</source>
- <translation>Comprobando si es un proveedor valido</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="587"/>
+ <source>Setting up Encrypted Internet</source>
+ <translation>Configurando Internet Cifrado</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="280"/>
- <source>Checking for a secure connection</source>
- <translation>Comprobando si la conexion es segura</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="609"/>
+ <source>Checking EIP</source>
+ <translation>Corroborando EIP</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="307"/>
- <source>Getting info from the provider</source>
- <translation>Obteniendo info del proveedor</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="615"/>
+ <source>Download EIP configuration</source>
+ <translation>Descargando configuración de EIP</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="622"/>
+ <source>Download client certificate</source>
+ <translation>Descargando certificado de cliente</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="691"/>
+ <source>Congratulations!</source>
+ <translation>Felicitaciones!</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="694"/>
+ <source>You have successfully configured the LEAP client.</source>
+ <translation>Ha configurado correctamente el cliente de LEAP</translation>
</message>
</context>
<context>
- <name>StatusAwareTrayIconMixin</name>
+ <name>__impl</name>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="78"/>
- <source>EIP Connection Status</source>
- <translation>Estado de la Conexion EIP</translation>
+ <location filename="../src/leap/crypto/srpauth.py" line="161"/>
+ <source>Unknown user</source>
+ <translation>Usuario desconocido</translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="87"/>
- <source>&lt;b&gt;disconnected&lt;/b&gt;</source>
- <translation>&lt;b&gt;desconectado&lt;/b&gt;</translation>
+ <location filename="../src/leap/crypto/srpauth.py" line="167"/>
+ <source>The server did not send the salt parameter</source>
+ <translation>El servidor no ha envíado el parámetro salt</translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="126"/>
- <source>Encryption ON turn &amp;off</source>
- <translation>Cifrado ON | turn &amp;off</translation>
+ <location filename="../src/leap/crypto/srpauth.py" line="171"/>
+ <source>The server did not send the B parameter</source>
+ <translation>El servidor no ha mandado el parámetro B</translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="131"/>
- <source>&amp;Details...</source>
- <translation>&amp;Detalles...</translation>
+ <location filename="../src/leap/crypto/srpauth.py" line="199"/>
+ <source>The data sent from the server had errors</source>
+ <translation>Los datos enviados por el servidor contanían errores</translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="135"/>
- <source>&amp;About</source>
- <translation>&amp;Acerca de</translation>
+ <location filename="../src/leap/crypto/srpauth.py" line="220"/>
+ <source>Could not connect to the server</source>
+ <translation>No se pudo conectar al servidor</translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="138"/>
- <source>About Q&amp;t</source>
- <translation>Acerca de Q&amp;t</translation>
+ <location filename="../src/leap/crypto/srpauth.py" line="228"/>
+ <source>Wrong password</source>
+ <translation>Password incorrecto</translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="141"/>
- <source>&amp;Quit</source>
- <translation>&amp;Salir</translation>
+ <location filename="../src/leap/crypto/srpauth.py" line="234"/>
+ <source>Unknown error (%s)</source>
+ <translation>Error desconocido (%s)</translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="152"/>
- <source>Encryption ON turn o&amp;ff</source>
- <translation>Cifrado ON | turn o&amp;ff</translation>
+ <location filename="../src/leap/crypto/srpauth.py" line="243"/>
+ <source>Problem getting data from server</source>
+ <translation>Problemas obteniendo datos desde el servidor</translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="157"/>
- <source>Encryption OFF turn &amp;on</source>
- <translation>Cifrado OFF | turn &amp;on</translation>
+ <location filename="../src/leap/crypto/srpauth.py" line="264"/>
+ <source>Bad data from server</source>
+ <translation>Datos incorrectos desde el servidor</translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="162"/>
- <source>connecting...</source>
- <translation>conectando...</translation>
+ <location filename="../src/leap/crypto/srpauth.py" line="270"/>
+ <source>Auth verification failed</source>
+ <translation>Verificación de autenticación falló</translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="185"/>
- <source>About</source>
- <translation>Acerca de</translation>
+ <location filename="../src/leap/crypto/srpauth.py" line="400"/>
+ <source>Succeeded</source>
+ <translation>Exitoso!</translation>
</message>
</context>
</TS> \ No newline at end of file
diff --git a/data/ts/en_US.ts b/data/ts/en_US.ts
index 40cd156a..250b58ce 100644
--- a/data/ts/en_US.ts
+++ b/data/ts/en_US.ts
@@ -1,476 +1,834 @@
<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE TS><TS version="2.0">
+<!DOCTYPE TS><TS version="1.1">
<context>
- <name>ConnectionPage</name>
+ <name>EIPBootstrapper</name>
<message>
- <location filename="../src/leap/gui/firstrun/connect.py" line="26"/>
- <source>Connecting...</source>
+ <location filename="../src/leap/services/eip/eipbootstrapper.py" line="151"/>
+ <source>The downloaded certificate is not a valid PEM file</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LinuxVPNLauncher</name>
+ <message>
+ <location filename="../src/leap/services/eip/vpnlaunchers.py" line="380"/>
+ <source>No gateway was found!</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LoggerWindow</name>
+ <message>
+ <location filename="../src/leap/gui/ui/loggerwindow.ui" line="14"/>
+ <source>Logs</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/loggerwindow.ui" line="29"/>
+ <source>Debug</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/loggerwindow.ui" line="49"/>
+ <source>Info</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/connect.py" line="27"/>
- <source>Setting up a encrypted connection with the provider</source>
+ <location filename="../src/leap/gui/ui/loggerwindow.ui" line="69"/>
+ <source>Warning</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/connect.py" line="89"/>
- <source>Getting EIP configuration files</source>
+ <location filename="../src/leap/gui/ui/loggerwindow.ui" line="89"/>
+ <source>Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/connect.py" line="105"/>
- <source>Authentication error: %s</source>
+ <location filename="../src/leap/gui/ui/loggerwindow.ui" line="109"/>
+ <source>Critical</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/connect.py" line="113"/>
- <source>Getting EIP certificate</source>
+ <location filename="../src/leap/gui/ui/loggerwindow.ui" line="129"/>
+ <source>Save to file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/loggerwindow.py" line="113"/>
+ <source>Save As</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>EIPConductorAppMixin</name>
+ <name>LoginWidget</name>
+ <message>
+ <location filename="../src/leap/gui/ui/login.ui" line="14"/>
+ <source>Form</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/login.ui" line="49"/>
+ <source>Create a new account</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/login.ui" line="56"/>
+ <source>&lt;b&gt;Provider:&lt;/b&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <location filename="../src/leap/baseapp/eip.py" line="225"/>
- <source>&amp;Disconnect</source>
+ <location filename="../src/leap/gui/ui/login.ui" line="76"/>
+ <source>Remember username and password</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/eip.py" line="239"/>
- <source>&amp;Connect</source>
+ <location filename="../src/leap/gui/ui/login.ui" line="83"/>
+ <source>&lt;b&gt;Username:&lt;/b&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/login.ui" line="93"/>
+ <source>&lt;b&gt;Password:&lt;/b&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/login.ui" line="103"/>
+ <source>Log In</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>EIPErrors</name>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="14"/>
+ <source>LEAP</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="62"/>
+ <source>There are new updates available, please restart.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="78"/>
+ <source>More...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="233"/>
+ <source>Show Log</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="261"/>
+ <source>&amp;Session</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="521"/>
+ <source>Help</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="281"/>
+ <source>&amp;Sign out</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="286"/>
+ <source>&amp;Quit</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="291"/>
+ <source>About &amp;LEAP</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="66"/>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="296"/>
+ <source>&amp;Help</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="301"/>
+ <source>&amp;Wizard</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="306"/>
+ <source>Show &amp;logs</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="230"/>
+ <source>No default provider</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="233"/>
+ <source>Encrypted internet is OFF</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="1051"/>
+ <source>Turn ON</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="553"/>
+ <source>Hide Main Window</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="391"/>
+ <source>The LEAPClient app is ready to update, please restart the application.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="400"/>
+ <source> The following components will be updated:
+%s</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="403"/>
+ <source>Updates available</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="519"/>
+ <source>Preferences</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="552"/>
+ <source>Show Main Window</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="606"/>
+ <source>About LEAP - %s</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="614"/>
+ <source>version: &lt;b&gt;%s&lt;/b&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;a href=&quot;https://leap.se&quot;&gt;More about LEAP&lt;/a&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="716"/>
+ <source>Could not load provider configuration.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="746"/>
+ <source>Please select a valid provider</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="751"/>
+ <source>Please provide a valid username</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="756"/>
+ <source>Please provide a valid Password</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="759"/>
+ <source>Logging in...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="995"/>
+ <source>Turn OFF</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="1009"/>
<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>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="77"/>
- <source>We could not find &lt;b&gt;pkexec&lt;/b&gt; in your system.&lt;br/&gt; Do you want to try &lt;b&gt;setuid workaround&lt;/b&gt;? (&lt;i&gt;DOES NOTHING YET&lt;/i&gt;)</source>
+ <location filename="../src/leap/gui/mainwindow.py" line="1015"/>
+ <source>We could not find &lt;b&gt;pkexec&lt;/b&gt; in your system.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="1020"/>
+ <source>We could not find openvpn binary.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="88"/>
- <source>No suitable openvpn command found. &lt;br/&gt;(Might be a permissions problem)</source>
+ <location filename="../src/leap/gui/mainwindow.py" line="1048"/>
+ <source>OFF</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="97"/>
- <source>there is a problem with provider certificate</source>
+ <location filename="../src/leap/gui/mainwindow.py" line="1100"/>
+ <source>Starting...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="104"/>
- <source>an error occurred during configuratio of leap services</source>
+ <location filename="../src/leap/gui/mainwindow.py" line="1108"/>
+ <source>Not supported</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="119"/>
- <source>Server does not allow secure connections</source>
+ <location filename="../src/leap/gui/mainwindow.py" line="1111"/>
+ <source>Disabled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="126"/>
- <source>Server certificate could not be verified</source>
+ <location filename="../src/leap/gui/mainwindow.py" line="1139"/>
+ <source>Could not load Encrypted Internet Configuration.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="137"/>
- <source>We could not find your eip certs in the expected path</source>
+ <location filename="../src/leap/gui/mainwindow.py" line="1239"/>
+ <source>Encrypted Internet could not be launched because you did not authenticate properly.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/eip/exceptions.py" line="111"/>
- <source>Another OpenVPN Process has been detected. Please close it before starting leap-client</source>
+ <location filename="../src/leap/gui/mainwindow.py" line="1245"/>
+ <source>Encrypted Internet finished in an unexpected manner!</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Errors</name>
+ <name>ProviderBootstrapper</name>
<message>
- <location filename="../src/leap/base/exceptions.py" line="57"/>
- <source>Interface not found</source>
+ <location filename="../src/leap/services/eip/providerbootstrapper.py" line="113"/>
+ <source>Provider certificate could not be verified</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/base/exceptions.py" line="64"/>
- <source>Looks like your computer is not connected to the internet</source>
+ <location filename="../src/leap/services/eip/providerbootstrapper.py" line="116"/>
+ <source>Provider does not support HTTPS</source>
<translation type="unfinished"></translation>
</message>
+</context>
+<context>
+ <name>SRPAuth</name>
<message>
- <location filename="../src/leap/base/exceptions.py" line="72"/>
- <source>Looks like there are problems with your internet connection</source>
+ <location filename="../src/leap/crypto/srpauth.py" line="598"/>
+ <source>Succeeded</source>
<translation type="unfinished"></translation>
</message>
+</context>
+<context>
+ <name>StatusPanel</name>
<message>
- <location filename="../src/leap/base/exceptions.py" line="80"/>
- <source>It looks like there is no internet connection.</source>
+ <location filename="../src/leap/gui/ui/statuspanel.ui" line="14"/>
+ <source>Form</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/base/exceptions.py" line="88"/>
- <source>Domain cannot be found</source>
+ <location filename="../src/leap/gui/ui/statuspanel.ui" line="23"/>
+ <source>user@domain.org</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/base/exceptions.py" line="95"/>
- <source>The Encrypted Connection was lost.</source>
+ <location filename="../src/leap/gui/ui/statuspanel.ui" line="44"/>
+ <source>Encrypted Internet: </source>
<translation type="unfinished"></translation>
</message>
-</context>
-<context>
- <name>IntroPage</name>
<message>
- <location filename="../src/leap/gui/firstrun/intro.py" line="14"/>
- <source>First run wizard</source>
+ <location filename="../src/leap/gui/ui/statuspanel.ui" line="54"/>
+ <source>Off</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/intro.py" line="37"/>
- <source>Sign up for a new account</source>
+ <location filename="../src/leap/gui/ui/statuspanel.ui" line="83"/>
+ <source>Turn On</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/intro.py" line="40"/>
- <source>Log In with my credentials</source>
+ <location filename="../src/leap/gui/ui/statuspanel.ui" line="178"/>
+ <source>0.0 Kb</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/intro.py" line="24"/>
- <source>Now we will guide you through some configuration that is needed before you can connect for the first time.&lt;br&gt;&lt;br&gt;If you ever need to modify these options again, you can find the wizard in the &apos;&lt;i&gt;Settings&lt;/i&gt;&apos; menu from the main window.&lt;br&gt;&lt;br&gt;Do you want to &lt;b&gt;sign up&lt;/b&gt; for a new account, or &lt;b&gt;log in&lt;/b&gt; with an already existing username?&lt;br&gt;</source>
+ <location filename="../src/leap/gui/ui/statuspanel.ui" line="209"/>
+ <source>...</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>LastPage</name>
+ <name>StatusPanelWidget</name>
+ <message>
+ <location filename="../src/leap/gui/statuspanel.py" line="180"/>
+ <source>Turn OFF</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/statuspanel.py" line="190"/>
+ <source>Turn ON</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/statuspanel.py" line="232"/>
+ <source>ON</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/statuspanel.py" line="236"/>
+ <source>Authenticating...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/statuspanel.py" line="238"/>
+ <source>Retrieving configuration...</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <location filename="../src/leap/gui/firstrun/last.py" line="18"/>
- <source>Connecting to Encrypted Internet Proxy service...</source>
+ <location filename="../src/leap/gui/statuspanel.py" line="240"/>
+ <source>Waiting to start...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/last.py" line="66"/>
- <source>Click &apos;&lt;i&gt;%s&lt;/i&gt;&apos; to end the wizard and save your settings.</source>
+ <location filename="../src/leap/gui/statuspanel.py" line="242"/>
+ <source>Assigning IP</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/last.py" line="62"/>
- <source>You are now using an encrypted connection!</source>
+ <location filename="../src/leap/gui/statuspanel.py" line="250"/>
+ <source>Unable to start VPN, it&apos;s already running.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/statuspanel.py" line="264"/>
+ <source>Encryption is OFF</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/statuspanel.py" line="269"/>
+ <source>Turning ON</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/statuspanel.py" line="271"/>
+ <source>Encryption is ON</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>LogInPage</name>
+ <name>Wizard</name>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="104"/>
- <source>Username must be in the username@provider form.</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="14"/>
+ <source>LEAP First run</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="150"/>
- <source>Resolving domain name</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="31"/>
+ <source>Welcome</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="171"/>
- <source>Authentication error: %s</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="34"/>
+ <source>This is the LEAP Client first run wizard</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="188"/>
- <source>Credentials validated.</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="43"/>
+ <source>Log In with my credentials</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="28"/>
- <source>Log In</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="50"/>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Now we will guide you through some configuration that is needed before you can connect for the first time.&lt;/p&gt;&lt;p&gt;If you ever need to modify these options again, you can find the wizard in the &lt;span style=&quot; font-style:italic;&quot;&gt;&apos;Settings&apos;&lt;/span&gt; menu from the main window.&lt;/p&gt;&lt;p&gt;Do you want to &lt;span style=&quot; font-weight:600;&quot;&gt;sign up&lt;/span&gt; for a new account, or &lt;span style=&quot; font-weight:600;&quot;&gt;log in&lt;/span&gt; with an already existing username?&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="46"/>
- <source>User &amp;name:</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="63"/>
+ <source>Sign up for a new account</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="61"/>
- <source>&amp;Password:</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="100"/>
+ <source>Provider selection</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="88"/>
- <source>Log in</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="103"/>
+ <source>Please enter the domain of the provider you want to use for your connection</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="29"/>
- <source>Log in with your credentials</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="131"/>
+ <source>Check</source>
<translation type="unfinished"></translation>
</message>
-</context>
-<context>
- <name>LogPaneMixin</name>
<message>
- <location filename="../src/leap/baseapp/log.py" line="25"/>
- <source>&amp;Connect</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="151"/>
+ <source>https://</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/log.py" line="38"/>
- <source>Disconnected</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="161"/>
+ <source>Checking for a valid provider</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="167"/>
+ <source>Getting provider information</source>
<translation type="unfinished"></translation>
</message>
-</context>
-<context>
- <name>ProviderInfoPage</name>
<message>
- <location filename="../src/leap/gui/firstrun/providerinfo.py" line="19"/>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="174"/>
+ <source>Can we stablish a secure connection?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="247"/>
+ <source>Can we reach this provider?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="278"/>
<source>Provider Information</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerinfo.py" line="20"/>
- <source>Services offered by this provider</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="281"/>
+ <source>Description of services offered by this provider</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerinfo.py" line="99"/>
- <source>enrollment policy</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="290"/>
+ <source>Name</source>
<translation type="unfinished"></translation>
</message>
-</context>
-<context>
- <name>ProviderSetupValidationPage</name>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="28"/>
- <source>Provider setup</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="322"/>
+ <source>Desc</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="332"/>
+ <source>&lt;b&gt;Services offered:&lt;/b&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="89"/>
- <source>Fetching CA certificate</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="342"/>
+ <source>services</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="111"/>
- <source>Checking CA fingerprint</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="362"/>
+ <source>&lt;b&gt;Enrollment policy:&lt;/b&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="129"/>
- <source>Validating api certificate</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="372"/>
+ <source>policy</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="29"/>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="392"/>
+ <source>&lt;b&gt;URL:&lt;/b&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="402"/>
+ <source>URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="409"/>
+ <source>&lt;b&gt;Description:&lt;/b&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="420"/>
+ <source>Provider setup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="423"/>
<source>Gathering configuration options for this provider</source>
<translation type="unfinished"></translation>
</message>
-</context>
-<context>
- <name>RegisterUserPage</name>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="31"/>
- <source>Sign Up</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="445"/>
+ <source>We are downloading some bits that we need to establish a secure connection with the provider for the first time.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="212"/>
- <source>Registration succeeded!</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="468"/>
+ <source>Setting up provider</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="248"/>
- <source>Password does not match..</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="518"/>
+ <source>Getting info from the Certificate Authority</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="252"/>
- <source>Password too short.</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="525"/>
+ <source>Do we trust this Certificate Authority?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="257"/>
- <source>Password too obvious.</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="532"/>
+ <source>Establishing a trust relationship with this provider</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="286"/>
- <source>Error connecting to provider (timeout)</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="591"/>
+ <source>Register new user</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="291"/>
- <source>Error Connecting to provider (connerr).</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="594"/>
+ <source>Register a new user with provider</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="299"/>
- <source>Error during registration (%s)</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="609"/>
+ <source>&lt;b&gt;Password:&lt;/b&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="309"/>
- <source>Could not register (bad response)</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="628"/>
+ <source>&lt;b&gt;Re-enter password:&lt;/b&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="316"/>
- <source>Username not available.</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="638"/>
+ <source>Register</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="52"/>
- <source>User &amp;name:</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="671"/>
+ <source>&lt;b&gt;User:&lt;/b&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="64"/>
- <source>&amp;Password:</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="684"/>
+ <source>Remember my username and password</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="70"/>
- <source>Password (again):</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="708"/>
+ <source>Service selection</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="76"/>
- <source>&amp;Remember username and password.</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="711"/>
+ <source>Please select the services you would like to have</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="382"/>
- <source>Register a new user with provider &lt;em&gt;%s&lt;/em&gt;</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="733"/>
+ <source>Congratulations!</source>
<translation type="unfinished"></translation>
</message>
-</context>
-<context>
- <name>SelectProviderPage</name>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="32"/>
- <source>Enter Provider</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="736"/>
+ <source>You have successfully configured the LEAP Client.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="91"/>
- <source>chec&amp;k!</source>
+ <location filename="../src/leap/gui/wizard.py" line="86"/>
+ <source>Encrypted Internet</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="101"/>
- <source>Server certificate could not be verified.</source>
+ <location filename="../src/leap/gui/wizard.py" line="88"/>
+ <source>Encrypted Mail</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="140"/>
- <source>Certificate validation</source>
+ <location filename="../src/leap/gui/wizard.py" line="92"/>
+ <source>(will need admin password to start)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="294"/>
- <source>Could not get info from provider.</source>
+ <location filename="../src/leap/gui/wizard.py" line="155"/>
+ <source>&amp;Next &gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="297"/>
- <source>Could not download provider info (refused conn.).</source>
+ <location filename="../src/leap/gui/wizard.py" line="157"/>
+ <source>Connect</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="107"/>
- <source>&amp;Trust this provider certificate.</source>
+ <location filename="../src/leap/gui/wizard.py" line="214"/>
+ <source>Passwords don&apos;t match</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="348"/>
- <source>Do you want to &lt;b&gt;trust this provider certificate?&lt;/b&gt;</source>
+ <location filename="../src/leap/gui/wizard.py" line="217"/>
+ <source>Password too short</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="351"/>
- <source>SHA-256 fingerprint: &lt;i&gt;%s&lt;/i&gt;&lt;br&gt;</source>
+ <location filename="../src/leap/gui/wizard.py" line="220"/>
+ <source>Password too easy</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="33"/>
- <source>Please enter the domain of the provider you want to use for your connection</source>
+ <location filename="../src/leap/gui/wizard.py" line="223"/>
+ <source>Password equal to username</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/wizard.py" line="252"/>
+ <source>Starting registration...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/wizard.py" line="281"/>
+ <source>User %s successfully registered.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/wizard.py" line="301"/>
+ <source>Unknown error</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/wizard.py" line="407"/>
+ <source>&lt;font color=&apos;red&apos;&gt;&lt;b&gt;Non-existent provider&lt;/b&gt;&lt;/font&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/wizard.py" line="425"/>
+ <source>&lt;font color=&apos;red&apos;&gt;&lt;b&gt;%s&lt;/b&gt;&lt;/font&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/wizard.py" line="453"/>
+ <source>Unable to load provider configuration</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/wizard.py" line="459"/>
+ <source>&lt;font color=&apos;red&apos;&gt;&lt;b&gt;Not a valid provider&lt;/b&gt;&lt;/font&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/wizard.py" line="525"/>
+ <source>Services by %s</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="226"/>
- <source>Checking if it is a valid provider</source>
+ <location filename="../src/leap/gui/wizard.py" line="544"/>
+ <source>Something went wrong while trying to load service %s</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="280"/>
- <source>Checking for a secure connection</source>
+ <location filename="../src/leap/gui/wizard.py" line="561"/>
+ <source>Gathering configuration options for %s</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="307"/>
- <source>Getting info from the provider</source>
+ <location filename="../src/leap/gui/wizard.py" line="570"/>
+ <source>Description of services offered by %s</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/wizard.py" line="591"/>
+ <source>Register a new user with %s</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>StatusAwareTrayIconMixin</name>
+ <name>__impl</name>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="78"/>
- <source>EIP Connection Status</source>
+ <location filename="../src/leap/crypto/srpauth.py" line="259"/>
+ <source>Unknown user</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="87"/>
- <source>&lt;b&gt;disconnected&lt;/b&gt;</source>
+ <location filename="../src/leap/crypto/srpauth.py" line="271"/>
+ <source>The server did not send the salt parameter</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="126"/>
- <source>Encryption ON turn &amp;off</source>
+ <location filename="../src/leap/crypto/srpauth.py" line="275"/>
+ <source>The server did not send the B parameter</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="131"/>
- <source>&amp;Details...</source>
+ <location filename="../src/leap/crypto/srpauth.py" line="307"/>
+ <source>The data sent from the server had errors</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="135"/>
- <source>&amp;About</source>
+ <location filename="../src/leap/crypto/srpauth.py" line="329"/>
+ <source>Could not connect to the server</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="138"/>
- <source>About Q&amp;t</source>
+ <location filename="../src/leap/crypto/srpauth.py" line="348"/>
+ <source>Wrong password</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="141"/>
- <source>&amp;Quit</source>
+ <location filename="../src/leap/crypto/srpauth.py" line="355"/>
+ <source>Unknown error (%s)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/crypto/srpauth.py" line="386"/>
+ <source>Problem getting data from server</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/crypto/srpauth.py" line="410"/>
+ <source>Bad data from server</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/crypto/srpauth.py" line="417"/>
+ <source>Auth verification failed</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/crypto/srpauth.py" line="425"/>
+ <source>Session cookie verification failed</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/crypto/srpauth.py" line="262"/>
+ <source>There was a problem with authentication</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>msg</name>
+ <message>
+ <location filename="../src/leap/platform_init/initializers.py" line="89"/>
+ <source>Missing up/down scripts</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/platform_init/initializers.py" line="175"/>
+ <source>TAP Driver</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="152"/>
- <source>Encryption ON turn o&amp;ff</source>
+ <location filename="../src/leap/platform_init/initializers.py" line="299"/>
+ <source>LEAPClient needs to install the necessary drivers for Encrypted Internet to work. Would you like to proceed?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="157"/>
- <source>Encryption OFF turn &amp;on</source>
+ <location filename="../src/leap/platform_init/initializers.py" line="182"/>
+ <source>Encrypted Internet uses VPN, which needs a TAP device installed and none has been found. This will ask for administrative privileges.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="162"/>
- <source>connecting...</source>
+ <location filename="../src/leap/platform_init/initializers.py" line="296"/>
+ <source>TUN Driver</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="185"/>
- <source>About</source>
+ <location filename="../src/leap/platform_init/initializers.py" line="304"/>
+ <source>Encrypted Internet uses VPN, which needs a kernel extension for a TUN device installed, and none has been found. This will ask for administrative privileges.</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/docs/COPYING b/docs/COPYING
deleted file mode 100644
index 94a9ed02..00000000
--- a/docs/COPYING
+++ /dev/null
@@ -1,674 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
- <one line to give the program's name and a brief idea of what it does.>
- Copyright (C) <year> <name of author>
-
- 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/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- <program> Copyright (C) <year> <name of author>
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-<http://www.gnu.org/licenses/>.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/docs/api/leap.base.rst b/docs/api/leap.base.rst
deleted file mode 100644
index 778836c4..00000000
--- a/docs/api/leap.base.rst
+++ /dev/null
@@ -1,98 +0,0 @@
-base Package
-============
-
-:mod:`auth` Module
-------------------
-
-.. automodule:: leap.base.auth
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`authentication` Module
-----------------------------
-
-.. automodule:: leap.base.authentication
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`checks` Module
---------------------
-
-.. automodule:: leap.base.checks
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`config` Module
---------------------
-
-.. automodule:: leap.base.config
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`connection` Module
-------------------------
-
-.. automodule:: leap.base.connection
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`constants` Module
------------------------
-
-.. automodule:: leap.base.constants
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`exceptions` Module
-------------------------
-
-.. automodule:: leap.base.exceptions
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`network` Module
----------------------
-
-.. automodule:: leap.base.network
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`pluggableconfig` Module
------------------------------
-
-.. automodule:: leap.base.pluggableconfig
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`providers` Module
------------------------
-
-.. automodule:: leap.base.providers
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`specs` Module
--------------------
-
-.. automodule:: leap.base.specs
- :members:
- :undoc-members:
- :show-inheritance:
-
-Subpackages
------------
-
-.. toctree::
-
- leap.base.tests
-
diff --git a/docs/api/leap.base.tests.rst b/docs/api/leap.base.tests.rst
deleted file mode 100644
index 7af035b0..00000000
--- a/docs/api/leap.base.tests.rst
+++ /dev/null
@@ -1,43 +0,0 @@
-tests Package
-=============
-
-:mod:`test_auth` Module
------------------------
-
-.. automodule:: leap.base.tests.test_auth
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_checks` Module
--------------------------
-
-.. automodule:: leap.base.tests.test_checks
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_config` Module
--------------------------
-
-.. automodule:: leap.base.tests.test_config
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_providers` Module
-----------------------------
-
-.. automodule:: leap.base.tests.test_providers
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_validation` Module
------------------------------
-
-.. automodule:: leap.base.tests.test_validation
- :members:
- :undoc-members:
- :show-inheritance:
-
diff --git a/docs/api/leap.baseapp.rst b/docs/api/leap.baseapp.rst
deleted file mode 100644
index f24b4976..00000000
--- a/docs/api/leap.baseapp.rst
+++ /dev/null
@@ -1,75 +0,0 @@
-baseapp Package
-===============
-
-:mod:`constants` Module
------------------------
-
-.. automodule:: leap.baseapp.constants
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`dialogs` Module
----------------------
-
-.. automodule:: leap.baseapp.dialogs
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`eip` Module
------------------
-
-.. automodule:: leap.baseapp.eip
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`leap_app` Module
-----------------------
-
-.. automodule:: leap.baseapp.leap_app
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`log` Module
------------------
-
-.. automodule:: leap.baseapp.log
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`mainwindow` Module
-------------------------
-
-.. automodule:: leap.baseapp.mainwindow
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`network` Module
----------------------
-
-.. automodule:: leap.baseapp.network
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`permcheck` Module
------------------------
-
-.. automodule:: leap.baseapp.permcheck
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`systray` Module
----------------------
-
-.. automodule:: leap.baseapp.systray
- :members:
- :undoc-members:
- :show-inheritance:
-
diff --git a/docs/api/leap.certs.rst b/docs/api/leap.certs.rst
deleted file mode 100644
index e9cc2524..00000000
--- a/docs/api/leap.certs.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-certs Package
-=============
-
-:mod:`certs` Package
---------------------
-
-.. automodule:: leap.certs
- :members:
- :undoc-members:
- :show-inheritance:
-
diff --git a/docs/api/leap.config.rst b/docs/api/leap.config.rst
new file mode 100644
index 00000000..c1462817
--- /dev/null
+++ b/docs/api/leap.config.rst
@@ -0,0 +1,27 @@
+config Package
+==============
+
+:mod:`leapsettings` Module
+--------------------------
+
+.. automodule:: leap.config.leapsettings
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`provider_spec` Module
+---------------------------
+
+.. automodule:: leap.config.provider_spec
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`providerconfig` Module
+----------------------------
+
+.. automodule:: leap.config.providerconfig
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
diff --git a/docs/api/leap.crypto.rst b/docs/api/leap.crypto.rst
index a04e2e1d..6db77e86 100644
--- a/docs/api/leap.crypto.rst
+++ b/docs/api/leap.crypto.rst
@@ -1,18 +1,26 @@
crypto Package
==============
-:mod:`certs` Module
--------------------
+:mod:`constants` Module
+-----------------------
-.. automodule:: leap.crypto.certs
+.. automodule:: leap.crypto.constants
:members:
:undoc-members:
:show-inheritance:
-:mod:`leapkeyring` Module
+:mod:`srpauth` Module
+---------------------
+
+.. automodule:: leap.crypto.srpauth
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`srpregister` Module
-------------------------
-.. automodule:: leap.crypto.leapkeyring
+.. automodule:: leap.crypto.srpregister
:members:
:undoc-members:
:show-inheritance:
diff --git a/docs/api/leap.crypto.tests.rst b/docs/api/leap.crypto.tests.rst
index 54ffa62f..f9e32580 100644
--- a/docs/api/leap.crypto.tests.rst
+++ b/docs/api/leap.crypto.tests.rst
@@ -1,10 +1,26 @@
tests Package
=============
-:mod:`test_certs` Module
-------------------------
+:mod:`tests` Package
+--------------------
-.. automodule:: leap.crypto.tests.test_certs
+.. automodule:: leap.crypto.tests
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`fake_provider` Module
+---------------------------
+
+.. automodule:: leap.crypto.tests.fake_provider
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`test_srpregister` Module
+------------------------------
+
+.. automodule:: leap.crypto.tests.test_srpregister
:members:
:undoc-members:
:show-inheritance:
diff --git a/docs/api/leap.eip.rst b/docs/api/leap.eip.rst
deleted file mode 100644
index e418461b..00000000
--- a/docs/api/leap.eip.rst
+++ /dev/null
@@ -1,74 +0,0 @@
-eip Package
-===========
-
-:mod:`checks` Module
---------------------
-
-.. automodule:: leap.eip.checks
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`config` Module
---------------------
-
-.. automodule:: leap.eip.config
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`constants` Module
------------------------
-
-.. automodule:: leap.eip.constants
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`eipconnection` Module
----------------------------
-
-.. automodule:: leap.eip.eipconnection
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`exceptions` Module
-------------------------
-
-.. automodule:: leap.eip.exceptions
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`openvpnconnection` Module
--------------------------------
-
-.. automodule:: leap.eip.openvpnconnection
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`specs` Module
--------------------
-
-.. automodule:: leap.eip.specs
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`udstelnet` Module
------------------------
-
-.. automodule:: leap.eip.udstelnet
- :members:
- :undoc-members:
- :show-inheritance:
-
-Subpackages
------------
-
-.. toctree::
-
- leap.eip.tests
-
diff --git a/docs/api/leap.eip.tests.rst b/docs/api/leap.eip.tests.rst
deleted file mode 100644
index 932a074f..00000000
--- a/docs/api/leap.eip.tests.rst
+++ /dev/null
@@ -1,43 +0,0 @@
-tests Package
-=============
-
-:mod:`data` Module
-------------------
-
-.. automodule:: leap.eip.tests.data
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_checks` Module
--------------------------
-
-.. automodule:: leap.eip.tests.test_checks
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_config` Module
--------------------------
-
-.. automodule:: leap.eip.tests.test_config
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_eipconnection` Module
---------------------------------
-
-.. automodule:: leap.eip.tests.test_eipconnection
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_openvpnconnection` Module
-------------------------------------
-
-.. automodule:: leap.eip.tests.test_openvpnconnection
- :members:
- :undoc-members:
- :show-inheritance:
-
diff --git a/docs/api/leap.gui.firstrun.rst b/docs/api/leap.gui.firstrun.rst
deleted file mode 100644
index 36470c33..00000000
--- a/docs/api/leap.gui.firstrun.rst
+++ /dev/null
@@ -1,91 +0,0 @@
-firstrun Package
-================
-
-:mod:`firstrun` Package
------------------------
-
-.. automodule:: leap.gui.firstrun
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`intro` Module
--------------------
-
-.. automodule:: leap.gui.firstrun.intro
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`last` Module
-------------------
-
-.. automodule:: leap.gui.firstrun.last
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`login` Module
--------------------
-
-.. automodule:: leap.gui.firstrun.login
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`mixins` Module
---------------------
-
-.. automodule:: leap.gui.firstrun.mixins
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`providerinfo` Module
---------------------------
-
-.. automodule:: leap.gui.firstrun.providerinfo
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`providerselect` Module
-----------------------------
-
-.. automodule:: leap.gui.firstrun.providerselect
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`providersetup` Module
----------------------------
-
-.. automodule:: leap.gui.firstrun.providersetup
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`register` Module
-----------------------
-
-.. automodule:: leap.gui.firstrun.register
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`regvalidation` Module
----------------------------
-
-.. automodule:: leap.gui.firstrun.regvalidation
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`wizard` Module
---------------------
-
-.. automodule:: leap.gui.firstrun.wizard
- :members:
- :undoc-members:
- :show-inheritance:
-
diff --git a/docs/api/leap.gui.rst b/docs/api/leap.gui.rst
index a35a7856..d78de5d3 100644
--- a/docs/api/leap.gui.rst
+++ b/docs/api/leap.gui.rst
@@ -9,18 +9,34 @@ gui Package
:undoc-members:
:show-inheritance:
-:mod:`constants` Module
+:mod:`locale_rc` Module
-----------------------
-.. automodule:: leap.gui.constants
+.. automodule:: leap.gui.locale_rc
:members:
:undoc-members:
:show-inheritance:
-:mod:`locale_rc` Module
------------------------
+:mod:`loggerwindow` Module
+--------------------------
-.. automodule:: leap.gui.locale_rc
+.. automodule:: leap.gui.loggerwindow
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`loggerwindow_rc` Module
+-----------------------------
+
+.. automodule:: leap.gui.loggerwindow_rc
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`mainwindow` Module
+------------------------
+
+.. automodule:: leap.gui.mainwindow
:members:
:undoc-members:
:show-inheritance:
@@ -33,43 +49,51 @@ gui Package
:undoc-members:
:show-inheritance:
-:mod:`progress` Module
-----------------------
+:mod:`twisted_main` Module
+--------------------------
-.. automodule:: leap.gui.progress
+.. automodule:: leap.gui.twisted_main
:members:
:undoc-members:
:show-inheritance:
-:mod:`styles` Module
---------------------
+:mod:`ui_loggerwindow` Module
+-----------------------------
-.. automodule:: leap.gui.styles
+.. automodule:: leap.gui.ui_loggerwindow
:members:
:undoc-members:
:show-inheritance:
-:mod:`threads` Module
----------------------
+:mod:`ui_mainwindow` Module
+---------------------------
-.. automodule:: leap.gui.threads
+.. automodule:: leap.gui.ui_mainwindow
:members:
:undoc-members:
:show-inheritance:
-:mod:`utils` Module
--------------------
+:mod:`ui_wizard` Module
+-----------------------
-.. automodule:: leap.gui.utils
+.. automodule:: leap.gui.ui_wizard
:members:
:undoc-members:
:show-inheritance:
-Subpackages
------------
+:mod:`wizard` Module
+--------------------
-.. toctree::
+.. automodule:: leap.gui.wizard
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`wizardpage` Module
+------------------------
- leap.gui.firstrun
- leap.gui.tests
+.. automodule:: leap.gui.wizardpage
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/api/leap.gui.tests.rst b/docs/api/leap.gui.tests.rst
deleted file mode 100644
index 60b0a6ca..00000000
--- a/docs/api/leap.gui.tests.rst
+++ /dev/null
@@ -1,59 +0,0 @@
-tests Package
-=============
-
-:mod:`test_firstrun_login` Module
----------------------------------
-
-.. automodule:: leap.gui.tests.test_firstrun_login
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_firstrun_providerselect` Module
-------------------------------------------
-
-.. automodule:: leap.gui.tests.test_firstrun_providerselect
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_firstrun_register` Module
-------------------------------------
-
-.. automodule:: leap.gui.tests.test_firstrun_register
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_firstrun_wizard` Module
-----------------------------------
-
-.. automodule:: leap.gui.tests.test_firstrun_wizard
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_mainwindow_rc` Module
---------------------------------
-
-.. automodule:: leap.gui.tests.test_mainwindow_rc
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_progress` Module
----------------------------
-
-.. automodule:: leap.gui.tests.test_progress
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_threads` Module
---------------------------
-
-.. automodule:: leap.gui.tests.test_threads
- :members:
- :undoc-members:
- :show-inheritance:
-
diff --git a/docs/api/leap.keymanager.rst b/docs/api/leap.keymanager.rst
new file mode 100644
index 00000000..0de8fc5f
--- /dev/null
+++ b/docs/api/leap.keymanager.rst
@@ -0,0 +1,50 @@
+keymanager Package
+==================
+
+:mod:`keymanager` Package
+-------------------------
+
+.. automodule:: leap.keymanager
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`errors` Module
+--------------------
+
+.. automodule:: leap.keymanager.errors
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`gpg` Module
+-----------------
+
+.. automodule:: leap.keymanager.gpg
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`keys` Module
+------------------
+
+.. automodule:: leap.keymanager.keys
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`openpgp` Module
+---------------------
+
+.. automodule:: leap.keymanager.openpgp
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+Subpackages
+-----------
+
+.. toctree::
+
+ leap.keymanager.tests
+
diff --git a/docs/api/leap.keymanager.tests.rst b/docs/api/leap.keymanager.tests.rst
new file mode 100644
index 00000000..9836bd01
--- /dev/null
+++ b/docs/api/leap.keymanager.tests.rst
@@ -0,0 +1,11 @@
+tests Package
+=============
+
+:mod:`test_keymanager` Module
+-----------------------------
+
+.. automodule:: leap.keymanager.tests.test_keymanager
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
diff --git a/docs/api/leap.platform_init.rst b/docs/api/leap.platform_init.rst
new file mode 100644
index 00000000..a638df35
--- /dev/null
+++ b/docs/api/leap.platform_init.rst
@@ -0,0 +1,27 @@
+platform_init Package
+=====================
+
+:mod:`platform_init` Package
+----------------------------
+
+.. automodule:: leap.platform_init
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`initializers` Module
+--------------------------
+
+.. automodule:: leap.platform_init.initializers
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`locks` Module
+-------------------
+
+.. automodule:: leap.platform_init.locks
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
diff --git a/docs/api/leap.rst b/docs/api/leap.rst
index ce715c5d..102f3b61 100644
--- a/docs/api/leap.rst
+++ b/docs/api/leap.rst
@@ -30,12 +30,11 @@ Subpackages
.. toctree::
- leap.base
- leap.baseapp
- leap.certs
+ leap.config
leap.crypto
- leap.eip
leap.gui
- leap.testing
+ leap.keymanager
+ leap.platform_init
+ leap.services
leap.util
diff --git a/docs/api/leap.services.eip.rst b/docs/api/leap.services.eip.rst
new file mode 100644
index 00000000..450c1a93
--- /dev/null
+++ b/docs/api/leap.services.eip.rst
@@ -0,0 +1,66 @@
+eip Package
+===========
+
+:mod:`eipbootstrapper` Module
+-----------------------------
+
+.. automodule:: leap.services.eip.eipbootstrapper
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`eipconfig` Module
+-----------------------
+
+.. automodule:: leap.services.eip.eipconfig
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`eipspec` Module
+---------------------
+
+.. automodule:: leap.services.eip.eipspec
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`providerbootstrapper` Module
+----------------------------------
+
+.. automodule:: leap.services.eip.providerbootstrapper
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`udstelnet` Module
+-----------------------
+
+.. automodule:: leap.services.eip.udstelnet
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`vpnlaunchers` Module
+--------------------------
+
+.. automodule:: leap.services.eip.vpnlaunchers
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`vpnprocess` Module
+------------------------
+
+.. automodule:: leap.services.eip.vpnprocess
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+Subpackages
+-----------
+
+.. toctree::
+
+ leap.services.eip.tests
+
diff --git a/docs/api/leap.services.eip.tests.rst b/docs/api/leap.services.eip.tests.rst
new file mode 100644
index 00000000..ea6e8ba5
--- /dev/null
+++ b/docs/api/leap.services.eip.tests.rst
@@ -0,0 +1,11 @@
+tests Package
+=============
+
+:mod:`test_eipconfig` Module
+----------------------------
+
+.. automodule:: leap.services.eip.tests.test_eipconfig
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
diff --git a/docs/api/leap.services.mail.rst b/docs/api/leap.services.mail.rst
new file mode 100644
index 00000000..bd479972
--- /dev/null
+++ b/docs/api/leap.services.mail.rst
@@ -0,0 +1,27 @@
+mail Package
+============
+
+:mod:`smtpbootstrapper` Module
+------------------------------
+
+.. automodule:: leap.services.mail.smtpbootstrapper
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`smtpconfig` Module
+------------------------
+
+.. automodule:: leap.services.mail.smtpconfig
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`smtpspec` Module
+----------------------
+
+.. automodule:: leap.services.mail.smtpspec
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
diff --git a/docs/api/leap.services.rst b/docs/api/leap.services.rst
new file mode 100644
index 00000000..e5b22e64
--- /dev/null
+++ b/docs/api/leap.services.rst
@@ -0,0 +1,28 @@
+services Package
+================
+
+:mod:`abstractbootstrapper` Module
+----------------------------------
+
+.. automodule:: leap.services.abstractbootstrapper
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`tx` Module
+----------------
+
+.. automodule:: leap.services.tx
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+Subpackages
+-----------
+
+.. toctree::
+
+ leap.services.eip
+ leap.services.mail
+ leap.services.soledad
+
diff --git a/docs/api/leap.services.soledad.rst b/docs/api/leap.services.soledad.rst
new file mode 100644
index 00000000..efefeb50
--- /dev/null
+++ b/docs/api/leap.services.soledad.rst
@@ -0,0 +1,27 @@
+soledad Package
+===============
+
+:mod:`soledadbootstrapper` Module
+---------------------------------
+
+.. automodule:: leap.services.soledad.soledadbootstrapper
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`soledadconfig` Module
+---------------------------
+
+.. automodule:: leap.services.soledad.soledadconfig
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`soledadspec` Module
+-------------------------
+
+.. automodule:: leap.services.soledad.soledadspec
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
diff --git a/docs/api/leap.testing.rst b/docs/api/leap.testing.rst
deleted file mode 100644
index dfad1b0c..00000000
--- a/docs/api/leap.testing.rst
+++ /dev/null
@@ -1,43 +0,0 @@
-testing Package
-===============
-
-:mod:`basetest` Module
-----------------------
-
-.. automodule:: leap.testing.basetest
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`https_server` Module
---------------------------
-
-.. automodule:: leap.testing.https_server
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`pyqt` Module
-------------------
-
-.. automodule:: leap.testing.pyqt
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`qunittest` Module
------------------------
-
-.. automodule:: leap.testing.qunittest
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_basetest` Module
----------------------------
-
-.. automodule:: leap.testing.test_basetest
- :members:
- :undoc-members:
- :show-inheritance:
-
diff --git a/docs/api/leap.util.rst b/docs/api/leap.util.rst
index dbb69ebe..601f6cba 100644
--- a/docs/api/leap.util.rst
+++ b/docs/api/leap.util.rst
@@ -1,26 +1,10 @@
util Package
============
-:mod:`coroutines` Module
-------------------------
-
-.. automodule:: leap.util.coroutines
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`dicts` Module
+:mod:`util` Package
-------------------
-.. automodule:: leap.util.dicts
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`fileutil` Module
-----------------------
-
-.. automodule:: leap.util.fileutil
+.. automodule:: leap.util
:members:
:undoc-members:
:show-inheritance:
@@ -33,34 +17,35 @@ util Package
:undoc-members:
:show-inheritance:
-:mod:`misc` Module
-------------------
+:mod:`leap_log_handler` Module
+------------------------------
-.. automodule:: leap.util.misc
+.. automodule:: leap.util.leap_log_handler
:members:
:undoc-members:
:show-inheritance:
-:mod:`translations` Module
---------------------------
+:mod:`privilege_policies` Module
+--------------------------------
-.. automodule:: leap.util.translations
+.. automodule:: leap.util.privilege_policies
:members:
:undoc-members:
:show-inheritance:
-:mod:`web` Module
------------------
+:mod:`request_helpers` Module
+-----------------------------
-.. automodule:: leap.util.web
+.. automodule:: leap.util.request_helpers
:members:
:undoc-members:
:show-inheritance:
-Subpackages
------------
-
-.. toctree::
+:mod:`requirement_checker` Module
+---------------------------------
- leap.util.tests
+.. automodule:: leap.util.requirement_checker
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/api/leap.util.tests.rst b/docs/api/leap.util.tests.rst
deleted file mode 100644
index eb6654c4..00000000
--- a/docs/api/leap.util.tests.rst
+++ /dev/null
@@ -1,19 +0,0 @@
-tests Package
-=============
-
-:mod:`test_fileutil` Module
----------------------------
-
-.. automodule:: leap.util.tests.test_fileutil
- :members:
- :undoc-members:
- :show-inheritance:
-
-:mod:`test_leap_argparse` Module
---------------------------------
-
-.. automodule:: leap.util.tests.test_leap_argparse
- :members:
- :undoc-members:
- :show-inheritance:
-
diff --git a/docs/checklist_for_leap_client_release.wiki b/docs/checklist_for_leap_client_release.wiki
index ae88316f..d3bdf1ee 100644
--- a/docs/checklist_for_leap_client_release.wiki
+++ b/docs/checklist_for_leap_client_release.wiki
@@ -1,47 +1,39 @@
= LEAP CLient Release Checklist (*) =
-Not that we are following all steps here, but it is good to keep some
-sort of sanity...
-
- * [X] validate rc
- * [X] all rc-critical closed!
- * [X] all bbots green
- * [X] uploaded translations: make translations
- * [X] re-generate pyqt resources
+ * [ ] validate rc
+ * [ ] all rc-critical closed!
+ * [ ] all bbots green
+ * [ ] uploaded translations: make translations
+ * [ ] re-generate pyside resources
- * [O] update docs
- * [X] check installation sections
+ * [ ] update docs
* [ ] CREDITS
- * [X] relnotes.txt
- * [X] docs/known_issues.rst
- * [X] NEWS.rst: Add release name and date to top-most item in NEWS.
+ * [ ] relnotes.txt
+ * [ ] docs/known_issues.rst
+ * [ ] NEWS.rst: Add release name and date to top-most item in NEWS.
- * [X] change docs/install.rst to point to just the current
+ * [ ] change docs/quickstart.rst to point to just the current
leap-client-X.Y.Z.deb binaries and .tar.gz source code files
- * [X] on release/vX.Y.Z branch: git pull
+ * [ ] on release/vX.Y.Z branch: git pull
* [ ] git tag X.Y.Z
* [ ] build locally to make sure the release is reporting itself as the
- intended version ...
+ intended version (FIXME!)
* [ ] make sure buildbot is green
* [ ] make sure other people aren't committing at that moment
* [ ] FUTURE: push tag along with some other documentation-only patch (typically to
relnotes.txt) to trigger buildslaves
- * [ ] git push --tags leap; git push leap
+ * [ ] git push --tags official; git push official
* [ ] that will build tarballs
* [ ] make sure buildbot is green (in a parallel universe, he)
* [ ] download tarballs, sign with "gpg -ba -u deadbeef TAR", upload *.asc
* [ ] symlink the release tarball on leap.se downloads page:
/var/www/source/leap-client/releases/ CHANGEME XXX
- * [X] update news pages. release notes.
+ * [ ] update news pages. release notes.
* [ ] send out relnotes.txt to internal list.
-
* [ ] wait ...?
* [ ] PYPI UPLOAD: with "python ./setup.py sdist upload register"
- * [ ] login to pypi
- * [ ] from Edit, add new release
- * [ ] upload .tar.gz, .asc
* [ ] make an "announcement of new release" on leap.se
* [ ] close the Milestone on the chili Roadmap
@@ -50,4 +42,4 @@ sort of sanity...
notes
-----
-(*) the original version of this handy checklist kindly borrowed from tahoe-lafs documentation =)
+(*) this checklist kindly borrowed from tahoe-lafs documentation =)
diff --git a/docs/conf.py b/docs/conf.py
index 296cb262..39f17d9b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -16,7 +16,21 @@ import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.insert(0, os.path.abspath('.'))
+sys.path.insert(0, os.path.abspath('../src'))
+sys.path.insert(0, os.path.abspath('../src/leap'))
+sys.path.insert(0, os.path.abspath('../src/leap/crypto'))
+sys.path.insert(0, os.path.abspath('../src/leap/keymanager'))
+sys.path.insert(0, os.path.abspath('../src/leap/services'))
+sys.path.insert(0, os.path.abspath('../src/leap/services/eip'))
+sys.path.insert(0, os.path.abspath('../src/leap/util'))
+
+sys.path.insert(0, os.path.abspath(
+ os.path.expanduser(
+ '~/Virtualenvs/leap-client/local/lib/python2.7/'
+ 'site-packages/leap/common')))
+
+# TODO: should add all the virtualenv site-packages to the path
+# as a workaround, install all in your path.
# -- General configuration -----------------------------------------------------
@@ -25,7 +39,11 @@ import sys, os
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc']
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
+
+intersphinx_mapping = {'python': ('http://docs.python.org/2.7', None),
+ 'twisted': ('http://twistedsphinx.funsize.net/',
+ None)}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -48,9 +66,9 @@ copyright = u'2012, The LEAP Encryption Access Project'
# built documents.
#
# The short X.Y version.
-version = '0.2.0'
+version = '0.2.1-dev1'
# The full version, including alpha/beta/rc tags.
-release = '0.2.0'
+release = '0.2.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/docs/config/files.rst b/docs/config/files.rst
index 0f4abead..70d3bff8 100644
--- a/docs/config/files.rst
+++ b/docs/config/files.rst
@@ -3,7 +3,7 @@
Configuration Files
===================
-This document covers the different configuration files used by the LEAP Client.
+This document covers the different configuration files used by Bitmask.
leap.conf
---------
diff --git a/docs/dev/environment.rst b/docs/dev/environment.rst
index c3868b81..7ce536f9 100644
--- a/docs/dev/environment.rst
+++ b/docs/dev/environment.rst
@@ -3,24 +3,25 @@
Setting up a development environment
====================================
-This document covers how to get an enviroment ready to contribute code to the LEAP Client.
+This document covers how to get an enviroment ready to contribute code to Bitmask.
Cloning the repo
----------------
.. note::
- Stable releases will be in *master* branch (nothing there yet, move on!).
+ Stable releases are in *master* branch.
Development code lives in *develop* branch.
::
- git clone git://leap.se/leap_client
+ git clone git://leap.se/bitmask
+ git checkout develop
Base Dependencies
------------------
-Leap client depends on these libraries:
+Bitmask depends on these libraries:
* `python 2.6 or 2.7`
-* `qt4` libraries (see also :ref:`Troubleshooting PyQt install <pyqtvirtualenv>` about how to install inside your virtualenv)
+* `qt4` libraries (see also :ref:`Troubleshooting PySide install <pysidevirtualenv>` about how to install inside your virtualenv)
* `openssl`
* `openvpn <http://openvpn.net/index.php/open-source/345-openvpn-project.html>`_
@@ -28,7 +29,7 @@ Debian
^^^^^^
In debian-based systems::
- $ apt-get install openvpn python-qt4 python-crypto python-openssl
+ $ apt-get install openvpn python-pyside python-openssl
To install the software from sources::
@@ -50,6 +51,10 @@ The basic problem being addressed is one of dependencies and versions, and indir
Read more about it in the `project documentation page <http://pypi.python.org/pypi/virtualenv/>`_.
+.. note::
+ this section could be completed with useful options that can be passed to the virtualenv command (e.g., to make portable paths, site-packages, ...). We also should document how to use virtualenvwrapper.
+
+
Create and activate your dev environment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -58,36 +63,36 @@ Create and activate your dev environment
$ virtualenv </path/to/new/environment>
$ source </path/to/new/environment>/bin/activate
-Install python dependencies
-^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. _pysidevirtualenv:
-You can install python dependencies with pip. If you do it inside your working environment, they will be installed avoiding the need for administrative permissions::
+Avoid compiling PySide inside a virtualenv
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- $ pip install -r pkg/requirements.pip
+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.
-.. _pyqtvirtualenv:
+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*)::
-Troubleshooting PyQt install inside a virtualenv
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ $ pkg/postmkvenv.sh
-If you attempt to install PyQt inside a virtualenv using pip, it will fail because PyQt4 does not use the standard setup.py mechanism.
+A second option if that does not work for you would be to install PySide globally and pass the ``--site-packages`` option when you are creating your virtualenv::
-As a workaround, you can run the following script after creating your virtualenv. It will symlink to your global PyQt installation (*this is the recommended way if you are running a debian-based system*)::
+ $ apt-get install python-pyside
+ $ virtualenv --site-packages .
- $ pkg/postmkvenv.sh
+After that, you must export ``LEAP_VENV_SKIP_PYSIDE`` to skip the isntallation::
-A second option if that does not work for you would be to install PyQt globally and pass the ``--site-packages`` option when you are creating your virtualenv::
+ $ export LEAP_VENV_SKIP_PYSIDE=1
- $ apt-get install python-qt4
- $ virtualenv --site-packages .
+And now you are ready to proceed with the next section.
-Or, if you prefer, you can also `download the official PyQt tarball <http://www.riverbankcomputing.com/software/pyqt/download>`_ and execute ``configure.py`` in the root folder of their distribution, which generates a ``Makefile``::
+.. _pydepinstall:
- $ python configure.py
- $ make && make install
+Install python dependencies
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
-.. note::
- this section could be completed with useful options that can be passed to the virtualenv command (e.g., to make portable paths, site-packages, ...).
+You can install python dependencies with ``pip``. If you do it inside your working environment, they will be installed avoiding the need for administrative permissions::
+
+ $ pip install -r pkg/requirements.pip
.. _copyscriptfiles:
@@ -95,10 +100,10 @@ Or, if you prefer, you can also `download the official PyQt tarball <http://www.
Copy script files
-----------------
-The openvpn invocation expects some files to be in place. If you have not installed `leap-client` from a debian package, you must copy these files manually::
+The openvpn invocation expects some files to be in place. If you have not installed `bitmask` from a debian package, you must copy these files manually by now::
$ sudo mkdir -p /etc/leap
- $ sudo cp pkg/linux/resolv-update /etc/leap
+ $ sudo cp pkg/linux/resolv-update /etc/leap
.. _policykit:
@@ -106,9 +111,9 @@ Running openvpn without root privileges
---------------------------------------
In linux, we are using ``policykit`` to be able to run openvpn without root privileges, and a policy file is needed to be installed for that to be possible.
-The setup script tries to install the policy file when installing the client system-wide, so if you have installed the client in your global site-packages at least once it should have copied this file for you.
+The setup script tries to install the policy file when installing bitmask system-wide, so if you have installed bitmask in your global site-packages at least once it should have copied this file for you.
-If you *only* are running the client from inside a virtualenv, you will need to copy this file by hand::
+If you *only* are running bitmask from inside a virtualenv, you will need to copy this file by hand::
$ sudo cp pkg/linux/polkit/net.openvpn.gui.leap.policy /usr/share/polkit-1/actions/
diff --git a/docs/dev/internals.rst b/docs/dev/internals.rst
index 8bb19211..e43868f3 100644
--- a/docs/dev/internals.rst
+++ b/docs/dev/internals.rst
@@ -3,7 +3,7 @@
Internals
=========
-This section covers briefly the internal organization of the LEAP Client source tree.
+This section covers briefly the internal organization of Bitmask source tree.
.. note::
diff --git a/docs/dev/internationalization.rst b/docs/dev/internationalization.rst
index 1a9af0be..f9f522b9 100644
--- a/docs/dev/internationalization.rst
+++ b/docs/dev/internationalization.rst
@@ -3,15 +3,15 @@
Internationalization
====================
-This part of the documentation covers the localization and translation of LEAP Client.
+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 the LEAP Client PyQt Application
---------------------------------------------
+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/leap-client/resource/leap-client/" title="See more information on Transifex.com">Top translations: leap-client » leap-client</a><br/><img border="0" src="https://www.transifex.com/projects/p/leap-client/resource/leap-client/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>
+ <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
@@ -21,7 +21,7 @@ For translators
as internal notes.
-We are using `transifex <http://transifex.com/projects/p/leap-client>`_ to coordinate translation efforts. If you want to contribute, just sign up there and ...
+We are using `transifex <http://transifex.com/projects/p/bitmask>`_ to coordinate translation efforts. If you want to contribute, just sign up there and ...
.. note::
... and what??
@@ -32,7 +32,7 @@ For devs: i18n conventions
.. note::
should say something about our special cases (provider labels and exceptions) when we get decision about it.
-Refer to `pyqt documentation <http://www.riverbankcomputing.co.uk/static/Docs/PyQt4/html/i18n.html>`_.
+Refer to `pyside documentation <http://qt-project.org/wiki/PySide_Internationalization>`_.
tl;dr;::
@@ -40,15 +40,11 @@ tl;dr;::
for any string that you want to be translated, as long as the instance derives from ``QObject``.
-If you have to translate something that it is not a ``QObject``, use the magic leap ``translate`` method:
-
-
-.. code-block:: python
-
- from leap.util.translations import translate
-
- class Foo(object):
- bar = translate(<Context>, <string>, <comment>)
+.. If you have to translate something that it is not a ``QObject``, use the magic leap ``translate`` method:
+.. .. code-block:: python
+.. from leap.util.translations import translate
+.. class Foo(object):
+.. bar = translate(<Context>, <string>, <comment>)
.. Note about this: there seems to be some problems with the .tr method
@@ -59,17 +55,17 @@ If you have to translate something that it is not a ``QObject``, use the magic l
For i18n maintainers
^^^^^^^^^^^^^^^^^^^^
-You need ``pylupdate4`` and ``lrelease`` for these steps. To get it, in debian::
+You need ``pyside-lupdate`` and ``lrelease`` for these steps. To get it, in debian::
- $ apt-get install pyqt4-dev-tools qt4-linguist-tools
+ $ apt-get install pyside-tools qt4-linguist-tools
If you do not already have it, install the ``transifex-client`` from the cheese shop::
- pip install transifex-client
+ pip install transifex-client
You can learn more about the transifex-client `here <http://help.transifex.com/features/client/index.html>`_.
-**1.** Add any new source files to the project file, ``data/leap_client.pro``. *We should automate this with some templating, it's tedious.*
+**1.** Add any new source files to the project file, ``data/bitmask.pro``. *We should automate this with some templating, it's tedious.*
**2.** Update the source .ts file ``data/ts/en_US.ts``.::
@@ -84,7 +80,7 @@ You can learn more about the transifex-client `here <http://help.transifex.com/f
**5.** *Et voila!* Get updated .ts files for each language from ``Transifex``. For instance, to pull updated spanish translations::
$ tx pull -l es
- Pulling new translations for resource leap-client.leap-client (source: data/ts/en_US.ts)
+ Pulling new translations for resource bitmask.bitmask (source: data/ts/en_US.ts)
-> es: data/translations/es.ts
Done.
@@ -93,7 +89,7 @@ Note that there is a configuration option in ``.tx/config`` for setting the mini
**6.** Generate .qm files from the updated .ts files::
- $ make translations
+ $ make translations
and yes, it's the same command than in step 2. One less thing to remember :)
@@ -105,7 +101,7 @@ and yes, it's the same command than in step 2. One less thing to remember :)
If you want to try it, just set your LANG environment variable::
- $ LANG=es_ES leap-client
+ $ LANG=es_ES bitmask
Translating the Documentation
diff --git a/docs/dev/resources.rst b/docs/dev/resources.rst
index 7cfa2b70..e68649a1 100644
--- a/docs/dev/resources.rst
+++ b/docs/dev/resources.rst
@@ -1,7 +1,7 @@
.. _resources:
-PyQt Resource files
-===================
+PySide Resource files
+=====================
Compiling resource/ui files
---------------------------
diff --git a/docs/dev/tests.rst b/docs/dev/tests.rst
index 7f5fbaaf..d55c206a 100644
--- a/docs/dev/tests.rst
+++ b/docs/dev/tests.rst
@@ -39,8 +39,8 @@ Hint: colorized output
Install ``rednose`` locally, export the ``NOSE_REDNOSE`` variable, and give your eyes a rest :)::
- (leap_client)% pip install rednose
- (leap_client)% export NOSE_REDNOSE=1
+ (bitmask)% pip install rednose
+ (bitmask)% export NOSE_REDNOSE=1
Testing all the supported python versions
-----------------------------------------
diff --git a/docs/dev/workflow.rst b/docs/dev/workflow.rst
index 5ceccca4..e36431ff 100644
--- a/docs/dev/workflow.rst
+++ b/docs/dev/workflow.rst
@@ -20,20 +20,22 @@ If you introduce a new dependency, please add it under ``pkg/requirements`` or `
Git flow
--------
-See `A successful git branching model <http://nvie.com/posts/a-successful-git-branching-model/>`_ for more information. The slight modification we make is that release tags are made in the release branch before getting merged to master, rather than getting tagged in master.
+We are basing our workflow on what is described in `A successful git branching model <http://nvie.com/posts/a-successful-git-branching-model/>`_.
.. image:: https://leap.se/code/attachments/13/git-branching-model.png
The author of the aforementioned post has also a handy pdf version of it: `branching_model.pdf`_
+However, we use a setup in which each developer maintains her own feature branch in her private repo. After a code review, this feature branch is rebased onto the authoritative integration branch. Thus, the leapcode repo in leap.se (mirrored in github) only maintains the master and develop branches.
+
A couple of tools that help to follow this process are `git-flow`_ and `git-sweep`_.
.. _`branching_model.pdf`: https://leap.se/code/attachments/14/Git-branching-model.pdf
.. _`git-flow`: https://github.com/nvie/gitflow
.. _`git-sweep`: http://pypi.python.org/pypi/git-sweep
-Merge into integration branch
------------------------------
+Code review and merges into integration branch
+-----------------------------------------------
All code ready to be merged into the integration branch is expected to:
* Have tests
diff --git a/docs/index.rst b/docs/index.rst
index 3e1a603f..e3078929 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -9,7 +9,7 @@ LEAP Client
Release v\ |version|. (`Impatient? jump to the` :ref:`Installation <install>` `section!`)
.. if you change this paragraph, change it in user/intro too
-The **LEAP Encryption Access Project Client** is a :ref:`GPL3 Licensed <gpl3>` multiplatform client, written in python using PyQt4, that supports the features offered by :ref:`the LEAP Platform <leapplatform>`. Currently is being tested on Linux, support for OSX and Windows will come soon.
+The **LEAP Encryption Access Project Client** is a :ref:`GPL3 Licensed <gpl3>` multiplatform client, written in python using PySide, that supports the features offered by :ref:`the LEAP Platform <leapplatform>`. Currently is being tested on Linux, support for OSX and Windows will come soon.
User Guide
----------
@@ -77,15 +77,12 @@ Different directories and files used for the configuration of the client.
API Documentation
-----------------
-If you are looking for a reference to specific classes or functions, you are likely to find it here
+If you are looking for a reference to specific classes or functions, you are likely to find it here.
.. I should investigate a bit more how to skip some things, and how to give nice format
to the docstrings.
Maybe we should not have sphinx-apidocs building everything, but a minimal index of our own.
-.. note::
- when it's a bit more polished, that's it :)
-
.. toctree::
:maxdepth: 2
diff --git a/docs/known_issues.rst b/docs/known_issues.rst
deleted file mode 100644
index 94422559..00000000
--- a/docs/known_issues.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-0.2.0
------
-
-* On the login page, the error messages are not descriptive.
diff --git a/docs/man/leap-client.1.rst b/docs/man/bitmask.1.rst
index 1ef5b3cc..12c87a5d 100644
--- a/docs/man/leap-client.1.rst
+++ b/docs/man/bitmask.1.rst
@@ -1,6 +1,6 @@
-===========
-leap-client
-===========
+=======
+bitmask
+=======
------------------------------------------------------------------------
graphical client to control LEAP, the encrypted internet access toolkit.
@@ -16,12 +16,12 @@ graphical client to control LEAP, the encrypted internet access toolkit.
SYNOPSIS
========
-leap-client [-h] [-d] [-l [LOG FILE]] [--openvpn-verbosity [OPENVPN_VERB]]
+bitmask [-h] [-d] [-l [LOG FILE]] [--openvpn-verbosity [OPENVPN_VERB]]
DESCRIPTION
===========
-*leap-client* is a graphical client to control LEAP, the encrypted internet access toolkit.
+*bitmask* is a graphical client to control LEAP, the encrypted internet access toolkit.
When launched, it places an icon in the system tray from where the LEAP services can be controlled.
@@ -49,14 +49,14 @@ WARNING
This software is still in early alfa testing. So don't trust your life to it!
-At the current time, the LEAP Client is not compatible with ``openresolv``, but it works with ``resolvconf``.
+At the current time, Bitmask is not compatible with ``openresolv``, but it works with ``resolvconf``.
FILES
=====
/etc/leap/resolv-update
-----------------------
-Post up/down script passed to openvpn. It writes /etc/resolv.conf to avoid dns leaks, and restores the original resolv.conf on exit.
+Post up/down script passed to openvpn. It writes /etc/resolv.conf to avoid dns leaks, and restores the original resolv.conf on exit.
/etc/leap/resolv-head
---------------------
diff --git a/docs/pkg/debian.rst b/docs/pkg/debian.rst
index e98032a5..204d4073 100644
--- a/docs/pkg/debian.rst
+++ b/docs/pkg/debian.rst
@@ -10,9 +10,9 @@ Dependencies
------------
* ``openvpn``
-* ``python-qt4``
+* ``python-pyside``
* ``python-crypto``
-* ``python setuptools``
+* ``python-setuptools``
* ``python-requests``
* ``python-openssl``
diff --git a/docs/testers/howto.rst b/docs/testers/howto.rst
index c4a928ed..dde893d1 100644
--- a/docs/testers/howto.rst
+++ b/docs/testers/howto.rst
@@ -17,6 +17,10 @@ Fetching latest development code
To allow rapid testing in different platforms, we have put together a quick script that is able to fetch latest development code. It more or less does all the steps covered in the :ref:`Setting up a Work Enviroment <environment>` section, only that in a more compact way suitable (ahem) also for non developers.
+.. note::
+
+ In the near future, we will be using ``standalone bundles`` with the ability to self-update.
+
Install dependencies
^^^^^^^^^^^^^^^^^^^^
First, install all the base dependencies plus git, virtualenv and development files needed to compile several extensions::
@@ -30,19 +34,19 @@ Bootstrap script
This will fetch the *develop* branch. If you want to test another branch, just change it in the line starting with *pip install...*. Alternatively, bug kali so she add an option branch to a decent script.
.. note::
- This script could make use of the after_install hook. Read http://pypi.python.org/pypi/virtualenv/
+ This script could make use of the after_install hook. Read http://pypi.python.org/pypi/virtualenv/
Download and source the following script in the parent folder where you want your testing build to be downloaded. For instance, to `/tmp/`:
.. code-block:: bash
cd /tmp
- wget https://raw.github.com/leapcode/leap_client/develop/pkg/scripts/leap_client_bootstrap.sh
+ wget https://raw.github.com/leapcode/bitmask/develop/pkg/scripts/leap_client_bootstrap.sh
source leap_client_bootstrap.sh
-Tada! If everything went well, you should be able to run the client by typing::
+Tada! If everything went well, you should be able to run bitmask by typing::
- bin/leap-client
+ bin/bitmask
Noticed that your prompt changed? That was *virtualenv*. Keep reading...
@@ -50,7 +54,7 @@ Activating the virtualenv
^^^^^^^^^^^^^^^^^^^^^^^^^
The above bootstrap script has fetched latest code inside a virtualenv, which is an isolated, *virtual* python local environment that avoids messing with your global paths. You will notice you are *inside* a virtualenv because you will see a modified prompt reminding it to you (*leap-client-testbuild* in this case).
-Thus, if you forget to *activate your virtualenv*, the client will not run from the local path, and it will be looking for something else in your global path. So, **you have to remember to activate your virtualenv** each time that you open a new shell and want to execute the code you are testing. You can do this by typing::
+Thus, if you forget to *activate your virtualenv*, bitmask will not run from the local path, and it will be looking for something else in your global path. So, **you have to remember to activate your virtualenv** each time that you open a new shell and want to execute the code you are testing. You can do this by typing::
$ source bin/activate
@@ -61,11 +65,11 @@ Refer to :ref:`Working with virtualenv <virtualenv>` to learn more about virtual
Copying config files
^^^^^^^^^^^^^^^^^^^^
-If you have never installed the ``leap-client`` globally, **you need to copy some files to its proper path before running it for the first time** (you only need to do this once). This, unless the virtualenv-based operations, will need root permissions. See :ref:`copy script files <copyscriptfiles>` and :ref:`running openvpn without root privileges <policykit>` sections for more info on this. In short::
+If you have never installed ``bitmask`` globally, **you need to copy some files to its proper path before running it for the first time** (you only need to do this once). This, unless the virtualenv-based operations, will need root permissions. See :ref:`copy script files <copyscriptfiles>` and :ref:`running openvpn without root privileges <policykit>` sections for more info on this. In short::
$ sudo cp pkg/linux/polkit/net.openvpn.gui.leap.policy /usr/share/polkit-1/actions/
$ sudo mkdir -p /etc/leap
- $ sudo cp pkg/linux/resolv-update /etc/leap
+ $ sudo cp pkg/linux/resolv-update /etc/leap
Local config files
^^^^^^^^^^^^^^^^^^^
@@ -79,8 +83,8 @@ Pulling latest changes
You should be able to cd into the downloaded repo and pull latest changes::
- (leap-client-testbuild)$ cd src/leap-client
- (leap-client-testbuild)$ git pull origin develop
+ (bitmask-testbuild)$ cd src/bitmask
+ (bitmask-testbuild)$ git pull origin develop
However, as a tester you are encouraged to run the whole bootstrap process from time to time to help us catching install and versioniing bugs too.
@@ -91,13 +95,13 @@ When we have a release candidate for the supported platforms (Debian stable, Ubu
Testing the status of translations
----------------------------------
-We need translators! You can go to `transifex <https://www.transifex.com/projects/p/leap-client/>`_, get an account and start contributing.
+We need translators! You can go to `transifex <https://www.transifex.com/projects/p/bitmask/>`_, get an account and start contributing.
-If you want to check the current status of the client localization in a language other than the one set in your machine, you can do it with a simple trick (under linux). For instance, do::
+If you want to check the current status of bitmask localization in a language other than the one set in your machine, you can do it with a simple trick (under linux). For instance, do::
- $ lang=es_ES leap-client
+ $ lang=es_ES bitmask
-for running LEAP Client with the spanish locales.
+for running Bitmask with the spanish locales.
Reporting bugs
--------------
@@ -106,6 +110,6 @@ Reporting bugs
There is a great text on the art of bug reporting, that can be found `online <http://www.chiark.greenend.org.uk/~sgtatham/bugs.html>`_.
-.. TODO add a line with ref. to running the client in debug mode...
+.. TODO add a line with ref. to running Bitmask in debug mode...
-We use the `LEAP Client Bug Tracker <https://leap.se/code/projects/eip-client>`_, although you can also use `Github issues <https://github.com/leapcode/leap_client/issues>`_.
+We use the `Bitmask Bug Tracker <https://leap.se/code/projects/eip-client>`_, although you can also use `Github issues <https://github.com/leapcode/bitmask/issues>`_.
diff --git a/docs/user/install.rst b/docs/user/install.rst
index a52c1b87..da1d914c 100644
--- a/docs/user/install.rst
+++ b/docs/user/install.rst
@@ -3,64 +3,59 @@
Installation
============
-This part of the documentation covers the installation of the LEAP Client.
+This part of the documentation covers the installation of Bitmask.
We assume that you want to get it properly installed before being able to use it.
-Debian package
---------------
+.. note::
-If you are lucky enough to be using a debian system... well, you are lucky enough :)
+ The recommended way of installing in the near future will be the standalone bundles, but those are not quite ready yet. Methods described in this page assume you are familiar with python code, and you can find your way through the process of dependencies install. You can refer to the sections :ref:`setting up a working environment <environment>` or :ref:`fetching latest code for testing <fetchinglatest>`.
-To be able to install ``leap-client``, you need to add the leap repo to your software sources. First you add the leap keys to your keyring::
- gpg --recv-key 0x1E34A1828E207901 0x485B12FA218E81EB
- gpg -a --export 0x1E34A1828E207901 | sudo apt-key add -
-
-Now you edit :file:`/etc/apt/sources.list` and add the leap repository::
+Distribute & Pip
+----------------
- deb http://deb.leap.se/debian unstable main
+.. warning:: The package in the cheese shop is from the stable, `0.2.0` release, which is now outdated. You are encouraged to install the development version instead.
-Get the package ``leap-keyring``, so you can install the packages in a trusted way and get key updates automatically::
+Installing Bitmask is as simple as using `pip <http://www.pip-installer.org/>`_ for the already released versions ::
- apt-get update
- apt-get install leap-keyring
+ $ pip install bitmask
-And, finally, install the client::
+Debian package
+--------------
- apt-get install leap-client
+.. warning::
+ The debian package in the leap repositories is from the stable, `0.2.0` release, which is now outdated. You are encouraged to install the development version instead,
-Distribute & Pip
-----------------
+First, you need to bootstrap your apt-key::
-Install the dependencies::
+ # gpg --recv-key 0x1E34A1828E207901 0x485B12FA218E81EB
+ # gpg --list-sigs 0x1E34A1828E207901
+ # gpg --list-sigs 0x485B12FA218E81EB
+ # gpg -a --export 0x1E34A1828E207901 | sudo apt-key add -
- apt-get install openvpn python-qt4 python-dev python-openssl
+Add the archive to your sources.list::
+ # echo "deb http://deb.leap.se/debian unstable main" >> /etc/apt/sources.list
+ # apt-get update
+ # apt-get install leap-keyring
-And then installing the client with `pip <http://www.pip-installer.org/>`_ is as simple as::
+And then you can happily install bitmask::
- pip install leap-client
+ apt-get install bitmask
Show me the code!
-----------------
-You can get the latest tarball ::
-
- wget https://leap.se/downloads/leap-client/tarball/latest
-
-Or the zipball::
+You can get the code from LEAP public git repository ::
- wget http://leap.se/downloads/leap-client/zipball/latest
-
-Or, if you prefer, you can also get the code from LEAP public git repository ::
-
- git clone git://leap.se/leap_client
+ $ git clone git://leap.se/bitmask
Or from the github mirror ::
- git clone git://github.com/leapcode/leap_client.git
+ $ git clone git://github.com/leapcode/bitmask.git
-Once you have grabbed a copy of the sources for whatever mean, you can install it into your site-packages::
+Once you have grabbed a copy of the sources, you can install it into your site-packages easily ::
$ pyton setup.py install
+
diff --git a/docs/user/intro.rst b/docs/user/intro.rst
index abb6d487..22ad9356 100644
--- a/docs/user/intro.rst
+++ b/docs/user/intro.rst
@@ -3,15 +3,15 @@
Introduction
============
-The LEAP Client
----------------
+Bitmask
+-------
.. if yoy change this, change it also in the index.rst
-The **LEAP Client** is a :ref:`GPL3 Licensed <gpl3>` multiplatform client, written in python using PyQt4, that supports the features offered by :ref:`the LEAP Platform <leapplatform>`. Currently is being tested on Linux, support for OSX and Windows will come soon.
+**Bitmask** is a :ref:`GPL3 Licensed <gpl3>` multiplatform client, written in python using PySide, that supports the features offered by :ref:`the LEAP Platform <leapplatform>`. Currently is being tested on Linux, support for OSX and Windows will come soon.
Features
^^^^^^^^
-The LEAP Client allows to easily secure communications.
+Bitmask allows to easily secure communications.
- Provider selection
- User registration
@@ -78,22 +78,22 @@ GPLv3 License
.. image:: gpl.*
-The LEAP Client is released under the terms of the `GNU GPL version 3`_ or later.
+Bitmask is released under the terms of the `GNU GPL version 3`_ or later.
::
- The LEAP Client is free software: you can redistribute it and/or modify
+ Bitmask 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.
- The LEAP Client is distributed in the hope that it will be useful,
+ Bitmask 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 the LEAP Client. If not, see http://www.gnu.org/licenses/.
+ along with Bitmask. If not, see http://www.gnu.org/licenses/.
.. _`GNU GPL version 3`: http://www.gnu.org/licenses/gpl.txt
diff --git a/docs/user/running.rst b/docs/user/running.rst
index 35ec1f6d..da83e9ef 100644
--- a/docs/user/running.rst
+++ b/docs/user/running.rst
@@ -3,38 +3,42 @@
Running
==================
-This document dovers how to launch the LEAP Client.
+This document covers how to launch Bitmask.
-Launching the client
---------------------
-After a successful installation, there should be a launcher called leap-client somewhere in your path::
+Launching Bitmask
+-----------------
+After a successful installation, there should be a launcher called `bitmask` somewhere in your path::
- % leap-client
+ % bitmask
+The first time you launch it, it should launch the first run wizard that will guide you through the setup of the LEAP Services.
+
+.. note::
+
+ You will need to enter a valid test provider running the LEAP Platform. You can use the LEAP test service, *https://bitmask.net*
.. _debugmode:
Debug mode
----------
-If you are happy having lots of output in your terminal, you will like to know that you can run the client in debug mode::
+If you are happy having lots of output in your terminal, you will like to know that you can run bitmask in debug mode::
- $ leap-client --debug
+ $ bitmask --debug
If you ask for it, you can also have all that debug info in a beautiful file ready to be attached to your bug reports::
- $ leap-client --debug --logfile /tmp/leap.log
+ $ bitmask --debug --logfile /tmp/leap.log
-.. warning::
- the following is broken since it will clutter your stdout with all the commands sent to the management interface.
+.. warning
+.. the following is broken since it will clutter your stdout with all the commands sent to the management interface.
See bug #1232
-If you want to increment the level of verbosity passed to openvpn, you can do::
-
-
- $ leap-client --openvpn-verbosity 4
+.. not working...
+.. If you want to increment the level of verbosity passed to openvpn, you can do::
+.. $ bitmask --openvpn-verbosity 4
Options
------------
To see all the available command line options::
- $ leap-client --help
+ $ bitmask --help
diff --git a/ez_setup.py b/ez_setup.py
deleted file mode 100644
index b74adc06..00000000
--- a/ez_setup.py
+++ /dev/null
@@ -1,284 +0,0 @@
-#!python
-"""Bootstrap setuptools installation
-
-If you want to use setuptools in your package's setup.py, just include this
-file in the same directory with it, and add this to the top of your setup.py::
-
- from ez_setup import use_setuptools
- use_setuptools()
-
-If you want to require a specific version of setuptools, set a download
-mirror, or use an alternate download directory, you can do so by supplying
-the appropriate options to ``use_setuptools()``.
-
-This file can also be run as a script to install or upgrade setuptools.
-"""
-import sys
-DEFAULT_VERSION = "0.6c11"
-DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
-
-md5_data = {
- 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
- 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
- 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
- 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
- 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
- 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
- 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
- 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
- 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
- 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
- 'setuptools-0.6c10-py2.3.egg': 'ce1e2ab5d3a0256456d9fc13800a7090',
- 'setuptools-0.6c10-py2.4.egg': '57d6d9d6e9b80772c59a53a8433a5dd4',
- 'setuptools-0.6c10-py2.5.egg': 'de46ac8b1c97c895572e5e8596aeb8c7',
- 'setuptools-0.6c10-py2.6.egg': '58ea40aef06da02ce641495523a0b7f5',
- 'setuptools-0.6c11-py2.3.egg': '2baeac6e13d414a9d28e7ba5b5a596de',
- 'setuptools-0.6c11-py2.4.egg': 'bd639f9b0eac4c42497034dec2ec0c2b',
- 'setuptools-0.6c11-py2.5.egg': '64c94f3bf7a72a13ec83e0b24f2749b2',
- 'setuptools-0.6c11-py2.6.egg': 'bfa92100bd772d5a213eedd356d64086',
- 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
- 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
- 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
- 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
- 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
- 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
- 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
- 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
- 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
- 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
- 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
- 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
- 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
- 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
- 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
- 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
- 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
- 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
- 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
- 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
- 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
- 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
- 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
- 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
-}
-
-import sys, os
-try: from hashlib import md5
-except ImportError: from md5 import md5
-
-def _validate_md5(egg_name, data):
- if egg_name in md5_data:
- digest = md5(data).hexdigest()
- if digest != md5_data[egg_name]:
- print >>sys.stderr, (
- "md5 validation of %s failed! (Possible download problem?)"
- % egg_name
- )
- sys.exit(2)
- return data
-
-def use_setuptools(
- version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
- download_delay=15
-):
- """Automatically find/download setuptools and make it available on sys.path
-
- `version` should be a valid setuptools version number that is available
- as an egg for download under the `download_base` URL (which should end with
- a '/'). `to_dir` is the directory where setuptools will be downloaded, if
- it is not already available. If `download_delay` is specified, it should
- be the number of seconds that will be paused before initiating a download,
- should one be required. If an older version of setuptools is installed,
- this routine will print a message to ``sys.stderr`` and raise SystemExit in
- an attempt to abort the calling script.
- """
- was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
- def do_download():
- egg = download_setuptools(version, download_base, to_dir, download_delay)
- sys.path.insert(0, egg)
- import setuptools; setuptools.bootstrap_install_from = egg
- try:
- import pkg_resources
- except ImportError:
- return do_download()
- try:
- pkg_resources.require("setuptools>="+version); return
- except pkg_resources.VersionConflict, e:
- if was_imported:
- print >>sys.stderr, (
- "The required version of setuptools (>=%s) is not available, and\n"
- "can't be installed while this script is running. Please install\n"
- " a more recent version first, using 'easy_install -U setuptools'."
- "\n\n(Currently using %r)"
- ) % (version, e.args[0])
- sys.exit(2)
- except pkg_resources.DistributionNotFound:
- pass
-
- del pkg_resources, sys.modules['pkg_resources'] # reload ok
- return do_download()
-
-def download_setuptools(
- version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
- delay = 15
-):
- """Download setuptools from a specified location and return its filename
-
- `version` should be a valid setuptools version number that is available
- as an egg for download under the `download_base` URL (which should end
- with a '/'). `to_dir` is the directory where the egg will be downloaded.
- `delay` is the number of seconds to pause before an actual download attempt.
- """
- import urllib2, shutil
- egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
- url = download_base + egg_name
- saveto = os.path.join(to_dir, egg_name)
- src = dst = None
- if not os.path.exists(saveto): # Avoid repeated downloads
- try:
- from distutils import log
- if delay:
- log.warn("""
----------------------------------------------------------------------------
-This script requires setuptools version %s to run (even to display
-help). I will attempt to download it for you (from
-%s), but
-you may need to enable firewall access for this script first.
-I will start the download in %d seconds.
-
-(Note: if this machine does not have network access, please obtain the file
-
- %s
-
-and place it in this directory before rerunning this script.)
----------------------------------------------------------------------------""",
- version, download_base, delay, url
- ); from time import sleep; sleep(delay)
- log.warn("Downloading %s", url)
- src = urllib2.urlopen(url)
- # Read/write all in one block, so we don't create a corrupt file
- # if the download is interrupted.
- data = _validate_md5(egg_name, src.read())
- dst = open(saveto,"wb"); dst.write(data)
- finally:
- if src: src.close()
- if dst: dst.close()
- return os.path.realpath(saveto)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-def main(argv, version=DEFAULT_VERSION):
- """Install or upgrade setuptools and EasyInstall"""
- try:
- import setuptools
- except ImportError:
- egg = None
- try:
- egg = download_setuptools(version, delay=0)
- sys.path.insert(0,egg)
- from setuptools.command.easy_install import main
- return main(list(argv)+[egg]) # we're done here
- finally:
- if egg and os.path.exists(egg):
- os.unlink(egg)
- else:
- if setuptools.__version__ == '0.0.1':
- print >>sys.stderr, (
- "You have an obsolete version of setuptools installed. Please\n"
- "remove it from your system entirely before rerunning this script."
- )
- sys.exit(2)
-
- req = "setuptools>="+version
- import pkg_resources
- try:
- pkg_resources.require(req)
- except pkg_resources.VersionConflict:
- try:
- from setuptools.command.easy_install import main
- except ImportError:
- from easy_install import main
- main(list(argv)+[download_setuptools(delay=0)])
- sys.exit(0) # try to force an exit
- else:
- if argv:
- from setuptools.command.easy_install import main
- main(argv)
- else:
- print "Setuptools version",version,"or greater has been installed."
- print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
-
-def update_md5(filenames):
- """Update our built-in md5 registry"""
-
- import re
-
- for name in filenames:
- base = os.path.basename(name)
- f = open(name,'rb')
- md5_data[base] = md5(f.read()).hexdigest()
- f.close()
-
- data = [" %r: %r,\n" % it for it in md5_data.items()]
- data.sort()
- repl = "".join(data)
-
- import inspect
- srcfile = inspect.getsourcefile(sys.modules[__name__])
- f = open(srcfile, 'rb'); src = f.read(); f.close()
-
- match = re.search("\nmd5_data = {\n([^}]+)}", src)
- if not match:
- print >>sys.stderr, "Internal error!"
- sys.exit(2)
-
- src = src[:match.start(1)] + repl + src[match.end(1):]
- f = open(srcfile,'w')
- f.write(src)
- f.close()
-
-
-if __name__=='__main__':
- if len(sys.argv)>2 and sys.argv[1]=='--md5update':
- update_md5(sys.argv[2:])
- else:
- main(sys.argv[1:])
-
-
-
-
-
-
diff --git a/openvpn/README b/openvpn/README
new file mode 100644
index 00000000..bf2205c2
--- /dev/null
+++ b/openvpn/README
@@ -0,0 +1,6 @@
+OpenVPN binary, build scripts
+Works using a GCC minGW32 cross-compiler on Debian/Ubuntu
+Produces a working MS Windows executable
+openvpn.exe: PE32 executable (DLL) (console) Intel 80386, for MS Windows
+goes smooth for the 99%, might still need some slapping the flags around now and then
+ -jrml
diff --git a/openvpn/Sources b/openvpn/Sources
new file mode 100644
index 00000000..e2fe7bb3
--- /dev/null
+++ b/openvpn/Sources
@@ -0,0 +1,4 @@
+lzo -2.06 .tar.gz
+opensc -0.12.2 .tar.gz
+openssl -1.0.1c .tar.gz
+polarssl -1.1.4 .tgz
diff --git a/openvpn/build.zsh b/openvpn/build.zsh
new file mode 100755
index 00000000..b36717c1
--- /dev/null
+++ b/openvpn/build.zsh
@@ -0,0 +1,191 @@
+#!/bin/zsh
+#
+# Copyright (C) 2012 Denis Roio <jaromil@dyne.org>
+#
+# This source code is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This source code 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.
+# Please refer to the GNU Public License for more details.
+#
+# You should have received a copy of the GNU Public License along with
+# this source code; if not, write to:
+# Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+
+REPO="http://files.dyne.org/leap/openvpn/sources"
+TOPSRC=`pwd`
+QUIET=0
+DEBUG=0
+
+
+autoload colors; colors
+# standard output message routines
+# it's always useful to wrap them, in case we change behaviour later
+notice() { if [[ $QUIET == 0 ]]; then print "$fg_bold[green][*]$fg_no_bold[default] $1" >&2; fi }
+error() { if [[ $QUIET == 0 ]]; then print "$fg[red][!]$fg[default] $1" >&2; fi }
+func() { if [[ $DEBUG == 1 ]]; then print "$fg[blue][D]$fg[default] $1" >&2; fi }
+act() {
+ if [[ $QUIET == 0 ]]; then
+ if [ "$1" = "-n" ]; then
+ print -n "$fg_bold[white] . $fg_no_bold[default] $2" >&2;
+ else
+ print "$fg_bold[white] . $fg_no_bold[default] $1" >&2;
+ fi
+ fi
+}
+
+{ test "$1" = "clean" } && {
+ notice "Cleaning up all build in ${TOPSRC}"
+ for src in `cat Sources | awk '
+/^#/ {next}
+/^./ { print $1 }'`; do
+ { test "$src" != "" } && { rm -rf "${src}" }
+ done
+ act "Done."
+ return 0
+}
+
+os="`uname -s`"
+target="$1"
+notice "OpenVPN build on $os for $target in ${TOPSRC}"
+
+prepare_sources() {
+ notice "Preparing sources"
+ # look for a file names "Sources", download and decompress entries
+ # format of file: name version compression (complete filename when merged)
+ { test -r Sources } || {
+ error "Sources not found, nothing to build here"
+ return 1
+ }
+ for src in `cat Sources | awk '
+/^#/ {next}
+/^./ { print $1 ";" $2 ";" $3 }'`; do
+ name="${src[(ws:;:)1]}"
+ ver="${src[(ws:;:)2]}"
+ arch="${src[(ws:;:)3]}"
+ file="${name}${ver}${arch}"
+ func "preparing source for ${name}${ver}"
+
+ { test "$1" != "" } && {
+ test "$1" != "$name" } && {
+ continue }
+
+ # download the file
+ { test -r ${file} } || {
+ act "downloading ${file}"
+ curl ${REPO}/${file} -o ${file}
+ }
+ # decompress the file
+ { test -r ${name} } || {
+ act "decompressing ${name}"
+ case $arch in
+ ## BARE SOURCE
+ .tar.gz) tar xfz ${file}; mv ${name}${ver} ${name} ;;
+ .tar.bz2) tar xfj ${file}; mv ${name}${ver} ${name} ;;
+ .tgz) tar xfz ${file}; mv ${name}${ver} ${name} ;;
+ *) error "compression not supported: $arch"
+ esac
+ }
+ act "${name} source ready"
+ done
+}
+
+act "Downloading sources"
+
+# git clone latest openvpn
+{ test -r openvpn } || { git clone https://github.com/OpenVPN/openvpn.git }
+
+case "$os" in
+ Darwin)
+ prepare_sources lzo
+ prepare_sources polarssl
+ ;;
+ Linux) # Cross-compile for Win32
+ prepare_sources lzo
+ prepare_sources opensc
+ prepare_sources openssl
+ # tap windows
+ { test -r tap-windows } || { git clone https://github.com/OpenVPN/tap-windows.git }
+ ;;
+esac
+
+notice "Sources ready, now compiling..."
+LOG="`pwd`/build.log"; touch ${LOG}
+act "logs saved in build.log"
+
+case "$target" in
+ osx)
+ { test -r polarssl/library/libpolarssl.a } || {
+ act "building PolarSSL..."
+ pushd polarssl
+ CC=clang cmake . >> ${LOG}
+ make -C library clean
+ cat CMakeCache.txt | awk '
+/^CMAKE_C_COMPILER/ { print "CMAKE_C_COMPILER:FILEPATH=/usr/bin/clang"; next }
+/^CMAKE_BUILD_TYPE/ { print $1 "Release"; next }
+/^CMAKE_C_FLAGS:STRING/ { print "CMAKE_C_FLAGS:STRING=-arch x86_64 -arch i386"; next }
+{ print $0 }
+' > CMakeCache.leap
+ cp CMakeCache.leap CMakeCache.txt
+ make -C library >> ${LOG}
+ popd
+ act "done."
+ }
+
+ act "building OpenVPN"
+ pushd openvpn
+ CC=clang CFLAGS="-arch x86_64 -arch i386" \
+ LZO_LIBS="/opt/local/lib/liblzo2.a" LZO_CFLAGS="-I/opt/local/include" \
+ POLARSSL_CFLAGS="-I${TOPSRC}/polarssl/include" \
+ POLARSSL_LIBS="${TOPSRC}/polarssl/library/libpolarssl.a" \
+ ./configure --with-crypto-library=polarssl >> ${LOG}
+ make src/openvpn/openvpn
+ popd
+ act "done."
+ ;;
+
+ win32)
+ { test -r lzo/src/liblzo2.la } || { pushd lzo
+ act "building LZO lib"
+ ./configure --host=i586-mingw32msvc >> ${LOG}
+ make >> ${LOG}; popd }
+ # openssl
+ { test -r openssl/libssl.a } || {
+ act "building OpenSSL lib"
+ pushd openssl
+ ./Configure --cross-compile-prefix=i586-mingw32msvc- mingw >> ${LOG}
+ make ${LOG}; popd }
+
+ pushd openvpn
+ act "building latest OpenVPN"
+ { test -r configure } || {
+ sed -i -e 's/-municode//' src/openvpn/Makefile.am
+ autoreconf -i >> ${LOG}
+ }
+ CFLAGS="-I/usr/i586-mingw32msvc/include/ddk -D_WIN32_WINNT=0x0501" \
+ LZO_LIBS="${TOPSRC}/lzo/src/liblzo2.la" \
+ LZO_CFLAGS="-I${TOPSRC}/lzo/include" \
+ TAP_CFLAGS="-I${TOPSRC}/tap-windows/src" \
+ OPENSSL_SSL_CFLAGS="-I${TOPSRC}/openssl/include" \
+ OPENSSL_CRYPTO_CFLAGS="-I${TOPSRC}/openssl/crypto" \
+ OPENSSL_SSL_LIBS="${TOPSRC}/openssl/libssl.a" \
+ OPENSSL_CRYPTO_LIBS="${TOPSRC}/openssl/libcrypto.a" \
+ ./configure --host=i586-mingw32msvc >> ${LOG}
+ make >> ${LOG}
+ popd
+
+ act "If OpenVPN build reports a final error on linkage, it might be due to a libtool bug"
+ act "(something like undefined reference to _WinMain@16)"
+ act "You need to go inside openvpn/src/openvpn and issue the last compile line manually"
+ act "adding an flat '-shared' at the end of it, then do 'cp .libs/openvpn.exe .'"
+ act "Happy hacking."
+ ;;
+ *)
+ error "Unknown target: $target"
+ ;;
+esac
diff --git a/pkg/branding/__init__.py b/pkg/branding/__init__.py
deleted file mode 100644
index 0bd6befb..00000000
--- a/pkg/branding/__init__.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from .config import APP_BASE_NAME, APP_PREFIX, BRANDED_BUILD, BRANDED_OPTS
-
-
-def get_name():
- if BRANDED_BUILD is True:
- return APP_PREFIX + BRANDED_OPTS.get('short_name', 'name_unknown')
- else:
- return APP_BASE_NAME
-
-
-def get_shortname():
- if BRANDED_BUILD is True:
- return BRANDED_OPTS.get('short_name', 'name_unknown')
-
-__all__ = ['get_name']
diff --git a/pkg/branding/config.py b/pkg/branding/config.py
deleted file mode 100644
index bcacc3bc..00000000
--- a/pkg/branding/config.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# Configuration file for branding
-
-BRANDED_BUILD = False
-
-APP_BASE_NAME = "leap-client"
-APP_PREFIX = "%s-" % APP_BASE_NAME
-
-BRANDED_OPTS = {
- 'short_name': "",
- 'provider_domain': "",
- 'provider_ca_path': ""}
diff --git a/pkg/dev-reqs.pip b/pkg/dev-reqs.pip
deleted file mode 100644
index 44799a26..00000000
--- a/pkg/dev-reqs.pip
+++ /dev/null
@@ -1,4 +0,0 @@
-ipython
-ipdb
-pdb4qt
-pygeoip
diff --git a/pkg/distribute_setup.py b/pkg/distribute_setup.py
deleted file mode 100644
index 8f5b0637..00000000
--- a/pkg/distribute_setup.py
+++ /dev/null
@@ -1,515 +0,0 @@
-#!python
-"""Bootstrap distribute installation
-
-If you want to use setuptools in your package's setup.py, just include this
-file in the same directory with it, and add this to the top of your setup.py::
-
- from distribute_setup import use_setuptools
- use_setuptools()
-
-If you want to require a specific version of setuptools, set a download
-mirror, or use an alternate download directory, you can do so by supplying
-the appropriate options to ``use_setuptools()``.
-
-This file can also be run as a script to install or upgrade setuptools.
-"""
-import os
-import sys
-import time
-import fnmatch
-import tempfile
-import tarfile
-from distutils import log
-
-try:
- from site import USER_SITE
-except ImportError:
- USER_SITE = None
-
-try:
- import subprocess
-
- def _python_cmd(*args):
- args = (sys.executable,) + args
- return subprocess.call(args) == 0
-
-except ImportError:
- # will be used for python 2.3
- def _python_cmd(*args):
- args = (sys.executable,) + args
- # quoting arguments if windows
- if sys.platform == 'win32':
- def quote(arg):
- if ' ' in arg:
- return '"%s"' % arg
- return arg
- args = [quote(arg) for arg in args]
- return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
-
-DEFAULT_VERSION = "0.6.28"
-DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
-SETUPTOOLS_FAKED_VERSION = "0.6c11"
-
-SETUPTOOLS_PKG_INFO = """\
-Metadata-Version: 1.0
-Name: setuptools
-Version: %s
-Summary: xxxx
-Home-page: xxx
-Author: xxx
-Author-email: xxx
-License: xxx
-Description: xxx
-""" % SETUPTOOLS_FAKED_VERSION
-
-
-def _install(tarball, install_args=()):
- # extracting the tarball
- tmpdir = tempfile.mkdtemp()
- log.warn('Extracting in %s', tmpdir)
- old_wd = os.getcwd()
- try:
- os.chdir(tmpdir)
- tar = tarfile.open(tarball)
- _extractall(tar)
- tar.close()
-
- # going in the directory
- subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
- os.chdir(subdir)
- log.warn('Now working in %s', subdir)
-
- # installing
- log.warn('Installing Distribute')
- if not _python_cmd('setup.py', 'install', *install_args):
- log.warn('Something went wrong during the installation.')
- log.warn('See the error message above.')
- finally:
- os.chdir(old_wd)
-
-
-def _build_egg(egg, tarball, to_dir):
- # extracting the tarball
- tmpdir = tempfile.mkdtemp()
- log.warn('Extracting in %s', tmpdir)
- old_wd = os.getcwd()
- try:
- os.chdir(tmpdir)
- tar = tarfile.open(tarball)
- _extractall(tar)
- tar.close()
-
- # going in the directory
- subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
- os.chdir(subdir)
- log.warn('Now working in %s', subdir)
-
- # building an egg
- log.warn('Building a Distribute egg in %s', to_dir)
- _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
-
- finally:
- os.chdir(old_wd)
- # returning the result
- log.warn(egg)
- if not os.path.exists(egg):
- raise IOError('Could not build the egg.')
-
-
-def _do_download(version, download_base, to_dir, download_delay):
- egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
- % (version, sys.version_info[0], sys.version_info[1]))
- if not os.path.exists(egg):
- tarball = download_setuptools(version, download_base,
- to_dir, download_delay)
- _build_egg(egg, tarball, to_dir)
- sys.path.insert(0, egg)
- import setuptools
- setuptools.bootstrap_install_from = egg
-
-
-def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
- to_dir=os.curdir, download_delay=15, no_fake=True):
- # making sure we use the absolute path
- to_dir = os.path.abspath(to_dir)
- was_imported = 'pkg_resources' in sys.modules or \
- 'setuptools' in sys.modules
- try:
- try:
- import pkg_resources
- if not hasattr(pkg_resources, '_distribute'):
- if not no_fake:
- _fake_setuptools()
- raise ImportError
- except ImportError:
- return _do_download(version, download_base, to_dir, download_delay)
- try:
- pkg_resources.require("distribute>=" + version)
- return
- except pkg_resources.VersionConflict:
- e = sys.exc_info()[1]
- if was_imported:
- sys.stderr.write(
- "The required version of distribute (>=%s) is not available,\n"
- "and can't be installed while this script is running. Please\n"
- "install a more recent version first, using\n"
- "'easy_install -U distribute'."
- "\n\n(Currently using %r)\n" % (version, e.args[0]))
- sys.exit(2)
- else:
- del pkg_resources, sys.modules['pkg_resources'] # reload ok
- return _do_download(version, download_base, to_dir,
- download_delay)
- except pkg_resources.DistributionNotFound:
- return _do_download(version, download_base, to_dir,
- download_delay)
- finally:
- if not no_fake:
- _create_fake_setuptools_pkg_info(to_dir)
-
-
-def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
- to_dir=os.curdir, delay=15):
- """Download distribute from a specified location and return its filename
-
- `version` should be a valid distribute version number that is available
- as an egg for download under the `download_base` URL (which should end
- with a '/'). `to_dir` is the directory where the egg will be downloaded.
- `delay` is the number of seconds to pause before an actual download
- attempt.
- """
- # making sure we use the absolute path
- to_dir = os.path.abspath(to_dir)
- try:
- from urllib.request import urlopen
- except ImportError:
- from urllib2 import urlopen
- tgz_name = "distribute-%s.tar.gz" % version
- url = download_base + tgz_name
- saveto = os.path.join(to_dir, tgz_name)
- src = dst = None
- if not os.path.exists(saveto): # Avoid repeated downloads
- try:
- log.warn("Downloading %s", url)
- src = urlopen(url)
- # Read/write all in one block, so we don't create a corrupt file
- # if the download is interrupted.
- data = src.read()
- dst = open(saveto, "wb")
- dst.write(data)
- finally:
- if src:
- src.close()
- if dst:
- dst.close()
- return os.path.realpath(saveto)
-
-
-def _no_sandbox(function):
- def __no_sandbox(*args, **kw):
- try:
- from setuptools.sandbox import DirectorySandbox
- if not hasattr(DirectorySandbox, '_old'):
- def violation(*args):
- pass
- DirectorySandbox._old = DirectorySandbox._violation
- DirectorySandbox._violation = violation
- patched = True
- else:
- patched = False
- except ImportError:
- patched = False
-
- try:
- return function(*args, **kw)
- finally:
- if patched:
- DirectorySandbox._violation = DirectorySandbox._old
- del DirectorySandbox._old
-
- return __no_sandbox
-
-
-def _patch_file(path, content):
- """Will backup the file then patch it"""
- existing_content = open(path).read()
- if existing_content == content:
- # already patched
- log.warn('Already patched.')
- return False
- log.warn('Patching...')
- _rename_path(path)
- f = open(path, 'w')
- try:
- f.write(content)
- finally:
- f.close()
- return True
-
-_patch_file = _no_sandbox(_patch_file)
-
-
-def _same_content(path, content):
- return open(path).read() == content
-
-
-def _rename_path(path):
- new_name = path + '.OLD.%s' % time.time()
- log.warn('Renaming %s into %s', path, new_name)
- os.rename(path, new_name)
- return new_name
-
-
-def _remove_flat_installation(placeholder):
- if not os.path.isdir(placeholder):
- log.warn('Unkown installation at %s', placeholder)
- return False
- found = False
- for file in os.listdir(placeholder):
- if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
- found = True
- break
- if not found:
- log.warn('Could not locate setuptools*.egg-info')
- return
-
- log.warn('Removing elements out of the way...')
- pkg_info = os.path.join(placeholder, file)
- if os.path.isdir(pkg_info):
- patched = _patch_egg_dir(pkg_info)
- else:
- patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
-
- if not patched:
- log.warn('%s already patched.', pkg_info)
- return False
- # now let's move the files out of the way
- for element in ('setuptools', 'pkg_resources.py', 'site.py'):
- element = os.path.join(placeholder, element)
- if os.path.exists(element):
- _rename_path(element)
- else:
- log.warn('Could not find the %s element of the '
- 'Setuptools distribution', element)
- return True
-
-_remove_flat_installation = _no_sandbox(_remove_flat_installation)
-
-
-def _after_install(dist):
- log.warn('After install bootstrap.')
- placeholder = dist.get_command_obj('install').install_purelib
- _create_fake_setuptools_pkg_info(placeholder)
-
-
-def _create_fake_setuptools_pkg_info(placeholder):
- if not placeholder or not os.path.exists(placeholder):
- log.warn('Could not find the install location')
- return
- pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
- setuptools_file = 'setuptools-%s-py%s.egg-info' % \
- (SETUPTOOLS_FAKED_VERSION, pyver)
- pkg_info = os.path.join(placeholder, setuptools_file)
- if os.path.exists(pkg_info):
- log.warn('%s already exists', pkg_info)
- return
-
- if not os.access(pkg_info, os.W_OK):
- log.warn("Don't have permissions to write %s, skipping", pkg_info)
-
- log.warn('Creating %s', pkg_info)
- f = open(pkg_info, 'w')
- try:
- f.write(SETUPTOOLS_PKG_INFO)
- finally:
- f.close()
-
- pth_file = os.path.join(placeholder, 'setuptools.pth')
- log.warn('Creating %s', pth_file)
- f = open(pth_file, 'w')
- try:
- f.write(os.path.join(os.curdir, setuptools_file))
- finally:
- f.close()
-
-_create_fake_setuptools_pkg_info = _no_sandbox(
- _create_fake_setuptools_pkg_info
-)
-
-
-def _patch_egg_dir(path):
- # let's check if it's already patched
- pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
- if os.path.exists(pkg_info):
- if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
- log.warn('%s already patched.', pkg_info)
- return False
- _rename_path(path)
- os.mkdir(path)
- os.mkdir(os.path.join(path, 'EGG-INFO'))
- pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
- f = open(pkg_info, 'w')
- try:
- f.write(SETUPTOOLS_PKG_INFO)
- finally:
- f.close()
- return True
-
-_patch_egg_dir = _no_sandbox(_patch_egg_dir)
-
-
-def _before_install():
- log.warn('Before install bootstrap.')
- _fake_setuptools()
-
-
-def _under_prefix(location):
- if 'install' not in sys.argv:
- return True
- args = sys.argv[sys.argv.index('install') + 1:]
- for index, arg in enumerate(args):
- for option in ('--root', '--prefix'):
- if arg.startswith('%s=' % option):
- top_dir = arg.split('root=')[-1]
- return location.startswith(top_dir)
- elif arg == option:
- if len(args) > index:
- top_dir = args[index + 1]
- return location.startswith(top_dir)
- if arg == '--user' and USER_SITE is not None:
- return location.startswith(USER_SITE)
- return True
-
-
-def _fake_setuptools():
- log.warn('Scanning installed packages')
- try:
- import pkg_resources
- except ImportError:
- # we're cool
- log.warn('Setuptools or Distribute does not seem to be installed.')
- return
- ws = pkg_resources.working_set
- try:
- setuptools_dist = ws.find(
- pkg_resources.Requirement.parse('setuptools', replacement=False)
- )
- except TypeError:
- # old distribute API
- setuptools_dist = ws.find(
- pkg_resources.Requirement.parse('setuptools')
- )
-
- if setuptools_dist is None:
- log.warn('No setuptools distribution found')
- return
- # detecting if it was already faked
- setuptools_location = setuptools_dist.location
- log.warn('Setuptools installation detected at %s', setuptools_location)
-
- # if --root or --preix was provided, and if
- # setuptools is not located in them, we don't patch it
- if not _under_prefix(setuptools_location):
- log.warn('Not patching, --root or --prefix is installing Distribute'
- ' in another location')
- return
-
- # let's see if its an egg
- if not setuptools_location.endswith('.egg'):
- log.warn('Non-egg installation')
- res = _remove_flat_installation(setuptools_location)
- if not res:
- return
- else:
- log.warn('Egg installation')
- pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
- if (os.path.exists(pkg_info) and
- _same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
- log.warn('Already patched.')
- return
- log.warn('Patching...')
- # let's create a fake egg replacing setuptools one
- res = _patch_egg_dir(setuptools_location)
- if not res:
- return
- log.warn('Patched done.')
- _relaunch()
-
-
-def _relaunch():
- log.warn('Relaunching...')
- # we have to relaunch the process
- # pip marker to avoid a relaunch bug
- _cmd = ['-c', 'install', '--single-version-externally-managed']
- if sys.argv[:3] == _cmd:
- sys.argv[0] = 'setup.py'
- args = [sys.executable] + sys.argv
- sys.exit(subprocess.call(args))
-
-
-def _extractall(self, path=".", members=None):
- """Extract all members from the archive to the current working
- directory and set owner, modification time and permissions on
- directories afterwards. `path' specifies a different directory
- to extract to. `members' is optional and must be a subset of the
- list returned by getmembers().
- """
- import copy
- import operator
- from tarfile import ExtractError
- directories = []
-
- if members is None:
- members = self
-
- for tarinfo in members:
- if tarinfo.isdir():
- # Extract directories with a safe mode.
- directories.append(tarinfo)
- tarinfo = copy.copy(tarinfo)
- tarinfo.mode = 448 # decimal for oct 0700
- self.extract(tarinfo, path)
-
- # Reverse sort directories.
- if sys.version_info < (2, 4):
- def sorter(dir1, dir2):
- return cmp(dir1.name, dir2.name)
- directories.sort(sorter)
- directories.reverse()
- else:
- directories.sort(key=operator.attrgetter('name'), reverse=True)
-
- # Set correct owner, mtime and filemode on directories.
- for tarinfo in directories:
- dirpath = os.path.join(path, tarinfo.name)
- try:
- self.chown(tarinfo, dirpath)
- self.utime(tarinfo, dirpath)
- self.chmod(tarinfo, dirpath)
- except ExtractError:
- e = sys.exc_info()[1]
- if self.errorlevel > 1:
- raise
- else:
- self._dbg(1, "tarfile: %s" % e)
-
-
-def _build_install_args(argv):
- install_args = []
- user_install = '--user' in argv
- if user_install and sys.version_info < (2, 6):
- log.warn("--user requires Python 2.6 or later")
- raise SystemExit(1)
- if user_install:
- install_args.append('--user')
- return install_args
-
-
-def main(argv, version=DEFAULT_VERSION):
- """Install or upgrade setuptools and EasyInstall"""
- tarball = download_setuptools()
- _install(tarball, _build_install_args(argv))
-
-
-if __name__ == '__main__':
- main(sys.argv[1:])
diff --git a/pkg/install_pyqt.sh b/pkg/install_pyqt.sh
deleted file mode 100755
index d6739816..00000000
--- a/pkg/install_pyqt.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/sh
-pip install sip # fails
-cd build/sip
-python configure.py
-make && make install
-cd ../..
-pip install PyQt # fails
-cd build/PyQt
-python configure.py
-make && make install
diff --git a/pkg/install_venv.py b/pkg/install_venv.py
index 17dfb984..80bc5d4b 100644
--- a/pkg/install_venv.py
+++ b/pkg/install_venv.py
@@ -30,7 +30,7 @@ import sys
ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
VENV = os.path.join(ROOT, '.venv')
PIP_REQUIRES = os.path.join(ROOT, 'pkg', 'requirements.pip')
-TEST_REQUIRES = os.path.join(ROOT, 'pkg', 'test-requirements.pip')
+TEST_REQUIRES = os.path.join(ROOT, 'pkg', 'requirements-testing.pip')
PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
@@ -167,7 +167,7 @@ def create_virtualenv(venv=VENV, no_site_packages=True):
"""
print 'Creating venv...',
if no_site_packages:
- #setuptools and virtualenv don't play nicely together,
+ #setuptools and virtualenv don't play nicely together,
#so we create the virtualenv with the distribute package instead.
#See: view-source:http://pypi.python.org/pypi/distribute
run_command(['virtualenv', '-q', '--distribute', '--no-site-packages', VENV])
diff --git a/pkg/osx/Makefile b/pkg/osx/Makefile
index f2520fcf..15dfb810 100644
--- a/pkg/osx/Makefile
+++ b/pkg/osx/Makefile
@@ -1,12 +1,10 @@
-#WARNING: You need to run this with an activated VIRTUALENV.
-
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 : dist trim installer dmg
+pkg : check-env dist tuntap installer dmg
dist :
~/pyinstaller/pyinstaller.py -w -s leap-client.spec
@@ -16,20 +14,22 @@ dist :
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)
- #XXX should build tuntap extensions ourselves
+ # build tuntaposx kernel extension
mkdir $(INSTR)/StartupItems
mkdir $(INSTR)/Extensions
- cp -r /opt/local/Library/StartupItems/tun $(INSTR)/StartupItems
- cp -r /opt/local/Library/StartupItems/tap $(INSTR)/StartupItems
- cp -r /opt/local/Library/Extensions/tun.kext $(INSTR)/Extensions
- cp -r /opt/local/Library/Extensions/tap.kext $(INSTR)/Extensions
- #copy the binary that we have previously built
- #XXX not building it yet...
+ 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)
@@ -42,5 +42,10 @@ 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 48d96ffb..03aac4f2 100644
--- a/pkg/osx/README.rst
+++ b/pkg/osx/README.rst
@@ -9,37 +9,29 @@ basically you need this to setup your environment:
# install xcode and macports
# port -v selfupdate
# port install python26
-# port install python_select # unneeded?
+# port install python_select
+# port select python python26
# port install py26-pyqt4
-# port install py26-twisted
# port install py26-pip
# port install py26-virtualenv
# port install git-core
-# port install gnutls
# port install platypus
+# port install upx
Requirements
============
-pyinstaller (in ~/pyinstaller)
+pyinstaller
+-----------
+Expected in ~/pyinstaller
+
+You need the development version.
+Tested with: 2.0.373
+
platypus (tested with latest macports)
... + install environment as usual,
inside virtualenv.
-.. note:: there is something missing here, about troubles building gnutls extension,
- I think I ended by symlinking global install via macports.
-
-Pyinstaller fix for sip api
----------------------------
-We need a workaround for setting the right sip api.
-Paste this in the top of pyinstaller/support/rthooks/pyi_rth_qt4plugins.py::
-
- import sip
- sip.setapi('QString', 2)
- sip.setapi('QVariant', 2)
-
-See www.pyinstaller.org/wiki/Recipe/PyQtChangeApiVersion.
-
Building the package
====================
diff --git a/pkg/osx/build_tuntaposx b/pkg/osx/build_tuntaposx
new file mode 100755
index 00000000..10bb7c9c
--- /dev/null
+++ b/pkg/osx/build_tuntaposx
@@ -0,0 +1,45 @@
+#!/bin/zsh
+#
+# Copyright (C) 2012 ...
+#
+
+REPO="https://github.com/bbits/tuntaposx.git"
+
+autoload colors; colors
+# standard output message routines
+# it's always useful to wrap them, in case we change behaviour later
+notice() { if [[ $QUIET == 0 ]]; then print "$fg_bold[green][*]$fg_no_bold[default] $1" >&2; fi }
+error() { if [[ $QUIET == 0 ]]; then print "$fg[red][!]$fg[default] $1" >&2; fi }
+func() { if [[ $DEBUG == 1 ]]; then print "$fg[blue][D]$fg[default] $1" >&2; fi }
+act() {
+ if [[ $QUIET == 0 ]]; then
+ if [ "$1" = "-n" ]; then
+ print -n "$fg_bold[white] . $fg_no_bold[default] $2" >&2;
+ else
+ print "$fg_bold[white] . $fg_no_bold[default] $1" >&2;
+ fi
+ fi
+}
+
+{ test "$1" = "clean" } && {
+ notice "Cleaning up all tuntaposx build"
+ rm -rf tuntaposx
+ act "Done."
+ return 0
+}
+
+build_tuntap() {
+ test -d tuntaposx || git clone $REPO
+ notice "Cloning tuntaposx sources"
+ cd tuntaposx/tuntap
+ notice "Building tuntaposx"
+ make
+ mkdir -p ../../dist/tun.kext
+ cp -r tun.kext/* ../../dist/tun.kext
+ mkdir -p ../../dist/tuntaposx/StartupItems
+ cp -r startup_item/tun ../../dist/tuntaposx/StartupItems
+ cd ../..
+}
+
+act "Building tuntap"
+build_tuntap
diff --git a/pkg/osx/install/ProcessNetworkChanges.plist.template b/pkg/osx/install/ProcessNetworkChanges.plist.template
index faea8dee..eaf54fcf 100644
--- a/pkg/osx/install/ProcessNetworkChanges.plist.template
+++ b/pkg/osx/install/ProcessNetworkChanges.plist.template
@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>Label</key>
- <string>net.tunnelblick.openvpn.process-network-changes</string>
+ <string>se.leap.openvpn.process-network-changes</string>
<key>ProgramArguments</key>
<array>
<string>${DIR}/process-network-changes</string>
diff --git a/pkg/osx/install/client.down.sh b/pkg/osx/install/client.down.sh
index 47f00ed7..52ba4de6 100755
--- a/pkg/osx/install/client.down.sh
+++ b/pkg/osx/install/client.down.sh
@@ -2,7 +2,8 @@
# Note: must be bash; uses bash-specific tricks
#
# ******************************************************************************************************************
-# This Tunnelblick script does everything! It handles TUN and TAP interfaces,
+# 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
@@ -11,6 +12,7 @@
# 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
@@ -22,34 +24,34 @@ 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') *Tunnelblick $LOG_MESSAGE_COMMAND: WARNING: No existing OpenVPN DNS configuration found; not tearing down anything; exiting."
+ 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 Tunnelblick options out of the argument list
+# 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
-TUNNELBLICK_CONFIG="$(/usr/sbin/scutil <<-EOF
+LEAPCLIENT_CONFIG="$(/usr/sbin/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')"
-PSID="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*Service :' | sed -e 's/^.*: //g')"
-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')"
-bRouteGatewayIsDhcp="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*RouteGatewayIsDhcp :' | sed -e 's/^.*: //g')"
+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') *Tunnelblick $LOG_MESSAGE_COMMAND: "${@} >> "${SCRIPT_LOG_FILE}"
+ echo "$(date '+%a %b %e %T %Y') *LEAP CLient $LOG_MESSAGE_COMMAND: "${@} >> "${SCRIPT_LOG_FILE}"
}
trim()
@@ -97,7 +99,7 @@ WINS_OLD="$(/usr/sbin/scutil <<-EOF
quit
EOF)"
TB_NO_SUCH_KEY="<dictionary> {
- TunnelblickNoSuchKey : true
+ LEAPClientNoSuchKey : true
}"
if [ "${DNS_OLD}" = "${TB_NO_SUCH_KEY}" ] ; then
diff --git a/pkg/osx/install/client.up.sh b/pkg/osx/install/client.up.sh
index fc7e341a..be9814c2 100755
--- a/pkg/osx/install/client.up.sh
+++ b/pkg/osx/install/client.up.sh
@@ -2,7 +2,8 @@
# Note: must be bash; uses bash-specific tricks
#
# ******************************************************************************************************************
-# This Tunnelblick script does everything! It handles TUN and TAP interfaces,
+# 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
@@ -11,6 +12,7 @@
# 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
@@ -19,7 +21,7 @@ export PATH="/bin:/sbin:/usr/sbin:/usr/bin"
# Process optional arguments (if any) for the script
# Each one begins with a "-"
-# They come from Tunnelblick, and come first, before the OpenVPN arguments
+# 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"
@@ -63,24 +65,25 @@ readonly ARG_MONITOR_NETWORK_CONFIGURATION ARG_RESTORE_ON_DNS_RESET ARG_RESTORE_
# 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/Tunnelblick/Users/"
+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/Tunnelblick/Configurations/$TBSUFFIX"
+ 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')"
-readonly SCRIPT_LOG_FILE="/Library/Application Support/Tunnelblick/Logs/${CONFIG_PATH_DASHES_SLASHES}.script.log"
+# 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="/Library/Application Support/Tunnelblick/LeaseWatch.plist"
+LEASEWATCHER_PLIST_PATH="/Users/$LEAPUSER/.config/leap/logs/LeaseWatch.plist"
readonly OSVER="$(sw_vers | grep 'ProductVersion:' | grep -o '10\.[0-9]*')"
@@ -92,7 +95,7 @@ bRouteGatewayIsDhcp="false"
readonly LOG_MESSAGE_COMMAND=$(basename "${0}")
logMessage()
{
- echo "$(date '+%a %b %e %T %Y') *Tunnelblick $LOG_MESSAGE_COMMAND: "${@} >> "${SCRIPT_LOG_FILE}"
+ echo "$(date '+%a %b %e %T %Y') *LEAP Client $LOG_MESSAGE_COMMAND: "${@} >> "${SCRIPT_LOG_FILE}"
}
# @param String string - Content to trim
@@ -270,7 +273,7 @@ EOF )"
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 Tunnelblick,
+ # 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
@@ -290,7 +293,7 @@ EOF )"
CORRECT_OLD_WINS_KEY="State:"
fi
- # If we are not expecting any WINS value, add <TunnelblickNoSuchKey : true> to the expected WINS setup
+ # 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=""
@@ -315,14 +318,14 @@ EOF )"
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: "TunnelblickNoSuchKey : true"
+ # Indicate 'no such key' by a dictionary with a single entry: "LEAPClientNoSuchKey : true"
d.init
- d.add TunnelblickNoSuchKey true
+ d.add LEAPClientNoSuchKey true
get ${CORRECT_OLD_DNS_KEY}/Network/Service/${PSID}/DNS
set State:/Network/OpenVPN/OldDNS
d.init
- d.add TunnelblickNoSuchKey true
+ d.add LEAPClientNoSuchKey true
get ${CORRECT_OLD_WINS_KEY}/Network/Service/${PSID}/SMB
set State:/Network/OpenVPN/OldSMB
@@ -353,7 +356,7 @@ EOF )"
${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 TunnelblickNoSuchKey true
+ ${NO_NOSUCH_KEY_WINS}d.add LEAPClientNoSuchKey true
set State:/Network/OpenVPN/SMB
# We are done
diff --git a/pkg/osx/install/install-leapc.sh b/pkg/osx/install/install-leapc.sh
index 2ecfc08e..e47abb7c 100755
--- a/pkg/osx/install/install-leapc.sh
+++ b/pkg/osx/install/install-leapc.sh
@@ -1,17 +1,42 @@
-#!/bin/sh
-echo "Installing LEAP Client in /Applications"
-cp -r "LEAP Client.app" "/Applications"
+#!/bin/bash
-echo "Copying openvpn binary"
+# 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"
+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"
-/Library/StartupItems/tun/tun start
+echo "Loading tun/tap kernel extension..."
-echo "Installation Finished!"
+$tunstartup start
-ln -s /Applications/LEAP\ Client.app/ /Volumes/LEAP\ Client\ installer/
+echo "Installation Finished!"
diff --git a/pkg/osx/install/tun.kext/Info.plist b/pkg/osx/install/tun.kext/Info.plist
new file mode 100644
index 00000000..fb69ba85
--- /dev/null
+++ b/pkg/osx/install/tun.kext/Info.plist
@@ -0,0 +1,36 @@
+<?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/leap-client.spec b/pkg/osx/leap-client.spec
index 75bf991b..91aa20d6 100644
--- a/pkg/osx/leap-client.spec
+++ b/pkg/osx/leap-client.spec
@@ -2,8 +2,8 @@
a = Analysis(['../../src/leap/app.py'],
pathex=[
'../../src/leap',
- '/Users/kaliy/leap/leap-client-testbuild/src/leap-client/pkg/osx'],
- hiddenimports=['atexit'],
+ '/Users/kaliy/leap/leap_client/src/leap-client/pkg/osx'],
+ hiddenimports=['atexit', 'leap.common'],
hookspath=None)
pyz = PYZ(a.pure)
exe = EXE(pyz,
@@ -18,7 +18,7 @@ coll = COLLECT(exe,
a.binaries +
# this will easitly break if we setup the venv
# somewhere else. FIXME
- [('cacert.pem', '../../../../lib/python2.6/site-packages/requests/cacert.pem', 'DATA'),
+ [('cacert.pem', '/Users/kaliy/.Virtualenvs/leap-client/lib/python2.6/site-packages/requests-1.1.0-py2.6.egg/requests/cacert.pem', 'DATA'),
],
a.zipfiles,
a.datas,
diff --git a/pkg/osx/tuntaposx b/pkg/osx/tuntaposx
new file mode 160000
+Subproject 4e07e2e96b092fb3bb9bbf53ae97c0a53f8aed9
diff --git a/pkg/postmkvenv.sh b/pkg/postmkvenv.sh
index 593b11da..04f8d372 100755
--- a/pkg/postmkvenv.sh
+++ b/pkg/postmkvenv.sh
@@ -9,14 +9,14 @@
# use import PyQt4; PyQt4.__path__ instead
platform='unknown'
-unamestr=`uname`
+unamestr=$(uname)
if [[ "$unamestr" == 'Linux' ]]; then
platform='linux'
elif [[ "$unamestr" == 'Darwin' ]]; then
platform='darwin'
fi
-LIBS=( PyQt4 sip.so )
+LIBS=( PySide pysideuic )
PYTHON_VERSION=python$(python -c "import sys; print (str(sys.version_info[0])+'.'+str(sys.version_info[1]))")
VAR=( $(which -a $PYTHON_VERSION) )
@@ -34,5 +34,7 @@ fi
for LIB in ${LIBS[@]}
do
- ln -s $LIB_SYSTEM_PATH/$LIB $LIB_VIRTUALENV_PATH/$LIB
+ if [[ ! -e $LIB_VIRTUALENV_PATH/$LIB ]]; then
+ ln -s $LIB_SYSTEM_PATH/$LIB $LIB_VIRTUALENV_PATH/$LIB
+ fi
done
diff --git a/pkg/requirements-dev.pip b/pkg/requirements-dev.pip
new file mode 100644
index 00000000..71ffdc37
--- /dev/null
+++ b/pkg/requirements-dev.pip
@@ -0,0 +1,17 @@
+# ---------------------------
+# -- external requirements --
+# -- during development --
+# ---------------------------
+#
+# For temporary work, you can point this to your developer repo.
+# consolidated changes will be pushed to pypi and then added
+# to the main requirements.pip
+#
+# NOTE: you have to run pip install -r pkg/requirements.pip for pip
+# to install it. (do it after python setup.py develop and it
+# will only install this)
+
+sphinx
+
+-e git+git://github.com/leapcode/leap_pycommon.git@develop#egg=leap.common
+-e git+git://github.com/leapcode/soledad.git@develop#egg=leap.soledad
diff --git a/pkg/requirements-testing.pip b/pkg/requirements-testing.pip
new file mode 100644
index 00000000..2df5fe56
--- /dev/null
+++ b/pkg/requirements-testing.pip
@@ -0,0 +1,18 @@
+nose
+nose-exclude
+nose-progressive
+
+
+unittest2 # TODO we should include this dep only for python2.6
+coverage
+pep8>=1.1
+tox
+
+#sphinx>=1.1.2
+
+# double reqs
+# (the client already includes, which gives some errors)
+# -----------
+# mock # re-add XXX
+#twisted
+#zope.interface
diff --git a/pkg/requirements.pip b/pkg/requirements.pip
index 35135644..042633a0 100644
--- a/pkg/requirements.pip
+++ b/pkg/requirements.pip
@@ -1,17 +1,28 @@
# in order of addition to the project.
-# do not change the ordering.
+# try not to change the ordering.
+#
+# PySide -- It's a requirement indeed, but
+# it gives troubles when operating inside virtualenvs.
+# Use LEAP_VENV_SKIP_PYSIDE to avoid installing it!
-argparse # only for python 2.6
-requests<1.0.0
-psutil
-netifaces
-pyopenssl
-jsonschema #>0.7
+argparse
+requests
srp>=1.0.2
-pycrypto
-keyring>=0.9.2
+pyopenssl
+keyring
python-dateutil
-sh
-pyxdg
+psutil
+ipaddr
+twisted
+qt4reactor
+python-gnupg
+
+leap.common>=0.2.5
+leap.soledad>=0.1.0
+leap.keymanager>=0.2.0
+
+# Remove this when nickserver is online
+mock
-#pygeoip # optional
+# Remove this when u1db fixes its dependency on oauth
+oauth
diff --git a/pkg/scripts/leap_client_bootstrap.sh b/pkg/scripts/leap_client_bootstrap.sh
index 6c302d3f..dcde64f9 100644
--- a/pkg/scripts/leap_client_bootstrap.sh
+++ b/pkg/scripts/leap_client_bootstrap.sh
@@ -38,7 +38,7 @@ pip install -e 'git://leap.se/leap_client@develop#egg=leap-client'
cd leap-client-testbuild
-# symlink the pyqt libraries to the system libs
+# symlink the pyside libraries to the system libs
./src/leap-client/pkg/postmkvenv.sh
echo "${cc_green}leap-client installed! =)"
diff --git a/pkg/test-requirements.pip b/pkg/test-requirements.pip
deleted file mode 100644
index a7349bfc..00000000
--- a/pkg/test-requirements.pip
+++ /dev/null
@@ -1,8 +0,0 @@
-unittest2 # TODO we should include this dep only for python2.6
-coverage
-mock
-nose
-pep8==1.1
-sphinx>=1.1.2
-nose-exclude
-tox
diff --git a/pkg/utils.py b/pkg/utils.py
index 52680ae5..deace14b 100644
--- a/pkg/utils.py
+++ b/pkg/utils.py
@@ -1,13 +1,37 @@
+# -*- coding: utf-8 -*-
+# utils.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
"""
-utils to help in the setup process
+Utils to help in the setup process
"""
+
import os
import re
import sys
-# gets reqs from the first matching file
def get_reqs_from_files(reqfiles):
+ """
+ Returns the contents of the top requirement file listed as a
+ string list with the lines
+
+ @param reqfiles: requirement files to parse
+ @type reqfiles: list of str
+ """
for reqfile in reqfiles:
if os.path.isfile(reqfile):
return open(reqfile, 'r').read().split('\n')
@@ -16,12 +40,26 @@ def get_reqs_from_files(reqfiles):
def parse_requirements(reqfiles=['requirements.txt',
'requirements.pip',
'pkg/requirements.pip']):
+ """
+ Parses the requirement files provided.
+
+ Checks the value of LEAP_VENV_SKIP_PYSIDE to see if it should
+ return PySide as a dep or not. Don't set, or set to 0 if you want
+ to install it through pip.
+
+ @param reqfiles: requirement files to parse
+ @type reqfiles: list of str
+ """
+
requirements = []
+ skip_pyside = os.getenv("LEAP_VENV_SKIP_PYSIDE", "0") != "0"
for line in get_reqs_from_files(reqfiles):
# -e git://foo.bar/baz/master#egg=foobar
if re.match(r'\s*-e\s+', line):
- requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1',
- line))
+ pass
+ # do not try to do anything with externals on vcs
+ #requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1',
+ #line))
# http://foo.bar/baz/foobar/zipball/master#egg=foobar
elif re.match(r'\s*https?:', line):
requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1',
@@ -34,9 +72,13 @@ def parse_requirements(reqfiles=['requirements.txt',
# adding it to the requirements list screws distro installs
elif line == 'argparse' and sys.version_info >= (2, 7):
pass
+ elif line == 'PySide' and skip_pyside:
+ pass
+ # do not include comments
+ elif line.lstrip().startswith('#'):
+ pass
else:
if line != '':
requirements.append(line)
- #print 'REQUIREMENTS', requirements
return requirements
diff --git a/pkg/windows/openvpn_manifest b/pkg/windows/openvpn_manifest
new file mode 100644
index 00000000..7c6a542a
--- /dev/null
+++ b/pkg/windows/openvpn_manifest
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <assemblyIdentity version="1.0.0.0" name="openvpn_leap" />
+ <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
+ <security>
+ <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
+ <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
+ </requestedPrivileges>
+ <applicationRequestMinimum>
+ <defaultAssemblyRequest permissionSetReference="Custom" />
+ <PermissionSet class="System.Security.PermissionSet" version="1" ID="Custom" SameSite="site" />
+ </applicationRequestMinimum>
+ </security>
+ </trustInfo>
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ </application>
+ </compatibility>
+</asmv1:assembly> \ No newline at end of file
diff --git a/relnotes.txt b/relnotes.txt
index ce378b67..5f711005 100644
--- a/relnotes.txt
+++ b/relnotes.txt
@@ -1,28 +1,27 @@
-ANNOUNCING LEAP Client, the internet encryption toolkit, v0.2.0
+ANNOUNCING Bitmask, the internet encryption toolkit, v0.3.0
-The LEAP team is pleased to announce the immediate
-availability of version 0.2.0 of LEAP Client
+The LEAP team is pleased to announce the immediate availability of
+version 0.3.0 of Bitmask
-https://leap.se/downloads/leap-client
+https://downloads.leap.se/client/
LEAP (LEAP Encryption Access Project) develops a plan to secure
everyday communication, breaking down into discrete services.
-The client for the current phase gives support to the EIP Service.
+The client for the current phase gives support to the EIP Service and
+the first beta release of Encrypted Mail.
EIP (the Encrypted Internet Proxy) provides circumvention,
location anonymization, and traffic encryption in a hassle-free,
automatically self-configuring fashion.
+Encrypted Mail
You can read the user manual and the developer notes online at:
-http://leap-client.readthedocs.org
+http://bitmask.readthedocs.org/
-This is the first testing release for the LEAP Client, and it
-is by no means intended for real production use. Especially you
-are encouraged to not provide any kind of sensitive password during
-your registration process.
-
-See the NEWS file for more details.
+WARNING: This is still a beta release of our services, a lot of
+testing and audits are still needed so DO NOT use this for strong
+security.
WHAT CAN THIS VERSION OF THE CLIENT DO FOR ME?
@@ -64,8 +63,8 @@ will be following soon.
BUGS
-You can send the bugs our way by pointing your telnet session to port 443 on
-https://leap.se/code. We will do our best to make them follow our
+You can send the bugs our way by pointing your telnet session to port 443 on
+https://leap.se/code. We will do our best to make them follow our
intensive bug-reeducation program.
@@ -76,7 +75,7 @@ If you are lucky enough, you can spot us sleepless in night trains,
rooftops, rainforests, and beyond any border.
-Kali Kaneko, and all the LEAP team.
+The LEAP team.
-Feb 15, 2012
+Aug 9, 2013
Somewhere in the middle of the intertubes.
diff --git a/run_tests.sh b/run_tests.sh
index ebea30b2..9f9262ba 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -15,6 +15,8 @@ function usage {
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,"
@@ -33,13 +35,15 @@ function process_option {
-p|--pep8) just_pep8=1;;
-P|--no-pep8) no_pep8=1;;
-c|--coverage) coverage=1;;
+ -A|--all) alltests=1;;
+ -i|--progressive) progressive=1;;
-*) noseopts="$noseopts $1";;
*) noseargs="$noseargs $1"
esac
}
venv=.venv
-with_venv=setup/tools/with_venv.sh
+with_venv=pkg/tools/with_venv.sh
always_venv=0
never_venv=0
force=0
@@ -51,6 +55,8 @@ wrapper=""
just_pep8=0
no_pep8=0
coverage=0
+alltests=0
+progressive=0
for arg in "$@"; do
process_option $arg
@@ -58,15 +64,29 @@ done
# If enabled, tell nose to collect coverage data
if [ $coverage -eq 1 ]; then
- noseopts="$noseopts --with-coverage --cover-package=leap-client"
+ 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..."
# 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=$?
@@ -75,9 +95,9 @@ function run_tests {
function run_pep8 {
echo "Running pep8 ..."
- srcfiles="src/leap tests"
+ srcfiles="src/leap"
# Just run PEP8 in current environment
- pep8_opts="--ignore=E202,W602 --exclude=*_rc.py --repeat"
+ pep8_opts="--ignore=E202,W602 --exclude=*_rc.py,ui_*,_version.py --repeat"
${wrapper} pep8 ${pep8_opts} ${srcfiles}
}
@@ -85,7 +105,9 @@ function run_pep8 {
# in the current debhelper build process,
# so I exclude the topmost tests
-NOSETESTS="nosetests leap $noseopts $noseargs"
+#NOSETESTS="nosetests leap --exclude=soledad* $noseopts $noseargs"
+NOSETESTS="$VIRTUAL_ENV/bin/nosetests . $noseopts $noseargs"
+#--with-coverage --cover-package=leap"
if [ $never_venv -eq 0 ]
then
@@ -99,14 +121,14 @@ then
else
if [ $always_venv -eq 1 ]; then
# Automatically install the virtualenv
- python setup/install_venv.py $installvenvopts
+ 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 setup/install_venv.py $installvenvopts
+ python pkg/install_venv.py $installvenvopts
wrapper=${with_venv}
fi
fi
@@ -131,15 +153,8 @@ if [ -z "$noseargs" ]; then
fi
fi
-function run_coverage {
- # XXX not working? getting 3rd party modules
- coverage_opts="--include `pwd`/src/leap/*,`pwd`/src/leap/eip/*"
- ${wrapper} coverage html -d docs/covhtml -i $coverage_opts
- echo "now point your browser at docs/covhtml/index.html"
-}
-
if [ $coverage -eq 1 ]; then
echo "Generating coverage report in docs/covhtml/"
- run_coverage
+ echo "now point your browser at docs/covhtml/index.html"
exit
fi
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 01bb9544..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,3 +0,0 @@
-[egg_info]
-tag_build = dev
-tag_svn_revision = true
diff --git a/setup.py b/setup.py
index ae7b2757..4b36ad72 100755
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
+
from __future__ import print_function
+
import sys
try:
@@ -12,17 +14,20 @@ except ImportError:
import os
from pkg import utils
-from pkg import branding
+
import versioneer
versioneer.versionfile_source = 'src/leap/_version.py'
versioneer.versionfile_build = 'leap/_version.py'
versioneer.tag_prefix = '' # tags are like 1.2.0
-#versioneer.parentdir_prefix = 'leap_client-'
-versioneer.parentdir_prefix = branding.APP_PREFIX
+versioneer.parentdir_prefix = 'bitmask-'
+
+from setuptools import Command
-branding.brandingfile = 'src/leap/_branding.py'
-branding.brandingfile_build = 'leap/_branding.py'
-branding.cert_path = 'src/leap/certs'
+# The following import avoids the premature unloading of the `util` submodule
+# when running tests, which would cause an error when nose finishes tests and
+# calls the exit function of the multiprocessing module.
+from multiprocessing import util
+assert(util)
setup_root = os.path.dirname(__file__)
sys.path.insert(0, os.path.join(setup_root, "src"))
@@ -43,98 +48,30 @@ trove_classifiers = [
"Topic :: Utilities"
]
-BRANDING_OPTS = """
-# Do NOT manually edit this file!
-# This file has been written from pkg/branding/config.py data by leap setup.py
-# script.
-
-BRANDING = {
- 'short_name': "%(short_name)s",
- 'provider_domain': "%(provider_domain)s",
- 'provider_ca_file': "%(provider_ca_file)s"}
-"""
-
-
-def write_to_branding_file(filename, branding_dict):
- f = open(filename, "w")
- f.write(BRANDING_OPTS % branding_dict)
- f.close()
-
-
-def copy_pemfile_to_certdir(frompath, topath):
- with open(frompath, "r") as cert_f:
- cert_s = cert_f.read()
- with open(topath, "w") as f:
- f.write(cert_s)
-
-
-def do_branding(targetfile=branding.brandingfile):
- if branding.BRANDED_BUILD:
- opts = branding.BRANDED_OPTS
- print("DOING BRANDING FOR LEAP")
- certpath = opts['provider_ca_path']
- shortname = opts['short_name']
- tocertfile = shortname + '-cacert.pem'
- topath = os.path.join(
- branding.cert_path,
- tocertfile)
- copy_pemfile_to_certdir(
- certpath,
- topath)
- opts['provider_ca_file'] = tocertfile
- write_to_branding_file(
- targetfile,
- opts)
- else:
- print('not running branding because BRANDED_BUILD set to False')
-
-from setuptools import Command
+parsed_reqs = utils.parse_requirements()
+cmdclass = versioneer.get_cmdclass()
+leap_launcher = 'bitmask=leap.app:main'
-class DoBranding(Command):
- description = "copy the branding info the the top level package"
- user_options = []
-
- def initialize_options(self):
- pass
-
- def finalize_options(self):
- pass
+from setuptools.command.develop import develop as _develop
- def run(self):
- do_branding()
-from distutils.command.build import build as _build
-from distutils.command.sdist import sdist as _sdist
+def copy_reqs(path, withsrc=False):
+ # add a copy of the processed requirements to the package
+ _reqpath = ('leap', 'util', 'reqs.txt')
+ if withsrc:
+ reqsfile = os.path.join(path, 'src', *_reqpath)
+ else:
+ reqsfile = os.path.join(path, *_reqpath)
+ print("UPDATING %s" % reqsfile)
+ if os.path.isfile(reqsfile):
+ os.unlink(reqsfile)
+ with open(reqsfile, "w") as f:
+ f.write('\n'.join(parsed_reqs))
-class cmd_build(_build):
- def run(self):
- #versioneer.cmd_build(self)
- _build.run(self)
-
- # versioneer
- versions = versioneer.get_versions(verbose=True)
- # now locate _version.py in the new build/ directory and replace it
- # with an updated value
- target_versionfile = os.path.join(
- self.build_lib,
- versioneer.versionfile_build)
- print("UPDATING %s" % target_versionfile)
- os.unlink(target_versionfile)
- f = open(target_versionfile, "w")
- f.write(versioneer.SHORT_VERSION_PY % versions)
- f.close()
-
- # branding
- target_brandingfile = os.path.join(
- self.build_lib,
- branding.brandingfile_build)
- do_branding(targetfile=target_brandingfile)
-
-
-class cmd_sdist(_sdist):
+class cmd_develop(_develop):
def run(self):
# versioneer:
versions = versioneer.get_versions(verbose=True)
@@ -142,43 +79,42 @@ class cmd_sdist(_sdist):
# unless we update this, the command will keep using the old version
self.distribution.metadata.version = versions["version"]
- # branding:
- do_branding()
- return _sdist.run(self)
+ _develop.run(self)
+ copy_reqs(self.egg_path)
+
+cmdclass["develop"] = cmd_develop
+
+# next two classes need to augment the versioneer modified ones
+
+versioneer_build = cmdclass['build']
+versioneer_sdist = cmdclass['sdist']
+
+
+class cmd_build(versioneer_build):
+ def run(self):
+ versioneer_build.run(self)
+ copy_reqs(self.build_lib)
+
+
+class cmd_sdist(versioneer_sdist):
+ def run(self):
+ return versioneer_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, versioneer.versionfile_source)
- print("UPDATING %s" % target_versionfile)
- os.unlink(target_versionfile)
- f = open(target_versionfile, "w")
- f.write(
- versioneer.SHORT_VERSION_PY % self._versioneer_generated_versions)
- f.close()
+ versioneer_sdist.make_release_tree(self, base_dir, files)
+ copy_reqs(base_dir, withsrc=True)
-cmdclass = versioneer.get_cmdclass()
-cmdclass["branding"] = DoBranding
-# Uncomment this to have the branding command run automatically
-# on the build and sdist commands.
-#cmdclass["build"] = cmd_build
-#cmdclass["sdist"] = cmd_sdist
+cmdclass["build"] = cmd_build
+cmdclass["sdist"] = cmd_sdist
-launcher_name = branding.get_shortname()
-if launcher_name:
- leap_launcher = 'leap-%s-client=leap.app:main' % launcher_name
-else:
- leap_launcher = 'leap-client=leap.app:main'
setup(
- name=branding.get_name(),
+ name="bitmask",
package_dir={"": "src"},
version=versioneer.get_version(),
cmdclass=cmdclass,
- description="the internet encryption toolkit",
+ description="The Internet Encryption Toolkit",
long_description=(
"Desktop Client for the LEAP Platform."
"\n"
@@ -192,11 +128,12 @@ setup(
"and has an enhanced level of security."
),
classifiers=trove_classifiers,
- install_requires=utils.parse_requirements(),
+ install_requires=parsed_reqs,
test_suite='nose.collector',
- test_requires=utils.parse_requirements(
- reqfiles=['pkg/test-requirements.pip']),
- keywords='LEAP, client, qt, encryption, proxy, openvpn',
+ tests_require=utils.parse_requirements(
+ reqfiles=['pkg/requirements-testing.pip']),
+ keywords=('Bitmask, LEAP, client, qt, encryption, '
+ 'proxy, openvpn, imap, smtp'),
author='The LEAP Encryption Access Project',
author_email='info@leap.se',
url='https://leap.se',
@@ -204,17 +141,18 @@ setup(
packages=find_packages(
'src',
exclude=['ez_setup', 'setup', 'examples', 'tests']),
+ namespace_packages=["leap"],
+ package_data={'': ['util/*.txt']},
include_package_data=True,
- zip_safe=False,
-
- # not being used since setuptools does not like it.
- # but looks that debhelper honors it...
+ # not being used? -- setuptools does not like it.
+ # looks like debhelper is honoring it...
data_files=[
- # ("share/man/man1",
- # ["docs/man/leap-client.1"]),
+ # ("share/man/man1",
+ # ["docs/man/bitmask.1"]),
("share/polkit-1/actions",
- ["pkg/linux/polkit/net.openvpn.gui.leap.policy"])
+ ["pkg/linux/polkit/net.openvpn.gui.leap.policy"]),
],
+ zip_safe=False,
platforms="all",
entry_points={
'console_scripts': [leap_launcher]
diff --git a/setup/install_venv.py b/setup/install_venv.py
deleted file mode 100644
index 3f3f0575..00000000
--- a/setup/install_venv.py
+++ /dev/null
@@ -1,242 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2010 United States Government as represented by the
-# Administrator of the National Aeronautics and Space Administration.
-# All Rights Reserved.
-#
-# Copyright 2010 OpenStack, LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-"""
-Installation script for Nova's development virtualenv
-"""
-
-import optparse
-import os
-import subprocess
-import sys
-import platform
-
-
-ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
-VENV = os.path.join(ROOT, '.venv')
-PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires')
-TEST_REQUIRES = os.path.join(ROOT, 'tools', 'test-requires')
-PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
-
-
-def die(message, *args):
- print >> sys.stderr, message % args
- sys.exit(1)
-
-
-def check_python_version():
- if sys.version_info < (2, 6):
- die("Need Python Version >= 2.6")
-
-
-def run_command_with_code(cmd, redirect_output=True, check_exit_code=True):
- """
- Runs a command in an out-of-process shell, returning the
- output of that command. Working directory is ROOT.
- """
- if redirect_output:
- stdout = subprocess.PIPE
- else:
- stdout = None
-
- print 'executing command: %s', cmd
- proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout)
- output = proc.communicate()[0]
- if check_exit_code and proc.returncode != 0:
- die('Command "%s" failed.\n%s', ' '.join(cmd), output)
- return (output, proc.returncode)
-
-
-def run_command(cmd, redirect_output=True, check_exit_code=True):
- return run_command_with_code(cmd, redirect_output, check_exit_code)[0]
-
-
-class Distro(object):
-
- def check_cmd(self, cmd):
- return bool(run_command(['which', cmd], check_exit_code=False).strip())
-
- def install_virtualenv(self):
- if self.check_cmd('virtualenv'):
- return
-
- if self.check_cmd('easy_install'):
- print 'Installing virtualenv via easy_install...',
- if run_command(['easy_install', 'virtualenv']):
- print 'Succeeded'
- return
- else:
- print 'Failed'
-
- die('ERROR: virtualenv not found.\n\nDevelopment'
- ' requires virtualenv, please install it using your'
- ' favorite package management tool')
-
- def post_process(self):
- """Any distribution-specific post-processing gets done here.
-
- In particular, this is useful for applying patches to code inside
- the venv."""
- pass
-
-
-class Debian(Distro):
- """This covers all Debian-based distributions."""
-
- def check_pkg(self, pkg):
- return run_command_with_code(['dpkg', '-l', pkg],
- check_exit_code=False)[1] == 0
-
- def apt_install(self, pkg, **kwargs):
- run_command(['sudo', 'apt-get', 'install', '-y', pkg], **kwargs)
-
- def apply_patch(self, originalfile, patchfile):
- run_command(['patch', originalfile, patchfile])
-
- def install_virtualenv(self):
- if self.check_cmd('virtualenv'):
- return
-
- if not self.check_pkg('python-virtualenv'):
- self.apt_install('python-virtualenv', check_exit_code=False)
-
- super(Debian, self).install_virtualenv()
-
-
-class Fedora(Distro):
- """This covers all Fedora-based distributions.
-
- Includes: Fedora, RHEL, CentOS, Scientific Linux"""
-
- def check_pkg(self, pkg):
- return run_command_with_code(['rpm', '-q', pkg],
- check_exit_code=False)[1] == 0
-
- def yum_install(self, pkg, **kwargs):
- run_command(['sudo', 'yum', 'install', '-y', pkg], **kwargs)
-
- def apply_patch(self, originalfile, patchfile):
- run_command(['patch', originalfile, patchfile])
-
- def install_virtualenv(self):
- if self.check_cmd('virtualenv'):
- return
-
- if not self.check_pkg('python-virtualenv'):
- self.yum_install('python-virtualenv', check_exit_code=False)
-
- super(Fedora, self).install_virtualenv()
-
-
-def get_distro():
- if os.path.exists('/etc/fedora-release') or \
- os.path.exists('/etc/redhat-release'):
- return Fedora()
- elif os.path.exists('/etc/debian_version'):
- return Debian()
- else:
- return Distro()
-
-
-def check_dependencies():
- get_distro().install_virtualenv()
-
-
-def create_virtualenv(venv=VENV, no_site_packages=True):
- """Creates the virtual environment and installs PIP only into the
- virtual environment
- """
- print 'Creating venv...',
- if no_site_packages:
- run_command(['virtualenv', '-q', '--no-site-packages', VENV])
- else:
- run_command(['virtualenv', '-q', VENV])
- print 'done.'
- print 'Installing pip in virtualenv...',
- if not run_command(['setup/tools/with_venv.sh', 'easy_install',
- 'pip>1.0']).strip():
- die("Failed to install pip.")
- print 'done.'
-
-
-def pip_install(*args):
- run_command(['setup/tools/with_venv.sh',
- 'pip', 'install', '--upgrade'] + list(args),
- redirect_output=False)
-
-
-def install_dependencies(venv=VENV):
- print 'Installing dependencies with pip (this can take a while)...'
-
- # First things first, make sure our venv has the latest pip and distribute.
- pip_install('pip')
- pip_install('distribute')
-
- pip_install('-r', PIP_REQUIRES)
- pip_install('-r', TEST_REQUIRES)
-
- # "
- pthfile = os.path.join(venv, "lib", PY_VERSION, "site-packages",
- "leap-client.pth")
- f = open(pthfile, 'w')
- f.write("%s\n" % ROOT)
-
-
-def post_process():
- get_distro().post_process()
-
-
-def print_help():
- help = """
- To activate the leap virtualenv for the extent of your current
- shell session you can run:
-
- $ source .venv/bin/activate
-
- Or, if you prefer, you can run commands in the virtualenv on a case by case
- basis by running:
-
- $ setup/tools/with_venv.sh <your command>
-
- Also, make test will automatically use the virtualenv.
- """
- print help
-
-
-def parse_args():
- """Parse command-line arguments"""
- parser = optparse.OptionParser()
- parser.add_option("-n", "--no-site-packages", dest="no_site_packages",
- default=False, action="store_true",
- help="Do not inherit packages from global Python install")
- return parser.parse_args()
-
-
-def main(argv):
- (options, args) = parse_args()
- check_python_version()
- check_dependencies()
- create_virtualenv(no_site_packages=options.no_site_packages)
- install_dependencies()
- post_process()
- print_help()
-
-if __name__ == '__main__':
- main(sys.argv)
diff --git a/setup/linux/leap.desktop b/setup/linux/leap.desktop
deleted file mode 100644
index 7a6d39d9..00000000
--- a/setup/linux/leap.desktop
+++ /dev/null
@@ -1,13 +0,0 @@
-[Desktop Entry]
-Version=0.1.0
-Encoding=UTF-8
-Name=EIP
-Comment=Anonymity and privacy
-Comment[en]=Anonymity and privacy
-Comment[es]=Anonimato y privacidad
-Comment[sv]=Anonymitet och avlyssningsskydd
-Exec=leap
-Terminal=false
-Type=Application
-Icon=leap.png
-Categories=Network;
diff --git a/setup/linux/polkit/net.openvpn.gui.leap.policy b/setup/linux/polkit/net.openvpn.gui.leap.policy
deleted file mode 100644
index 70a22b65..00000000
--- a/setup/linux/polkit/net.openvpn.gui.leap.policy
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE policyconfig PUBLIC
- "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
- "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
-<policyconfig>
-
- <vendor>LEAP Project</vendor>
- <vendor_url>http://leap.se/</vendor_url>
-
- <action id="net.openvpn,gui.leap.run-openvpn">
- <description>Runs the openvpn binary</description>
- <description xml:lang="es">Ejecuta el binario openvpn</description>
- <message>OpenVPN needs that you authenticate to start</message>
- <message xml:lang="es">OpenVPN necesita autorizacion para comenzar</message>
- <icon_name>package-x-generic</icon_name>
- <defaults>
- <allow_any>auth_self_keep</allow_any>
- <allow_inactive>auth_self_keep</allow_inactive>
- <allow_active>auth_self_keep</allow_active>
- </defaults>
- <annotate key="org.freedesktop.policykit.exec.path">/usr/sbin/openvpn</annotate>
- </action>
-</policyconfig>
diff --git a/setup/requirements.pip b/setup/requirements.pip
deleted file mode 100644
index 1352d5e6..00000000
--- a/setup/requirements.pip
+++ /dev/null
@@ -1 +0,0 @@
-argparse
diff --git a/setup/scripts/leap b/setup/scripts/leap
deleted file mode 100755
index 6e62b597..00000000
--- a/setup/scripts/leap
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env python
-
-from leap.app import main
-
-if __name__ == "__main__":
- main()
diff --git a/setup/test-requires b/setup/test-requires
deleted file mode 100644
index 26db61c8..00000000
--- a/setup/test-requires
+++ /dev/null
@@ -1,5 +0,0 @@
-coverage
-mock
-nose
-pep8==1.1
-sphinx>=1.1.2
diff --git a/setup/tools/with_venv.sh b/setup/tools/with_venv.sh
deleted file mode 100755
index 0e58f1ab..00000000
--- a/setup/tools/with_venv.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-TOOLS=`dirname $0`
-VENV=$TOOLS/../../.venv
-source $VENV/bin/activate && $@
diff --git a/src/leap/__init__.py b/src/leap/__init__.py
index 0e880867..f48ad105 100644
--- a/src/leap/__init__.py
+++ b/src/leap/__init__.py
@@ -1,35 +1,6 @@
-"""
-LEAP Encryption Access Project
-website: U{https://leap.se/}
-"""
-
-from leap import eip
-from leap import baseapp
-from leap import util
-#from leap import soledad
-
-__all__ = [eip, baseapp, util]
-__version__ = "unknown"
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
try:
- from ._version import get_versions
- __version__ = get_versions()['version']
- del get_versions
+ __import__('pkg_resources').declare_namespace(__name__)
except ImportError:
- #running on a tree that has not run
- #the setup.py setver
- pass
-
-__appname__ = "unknown"
-try:
- from leap._appname import __appname__
-except ImportError:
- #running on a tree that has not run
- #the setup.py setver
- pass
-
-__full_version__ = __appname__ + '/' + str(__version__)
-
-try:
- from leap._branding import BRANDING as __branding
-except ImportError:
- __branding = {}
+ from pkgutil import extend_path
+ __path__ = extend_path(__path__, __name__)
diff --git a/src/leap/_version.py b/src/leap/_version.py
index 6f7e3d05..05542975 100644
--- a/src/leap/_version.py
+++ b/src/leap/_version.py
@@ -16,6 +16,8 @@ git_full = "$Format:%H$"
import subprocess
import sys
+import re
+import os.path
def run_command(args, cwd=None, verbose=False):
@@ -38,11 +40,6 @@ def run_command(args, cwd=None, verbose=False):
return stdout
-import sys
-import re
-import os.path
-
-
def get_expanded_variables(versionfile_source):
# 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
@@ -187,7 +184,7 @@ def versions_from_parentdir(parentdir_prefix, versionfile_source,
return {"version": dirname[len(parentdir_prefix):], "full": ""}
tag_prefix = ""
-parentdir_prefix = "leap_client-"
+parentdir_prefix = "bitmask-"
versionfile_source = "src/leap/_version.py"
diff --git a/src/leap/app.py b/src/leap/app.py
index 1b2ccd61..22340d4d 100644
--- a/src/leap/app.py
+++ b/src/leap/app.py
@@ -1,72 +1,178 @@
-# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
-from functools import partial
+# -*- coding: utf-8 -*-
+# app.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
import logging
-import platform
import signal
+import sys
+import os
+
+from functools import partial
-# This is only needed for Python v2 but is harmless for Python v3.
-import sip
-sip.setapi('QVariant', 2)
-sip.setapi('QString', 2)
-from PyQt4.QtGui import (QApplication, QSystemTrayIcon, QMessageBox)
-from PyQt4 import QtCore
+from PySide import QtCore, QtGui
-from leap import __version__ as VERSION
-from leap.baseapp.mainwindow import LeapWindow
-from leap.util import polkit
+from leap.common.events import server as event_server
+from leap.util import __version__ as VERSION
+from leap.util import leap_argparse
+from leap.util.leap_log_handler import LeapLogHandler
+from leap.util.streamtologger import StreamToLogger
+from leap.util.requirement_checker import check_requirements
from leap.gui import locale_rc
+from leap.gui import twisted_main
+from leap.gui.mainwindow import MainWindow
+from leap.platform_init import IS_MAC
+from leap.platform_init.locks import we_are_the_one_and_only
+from leap.services.tx import leap_services
+
+
+import codecs
+codecs.register(lambda name: codecs.lookup('utf-8')
+ if name == 'cp65001' else None)
+
+# pylint: avoid unused import
+assert(locale_rc)
def sigint_handler(*args, **kwargs):
+ """
+ Signal handler for SIGINT
+ """
logger = kwargs.get('logger', None)
- logger.debug('SIGINT catched. shutting down...')
+ if logger:
+ logger.debug("SIGINT catched. shutting down...")
mainwindow = args[0]
- mainwindow.shutdownSignal.emit()
+ mainwindow.quit()
-def main():
+def install_qtreactor(logger):
+ import qt4reactor
+ qt4reactor.install()
+ logger.debug("Qt4 reactor installed")
+
+
+def add_logger_handlers(debug=False, logfile=None):
"""
- launches the main event loop
- long live to the (hidden) leap window!
+ Create the logger and attach the handlers.
+
+ :param debug: the level of the messages that we should log
+ :type debug: bool
+ :param logfile: the file name of where we should to save the logs
+ :type logfile: str
+ :return: the new logger with the attached handlers.
+ :rtype: logging.Logger
"""
- import sys
- from leap.util import leap_argparse
- parser, opts = leap_argparse.init_leapc_args()
- debug = getattr(opts, 'debug', False)
-
- # XXX get severity from command line args
+ # TODO: get severity from command line args
if debug:
level = logging.DEBUG
else:
level = logging.WARNING
+ # Create logger and formatter
logger = logging.getLogger(name='leap')
logger.setLevel(level)
+ log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+ formatter = logging.Formatter(log_format)
+
+ # Console handler
console = logging.StreamHandler()
console.setLevel(level)
- formatter = logging.Formatter(
- '%(asctime)s '
- '- %(name)s - %(levelname)s - %(message)s')
console.setFormatter(formatter)
logger.addHandler(console)
+ logger.debug('Console handler plugged!')
- logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
- logger.info('LEAP client version %s', VERSION)
- logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
- logfile = getattr(opts, 'log_file', False)
- if logfile:
- logger.debug('setting logfile to %s ', logfile)
+ # LEAP custom handler
+ leap_handler = LeapLogHandler()
+ leap_handler.setLevel(level)
+ logger.addHandler(leap_handler)
+ logger.debug('Leap handler plugged!')
+
+ # File handler
+ if logfile is not None:
+ logger.debug('Setting logfile to %s ', logfile)
fileh = logging.FileHandler(logfile)
fileh.setLevel(logging.DEBUG)
fileh.setFormatter(formatter)
logger.addHandler(fileh)
+ logger.debug('File handler plugged!')
+
+ return logger
+
+
+def replace_stdout_stderr_with_logging(logger):
+ """
+ Replace:
+ - the standard output
+ - the standard error
+ - the twisted log output
+ with a custom one that writes to the logger.
+ """
+ sys.stdout = StreamToLogger(logger, logging.DEBUG)
+ sys.stderr = StreamToLogger(logger, logging.ERROR)
+
+ # Replace twisted's logger to use our custom output.
+ from twisted.python import log
+ log.startLogging(sys.stdout)
+
+
+def main():
+ """
+ Starts the main event loop and launches the main window.
+ """
+ try:
+ event_server.ensure_server(event_server.SERVER_PORT)
+ except Exception as e:
+ # We don't even have logger configured in here
+ print "Could not ensure server: %r" % (e,)
+
+ _, opts = leap_argparse.init_leapc_args()
+ standalone = opts.standalone
+ bypass_checks = getattr(opts, 'danger', False)
+ debug = opts.debug
+ logfile = opts.log_file
+ openvpn_verb = opts.openvpn_verb
+
+ logger = add_logger_handlers(debug, logfile)
+ replace_stdout_stderr_with_logging(logger)
+
+ if not we_are_the_one_and_only():
+ # Bitmask is already running
+ logger.warning("Tried to launch more than one instance "
+ "of Bitmask. Raising the existing "
+ "one instead.")
+ sys.exit(1)
+
+ check_requirements()
+
+ logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
+ logger.info('Bitmask version %s', VERSION)
+ logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
logger.info('Starting app')
- app = QApplication(sys.argv)
- # launch polkit-auth agent if needed
- if platform.system() == "Linux":
- polkit.check_if_running_polkit_auth()
+ # We force the style if on KDE so that it doesn't load all the kde
+ # libs, which causes a compatibility issue in some systems.
+ # For more info, see issue #3194
+ if os.environ.get("KDE_SESSION_UID") is not None:
+ sys.argv.append("-style")
+ sys.argv.append("Cleanlooks")
+
+ app = QtGui.QApplication(sys.argv)
+
+ # install the qt4reactor.
+ install_qtreactor(logger)
# To test:
# $ LANG=es ./app.py
@@ -75,52 +181,44 @@ def main():
if qtTranslator.load("qt_%s" % locale, ":/translations"):
app.installTranslator(qtTranslator)
appTranslator = QtCore.QTranslator()
- if appTranslator.load("leap_client_%s" % locale, ":/translations"):
+ if appTranslator.load("%s.qm" % locale[:2], ":/translations"):
app.installTranslator(appTranslator)
- # needed for initializing qsettings
- # it will write .config/leap/leap.conf
- # top level app settings
- # in a platform independent way
+ # Needed for initializing qsettings it will write
+ # .config/leap/leap.conf top level app settings in a platform
+ # independent way
app.setOrganizationName("leap")
app.setApplicationName("leap")
app.setOrganizationDomain("leap.se")
- # XXX we could check here
- # if leap-client is already running, and abort
- # gracefully in that case.
-
- if not QSystemTrayIcon.isSystemTrayAvailable():
- QMessageBox.critical(None, "Systray",
- "I couldn't detect"
- "any system tray on this system.")
- sys.exit(1)
- if not debug:
- QApplication.setQuitOnLastWindowClosed(False)
-
- window = LeapWindow(opts)
-
- # this dummy timer ensures that
- # control is given to the outside loop, so we
- # can hook our sigint handler.
- timer = QtCore.QTimer()
- timer.start(500)
- timer.timeout.connect(lambda: None)
+ # XXX ---------------------------------------------------------
+ # In quarantine, looks like we don't need it anymore.
+ # This dummy timer ensures that control is given to the outside
+ # loop, so we can hook our sigint handler.
+ #timer = QtCore.QTimer()
+ #timer.start(500)
+ #timer.timeout.connect(lambda: None)
+ # XXX ---------------------------------------------------------
+
+ window = MainWindow(
+ lambda: twisted_main.quit(app),
+ standalone=standalone,
+ openvpn_verb=openvpn_verb,
+ bypass_checks=bypass_checks)
sigint_window = partial(sigint_handler, window, logger=logger)
signal.signal(signal.SIGINT, sigint_window)
- if debug:
- # we only show the main window
- # if debug mode active.
- # if not, it will be set visible
- # from the systray menu.
- window.show()
- if sys.platform == "darwin":
- window.raise_()
-
- # run main loop
- sys.exit(app.exec_())
+ if IS_MAC:
+ window.raise_()
+
+ # This was a good idea, but for this to work as intended we
+ # should centralize the start of all services in there.
+ #tx_app = leap_services()
+ #assert(tx_app)
+
+ # Run main loop
+ twisted_main.start(app)
if __name__ == "__main__":
main()
diff --git a/src/leap/base/auth.py b/src/leap/base/auth.py
deleted file mode 100644
index c2d3f424..00000000
--- a/src/leap/base/auth.py
+++ /dev/null
@@ -1,355 +0,0 @@
-import binascii
-import json
-import logging
-#import urlparse
-
-import requests
-import srp
-
-from PyQt4 import QtCore
-
-from leap.base import constants as baseconstants
-from leap.crypto import leapkeyring
-from leap.util.misc import null_check
-from leap.util.web import get_https_domain_and_port
-
-logger = logging.getLogger(__name__)
-
-SIGNUP_TIMEOUT = getattr(baseconstants, 'SIGNUP_TIMEOUT', 5)
-
-"""
-Registration and authentication classes for the
-SRP auth mechanism used in the leap platform.
-
-We're using the srp library which uses a c-based implementation
-of the protocol if the c extension is available, and a python-based
-one if not.
-"""
-
-
-class SRPAuthenticationError(Exception):
- """
- exception raised
- for authentication errors
- """
-
-
-safe_unhexlify = lambda x: binascii.unhexlify(x) \
- if (len(x) % 2 == 0) else binascii.unhexlify('0' + x)
-
-
-class LeapSRPRegister(object):
-
- def __init__(self,
- schema="https",
- provider=None,
- verify=True,
- register_path="1/users",
- method="POST",
- fetcher=requests,
- srp=srp,
- hashfun=srp.SHA256,
- ng_constant=srp.NG_1024):
-
- null_check(provider, "provider")
-
- self.schema = schema
-
- domain, port = get_https_domain_and_port(provider)
- self.provider = domain
- self.port = port
-
- self.verify = verify
- self.register_path = register_path
- self.method = method
- self.fetcher = fetcher
- self.srp = srp
- self.HASHFUN = hashfun
- self.NG = ng_constant
-
- self.init_session()
-
- def init_session(self):
- self.session = self.fetcher.session()
-
- def get_registration_uri(self):
- # XXX assert is https!
- # use urlparse
- if self.port:
- uri = "%s://%s:%s/%s" % (
- self.schema,
- self.provider,
- self.port,
- self.register_path)
- else:
- uri = "%s://%s/%s" % (
- self.schema,
- self.provider,
- self.register_path)
-
- return uri
-
- def register_user(self, username, password, keep=False):
- """
- @rtype: tuple
- @rparam: (ok, request)
- """
- salt, vkey = self.srp.create_salted_verification_key(
- username,
- password,
- self.HASHFUN,
- self.NG)
-
- user_data = {
- 'user[login]': username,
- 'user[password_verifier]': binascii.hexlify(vkey),
- 'user[password_salt]': binascii.hexlify(salt)}
-
- uri = self.get_registration_uri()
- logger.debug('post to uri: %s' % uri)
-
- # XXX get self.method
- req = self.session.post(
- uri, data=user_data,
- timeout=SIGNUP_TIMEOUT,
- verify=self.verify)
- # we catch it in the form
- #req.raise_for_status()
- return (req.ok, req)
-
-
-class SRPAuth(requests.auth.AuthBase):
-
- def __init__(self, username, password, server=None, verify=None):
- # sanity check
- null_check(server, 'server')
- self.username = username
- self.password = password
- self.server = server
- self.verify = verify
-
- logger.debug('SRPAuth. verify=%s' % verify)
- logger.debug('server: %s. username=%s' % (server, username))
-
- self.init_data = None
- self.session = requests.session()
-
- self.init_srp()
-
- def init_srp(self):
- usr = srp.User(
- self.username,
- self.password,
- srp.SHA256,
- srp.NG_1024)
- uname, A = usr.start_authentication()
-
- self.srp_usr = usr
- self.A = A
-
- def get_auth_data(self):
- return {
- 'login': self.username,
- 'A': binascii.hexlify(self.A)
- }
-
- def get_init_data(self):
- try:
- init_session = self.session.post(
- self.server + '/1/sessions/',
- data=self.get_auth_data(),
- verify=self.verify)
- except requests.exceptions.ConnectionError:
- raise SRPAuthenticationError(
- "No connection made (salt).")
- except:
- raise SRPAuthenticationError(
- "Unknown error (salt).")
- if init_session.status_code not in (200, ):
- raise SRPAuthenticationError(
- "No valid response (salt).")
-
- self.init_data = init_session.json
- return self.init_data
-
- def get_server_proof_data(self):
- try:
- auth_result = self.session.put(
- #self.server + '/1/sessions.json/' + self.username,
- self.server + '/1/sessions/' + self.username,
- data={'client_auth': binascii.hexlify(self.M)},
- verify=self.verify)
- except requests.exceptions.ConnectionError:
- raise SRPAuthenticationError(
- "No connection made (HAMK).")
-
- if auth_result.status_code not in (200, ):
- raise SRPAuthenticationError(
- "No valid response (HAMK).")
-
- self.auth_data = auth_result.json
- return self.auth_data
-
- def authenticate(self):
- logger.debug('start authentication...')
-
- init_data = self.get_init_data()
- salt = init_data.get('salt', None)
- B = init_data.get('B', None)
-
- # XXX refactor this function
- # move checks and un-hex
- # to routines
-
- if not salt or not B:
- raise SRPAuthenticationError(
- "Server did not send initial data.")
-
- try:
- unhex_salt = safe_unhexlify(salt)
- except TypeError:
- raise SRPAuthenticationError(
- "Bad data from server (salt)")
- try:
- unhex_B = safe_unhexlify(B)
- except TypeError:
- raise SRPAuthenticationError(
- "Bad data from server (B)")
-
- self.M = self.srp_usr.process_challenge(
- unhex_salt,
- unhex_B
- )
-
- proof_data = self.get_server_proof_data()
-
- HAMK = proof_data.get("M2", None)
- if not HAMK:
- errors = proof_data.get('errors', None)
- if errors:
- logger.error(errors)
- raise SRPAuthenticationError("Server did not send HAMK.")
-
- try:
- unhex_HAMK = safe_unhexlify(HAMK)
- except TypeError:
- raise SRPAuthenticationError(
- "Bad data from server (HAMK)")
-
- self.srp_usr.verify_session(
- unhex_HAMK)
-
- try:
- assert self.srp_usr.authenticated()
- logger.debug('user is authenticated!')
- except (AssertionError):
- raise SRPAuthenticationError(
- "Auth verification failed.")
-
- def __call__(self, req):
- self.authenticate()
- req.cookies = self.session.cookies
- return req
-
-
-def srpauth_protected(user=None, passwd=None, server=None, verify=True):
- """
- decorator factory that accepts
- user and password keyword arguments
- and add those to the decorated request
- """
- def srpauth(fn):
- def wrapper(*args, **kwargs):
- if user and passwd:
- auth = SRPAuth(user, passwd, server, verify)
- kwargs['auth'] = auth
- kwargs['verify'] = verify
- if not args:
- logger.warning('attempting to get from empty uri!')
- return fn(*args, **kwargs)
- return wrapper
- return srpauth
-
-
-def get_leap_credentials():
- settings = QtCore.QSettings()
- full_username = settings.value('username')
- username, domain = full_username.split('@')
- seed = settings.value('%s_seed' % domain, None)
- password = leapkeyring.leap_get_password(full_username, seed=seed)
- return (username, password)
-
-
-# XXX TODO
-# Pass verify as single argument,
-# in srpauth_protected style
-
-def magick_srpauth(fn):
- """
- decorator that gets user and password
- from the config file and adds those to
- the decorated request
- """
- logger.debug('magick srp auth decorator called')
-
- def wrapper(*args, **kwargs):
- #uri = args[0]
- # XXX Ugh!
- # Problem with this approach.
- # This won't work when we're using
- # api.foo.bar
- # Unless we keep a table with the
- # equivalencies...
- user, passwd = get_leap_credentials()
-
- # XXX pass verify and server too
- # (pop)
- auth = SRPAuth(user, passwd)
- kwargs['auth'] = auth
- return fn(*args, **kwargs)
- return wrapper
-
-
-if __name__ == "__main__":
- """
- To test against test_provider (twisted version)
- Register an user: (will be valid during the session)
- >>> python auth.py add test password
-
- Test login with that user:
- >>> python auth.py login test password
- """
-
- import sys
-
- if len(sys.argv) not in (4, 5):
- print 'Usage: auth <add|login> <user> <pass> [server]'
- sys.exit(0)
-
- action = sys.argv[1]
- user = sys.argv[2]
- passwd = sys.argv[3]
-
- if len(sys.argv) == 5:
- SERVER = sys.argv[4]
- else:
- SERVER = "https://localhost:8443"
-
- if action == "login":
-
- @srpauth_protected(
- user=user, passwd=passwd, server=SERVER, verify=False)
- def test_srp_protected_get(*args, **kwargs):
- req = requests.get(*args, **kwargs)
- req.raise_for_status
- return req
-
- #req = test_srp_protected_get('https://localhost:8443/1/cert')
- req = test_srp_protected_get('%s/1/cert' % SERVER)
- #print 'cert :', req.content[:200] + "..."
- print req.content
- sys.exit(0)
-
- if action == "add":
- auth = LeapSRPRegister(provider=SERVER, verify=False)
- auth.register_user(user, passwd)
diff --git a/src/leap/base/authentication.py b/src/leap/base/authentication.py
deleted file mode 100644
index 09ff1d07..00000000
--- a/src/leap/base/authentication.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""
-Authentication Base Class
-"""
-
-
-class Authentication(object):
- """
- I have no idea how Authentication (certs,?)
- will be done, but stub it here.
- """
- pass
diff --git a/src/leap/base/checks.py b/src/leap/base/checks.py
deleted file mode 100644
index 7a27f355..00000000
--- a/src/leap/base/checks.py
+++ /dev/null
@@ -1,214 +0,0 @@
-# -*- coding: utf-8 -*-
-import logging
-import platform
-import re
-import socket
-
-import netifaces
-import sh
-
-from leap.base import constants
-from leap.base import exceptions
-
-logger = logging.getLogger(name=__name__)
-_platform = platform.system()
-
-#EVENTS OF NOTE
-EVENT_CONNECT_REFUSED = "[ECONNREFUSED]: Connection refused (code=111)"
-
-ICMP_TARGET = "8.8.8.8"
-
-
-class LeapNetworkChecker(object):
- """
- all network related checks
- """
- def __init__(self, *args, **kwargs):
- provider_gw = kwargs.pop('provider_gw', None)
- self.provider_gateway = provider_gw
-
- def run_all(self, checker=None):
- if not checker:
- checker = self
- #self.error = None # ?
-
- # for MVS
- checker.check_tunnel_default_interface()
- checker.check_internet_connection()
- checker.is_internet_up()
-
- if self.provider_gateway:
- checker.ping_gateway(self.provider_gateway)
-
- checker.parse_log_and_react([], ())
-
- def check_internet_connection(self):
- if _platform == "Linux":
- try:
- output = sh.ping("-c", "5", "-w", "5", ICMP_TARGET)
- # XXX should redirect this to netcheck logger.
- # and don't clutter main log.
- logger.debug('Network appears to be up.')
- except sh.ErrorReturnCode_1 as e:
- packet_loss = re.findall("\d+% packet loss", e.message)[0]
- logger.debug("Unidentified Connection Error: " + packet_loss)
- if not self.is_internet_up():
- error = "No valid internet connection found."
- else:
- error = "Provider server appears to be down."
-
- logger.error(error)
- raise exceptions.NoInternetConnection(error)
-
- else:
- raise NotImplementedError
-
- def is_internet_up(self):
- iface, gateway = self.get_default_interface_gateway()
- try:
- self.ping_gateway(self.provider_gateway)
- except exceptions.NoConnectionToGateway:
- return False
- return True
-
- def _get_route_table_linux(self):
- # do not use context manager, tests pass a StringIO
- f = open("/proc/net/route")
- route_table = f.readlines()
- f.close()
- #toss out header
- route_table.pop(0)
- if not route_table:
- raise exceptions.NoDefaultInterfaceFoundError
- return route_table
-
- def _get_def_iface_osx(self):
- default_iface = None
- #gateway = None
- routes = list(sh.route('-n', 'get', ICMP_TARGET, _iter=True))
- iface = filter(lambda l: "interface" in l, routes)
- if not iface:
- return None, None
- def_ifacel = re.findall('\w+\d', iface[0])
- default_iface = def_ifacel[0] if def_ifacel else None
- if not default_iface:
- return None, None
- _gw = filter(lambda l: "gateway" in l, routes)
- gw = re.findall('\d+\.\d+\.\d+\.\d+', _gw[0])[0]
- return default_iface, gw
-
- def _get_tunnel_iface_linux(self):
- # XXX review.
- # valid also when local router has a default entry?
- route_table = self._get_route_table_linux()
- line = route_table.pop(0)
- iface, destination = line.split('\t')[0:2]
- if not destination == '00000000' or not iface == 'tun0':
- raise exceptions.TunnelNotDefaultRouteError()
- return True
-
- def check_tunnel_default_interface(self):
- """
- Raises an TunnelNotDefaultRouteError
- if tun0 is not the chosen default route
- (including when no routes are present)
- """
- #logger.debug('checking tunnel default interface...')
-
- if _platform == "Linux":
- valid = self._get_tunnel_iface_linux()
- return valid
- elif _platform == "Darwin":
- default_iface, gw = self._get_def_iface_osx()
- #logger.debug('iface: %s', default_iface)
- if default_iface != "tun0":
- logger.debug('tunnel not default route! gw: %s', default_iface)
- # XXX should catch this and act accordingly...
- # but rather, this test should only be launched
- # when we have successfully completed a connection
- # ... TRIGGER: Connection stablished (or whatever it is)
- # in the logs
- raise exceptions.TunnelNotDefaultRouteError
- else:
- #logger.debug('PLATFORM !!! %s', _platform)
- raise NotImplementedError
-
- def _get_def_iface_linux(self):
- default_iface = None
- gateway = None
-
- route_table = self._get_route_table_linux()
- while route_table:
- line = route_table.pop(0)
- iface, destination, gateway = line.split('\t')[0:3]
- if destination == '00000000':
- default_iface = iface
- break
- return default_iface, gateway
-
- def get_default_interface_gateway(self):
- """
- gets the interface we are going thru.
- (this should be merged with check tunnel default interface,
- imo...)
- """
- if _platform == "Linux":
- default_iface, gw = self._get_def_iface_linux()
- elif _platform == "Darwin":
- default_iface, gw = self._get_def_iface_osx()
- else:
- raise NotImplementedError
-
- if not default_iface:
- raise exceptions.NoDefaultInterfaceFoundError
-
- if default_iface not in netifaces.interfaces():
- raise exceptions.InterfaceNotFoundError
- logger.debug('-- default iface %s', default_iface)
- return default_iface, gw
-
- def ping_gateway(self, gateway):
- # TODO: Discuss how much packet loss (%) is acceptable.
-
- # XXX -- validate gateway
- # -- is it a valid ip? (there's something in util)
- # -- is it a domain?
- # -- can we resolve? -- raise NoDNSError if not.
-
- # XXX -- sh.ping implemtation needs review!
- try:
- output = sh.ping("-c", "10", gateway).stdout
- except sh.ErrorReturnCode_1 as e:
- output = e.message
- finally:
- packet_loss = int(re.findall("(\d+)% packet loss", output)[0])
-
- logger.debug('packet loss %s%%' % packet_loss)
- if packet_loss > constants.MAX_ICMP_PACKET_LOSS:
- logger.warning("Packet LOSS = %s" % (packet_loss,))
- #raise exceptions.NoConnectionToGateway
-
- def check_name_resolution(self, domain_name):
- try:
- socket.gethostbyname(domain_name)
- return True
- except socket.gaierror:
- raise exceptions.CannotResolveDomainError
-
- def parse_log_and_react(self, log, error_matrix=None):
- """
- compares the recent openvpn status log to
- strings passed in and executes the callbacks passed in.
- @param log: openvpn log
- @type log: list of strings
- @param error_matrix: tuples of strings and tuples of callbacks
- @type error_matrix: tuples strings and call backs
- """
- for line in log:
- # we could compile a regex here to save some cycles up -- kali
- for each in error_matrix:
- error, callbacks = each
- if error in line:
- for cb in callbacks:
- if callable(cb):
- cb()
diff --git a/src/leap/base/config.py b/src/leap/base/config.py
deleted file mode 100644
index 85bb3d66..00000000
--- a/src/leap/base/config.py
+++ /dev/null
@@ -1,348 +0,0 @@
-"""
-Configuration Base Class
-"""
-import grp
-import json
-import logging
-import re
-import socket
-import time
-import os
-
-logger = logging.getLogger(name=__name__)
-
-from dateutil import parser as dateparser
-from xdg import BaseDirectory
-import requests
-
-from leap.base import exceptions
-from leap.base import constants
-from leap.base.pluggableconfig import PluggableConfig
-from leap.util.fileutil import (mkdir_p)
-
-# move to base!
-from leap.eip import exceptions as eipexceptions
-
-
-class BaseLeapConfig(object):
- slug = None
-
- # XXX we have to enforce that every derived class
- # has a slug (via interface)
- # get property getter that raises NI..
-
- def save(self):
- raise NotImplementedError("abstract base class")
-
- def load(self):
- raise NotImplementedError("abstract base class")
-
- def get_config(self, *kwargs):
- raise NotImplementedError("abstract base class")
-
- @property
- def config(self):
- return self.get_config()
-
- def get_value(self, *kwargs):
- raise NotImplementedError("abstract base class")
-
-
-class MetaConfigWithSpec(type):
- """
- metaclass for JSONLeapConfig classes.
- It creates a configuration spec out of
- the `spec` dictionary. The `properties` attribute
- of the spec dict is turn into the `schema` attribute
- of the new class (which will be used to validate against).
- """
- # XXX in the near future, this is the
- # place where we want to enforce
- # singletons, read-only and similar stuff.
-
- def __new__(meta, classname, bases, classDict):
- schema_obj = classDict.get('spec', None)
-
- # not quite happy with this workaround.
- # I want to raise if missing spec dict, but only
- # for grand-children of this metaclass.
- # maybe should use abc module for this.
- abcderived = ("JSONLeapConfig",)
- if schema_obj is None and classname not in abcderived:
- raise exceptions.ImproperlyConfigured(
- "missing spec dict on your derived class (%s)" % classname)
-
- # we create a configuration spec attribute
- # from the spec dict
- config_class = type(
- classname + "Spec",
- (PluggableConfig, object),
- {'options': schema_obj})
- classDict['spec'] = config_class
-
- return type.__new__(meta, classname, bases, classDict)
-
-##########################################################
-# some hacking still in progress:
-
-# Configs have:
-
-# - a slug (from where a filename/folder is derived)
-# - a spec (for validation and defaults).
-# this spec is conformant to the json-schema.
-# basically a dict that will be used
-# for type casting and validation, and defaults settings.
-
-# all config objects, since they are derived from BaseConfig, implement basic
-# useful methods:
-# - save
-# - load
-
-##########################################################
-
-
-class JSONLeapConfig(BaseLeapConfig):
-
- __metaclass__ = MetaConfigWithSpec
-
- def __init__(self, *args, **kwargs):
- # sanity check
- try:
- assert self.slug is not None
- except AssertionError:
- raise exceptions.ImproperlyConfigured(
- "missing slug on JSONLeapConfig"
- " derived class")
- try:
- assert self.spec is not None
- except AssertionError:
- raise exceptions.ImproperlyConfigured(
- "missing spec on JSONLeapConfig"
- " derived class")
- assert issubclass(self.spec, PluggableConfig)
-
- self.domain = kwargs.pop('domain', None)
- self._config = self.spec(format="json")
- self._config.load()
- self.fetcher = kwargs.pop('fetcher', requests)
-
- # mandatory baseconfig interface
-
- def save(self, to=None, force=False):
- """
- force param will skip the dirty check.
- :type force: bool
- """
- # XXX this force=True does not feel to right
- # but still have to look for a better way
- # of dealing with dirtiness and the
- # trick of loading remote config only
- # when newer.
-
- if force:
- do_save = True
- else:
- do_save = self._config.is_dirty()
-
- if do_save:
- if to is None:
- to = self.filename
- folder, filename = os.path.split(to)
- if folder and not os.path.isdir(folder):
- mkdir_p(folder)
- self._config.serialize(to)
- return True
-
- else:
- return False
-
- def load(self, fromfile=None, from_uri=None, fetcher=None,
- force_download=False, verify=True):
-
- if from_uri is not None:
- fetched = self.fetch(
- from_uri,
- fetcher=fetcher,
- verify=verify,
- force_dl=force_download)
- if fetched:
- return
- if fromfile is None:
- fromfile = self.filename
- if os.path.isfile(fromfile):
- self._config.load(fromfile=fromfile)
- else:
- logger.warning('tried to load config from non-existent path')
- logger.warning('Not Found: %s', fromfile)
-
- def fetch(self, uri, fetcher=None, verify=True, force_dl=False):
- if not fetcher:
- fetcher = self.fetcher
-
- logger.debug('uri: %s (verify: %s)' % (uri, verify))
-
- rargs = (uri, )
- rkwargs = {'verify': verify}
- headers = {}
-
- curmtime = self.get_mtime() if not force_dl else None
- if curmtime:
- logger.debug('requesting with if-modified-since %s' % curmtime)
- headers['if-modified-since'] = curmtime
- rkwargs['headers'] = headers
-
- #request = fetcher.get(uri, verify=verify)
- request = fetcher.get(*rargs, **rkwargs)
- request.raise_for_status()
-
- if request.status_code == 304:
- logger.debug('...304 Not Changed')
- # On this point, we have to assume that
- # we HAD the filename. If that filename is corruct,
- # we should enforce a force_download in the load
- # method above.
- self._config.load(fromfile=self.filename)
- return True
-
- if request.json:
- mtime = None
- last_modified = request.headers.get('last-modified', None)
- if last_modified:
- _mtime = dateparser.parse(last_modified)
- mtime = int(_mtime.strftime("%s"))
- if callable(request.json):
- _json = request.json()
- else:
- # back-compat
- _json = request.json
- self._config.load(json.dumps(_json), mtime=mtime)
- self._config.set_dirty()
- else:
- # not request.json
- # might be server did not announce content properly,
- # let's try deserializing all the same.
- try:
- self._config.load(request.content)
- self._config.set_dirty()
- except ValueError:
- raise eipexceptions.LeapBadConfigFetchedError
-
- return True
-
- def get_mtime(self):
- try:
- _mtime = os.stat(self.filename)[8]
- mtime = time.strftime("%c GMT", time.gmtime(_mtime))
- return mtime
- except OSError:
- return None
-
- def get_config(self):
- return self._config.config
-
- # public methods
-
- def get_filename(self):
- return self._slug_to_filename()
-
- @property
- def filename(self):
- return self.get_filename()
-
- def validate(self, data):
- logger.debug('validating schema')
- self._config.validate(data)
- return True
-
- # private
-
- def _slug_to_filename(self):
- # is this going to work in winland if slug is "foo/bar" ?
- folder, filename = os.path.split(self.slug)
- config_file = get_config_file(filename, folder)
- return config_file
-
- def exists(self):
- return os.path.isfile(self.filename)
-
-
-#
-# utility functions
-#
-# (might be moved to some class as we see fit, but
-# let's remain functional for a while)
-# maybe base.config.util ??
-#
-
-
-def get_config_dir():
- """
- get the base dir for all leap config
- @rparam: config path
- @rtype: string
- """
- home = os.path.expanduser("~")
- if re.findall("leap_tests-[_a-zA-Z0-9]{6}", home):
- # we're inside a test! :)
- return os.path.join(home, ".config/leap")
- else:
- # XXX dirspec is cross-platform,
- # we should borrow some of those
- # routines for osx/win and wrap this call.
- return os.path.join(BaseDirectory.xdg_config_home,
- 'leap')
-
-
-def get_config_file(filename, folder=None):
- """
- concatenates the given filename
- with leap config dir.
- @param filename: name of the file
- @type filename: string
- @rparam: full path to config file
- """
- path = []
- path.append(get_config_dir())
- if folder is not None:
- path.append(folder)
- path.append(filename)
- return os.path.join(*path)
-
-
-def get_default_provider_path():
- default_subpath = os.path.join("providers",
- constants.DEFAULT_PROVIDER)
- default_provider_path = get_config_file(
- '',
- folder=default_subpath)
- return default_provider_path
-
-
-def get_provider_path(domain):
- # XXX if not domain, return get_default_provider_path
- default_subpath = os.path.join("providers", domain)
- provider_path = get_config_file(
- '',
- folder=default_subpath)
- return provider_path
-
-
-def validate_ip(ip_str):
- """
- raises exception if the ip_str is
- not a valid representation of an ip
- """
- socket.inet_aton(ip_str)
-
-
-def get_username():
- try:
- return os.getlogin()
- except OSError as e:
- import pwd
- return pwd.getpwuid(os.getuid())[0]
-
-
-def get_groupname():
- gid = os.getgroups()[-1]
- return grp.getgrgid(gid).gr_name
diff --git a/src/leap/base/connection.py b/src/leap/base/connection.py
deleted file mode 100644
index 41d13935..00000000
--- a/src/leap/base/connection.py
+++ /dev/null
@@ -1,115 +0,0 @@
-"""
-Base Connection Classs
-"""
-from __future__ import (division, unicode_literals, print_function)
-
-import logging
-
-from leap.base.authentication import Authentication
-
-logger = logging.getLogger(name=__name__)
-
-
-class Connection(Authentication):
- # JSONLeapConfig
- #spec = {}
-
- def __init__(self, *args, **kwargs):
- self.connection_state = None
- self.desired_connection_state = None
- #XXX FIXME diamond inheritance gotcha..
- #If you inherit from >1 class,
- #super is only initializing one
- #of the bases..!!
- # I think we better pass config as a constructor
- # parameter -- kali 2012-08-30 04:33
- super(Connection, self).__init__(*args, **kwargs)
-
- def connect(self):
- """
- entry point for connection process
- """
- pass
-
- def disconnect(self):
- """
- disconnects client
- """
- pass
-
- #def shutdown(self):
- #"""
- #shutdown and quit
- #"""
- #self.desired_con_state = self.status.DISCONNECTED
-
- def connection_state(self):
- """
- returns the current connection state
- """
- return self.status.current
-
- def desired_connection_state(self):
- """
- returns the desired_connection state
- """
- return self.desired_connection_state
-
- def get_icon_name(self):
- """
- get icon name from status object
- """
- return self.status.get_state_icon()
-
- #
- # private methods
- #
-
- def _disconnect(self):
- """
- private method for disconnecting
- """
- if self.subp is not None:
- self.subp.terminate()
- self.subp = None
- # XXX signal state changes! :)
-
- def _is_alive(self):
- """
- don't know yet
- """
- pass
-
- def _connect(self):
- """
- entry point for connection cascade methods.
- """
- #conn_result = ConState.DISCONNECTED
- try:
- conn_result = self._try_connection()
- except UnrecoverableError as except_msg:
- logger.error("FATAL: %s" % unicode(except_msg))
- conn_result = self.status.UNRECOVERABLE
- except Exception as except_msg:
- self.error_queue.append(except_msg)
- logger.error("Failed Connection: %s" %
- unicode(except_msg))
- return conn_result
-
-
-class ConnectionError(Exception):
- """
- generic connection error
- """
- def __str__(self):
- if len(self.args) >= 1:
- return repr(self.args[0])
- else:
- raise self()
-
-
-class UnrecoverableError(ConnectionError):
- """
- we cannot do anything about it, sorry
- """
- pass
diff --git a/src/leap/base/constants.py b/src/leap/base/constants.py
deleted file mode 100644
index f5665e5f..00000000
--- a/src/leap/base/constants.py
+++ /dev/null
@@ -1,42 +0,0 @@
-"""constants to be used in base module"""
-from leap import __branding
-APP_NAME = __branding.get("short_name", "leap-client")
-OPENVPN_BIN = "openvpn"
-
-# default provider placeholder
-# using `example.org` we make sure that this
-# is not going to be resolved during the tests phases
-# (we expect testers to add it to their /etc/hosts
-
-DEFAULT_PROVIDER = __branding.get(
- "provider_domain",
- "testprovider.example.org")
-
-DEFINITION_EXPECTED_PATH = "provider.json"
-
-DEFAULT_PROVIDER_DEFINITION = {
- u"api_uri": "https://api.%s/" % DEFAULT_PROVIDER,
- u"api_version": u"1",
- u"ca_cert_fingerprint": "SHA256: fff",
- u"ca_cert_uri": u"https://%s/ca.crt" % DEFAULT_PROVIDER,
- u"default_language": u"en",
- u"description": {
- u"en": u"A demonstration service provider using the LEAP platform"
- },
- u"domain": "%s" % DEFAULT_PROVIDER,
- u"enrollment_policy": u"open",
- u"languages": [
- u"en"
- ],
- u"name": {
- u"en": u"Test Provider"
- },
- u"services": [
- "openvpn"
- ]
-}
-
-
-MAX_ICMP_PACKET_LOSS = 10
-
-ROUTE_CHECK_INTERVAL = 10
diff --git a/src/leap/base/exceptions.py b/src/leap/base/exceptions.py
deleted file mode 100644
index 3d8dde48..00000000
--- a/src/leap/base/exceptions.py
+++ /dev/null
@@ -1,97 +0,0 @@
-"""
-Exception attributes and their meaning/uses
--------------------------------------------
-
-* critical: if True, will abort execution prematurely,
- after attempting any cleaning
- action.
-
-* failfirst: breaks any error_check loop that is examining
- the error queue.
-
-* message: the message that will be used in the __repr__ of the exception.
-
-* usermessage: the message that will be passed to user in ErrorDialogs
- in Qt-land.
-"""
-from leap.util.translations import translate
-
-
-class LeapException(Exception):
- """
- base LeapClient exception
- sets some parameters that we will check
- during error checking routines
- """
-
- critical = False
- failfirst = False
- warning = False
-
-
-class CriticalError(LeapException):
- """
- we cannot do anything about it
- """
- critical = True
- failfirst = True
-
-
-# In use ???
-# don't thing so. purge if not...
-
-class MissingConfigFileError(Exception):
- pass
-
-
-class ImproperlyConfigured(Exception):
- pass
-
-
-# NOTE: "Errors" (context) has to be a explicit string!
-
-
-class InterfaceNotFoundError(LeapException):
- # XXX should take iface arg on init maybe?
- message = "interface not found"
- usermessage = translate(
- "Errors",
- "Interface not found")
-
-
-class NoDefaultInterfaceFoundError(LeapException):
- message = "no default interface found"
- usermessage = translate(
- "Errors",
- "Looks like your computer "
- "is not connected to the internet")
-
-
-class NoConnectionToGateway(LeapException):
- message = "no connection to gateway"
- usermessage = translate(
- "Errors",
- "Looks like there are problems "
- "with your internet connection")
-
-
-class NoInternetConnection(LeapException):
- message = "No Internet connection found"
- usermessage = translate(
- "Errors",
- "It looks like there is no internet connection.")
- # and now we try to connect to our web to troubleshoot LOL :P
-
-
-class CannotResolveDomainError(LeapException):
- message = "Cannot resolve domain"
- usermessage = translate(
- "Errors",
- "Domain cannot be found")
-
-
-class TunnelNotDefaultRouteError(LeapException):
- message = "Tunnel connection dissapeared. VPN down?"
- usermessage = translate(
- "Errors",
- "The Encrypted Connection was lost.")
diff --git a/src/leap/base/network.py b/src/leap/base/network.py
deleted file mode 100644
index d841e692..00000000
--- a/src/leap/base/network.py
+++ /dev/null
@@ -1,107 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import (print_function)
-import logging
-import threading
-
-from leap.eip import config as eipconfig
-from leap.base.checks import LeapNetworkChecker
-from leap.base.constants import ROUTE_CHECK_INTERVAL
-from leap.base.exceptions import TunnelNotDefaultRouteError
-from leap.util.misc import null_check
-from leap.util.coroutines import (launch_thread, process_events)
-
-from time import sleep
-
-logger = logging.getLogger(name=__name__)
-
-
-class NetworkCheckerThread(object):
- """
- Manages network checking thread that makes sure we have a working network
- connection.
- """
- def __init__(self, *args, **kwargs):
-
- self.status_signals = kwargs.pop('status_signals', None)
- self.error_cb = kwargs.pop(
- 'error_cb',
- lambda exc: logger.error("%s", exc.message))
- self.shutdown = threading.Event()
-
- # XXX get provider passed here
- provider = kwargs.pop('provider', None)
- null_check(provider, 'provider')
-
- eipconf = eipconfig.EIPConfig(domain=provider)
- eipconf.load()
- eipserviceconf = eipconfig.EIPServiceConfig(domain=provider)
- eipserviceconf.load()
-
- gw = eipconfig.get_eip_gateway(
- eipconfig=eipconf,
- eipserviceconfig=eipserviceconf)
- self.checker = LeapNetworkChecker(
- provider_gw=gw)
-
- def start(self):
- self.process_handle = self._launch_recurrent_network_checks(
- (self.error_cb,))
-
- def stop(self):
- self.process_handle.join(timeout=0.1)
- self.shutdown.set()
- logger.debug("network checked stopped.")
-
- def run_checks(self):
- pass
-
- #private methods
-
- #here all the observers in fail_callbacks expect one positional argument,
- #which is exception so we can try by passing a lambda with logger to
- #check it works.
-
- def _network_checks_thread(self, fail_callbacks):
- #TODO: replace this with waiting for a signal from openvpn
- while True:
- try:
- self.checker.check_tunnel_default_interface()
- break
- except TunnelNotDefaultRouteError:
- # XXX ??? why do we sleep here???
- # aa: If the openvpn isn't up and running yet,
- # let's give it a moment to breath.
- #logger.error('NOT DEFAULT ROUTE!----')
- # Instead of this, we should flag when the
- # iface IS SUPPOSED to be up imo. -- kali
- sleep(1)
-
- fail_observer_dict = dict(((
- observer,
- process_events(observer)) for observer in fail_callbacks))
-
- while not self.shutdown.is_set():
- try:
- self.checker.check_tunnel_default_interface()
- self.checker.check_internet_connection()
- sleep(ROUTE_CHECK_INTERVAL)
- except Exception as exc:
- for obs in fail_observer_dict:
- fail_observer_dict[obs].send(exc)
- sleep(ROUTE_CHECK_INTERVAL)
-
- #reset event
- # I see a problem with this. You cannot stop it, it
- # resets itself forever. -- kali
-
- # XXX use QTimer for the recurrent triggers,
- # and ditch the sleeps.
- logger.debug('resetting event')
- self.shutdown.clear()
-
- def _launch_recurrent_network_checks(self, fail_callbacks):
- # XXX reimplement using QTimer -- kali
- watcher = launch_thread(
- self._network_checks_thread,
- (fail_callbacks,))
- return watcher
diff --git a/src/leap/base/pluggableconfig.py b/src/leap/base/pluggableconfig.py
deleted file mode 100644
index 6f9f3f6f..00000000
--- a/src/leap/base/pluggableconfig.py
+++ /dev/null
@@ -1,462 +0,0 @@
-"""
-generic configuration handlers
-"""
-import copy
-import json
-import logging
-import os
-import time
-import urlparse
-
-import jsonschema
-
-from leap.util.translations import LEAPTranslatable
-
-logger = logging.getLogger(__name__)
-
-
-__all__ = ['PluggableConfig',
- 'adaptors',
- 'types',
- 'UnknownOptionException',
- 'MissingValueException',
- 'ConfigurationProviderException',
- 'TypeCastException']
-
-# exceptions
-
-
-class ValidationError(Exception):
- pass
-
-
-class UnknownOptionException(Exception):
- """exception raised when a non-configuration
- value is present in the configuration"""
-
-
-class MissingValueException(Exception):
- """exception raised when a required value is missing"""
-
-
-class ConfigurationProviderException(Exception):
- """exception raised when a configuration provider is missing, etc"""
-
-
-class TypeCastException(Exception):
- """exception raised when a
- configuration item cannot be coerced to a type"""
-
-
-class ConfigAdaptor(object):
- """
- abstract base class for config adaotors for
- serialization/deserialization and custom validation
- and type casting.
- """
- def read(self, filename):
- raise NotImplementedError("abstract base class")
-
- def write(self, config, filename):
- with open(filename, 'w') as f:
- self._write(f, config)
-
- def _write(self, fp, config):
- raise NotImplementedError("abstract base class")
-
- def validate(self, config, schema):
- raise NotImplementedError("abstract base class")
-
-
-adaptors = {}
-
-
-class JSONSchemaEncoder(json.JSONEncoder):
- """
- custom default encoder that
- casts python objects to json objects for
- the schema validation
- """
- def default(self, obj):
- if obj is str:
- return 'string'
- if obj is unicode:
- return 'string'
- if obj is int:
- return 'integer'
- if obj is list:
- return 'array'
- if obj is dict:
- return 'object'
- if obj is bool:
- return 'boolean'
-
-
-class JSONAdaptor(ConfigAdaptor):
- indent = 2
- extensions = ['json']
-
- def read(self, _from):
- if isinstance(_from, file):
- _from_string = _from.read()
- if isinstance(_from, str):
- _from_string = _from
- return json.loads(_from_string)
-
- def _write(self, fp, config):
- fp.write(json.dumps(config,
- indent=self.indent,
- sort_keys=True))
-
- def validate(self, config, schema_obj):
- schema_json = JSONSchemaEncoder().encode(schema_obj)
- schema = json.loads(schema_json)
- try:
- jsonschema.validate(config, schema)
- except jsonschema.ValidationError:
- raise ValidationError
-
-
-adaptors['json'] = JSONAdaptor()
-
-#
-# Adaptors
-#
-# Allow to apply a predefined set of types to the
-# specs, so it checks the validity of formats and cast it
-# to proper python types.
-
-# TODO:
-# - HTTPS uri
-
-
-class DateType(object):
- fmt = '%Y-%m-%d'
-
- def to_python(self, data):
- return time.strptime(data, self.fmt)
-
- def get_prep_value(self, data):
- return time.strftime(self.fmt, data)
-
-
-class TranslatableType(object):
- """
- a type that casts to LEAPTranslatable objects.
- Used for labels we get from providers and stuff.
- """
-
- def to_python(self, data):
- return LEAPTranslatable(data)
-
- # needed? we already have an extended dict...
- #def get_prep_value(self, data):
- #return dict(data)
-
-
-class URIType(object):
-
- def to_python(self, data):
- parsed = urlparse.urlparse(data)
- if not parsed.scheme:
- raise TypeCastException("uri %s has no schema" % data)
- return parsed
-
- def get_prep_value(self, data):
- return data.geturl()
-
-
-class HTTPSURIType(object):
-
- def to_python(self, data):
- parsed = urlparse.urlparse(data)
- if not parsed.scheme:
- raise TypeCastException("uri %s has no schema" % data)
- if parsed.scheme != "https":
- raise TypeCastException(
- "uri %s does not has "
- "https schema" % data)
- return parsed
-
- def get_prep_value(self, data):
- return data.geturl()
-
-
-types = {
- 'date': DateType(),
- 'uri': URIType(),
- 'https-uri': HTTPSURIType(),
- 'translatable': TranslatableType(),
-}
-
-
-class PluggableConfig(object):
-
- options = {}
-
- def __init__(self,
- adaptors=adaptors,
- types=types,
- format=None):
-
- self.config = {}
- self.adaptors = adaptors
- self.types = types
- self._format = format
- self.mtime = None
- self.dirty = False
-
- @property
- def option_dict(self):
- if hasattr(self, 'options') and isinstance(self.options, dict):
- return self.options.get('properties', None)
-
- def items(self):
- """
- act like an iterator
- """
- if isinstance(self.option_dict, dict):
- return self.option_dict.items()
- return self.options
-
- def validate(self, config, format=None):
- """
- validate config
- """
- schema = self.options
- if format is None:
- format = self._format
-
- if format:
- adaptor = self.get_adaptor(self._format)
- adaptor.validate(config, schema)
- else:
- # we really should make format mandatory...
- logger.error('no format passed to validate')
-
- # first round of validation is ok.
- # now we proceed to cast types if any specified.
- self.to_python(config)
-
- def to_python(self, config):
- """
- cast types following first type and then format indications.
- """
- unseen_options = [i for i in config if i not in self.option_dict]
- if unseen_options:
- raise UnknownOptionException(
- "Unknown options: %s" % ', '.join(unseen_options))
-
- for key, value in config.items():
- _type = self.option_dict[key].get('type')
- if _type is None and 'default' in self.option_dict[key]:
- _type = type(self.option_dict[key]['default'])
- if _type is not None:
- tocast = True
- if not callable(_type) and isinstance(value, _type):
- tocast = False
- if tocast:
- try:
- config[key] = _type(value)
- except BaseException, e:
- raise TypeCastException(
- "Could not coerce %s, %s, "
- "to type %s: %s" % (key, value, _type.__name__, e))
- _format = self.option_dict[key].get('format', None)
- _ftype = self.types.get(_format, None)
- if _ftype:
- try:
- config[key] = _ftype.to_python(value)
- except BaseException, e:
- raise TypeCastException(
- "Could not coerce %s, %s, "
- "to format %s: %s" % (key, value,
- _ftype.__class__.__name__,
- e))
-
- return config
-
- def prep_value(self, config):
- """
- the inverse of to_python method,
- called just before serialization
- """
- for key, value in config.items():
- _format = self.option_dict[key].get('format', None)
- _ftype = self.types.get(_format, None)
- if _ftype and hasattr(_ftype, 'get_prep_value'):
- try:
- config[key] = _ftype.get_prep_value(value)
- except BaseException, e:
- raise TypeCastException(
- "Could not serialize %s, %s, "
- "by format %s: %s" % (key, value,
- _ftype.__class__.__name__,
- e))
- else:
- config[key] = value
- return config
-
- # methods for adding configuration
-
- def get_default_values(self):
- """
- return a config options from configuration defaults
- """
- defaults = {}
- for key, value in self.items():
- if 'default' in value:
- defaults[key] = value['default']
- return copy.deepcopy(defaults)
-
- def get_adaptor(self, format):
- """
- get specified format adaptor or
- guess for a given filename
- """
- adaptor = self.adaptors.get(format, None)
- if adaptor:
- return adaptor
-
- # not registered in adaptors dict, let's try all
- for adaptor in self.adaptors.values():
- if format in adaptor.extensions:
- return adaptor
-
- def filename2format(self, filename):
- extension = os.path.splitext(filename)[-1]
- return extension.lstrip('.') or None
-
- def serialize(self, filename, format=None, full=False):
- if not format:
- format = self._format
- if not format:
- format = self.filename2format(filename)
- if not format:
- raise Exception('Please specify a format')
- # TODO: more specific exception type
-
- adaptor = self.get_adaptor(format)
- if not adaptor:
- raise Exception("Adaptor not found for format: %s" % format)
-
- config = copy.deepcopy(self.config)
- serializable = self.prep_value(config)
- adaptor.write(serializable, filename)
-
- if self.mtime:
- self.touch_mtime(filename)
-
- def touch_mtime(self, filename):
- mtime = self.mtime
- os.utime(filename, (mtime, mtime))
-
- def deserialize(self, string=None, fromfile=None, format=None):
- """
- load configuration from a file or string
- """
-
- def _try_deserialize():
- if fromfile:
- with open(fromfile, 'r') as f:
- content = adaptor.read(f)
- elif string:
- content = adaptor.read(string)
- return content
-
- # XXX cleanup this!
-
- if fromfile:
- assert os.path.exists(fromfile)
- if not format:
- format = self.filename2format(fromfile)
-
- if not format:
- format = self._format
- if format:
- adaptor = self.get_adaptor(format)
- else:
- adaptor = None
-
- if adaptor:
- content = _try_deserialize()
- return content
-
- # no adaptor, let's try rest of adaptors
-
- adaptors = self.adaptors[:]
-
- if format:
- adaptors.sort(
- key=lambda x: int(
- format in x.extensions),
- reverse=True)
-
- for adaptor in adaptors:
- content = _try_deserialize()
- return content
-
- def set_dirty(self):
- self.dirty = True
-
- def is_dirty(self):
- return self.dirty
-
- def load(self, *args, **kwargs):
- """
- load from string or file
- if no string of fromfile option is given,
- it will attempt to load from defaults
- defined in the schema.
- """
- string = args[0] if args else None
- fromfile = kwargs.get("fromfile", None)
- mtime = kwargs.pop("mtime", None)
- self.mtime = mtime
- content = None
-
- # start with defaults, so we can
- # have partial values applied.
- content = self.get_default_values()
- if string and isinstance(string, str):
- content = self.deserialize(string)
-
- if not string and fromfile is not None:
- #import ipdb;ipdb.set_trace()
- content = self.deserialize(fromfile=fromfile)
-
- if not content:
- logger.error('no content could be loaded')
- # XXX raise!
- return
-
- # lazy evaluation until first level of nesting
- # to allow lambdas with context-dependant info
- # like os.path.expanduser
- for k, v in content.iteritems():
- if callable(v):
- content[k] = v()
-
- self.validate(content)
- self.config = content
- return True
-
-
-def testmain(): # pragma: no cover
-
- from tests import test_validation as t
- import pprint
-
- config = PluggableConfig(_format="json")
- properties = copy.deepcopy(t.sample_spec)
-
- config.options = properties
- config.load(fromfile='data.json')
-
- print 'config'
- pprint.pprint(config.config)
-
- config.serialize('/tmp/testserial.json')
-
-if __name__ == "__main__":
- testmain()
diff --git a/src/leap/base/providers.py b/src/leap/base/providers.py
deleted file mode 100644
index d41f3695..00000000
--- a/src/leap/base/providers.py
+++ /dev/null
@@ -1,29 +0,0 @@
-"""all dealing with leap-providers: definition files, updating"""
-from leap.base import config as baseconfig
-from leap.base import specs
-
-
-class LeapProviderDefinition(baseconfig.JSONLeapConfig):
- spec = specs.leap_provider_spec
-
- def _get_slug(self):
- domain = getattr(self, 'domain', None)
- if domain:
- path = baseconfig.get_provider_path(domain)
- else:
- path = baseconfig.get_default_provider_path()
-
- return baseconfig.get_config_file(
- 'provider.json', folder=path)
-
- def _set_slug(self, *args, **kwargs):
- raise AttributeError("you cannot set slug")
-
- slug = property(_get_slug, _set_slug)
-
-
-class LeapProviderSet(object):
- # we gather them from the filesystem
- # TODO: (MVS+)
- def __init__(self):
- self.count = 0
diff --git a/src/leap/base/tests/test_auth.py b/src/leap/base/tests/test_auth.py
deleted file mode 100644
index b3009a9b..00000000
--- a/src/leap/base/tests/test_auth.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from BaseHTTPServer import BaseHTTPRequestHandler
-import urlparse
-try:
- import unittest2 as unittest
-except ImportError:
- import unittest
-
-import requests
-#from mock import Mock
-
-from leap.base import auth
-#from leap.base import exceptions
-from leap.eip.tests.test_checks import NoLogRequestHandler
-from leap.testing.basetest import BaseLeapTest
-from leap.testing.https_server import BaseHTTPSServerTestCase
-
-
-class LeapSRPRegisterTests(BaseHTTPSServerTestCase, BaseLeapTest):
- __name__ = "leap_srp_register_test"
- provider = "testprovider.example.org"
-
- class request_handler(NoLogRequestHandler, BaseHTTPRequestHandler):
- responses = {
- '/': ['OK', '']}
-
- def do_GET(self):
- path = urlparse.urlparse(self.path)
- message = '\n'.join(self.responses.get(
- path.path, None))
- self.send_response(200)
- self.end_headers()
- self.wfile.write(message)
-
- def setUp(self):
- pass
-
- def tearDown(self):
- pass
-
- def test_srp_auth_should_implement_check_methods(self):
- SERVER = "https://localhost:8443"
- srp_auth = auth.LeapSRPRegister(provider=SERVER, verify=False)
-
- self.assertTrue(hasattr(srp_auth, "init_session"),
- "missing meth")
- self.assertTrue(hasattr(srp_auth, "get_registration_uri"),
- "missing meth")
- self.assertTrue(hasattr(srp_auth, "register_user"),
- "missing meth")
-
- def test_srp_auth_basic_functionality(self):
- SERVER = "https://localhost:8443"
- srp_auth = auth.LeapSRPRegister(provider=SERVER, verify=False)
-
- self.assertIsInstance(srp_auth.session, requests.sessions.Session)
- self.assertEqual(
- srp_auth.get_registration_uri(),
- "https://localhost:8443/1/users")
diff --git a/src/leap/base/tests/test_checks.py b/src/leap/base/tests/test_checks.py
deleted file mode 100644
index 838ab413..00000000
--- a/src/leap/base/tests/test_checks.py
+++ /dev/null
@@ -1,189 +0,0 @@
-try:
- import unittest2 as unittest
-except ImportError:
- import unittest
-import os
-import sh
-
-from mock import (patch, Mock)
-from StringIO import StringIO
-
-from leap.base import checks
-from leap.base import exceptions
-from leap.testing.basetest import BaseLeapTest
-
-_uid = os.getuid()
-
-
-class LeapNetworkCheckTest(BaseLeapTest):
- __name__ = "leap_network_check_tests"
-
- def setUp(self):
- os.environ['PATH'] += ':/bin'
- pass
-
- def tearDown(self):
- pass
-
- def test_checker_should_implement_check_methods(self):
- checker = checks.LeapNetworkChecker()
-
- self.assertTrue(hasattr(checker, "check_internet_connection"),
- "missing meth")
- self.assertTrue(hasattr(checker, "check_tunnel_default_interface"),
- "missing meth")
- self.assertTrue(hasattr(checker, "is_internet_up"),
- "missing meth")
- self.assertTrue(hasattr(checker, "ping_gateway"),
- "missing meth")
- self.assertTrue(hasattr(checker, "parse_log_and_react"),
- "missing meth")
-
- def test_checker_should_actually_call_all_tests(self):
- checker = checks.LeapNetworkChecker()
- mc = Mock()
- checker.run_all(checker=mc)
- self.assertTrue(mc.check_internet_connection.called, "not called")
- self.assertTrue(mc.check_tunnel_default_interface.called, "not called")
- self.assertTrue(mc.is_internet_up.called, "not called")
- self.assertTrue(mc.parse_log_and_react.called, "not called")
-
- # ping gateway only called if we pass provider_gw
- checker = checks.LeapNetworkChecker(provider_gw="0.0.0.0")
- mc = Mock()
- checker.run_all(checker=mc)
- self.assertTrue(mc.check_internet_connection.called, "not called")
- self.assertTrue(mc.check_tunnel_default_interface.called, "not called")
- self.assertTrue(mc.ping_gateway.called, "not called")
- self.assertTrue(mc.is_internet_up.called, "not called")
- self.assertTrue(mc.parse_log_and_react.called, "not called")
-
- def test_get_default_interface_no_interface(self):
- checker = checks.LeapNetworkChecker()
- with patch('leap.base.checks.open', create=True) as mock_open:
- with self.assertRaises(exceptions.NoDefaultInterfaceFoundError):
- mock_open.return_value = StringIO(
- "Iface\tDestination Gateway\t"
- "Flags\tRefCntd\tUse\tMetric\t"
- "Mask\tMTU\tWindow\tIRTT")
- checker.get_default_interface_gateway()
-
- def test_check_tunnel_default_interface(self):
- checker = checks.LeapNetworkChecker()
- with patch('leap.base.checks.open', create=True) as mock_open:
- with self.assertRaises(exceptions.TunnelNotDefaultRouteError):
- mock_open.return_value = StringIO(
- "Iface\tDestination Gateway\t"
- "Flags\tRefCntd\tUse\tMetric\t"
- "Mask\tMTU\tWindow\tIRTT\n"
- "wlan0\t00000000\t0102A8C0\t"
- "0003\t0\t0\t0\t00000000\t0\t0\t0")
- checker.check_tunnel_default_interface()
-
- with patch('leap.base.checks.open', create=True) as mock_open:
- mock_open.return_value = StringIO(
- "Iface\tDestination Gateway\t"
- "Flags\tRefCntd\tUse\tMetric\t"
- "Mask\tMTU\tWindow\tIRTT\n"
- "tun0\t00000000\t01002A0A\t0003\t0\t0\t0\t00000080\t0\t0\t0")
- checker.check_tunnel_default_interface()
-
- @unittest.skip
- def test_ping_gateway_fail(self):
- """
- this fucking test is failing inside a chroot
- """
- checker = checks.LeapNetworkChecker()
- with patch.object(sh, "ping") as mocked_ping:
- with self.assertRaises(exceptions.NoConnectionToGateway):
- mocked_ping.return_value = Mock
- mocked_ping.return_value.stdout = "11% packet loss"
- checker.ping_gateway("4.2.2.2")
-
- @unittest.skip
- def test_ping_gateway(self):
- """
- this fucking test is failing inside a chroot
- """
- checker = checks.LeapNetworkChecker()
- with patch.object(sh, "ping") as mocked_ping:
- mocked_ping.return_value = Mock
- mocked_ping.return_value.stdout = """
-PING 4.2.2.2 (4.2.2.2) 56(84) bytes of data.
-64 bytes from 4.2.2.2: icmp_req=1 ttl=54 time=33.8 ms
-64 bytes from 4.2.2.2: icmp_req=2 ttl=54 time=30.6 ms
-64 bytes from 4.2.2.2: icmp_req=3 ttl=54 time=31.4 ms
-64 bytes from 4.2.2.2: icmp_req=4 ttl=54 time=36.1 ms
-64 bytes from 4.2.2.2: icmp_req=5 ttl=54 time=30.8 ms
-64 bytes from 4.2.2.2: icmp_req=6 ttl=54 time=30.4 ms
-64 bytes from 4.2.2.2: icmp_req=7 ttl=54 time=30.7 ms
-64 bytes from 4.2.2.2: icmp_req=8 ttl=54 time=32.7 ms
-64 bytes from 4.2.2.2: icmp_req=9 ttl=54 time=31.4 ms
-64 bytes from 4.2.2.2: icmp_req=10 ttl=54 time=33.3 ms
-
---- 4.2.2.2 ping statistics ---
-10 packets transmitted, 10 received, 0% packet loss, time 9016ms
-rtt min/avg/max/mdev = 30.497/32.172/36.161/1.755 ms"""
- checker.ping_gateway("4.2.2.2")
-
- @unittest.skip
- def test_check_internet_connection_failures(self):
- """
- this fucking test is failing inside a chroot
- """
- checker = checks.LeapNetworkChecker()
- TimeoutError = get_ping_timeout_error()
- with patch.object(sh, "ping") as mocked_ping:
- mocked_ping.side_effect = TimeoutError
- with self.assertRaises(exceptions.NoInternetConnection):
- with patch.object(checker, "ping_gateway") as mock_gateway:
- mock_gateway.side_effect = exceptions.NoConnectionToGateway
- checker.check_internet_connection()
-
- with patch.object(sh, "ping") as mocked_ping:
- mocked_ping.side_effect = TimeoutError
- with self.assertRaises(exceptions.NoInternetConnection):
- with patch.object(checker, "ping_gateway") as mock_gateway:
- mock_gateway.return_value = True
- checker.check_internet_connection()
-
- def test_parse_log_and_react(self):
- checker = checks.LeapNetworkChecker()
- to_call = Mock()
- log = [("leap.openvpn - INFO - Mon Nov 19 13:36:24 2012 "
- "read UDPv4 [ECONNREFUSED]: Connection refused (code=111)")]
- err_matrix = [(checks.EVENT_CONNECT_REFUSED, (to_call, ))]
- checker.parse_log_and_react(log, err_matrix)
- self.assertTrue(to_call.called)
-
- log = [("2012-11-19 13:36:26,177 - leap.openvpn - INFO - "
- "Mon Nov 19 13:36:24 2012 ERROR: Linux route delete command "
- "failed: external program exited"),
- ("2012-11-19 13:36:26,178 - leap.openvpn - INFO - "
- "Mon Nov 19 13:36:24 2012 ERROR: Linux route delete command "
- "failed: external program exited"),
- ("2012-11-19 13:36:26,180 - leap.openvpn - INFO - "
- "Mon Nov 19 13:36:24 2012 ERROR: Linux route delete command "
- "failed: external program exited"),
- ("2012-11-19 13:36:26,181 - leap.openvpn - INFO - "
- "Mon Nov 19 13:36:24 2012 /sbin/ifconfig tun0 0.0.0.0"),
- ("2012-11-19 13:36:26,182 - leap.openvpn - INFO - "
- "Mon Nov 19 13:36:24 2012 Linux ip addr del failed: external "
- "program exited with error stat"),
- ("2012-11-19 13:36:26,183 - leap.openvpn - INFO - "
- "Mon Nov 19 13:36:26 2012 SIGTERM[hard,] received, process"
- "exiting"), ]
- to_call.reset_mock()
- checker.parse_log_and_react(log, err_matrix)
- self.assertFalse(to_call.called)
-
- to_call.reset_mock()
- checker.parse_log_and_react([], err_matrix)
- self.assertFalse(to_call.called)
-
-
-def get_ping_timeout_error():
- try:
- sh.ping("-c", "1", "-w", "1", "8.8.7.7")
- except Exception as e:
- return e
diff --git a/src/leap/base/tests/test_config.py b/src/leap/base/tests/test_config.py
deleted file mode 100644
index d03149b2..00000000
--- a/src/leap/base/tests/test_config.py
+++ /dev/null
@@ -1,247 +0,0 @@
-import json
-import os
-import platform
-import socket
-#import tempfile
-
-import mock
-import requests
-
-from leap.base import config
-from leap.base import constants
-from leap.base import exceptions
-from leap.eip import constants as eipconstants
-from leap.util.fileutil import mkdir_p
-from leap.testing.basetest import BaseLeapTest
-
-
-try:
- import unittest2 as unittest
-except ImportError:
- import unittest
-
-_system = platform.system()
-
-
-class JSONLeapConfigTest(BaseLeapTest):
- def setUp(self):
- pass
-
- def tearDown(self):
- pass
-
- def test_metaclass(self):
- with self.assertRaises(exceptions.ImproperlyConfigured) as exc:
- class DummyTestConfig(config.JSONLeapConfig):
- __metaclass__ = config.MetaConfigWithSpec
- exc.startswith("missing spec dict")
-
- class DummyTestConfig(config.JSONLeapConfig):
- __metaclass__ = config.MetaConfigWithSpec
- spec = {'properties': {}}
- with self.assertRaises(exceptions.ImproperlyConfigured) as exc:
- DummyTestConfig()
- exc.startswith("missing slug")
-
- class DummyTestConfig(config.JSONLeapConfig):
- __metaclass__ = config.MetaConfigWithSpec
- spec = {'properties': {}}
- slug = "foo"
- DummyTestConfig()
-
-######################################3
-#
-# provider fetch tests block
-#
-
-
-class ProviderTest(BaseLeapTest):
- # override per test fixtures
-
- def setUp(self):
- pass
-
- def tearDown(self):
- pass
-
-
-# XXX depreacated. similar test in eip.checks
-
-#class BareHomeTestCase(ProviderTest):
-#
- #__name__ = "provider_config_tests_bare_home"
-#
- #def test_should_raise_if_missing_eip_json(self):
- #with self.assertRaises(exceptions.MissingConfigFileError):
- #config.get_config_json(os.path.join(self.home, 'eip.json'))
-
-
-class ProviderDefinitionTestCase(ProviderTest):
- # XXX MOVE TO eip.test_checks
- # -- kali 2012-08-24 00:38
-
- __name__ = "provider_config_tests"
-
- def setUp(self):
- # dump a sample eip file
- # XXX Move to Use EIP Spec Instead!!!
- # XXX tests to be moved to eip.checks and eip.providers
- # XXX can use eipconfig.dump_default_eipconfig
-
- path = os.path.join(self.home, '.config', 'leap')
- mkdir_p(path)
- with open(os.path.join(path, 'eip.json'), 'w') as fp:
- json.dump(eipconstants.EIP_SAMPLE_JSON, fp)
-
-
-# these tests below should move to
-# eip.checks
-# config.Configuration has been deprecated
-
-# TODO:
-# - We're instantiating a ProviderTest because we're doing the home wipeoff
-# on setUpClass instead of the setUp (for speedup of the general cases).
-
-# We really should be testing all of them in the same testCase, and
-# doing an extra wipe of the tempdir... but be careful!!!! do not mess with
-# os.environ home more than needed... that could potentially bite!
-
-# XXX actually, another thing to fix here is separating tests:
-# - test that requests has been called.
-# - check deeper for error types/msgs
-
-# we SHOULD inject requests dep in the constructor
-# (so we can pass mock easily).
-
-
-#class ProviderFetchConError(ProviderTest):
- #def test_connection_error(self):
- #with mock.patch.object(requests, "get") as mock_method:
- #mock_method.side_effect = requests.ConnectionError
- #cf = config.Configuration()
- #self.assertIsInstance(cf.error, str)
-#
-#
-#class ProviderFetchHttpError(ProviderTest):
- #def test_file_not_found(self):
- #with mock.patch.object(requests, "get") as mock_method:
- #mock_method.side_effect = requests.HTTPError
- #cf = config.Configuration()
- #self.assertIsInstance(cf.error, str)
-#
-#
-#class ProviderFetchInvalidUrl(ProviderTest):
- #def test_invalid_url(self):
- #cf = config.Configuration("ht")
- #self.assertTrue(cf.error)
-
-
-# end provider fetch tests
-###########################################
-
-
-class ConfigHelperFunctions(BaseLeapTest):
-
- __name__ = "config_helper_tests"
-
- def setUp(self):
- pass
-
- def tearDown(self):
- pass
-
- # tests
-
- @unittest.skipUnless(_system == "Linux", "linux only")
- def test_lin_get_config_file(self):
- """
- config file path where expected? (linux)
- """
- self.assertEqual(
- config.get_config_file(
- 'test', folder="foo/bar"),
- os.path.expanduser(
- '~/.config/leap/foo/bar/test')
- )
-
- @unittest.skipUnless(_system == "Darwin", "mac only")
- def test_mac_get_config_file(self):
- """
- config file path where expected? (mac)
- """
- self._missing_test_for_plat(do_raise=True)
-
- @unittest.skipUnless(_system == "Windows", "win only")
- def test_win_get_config_file(self):
- """
- config file path where expected?
- """
- self._missing_test_for_plat(do_raise=True)
-
- #
- # XXX hey, I'm raising exceptions here
- # on purpose. just wanted to make sure
- # that the skip stuff is doing it right.
- # If you're working on win/macos tests,
- # feel free to remove tests that you see
- # are too redundant.
-
- @unittest.skipUnless(_system == "Linux", "linux only")
- def test_lin_get_config_dir(self):
- """
- nice config dir? (linux)
- """
- self.assertEqual(
- config.get_config_dir(),
- os.path.expanduser('~/.config/leap'))
-
- @unittest.skipUnless(_system == "Darwin", "mac only")
- def test_mac_get_config_dir(self):
- """
- nice config dir? (mac)
- """
- self._missing_test_for_plat(do_raise=True)
-
- @unittest.skipUnless(_system == "Windows", "win only")
- def test_win_get_config_dir(self):
- """
- nice config dir? (win)
- """
- self._missing_test_for_plat(do_raise=True)
-
- # provider paths
-
- @unittest.skipUnless(_system == "Linux", "linux only")
- def test_get_default_provider_path(self):
- """
- is default provider path ok?
- """
- self.assertEqual(
- config.get_default_provider_path(),
- os.path.expanduser(
- '~/.config/leap/providers/%s/' %
- constants.DEFAULT_PROVIDER)
- )
-
- # validate ip
-
- def test_validate_ip(self):
- """
- check our ip validation
- """
- config.validate_ip('3.3.3.3')
- with self.assertRaises(socket.error):
- config.validate_ip('255.255.255.256')
- with self.assertRaises(socket.error):
- config.validate_ip('foobar')
-
- @unittest.skip
- def test_validate_domain(self):
- """
- code to be written yet
- """
- raise NotImplementedError
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/base/tests/test_providers.py b/src/leap/base/tests/test_providers.py
deleted file mode 100644
index 92bc1f2f..00000000
--- a/src/leap/base/tests/test_providers.py
+++ /dev/null
@@ -1,148 +0,0 @@
-import copy
-import json
-try:
- import unittest2 as unittest
-except ImportError:
- import unittest
-import os
-
-from leap.base.pluggableconfig import ValidationError
-from leap.testing.basetest import BaseLeapTest
-from leap.base import providers
-
-
-EXPECTED_DEFAULT_CONFIG = {
- u"api_version": u"0.1.0",
- #u"description": "LEAPTranslatable<{u'en': u'Test provider'}>",
- u"description": {u'en': u'Test provider'},
- u"default_language": u"en",
- #u"display_name": {u'en': u"Test Provider"},
- u"domain": u"testprovider.example.org",
- #u'name': "LEAPTranslatable<{u'en': u'Test Provider'}>",
- u'name': {u'en': u'Test Provider'},
- u"enrollment_policy": u"open",
- #u"serial": 1,
- u"services": [
- u"eip"
- ],
- u"languages": [u"en"],
- u"version": u"0.1.0"
-}
-
-
-class TestLeapProviderDefinition(BaseLeapTest):
- def setUp(self):
- self.domain = "testprovider.example.org"
- self.definition = providers.LeapProviderDefinition(
- domain=self.domain)
- self.definition.save(force=True)
- self.definition.load() # why have to load after save??
- self.config = self.definition.config
-
- def tearDown(self):
- if hasattr(self, 'testfile') and os.path.isfile(self.testfile):
- os.remove(self.testfile)
-
- # tests
-
- # XXX most of these tests can be made more abstract
- # and moved to test_baseconfig *triangulate!*
-
- def test_provider_slug_property(self):
- slug = self.definition.slug
- self.assertEquals(
- slug,
- os.path.join(
- self.home,
- '.config', 'leap', 'providers',
- '%s' % self.domain,
- 'provider.json'))
- with self.assertRaises(AttributeError):
- self.definition.slug = 23
-
- def test_provider_dump(self):
- # check a good provider definition is dumped to disk
- self.testfile = self.get_tempfile('test.json')
- self.definition.save(to=self.testfile, force=True)
- deserialized = json.load(open(self.testfile, 'rb'))
- self.maxDiff = None
- #import ipdb;ipdb.set_trace()
- self.assertEqual(deserialized, EXPECTED_DEFAULT_CONFIG)
-
- def test_provider_dump_to_slug(self):
- # same as above, but we test the ability to save to a
- # file generated from the slug.
- # XXX THIS TEST SHOULD MOVE TO test_baseconfig
- self.definition.save()
- filename = self.definition.filename
- self.assertTrue(os.path.isfile(filename))
- deserialized = json.load(open(filename, 'rb'))
- self.assertEqual(deserialized, EXPECTED_DEFAULT_CONFIG)
-
- def test_provider_load(self):
- # check loading provider from disk file
- self.testfile = self.get_tempfile('test_load.json')
- with open(self.testfile, 'w') as wf:
- wf.write(json.dumps(EXPECTED_DEFAULT_CONFIG))
- self.definition.load(fromfile=self.testfile)
- #self.assertDictEqual(self.config,
- #EXPECTED_DEFAULT_CONFIG)
- self.assertItemsEqual(self.config, EXPECTED_DEFAULT_CONFIG)
-
- def test_provider_validation(self):
- self.definition.validate(self.config)
- _config = copy.deepcopy(self.config)
- # bad type, raise validation error
- _config['domain'] = 111
- with self.assertRaises(ValidationError):
- self.definition.validate(_config)
-
- @unittest.skip
- def test_load_malformed_json_definition(self):
- raise NotImplementedError
-
- @unittest.skip
- def test_type_validation(self):
- # check various type validation
- # type cast
- raise NotImplementedError
-
-
-class TestLeapProviderSet(BaseLeapTest):
-
- def setUp(self):
- self.providers = providers.LeapProviderSet()
-
- def tearDown(self):
- pass
- ###
-
- def test_get_zero_count(self):
- self.assertEqual(self.providers.count, 0)
-
- @unittest.skip
- def test_count_defined_providers(self):
- # check the method used for making
- # the list of providers
- raise NotImplementedError
-
- @unittest.skip
- def test_get_default_provider(self):
- raise NotImplementedError
-
- @unittest.skip
- def test_should_be_at_least_one_provider_after_init(self):
- # when we init an empty environment,
- # there should be at least one provider,
- # that will be a dump of the default provider definition
- # somehow a high level test
- raise NotImplementedError
-
- @unittest.skip
- def test_get_eip_remote_from_default_provider(self):
- # from: default provider
- # expect: remote eip domain
- raise NotImplementedError
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/base/tests/test_validation.py b/src/leap/base/tests/test_validation.py
deleted file mode 100644
index b45fbe3a..00000000
--- a/src/leap/base/tests/test_validation.py
+++ /dev/null
@@ -1,93 +0,0 @@
-import copy
-import datetime
-from functools import partial
-#import json
-try:
- import unittest2 as unittest
-except ImportError:
- import unittest
-import os
-
-from leap.base.config import JSONLeapConfig
-from leap.base import pluggableconfig
-from leap.testing.basetest import BaseLeapTest
-
-SAMPLE_CONFIG_DICT = {
- 'prop_one': 1,
- 'prop_uri': "http://example.org",
- 'prop_date': '2012-12-12',
-}
-
-EXPECTED_CONFIG = {
- 'prop_one': 1,
- 'prop_uri': "http://example.org",
- 'prop_date': datetime.datetime(2012, 12, 12)
-}
-
-sample_spec = {
- 'description': 'sample schema definition',
- 'type': 'object',
- 'properties': {
- 'prop_one': {
- 'type': int,
- 'default': 1,
- 'required': True
- },
- 'prop_uri': {
- 'type': str,
- 'default': 'http://example.org',
- 'required': True,
- 'format': 'uri'
- },
- 'prop_date': {
- 'type': str,
- 'default': '2012-12-12',
- 'format': 'date'
- }
- }
-}
-
-
-class SampleConfig(JSONLeapConfig):
- spec = sample_spec
-
- @property
- def slug(self):
- return os.path.expanduser('~/sampleconfig.json')
-
-
-class TestJSONLeapConfigValidation(BaseLeapTest):
- def setUp(self):
- self.sampleconfig = SampleConfig()
- self.sampleconfig.save()
- self.sampleconfig.load()
- self.config = self.sampleconfig.config
-
- def tearDown(self):
- if hasattr(self, 'testfile') and os.path.isfile(self.testfile):
- os.remove(self.testfile)
-
- # tests
-
- def test_good_validation(self):
- self.sampleconfig.validate(SAMPLE_CONFIG_DICT)
-
- def test_broken_int(self):
- _config = copy.deepcopy(SAMPLE_CONFIG_DICT)
- _config['prop_one'] = '1'
- self.assertRaises(
- pluggableconfig.ValidationError,
- partial(self.sampleconfig.validate, _config))
-
- def test_format_property(self):
- # JsonSchema Validator does not check the format property.
- # We should have to extend the Configuration class
- blah = copy.deepcopy(SAMPLE_CONFIG_DICT)
- blah['prop_uri'] = 'xxx'
- self.assertRaises(
- pluggableconfig.TypeCastException,
- partial(self.sampleconfig.validate, blah))
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/baseapp/constants.py b/src/leap/baseapp/constants.py
deleted file mode 100644
index e312be21..00000000
--- a/src/leap/baseapp/constants.py
+++ /dev/null
@@ -1,6 +0,0 @@
-# This timer used for polling vpn manager state.
-
-# XXX what is an optimum polling interval?
-# too little will be overkill, too much will
-# miss transition states.
-TIMER_MILLISECONDS = 250.0
diff --git a/src/leap/baseapp/dialogs.py b/src/leap/baseapp/dialogs.py
deleted file mode 100644
index d256fc99..00000000
--- a/src/leap/baseapp/dialogs.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
-import logging
-
-from PyQt4.QtGui import (QDialog, QFrame, QPushButton, QLabel, QMessageBox)
-
-logger = logging.getLogger(name=__name__)
-
-
-class ErrorDialog(QDialog):
- def __init__(self, parent=None, errtype=None, msg=None, label=None):
- super(ErrorDialog, self).__init__(parent)
- frameStyle = QFrame.Sunken | QFrame.Panel
- self.warningLabel = QLabel()
- self.warningLabel.setFrameStyle(frameStyle)
- self.warningButton = QPushButton("QMessageBox.&warning()")
-
- if msg is not None:
- self.msg = msg
- if label is not None:
- self.label = label
- if errtype == "critical":
- self.criticalMessage(self.msg, self.label)
-
- def warningMessage(self, msg, label):
- msgBox = QMessageBox(QMessageBox.Warning,
- "LEAP Client Error",
- msg,
- QMessageBox.NoButton, self)
- msgBox.addButton("&Ok", QMessageBox.AcceptRole)
- if msgBox.exec_() == QMessageBox.AcceptRole:
- pass
- # do whatever we want to do after
- # closing the dialog. we can pass that
- # in the constructor
-
- def criticalMessage(self, msg, label):
- msgBox = QMessageBox(QMessageBox.Critical,
- "LEAP Client Error",
- msg,
- QMessageBox.NoButton, self)
- msgBox.addButton("&Ok", QMessageBox.AcceptRole)
- msgBox.exec_()
-
- # It's critical, so we exit.
- # We should better emit a signal and connect it
- # with the proper shutdownAndQuit method, but
- # this suffices for now.
- logger.info('Quitting')
- import sys
- sys.exit()
-
- def confirmMessage(self, msg, label, action):
- msgBox = QMessageBox(QMessageBox.Critical,
- self.tr("LEAP Client Error"),
- msg,
- QMessageBox.NoButton, self)
- msgBox.addButton("&Ok", QMessageBox.AcceptRole)
- msgBox.addButton("&Cancel", QMessageBox.RejectRole)
-
- if msgBox.exec_() == QMessageBox.AcceptRole:
- action()
diff --git a/src/leap/baseapp/eip.py b/src/leap/baseapp/eip.py
deleted file mode 100644
index b34cc82e..00000000
--- a/src/leap/baseapp/eip.py
+++ /dev/null
@@ -1,243 +0,0 @@
-from __future__ import print_function
-import logging
-import time
-#import sys
-
-from PyQt4 import QtCore
-
-from leap.baseapp.dialogs import ErrorDialog
-from leap.baseapp import constants
-from leap.eip import exceptions as eip_exceptions
-from leap.eip.eipconnection import EIPConnection
-from leap.base.checks import EVENT_CONNECT_REFUSED
-from leap.util import geo
-
-logger = logging.getLogger(name=__name__)
-
-
-class EIPConductorAppMixin(object):
- """
- initializes an instance of EIPConnection,
- gathers errors, and passes status-change signals
- from Qt land along to the conductor.
- Connects the eip connect/disconnect logic
- to the switches in the app (buttons/menu items).
- """
- ERR_DIALOG = False
-
- def __init__(self, *args, **kwargs):
- opts = kwargs.pop('opts')
- config_file = getattr(opts, 'config_file', None)
- provider = kwargs.pop('provider')
-
- self.eip_service_started = False
-
- # conductor (eip connection) is in charge of all
- # vpn-related configuration / monitoring.
- # we pass a tuple of signals that will be
- # triggered when status changes.
-
- self.conductor = EIPConnection(
- watcher_cb=self.newLogLine.emit,
- config_file=config_file,
- checker_signals=(self.eipStatusChange.emit, ),
- status_signals=(self.openvpnStatusChange.emit, ),
- debug=self.debugmode,
- ovpn_verbosity=opts.openvpn_verb,
- provider=provider)
-
- # Do we want to enable the skip checks w/o being
- # in debug mode??
- #self.skip_download = opts.no_provider_checks
- #self.skip_verify = opts.no_ca_verify
- self.skip_download = False
- self.skip_verify = False
-
- def run_eip_checks(self):
- """
- runs eip checks and
- the error checking loop
- """
- logger.debug('running EIP CHECKS')
- self.conductor.run_checks(
- skip_download=self.skip_download,
- skip_verify=self.skip_verify)
- self.error_check()
-
- self.start_eipconnection.emit()
-
- def error_check(self):
- """
- consumes the conductor error queue.
- pops errors, and acts accordingly (launching user dialogs).
- """
- logger.debug('error check')
-
- errq = self.conductor.error_queue
- while errq.qsize() != 0:
- logger.debug('%s errors left in conductor queue', errq.qsize())
- # we get exception and original traceback from queue
- error, tb = errq.get()
-
- # redundant log, debugging the loop.
- logger.error('%s: %s', error.__class__.__name__, error.message)
-
- if issubclass(error.__class__, eip_exceptions.EIPClientError):
- self.triggerEIPError.emit(error)
-
- else:
- # deprecated form of raising exception.
- raise error, None, tb
-
- if error.failfirst is True:
- break
-
- @QtCore.pyqtSlot(object)
- def onEIPError(self, error):
- """
- check severity and launches
- dialogs informing user about the errors.
- in the future we plan to derive errors to
- our log viewer.
- """
- if self.ERR_DIALOG:
- logger.warning('another error dialog suppressed')
- return
-
- # XXX this is actually a one-shot.
- # On the dialog there should be
- # a reset signal binded to the ok button
- # or something like that.
- self.ERR_DIALOG = True
-
- if getattr(error, 'usermessage', None):
- message = error.usermessage
- else:
- message = error.message
-
- # XXX
- # check headless = False before
- # launching dialog.
- # (so Qt tests can assert stuff)
-
- if error.critical:
- logger.critical(error.message)
- #critical error (non recoverable),
- #we give user some info and quit.
- #(critical error dialog will exit app)
- ErrorDialog(errtype="critical",
- msg=message,
- label="critical error")
-
- elif error.warning:
- logger.warning(error.message)
-
- else:
- dialog = ErrorDialog()
- dialog.warningMessage(message, 'error')
-
- @QtCore.pyqtSlot()
- def statusUpdate(self):
- """
- polls status and updates ui with real time
- info about transferred bytes / connection state.
- right now is triggered by a timer tick
- (timer controlled by StatusAwareTrayIcon class)
- """
- # TODO I guess it's too expensive to poll
- # continously. move to signal events instead.
- # (i.e., subscribe to connection status changes
- # from openvpn manager)
-
- if not self.eip_service_started:
- # there is a race condition
- # going on here. Depending on how long we take
- # to init the qt app, the management socket
- # is not ready yet.
- return
-
- #if self.conductor.with_errors:
- #XXX how to wait on pkexec???
- #something better that this workaround, plz!!
- #I removed the pkexec pass authentication at all.
- #time.sleep(5)
- #logger.debug('timeout')
- #logger.error('errors. disconnect')
- #self.start_or_stopVPN() # is stop
-
- state = self.conductor.poll_connection_state()
- if not state:
- return
-
- ts, con_status, ok, ip, remote = state
- self.set_statusbarMessage(con_status)
- self.setIconToolTip()
-
- ts = time.strftime("%a %b %d %X", ts)
- if self.debugmode:
- self.updateTS.setText(ts)
- self.status_label.setText(con_status)
- self.ip_label.setText(ip)
- self.remote_label.setText(remote)
- self.remote_country.setText(
- geo.get_country_name(remote))
-
- # status i/o
-
- status = self.conductor.get_status_io()
- if status and self.debugmode:
- #XXX move this to systray menu indicators
- ts, (tun_read, tun_write, tcp_read, tcp_write, auth_read) = status
- ts = time.strftime("%a %b %d %X", ts)
- self.updateTS.setText(ts)
- self.tun_read_bytes.setText(tun_read)
- self.tun_write_bytes.setText(tun_write)
-
- # connection information via management interface
- log = self.conductor.get_log()
- error_matrix = [(EVENT_CONNECT_REFUSED, (self.start_or_stopVPN, ))]
- if hasattr(self.network_checker, 'checker'):
- self.network_checker.checker.parse_log_and_react(log, error_matrix)
-
- @QtCore.pyqtSlot()
- def start_or_stopVPN(self, **kwargs):
- """
- stub for running child process with vpn
- """
- if self.conductor.has_errors():
- logger.debug('not starting vpn; conductor has errors')
- return
-
- if self.eip_service_started is False:
- try:
- self.conductor.connect()
-
- except eip_exceptions.EIPNoCommandError as exc:
- logger.error('tried to run openvpn but no command is set')
- self.triggerEIPError.emit(exc)
-
- except Exception as err:
- # raise generic exception (Bad Thing Happened?)
- logger.exception(err)
- else:
- # no errors, so go on.
- if self.debugmode:
- self.startStopButton.setText(self.tr('&Disconnect'))
- self.eip_service_started = True
- self.toggleEIPAct()
-
- # XXX decouple! (timer is init by icons class).
- # we could bring Timer Init to this Mixin
- # or to its own Mixin.
- self.timer.start(constants.TIMER_MILLISECONDS)
- return
-
- if self.eip_service_started is True:
- self.network_checker.stop()
- self.conductor.disconnect()
- if self.debugmode:
- self.startStopButton.setText(self.tr('&Connect'))
- self.eip_service_started = False
- self.toggleEIPAct()
- self.timer.stop()
- return
diff --git a/src/leap/baseapp/leap_app.py b/src/leap/baseapp/leap_app.py
deleted file mode 100644
index 4d3aebd6..00000000
--- a/src/leap/baseapp/leap_app.py
+++ /dev/null
@@ -1,153 +0,0 @@
-import logging
-
-import sip
-sip.setapi('QVariant', 2)
-
-from PyQt4 import QtCore
-from PyQt4 import QtGui
-
-from leap.gui import mainwindow_rc
-
-logger = logging.getLogger(name=__name__)
-
-
-APP_LOGO = ':/images/leap-color-small.png'
-
-
-class MainWindowMixin(object):
- """
- create the main window
- for leap app
- """
-
- def __init__(self, *args, **kwargs):
- # XXX set initial visibility
- # debug = no visible
-
- widget = QtGui.QWidget()
- self.setCentralWidget(widget)
-
- mainLayout = QtGui.QVBoxLayout()
- # add widgets to layout
- #self.createWindowHeader()
- #mainLayout.addWidget(self.headerBox)
-
- # created in systray
- mainLayout.addWidget(self.statusIconBox)
- if self.debugmode:
- mainLayout.addWidget(self.statusBox)
- mainLayout.addWidget(self.loggerBox)
- widget.setLayout(mainLayout)
-
- self.createMainActions()
- self.createMainMenus()
-
- self.setWindowTitle("LEAP Client")
- self.set_app_icon()
- self.set_statusbarMessage('ready')
-
- def createMainActions(self):
- #self.openAct = QtGui.QAction("&Open...", self, shortcut="Ctrl+O",
- #triggered=self.open)
-
- self.firstRunWizardAct = QtGui.QAction(
- "&First run wizard...", self,
- triggered=self.stop_connection_and_launch_first_run_wizard)
- self.aboutAct = QtGui.QAction("&About", self, triggered=self.about)
-
- #self.aboutQtAct = QtGui.QAction("About &Qt", self,
- #triggered=QtGui.qApp.aboutQt)
-
- def createMainMenus(self):
- self.connMenu = QtGui.QMenu("&Connections", self)
- #self.viewMenu.addSeparator()
- self.connMenu.addAction(self.quitAction)
-
- self.settingsMenu = QtGui.QMenu("&Settings", self)
- self.settingsMenu.addAction(self.firstRunWizardAct)
-
- self.helpMenu = QtGui.QMenu("&Help", self)
- self.helpMenu.addAction(self.aboutAct)
- #self.helpMenu.addAction(self.aboutQtAct)
-
- self.menuBar().addMenu(self.connMenu)
- self.menuBar().addMenu(self.settingsMenu)
- self.menuBar().addMenu(self.helpMenu)
-
- def stop_connection_and_launch_first_run_wizard(self):
- settings = QtCore.QSettings()
- settings.setValue('FirstRunWizardDone', False)
- logger.debug('should run first run wizard again...')
-
- status = self.conductor.get_icon_name()
- if status != "disconnected":
- self.start_or_stopVPN()
-
- self.launch_first_run_wizard()
- #from leap.gui.firstrunwizard import FirstRunWizard
- #wizard = FirstRunWizard(
- #parent=self,
- #success_cb=self.initReady.emit)
- #wizard.show()
-
- def set_app_icon(self):
- icon = QtGui.QIcon(APP_LOGO)
- self.setWindowIcon(icon)
-
- #def createWindowHeader(self):
- #"""
- #description lines for main window
- #"""
- #self.headerBox = QtGui.QGroupBox()
- #self.headerLabel = QtGui.QLabel(
- #"<font size=40>LEAP Encryption Access Project</font>")
- #self.headerLabelSub = QtGui.QLabel(
- #"<br><i>your internet encryption toolkit</i>")
-#
- #pixmap = QtGui.QPixmap(APP_LOGO)
- #leap_lbl = QtGui.QLabel()
- #leap_lbl.setPixmap(pixmap)
-#
- #headerLayout = QtGui.QHBoxLayout()
- #headerLayout.addWidget(leap_lbl)
- #headerLayout.addWidget(self.headerLabel)
- #headerLayout.addWidget(self.headerLabelSub)
- #headerLayout.addStretch()
- #self.headerBox.setLayout(headerLayout)
-
- def set_statusbarMessage(self, msg):
- self.statusBar().showMessage(msg)
-
- def closeEvent(self, event):
- """
- redefines close event (persistent window behaviour)
- """
- if self.trayIcon.isVisible() and not self.debugmode:
- QtGui.QMessageBox.information(
- self, "Systray",
- "The program will keep running "
- "in the system tray. To "
- "terminate the program, choose "
- "<b>Quit</b> in the "
- "context menu of the system tray entry.")
- self.hide()
- event.ignore()
- return
- self.cleanupAndQuit()
-
- def cleanupAndQuit(self):
- """
- cleans state before shutting down app.
- """
- # save geometry for restoring
- settings = QtCore.QSettings()
- geom_key = "DebugGeometry" if self.debugmode else "Geometry"
- settings.setValue(geom_key, self.saveGeometry())
-
- # TODO:make sure to shutdown all child process / threads
- # in conductor
- # XXX send signal instead?
- logger.info('Shutting down')
- self.conductor.disconnect(shutdown=True)
- logger.info('Exiting. Bye.')
- QtGui.qApp.quit()
diff --git a/src/leap/baseapp/log.py b/src/leap/baseapp/log.py
deleted file mode 100644
index 636e5bae..00000000
--- a/src/leap/baseapp/log.py
+++ /dev/null
@@ -1,69 +0,0 @@
-import logging
-
-from PyQt4 import QtGui
-from PyQt4 import QtCore
-
-vpnlogger = logging.getLogger('leap.openvpn')
-
-
-class LogPaneMixin(object):
- """
- a simple log pane
- that writes new lines as they come
- """
- EXCLUDES = ('MANAGEMENT',)
-
- def createLogBrowser(self):
- """
- creates Browser widget for displaying logs
- (in debug mode only).
- """
- self.loggerBox = QtGui.QGroupBox()
- logging_layout = QtGui.QVBoxLayout()
- self.logbrowser = QtGui.QTextBrowser()
-
- startStopButton = QtGui.QPushButton(self.tr("&Connect"))
- self.startStopButton = startStopButton
-
- logging_layout.addWidget(self.logbrowser)
- logging_layout.addWidget(self.startStopButton)
- self.loggerBox.setLayout(logging_layout)
-
- # status box
-
- self.statusBox = QtGui.QGroupBox()
- grid = QtGui.QGridLayout()
-
- self.updateTS = QtGui.QLabel('')
- self.status_label = QtGui.QLabel(self.tr('Disconnected'))
- self.ip_label = QtGui.QLabel('')
- self.remote_label = QtGui.QLabel('')
- self.remote_country = QtGui.QLabel('')
-
- tun_read_label = QtGui.QLabel("tun read")
- self.tun_read_bytes = QtGui.QLabel("0")
- tun_write_label = QtGui.QLabel("tun write")
- self.tun_write_bytes = QtGui.QLabel("0")
-
- grid.addWidget(self.updateTS, 0, 0)
- grid.addWidget(self.status_label, 0, 1)
- grid.addWidget(self.ip_label, 1, 0)
- grid.addWidget(self.remote_label, 1, 1)
- grid.addWidget(self.remote_country, 2, 1)
- grid.addWidget(tun_read_label, 3, 0)
- grid.addWidget(self.tun_read_bytes, 3, 1)
- grid.addWidget(tun_write_label, 4, 0)
- grid.addWidget(self.tun_write_bytes, 4, 1)
-
- self.statusBox.setLayout(grid)
-
- @QtCore.pyqtSlot(str)
- def onLoggerNewLine(self, line):
- """
- simple slot: writes new line to logger Pane.
- """
- msg = line[:-1]
- if self.debugmode and all(map(lambda w: w not in msg,
- LogPaneMixin.EXCLUDES)):
- self.logbrowser.append(msg)
- vpnlogger.info(msg)
diff --git a/src/leap/baseapp/mainwindow.py b/src/leap/baseapp/mainwindow.py
deleted file mode 100644
index 91b0dc61..00000000
--- a/src/leap/baseapp/mainwindow.py
+++ /dev/null
@@ -1,191 +0,0 @@
-# vim: set fileencoding=utf-8 :
-#!/usr/bin/env python
-import logging
-
-import sip
-sip.setapi('QString', 2)
-sip.setapi('QVariant', 2)
-
-from PyQt4 import QtCore
-from PyQt4 import QtGui
-
-from leap.baseapp.eip import EIPConductorAppMixin
-from leap.baseapp.log import LogPaneMixin
-from leap.baseapp.systray import StatusAwareTrayIconMixin
-from leap.baseapp.network import NetworkCheckerAppMixin
-from leap.baseapp.leap_app import MainWindowMixin
-from leap.eip.checks import ProviderCertChecker
-from leap.gui.threads import FunThread
-
-logger = logging.getLogger(name=__name__)
-
-
-class LeapWindow(QtGui.QMainWindow,
- MainWindowMixin, EIPConductorAppMixin,
- StatusAwareTrayIconMixin,
- NetworkCheckerAppMixin,
- LogPaneMixin):
- """
- main window for the leap app.
- Initializes all of its base classes
- We keep here some signal initialization
- that gets tricky otherwise.
- """
-
- # signals
-
- newLogLine = QtCore.pyqtSignal([str])
- mainappReady = QtCore.pyqtSignal([])
- initReady = QtCore.pyqtSignal([])
- networkError = QtCore.pyqtSignal([object])
- triggerEIPError = QtCore.pyqtSignal([object])
- start_eipconnection = QtCore.pyqtSignal([])
- shutdownSignal = QtCore.pyqtSignal([])
- initNetworkChecker = QtCore.pyqtSignal([])
-
- # this is status change got from openvpn management
- openvpnStatusChange = QtCore.pyqtSignal([object])
- # this is global eip status
- eipStatusChange = QtCore.pyqtSignal([str])
-
- def __init__(self, opts):
- logger.debug('init leap window')
- self.debugmode = getattr(opts, 'debug', False)
- super(LeapWindow, self).__init__()
- if self.debugmode:
- self.createLogBrowser()
-
- settings = QtCore.QSettings()
- self.provider_domain = settings.value("provider_domain", None)
- self.username = settings.value("username", None)
-
- logger.debug('provider: %s', self.provider_domain)
- logger.debug('username: %s', self.username)
-
- provider = self.provider_domain
- EIPConductorAppMixin.__init__(
- self, opts=opts, provider=provider)
- StatusAwareTrayIconMixin.__init__(self)
-
- # XXX network checker should probably not
- # trigger run_checks on init... but wait
- # for ready signal instead...
- NetworkCheckerAppMixin.__init__(self, provider=provider)
- MainWindowMixin.__init__(self)
-
- geom_key = "DebugGeometry" if self.debugmode else "Geometry"
- geom = settings.value(geom_key)
- if geom:
- self.restoreGeometry(geom)
-
- # XXX check for wizard
- self.wizard_done = settings.value("FirstRunWizardDone")
-
- self.initchecks = FunThread(self.run_eip_checks)
-
- # bind signals
- self.initchecks.finished.connect(
- lambda: logger.debug('Initial checks thread finished'))
- self.trayIcon.activated.connect(self.iconActivated)
- self.newLogLine.connect(
- lambda line: self.onLoggerNewLine(line))
- self.timer.timeout.connect(
- lambda: self.onTimerTick())
- self.networkError.connect(
- lambda exc: self.onNetworkError(exc))
- self.triggerEIPError.connect(
- lambda exc: self.onEIPError(exc))
-
- if self.debugmode:
- self.startStopButton.clicked.connect(
- lambda: self.start_or_stopVPN())
- self.start_eipconnection.connect(
- self.do_start_eipconnection)
- self.shutdownSignal.connect(
- self.cleanupAndQuit)
- self.initNetworkChecker.connect(
- lambda: self.init_network_checker(self.conductor.provider))
-
- # status change.
- # TODO unify
- self.openvpnStatusChange.connect(
- lambda status: self.onOpenVPNStatusChange(status))
- self.eipStatusChange.connect(
- lambda newstatus: self.onEIPConnStatusChange(newstatus))
- self.eipStatusChange.connect(
- lambda newstatus: self.toggleEIPAct())
-
- # do first run wizard and init signals
- self.mainappReady.connect(self.do_first_run_wizard_check)
- self.initReady.connect(self.runchecks_and_eipconnect)
-
- # ... all ready. go!
- # connected to do_first_run_wizard_check
- self.mainappReady.emit()
-
- def do_first_run_wizard_check(self):
- """
- checks whether first run wizard needs to be run
- launches it if needed
- and emits initReady signal if not.
- """
-
- logger.debug('first run wizard check...')
- need_wizard = False
-
- # do checks (can overlap if wizard was interrupted)
- if not self.wizard_done:
- need_wizard = True
-
- if not self.provider_domain:
- need_wizard = True
- else:
- pcertchecker = ProviderCertChecker(domain=self.provider_domain)
- if not pcertchecker.is_cert_valid(do_raise=False):
- logger.warning('missing valid client cert. need wizard')
- need_wizard = True
-
- # launch wizard if needed
- if need_wizard:
- logger.debug('running first run wizard')
- self.launch_first_run_wizard()
- else: # no wizard needed
- self.initReady.emit()
-
- def launch_first_run_wizard(self):
- """
- launches wizard and blocks
- """
- from leap.gui.firstrun.wizard import FirstRunWizard
- wizard = FirstRunWizard(
- self.conductor,
- parent=self,
- username=self.username,
- start_eipconnection_signal=self.start_eipconnection,
- eip_statuschange_signal=self.eipStatusChange,
- quitcallback=self.onWizardCancel)
- wizard.show()
-
- def onWizardCancel(self):
- if not self.wizard_done:
- logger.debug(
- 'clicked on Cancel during first '
- 'run wizard. shutting down')
- self.cleanupAndQuit()
-
- def runchecks_and_eipconnect(self):
- """
- shows icon and run init checks
- """
- self.show_systray_icon()
- self.initchecks.begin()
-
- def do_start_eipconnection(self):
- """
- shows icon and init eip connection
- called from the end of wizard
- """
- self.show_systray_icon()
- # this will setup the command
- self.conductor.run_openvpn_checks()
- self.start_or_stopVPN()
diff --git a/src/leap/baseapp/network.py b/src/leap/baseapp/network.py
deleted file mode 100644
index dc5182a4..00000000
--- a/src/leap/baseapp/network.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from __future__ import print_function
-
-import logging
-
-logger = logging.getLogger(name=__name__)
-
-from PyQt4 import QtCore
-
-from leap.baseapp.dialogs import ErrorDialog
-from leap.base.network import NetworkCheckerThread
-
-from leap.util.misc import null_check
-
-
-class NetworkCheckerAppMixin(object):
- """
- initialize an instance of the Network Checker,
- which gathers error and passes them on.
- """
- ERR_NETERR = False
-
- def __init__(self, *args, **kwargs):
- provider = kwargs.pop('provider', None)
- self.network_checker = None
- if provider:
- self.init_network_checker(provider)
-
- def init_network_checker(self, provider):
- null_check(provider, "provider_domain")
- if not self.network_checker:
- self.network_checker = NetworkCheckerThread(
- error_cb=self.networkError.emit,
- debug=self.debugmode,
- provider=provider)
- self.network_checker.start()
-
- @QtCore.pyqtSlot(object)
- def runNetworkChecks(self):
- logger.debug('running checks (from NetworkChecker Mixin slot)')
- self.network_checker.run_checks()
-
- @QtCore.pyqtSlot(object)
- def onNetworkError(self, exc):
- """
- slot that receives a network exceptions
- and raises a user error message
- """
- # FIXME this should not HANDLE anything after
- # the network check thread has been stopped.
-
- logger.debug('handling network exception')
- if not self.ERR_NETERR:
- self.ERR_NETERR = True
-
- logger.error(exc.message)
- dialog = ErrorDialog(parent=self)
- if exc.critical:
- dialog.criticalMessage(exc.usermessage, "network error")
- else:
- dialog.warningMessage(exc.usermessage, "network error")
-
- self.start_or_stopVPN()
- self.network_checker.stop()
diff --git a/src/leap/baseapp/permcheck.py b/src/leap/baseapp/permcheck.py
deleted file mode 100644
index 6b74cb6e..00000000
--- a/src/leap/baseapp/permcheck.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import commands
-import os
-
-from leap.util.fileutil import which
-
-
-def is_pkexec_in_system():
- pkexec_path = which('pkexec')
- if not pkexec_path:
- return False
- return os.access(pkexec_path, os.X_OK)
-
-
-def is_auth_agent_running():
- return bool(
- commands.getoutput(
- 'ps aux | grep polkit-[g]nome-authentication-agent-1'))
diff --git a/src/leap/baseapp/systray.py b/src/leap/baseapp/systray.py
deleted file mode 100644
index 77eb3fe9..00000000
--- a/src/leap/baseapp/systray.py
+++ /dev/null
@@ -1,268 +0,0 @@
-import logging
-import sys
-
-import sip
-sip.setapi('QString', 2)
-sip.setapi('QVariant', 2)
-
-from PyQt4 import QtCore
-from PyQt4 import QtGui
-
-from leap import __branding as BRANDING
-from leap import __version__ as VERSION
-
-from leap.gui import mainwindow_rc
-
-logger = logging.getLogger(__name__)
-
-
-class StatusAwareTrayIconMixin(object):
- """
- a mix of several functions needed
- to create a systray and make it
- get updated from conductor status
- polling.
- """
- states = {
- "disconnected": 0,
- "connecting": 1,
- "connected": 2}
-
- iconpath = {
- "disconnected": ':/images/conn_error.png',
- "connecting": ':/images/conn_connecting.png',
- "connected": ':/images/conn_connected.png'}
-
- Icons = {
- 'disconnected': lambda self: QtGui.QIcon(
- self.iconpath['disconnected']),
- 'connecting': lambda self: QtGui.QIcon(
- self.iconpath['connecting']),
- 'connected': lambda self: QtGui.QIcon(
- self.iconpath['connected'])
- }
-
- def __init__(self, *args, **kwargs):
- self.createIconGroupBox()
- self.createActions()
- self.createTrayIcon()
-
- # not sure if this really belongs here, but...
- self.timer = QtCore.QTimer()
-
- def show_systray_icon(self):
- #logger.debug('showing tray icon................')
- self.trayIcon.show()
-
- def createIconGroupBox(self):
- """
- dummy icongroupbox
- (to be removed from here -- reference only)
- """
- con_widgets = {
- 'disconnected': QtGui.QLabel(),
- 'connecting': QtGui.QLabel(),
- 'connected': QtGui.QLabel(),
- }
- con_widgets['disconnected'].setPixmap(
- QtGui.QPixmap(
- self.iconpath['disconnected']))
- con_widgets['connecting'].setPixmap(
- QtGui.QPixmap(
- self.iconpath['connecting']))
- con_widgets['connected'].setPixmap(
- QtGui.QPixmap(
- self.iconpath['connected'])),
- self.ConnectionWidgets = con_widgets
-
- self.statusIconBox = QtGui.QGroupBox(
- self.tr("EIP Connection Status"))
- statusIconLayout = QtGui.QHBoxLayout()
- statusIconLayout.addWidget(self.ConnectionWidgets['disconnected'])
- statusIconLayout.addWidget(self.ConnectionWidgets['connecting'])
- statusIconLayout.addWidget(self.ConnectionWidgets['connected'])
- statusIconLayout.itemAt(1).widget().hide()
- statusIconLayout.itemAt(2).widget().hide()
-
- self.leapConnStatus = QtGui.QLabel(
- self.tr("<b>disconnected</b>"))
- statusIconLayout.addWidget(self.leapConnStatus)
-
- self.statusIconBox.setLayout(statusIconLayout)
-
- def createTrayIcon(self):
- """
- creates the tray icon
- """
- self.trayIconMenu = QtGui.QMenu(self)
-
- self.trayIconMenu.addAction(self.connAct)
- self.trayIconMenu.addSeparator()
- self.trayIconMenu.addAction(self.detailsAct)
- self.trayIconMenu.addSeparator()
- self.trayIconMenu.addAction(self.aboutAct)
- # we should get this hidden inside the "about" dialog
- # (as a little button maybe)
- #self.trayIconMenu.addAction(self.aboutQtAct)
- self.trayIconMenu.addSeparator()
- self.trayIconMenu.addAction(self.quitAction)
-
- self.trayIcon = QtGui.QSystemTrayIcon(self)
- self.setIcon('disconnected')
- self.trayIcon.setContextMenu(self.trayIconMenu)
-
- #self.trayIconMenu.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
- #self.trayIconMenu.customContextMenuRequested.connect(
- #self.on_context_menu)
-
- #def bad(self):
- #logger.error('this should not be called')
-
- def createActions(self):
- """
- creates actions to be binded to tray icon
- """
- # XXX change action name on (dis)connect
- self.connAct = QtGui.QAction(
- self.tr("Encryption ON turn &off"),
- self,
- triggered=lambda: self.start_or_stopVPN())
-
- self.detailsAct = QtGui.QAction(
- self.tr("&Details..."),
- self,
- triggered=self.detailsWin)
- self.aboutAct = QtGui.QAction(
- self.tr("&About"), self,
- triggered=self.about)
- self.aboutQtAct = QtGui.QAction(
- self.tr("About Q&t"), self,
- triggered=QtGui.qApp.aboutQt)
- self.quitAction = QtGui.QAction(
- self.tr("&Quit"), self,
- triggered=self.cleanupAndQuit)
-
- def toggleEIPAct(self):
- # this is too simple by now.
- # XXX get STATUS CONSTANTS INSTEAD
-
- icon_status = self.conductor.get_icon_name()
- if icon_status == "connected":
- self.connAct.setEnabled(True)
- self.connAct.setText(
- self.tr('Encryption ON turn o&ff'))
- return
- if icon_status == "disconnected":
- self.connAct.setEnabled(True)
- self.connAct.setText(
- self.tr('Encryption OFF turn &on'))
- return
- if icon_status == "connecting":
- self.connAct.setDisabled(True)
- self.connAct.setText(self.tr('connecting...'))
- return
-
- def detailsWin(self):
- visible = self.isVisible()
- if visible:
- self.hide()
- else:
- self.show()
- if sys.platform == "darwin":
- self.raise_()
-
- def about(self):
- # move to widget
- flavor = BRANDING.get('short_name', None)
- content = self.tr(
- ("LEAP client<br>"
- "(version <b>%s</b>)<br>" % VERSION))
- if flavor:
- content = content + ('<br>Flavor: <i>%s</i><br>' % flavor)
- content = content + (
- "<br><a href='https://leap.se/'>"
- "https://leap.se</a>")
- QtGui.QMessageBox.about(self, self.tr("About"), content)
-
- def setConnWidget(self, icon_name):
- oldlayout = self.statusIconBox.layout()
-
- for i in range(3):
- oldlayout.itemAt(i).widget().hide()
- new = self.states[icon_name]
- oldlayout.itemAt(new).widget().show()
-
- def setIcon(self, name):
- icon_fun = self.Icons.get(name)
- if icon_fun and callable(icon_fun):
- icon = icon_fun(self)
- self.trayIcon.setIcon(icon)
-
- def getIcon(self, icon_name):
- return self.states.get(icon_name, None)
-
- def setIconToolTip(self):
- """
- get readable status and place it on systray tooltip
- """
- status = self.conductor.status.get_readable_status()
- self.trayIcon.setToolTip(status)
-
- def iconActivated(self, reason):
- """
- handles left click, left double click
- showing the trayicon menu
- """
- if reason in (QtGui.QSystemTrayIcon.Trigger,
- QtGui.QSystemTrayIcon.DoubleClick):
- context_menu = self.trayIcon.contextMenu()
- # for some reason, context_menu.show()
- # is failing in a way beyond my understanding.
- # (not working the first time it's clicked).
- # this works however.
- # XXX in osx it shows some glitches.
- context_menu.exec_(self.trayIcon.geometry().center())
-
- @QtCore.pyqtSlot()
- def onTimerTick(self):
- self.statusUpdate()
-
- @QtCore.pyqtSlot(object)
- def onOpenVPNStatusChange(self, status):
- """
- updates icon, according to the openvpn status change.
- """
- icon_name = self.conductor.get_icon_name()
- if not icon_name:
- return
-
- # XXX refactor. Use QStateMachine
-
- if icon_name in ("disconnected", "connected"):
- self.eipStatusChange.emit(icon_name)
-
- if icon_name in ("connecting"):
- # let's see how it matches
- leap_status_name = self.conductor.get_leap_status()
- self.eipStatusChange.emit(leap_status_name)
-
- if icon_name == "connected":
- # When we change to "connected', we launch
- # the network checker.
- self.initNetworkChecker.emit()
-
- self.setIcon(icon_name)
- # change connection pixmap widget
- self.setConnWidget(icon_name)
-
- @QtCore.pyqtSlot(str)
- def onEIPConnStatusChange(self, newstatus):
- """
- slot for EIP status changes
- not to be confused with onOpenVPNStatusChange.
- this only updates the non-debug LEAP Status line
- next to the connection icon.
- """
- # XXX move bold to style sheet
- self.leapConnStatus.setText(
- "<b>%s</b>" % newstatus)
diff --git a/src/leap/certs/__init__.py b/src/leap/certs/__init__.py
deleted file mode 100644
index c4d009b1..00000000
--- a/src/leap/certs/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-import os
-
-_where = os.path.split(__file__)[0]
-
-
-def where(filename):
- return os.path.join(_where, filename)
diff --git a/src/leap/base/__init__.py b/src/leap/config/__init__.py
index e69de29b..e69de29b 100644
--- a/src/leap/base/__init__.py
+++ b/src/leap/config/__init__.py
diff --git a/src/leap/config/leapsettings.py b/src/leap/config/leapsettings.py
new file mode 100644
index 00000000..35010280
--- /dev/null
+++ b/src/leap/config/leapsettings.py
@@ -0,0 +1,253 @@
+# -*- coding: utf-8 -*-
+# leapsettings.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+QSettings abstraction
+"""
+import os
+import logging
+
+from PySide import QtCore
+
+from leap.common.check import leap_assert, leap_assert_type
+from leap.common.config.prefixers import get_platform_prefixer
+
+logger = logging.getLogger(__name__)
+
+
+def to_bool(val):
+ """
+ Returns the boolean value corresponding to val. Will return False
+ in case val is not a string or something that behaves like one.
+
+ :param val: value to cast
+ :type val: either bool already or str
+
+ :rtype: bool
+ """
+ if isinstance(val, bool):
+ return val
+
+ bool_val = False
+ try:
+ bool_val = val.lower() == "true"
+ except:
+ pass
+
+ return bool_val
+
+
+class LeapSettings(object):
+ """
+ Leap client QSettings wrapper
+ """
+
+ CONFIG_NAME = "leap.conf"
+
+ # keys
+ GEOMETRY_KEY = "Geometry"
+ WINDOWSTATE_KEY = "WindowState"
+ USER_KEY = "User"
+ PROPERPROVIDER_KEY = "ProperProvider"
+ REMEMBER_KEY = "RememberUserAndPass"
+ DEFAULTPROVIDER_KEY = "DefaultProvider"
+ ALERTMISSING_KEY = "AlertMissingScripts"
+
+ def __init__(self, standalone=False):
+ """
+ Constructor
+
+ :param standalone: parameter used to define the location of
+ the config
+ :type standalone: bool
+ """
+
+ settings_path = os.path.join(get_platform_prefixer()
+ .get_path_prefix(standalone=standalone),
+ "leap",
+ self.CONFIG_NAME)
+ self._settings = QtCore.QSettings(settings_path,
+ QtCore.QSettings.IniFormat)
+
+ def get_geometry(self):
+ """
+ Returns the saved geometry or None if it wasn't saved
+
+ :rtype: bytearray or None
+ """
+ return self._settings.value(self.GEOMETRY_KEY, None)
+
+ def set_geometry(self, geometry):
+ """
+ Saves the geometry to the settings
+
+ :param geometry: bytearray representing the geometry
+ :type geometry: bytearray
+ """
+ leap_assert(geometry, "We need a geometry")
+ self._settings.setValue(self.GEOMETRY_KEY, geometry)
+
+ def get_windowstate(self):
+ """
+ Returns the window state or None if it wasn't saved
+
+ :rtype: bytearray or None
+ """
+ return self._settings.value(self.WINDOWSTATE_KEY, None)
+
+ def set_windowstate(self, windowstate):
+ """
+ Saves the window state to the settings
+
+ :param windowstate: bytearray representing the window state
+ :type windowstate: bytearray
+ """
+ leap_assert(windowstate, "We need a window state")
+ self._settings.setValue(self.WINDOWSTATE_KEY, windowstate)
+
+ def get_enabled_services(self, provider):
+ """
+ Returns a list of enabled services for the given provider
+
+ :param provider: provider domain
+ :type provider: str
+
+ :rtype: list of str
+ """
+
+ leap_assert(len(provider) > 0, "We need a nonempty provider")
+ enabled_services = self._settings.value("%s/Services" % (provider,),
+ [])
+ if isinstance(enabled_services, (str, unicode)):
+ enabled_services = enabled_services.split(",")
+
+ return enabled_services
+
+ def set_enabled_services(self, provider, services):
+ """
+ Saves the list of enabled services for the given provider
+
+ :param provider: provider domain
+ :type provider: str
+
+ :param services: list of services to save
+ :type services: list of str
+ """
+
+ leap_assert(len(provider) > 0, "We need a nonempty provider")
+ leap_assert_type(services, list)
+
+ self._settings.setValue("%s/Services" % (provider,),
+ services)
+
+ def get_user(self):
+ """
+ Returns the configured user to remember, None if there isn't one
+
+ :rtype: str or None
+ """
+ return self._settings.value(self.USER_KEY, None)
+
+ def set_user(self, user):
+ """
+ Saves the user to remember
+
+ :param user: user name to remember
+ :type user: str
+ """
+ leap_assert(len(user) > 0, "We cannot save an empty user")
+ self._settings.setValue(self.USER_KEY, user)
+
+ def get_remember(self):
+ """
+ Returns the value of the remember selection.
+
+ :rtype: bool
+ """
+ return to_bool(self._settings.value(self.REMEMBER_KEY, False))
+
+ def set_remember(self, remember):
+ """
+ Sets wheter the app should remember username and password
+
+ :param remember: True if the app should remember username and
+ password, False otherwise
+ :rtype: bool
+ """
+ leap_assert_type(remember, bool)
+ self._settings.setValue(self.REMEMBER_KEY, remember)
+
+ # TODO: make this scale with multiple providers, we are assuming
+ # just one for now
+ def get_properprovider(self):
+ """
+ Returns True if there is a properly configured provider.
+
+ .. note:: this assumes only one provider for now.
+
+ :rtype: bool
+ """
+ return to_bool(self._settings.value(self.PROPERPROVIDER_KEY, False))
+
+ def set_properprovider(self, properprovider):
+ """
+ Sets whether the app should automatically login.
+
+ :param properprovider: True if the provider is properly configured,
+ False otherwise.
+ :type properprovider: bool
+ """
+ leap_assert_type(properprovider, bool)
+ self._settings.setValue(self.PROPERPROVIDER_KEY, properprovider)
+
+ def get_defaultprovider(self):
+ """
+ Returns the default provider to be used for autostarting EIP
+
+ :rtype: str or None
+ """
+ return self._settings.value(self.DEFAULTPROVIDER_KEY, None)
+
+ def set_defaultprovider(self, provider):
+ """
+ Sets the default provider to be used for autostarting EIP
+
+ :param provider: provider to use
+ :type provider: str or None
+ """
+ if provider is None:
+ self._settings.remove(self.DEFAULTPROVIDER_KEY)
+ else:
+ self._settings.setValue(self.DEFAULTPROVIDER_KEY, provider)
+
+ def get_alert_missing_scripts(self):
+ """
+ Returns the setting for alerting of missing up/down scripts.
+
+ :rtype: bool
+ """
+ return to_bool(self._settings.value(self.ALERTMISSING_KEY, True))
+
+ def set_alert_missing_scripts(self, value):
+ """
+ Sets the setting for alerting of missing up/down scripts.
+
+ :param value: the value to set
+ :type value: bool
+ """
+ leap_assert_type(value, bool)
+ self._settings.setValue(self.ALERTMISSING_KEY, value)
diff --git a/src/leap/base/specs.py b/src/leap/config/provider_spec.py
index fbe8a0e9..cf942c7b 100644
--- a/src/leap/base/specs.py
+++ b/src/leap/config/provider_spec.py
@@ -1,3 +1,20 @@
+# -*- coding: utf-8 -*-
+# provider_spec.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
leap_provider_spec = {
'description': 'provider definition',
'type': 'object',
@@ -5,7 +22,6 @@ leap_provider_spec = {
'version': {
'type': unicode,
'default': '0.1.0'
- #'required': True
},
"default_language": {
'type': unicode,
@@ -14,14 +30,11 @@ leap_provider_spec = {
'domain': {
'type': unicode, # XXX define uri type
'default': 'testprovider.example.org'
- #'required': True,
},
'name': {
- #'type': LEAPTranslatable,
'type': dict,
'format': 'translatable',
'default': {u'en': u'Test Provider'}
- #'required': True
},
'description': {
#'type': LEAPTranslatable,
@@ -57,6 +70,36 @@ leap_provider_spec = {
'languages': {
'type': list,
'default': ['en']
+ },
+ 'service': {
+ 'levels': {
+ 'type': list
+ },
+ 'default_service_level': {
+ 'type': int,
+ 'default': 1
+ },
+ 'allow_free': {
+ 'type': unicode
+ },
+ 'allow_paid': {
+ 'type': unicode
+ },
+ 'allow_anonymous': {
+ 'type': unicode
+ },
+ 'allow_registration': {
+ 'type': unicode
+ },
+ 'bandwidth_limit': {
+ 'type': int
+ },
+ 'allow_limited_bandwidth': {
+ 'type': unicode
+ },
+ 'allow_unlimited_bandwidth': {
+ 'type': unicode
+ }
}
}
}
diff --git a/src/leap/config/providerconfig.py b/src/leap/config/providerconfig.py
new file mode 100644
index 00000000..f899b17c
--- /dev/null
+++ b/src/leap/config/providerconfig.py
@@ -0,0 +1,218 @@
+# -*- coding: utf-8 -*-
+# providerconfig.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Provider configuration
+"""
+import logging
+import os
+
+from leap.common.check import leap_check
+from leap.common.config.baseconfig import BaseConfig, LocalizedKey
+from leap.config.provider_spec import leap_provider_spec
+
+logger = logging.getLogger(__name__)
+
+
+class MissingCACert(Exception):
+ """
+ Raised when a CA certificate is needed but not found.
+ """
+ pass
+
+
+class ProviderConfig(BaseConfig):
+ """
+ Provider configuration abstraction class
+ """
+ def __init__(self):
+ BaseConfig.__init__(self)
+
+ def _get_schema(self):
+ """
+ Returns the schema corresponding to the version given.
+
+ :rtype: dict or None if the version is not supported.
+ """
+ return leap_provider_spec
+
+ def _get_spec(self):
+ """
+ Returns the spec object for the specific configuration.
+
+ Override the BaseConfig one because we do not support multiple schemas
+ for the provider yet.
+
+ :rtype: dict or None if the version is not supported.
+ """
+ return self._get_schema()
+
+ def get_api_uri(self):
+ return self._safe_get_value("api_uri")
+
+ def get_api_version(self):
+ return self._safe_get_value("api_version")
+
+ def get_ca_cert_fingerprint(self):
+ return self._safe_get_value("ca_cert_fingerprint")
+
+ def get_ca_cert_uri(self):
+ return self._safe_get_value("ca_cert_uri")
+
+ def get_default_language(self):
+ return self._safe_get_value("default_language")
+
+ @LocalizedKey
+ def get_description(self):
+ return self._safe_get_value("description")
+
+ @classmethod
+ def sanitize_path_component(cls, component):
+ """
+ If the provider tries to instrument the component of a path
+ that is controlled by them, this will take care of
+ removing/escaping all the necessary elements.
+
+ :param component: Path component to process
+ :type component: unicode or str
+
+ :returns: The path component properly escaped
+ :rtype: unicode or str
+ """
+ # TODO: Fix for windows, names like "aux" or "con" aren't
+ # allowed.
+ return component.replace(os.path.sep, "")
+
+ def get_domain(self):
+ return ProviderConfig.sanitize_path_component(
+ self._safe_get_value("domain"))
+
+ def get_enrollment_policy(self):
+ """
+ Returns the enrollment policy
+
+ :rtype: string
+ """
+ return self._safe_get_value("enrollment_policy")
+
+ def get_languages(self):
+ return self._safe_get_value("languages")
+
+ @LocalizedKey
+ def get_name(self):
+ return self._safe_get_value("name")
+
+ def get_services(self):
+ """
+ Returns a list with the available services in the current provider.
+
+ :rtype: list
+ """
+ services = self._safe_get_value("services")
+ return services
+
+ def get_services_string(self):
+ """
+ Returns a string with the available services in the current
+ provider, ready to be shown to the user.
+ """
+ services_str = ", ".join(self.get_services())
+ services_str = services_str.replace(
+ "openvpn", "Encrypted Internet")
+ return services_str
+
+ def get_ca_cert_path(self, about_to_download=False):
+ """
+ Returns the path to the certificate for the current provider.
+ It may raise MissingCACert if
+ the certificate does not exists and not about_to_download
+
+ :param about_to_download: defines wether we want the path to
+ download the cert or not. This helps avoid
+ checking if the cert exists because we
+ are about to write it.
+ :type about_to_download: bool
+ """
+
+ cert_path = os.path.join(self.get_path_prefix(),
+ "leap",
+ "providers",
+ self.get_domain(),
+ "keys",
+ "ca",
+ "cacert.pem")
+
+ if not about_to_download:
+ cert_exists = os.path.exists(cert_path)
+ error_msg = "You need to download the certificate first"
+ leap_check(cert_exists, error_msg, MissingCACert)
+ logger.debug("Going to verify SSL against %s" % (cert_path,))
+
+ return cert_path
+
+ def provides_eip(self):
+ """
+ Returns True if this particular provider has the EIP service,
+ False otherwise.
+
+ :rtype: bool
+ """
+ return "openvpn" in self.get_services()
+
+ def provides_mx(self):
+ """
+ Returns True if this particular provider has the MX service,
+ False otherwise.
+
+ :rtype: bool
+ """
+ return "mx" in self.get_services()
+
+
+if __name__ == "__main__":
+ logger = logging.getLogger(name='leap')
+ logger.setLevel(logging.DEBUG)
+ console = logging.StreamHandler()
+ console.setLevel(logging.DEBUG)
+ formatter = logging.Formatter(
+ '%(asctime)s '
+ '- %(name)s - %(levelname)s - %(message)s')
+ console.setFormatter(formatter)
+ logger.addHandler(console)
+
+ provider = ProviderConfig()
+
+ try:
+ provider.get_api_version()
+ except Exception as e:
+ assert isinstance(e, AssertionError), "Expected an assert"
+ print "Safe value getting is working"
+
+ # standalone minitest
+ #if provider.load("provider_bad.json"):
+ if provider.load("leap/providers/bitmask.net/provider.json"):
+ print provider.get_api_version()
+ print provider.get_ca_cert_fingerprint()
+ print provider.get_ca_cert_uri()
+ print provider.get_default_language()
+ print provider.get_description()
+ print provider.get_description(lang="asd")
+ print provider.get_domain()
+ print provider.get_enrollment_policy()
+ print provider.get_languages()
+ print provider.get_name()
+ print provider.get_services()
diff --git a/src/leap/config/tests/test_providerconfig.py b/src/leap/config/tests/test_providerconfig.py
new file mode 100644
index 00000000..ff2828e6
--- /dev/null
+++ b/src/leap/config/tests/test_providerconfig.py
@@ -0,0 +1,279 @@
+# -*- coding: utf-8 -*-
+# test_providerconfig.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Tests for providerconfig
+"""
+
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+import os
+import json
+import copy
+
+from leap.common.testing.basetest import BaseLeapTest
+from leap.config.providerconfig import ProviderConfig, MissingCACert
+from leap.services import get_supported
+
+from mock import Mock
+
+
+sample_config = {
+ "api_uri": "https://api.test.bitmask.net:4430",
+ "api_version": "1",
+ "ca_cert_fingerprint":
+ "SHA256: 0f17c033115f6b76ff67871872303ff65034efe7dd1b910062ca323eb4da5c7e",
+ "ca_cert_uri": "https://test.bitmask.net/ca.crt",
+ "default_language": "en",
+ "description": {
+ "en": "Test description for provider",
+ "es": "Descripcion de prueba para el proveedor"
+ },
+ "domain": "test.bitmask.net",
+ "enrollment_policy": "open",
+ "languages": [
+ "en",
+ "es"
+ ],
+ "name": {
+ "en": "Bitmask testing environment",
+ "es": "Entorno de pruebas de Bitmask"
+ },
+ "service": {
+ "allow_anonymous": True,
+ "allow_free": True,
+ "allow_limited_bandwidth": True,
+ "allow_paid": False,
+ "allow_registration": True,
+ "allow_unlimited_bandwidth": False,
+ "bandwidth_limit": 400000,
+ "default_service_level": 1,
+ "levels": [
+ {
+ "bandwidth": "limited",
+ "id": 1,
+ "name": "anonymous"
+ },
+ {
+ "bandwidth": "limited",
+ "id": 2,
+ "name": "free",
+ "storage": 50
+ }
+ ]
+ },
+ "services": [
+ "openvpn"
+ ]
+}
+
+
+class ProviderConfigTest(BaseLeapTest):
+ """Tests for ProviderConfig"""
+
+ def setUp(self):
+ self._provider_config = ProviderConfig()
+ json_string = json.dumps(sample_config)
+ self._provider_config.load(data=json_string)
+
+ # At certain points we are going to be replacing these method
+ # to avoid creating a file.
+ # We need to save the old implementation and restore it in
+ # tearDown so we are sure everything is as expected for each
+ # test. If we do it inside each specific test, a failure in
+ # the test will leave the implementation with the mock.
+ self._old_ospath_exists = os.path.exists
+
+ def tearDown(self):
+ os.path.exists = self._old_ospath_exists
+
+ def test_configs_ok(self):
+ """
+ Test if the configs loads ok
+ """
+ # TODO: this test should go to the BaseConfig tests
+ pc = self._provider_config
+ self.assertEqual(pc.get_api_uri(), sample_config['api_uri'])
+ self.assertEqual(pc.get_api_version(), sample_config['api_version'])
+ self.assertEqual(pc.get_ca_cert_fingerprint(),
+ sample_config['ca_cert_fingerprint'])
+ self.assertEqual(pc.get_ca_cert_uri(), sample_config['ca_cert_uri'])
+ self.assertEqual(pc.get_default_language(),
+ sample_config['default_language'])
+
+ self.assertEqual(pc.get_domain(), sample_config['domain'])
+ self.assertEqual(pc.get_enrollment_policy(),
+ sample_config['enrollment_policy'])
+ self.assertEqual(pc.get_languages(), sample_config['languages'])
+
+ def test_localizations(self):
+ pc = self._provider_config
+
+ self.assertEqual(pc.get_description(lang='en'),
+ sample_config['description']['en'])
+ self.assertEqual(pc.get_description(lang='es'),
+ sample_config['description']['es'])
+
+ self.assertEqual(pc.get_name(lang='en'), sample_config['name']['en'])
+ self.assertEqual(pc.get_name(lang='es'), sample_config['name']['es'])
+
+ def _localize(self, lang):
+ """
+ Helper to change default language of the provider config.
+ """
+ pc = self._provider_config
+ config = copy.deepcopy(sample_config)
+ config['default_language'] = lang
+ json_string = json.dumps(config)
+ pc.load(data=json_string)
+
+ return config
+
+ def test_default_localization1(self):
+ pc = self._provider_config
+ config = self._localize(sample_config['languages'][0])
+
+ default_language = config['default_language']
+ default_description = config['description'][default_language]
+ default_name = config['name'][default_language]
+
+ self.assertEqual(pc.get_description(lang='xx'), default_description)
+ self.assertEqual(pc.get_description(), default_description)
+
+ self.assertEqual(pc.get_name(lang='xx'), default_name)
+ self.assertEqual(pc.get_name(), default_name)
+
+ def test_default_localization2(self):
+ pc = self._provider_config
+ config = self._localize(sample_config['languages'][1])
+
+ default_language = config['default_language']
+ default_description = config['description'][default_language]
+ default_name = config['name'][default_language]
+
+ self.assertEqual(pc.get_description(lang='xx'), default_description)
+ self.assertEqual(pc.get_description(), default_description)
+
+ self.assertEqual(pc.get_name(lang='xx'), default_name)
+ self.assertEqual(pc.get_name(), default_name)
+
+ def test_get_ca_cert_path_as_expected(self):
+ pc = self._provider_config
+ pc.get_path_prefix = Mock(return_value='test')
+
+ provider_domain = sample_config['domain']
+ expected_path = os.path.join('test', 'leap', 'providers',
+ provider_domain, 'keys', 'ca',
+ 'cacert.pem')
+
+ # mock 'os.path.exists' so we don't get an error for unexisting file
+ os.path.exists = Mock(return_value=True)
+ cert_path = pc.get_ca_cert_path()
+
+ self.assertEqual(cert_path, expected_path)
+
+ def test_get_ca_cert_path_about_to_download(self):
+ pc = self._provider_config
+ pc.get_path_prefix = Mock(return_value='test')
+
+ provider_domain = sample_config['domain']
+ expected_path = os.path.join('test', 'leap', 'providers',
+ provider_domain, 'keys', 'ca',
+ 'cacert.pem')
+
+ cert_path = pc.get_ca_cert_path(about_to_download=True)
+
+ self.assertEqual(cert_path, expected_path)
+
+ def test_get_ca_cert_path_fails(self):
+ pc = self._provider_config
+ pc.get_path_prefix = Mock(return_value='test')
+
+ # mock 'get_domain' so we don't need to load a config
+ provider_domain = 'test.provider.com'
+ pc.get_domain = Mock(return_value=provider_domain)
+
+ with self.assertRaises(MissingCACert):
+ pc.get_ca_cert_path()
+
+ def test_provides_eip(self):
+ pc = self._provider_config
+ config = copy.deepcopy(sample_config)
+
+ # It provides
+ config['services'] = ['openvpn', 'test_service']
+ json_string = json.dumps(config)
+ pc.load(data=json_string)
+ self.assertTrue(pc.provides_eip())
+
+ # It does not provides
+ config['services'] = ['test_service', 'other_service']
+ json_string = json.dumps(config)
+ pc.load(data=json_string)
+ self.assertFalse(pc.provides_eip())
+
+ def test_provides_mx(self):
+ pc = self._provider_config
+ config = copy.deepcopy(sample_config)
+
+ # It provides
+ config['services'] = ['mx', 'other_service']
+ json_string = json.dumps(config)
+ pc.load(data=json_string)
+ self.assertTrue(pc.provides_mx())
+
+ # It does not provides
+ config['services'] = ['test_service', 'other_service']
+ json_string = json.dumps(config)
+ pc.load(data=json_string)
+ self.assertFalse(pc.provides_mx())
+
+ def test_supports_unknown_service(self):
+ pc = self._provider_config
+ config = copy.deepcopy(sample_config)
+
+ config['services'] = ['unknown']
+ json_string = json.dumps(config)
+ pc.load(data=json_string)
+ self.assertFalse('unknown' in get_supported(pc.get_services()))
+
+ def test_provides_unknown_service(self):
+ pc = self._provider_config
+ config = copy.deepcopy(sample_config)
+
+ config['services'] = ['unknown']
+ json_string = json.dumps(config)
+ pc.load(data=json_string)
+ self.assertTrue('unknown' in pc.get_services())
+
+ def test_get_services_string(self):
+ pc = self._provider_config
+ config = copy.deepcopy(sample_config)
+ config['services'] = [
+ 'openvpn', 'asdf', 'openvpn', 'not_supported_service']
+ json_string = json.dumps(config)
+ pc.load(data=json_string)
+
+ self.assertEqual(pc.get_services_string(),
+ "Encrypted Internet, asdf, Encrypted Internet,"
+ " not_supported_service")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/leap/crypto/certs.py b/src/leap/crypto/certs.py
deleted file mode 100644
index cbb5725a..00000000
--- a/src/leap/crypto/certs.py
+++ /dev/null
@@ -1,112 +0,0 @@
-import logging
-import os
-from StringIO import StringIO
-import ssl
-import time
-
-from dateutil.parser import parse
-from OpenSSL import crypto
-
-from leap.util.misc import null_check
-
-logger = logging.getLogger(__name__)
-
-
-class BadCertError(Exception):
- """
- raised for malformed certs
- """
-
-
-class NoCertError(Exception):
- """
- raised for cert not found in given path
- """
-
-
-def get_https_cert_from_domain(domain, port=443):
- """
- @param domain: a domain name to get a certificate from.
- """
- cert = ssl.get_server_certificate((domain, port))
- x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
- return x509
-
-
-def get_cert_from_file(_file):
- null_check(_file, "pem file")
- if isinstance(_file, (str, unicode)):
- if not os.path.isfile(_file):
- raise NoCertError
- with open(_file) as f:
- cert = f.read()
- else:
- cert = _file.read()
- x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
- return x509
-
-
-def get_pkey_from_file(_file):
- getkey = lambda f: crypto.load_privatekey(
- crypto.FILETYPE_PEM, f.read())
-
- if isinstance(_file, str):
- with open(_file) as f:
- key = getkey(f)
- else:
- key = getkey(_file)
- return key
-
-
-def can_load_cert_and_pkey(string):
- """
- loads certificate and private key from
- a buffer
- """
- try:
- f = StringIO(string)
- cert = get_cert_from_file(f)
-
- f = StringIO(string)
- key = get_pkey_from_file(f)
-
- null_check(cert, 'certificate')
- null_check(key, 'private key')
- except Exception as exc:
- logger.error(type(exc), exc.message)
- raise BadCertError
- else:
- return True
-
-
-def get_cert_fingerprint(domain=None, port=443, filepath=None,
- hash_type="SHA256", sep=":"):
- """
- @param domain: a domain name to get a fingerprint from
- @type domain: str
- @param filepath: path to a file containing a PEM file
- @type filepath: str
- @param hash_type: the hash function to be used in the fingerprint.
- must be one of SHA1, SHA224, SHA256, SHA384, SHA512
- @type hash_type: str
- @rparam: hex_fpr, a hexadecimal representation of a bytestring
- containing the fingerprint.
- @rtype: string
- """
- if domain:
- cert = get_https_cert_from_domain(domain, port=port)
- if filepath:
- cert = get_cert_from_file(filepath)
- hex_fpr = cert.digest(hash_type)
- return hex_fpr
-
-
-def get_time_boundaries(certfile):
- cert = get_cert_from_file(certfile)
- null_check(cert, 'certificate')
-
- fromts, tots = (cert.get_notBefore(), cert.get_notAfter())
- from_, to_ = map(
- lambda ts: time.gmtime(time.mktime(parse(ts).timetuple())),
- (fromts, tots))
- return from_, to_
diff --git a/src/leap/crypto/certs_gnutls.py b/src/leap/crypto/certs_gnutls.py
deleted file mode 100644
index 20c0e043..00000000
--- a/src/leap/crypto/certs_gnutls.py
+++ /dev/null
@@ -1,112 +0,0 @@
-'''
-We're using PyOpenSSL now
-
-import ctypes
-from StringIO import StringIO
-import socket
-
-import gnutls.connection
-import gnutls.crypto
-import gnutls.library
-
-from leap.util.misc import null_check
-
-
-class BadCertError(Exception):
- """raised for malformed certs"""
-
-
-def get_https_cert_from_domain(domain):
- """
- @param domain: a domain name to get a certificate from.
- """
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- cred = gnutls.connection.X509Credentials()
-
- session = gnutls.connection.ClientSession(sock, cred)
- session.connect((domain, 443))
- session.handshake()
- cert = session.peer_certificate
- return cert
-
-
-def get_cert_from_file(_file):
- getcert = lambda f: gnutls.crypto.X509Certificate(f.read())
- if isinstance(_file, str):
- with open(_file) as f:
- cert = getcert(f)
- else:
- cert = getcert(_file)
- return cert
-
-
-def get_pkey_from_file(_file):
- getkey = lambda f: gnutls.crypto.X509PrivateKey(f.read())
- if isinstance(_file, str):
- with open(_file) as f:
- key = getkey(f)
- else:
- key = getkey(_file)
- return key
-
-
-def can_load_cert_and_pkey(string):
- try:
- f = StringIO(string)
- cert = get_cert_from_file(f)
-
- f = StringIO(string)
- key = get_pkey_from_file(f)
-
- null_check(cert, 'certificate')
- null_check(key, 'private key')
- except:
- # XXX catch GNUTLSError?
- raise BadCertError
- else:
- return True
-
-def get_cert_fingerprint(domain=None, filepath=None,
- hash_type="SHA256", sep=":"):
- """
- @param domain: a domain name to get a fingerprint from
- @type domain: str
- @param filepath: path to a file containing a PEM file
- @type filepath: str
- @param hash_type: the hash function to be used in the fingerprint.
- must be one of SHA1, SHA224, SHA256, SHA384, SHA512
- @type hash_type: str
- @rparam: hex_fpr, a hexadecimal representation of a bytestring
- containing the fingerprint.
- @rtype: string
- """
- if domain:
- cert = get_https_cert_from_domain(domain)
- if filepath:
- cert = get_cert_from_file(filepath)
-
- _buffer = ctypes.create_string_buffer(64)
- buffer_length = ctypes.c_size_t(64)
-
- SUPPORTED_DIGEST_FUN = ("SHA1", "SHA224", "SHA256", "SHA384", "SHA512")
- if hash_type in SUPPORTED_DIGEST_FUN:
- digestfunction = getattr(
- gnutls.library.constants,
- "GNUTLS_DIG_%s" % hash_type)
- else:
- # XXX improperlyconfigured or something
- raise Exception("digest function not supported")
-
- gnutls.library.functions.gnutls_x509_crt_get_fingerprint(
- cert._c_object, digestfunction,
- ctypes.byref(_buffer), ctypes.byref(buffer_length))
-
- # deinit
- #server_cert._X509Certificate__deinit(server_cert._c_object)
- # needed? is segfaulting
-
- fpr = ctypes.string_at(_buffer, buffer_length.value)
- hex_fpr = sep.join(u"%02X" % ord(char) for char in fpr)
-
- return hex_fpr
-'''
diff --git a/src/leap/crypto/leapkeyring.py b/src/leap/crypto/leapkeyring.py
deleted file mode 100644
index c241d0bc..00000000
--- a/src/leap/crypto/leapkeyring.py
+++ /dev/null
@@ -1,70 +0,0 @@
-import keyring
-
-from leap.base.config import get_config_file
-
-#############
-# Disclaimer
-#############
-# This currently is not a keyring, it's more like a joke.
-# No, seriously.
-# We're affected by this **bug**
-
-# https://bitbucket.org/kang/python-keyring-lib/
-# issue/65/dbusexception-method-opensession-with
-
-# so using the gnome keyring does not seem feasible right now.
-# I thought this was the next best option to store secrets in plain sight.
-
-# in the future we should move to use the gnome/kde/macosx/win keyrings.
-
-
-class LeapCryptedFileKeyring(keyring.backend.CryptedFileKeyring):
-
- filename = ".secrets"
-
- @property
- def file_path(self):
- return get_config_file(self.filename)
-
- def __init__(self, seed=None):
- self.seed = seed
-
- def _get_new_password(self):
- # XXX every time this method is called,
- # $deity kills a kitten.
- return "secret%s" % self.seed
-
- def _init_file(self):
- self.keyring_key = self._get_new_password()
- self.set_password('keyring_setting', 'pass_ref', 'pass_ref_value')
-
- def _unlock(self):
- self.keyring_key = self._get_new_password()
- print 'keyring key ', self.keyring_key
- try:
- ref_pw = self.get_password(
- 'keyring_setting',
- 'pass_ref')
- print 'ref pw ', ref_pw
- assert ref_pw == "pass_ref_value"
- except AssertionError:
- self._lock()
- raise ValueError('Incorrect password')
-
-
-def leap_set_password(key, value, seed="xxx"):
- key, value = map(unicode, (key, value))
- keyring.set_keyring(LeapCryptedFileKeyring(seed=seed))
- keyring.set_password('leap', key, value)
-
-
-def leap_get_password(key, seed="xxx"):
- keyring.set_keyring(LeapCryptedFileKeyring(seed=seed))
- #import ipdb;ipdb.set_trace()
- return keyring.get_password('leap', key)
-
-
-if __name__ == "__main__":
- leap_set_password('test', 'bar')
- passwd = leap_get_password('test')
- assert passwd == 'bar'
diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py
new file mode 100644
index 00000000..fc0533fc
--- /dev/null
+++ b/src/leap/crypto/srpauth.py
@@ -0,0 +1,606 @@
+# -*- coding: utf-8 -*-
+# srpauth.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import binascii
+import logging
+
+import requests
+import srp
+import json
+
+#this error is raised from requests
+from simplejson.decoder import JSONDecodeError
+from functools import partial
+
+from PySide import QtCore
+from twisted.internet import threads
+
+from leap.common.check import leap_assert
+from leap.util.constants import REQUEST_TIMEOUT
+from leap.util import request_helpers as reqhelper
+from leap.common.events import signal as events_signal
+from leap.common.events import events_pb2 as proto
+
+logger = logging.getLogger(__name__)
+
+
+class SRPAuthenticationError(Exception):
+ """
+ Exception raised for authentication errors
+ """
+ pass
+
+
+class SRPAuthConnectionError(SRPAuthenticationError):
+ """
+ Exception raised when there's a connection error
+ """
+ pass
+
+
+class SRPAuthUnknownUser(SRPAuthenticationError):
+ """
+ Exception raised when trying to authenticate an unknown user
+ """
+ pass
+
+
+class SRPAuthBadStatusCode(SRPAuthenticationError):
+ """
+ Exception raised when we received an unknown bad status code
+ """
+ pass
+
+
+class SRPAuthNoSalt(SRPAuthenticationError):
+ """
+ Exception raised when we don't receive the salt param at a
+ specific point in the auth process
+ """
+ pass
+
+
+class SRPAuthNoB(SRPAuthenticationError):
+ """
+ Exception raised when we don't receive the B param at a specific
+ point in the auth process
+ """
+ pass
+
+
+class SRPAuthBadDataFromServer(SRPAuthenticationError):
+ """
+ Generic exception when we receive bad data from the server.
+ """
+ pass
+
+
+class SRPAuthJSONDecodeError(SRPAuthenticationError):
+ """
+ Exception raised when there's a problem decoding the JSON content
+ parsed as received from th e server.
+ """
+ pass
+
+
+class SRPAuthBadPassword(SRPAuthenticationError):
+ """
+ Exception raised when the user provided a bad password to auth.
+ """
+ pass
+
+
+class SRPAuthVerificationFailed(SRPAuthenticationError):
+ """
+ Exception raised when we can't verify the SRP data received from
+ the server.
+ """
+ pass
+
+
+class SRPAuthNoSessionId(SRPAuthenticationError):
+ """
+ Exception raised when we don't receive a session id from the
+ server.
+ """
+ pass
+
+
+class SRPAuth(QtCore.QObject):
+ """
+ SRPAuth singleton
+ """
+
+ class __impl(QtCore.QObject):
+ """
+ Implementation of the SRPAuth interface
+ """
+
+ LOGIN_KEY = "login"
+ A_KEY = "A"
+ CLIENT_AUTH_KEY = "client_auth"
+ SESSION_ID_KEY = "_session_id"
+
+ def __init__(self, provider_config):
+ """
+ Constructor for SRPAuth implementation
+
+ :param server: Server to which we will authenticate
+ :type server: str
+ """
+ QtCore.QObject.__init__(self)
+
+ leap_assert(provider_config,
+ "We need a provider config to authenticate")
+
+ self._provider_config = provider_config
+
+ # **************************************************** #
+ # Dependency injection helpers, override this for more
+ # granular testing
+ self._fetcher = requests
+ self._srp = srp
+ self._hashfun = self._srp.SHA256
+ self._ng = self._srp.NG_1024
+ # **************************************************** #
+
+ self._session = self._fetcher.session()
+ self._session_id = None
+ self._session_id_lock = QtCore.QMutex()
+ self._uid = None
+ self._uid_lock = QtCore.QMutex()
+ self._token = None
+ self._token_lock = QtCore.QMutex()
+
+ self._srp_user = None
+ self._srp_a = None
+
+ def _safe_unhexlify(self, val):
+ """
+ Rounds the val to a multiple of 2 and returns the
+ unhexlified value
+
+ :param val: hexlified value
+ :type val: str
+
+ :rtype: binary hex data
+ :return: unhexlified val
+ """
+ return binascii.unhexlify(val) \
+ if (len(val) % 2 == 0) else binascii.unhexlify('0' + val)
+
+ def _authentication_preprocessing(self, username, password):
+ """
+ Generates the SRP.User to get the A SRP parameter
+
+ :param username: username to login
+ :type username: str
+ :param password: password for the username
+ :type password: str
+ """
+ logger.debug("Authentication preprocessing...")
+ self._srp_user = self._srp.User(username,
+ password,
+ self._hashfun,
+ self._ng)
+ _, A = self._srp_user.start_authentication()
+
+ self._srp_a = A
+
+ def _start_authentication(self, _, username):
+ """
+ Sends the first request for authentication to retrieve the
+ salt and B parameter
+
+ Might raise all SRPAuthenticationError based:
+ SRPAuthenticationError
+ SRPAuthConnectionError
+ SRPAuthUnknownUser
+ SRPAuthBadStatusCode
+ SRPAuthNoSalt
+ SRPAuthNoB
+
+ :param _: IGNORED, output from the previous callback (None)
+ :type _: IGNORED
+ :param username: username to login
+ :type username: str
+
+ :return: salt and B parameters
+ :rtype: tuple
+ """
+ logger.debug("Starting authentication process...")
+ try:
+ auth_data = {
+ self.LOGIN_KEY: username,
+ self.A_KEY: binascii.hexlify(self._srp_a)
+ }
+ sessions_url = "%s/%s/%s/" % \
+ (self._provider_config.get_api_uri(),
+ self._provider_config.get_api_version(),
+ "sessions")
+ init_session = self._session.post(sessions_url,
+ data=auth_data,
+ verify=self._provider_config.
+ get_ca_cert_path(),
+ timeout=REQUEST_TIMEOUT)
+ # Clean up A value, we don't need it anymore
+ self._srp_a = None
+ except requests.exceptions.ConnectionError as e:
+ logger.error("No connection made (salt): %r" %
+ (e,))
+ raise SRPAuthConnectionError("Could not establish a "
+ "connection")
+ except Exception as e:
+ logger.error("Unknown error: %r" % (e,))
+ raise SRPAuthenticationError("Unknown error: %r" %
+ (e,))
+
+ content, mtime = reqhelper.get_content(init_session)
+
+ if init_session.status_code not in (200,):
+ logger.error("No valid response (salt): "
+ "Status code = %r. Content: %r" %
+ (init_session.status_code, content))
+ if init_session.status_code == 422:
+ raise SRPAuthUnknownUser(self.tr("Unknown user"))
+
+ raise SRPAuthBadStatusCode(self.tr("There was a problem with"
+ " authentication"))
+
+ json_content = json.loads(content)
+ salt = json_content.get("salt", None)
+ B = json_content.get("B", None)
+
+ if salt is None:
+ logger.error("No salt parameter sent")
+ raise SRPAuthNoSalt(self.tr("The server did not send "
+ "the salt parameter"))
+ if B is None:
+ logger.error("No B parameter sent")
+ raise SRPAuthNoB(self.tr("The server did not send "
+ "the B parameter"))
+
+ return salt, B
+
+ def _process_challenge(self, salt_B, username):
+ """
+ Given the salt and B processes the auth challenge and
+ generates the M2 parameter
+
+ Might raise SRPAuthenticationError based:
+ SRPAuthenticationError
+ SRPAuthBadDataFromServer
+ SRPAuthConnectionError
+ SRPAuthJSONDecodeError
+ SRPAuthBadPassword
+
+ :param salt_B: salt and B parameters for the username
+ :type salt_B: tuple
+ :param username: username for this session
+ :type username: str
+
+ :return: the M2 SRP parameter
+ :rtype: str
+ """
+ logger.debug("Processing challenge...")
+ try:
+ salt, B = salt_B
+ unhex_salt = self._safe_unhexlify(salt)
+ unhex_B = self._safe_unhexlify(B)
+ except (TypeError, ValueError) as e:
+ logger.error("Bad data from server: %r" % (e,))
+ raise SRPAuthBadDataFromServer(
+ self.tr("The data sent from the server had errors"))
+ M = self._srp_user.process_challenge(unhex_salt, unhex_B)
+
+ auth_url = "%s/%s/%s/%s" % (self._provider_config.get_api_uri(),
+ self._provider_config.
+ get_api_version(),
+ "sessions",
+ username)
+
+ auth_data = {
+ self.CLIENT_AUTH_KEY: binascii.hexlify(M)
+ }
+
+ try:
+ auth_result = self._session.put(auth_url,
+ data=auth_data,
+ verify=self._provider_config.
+ get_ca_cert_path(),
+ timeout=REQUEST_TIMEOUT)
+ except requests.exceptions.ConnectionError as e:
+ logger.error("No connection made (HAMK): %r" % (e,))
+ raise SRPAuthConnectionError(self.tr("Could not connect to "
+ "the server"))
+
+ try:
+ content, mtime = reqhelper.get_content(auth_result)
+ except JSONDecodeError:
+ raise SRPAuthJSONDecodeError("Bad JSON content in auth result")
+
+ if auth_result.status_code == 422:
+ error = ""
+ try:
+ error = json.loads(content).get("errors", "")
+ except ValueError:
+ logger.error("Problem parsing the received response: %s"
+ % (content,))
+ except AttributeError:
+ logger.error("Expecting a dict but something else was "
+ "received: %s", (content,))
+ logger.error("[%s] Wrong password (HAMK): [%s]" %
+ (auth_result.status_code, error))
+ raise SRPAuthBadPassword(self.tr("Wrong password"))
+
+ if auth_result.status_code not in (200,):
+ logger.error("No valid response (HAMK): "
+ "Status code = %s. Content = %r" %
+ (auth_result.status_code, content))
+ raise SRPAuthBadStatusCode(self.tr("Unknown error (%s)") %
+ (auth_result.status_code,))
+
+ return json.loads(content)
+
+ def _extract_data(self, json_content):
+ """
+ Extracts the necessary parameters from json_content (M2,
+ id, token)
+
+ Might raise SRPAuthenticationError based:
+ SRPBadDataFromServer
+
+ :param json_content: Data received from the server
+ :type json_content: dict
+ """
+ try:
+ M2 = json_content.get("M2", None)
+ uid = json_content.get("id", None)
+ token = json_content.get("token", None)
+ except Exception as e:
+ logger.error(e)
+ raise SRPAuthBadDataFromServer("Something went wrong with the "
+ "login")
+
+ self.set_uid(uid)
+ self.set_token(token)
+
+ if M2 is None or self.get_uid() is None:
+ logger.error("Something went wrong. Content = %r" %
+ (json_content,))
+ raise SRPAuthBadDataFromServer(self.tr("Problem getting data "
+ "from server"))
+
+ events_signal(
+ proto.CLIENT_UID, content=uid,
+ reqcbk=lambda req, res: None) # make the rpc call async
+
+ return M2
+
+ def _verify_session(self, M2):
+ """
+ Verifies the session based on the M2 parameter. If the
+ verification succeeds, it sets the session_id for this
+ session
+
+ Might raise SRPAuthenticationError based:
+ SRPAuthBadDataFromServer
+ SRPAuthVerificationFailed
+
+ :param M2: M2 SRP parameter
+ :type M2: str
+ """
+ logger.debug("Verifying session...")
+ try:
+ unhex_M2 = self._safe_unhexlify(M2)
+ except TypeError:
+ logger.error("Bad data from server (HAWK)")
+ raise SRPAuthBadDataFromServer(self.tr("Bad data from server"))
+
+ self._srp_user.verify_session(unhex_M2)
+
+ if not self._srp_user.authenticated():
+ logger.error("Auth verification failed")
+ raise SRPAuthVerificationFailed(self.tr("Auth verification "
+ "failed"))
+ logger.debug("Session verified.")
+
+ session_id = self._session.cookies.get(self.SESSION_ID_KEY, None)
+ if not session_id:
+ logger.error("Bad cookie from server (missing _session_id)")
+ raise SRPAuthNoSessionId(self.tr("Session cookie "
+ "verification "
+ "failed"))
+
+ events_signal(
+ proto.CLIENT_SESSION_ID, content=session_id,
+ reqcbk=lambda req, res: None) # make the rpc call async
+
+ self.set_session_id(session_id)
+
+ def _threader(self, cb, res, *args, **kwargs):
+ return threads.deferToThread(cb, res, *args, **kwargs)
+
+ def authenticate(self, username, password):
+ """
+ Executes the whole authentication process for a user
+
+ Might raise SRPAuthenticationError
+
+ :param username: username for this session
+ :type username: str
+ :param password: password for this user
+ :type password: str
+
+ :returns: A defer on a different thread
+ :rtype: twisted.internet.defer.Deferred
+ """
+ leap_assert(self.get_session_id() is None, "Already logged in")
+
+ d = threads.deferToThread(self._authentication_preprocessing,
+ username=username,
+ password=password)
+
+ d.addCallback(
+ partial(self._threader,
+ self._start_authentication),
+ username=username)
+ d.addCallback(
+ partial(self._threader,
+ self._process_challenge),
+ username=username)
+ d.addCallback(
+ partial(self._threader,
+ self._extract_data))
+ d.addCallback(partial(self._threader,
+ self._verify_session))
+
+ return d
+
+ def logout(self):
+ """
+ Logs out the current session.
+ Expects a session_id to exists, might raise AssertionError
+ """
+ logger.debug("Starting logout...")
+
+ leap_assert(self.get_session_id(),
+ "Cannot logout an unexisting session")
+
+ logout_url = "%s/%s/%s/" % (self._provider_config.get_api_uri(),
+ self._provider_config.
+ get_api_version(),
+ "sessions")
+ try:
+ self._session.delete(logout_url,
+ data=self.get_session_id(),
+ verify=self._provider_config.
+ get_ca_cert_path(),
+ timeout=REQUEST_TIMEOUT)
+ except Exception as e:
+ logger.warning("Something went wrong with the logout: %r" %
+ (e,))
+
+ self.set_session_id(None)
+ self.set_uid(None)
+ # Also reset the session
+ self._session = self._fetcher.session()
+ logger.debug("Successfully logged out.")
+
+ def set_session_id(self, session_id):
+ QtCore.QMutexLocker(self._session_id_lock)
+ self._session_id = session_id
+
+ def get_session_id(self):
+ QtCore.QMutexLocker(self._session_id_lock)
+ return self._session_id
+
+ def set_uid(self, uid):
+ QtCore.QMutexLocker(self._uid_lock)
+ self._uid = uid
+
+ def get_uid(self):
+ QtCore.QMutexLocker(self._uid_lock)
+ return self._uid
+
+ def set_token(self, token):
+ QtCore.QMutexLocker(self._token_lock)
+ self._token = token
+
+ def get_token(self):
+ QtCore.QMutexLocker(self._token_lock)
+ return self._token
+
+ __instance = None
+
+ authentication_finished = QtCore.Signal(bool, str)
+ logout_finished = QtCore.Signal(bool, str)
+
+ def __init__(self, provider_config):
+ """
+ Creates a singleton instance if needed
+ """
+ QtCore.QObject.__init__(self)
+
+ # Check whether we already have an instance
+ if SRPAuth.__instance is None:
+ # Create and remember instance
+ SRPAuth.__instance = SRPAuth.__impl(provider_config)
+
+ # Store instance reference as the only member in the handle
+ self.__dict__['_SRPAuth__instance'] = SRPAuth.__instance
+
+ def authenticate(self, username, password):
+ """
+ Executes the whole authentication process for a user
+
+ Might raise SRPAuthenticationError based
+
+ :param username: username for this session
+ :type username: str
+ :param password: password for this user
+ :type password: str
+ """
+
+ d = self.__instance.authenticate(username, password)
+ d.addCallback(self._gui_notify)
+ d.addErrback(self._errback)
+ return d
+
+ def _gui_notify(self, _):
+ """
+ Callback that notifies the UI with the proper signal.
+
+ :param _: IGNORED, output from the previous callback (None)
+ :type _: IGNORED
+ """
+ logger.debug("Successful login!")
+ self.authentication_finished.emit(True, self.tr("Succeeded"))
+
+ def _errback(self, failure):
+ """
+ General errback for the whole login process. Will notify the
+ UI with the proper signal.
+
+ :param failure: Failure object captured from a callback.
+ :type failure: twisted.python.failure.Failure
+ """
+ logger.error("Error logging in %s" % (failure,))
+ self.authentication_finished.emit(False, "%s" % (failure.value,))
+ failure.trap(Exception)
+
+ def get_session_id(self):
+ return self.__instance.get_session_id()
+
+ def get_uid(self):
+ return self.__instance.get_uid()
+
+ def get_token(self):
+ return self.__instance.get_token()
+
+ def logout(self):
+ """
+ Logs out the current session.
+ Expects a session_id to exists, might raise AssertionError
+ """
+ try:
+ self.__instance.logout()
+ self.logout_finished.emit(True, self.tr("Succeeded"))
+ return True
+ except Exception as e:
+ self.logout_finished.emit(False, "%s" % (e,))
+ return False
diff --git a/src/leap/crypto/srpregister.py b/src/leap/crypto/srpregister.py
new file mode 100644
index 00000000..de1978b5
--- /dev/null
+++ b/src/leap/crypto/srpregister.py
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+# srpregister.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import binascii
+import logging
+
+import requests
+import srp
+
+from PySide import QtCore
+from urlparse import urlparse
+
+from leap.config.providerconfig import ProviderConfig
+from leap.util.constants import SIGNUP_TIMEOUT
+from leap.common.check import leap_assert, leap_assert_type
+
+logger = logging.getLogger(__name__)
+
+
+class SRPRegister(QtCore.QObject):
+ """
+ Registers a user to a specific provider using SRP
+ """
+
+ USER_LOGIN_KEY = 'user[login]'
+ USER_VERIFIER_KEY = 'user[password_verifier]'
+ USER_SALT_KEY = 'user[password_salt]'
+
+ registration_finished = QtCore.Signal(bool, object)
+
+ def __init__(self,
+ provider_config=None,
+ register_path="users"):
+ """
+ Constructor
+
+ :param provider_config: provider configuration instance,
+ properly loaded
+ :type privider_config: ProviderConfig
+ :param register_path: webapp path for registering users
+ :type register_path; str
+ """
+ QtCore.QObject.__init__(self)
+ leap_assert(provider_config, "Please provide a provider")
+ leap_assert_type(provider_config, ProviderConfig)
+
+ self._provider_config = provider_config
+
+ # **************************************************** #
+ # Dependency injection helpers, override this for more
+ # granular testing
+ self._fetcher = requests
+ self._srp = srp
+ self._hashfun = self._srp.SHA256
+ self._ng = self._srp.NG_1024
+ # **************************************************** #
+
+ parsed_url = urlparse(provider_config.get_api_uri())
+ self._provider = parsed_url.hostname
+ self._port = parsed_url.port
+ if self._port is None:
+ self._port = "443"
+
+ self._register_path = register_path
+
+ self._session = self._fetcher.session()
+
+ def _get_registration_uri(self):
+ """
+ Returns the URI where the register request should be made for
+ the provider
+
+ :rtype: str
+ """
+
+ uri = "https://%s:%s/%s/%s" % (
+ self._provider,
+ self._port,
+ self._provider_config.get_api_version(),
+ self._register_path)
+
+ return uri
+
+ def register_user(self, username, password):
+ """
+ Registers a user with the validator based on the password provider
+
+ :param username: username to register
+ :type username: str
+ :param password: password for this username
+ :type password: str
+
+ :rtype: tuple
+ :rparam: (ok, request)
+ """
+ salt, verifier = self._srp.create_salted_verification_key(
+ username,
+ password,
+ self._hashfun,
+ self._ng)
+
+ user_data = {
+ self.USER_LOGIN_KEY: username,
+ self.USER_VERIFIER_KEY: binascii.hexlify(verifier),
+ self.USER_SALT_KEY: binascii.hexlify(salt)
+ }
+
+ uri = self._get_registration_uri()
+
+ logger.debug('Post to uri: %s' % uri)
+ logger.debug("Will try to register user = %s" % (username,))
+
+ ok = False
+ # This should be None, but we don't like when PySide segfaults,
+ # so it something else.
+ # To reproduce it, just do:
+ # self.registration_finished.emit(False, None)
+ req = []
+ try:
+ req = self._session.post(uri,
+ data=user_data,
+ timeout=SIGNUP_TIMEOUT,
+ verify=self._provider_config.
+ get_ca_cert_path())
+
+ except (requests.exceptions.SSLError,
+ requests.exceptions.ConnectionError) as exc:
+ logger.error(exc.message)
+ ok = False
+ else:
+ ok = req.ok
+
+ self.registration_finished.emit(ok, req)
+ return ok
+
+
+if __name__ == "__main__":
+ logger = logging.getLogger(name='leap')
+ logger.setLevel(logging.DEBUG)
+ console = logging.StreamHandler()
+ console.setLevel(logging.DEBUG)
+ formatter = logging.Formatter(
+ '%(asctime)s '
+ '- %(name)s - %(levelname)s - %(message)s')
+ console.setFormatter(formatter)
+ logger.addHandler(console)
+
+ provider = ProviderConfig()
+
+ if provider.load("leap/providers/bitmask.net/provider.json"):
+ register = SRPRegister(provider_config=provider)
+ print "Registering user..."
+ print register.register_user("test1", "sarasaaaa")
+ print register.register_user("test2", "sarasaaaa")
diff --git a/src/leap/crypto/tests/__init__.py b/src/leap/crypto/tests/__init__.py
index e69de29b..7f118735 100644
--- a/src/leap/crypto/tests/__init__.py
+++ b/src/leap/crypto/tests/__init__.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# __init__.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
diff --git a/src/leap/crypto/tests/eip-service.json b/src/leap/crypto/tests/eip-service.json
new file mode 100644
index 00000000..24df42a2
--- /dev/null
+++ b/src/leap/crypto/tests/eip-service.json
@@ -0,0 +1,43 @@
+{
+ "gateways": [
+ {
+ "capabilities": {
+ "adblock": false,
+ "filter_dns": false,
+ "limited": true,
+ "ports": [
+ "1194",
+ "443",
+ "53",
+ "80"
+ ],
+ "protocols": [
+ "tcp",
+ "udp"
+ ],
+ "transport": [
+ "openvpn"
+ ],
+ "user_ips": false
+ },
+ "host": "harrier.cdev.bitmask.net",
+ "ip_address": "199.254.238.50",
+ "location": "seattle__wa"
+ }
+ ],
+ "locations": {
+ "seattle__wa": {
+ "country_code": "US",
+ "hemisphere": "N",
+ "name": "Seattle, WA",
+ "timezone": "-7"
+ }
+ },
+ "openvpn_configuration": {
+ "auth": "SHA1",
+ "cipher": "AES-128-CBC",
+ "tls-cipher": "DHE-RSA-AES128-SHA"
+ },
+ "serial": 1,
+ "version": 1
+} \ No newline at end of file
diff --git a/src/leap/crypto/tests/fake_provider.py b/src/leap/crypto/tests/fake_provider.py
new file mode 100755
index 00000000..54af485d
--- /dev/null
+++ b/src/leap/crypto/tests/fake_provider.py
@@ -0,0 +1,376 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# fake_provider.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""A server faking some of the provider resources and apis,
+used for testing Leap Client requests
+
+It needs that you create a subfolder named 'certs',
+and that you place the following files:
+
+XXX check if in use
+
+[ ] test-openvpn.pem
+[ ] test-provider.json
+[ ] test-eip-service.json
+"""
+import binascii
+import json
+import os
+import sys
+import time
+
+import srp
+
+from OpenSSL import SSL
+
+from zope.interface import Interface, Attribute, implements
+
+from twisted.web.server import Site, Request
+from twisted.web.static import File, Data
+from twisted.web.resource import Resource
+from twisted.internet import reactor
+
+from leap.common.testing.https_server import where
+
+# See
+# http://twistedmatrix.com/documents/current/web/howto/web-in-60/index.html
+# for more examples
+
+"""
+Testing the FAKE_API:
+#####################
+
+ 1) register an user
+ >> curl -d "user[login]=me" -d "user[password_salt]=foo" \
+ -d "user[password_verifier]=beef" http://localhost:8000/1/users
+ << {"errors": null}
+
+ 2) check that if you try to register again, it will fail:
+ >> curl -d "user[login]=me" -d "user[password_salt]=foo" \
+ -d "user[password_verifier]=beef" http://localhost:8000/1/users
+ << {"errors": {"login": "already taken!"}}
+
+"""
+
+# Globals to mock user/sessiondb
+
+_USERDB = {}
+_SESSIONDB = {}
+
+_here = os.path.split(__file__)[0]
+
+
+safe_unhexlify = lambda x: binascii.unhexlify(x) \
+ if (len(x) % 2 == 0) else binascii.unhexlify('0' + x)
+
+
+class IUser(Interface):
+ """
+ Defines the User Interface
+ """
+ login = Attribute("User login.")
+ salt = Attribute("Password salt.")
+ verifier = Attribute("Password verifier.")
+ session = Attribute("Session.")
+ svr = Attribute("Server verifier.")
+
+
+class User(object):
+ """
+ User object.
+ We store it in our simple session mocks
+ """
+
+ implements(IUser)
+
+ def __init__(self, login, salt, verifier):
+ self.login = login
+ self.salt = salt
+ self.verifier = verifier
+ self.session = None
+ self.svr = None
+
+ def set_server_verifier(self, svr):
+ """
+ Adds a svr verifier object to this
+ User instance
+ """
+ self.svr = svr
+
+ def set_session(self, session):
+ """
+ Adds this instance of User to the
+ global session dict
+ """
+ _SESSIONDB[session] = self
+ self.session = session
+
+
+class FakeUsers(Resource):
+ """
+ Resource that handles user registration.
+ """
+
+ def __init__(self, name):
+ self.name = name
+
+ def render_POST(self, request):
+ """
+ Handles POST to the users api resource
+ Simulates a login.
+ """
+ args = request.args
+
+ login = args['user[login]'][0]
+ salt = args['user[password_salt]'][0]
+ verifier = args['user[password_verifier]'][0]
+
+ if login in _USERDB:
+ request.setResponseCode(422)
+ return "%s\n" % json.dumps(
+ {'errors': {'login': 'already taken!'}})
+
+ print '[server]', login, verifier, salt
+ user = User(login, salt, verifier)
+ _USERDB[login] = user
+ return json.dumps({'errors': None})
+
+
+def getSession(self, sessionInterface=None):
+ """
+ we overwrite twisted.web.server.Request.getSession method to
+ put the right cookie name in place
+ """
+ if not self.session:
+ #cookiename = b"_".join([b'TWISTED_SESSION'] + self.sitepath)
+ cookiename = b"_".join([b'_session_id'] + self.sitepath)
+ sessionCookie = self.getCookie(cookiename)
+ if sessionCookie:
+ try:
+ self.session = self.site.getSession(sessionCookie)
+ except KeyError:
+ pass
+ # if it still hasn't been set, fix it up.
+ if not self.session:
+ self.session = self.site.makeSession()
+ self.addCookie(cookiename, self.session.uid, path=b'/')
+ self.session.touch()
+ if sessionInterface:
+ return self.session.getComponent(sessionInterface)
+ return self.session
+
+
+def get_user(request):
+ """
+ Returns user from the session dict
+ """
+ login = request.args.get('login')
+ if login:
+ user = _USERDB.get(login[0], None)
+ if user:
+ return user
+
+ request.getSession = getSession.__get__(request, Request)
+ session = request.getSession()
+
+ user = _SESSIONDB.get(session, None)
+ return user
+
+
+class FakeSession(Resource):
+ def __init__(self, name):
+ """
+ Initializes session
+ """
+ self.name = name
+
+ def render_GET(self, request):
+ """
+ Handles GET requests.
+ """
+ return "%s\n" % json.dumps({'errors': None})
+
+ def render_POST(self, request):
+ """
+ Handles POST requests.
+ """
+ user = get_user(request)
+
+ if not user:
+ # XXX get real error from demo provider
+ return json.dumps({'errors': 'no such user'})
+
+ A = request.args['A'][0]
+
+ _A = safe_unhexlify(A)
+ _salt = safe_unhexlify(user.salt)
+ _verifier = safe_unhexlify(user.verifier)
+
+ svr = srp.Verifier(
+ user.login,
+ _salt,
+ _verifier,
+ _A,
+ hash_alg=srp.SHA256,
+ ng_type=srp.NG_1024)
+
+ s, B = svr.get_challenge()
+
+ _B = binascii.hexlify(B)
+
+ print '[server] login = %s' % user.login
+ print '[server] salt = %s' % user.salt
+ print '[server] len(_salt) = %s' % len(_salt)
+ print '[server] vkey = %s' % user.verifier
+ print '[server] len(vkey) = %s' % len(_verifier)
+ print '[server] s = %s' % binascii.hexlify(s)
+ print '[server] B = %s' % _B
+ print '[server] len(B) = %s' % len(_B)
+
+ # override Request.getSession
+ request.getSession = getSession.__get__(request, Request)
+ session = request.getSession()
+
+ user.set_session(session)
+ user.set_server_verifier(svr)
+
+ # yep, this is tricky.
+ # some things are *already* unhexlified.
+ data = {
+ 'salt': user.salt,
+ 'B': _B,
+ 'errors': None}
+
+ return json.dumps(data)
+
+ def render_PUT(self, request):
+ """
+ Handles PUT requests.
+ """
+ # XXX check session???
+ user = get_user(request)
+
+ if not user:
+ print '[server] NO USER'
+ return json.dumps({'errors': 'no such user'})
+
+ data = request.content.read()
+ auth = data.split("client_auth=")
+ M = auth[1] if len(auth) > 1 else None
+ # if not H, return
+ if not M:
+ return json.dumps({'errors': 'no M proof passed by client'})
+
+ svr = user.svr
+ HAMK = svr.verify_session(binascii.unhexlify(M))
+ if HAMK is None:
+ print '[server] verification failed!!!'
+ raise Exception("Authentication failed!")
+ #import ipdb;ipdb.set_trace()
+
+ assert svr.authenticated()
+ print "***"
+ print '[server] User successfully authenticated using SRP!'
+ print "***"
+
+ return json.dumps(
+ {'M2': binascii.hexlify(HAMK),
+ 'id': '9c943eb9d96a6ff1b7a7030bdeadbeef',
+ 'errors': None})
+
+
+class API_Sessions(Resource):
+ """
+ Top resource for the API v1
+ """
+ def getChild(self, name, request):
+ return FakeSession(name)
+
+
+class FileModified(File):
+ def render_GET(self, request):
+ since = request.getHeader('if-modified-since')
+ if since:
+ tsince = time.strptime(since.replace(" GMT", ""))
+ tfrom = time.strptime(time.ctime(os.path.getmtime(self.path)))
+ if tfrom > tsince:
+ return File.render_GET(self, request)
+ else:
+ request.setResponseCode(304)
+ return ""
+ return File.render_GET(self, request)
+
+
+class OpenSSLServerContextFactory(object):
+
+ def getContext(self):
+ """
+ Create an SSL context.
+ """
+ ctx = SSL.Context(SSL.SSLv23_METHOD)
+ #ctx = SSL.Context(SSL.TLSv1_METHOD)
+ ctx.use_certificate_file(where('leaptestscert.pem'))
+ ctx.use_privatekey_file(where('leaptestskey.pem'))
+
+ return ctx
+
+
+def get_provider_factory():
+ """
+ Instantiates a Site that serves the resources
+ that we expect from a valid provider.
+ Listens on:
+ * port 8000 for http connections
+ * port 8443 for https connections
+
+ :rparam: factory for a site
+ :rtype: Site instance
+ """
+ root = Data("", "")
+ root.putChild("", root)
+ root.putChild("provider.json", FileModified(
+ os.path.join(_here,
+ "test_provider.json")))
+ config = Resource()
+ config.putChild(
+ "eip-service.json",
+ FileModified(
+ os.path.join(_here, "eip-service.json")))
+ apiv1 = Resource()
+ apiv1.putChild("config", config)
+ apiv1.putChild("sessions", API_Sessions())
+ apiv1.putChild("users", FakeUsers(None))
+ apiv1.putChild("cert", FileModified(
+ os.path.join(_here,
+ 'openvpn.pem')))
+ root.putChild("1", apiv1)
+
+ factory = Site(root)
+ return factory
+
+
+if __name__ == "__main__":
+
+ from twisted.python import log
+ log.startLogging(sys.stdout)
+
+ factory = get_provider_factory()
+
+ # regular http (for debugging with curl)
+ reactor.listenTCP(8000, factory)
+ reactor.listenSSL(8443, factory, OpenSSLServerContextFactory())
+ reactor.run()
diff --git a/src/leap/crypto/tests/openvpn.pem b/src/leap/crypto/tests/openvpn.pem
new file mode 100644
index 00000000..a95e9370
--- /dev/null
+++ b/src/leap/crypto/tests/openvpn.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFtTCCA52gAwIBAgIJAIGJ8Dg+DtemMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTMwNjI2MjAyMDIyWhcNMTgwNjI2MjAyMDIyWjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEAxJaN0lWjFu+3j48c0WG8BvmPUf026Xli5d5NE4EjGsirwfre0oTeWZT9
+WRxqLGd2wDh6Mc9r6UqH6dwqLZKbsgwB5zI2lag7UWFttJF1U1c6AJynhaLMoy73
+sL9USTmQ57iYRFrVP/nGj9/L6I1XnV6midPi7a5aZreH9q8dWaAhmc9eFDU+Y4vS
+sTFS6aomajLrI6YWo5toKqLq8IMryD03IM78a7gJtLgfWs+pYZRUBlM5JaYX98eX
+mVPAYYH9krWxLVN3hTt1ngECzK+epo275zQJh960/2fNCfVJSXqSXcficLs+bR7t
+FEkNuOP1hFV6LuoLL+k5Su+hp5kXMYZTvYYDpW4nPJoBdSG1w5O5IxO6zh+9VLB7
+oLrlgoyWvBoou5coCBpZVU6UyWcOx58kuZF8wNr0GgdvWAFwOGVuVG5jmcVdhaKC
+0C8NxHrxlhcrcp0zwtDaOxfmZfcxiXs35iwUip5vS18Nv+XBK8ad9T79Ox8nSzP3
+RGPVDpExz7gPbZglqSe47XBIk0ZuIzgOgYpJj4JrpoewoIYb+OmUgI7UZjoGsMrV
++B2BqOKs7kF0HW3i5bR9YAi0ZYvnhQgjBtwCKm4zvLqwuPZHz9VWgIk6uezgStCP
+WyzQ8IcopK49fOjcKa6JT5JRU+27paIZf1BkQsTkJy/Nti4TvwMCAwEAAaOBpzCB
+pDAdBgNVHQ4EFgQUEgXSd3Yl3xAzbkWa7xeNe27d99cwdQYDVR0jBG4wbIAUEgXS
+d3Yl3xAzbkWa7xeNe27d99ehSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT
+b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCB
+ifA4Pg7XpjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQA6Vl9Ve4Qe
+ewzXAxr0BabFRhtIuF7DV+/niT46qJhW2KgYe6rwZqdAhEbgH3kTPJ5JmmcUnAEH
+nmrfoku/YAb5ObfdHUACsHy4cvSvFwBUQ9vXP6+oOFJhrGW4uzRI2pHGvnqB3lQ0
+JEPmPwduBCI5reRYauPbd4Wl4VhLGrjELb4JQZL24Q5ehXMnv415m7+aMkLzT2IA
+p6B2xgRR+JAeUdyCNOV1f5AqJWyAUJPWGR0e1OTKNfc49+2skK0NmzrpGsoktSHa
+uN6vGBCVGiZh7BTYblWMG5q9Am7idcdmC2fdpIf5yj7CKzV7WIPxPs0I7TuRcr41
+pUBLCAElcyCPB89lySol2BDs4gk4wZs4y2shUs3o0+mIpw/6o8tQF/9IL8ALkLqr
+q9SuND7O1RXcg74o3HeVmRKtoI/KdgaVhJ0rFvcq83ftfu3KMyWB6SOKOu6ZYON8
+AcSjsDDpnDrwGFvjAYHiTkS9NaaJC1/g7Y6jjhxmbTkXPA6V8MvLKQiOvqk/9gCh
+85FHsFkElIYnH6fbHIRxg20cnqmddTd+H5HgBIlhiKWuydtuoQFwzR/D3ypgLBaB
+OWLcBP7I+RYhKlJFIWnfiyB0xbyI4W/UfL8p8jQI8TE9oIlm3WqxJXfebDEDEstj
+8nS4Fb3G5Wr4pZMjfbtmBSAgHeWH6B90jg==
+-----END CERTIFICATE-----
diff --git a/src/leap/crypto/tests/test_certs.py b/src/leap/crypto/tests/test_certs.py
deleted file mode 100644
index e476b630..00000000
--- a/src/leap/crypto/tests/test_certs.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import unittest
-
-from leap.testing.https_server import where
-from leap.crypto import certs
-
-
-class CertTestCase(unittest.TestCase):
-
- def test_can_load_client_and_pkey(self):
- with open(where('leaptestscert.pem')) as cf:
- cs = cf.read()
- with open(where('leaptestskey.pem')) as kf:
- ks = kf.read()
- certs.can_load_cert_and_pkey(cs + ks)
-
- with self.assertRaises(certs.BadCertError):
- # screw header
- certs.can_load_cert_and_pkey(cs.replace("BEGIN", "BEGINN") + ks)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/crypto/tests/test_provider.json b/src/leap/crypto/tests/test_provider.json
new file mode 100644
index 00000000..c37bef8f
--- /dev/null
+++ b/src/leap/crypto/tests/test_provider.json
@@ -0,0 +1,15 @@
+{
+ "api_uri": "https://localhost:8443",
+ "api_version": "1",
+ "ca_cert_fingerprint": "SHA256: 0f17c033115f6b76ff67871872303ff65034efe7dd1b910062ca323eb4da5c7e",
+ "ca_cert_uri": "https://bitmask.net/ca.crt",
+ "default_language": "en",
+ "domain": "example.com",
+ "enrollment_policy": "open",
+ "name": {
+ "en": "Bitmask"
+ },
+ "services": [
+ "openvpn"
+ ]
+}
diff --git a/src/leap/crypto/tests/test_srpauth.py b/src/leap/crypto/tests/test_srpauth.py
new file mode 100644
index 00000000..e3258fd3
--- /dev/null
+++ b/src/leap/crypto/tests/test_srpauth.py
@@ -0,0 +1,790 @@
+# -*- coding: utf-8 -*-
+# test_srpauth.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Tests for:
+ * leap/crypto/srpauth.py
+"""
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+import os
+import sys
+import binascii
+import requests
+import mock
+
+from mock import MagicMock
+from nose.twistedtools import reactor, deferred
+from twisted.python import log
+from twisted.internet import threads
+from functools import partial
+from requests.models import Response
+from simplejson.decoder import JSONDecodeError
+
+from leap.common.testing.https_server import where
+from leap.config.providerconfig import ProviderConfig
+from leap.crypto import srpregister, srpauth
+from leap.crypto.tests import fake_provider
+from leap.util.request_helpers import get_content
+
+log.startLogging(sys.stdout)
+
+
+def _get_capath():
+ return where("cacert.pem")
+
+_here = os.path.split(__file__)[0]
+
+
+class ImproperlyConfiguredError(Exception):
+ """
+ Raised if the test provider is missing configuration
+ """
+
+
+class SRPAuthTestCase(unittest.TestCase):
+ """
+ Tests for the SRPAuth class
+ """
+ __name__ = "SRPAuth tests"
+
+ def setUp(self):
+ """
+ Sets up this TestCase with a simple and faked provider instance:
+
+ * runs a threaded reactor
+ * loads a mocked ProviderConfig that points to the certs in the
+ leap.common.testing module.
+ """
+ factory = fake_provider.get_provider_factory()
+ http = reactor.listenTCP(0, factory)
+ https = reactor.listenSSL(
+ 0, factory,
+ fake_provider.OpenSSLServerContextFactory())
+ get_port = lambda p: p.getHost().port
+ self.http_port = get_port(http)
+ self.https_port = get_port(https)
+
+ provider = ProviderConfig()
+ provider.get_ca_cert_path = mock.create_autospec(
+ provider.get_ca_cert_path)
+ provider.get_ca_cert_path.return_value = _get_capath()
+
+ provider.get_api_uri = mock.create_autospec(
+ provider.get_api_uri)
+ provider.get_api_uri.return_value = self._get_https_uri()
+
+ loaded = provider.load(path=os.path.join(
+ _here, "test_provider.json"))
+ if not loaded:
+ raise ImproperlyConfiguredError(
+ "Could not load test provider config")
+ self.register = srpregister.SRPRegister(provider_config=provider)
+ self.provider = provider
+ self.TEST_USER = "register_test_auth"
+ self.TEST_PASS = "pass"
+
+ # Reset the singleton
+ srpauth.SRPAuth._SRPAuth__instance = None
+ self.auth = srpauth.SRPAuth(self.provider)
+ self.auth_backend = self.auth._SRPAuth__instance
+
+ self.old_post = self.auth_backend._session.post
+ self.old_put = self.auth_backend._session.put
+ self.old_delete = self.auth_backend._session.delete
+
+ self.old_start_auth = self.auth_backend._start_authentication
+ self.old_proc_challenge = self.auth_backend._process_challenge
+ self.old_extract_data = self.auth_backend._extract_data
+ self.old_verify_session = self.auth_backend._verify_session
+ self.old_auth_preproc = self.auth_backend._authentication_preprocessing
+ self.old_get_sid = self.auth_backend.get_session_id
+ self.old_cookie_get = self.auth_backend._session.cookies.get
+ self.old_auth = self.auth_backend.authenticate
+
+ def tearDown(self):
+ self.auth_backend._session.post = self.old_post
+ self.auth_backend._session.put = self.old_put
+ self.auth_backend._session.delete = self.old_delete
+
+ self.auth_backend._start_authentication = self.old_start_auth
+ self.auth_backend._process_challenge = self.old_proc_challenge
+ self.auth_backend._extract_data = self.old_extract_data
+ self.auth_backend._verify_session = self.old_verify_session
+ self.auth_backend._authentication_preprocessing = self.old_auth_preproc
+ self.auth_backend.get_session_id = self.old_get_sid
+ self.auth_backend._session.cookies.get = self.old_cookie_get
+ self.auth_backend.authenticate = self.old_auth
+
+ # helper methods
+
+ def _get_https_uri(self):
+ """
+ Returns a https uri with the right https port initialized
+ """
+ return "https://localhost:%s" % (self.https_port,)
+
+ # Auth tests
+
+ def _prepare_auth_test(self, code=200, side_effect=None):
+ """
+ Creates the needed defers to test several test situations. It
+ adds up to the auth preprocessing step.
+
+ :param code: status code for the response of POST in requests
+ :type code: int
+ :param side_effect: side effect triggered by the POST method
+ in requests
+ :type side_effect: some kind of Exception
+
+ :returns: the defer that is created
+ :rtype: defer.Deferred
+ """
+ res = Response()
+ res.status_code = code
+ self.auth_backend._session.post = mock.create_autospec(
+ self.auth_backend._session.post,
+ return_value=res,
+ side_effect=side_effect)
+
+ d = threads.deferToThread(self.register.register_user,
+ self.TEST_USER,
+ self.TEST_PASS)
+
+ def wrapper_preproc(*args):
+ return threads.deferToThread(
+ self.auth_backend._authentication_preprocessing,
+ self.TEST_USER, self.TEST_PASS)
+
+ d.addCallback(wrapper_preproc)
+
+ return d
+
+ def test_safe_unhexlify(self):
+ input_value = "somestring"
+ test_value = binascii.hexlify(input_value)
+ self.assertEqual(
+ self.auth_backend._safe_unhexlify(test_value),
+ input_value)
+
+ def test_safe_unhexlify_not_raises(self):
+ input_value = "somestring"
+ test_value = binascii.hexlify(input_value)[:-1]
+
+ with self.assertRaises(TypeError):
+ binascii.unhexlify(test_value)
+
+ self.auth_backend._safe_unhexlify(test_value)
+
+ def test_preprocessing_loads_a(self):
+ self.assertEqual(self.auth_backend._srp_a, None)
+ self.auth_backend._authentication_preprocessing("user", "pass")
+ self.assertIsNotNone(self.auth_backend._srp_a)
+ self.assertTrue(len(self.auth_backend._srp_a) > 0)
+
+ @deferred()
+ def test_start_authentication(self):
+ d = threads.deferToThread(self.register.register_user, self.TEST_USER,
+ self.TEST_PASS)
+
+ def wrapper_preproc(*args):
+ return threads.deferToThread(
+ self.auth_backend._authentication_preprocessing,
+ self.TEST_USER, self.TEST_PASS)
+
+ d.addCallback(wrapper_preproc)
+
+ def wrapper(_):
+ return threads.deferToThread(
+ self.auth_backend._start_authentication,
+ None, self.TEST_USER)
+
+ d.addCallback(wrapper)
+ return d
+
+ @deferred()
+ def test_start_authentication_fails_connerror(self):
+ d = self._prepare_auth_test(
+ side_effect=requests.exceptions.ConnectionError())
+
+ def wrapper(_):
+ with self.assertRaises(srpauth.SRPAuthConnectionError):
+ self.auth_backend._start_authentication(None, self.TEST_USER)
+
+ d.addCallback(partial(threads.deferToThread, wrapper))
+ return d
+
+ @deferred()
+ def test_start_authentication_fails_any_error(self):
+ d = self._prepare_auth_test(side_effect=Exception())
+
+ def wrapper(_):
+ with self.assertRaises(srpauth.SRPAuthenticationError):
+ self.auth_backend._start_authentication(None, self.TEST_USER)
+
+ d.addCallback(partial(threads.deferToThread, wrapper))
+ return d
+
+ @deferred()
+ def test_start_authentication_fails_unknown_user(self):
+ d = self._prepare_auth_test(422)
+
+ def wrapper(_):
+ with self.assertRaises(srpauth.SRPAuthUnknownUser):
+ with mock.patch('leap.util.request_helpers.get_content',
+ new=mock.create_autospec(get_content)) as \
+ content:
+ content.return_value = ("{}", 0)
+
+ self.auth_backend._start_authentication(
+ None, self.TEST_USER)
+
+ d.addCallback(partial(threads.deferToThread, wrapper))
+ return d
+
+ @deferred()
+ def test_start_authentication_fails_errorcode(self):
+ d = self._prepare_auth_test(302)
+
+ def wrapper(_):
+ with self.assertRaises(srpauth.SRPAuthBadStatusCode):
+ with mock.patch('leap.util.request_helpers.get_content',
+ new=mock.create_autospec(get_content)) as \
+ content:
+ content.return_value = ("{}", 0)
+
+ self.auth_backend._start_authentication(None,
+ self.TEST_USER)
+
+ d.addCallback(partial(threads.deferToThread, wrapper))
+ return d
+
+ @deferred()
+ def test_start_authentication_fails_no_salt(self):
+ d = self._prepare_auth_test(200)
+
+ def wrapper(_):
+ with self.assertRaises(srpauth.SRPAuthNoSalt):
+ with mock.patch('leap.util.request_helpers.get_content',
+ new=mock.create_autospec(get_content)) as \
+ content:
+ content.return_value = ("{}", 0)
+
+ self.auth_backend._start_authentication(None,
+ self.TEST_USER)
+
+ d.addCallback(partial(threads.deferToThread, wrapper))
+ return d
+
+ @deferred()
+ def test_start_authentication_fails_no_B(self):
+ d = self._prepare_auth_test(200)
+
+ def wrapper(_):
+ with self.assertRaises(srpauth.SRPAuthNoB):
+ with mock.patch('leap.util.request_helpers.get_content',
+ new=mock.create_autospec(get_content)) as \
+ content:
+ content.return_value = ('{"salt": ""}', 0)
+
+ self.auth_backend._start_authentication(None,
+ self.TEST_USER)
+
+ d.addCallback(partial(threads.deferToThread, wrapper))
+ return d
+
+ @deferred()
+ def test_start_authentication_correct_saltb(self):
+ d = self._prepare_auth_test(200)
+
+ test_salt = "12345"
+ test_B = "67890"
+
+ def wrapper(_):
+ with mock.patch('leap.util.request_helpers.get_content',
+ new=mock.create_autospec(get_content)) as \
+ content:
+ content.return_value = ('{"salt":"%s", "B":"%s"}' % (test_salt,
+ test_B),
+ 0)
+
+ salt, B = self.auth_backend._start_authentication(
+ None,
+ self.TEST_USER)
+ self.assertEqual(salt, test_salt)
+ self.assertEqual(B, test_B)
+
+ d.addCallback(partial(threads.deferToThread, wrapper))
+ return d
+
+ def _prepare_auth_challenge(self):
+ """
+ Creates the needed defers to test several test situations. It
+ adds up to the start authentication step.
+
+ :returns: the defer that is created
+ :rtype: defer.Deferred
+ """
+ d = threads.deferToThread(self.register.register_user,
+ self.TEST_USER,
+ self.TEST_PASS)
+
+ def wrapper_preproc(*args):
+ return threads.deferToThread(
+ self.auth_backend._authentication_preprocessing,
+ self.TEST_USER, self.TEST_PASS)
+
+ d.addCallback(wrapper_preproc)
+
+ def wrapper_start(*args):
+ return threads.deferToThread(
+ self.auth_backend._start_authentication,
+ None, self.TEST_USER)
+
+ d.addCallback(wrapper_start)
+
+ return d
+
+ @deferred()
+ def test_process_challenge_wrong_saltb(self):
+ d = self._prepare_auth_challenge()
+
+ def wrapper(salt_B):
+ with self.assertRaises(srpauth.SRPAuthBadDataFromServer):
+ self.auth_backend._process_challenge("",
+ username=self.TEST_USER)
+
+ d.addCallback(partial(threads.deferToThread, wrapper))
+ return d
+
+ @deferred()
+ def test_process_challenge_requests_problem_raises(self):
+ d = self._prepare_auth_challenge()
+
+ self.auth_backend._session.put = mock.create_autospec(
+ self.auth_backend._session.put,
+ side_effect=requests.exceptions.ConnectionError())
+
+ def wrapper(salt_B):
+ with self.assertRaises(srpauth.SRPAuthConnectionError):
+ self.auth_backend._process_challenge(salt_B,
+ username=self.TEST_USER)
+
+ d.addCallback(partial(threads.deferToThread, wrapper))
+
+ return d
+
+ @deferred()
+ def test_process_challenge_json_decode_error(self):
+ d = self._prepare_auth_challenge()
+
+ def wrapper(salt_B):
+ with mock.patch('leap.util.request_helpers.get_content',
+ new=mock.create_autospec(get_content)) as \
+ content:
+ content.return_value = ("{", 0)
+ content.side_effect = JSONDecodeError("", "", 0)
+
+ with self.assertRaises(srpauth.SRPAuthJSONDecodeError):
+ self.auth_backend._process_challenge(
+ salt_B,
+ username=self.TEST_USER)
+
+ d.addCallback(partial(threads.deferToThread, wrapper))
+
+ return d
+
+ @deferred()
+ def test_process_challenge_bad_password(self):
+ d = self._prepare_auth_challenge()
+
+ res = Response()
+ res.status_code = 422
+ self.auth_backend._session.put = mock.create_autospec(
+ self.auth_backend._session.put,
+ return_value=res)
+
+ def wrapper(salt_B):
+ with mock.patch('leap.util.request_helpers.get_content',
+ new=mock.create_autospec(get_content)) as \
+ content:
+ content.return_value = ("", 0)
+ with self.assertRaises(srpauth.SRPAuthBadPassword):
+ self.auth_backend._process_challenge(
+ salt_B,
+ username=self.TEST_USER)
+
+ d.addCallback(partial(threads.deferToThread, wrapper))
+
+ return d
+
+ @deferred()
+ def test_process_challenge_bad_password2(self):
+ d = self._prepare_auth_challenge()
+
+ res = Response()
+ res.status_code = 422
+ self.auth_backend._session.put = mock.create_autospec(
+ self.auth_backend._session.put,
+ return_value=res)
+
+ def wrapper(salt_B):
+ with mock.patch('leap.util.request_helpers.get_content',
+ new=mock.create_autospec(get_content)) as \
+ content:
+ content.return_value = ("[]", 0)
+ with self.assertRaises(srpauth.SRPAuthBadPassword):
+ self.auth_backend._process_challenge(
+ salt_B,
+ username=self.TEST_USER)
+
+ d.addCallback(partial(threads.deferToThread, wrapper))
+
+ return d
+
+ @deferred()
+ def test_process_challenge_other_error_code(self):
+ d = self._prepare_auth_challenge()
+
+ res = Response()
+ res.status_code = 300
+ self.auth_backend._session.put = mock.create_autospec(
+ self.auth_backend._session.put,
+ return_value=res)
+
+ def wrapper(salt_B):
+ with mock.patch('leap.util.request_helpers.get_content',
+ new=mock.create_autospec(get_content)) as \
+ content:
+ content.return_value = ("{}", 0)
+ with self.assertRaises(srpauth.SRPAuthBadStatusCode):
+ self.auth_backend._process_challenge(
+ salt_B,
+ username=self.TEST_USER)
+
+ d.addCallback(partial(threads.deferToThread, wrapper))
+
+ return d
+
+ @deferred()
+ def test_process_challenge(self):
+ d = self._prepare_auth_challenge()
+
+ def wrapper(salt_B):
+ self.auth_backend._process_challenge(salt_B,
+ username=self.TEST_USER)
+
+ d.addCallback(partial(threads.deferToThread, wrapper))
+
+ return d
+
+ def test_extract_data_wrong_data(self):
+ with self.assertRaises(srpauth.SRPAuthBadDataFromServer):
+ self.auth_backend._extract_data(None)
+
+ with self.assertRaises(srpauth.SRPAuthBadDataFromServer):
+ self.auth_backend._extract_data("")
+
+ def test_extract_data_fails_on_wrong_data_from_server(self):
+ with self.assertRaises(srpauth.SRPAuthBadDataFromServer):
+ self.auth_backend._extract_data({})
+
+ with self.assertRaises(srpauth.SRPAuthBadDataFromServer):
+ self.auth_backend._extract_data({"M2": ""})
+
+ def test_extract_data_sets_uidtoken(self):
+ test_uid = "someuid"
+ test_m2 = "somem2"
+ test_token = "sometoken"
+ test_data = {
+ "M2": test_m2,
+ "id": test_uid,
+ "token": test_token
+ }
+ m2 = self.auth_backend._extract_data(test_data)
+
+ self.assertEqual(m2, test_m2)
+ self.assertEqual(self.auth_backend.get_uid(), test_uid)
+ self.assertEqual(self.auth_backend.get_uid(),
+ self.auth.get_uid())
+ self.assertEqual(self.auth_backend.get_token(), test_token)
+ self.assertEqual(self.auth_backend.get_token(),
+ self.auth.get_token())
+
+ def _prepare_verify_session(self):
+ """
+ Prepares the tests for verify session with needed steps
+ before. It adds up to the extract_data step.
+
+ :returns: The defer to chain to
+ :rtype: defer.Deferred
+ """
+ d = self._prepare_auth_challenge()
+
+ def wrapper_proc_challenge(salt_B):
+ return self.auth_backend._process_challenge(
+ salt_B,
+ username=self.TEST_USER)
+
+ def wrapper_extract_data(data):
+ return self.auth_backend._extract_data(data)
+
+ d.addCallback(partial(threads.deferToThread, wrapper_proc_challenge))
+ d.addCallback(partial(threads.deferToThread, wrapper_extract_data))
+
+ return d
+
+ @deferred()
+ def test_verify_session_unhexlifiable_m2(self):
+ d = self._prepare_verify_session()
+
+ def wrapper(M2):
+ with self.assertRaises(srpauth.SRPAuthBadDataFromServer):
+ self.auth_backend._verify_session("za") # unhexlifiable value
+
+ d.addCallback(wrapper)
+
+ return d
+
+ @deferred()
+ def test_verify_session_unverifiable_m2(self):
+ d = self._prepare_verify_session()
+
+ def wrapper(M2):
+ with self.assertRaises(srpauth.SRPAuthVerificationFailed):
+ # Correctly unhelifiable value, but not for verifying the
+ # session
+ self.auth_backend._verify_session("abc12")
+
+ d.addCallback(wrapper)
+
+ return d
+
+ @deferred()
+ def test_verify_session_fails_on_no_session_id(self):
+ d = self._prepare_verify_session()
+
+ def wrapper(M2):
+ self.auth_backend._session.cookies.get = mock.create_autospec(
+ self.auth_backend._session.cookies.get,
+ return_value=None)
+ with self.assertRaises(srpauth.SRPAuthNoSessionId):
+ self.auth_backend._verify_session(M2)
+
+ d.addCallback(wrapper)
+
+ return d
+
+ @deferred()
+ def test_verify_session_session_id(self):
+ d = self._prepare_verify_session()
+
+ test_session_id = "12345"
+
+ def wrapper(M2):
+ self.auth_backend._session.cookies.get = mock.create_autospec(
+ self.auth_backend._session.cookies.get,
+ return_value=test_session_id)
+ self.auth_backend._verify_session(M2)
+ self.assertEqual(self.auth_backend.get_session_id(),
+ test_session_id)
+ self.assertEqual(self.auth_backend.get_session_id(),
+ self.auth.get_session_id())
+
+ d.addCallback(wrapper)
+
+ return d
+
+ @deferred()
+ def test_verify_session(self):
+ d = self._prepare_verify_session()
+
+ def wrapper(M2):
+ self.auth_backend._verify_session(M2)
+
+ d.addCallback(wrapper)
+
+ return d
+
+ @deferred()
+ def test_authenticate(self):
+ self.auth_backend._authentication_preprocessing = mock.create_autospec(
+ self.auth_backend._authentication_preprocessing,
+ return_value=None)
+ self.auth_backend._start_authentication = mock.create_autospec(
+ self.auth_backend._start_authentication,
+ return_value=None)
+ self.auth_backend._process_challenge = mock.create_autospec(
+ self.auth_backend._process_challenge,
+ return_value=None)
+ self.auth_backend._extract_data = mock.create_autospec(
+ self.auth_backend._extract_data,
+ return_value=None)
+ self.auth_backend._verify_session = mock.create_autospec(
+ self.auth_backend._verify_session,
+ return_value=None)
+
+ d = self.auth_backend.authenticate(self.TEST_USER, self.TEST_PASS)
+
+ def check(*args):
+ self.auth_backend._authentication_preprocessing.\
+ assert_called_once_with(
+ username=self.TEST_USER,
+ password=self.TEST_PASS
+ )
+ self.auth_backend._start_authentication.assert_called_once_with(
+ None,
+ username=self.TEST_USER)
+ self.auth_backend._process_challenge.assert_called_once_with(
+ None,
+ username=self.TEST_USER)
+ self.auth_backend._extract_data.assert_called_once_with(
+ None)
+ self.auth_backend._verify_session.assert_called_once_with(None)
+
+ d.addCallback(check)
+
+ return d
+
+ @deferred()
+ def test_logout_fails_if_not_logged_in(self):
+
+ def wrapper(*args):
+ with self.assertRaises(AssertionError):
+ self.auth_backend.logout()
+
+ d = threads.deferToThread(wrapper)
+ return d
+
+ @deferred()
+ def test_logout_traps_delete(self):
+ self.auth_backend.get_session_id = mock.create_autospec(
+ self.auth_backend.get_session_id,
+ return_value="1234")
+ self.auth_backend._session.delete = mock.create_autospec(
+ self.auth_backend._session.delete,
+ side_effect=Exception())
+
+ def wrapper(*args):
+ self.auth_backend.logout()
+
+ d = threads.deferToThread(wrapper)
+ return d
+
+ @deferred()
+ def test_logout_clears(self):
+ self.auth_backend._session_id = "1234"
+
+ def wrapper(*args):
+ old_session = self.auth_backend._session
+ self.auth_backend.logout()
+ self.assertIsNone(self.auth_backend.get_session_id())
+ self.assertIsNone(self.auth_backend.get_uid())
+ self.assertNotEqual(old_session, self.auth_backend._session)
+
+ d = threads.deferToThread(wrapper)
+ return d
+
+
+class SRPAuthSingletonTestCase(unittest.TestCase):
+ def setUp(self):
+ self.old_auth = srpauth.SRPAuth._SRPAuth__impl.authenticate
+
+ def tearDown(self):
+ srpauth.SRPAuth._SRPAuth__impl.authenticate = self.old_auth
+
+ def test_singleton(self):
+ obj1 = srpauth.SRPAuth(ProviderConfig())
+ obj2 = srpauth.SRPAuth(ProviderConfig())
+ self.assertEqual(obj1._SRPAuth__instance, obj2._SRPAuth__instance)
+
+ @deferred()
+ def test_authenticate_notifies_gui(self):
+ auth = srpauth.SRPAuth(ProviderConfig())
+ auth._SRPAuth__instance.authenticate = mock.create_autospec(
+ auth._SRPAuth__instance.authenticate,
+ return_value=threads.deferToThread(lambda: None))
+ auth._gui_notify = mock.create_autospec(
+ auth._gui_notify)
+
+ d = auth.authenticate("", "")
+
+ def check(*args):
+ auth._gui_notify.assert_called_once_with(None)
+
+ d.addCallback(check)
+ return d
+
+ @deferred()
+ def test_authenticate_errsback(self):
+ auth = srpauth.SRPAuth(ProviderConfig())
+ auth._SRPAuth__instance.authenticate = mock.create_autospec(
+ auth._SRPAuth__instance.authenticate,
+ return_value=threads.deferToThread(MagicMock(
+ side_effect=Exception())))
+ auth._gui_notify = mock.create_autospec(
+ auth._gui_notify)
+ auth._errback = mock.create_autospec(
+ auth._errback)
+
+ d = auth.authenticate("", "")
+
+ def check(*args):
+ self.assertFalse(auth._gui_notify.called)
+ self.assertEqual(auth._errback.call_count, 1)
+
+ d.addCallback(check)
+ return d
+
+ @deferred()
+ def test_authenticate_runs_cleanly_when_raises(self):
+ auth = srpauth.SRPAuth(ProviderConfig())
+ auth._SRPAuth__instance.authenticate = mock.create_autospec(
+ auth._SRPAuth__instance.authenticate,
+ return_value=threads.deferToThread(MagicMock(
+ side_effect=Exception())))
+
+ d = auth.authenticate("", "")
+
+ return d
+
+ @deferred()
+ def test_authenticate_runs_cleanly(self):
+ auth = srpauth.SRPAuth(ProviderConfig())
+ auth._SRPAuth__instance.authenticate = mock.create_autospec(
+ auth._SRPAuth__instance.authenticate,
+ return_value=threads.deferToThread(MagicMock()))
+
+ d = auth.authenticate("", "")
+
+ return d
+
+ def test_logout(self):
+ auth = srpauth.SRPAuth(ProviderConfig())
+ auth._SRPAuth__instance.logout = mock.create_autospec(
+ auth._SRPAuth__instance.logout)
+
+ self.assertTrue(auth.logout())
+
+ def test_logout_rets_false_when_raises(self):
+ auth = srpauth.SRPAuth(ProviderConfig())
+ auth._SRPAuth__instance.logout = mock.create_autospec(
+ auth._SRPAuth__instance.logout,
+ side_effect=Exception())
+
+ self.assertFalse(auth.logout())
diff --git a/src/leap/crypto/tests/test_srpregister.py b/src/leap/crypto/tests/test_srpregister.py
new file mode 100644
index 00000000..66b815f2
--- /dev/null
+++ b/src/leap/crypto/tests/test_srpregister.py
@@ -0,0 +1,201 @@
+# -*- coding: utf-8 -*-
+# test_srpregister.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Tests for:
+ * leap/crypto/srpregister.py
+"""
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+import os
+import sys
+
+from mock import MagicMock
+from nose.twistedtools import reactor, deferred
+from twisted.python import log
+from twisted.internet import threads
+
+from leap.common.testing.https_server import where
+from leap.config.providerconfig import ProviderConfig
+from leap.crypto import srpregister, srpauth
+from leap.crypto.tests import fake_provider
+
+log.startLogging(sys.stdout)
+
+
+def _get_capath():
+ return where("cacert.pem")
+
+_here = os.path.split(__file__)[0]
+
+
+class ImproperlyConfiguredError(Exception):
+ """
+ Raised if the test provider is missing configuration
+ """
+
+
+class SRPTestCase(unittest.TestCase):
+ """
+ Tests for the SRPRegister class
+ """
+ __name__ = "SRPRegister tests"
+
+ @classmethod
+ def setUpClass(cls):
+ """
+ Sets up this TestCase with a simple and faked provider instance:
+
+ * runs a threaded reactor
+ * loads a mocked ProviderConfig that points to the certs in the
+ leap.common.testing module.
+ """
+ factory = fake_provider.get_provider_factory()
+ http = reactor.listenTCP(8001, factory)
+ https = reactor.listenSSL(
+ 0, factory,
+ fake_provider.OpenSSLServerContextFactory())
+ get_port = lambda p: p.getHost().port
+ cls.http_port = get_port(http)
+ cls.https_port = get_port(https)
+
+ provider = ProviderConfig()
+ provider.get_ca_cert_path = MagicMock()
+ provider.get_ca_cert_path.return_value = _get_capath()
+
+ provider.get_api_uri = MagicMock()
+ provider.get_api_uri.return_value = cls._get_https_uri()
+
+ loaded = provider.load(path=os.path.join(
+ _here, "test_provider.json"))
+ if not loaded:
+ raise ImproperlyConfiguredError(
+ "Could not load test provider config")
+ cls.register = srpregister.SRPRegister(provider_config=provider)
+
+ cls.auth = srpauth.SRPAuth(provider)
+
+ # helper methods
+
+ @classmethod
+ def _get_https_uri(cls):
+ """
+ Returns a https uri with the right https port initialized
+ """
+ return "https://localhost:%s" % (cls.https_port,)
+
+ # Register tests
+
+ def test_none_port(self):
+ provider = ProviderConfig()
+ provider.get_api_uri = MagicMock()
+ provider.get_api_uri.return_value = "http://localhost/"
+ loaded = provider.load(path=os.path.join(
+ _here, "test_provider.json"))
+ if not loaded:
+ raise ImproperlyConfiguredError(
+ "Could not load test provider config")
+
+ register = srpregister.SRPRegister(provider_config=provider)
+ self.assertEquals(register._port, "443")
+
+ @deferred()
+ def test_wrong_cert(self):
+ provider = ProviderConfig()
+ loaded = provider.load(path=os.path.join(
+ _here, "test_provider.json"))
+ provider.get_ca_cert_path = MagicMock()
+ provider.get_ca_cert_path.return_value = os.path.join(
+ _here,
+ "wrongcert.pem")
+ provider.get_api_uri = MagicMock()
+ provider.get_api_uri.return_value = self._get_https_uri()
+ if not loaded:
+ raise ImproperlyConfiguredError(
+ "Could not load test provider config")
+
+ register = srpregister.SRPRegister(provider_config=provider)
+ d = threads.deferToThread(register.register_user, "foouser_firsttime",
+ "barpass")
+ d.addCallback(self.assertFalse)
+ return d
+
+ @deferred()
+ def test_register_user(self):
+ """
+ Checks if the registration of an unused name works as expected when
+ it is the first time that we attempt to register that user, as well as
+ when we request a user that is taken.
+ """
+ # pristine registration
+ d = threads.deferToThread(self.register.register_user,
+ "foouser_firsttime",
+ "barpass")
+ d.addCallback(self.assertTrue)
+ return d
+
+ @deferred()
+ def test_second_register_user(self):
+ # second registration attempt with the same user should return errors
+ d = threads.deferToThread(self.register.register_user,
+ "foouser_second",
+ "barpass")
+ d.addCallback(self.assertTrue)
+
+ # FIXME currently we are catching this in an upper layer,
+ # we could bring the error validation to the SRPRegister class
+ def register_wrapper(_):
+ return threads.deferToThread(self.register.register_user,
+ "foouser_second",
+ "barpass")
+ d.addCallback(register_wrapper)
+ d.addCallback(self.assertFalse)
+ return d
+
+ @deferred()
+ def test_correct_http_uri(self):
+ """
+ Checks that registration autocorrect http uris to https ones.
+ """
+ HTTP_URI = "http://localhost:%s" % (self.https_port, )
+ HTTPS_URI = "https://localhost:%s/1/users" % (self.https_port, )
+ provider = ProviderConfig()
+ provider.get_ca_cert_path = MagicMock()
+ provider.get_ca_cert_path.return_value = _get_capath()
+ provider.get_api_uri = MagicMock()
+
+ # we introduce a http uri in the config file...
+ provider.get_api_uri.return_value = HTTP_URI
+ loaded = provider.load(path=os.path.join(
+ _here, "test_provider.json"))
+ if not loaded:
+ raise ImproperlyConfiguredError(
+ "Could not load test provider config")
+
+ register = srpregister.SRPRegister(provider_config=provider)
+
+ # ... and we check that we're correctly taking the HTTPS protocol
+ # instead
+ reg_uri = register._get_registration_uri()
+ self.assertEquals(reg_uri, HTTPS_URI)
+ register._get_registration_uri = MagicMock(return_value=HTTPS_URI)
+ d = threads.deferToThread(register.register_user, "test_failhttp",
+ "barpass")
+ d.addCallback(self.assertTrue)
+
+ return d
diff --git a/src/leap/crypto/tests/wrongcert.pem b/src/leap/crypto/tests/wrongcert.pem
new file mode 100644
index 00000000..e6cff38a
--- /dev/null
+++ b/src/leap/crypto/tests/wrongcert.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFtTCCA52gAwIBAgIJAIWZus5EIXNtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTMwNjI1MTc0NjExWhcNMTgwNjI1MTc0NjExWjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEA2ObM7ESjyuxFZYD/Y68qOPQgjgggW+cdXfBpU2p4n7clsrUeMhWdW40Y
+77Phzor9VOeqs3ZpHuyLzsYVp/kFDm8tKyo2ah5fJwzL0VCSLYaZkUQQ7GNUmTCk
+furaxl8cQx/fg395V7/EngsS9B3/y5iHbctbA4MnH3jaotO5EGeo6hw7/eyCotQ9
+KbBV9GJMcY94FsXBCmUB+XypKklWTLhSaS6Cu4Fo8YLW6WmcnsyEOGS2F7WVf5at
+7CBWFQZHaSgIBLmc818/mDYCnYmCVMFn/6Ndx7V2NTlz+HctWrQn0dmIOnCUeCwS
+wXq9PnBR1rSx/WxwyF/WpyjOFkcIo7vm72kS70pfrYsXcZD4BQqkXYj3FyKnPt3O
+ibLKtCxL8/83wOtErPcYpG6LgFkgAAlHQ9MkUi5dbmjCJtpqQmlZeK1RALdDPiB3
+K1KZimrGsmcE624dJxUIOJJpuwJDy21F8kh5ZAsAtE1prWETrQYNElNFjQxM83rS
+ZR1Ql2MPSB4usEZT57+KvpEzlOnAT3elgCg21XrjSFGi14hCEao4g2OEZH5GAwm5
+frf6UlSRZ/g3tLTfI8Hv1prw15W2qO+7q7SBAplTODCRk+Yb0YoA2mMM/QXBUcXs
+vKEDLSSxzNIBi3T62l39RB/ml+gPKo87ZMDivex1ZhrcJc3Yu3sCAwEAAaOBpzCB
+pDAdBgNVHQ4EFgQUPjE+4pun+8FreIdpoR8v6N7xKtUwdQYDVR0jBG4wbIAUPjE+
+4pun+8FreIdpoR8v6N7xKtWhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT
+b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCF
+mbrORCFzbTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCpvCPdtvXJ
+muTj379TZuCJs7/l0FhA7AHa1WAlHjsXHaA7N0+3ZWAbdtXDsowal6S+ldgU/kfV
+Lq7NrRq+amJWC7SYj6cvVwhrSwSvu01fe/TWuOzHrRv1uTfJ/VXLonVufMDd9opo
+bhqYxMaxLdIx6t/MYmZH4Wpiq0yfZuv//M8i7BBl/qvaWbLhg0yVAKRwjFvf59h6
+6tRFCLddELOIhLDQtk8zMbioPEbfAlKdwwP8kYGtDGj6/9/YTd/oTKRdgHuwyup3
+m0L20Y6LddC+tb0WpK5EyrNbCbEqj1L4/U7r6f/FKNA3bx6nfdXbscaMfYonKAKg
+1cRrRg45sErmCz0QyTnWzXyvbjR4oQRzyW3kJ1JZudZ+AwOi00J5FYa3NiLuxl1u
+gIGKWSrASQWhEdpa1nlCgX7PhdaQgYjEMpQvA0GCA0OF5JDu8en1yZqsOt1hCLIN
+lkz/5jKPqrclY5hV99bE3hgCHRmIPNHCZG3wbZv2yJKxJX1YLMmQwAmSh2N7YwGG
+yXRvCxQs5ChPHyRairuf/5MZCZnSVb45ppTVuNUijsbflKRUgfj/XvfqQ22f+C9N
+Om2dmNvAiS2TOIfuP47CF2OUa5q4plUwmr+nyXQGM0SIoHNCj+MBdFfb3oxxAtI+
+SLhbnzQv5e84Doqz3YF0XW8jyR7q8GFLNA==
+-----END CERTIFICATE-----
diff --git a/src/leap/eip/checks.py b/src/leap/eip/checks.py
deleted file mode 100644
index 52230db2..00000000
--- a/src/leap/eip/checks.py
+++ /dev/null
@@ -1,542 +0,0 @@
-import logging
-import time
-import os
-import sys
-
-import requests
-
-from leap import __branding as BRANDING
-from leap import certs as leapcerts
-from leap.base.auth import srpauth_protected, magick_srpauth
-from leap.base import config as baseconfig
-from leap.base import constants as baseconstants
-from leap.base import providers
-from leap.crypto import certs
-from leap.eip import config as eipconfig
-from leap.eip import constants as eipconstants
-from leap.eip import exceptions as eipexceptions
-from leap.eip import specs as eipspecs
-from leap.util.certs import get_mac_cabundle
-from leap.util.fileutil import mkdir_p
-from leap.util.web import get_https_domain_and_port
-
-logger = logging.getLogger(name=__name__)
-
-"""
-ProviderCertChecker
--------------------
-Checks on certificates. To be moved to base.
-docs TBD
-
-EIPConfigChecker
-----------
-It is used from the eip conductor (a instance of EIPConnection that is
-managed from the QtApp), running `run_all` method before trying to call
-`connect` or any other of the state-changing methods.
-
-It checks that the needed files are provided or can be discovered over the
-net. Much of these tests are not specific to EIP module, and can be splitted
-into base.tests to be invoked by the base leap init routines.
-However, I'm testing them alltogether for the sake of having the whole unit
-reachable and testable as a whole.
-
-"""
-
-
-def get_branding_ca_cert(domain):
- # deprecated
- ca_file = BRANDING.get('provider_ca_file')
- if ca_file:
- return leapcerts.where(ca_file)
-
-
-class ProviderCertChecker(object):
- """
- Several checks needed for getting
- client certs and checking tls connection
- with provider.
- """
- def __init__(self, fetcher=requests,
- domain=None,
- apidomain=None):
-
- self.fetcher = fetcher
- self.domain = domain
- #XXX needs some kind of autoinit
- #right now we set by hand
- #by loading and reading provider config
- self.apidomain = apidomain
- self.cacert = eipspecs.provider_ca_path(domain)
-
- def run_all(
- self, checker=None,
- skip_download=False, skip_verify=False):
-
- if not checker:
- checker = self
-
- do_verify = not skip_verify
- logger.debug('do_verify: %s', do_verify)
- # checker.download_ca_cert()
-
- # For MVS+
- # checker.download_ca_signature()
- # checker.get_ca_signatures()
- # checker.is_there_trust_path()
-
- # For MVS
- checker.is_there_provider_ca()
-
- checker.is_https_working(verify=do_verify, autocacert=False)
- checker.check_new_cert_needed(verify=do_verify)
-
- def download_ca_cert(self, uri=None, verify=True):
- req = self.fetcher.get(uri, verify=verify)
- req.raise_for_status()
-
- # should check domain exists
- capath = self._get_ca_cert_path(self.domain)
- with open(capath, 'w') as f:
- f.write(req.content)
-
- def check_ca_cert_fingerprint(
- self, hash_type="SHA256",
- fingerprint=None):
- """
- compares the fingerprint in
- the ca cert with a string
- we are passed
- returns True if they are equal, False if not.
- @param hash_type: digest function
- @type hash_type: str
- @param fingerprint: the fingerprint to compare with.
- @type fingerprint: str (with : separator)
- @rtype bool
- """
- ca_cert_path = self.ca_cert_path
- ca_cert_fpr = certs.get_cert_fingerprint(
- filepath=ca_cert_path)
- return ca_cert_fpr == fingerprint
-
- def verify_api_https(self, uri):
- assert uri.startswith('https://')
- cacert = self.ca_cert_path
- verify = cacert or True
-
- # DEBUG
- logger.debug('uri -> %s' % uri)
- logger.debug('cacertpath -> %s' % cacert)
-
- req = self.fetcher.get(uri, verify=verify)
- req.raise_for_status()
- return True
-
- def download_ca_signature(self):
- # MVS+
- raise NotImplementedError
-
- def get_ca_signatures(self):
- # MVS+
- raise NotImplementedError
-
- def is_there_trust_path(self):
- # MVS+
- raise NotImplementedError
-
- def is_there_provider_ca(self):
- if not self.cacert:
- return False
- cacert_exists = os.path.isfile(self.cacert)
- if cacert_exists:
- logger.debug('True')
- return True
- logger.debug('False!')
- return False
-
- def is_https_working(
- self, uri=None, verify=True,
- autocacert=False):
- if uri is None:
- uri = self._get_root_uri()
- # XXX raise InsecureURI or something better
- try:
- assert uri.startswith('https')
- except AssertionError:
- raise AssertionError(
- "uri passed should start with https")
- if autocacert and verify is True and self.cacert is not None:
- logger.debug('verify cert: %s', self.cacert)
- verify = self.cacert
- if sys.platform == "darwin":
- verify = get_mac_cabundle()
- logger.debug('checking https connection')
- logger.debug('uri: %s (verify:%s)', uri, verify)
-
- try:
- self.fetcher.get(uri, verify=verify)
-
- except requests.exceptions.SSLError as exc:
- raise eipexceptions.HttpsBadCertError
-
- except requests.exceptions.ConnectionError:
- logger.error('ConnectionError')
- raise eipexceptions.HttpsNotSupported
-
- else:
- return True
-
- def check_new_cert_needed(self, skip_download=False, verify=True):
- # XXX add autocacert
- if not self.is_cert_valid(do_raise=False):
- logger.debug('cert needed: true')
- self.download_new_client_cert(
- skip_download=skip_download,
- verify=verify)
- return True
- logger.debug('cert needed: false')
- return False
-
- def download_new_client_cert(self, uri=None, verify=True,
- skip_download=False,
- credentials=None):
- logger.debug('download new client cert')
- if skip_download:
- return True
- if uri is None:
- uri = self._get_client_cert_uri()
- # XXX raise InsecureURI or something better
- #assert uri.startswith('https')
-
- if verify is True and self.cacert is not None:
- verify = self.cacert
- logger.debug('verify = %s', verify)
-
- fgetfn = self.fetcher.get
-
- if credentials:
- user, passwd = credentials
- logger.debug('apidomain = %s', self.apidomain)
-
- @srpauth_protected(user, passwd,
- server="https://%s" % self.apidomain,
- verify=verify)
- def getfn(*args, **kwargs):
- return fgetfn(*args, **kwargs)
-
- else:
- # XXX FIXME fix decorated args
- @magick_srpauth(verify)
- def getfn(*args, **kwargs):
- return fgetfn(*args, **kwargs)
- try:
-
- req = getfn(uri, verify=verify)
- req.raise_for_status()
-
- except requests.exceptions.SSLError:
- logger.warning('SSLError while fetching cert. '
- 'Look below for stack trace.')
- raise eipexceptions.HttpsBadCertError("SSLError")
- except Exception as exc:
- raise eipexceptions.EIPClientError(exc.message)
-
- try:
- logger.debug('validating cert...')
- pemfile_content = req.content
- valid = self.is_valid_pemfile(pemfile_content)
- if not valid:
- logger.warning('invalid cert')
- return False
- cert_path = self._get_client_cert_path()
- self.write_cert(pemfile_content, to=cert_path)
- except:
- logger.warning('Error while validating cert')
- raise
- return True
-
- def is_cert_valid(self, cert_path=None, do_raise=True):
- exists = lambda: self.is_certificate_exists()
- valid_pemfile = lambda: self.is_valid_pemfile()
- not_expired = lambda: self.is_cert_not_expired()
-
- valid = exists() and valid_pemfile() and not_expired()
- if not valid:
- if do_raise:
- raise Exception('missing valid cert')
- else:
- return False
- return True
-
- def is_certificate_exists(self, certfile=None):
- if certfile is None:
- certfile = self._get_client_cert_path()
- return os.path.isfile(certfile)
-
- def is_cert_not_expired(self, certfile=None, now=time.gmtime):
- if certfile is None:
- certfile = self._get_client_cert_path()
- from_, to_ = certs.get_time_boundaries(certfile)
-
- return from_ < now() < to_
-
- def is_valid_pemfile(self, cert_s=None):
- """
- checks that the passed string
- is a valid pem certificate
- @param cert_s: string containing pem content
- @type cert_s: string
- @rtype: bool
- """
- if cert_s is None:
- certfile = self._get_client_cert_path()
- with open(certfile) as cf:
- cert_s = cf.read()
- try:
- valid = certs.can_load_cert_and_pkey(cert_s)
- except certs.BadCertError:
- logger.warning("Not valid pemfile")
- valid = False
- return valid
-
- @property
- def ca_cert_path(self):
- return self._get_ca_cert_path(self.domain)
-
- def _get_root_uri(self):
- return u"https://%s/" % self.domain
-
- def _get_client_cert_uri(self):
- return "https://%s/1/cert" % self.apidomain
-
- def _get_client_cert_path(self):
- return eipspecs.client_cert_path(domain=self.domain)
-
- def _get_ca_cert_path(self, domain):
- # XXX this folder path will be broken for win
- # and this should be moved to eipspecs.ca_path
-
- # XXX use baseconfig.get_provider_path(folder=Foo)
- # !!!
-
- capath = baseconfig.get_config_file(
- 'cacert.pem',
- folder='providers/%s/keys/ca' % domain)
- folder, fname = os.path.split(capath)
- if not os.path.isdir(folder):
- mkdir_p(folder)
- return capath
-
- def write_cert(self, pemfile_content, to=None):
- folder, filename = os.path.split(to)
- if not os.path.isdir(folder):
- mkdir_p(folder)
- with open(to, 'w') as cert_f:
- cert_f.write(pemfile_content)
-
- def set_api_domain(self, domain):
- self.apidomain = domain
-
-
-class EIPConfigChecker(object):
- """
- Several checks needed
- to ensure a EIPConnection
- can be sucessfully established.
- use run_all to run all checks.
- """
-
- def __init__(self, fetcher=requests, domain=None):
- # we do not want to accept too many
- # argument on init.
- # we want tests
- # to be explicitely run.
-
- self.fetcher = fetcher
-
- # if not domain, get from config
- self.domain = domain
- self.apidomain = None
- self.cacert = eipspecs.provider_ca_path(domain)
-
- self.defaultprovider = providers.LeapProviderDefinition(domain=domain)
- self.defaultprovider.load()
- self.eipconfig = eipconfig.EIPConfig(domain=domain)
- self.set_api_domain()
- self.eipserviceconfig = eipconfig.EIPServiceConfig(domain=domain)
- self.eipserviceconfig.load()
-
- def run_all(self, checker=None, skip_download=False):
- """
- runs all checks in a row.
- will raise if some error encountered.
- catching those exceptions is not
- our responsibility at this moment
- """
- if not checker:
- checker = self
-
- # let's call all tests
- # needed for a sane eip session.
-
- # TODO: get rid of check_default.
- # check_complete should
- # be enough. but here to make early tests easier.
- checker.check_default_eipconfig()
-
- checker.check_is_there_default_provider()
- checker.fetch_definition(skip_download=skip_download)
- checker.fetch_eip_service_config(skip_download=skip_download)
- checker.check_complete_eip_config()
- #checker.ping_gateway()
-
- # public checks
-
- def check_default_eipconfig(self):
- """
- checks if default eipconfig exists,
- and dumps a default file if not
- """
- # XXX ONLY a transient check
- # because some old function still checks
- # for eip config at the beginning.
-
- # it *really* does not make sense to
- # dump it right now, we can get an in-memory
- # config object and dump it to disk in a
- # later moment
- logger.debug('checking default eip config')
- if not self._is_there_default_eipconfig():
- self._dump_default_eipconfig()
-
- def check_is_there_default_provider(self, config=None):
- """
- raises EIPMissingDefaultProvider if no
- default provider found on eip config.
- This is catched by ui and runs FirstRunWizard (MVS+)
- """
- if config is None:
- config = self.eipconfig.config
- logger.debug('checking default provider')
- provider = config.get('provider', None)
- if provider is None:
- raise eipexceptions.EIPMissingDefaultProvider
- # XXX raise also if malformed ProviderDefinition?
- return True
-
- def fetch_definition(self, skip_download=False,
- force_download=False,
- config=None, uri=None,
- domain=None):
- """
- fetches a definition file from server
- """
- # TODO:
- # - Implement diff
- # - overwrite only if different.
- # (attend to serial field different, for instance)
-
- logger.debug('fetching definition')
-
- if skip_download:
- logger.debug('(fetching def skipped)')
- return True
- if config is None:
- config = self.defaultprovider.config
- if uri is None:
- if not domain:
- domain = config.get('provider', None)
- uri = self._get_provider_definition_uri(domain=domain)
-
- if sys.platform == "darwin":
- verify = get_mac_cabundle()
- else:
- verify = True
-
- self.defaultprovider.load(
- from_uri=uri,
- fetcher=self.fetcher,
- verify=verify)
- self.defaultprovider.save()
-
- def fetch_eip_service_config(self, skip_download=False,
- force_download=False,
- config=None, uri=None, # domain=None,
- autocacert=True, verify=True):
- if skip_download:
- return True
- if config is None:
- self.eipserviceconfig.load()
- config = self.eipserviceconfig.config
- if uri is None:
- #XXX
- #if not domain:
- #domain = self.domain or config.get('provider', None)
- uri = self._get_eip_service_uri(
- domain=self.apidomain)
-
- if autocacert and self.cacert is not None:
- verify = self.cacert
-
- self.eipserviceconfig.load(
- from_uri=uri,
- fetcher=self.fetcher,
- force_download=force_download,
- verify=verify)
- self.eipserviceconfig.save()
-
- def check_complete_eip_config(self, config=None):
- # TODO check for gateway
- if config is None:
- config = self.eipconfig.config
- try:
- assert 'provider' in config
- assert config['provider'] is not None
- # XXX assert there is gateway !!
- except AssertionError:
- raise eipexceptions.EIPConfigurationError
-
- # XXX TODO:
- # We should WRITE eip config if missing or
- # incomplete at this point
- #self.eipconfig.save()
-
- #
- # private helpers
- #
-
- def _is_there_default_eipconfig(self):
- return self.eipconfig.exists()
-
- def _dump_default_eipconfig(self):
- self.eipconfig.save(force=True)
-
- def _get_provider_definition_uri(self, domain=None, path=None):
- if domain is None:
- domain = self.domain or baseconstants.DEFAULT_PROVIDER
- if path is None:
- path = baseconstants.DEFINITION_EXPECTED_PATH
- uri = u"https://%s/%s" % (domain, path)
- logger.debug('getting provider definition from %s' % uri)
- return uri
-
- def _get_eip_service_uri(self, domain=None, path=None):
- if domain is None:
- domain = self.domain or baseconstants.DEFAULT_PROVIDER
- if path is None:
- path = eipconstants.EIP_SERVICE_EXPECTED_PATH
- uri = "https://%s/%s" % (domain, path)
- logger.debug('getting eip service file from %s', uri)
- return uri
-
- def set_api_domain(self):
- """sets api domain from defaultprovider config object"""
- api = self.defaultprovider.config.get('api_uri', None)
- # the caller is responsible for having loaded the config
- # object at this point
- if api:
- api_dom = get_https_domain_and_port(api)
- self.apidomain = "%s:%s" % api_dom
-
- def get_api_domain(self):
- """gets api domain"""
- return self.apidomain
diff --git a/src/leap/eip/config.py b/src/leap/eip/config.py
deleted file mode 100644
index 917871da..00000000
--- a/src/leap/eip/config.py
+++ /dev/null
@@ -1,398 +0,0 @@
-import logging
-import os
-import platform
-import re
-import tempfile
-
-from leap import __branding as BRANDING
-from leap import certs
-from leap.util.misc import null_check
-from leap.util.fileutil import (which, mkdir_p, check_and_fix_urw_only)
-
-from leap.base import config as baseconfig
-from leap.baseapp.permcheck import (is_pkexec_in_system,
- is_auth_agent_running)
-from leap.eip import exceptions as eip_exceptions
-from leap.eip import specs as eipspecs
-
-logger = logging.getLogger(name=__name__)
-provider_ca_file = BRANDING.get('provider_ca_file', None)
-
-_platform = platform.system()
-
-
-class EIPConfig(baseconfig.JSONLeapConfig):
- spec = eipspecs.eipconfig_spec
-
- def _get_slug(self):
- eipjsonpath = baseconfig.get_config_file(
- 'eip.json')
- return eipjsonpath
-
- def _set_slug(self, *args, **kwargs):
- raise AttributeError("you cannot set slug")
-
- slug = property(_get_slug, _set_slug)
-
-
-class EIPServiceConfig(baseconfig.JSONLeapConfig):
- spec = eipspecs.eipservice_config_spec
-
- def _get_slug(self):
- domain = getattr(self, 'domain', None)
- if domain:
- path = baseconfig.get_provider_path(domain)
- else:
- path = baseconfig.get_default_provider_path()
- return baseconfig.get_config_file(
- 'eip-service.json', folder=path)
-
- def _set_slug(self):
- raise AttributeError("you cannot set slug")
-
- slug = property(_get_slug, _set_slug)
-
-
-def get_socket_path():
- socket_path = os.path.join(
- tempfile.mkdtemp(prefix="leap-tmp"),
- 'openvpn.socket')
- #logger.debug('socket path: %s', socket_path)
- return socket_path
-
-
-def get_eip_gateway(eipconfig=None, eipserviceconfig=None):
- """
- return the first host in eip service config
- that matches the name defined in the eip.json config
- file.
- """
- # XXX eventually we should move to a more clever
- # gateway selection. maybe we could return
- # all gateways that match our cluster.
-
- null_check(eipconfig, "eipconfig")
- null_check(eipserviceconfig, "eipserviceconfig")
- PLACEHOLDER = "testprovider.example.org"
-
- conf = eipconfig.config
- eipsconf = eipserviceconfig.config
-
- primary_gateway = conf.get('primary_gateway', None)
- if not primary_gateway:
- return PLACEHOLDER
-
- gateways = eipsconf.get('gateways', None)
- if not gateways:
- logger.error('missing gateways in eip service config')
- return PLACEHOLDER
-
- if len(gateways) > 0:
- for gw in gateways:
- clustername = gw.get('cluster', None)
- if not clustername:
- logger.error('no cluster name')
- return
-
- if clustername == primary_gateway:
- # XXX at some moment, we must
- # make this a more generic function,
- # and return ports, protocols...
- ipaddress = gw.get('ip_address', None)
- if not ipaddress:
- logger.error('no ip_address')
- return
- return ipaddress
- logger.error('could not find primary gateway in provider'
- 'gateway list')
-
-
-def get_cipher_options(eipserviceconfig=None):
- """
- gathers optional cipher options from eip-service config.
- :param eipserviceconfig: EIPServiceConfig instance
- """
- null_check(eipserviceconfig, 'eipserviceconfig')
- eipsconf = eipserviceconfig.get_config()
-
- ALLOWED_KEYS = ("auth", "cipher", "tls-cipher")
- CIPHERS_REGEX = re.compile("[A-Z0-9\-]+")
- opts = []
- if 'openvpn_configuration' in eipsconf:
- config = eipserviceconfig.config.get(
- "openvpn_configuration", {})
- for key, value in config.items():
- if key in ALLOWED_KEYS and value is not None:
- sanitized_val = CIPHERS_REGEX.findall(value)
- if len(sanitized_val) != 0:
- _val = sanitized_val[0]
- opts.append('--%s' % key)
- opts.append('%s' % _val)
- return opts
-
-LINUX_UP_DOWN_SCRIPT = "/etc/leap/resolv-update"
-OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so"
-
-
-def has_updown_scripts():
- """
- checks the existence of the up/down scripts
- """
- # XXX should check permissions too
- is_file = os.path.isfile(LINUX_UP_DOWN_SCRIPT)
- if not is_file:
- logger.warning(
- "Could not find up/down scripts at %s! "
- "Risk of DNS Leaks!!!")
- return is_file
-
-
-def build_ovpn_options(daemon=False, socket_path=None, **kwargs):
- """
- build a list of options
- to be passed in the
- openvpn invocation
- @rtype: list
- @rparam: options
- """
- # XXX review which of the
- # options we don't need.
-
- # TODO pass also the config file,
- # since we will need to take some
- # things from there if present.
-
- provider = kwargs.pop('provider', None)
- eipconfig = EIPConfig(domain=provider)
- eipconfig.load()
- eipserviceconfig = EIPServiceConfig(domain=provider)
- eipserviceconfig.load()
-
- # get user/group name
- # also from config.
- user = baseconfig.get_username()
- group = baseconfig.get_groupname()
-
- opts = []
-
- opts.append('--client')
-
- opts.append('--dev')
- # XXX same in win?
- opts.append('tun')
- opts.append('--persist-tun')
- opts.append('--persist-key')
-
- verbosity = kwargs.get('ovpn_verbosity', None)
- if verbosity and 1 <= verbosity <= 6:
- opts.append('--verb')
- opts.append("%s" % verbosity)
-
- # remote ##############################
- # (server, port, protocol)
-
- opts.append('--remote')
-
- gw = get_eip_gateway(eipconfig=eipconfig,
- eipserviceconfig=eipserviceconfig)
- logger.debug('setting eip gateway to %s', gw)
- opts.append(str(gw))
-
- # get port/protocol from eipservice too
- opts.append('1194')
- #opts.append('80')
- opts.append('udp')
-
- opts.append('--tls-client')
- opts.append('--remote-cert-tls')
- opts.append('server')
-
- # get ciphers #######################
-
- ciphers = get_cipher_options(
- eipserviceconfig=eipserviceconfig)
- for cipheropt in ciphers:
- opts.append(str(cipheropt))
-
- # set user and group
- opts.append('--user')
- opts.append('%s' % user)
- opts.append('--group')
- opts.append('%s' % group)
-
- opts.append('--management-client-user')
- opts.append('%s' % user)
- opts.append('--management-signal')
-
- # set default options for management
- # interface. unix sockets or telnet interface for win.
- # XXX take them from the config object.
-
- if _platform == "Windows":
- opts.append('--management')
- opts.append('localhost')
- # XXX which is a good choice?
- opts.append('7777')
-
- if _platform in ("Linux", "Darwin"):
- opts.append('--management')
-
- if socket_path is None:
- socket_path = get_socket_path()
- opts.append(socket_path)
- opts.append('unix')
-
- opts.append('--script-security')
- opts.append('2')
-
- if _platform == "Linux":
- if has_updown_scripts():
- opts.append("--up")
- opts.append(LINUX_UP_DOWN_SCRIPT)
- opts.append("--down")
- opts.append(LINUX_UP_DOWN_SCRIPT)
- opts.append("--plugin")
- opts.append(OPENVPN_DOWN_ROOT)
- opts.append("'script_type=down %s'" % LINUX_UP_DOWN_SCRIPT)
-
- # certs
- client_cert_path = eipspecs.client_cert_path(provider)
- ca_cert_path = eipspecs.provider_ca_path(provider)
-
- # XXX FIX paths for MAC
- opts.append('--cert')
- opts.append(client_cert_path)
- opts.append('--key')
- opts.append(client_cert_path)
- opts.append('--ca')
- opts.append(ca_cert_path)
-
- # we cannot run in daemon mode
- # with the current subp setting.
- # see: https://leap.se/code/issues/383
- #if daemon is True:
- #opts.append('--daemon')
-
- logger.debug('vpn options: %s', ' '.join(opts))
- return opts
-
-
-def build_ovpn_command(debug=False, do_pkexec_check=True, vpnbin=None,
- socket_path=None, **kwargs):
- """
- build a string with the
- complete openvpn invocation
-
- @rtype [string, [list of strings]]
- @rparam: a list containing the command string
- and a list of options.
- """
- command = []
- use_pkexec = True
- ovpn = None
-
- # XXX get use_pkexec from config instead.
-
- if _platform == "Linux" and use_pkexec and do_pkexec_check:
-
- # check for both pkexec
- # AND a suitable authentication
- # agent running.
- logger.info('use_pkexec set to True')
-
- if not is_pkexec_in_system():
- logger.error('no pkexec in system')
- raise eip_exceptions.EIPNoPkexecAvailable
-
- if not is_auth_agent_running():
- logger.warning(
- "no polkit auth agent found. "
- "pkexec will use its own text "
- "based authentication agent. "
- "that's probably a bad idea")
- raise eip_exceptions.EIPNoPolkitAuthAgentAvailable
-
- command.append('pkexec')
-
- if vpnbin is None:
- if _platform == "Darwin":
- # XXX Should hardcode our installed path
- # /Applications/LEAPClient.app/Contents/Resources/openvpn.leap
- openvpn_bin = "openvpn.leap"
- else:
- openvpn_bin = "openvpn"
- #XXX hardcode for darwin
- ovpn = which(openvpn_bin)
- else:
- ovpn = vpnbin
- if ovpn:
- vpn_command = ovpn
- else:
- vpn_command = "openvpn"
- command.append(vpn_command)
- daemon_mode = not debug
-
- for opt in build_ovpn_options(daemon=daemon_mode, socket_path=socket_path,
- **kwargs):
- command.append(opt)
-
- # XXX check len and raise proper error
-
- if _platform == "Darwin":
- OSX_ASADMIN = 'do shell script "%s" with administrator privileges'
- # XXX fix workaround for Nones
- _command = [x if x else " " for x in command]
- # XXX debugging!
- # XXX get openvpn log path from debug flags
- _command.append('--log')
- _command.append('/tmp/leap_openvpn.log')
- return ["osascript", ["-e", OSX_ASADMIN % ' '.join(_command)]]
- else:
- return [command[0], command[1:]]
-
-
-def check_vpn_keys(provider=None):
- """
- performs an existance and permission check
- over the openvpn keys file.
- Currently we're expecting a single file
- per provider, containing the CA cert,
- the provider key, and our client certificate
- """
- assert provider is not None
- provider_ca = eipspecs.provider_ca_path(provider)
- client_cert = eipspecs.client_cert_path(provider)
-
- logger.debug('provider ca = %s', provider_ca)
- logger.debug('client cert = %s', client_cert)
-
- # if no keys, raise error.
- # it's catched by the ui and signal user.
-
- if not os.path.isfile(provider_ca):
- # not there. let's try to copy.
- folder, filename = os.path.split(provider_ca)
- if not os.path.isdir(folder):
- mkdir_p(folder)
- if provider_ca_file:
- cacert = certs.where(provider_ca_file)
- with open(provider_ca, 'w') as pca:
- with open(cacert, 'r') as cac:
- pca.write(cac.read())
-
- if not os.path.isfile(provider_ca):
- logger.error('key file %s not found. aborting.',
- provider_ca)
- raise eip_exceptions.EIPInitNoKeyFileError
-
- if not os.path.isfile(client_cert):
- logger.error('key file %s not found. aborting.',
- client_cert)
- raise eip_exceptions.EIPInitNoKeyFileError
-
- for keyfile in (provider_ca, client_cert):
- # bad perms? try to fix them
- try:
- check_and_fix_urw_only(keyfile)
- except OSError:
- raise eip_exceptions.EIPInitBadKeyFilePermError
diff --git a/src/leap/eip/constants.py b/src/leap/eip/constants.py
deleted file mode 100644
index 9af5a947..00000000
--- a/src/leap/eip/constants.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# not used anymore with the new JSONConfig.slug
-EIP_CONFIG = "eip.json"
-EIP_SERVICE_EXPECTED_PATH = "1/config/eip-service.json"
diff --git a/src/leap/eip/eipconnection.py b/src/leap/eip/eipconnection.py
deleted file mode 100644
index d012c567..00000000
--- a/src/leap/eip/eipconnection.py
+++ /dev/null
@@ -1,405 +0,0 @@
-"""
-EIP Connection Class
-"""
-from __future__ import (absolute_import,)
-import logging
-import Queue
-import sys
-import time
-
-from dateutil.parser import parse as dateparse
-
-from leap.eip.checks import ProviderCertChecker
-from leap.eip.checks import EIPConfigChecker
-from leap.eip import config as eipconfig
-from leap.eip import exceptions as eip_exceptions
-from leap.eip.openvpnconnection import OpenVPNConnection
-
-logger = logging.getLogger(name=__name__)
-
-
-class StatusMixIn(object):
-
- # a bunch of methods related with querying the connection
- # state/status and displaying useful info.
- # Needs to get clear on what is what, and
- # separate functions.
- # Should separate EIPConnectionStatus (self.status)
- # from the OpenVPN state/status command and parsing.
-
- ERR_CONNREFUSED = False
-
- def connection_state(self):
- """
- returns the current connection state
- """
- return self.status.current
-
- def get_icon_name(self):
- """
- get icon name from status object
- """
- return self.status.get_state_icon()
-
- def get_leap_status(self):
- return self.status.get_leap_status()
-
- def poll_connection_state(self):
- """
- """
- try:
- state = self.get_connection_state()
- except eip_exceptions.ConnectionRefusedError:
- # connection refused. might be not ready yet.
- if not self.ERR_CONNREFUSED:
- logger.warning('connection refused')
- self.ERR_CONNREFUSED = True
- return
- if not state:
- #logger.debug('no state')
- return
- (ts, status_step,
- ok, ip, remote) = state
- self.status.set_vpn_state(status_step)
- status_step = self.status.get_readable_status()
- return (ts, status_step, ok, ip, remote)
-
- def make_error(self):
- """
- capture error and wrap it in an
- understandable format
- """
- # mostly a hack to display errors in the debug UI
- # w/o breaking the polling.
- #XXX get helpful error codes
- self.with_errors = True
- now = int(time.time())
- return '%s,LAUNCHER ERROR,ERROR,-,-' % now
-
- def state(self):
- """
- Sends OpenVPN command: state
- """
- state = self._send_command("state")
- if not state:
- return None
- if isinstance(state, str):
- return state
- if isinstance(state, list):
- if len(state) == 1:
- return state[0]
- else:
- return state[-1]
-
- def vpn_status(self):
- """
- OpenVPN command: status
- """
- status = self._send_command("status")
- return status
-
- def vpn_status2(self):
- """
- OpenVPN command: last 2 statuses
- """
- return self._send_command("status 2")
-
- #
- # parse info as the UI expects
- #
-
- def get_status_io(self):
- status = self.vpn_status()
- if isinstance(status, str):
- lines = status.split('\n')
- if isinstance(status, list):
- lines = status
- try:
- (header, when, tun_read, tun_write,
- tcp_read, tcp_write, auth_read) = tuple(lines)
- except ValueError:
- return None
-
- when_ts = dateparse(when.split(',')[1]).timetuple()
- sep = ','
- # XXX clean up this!
- tun_read = tun_read.split(sep)[1]
- tun_write = tun_write.split(sep)[1]
- tcp_read = tcp_read.split(sep)[1]
- tcp_write = tcp_write.split(sep)[1]
- auth_read = auth_read.split(sep)[1]
-
- # XXX this could be a named tuple. prettier.
- return when_ts, (tun_read, tun_write, tcp_read, tcp_write, auth_read)
-
- def get_connection_state(self):
- state = self.state()
- if state is not None:
- ts, status_step, ok, ip, remote = state.split(',')
- ts = time.gmtime(float(ts))
- # XXX this could be a named tuple. prettier.
- return ts, status_step, ok, ip, remote
-
-
-class EIPConnection(OpenVPNConnection, StatusMixIn):
- """
- Aka conductor.
- Manages the execution of the OpenVPN process, auto starts, monitors the
- network connection, handles configuration, fixes leaky hosts, handles
- errors, etc.
- Status updates (connected, bandwidth, etc) are signaled to the GUI.
- """
-
- # XXX change name to EIPConductor ??
-
- def __init__(self,
- provider_cert_checker=ProviderCertChecker,
- config_checker=EIPConfigChecker,
- *args, **kwargs):
- #self.settingsfile = kwargs.get('settingsfile', None)
- #self.logfile = kwargs.get('logfile', None)
- self.provider = kwargs.pop('provider', None)
- self._providercertchecker = provider_cert_checker
- self._configchecker = config_checker
-
- self.error_queue = Queue.Queue()
-
- status_signals = kwargs.pop('status_signals', None)
- self.status = EIPConnectionStatus(callbacks=status_signals)
-
- checker_signals = kwargs.pop('checker_signals', None)
- self.checker_signals = checker_signals
-
- self.init_checkers()
-
- host = eipconfig.get_socket_path()
- kwargs['host'] = host
-
- super(EIPConnection, self).__init__(*args, **kwargs)
-
- def connect(self, **kwargs):
- """
- entry point for connection process
- """
- # in OpenVPNConnection
- self.try_openvpn_connection()
-
- def disconnect(self, shutdown=False):
- """
- disconnects client
- """
- self.terminate_openvpn_connection(shutdown=shutdown)
- self.status.change_to(self.status.DISCONNECTED)
-
- def has_errors(self):
- return True if self.error_queue.qsize() != 0 else False
-
- def init_checkers(self):
- """
- initialize checkers
- """
- self.provider_cert_checker = self._providercertchecker(
- domain=self.provider)
- self.config_checker = self._configchecker(domain=self.provider)
-
- def set_provider_domain(self, domain):
- """
- sets the provider domain.
- used from the first run wizard when we launch the run_checks
- and connect process after having initialized the conductor.
- """
- # This looks convoluted, right.
- # We have to reinstantiate checkers cause we're passing
- # the domain param that we did not know at the beginning
- # (only for the firstrunwizard case)
- self.provider = domain
- self.init_checkers()
-
- def run_checks(self, skip_download=False, skip_verify=False):
- """
- run all eip checks previous to attempting a connection
- """
- logger.debug('running conductor checks')
-
- def push_err(exc):
- # keep the original traceback!
- exc_traceback = sys.exc_info()[2]
- self.error_queue.put((exc, exc_traceback))
-
- try:
- # network (1)
- if self.checker_signals:
- for signal in self.checker_signals:
- signal('checking encryption keys')
- self.provider_cert_checker.run_all(skip_verify=skip_verify)
- except Exception as exc:
- push_err(exc)
- try:
- if self.checker_signals:
- for signal in self.checker_signals:
- signal('checking provider config')
- self.config_checker.run_all(skip_download=skip_download)
- except Exception as exc:
- push_err(exc)
- try:
- self.run_openvpn_checks()
- except Exception as exc:
- push_err(exc)
-
-
-class EIPConnectionStatus(object):
- """
- Keep track of client (gui) and openvpn
- states.
-
- These are the OpenVPN states:
- CONNECTING -- OpenVPN's initial state.
- WAIT -- (Client only) Waiting for initial response
- from server.
- AUTH -- (Client only) Authenticating with server.
- GET_CONFIG -- (Client only) Downloading configuration options
- from server.
- ASSIGN_IP -- Assigning IP address to virtual network
- interface.
- ADD_ROUTES -- Adding routes to system.
- CONNECTED -- Initialization Sequence Completed.
- RECONNECTING -- A restart has occurred.
- EXITING -- A graceful exit is in progress.
-
- We add some extra states:
-
- DISCONNECTED -- GUI initial state.
- UNRECOVERABLE -- An unrecoverable error has been raised
- while invoking openvpn service.
- """
- CONNECTING = 1
- WAIT = 2
- AUTH = 3
- GET_CONFIG = 4
- ASSIGN_IP = 5
- ADD_ROUTES = 6
- CONNECTED = 7
- RECONNECTING = 8
- EXITING = 9
-
- # gui specific states:
- UNRECOVERABLE = 11
- DISCONNECTED = 0
-
- def __init__(self, callbacks=None):
- """
- EIPConnectionStatus is initialized with a tuple
- of signals to be triggered.
- :param callbacks: a tuple of (callable) observers
- :type callbacks: tuple
- """
- self.current = self.DISCONNECTED
- self.previous = None
- # (callbacks to connect to signals in Qt-land)
- self.callbacks = callbacks
-
- def get_readable_status(self):
- # XXX DRY status / labels a little bit.
- # think we'll want to i18n this.
- human_status = {
- 0: 'disconnected',
- 1: 'connecting',
- 2: 'waiting',
- 3: 'authenticating',
- 4: 'getting config',
- 5: 'assigning ip',
- 6: 'adding routes',
- 7: 'connected',
- 8: 'reconnecting',
- 9: 'exiting',
- 11: 'unrecoverable error',
- }
- return human_status[self.current]
-
- def get_leap_status(self):
- # XXX improve nomenclature
- leap_status = {
- 0: 'disconnected',
- 1: 'connecting to gateway',
- 2: 'connecting to gateway',
- 3: 'authenticating',
- 4: 'establishing network encryption',
- 5: 'establishing network encryption',
- 6: 'establishing network encryption',
- 7: 'connected',
- 8: 'reconnecting',
- 9: 'exiting',
- 11: 'unrecoverable error',
- }
- return leap_status[self.current]
-
- def get_state_icon(self):
- """
- returns the high level icon
- for each fine-grain openvpn state
- """
- connecting = (self.CONNECTING,
- self.WAIT,
- self.AUTH,
- self.GET_CONFIG,
- self.ASSIGN_IP,
- self.ADD_ROUTES)
- connected = (self.CONNECTED,)
- disconnected = (self.DISCONNECTED,
- self.UNRECOVERABLE)
-
- # this can be made smarter,
- # but it's like it'll change,
- # so +readability.
-
- if self.current in connecting:
- return "connecting"
- if self.current in connected:
- return "connected"
- if self.current in disconnected:
- return "disconnected"
-
- def set_vpn_state(self, status):
- """
- accepts a state string from the management
- interface, and sets the internal state.
- :param status: openvpn STATE (uppercase).
- :type status: str
- """
- if hasattr(self, status):
- self.change_to(getattr(self, status))
-
- def set_current(self, to):
- """
- setter for the 'current' property
- :param to: destination state
- :type to: int
- """
- self.current = to
-
- def change_to(self, to):
- """
- :param to: destination state
- :type to: int
- """
- if to == self.current:
- return
- changed = False
- from_ = self.current
- self.current = to
-
- # We can add transition restrictions
- # here to ensure no transitions are
- # allowed outside the fsm.
-
- self.set_current(to)
- changed = True
-
- #trigger signals (as callbacks)
- #print('current state: %s' % self.current)
- if changed:
- self.previous = from_
- if self.callbacks:
- for cb in self.callbacks:
- if callable(cb):
- cb(self)
diff --git a/src/leap/eip/exceptions.py b/src/leap/eip/exceptions.py
deleted file mode 100644
index b7d398c3..00000000
--- a/src/leap/eip/exceptions.py
+++ /dev/null
@@ -1,175 +0,0 @@
-"""
-Generic error hierarchy
-Leap/EIP exceptions used for exception handling,
-logging, and notifying user of errors
-during leap operation.
-
-Exception hierarchy
--------------------
-All EIP Errors must inherit from EIPClientError (note: move that to
-a more generic LEAPClientBaseError).
-
-Exception attributes and their meaning/uses
--------------------------------------------
-
-* critical: if True, will abort execution prematurely,
- after attempting any cleaning
- action.
-
-* failfirst: breaks any error_check loop that is examining
- the error queue.
-
-* message: the message that will be used in the __repr__ of the exception.
-
-* usermessage: the message that will be passed to user in ErrorDialogs
- in Qt-land.
-
-TODO:
-
-* EIPClientError:
- Should inherit from LeapException
-
-* gettext / i18n for user messages.
-
-"""
-from leap.base.exceptions import LeapException
-from leap.util.translations import translate
-
-
-# This should inherit from LeapException
-class EIPClientError(Exception):
- """
- base EIPClient exception
- """
- critical = False
- failfirst = False
- warning = False
-
-
-class CriticalError(EIPClientError):
- """
- we cannot do anything about it, sorry
- """
- critical = True
- failfirst = True
-
-
-class Warning(EIPClientError):
- """
- just that, warnings
- """
- warning = True
-
-
-class EIPNoPolkitAuthAgentAvailable(CriticalError):
- message = "No polkit authentication agent could be found"
- usermessage = translate(
- "EIPErrors",
- "We could not find any authentication "
- "agent in your system.<br/>"
- "Make sure you have "
- "<b>polkit-gnome-authentication-agent-1</b> "
- "running and try again.")
-
-
-class EIPNoPkexecAvailable(Warning):
- message = "No pkexec binary found"
- usermessage = translate(
- "EIPErrors",
- "We could not find <b>pkexec</b> in your "
- "system.<br/> Do you want to try "
- "<b>setuid workaround</b>? "
- "(<i>DOES NOTHING YET</i>)")
- failfirst = True
-
-
-class EIPNoCommandError(EIPClientError):
- message = "no suitable openvpn command found"
- usermessage = translate(
- "EIPErrors",
- "No suitable openvpn command found. "
- "<br/>(Might be a permissions problem)")
-
-
-class EIPBadCertError(Warning):
- # XXX this should be critical and fail close
- message = "cert verification failed"
- usermessage = translate(
- "EIPErrors",
- "there is a problem with provider certificate")
-
-
-class LeapBadConfigFetchedError(Warning):
- message = "provider sent a malformed json file"
- usermessage = translate(
- "EIPErrors",
- "an error occurred during configuratio of leap services")
-
-
-class OpenVPNAlreadyRunning(CriticalError):
- message = "Another OpenVPN Process is already running."
- usermessage = translate(
- "EIPErrors",
- "Another OpenVPN Process has been detected. "
- "Please close it before starting leap-client")
-
-
-class HttpsNotSupported(LeapException):
- message = "connection refused while accessing via https"
- usermessage = translate(
- "EIPErrors",
- "Server does not allow secure connections")
-
-
-class HttpsBadCertError(LeapException):
- message = "verification error on cert"
- usermessage = translate(
- "EIPErrors",
- "Server certificate could not be verified")
-
-#
-# errors still needing some love
-#
-
-
-class EIPInitNoKeyFileError(CriticalError):
- message = "No vpn keys found in the expected path"
- usermessage = translate(
- "EIPErrors",
- "We could not find your eip certs in the expected path")
-
-
-class EIPInitBadKeyFilePermError(Warning):
- # I don't know if we should be telling user or not,
- # we try to fix permissions and should only re-raise
- # if permission check failed.
- pass
-
-
-class EIPInitNoProviderError(EIPClientError):
- pass
-
-
-class EIPInitBadProviderError(EIPClientError):
- pass
-
-
-class EIPConfigurationError(EIPClientError):
- pass
-
-#
-# Errors that probably we don't need anymore
-# chase down for them and check.
-#
-
-
-class MissingSocketError(Exception):
- pass
-
-
-class ConnectionRefusedError(Exception):
- pass
-
-
-class EIPMissingDefaultProvider(Exception):
- pass
diff --git a/src/leap/eip/openvpnconnection.py b/src/leap/eip/openvpnconnection.py
deleted file mode 100644
index bee8c010..00000000
--- a/src/leap/eip/openvpnconnection.py
+++ /dev/null
@@ -1,410 +0,0 @@
-"""
-OpenVPN Connection
-"""
-from __future__ import (print_function)
-from functools import partial
-import logging
-import os
-import psutil
-import shutil
-import select
-import socket
-from time import sleep
-
-logger = logging.getLogger(name=__name__)
-
-from leap.base.connection import Connection
-from leap.base.constants import OPENVPN_BIN
-from leap.util.coroutines import spawn_and_watch_process
-from leap.util.misc import get_openvpn_pids
-
-from leap.eip.udstelnet import UDSTelnet
-from leap.eip import config as eip_config
-from leap.eip import exceptions as eip_exceptions
-
-
-class OpenVPNManagement(object):
-
- # TODO explain a little bit how management interface works
- # and our telnet interface with support for unix sockets.
-
- """
- for more information, read openvpn management notes.
- zcat `dpkg -L openvpn | grep management`
- """
-
- def _connect_to_management(self):
- """
- Connect to openvpn management interface
- """
- if hasattr(self, 'tn'):
- self._close_management_socket()
- self.tn = UDSTelnet(self.host, self.port)
-
- # XXX make password optional
- # specially for win. we should generate
- # the pass on the fly when invoking manager
- # from conductor
-
- #self.tn.read_until('ENTER PASSWORD:', 2)
- #self.tn.write(self.password + '\n')
- #self.tn.read_until('SUCCESS:', 2)
- if self.tn:
- self._seek_to_eof()
- return True
-
- def _close_management_socket(self, announce=True):
- """
- Close connection to openvpn management interface
- """
- logger.debug('closing socket')
- if announce:
- self.tn.write("quit\n")
- self.tn.read_all()
- self.tn.get_socket().close()
- del self.tn
-
- def _seek_to_eof(self):
- """
- Read as much as available. Position seek pointer to end of stream
- """
- try:
- b = self.tn.read_eager()
- except EOFError:
- logger.debug("Could not read from socket. Assuming it died.")
- return
- while b:
- try:
- b = self.tn.read_eager()
- except EOFError:
- logger.debug("Could not read from socket. Assuming it died.")
-
- def _send_command(self, cmd):
- """
- Send a command to openvpn and return response as list
- """
- if not self.connected():
- try:
- self._connect_to_management()
- except eip_exceptions.MissingSocketError:
- #logger.warning('missing management socket')
- return []
- try:
- if hasattr(self, 'tn'):
- self.tn.write(cmd + "\n")
- except socket.error:
- logger.error('socket error')
- self._close_management_socket(announce=False)
- return []
- try:
- buf = self.tn.read_until(b"END", 2)
- self._seek_to_eof()
- blist = buf.split('\r\n')
- if blist[-1].startswith('END'):
- del blist[-1]
- return blist
- else:
- return []
- except socket.error as exc:
- logger.debug('socket error: %s' % exc.message)
- except select.error as exc:
- logger.debug('select error: %s' % exc.message)
-
- def _send_short_command(self, cmd):
- """
- parse output from commands that are
- delimited by "success" instead
- """
- if not self.connected():
- self.connect()
- self.tn.write(cmd + "\n")
- # XXX not working?
- buf = self.tn.read_until(b"SUCCESS", 2)
- self._seek_to_eof()
- blist = buf.split('\r\n')
- return blist
-
- #
- # random maybe useful vpn commands
- #
-
- def pid(self):
- #XXX broken
- return self._send_short_command("pid")
-
-
-class OpenVPNConnection(Connection, OpenVPNManagement):
- """
- All related to invocation
- of the openvpn binary.
- It's extended by EIPConnection.
- """
-
- # XXX Inheriting from Connection was an early design idea
- # but currently that's an empty class.
- # We can get rid of that if we don't use it for sharing
- # state with other leap modules.
-
- def __init__(self,
- watcher_cb=None,
- debug=False,
- host=None,
- port="unix",
- password=None,
- *args, **kwargs):
- """
- :param watcher_cb: callback to be \
-called for each line in watched stdout
- :param signal_map: dictionary of signal names and callables \
-to be triggered for each one of them.
- :type watcher_cb: function
- :type signal_map: dict
- """
- #XXX FIXME
- #change watcher_cb to line_observer
- # XXX if not host: raise ImproperlyConfigured
-
- logger.debug('init openvpn connection')
- self.debug = debug
- self.ovpn_verbosity = kwargs.get('ovpn_verbosity', None)
-
- self.watcher_cb = watcher_cb
- #self.signal_maps = signal_maps
-
- self.subp = None
- self.watcher = None
-
- self.server = None
- self.port = None
- self.proto = None
-
- self.command = None
- self.args = None
-
- # XXX get autostart from config
- self.autostart = True
-
- # management interface init
- self.host = host
- if isinstance(port, str) and port.isdigit():
- port = int(port)
- elif port == "unix":
- port = "unix"
- else:
- port = None
- self.port = port
- self.password = password
-
- def run_openvpn_checks(self):
- """
- runs check needed before launching
- openvpn subprocess. will raise if errors found.
- """
- logger.debug('running openvpn checks')
- # XXX I think that "check_if_running" should be called
- # from try openvpn connection instead. -- kali.
- # let's prepare tests for that before changing it...
- self._check_if_running_instance()
- self._set_ovpn_command()
- self._check_vpn_keys()
-
- def try_openvpn_connection(self):
- """
- attempts to connect
- """
- # XXX should make public method
- if self.command is None:
- raise eip_exceptions.EIPNoCommandError
- if self.subp is not None:
- logger.debug('cowardly refusing to launch subprocess again')
- # XXX this is not returning ???!!
- # FIXME -- so it's calling it all the same!!
-
- self._launch_openvpn()
-
- def connected(self):
- """
- Returns True if connected
- rtype: bool
- """
- # XXX make a property
- return hasattr(self, 'tn')
-
- def terminate_openvpn_connection(self, shutdown=False):
- """
- terminates openvpn child subprocess
- """
- if self.subp:
- try:
- self._stop_openvpn()
- except eip_exceptions.ConnectionRefusedError:
- logger.warning(
- 'unable to send sigterm signal to openvpn: '
- 'connection refused.')
-
- # XXX kali --
- # XXX review-me
- # I think this will block if child process
- # does not return.
- # Maybe we can .poll() for a given
- # interval and exit in any case.
-
- RETCODE = self.subp.wait()
- if RETCODE:
- logger.error(
- 'cannot terminate subprocess! Retcode %s'
- '(We might have left openvpn running)' % RETCODE)
-
- if shutdown:
- self._cleanup_tempfiles()
-
- def _cleanup_tempfiles(self):
- """
- remove all temporal files
- we might have left behind
- """
- # if self.port is 'unix', we have
- # created a temporal socket path that, under
- # normal circumstances, we should be able to
- # delete
-
- if self.port == "unix":
- logger.debug('cleaning socket file temp folder')
-
- tempfolder = os.path.split(self.host)[0]
- if os.path.isdir(tempfolder):
- try:
- shutil.rmtree(tempfolder)
- except OSError:
- logger.error('could not delete tmpfolder %s' % tempfolder)
-
- # checks
-
- def _check_if_running_instance(self):
- """
- check if openvpn is already running
- """
- openvpn_pids = get_openvpn_pids()
- if openvpn_pids:
- logger.debug('an openvpn instance is already running.')
- logger.debug('attempting to stop openvpn instance.')
- if not self._stop_openvpn():
- raise eip_exceptions.OpenVPNAlreadyRunning
- return
- else:
- logger.debug('no openvpn instance found.')
-
- def _set_ovpn_command(self):
- try:
- command, args = eip_config.build_ovpn_command(
- provider=self.provider,
- debug=self.debug,
- socket_path=self.host,
- ovpn_verbosity=self.ovpn_verbosity)
- except eip_exceptions.EIPNoPolkitAuthAgentAvailable:
- command = args = None
- raise
- except eip_exceptions.EIPNoPkexecAvailable:
- command = args = None
- raise
-
- # XXX if not command, signal error.
- self.command = command
- self.args = args
-
- def _check_vpn_keys(self):
- """
- checks for correct permissions on vpn keys
- """
- try:
- eip_config.check_vpn_keys(provider=self.provider)
- except eip_exceptions.EIPInitBadKeyFilePermError:
- logger.error('Bad VPN Keys permission!')
- # do nothing now
- # and raise the rest ...
-
- # starting and stopping openvpn subprocess
-
- def _launch_openvpn(self):
- """
- invocation of openvpn binaries in a subprocess.
- """
- #XXX TODO:
- #deprecate watcher_cb,
- #use _only_ signal_maps instead
-
- #logger.debug('_launch_openvpn called')
- if self.watcher_cb is not None:
- linewrite_callback = self.watcher_cb
- else:
- #XXX get logger instead
- linewrite_callback = lambda line: logger.debug(
- 'watcher: %s' % line)
-
- # the partial is not
- # being applied now because we're not observing the process
- # stdout like we did in the early stages. but I leave it
- # here since it will be handy for observing patterns in the
- # thru-the-manager updates (with regex)
- observers = (linewrite_callback,
- partial(lambda con_status,
- line: linewrite_callback, self.status))
- subp, watcher = spawn_and_watch_process(
- self.command,
- self.args,
- observers=observers)
- self.subp = subp
- self.watcher = watcher
-
- def _stop_openvpn(self):
- """
- stop openvpn process
- by sending SIGTERM to the management
- interface
- """
- # XXX method a bit too long, split
- logger.debug("atempting to terminate openvpn process...")
- if self.connected():
- try:
- self._send_command("signal SIGTERM\n")
- sleep(1)
- if not self.subp: # XXX ???
- return True
- except socket.error:
- logger.warning('management socket died')
- return
-
- #shutting openvpn failured
- #try patching in old openvpn host and trying again
- # XXX could be more than one!
- process = self._get_openvpn_process()
- if process:
- logger.debug('process: %s' % process.name)
- cmdline = process.cmdline
-
- manag_flag = "--management"
- if isinstance(cmdline, list) and manag_flag in cmdline:
- _index = cmdline.index(manag_flag)
- self.host = cmdline[_index + 1]
- self._send_command("signal SIGTERM\n")
-
- #make sure the process was terminated
- process = self._get_openvpn_process()
- if not process:
- logger.debug("Existing OpenVPN Process Terminated")
- return True
- else:
- logger.error("Unable to terminate existing OpenVPN Process.")
- return False
-
- return True
-
- def _get_openvpn_process(self):
- for process in psutil.process_iter():
- if OPENVPN_BIN in process.name:
- return process
- return None
-
- def get_log(self, lines=1):
- log = self._send_command("log %s" % lines)
- return log
diff --git a/src/leap/eip/specs.py b/src/leap/eip/specs.py
deleted file mode 100644
index c41fd29b..00000000
--- a/src/leap/eip/specs.py
+++ /dev/null
@@ -1,136 +0,0 @@
-from __future__ import (unicode_literals)
-import os
-
-from leap import __branding
-from leap.base import config as baseconfig
-
-# XXX move provider stuff to base config
-
-PROVIDER_CA_CERT = __branding.get(
- 'provider_ca_file',
- 'cacert.pem')
-
-provider_ca_path = lambda domain: str(os.path.join(
- #baseconfig.get_default_provider_path(),
- baseconfig.get_provider_path(domain),
- 'keys', 'ca',
- 'cacert.pem'
-)) if domain else None
-
-default_provider_ca_path = lambda: str(os.path.join(
- baseconfig.get_default_provider_path(),
- 'keys', 'ca',
- PROVIDER_CA_CERT
-))
-
-PROVIDER_DOMAIN = __branding.get('provider_domain', 'testprovider.example.org')
-
-
-client_cert_path = lambda domain: unicode(os.path.join(
- baseconfig.get_provider_path(domain),
- 'keys', 'client',
- 'openvpn.pem'
-)) if domain else None
-
-default_client_cert_path = lambda: unicode(os.path.join(
- baseconfig.get_default_provider_path(),
- 'keys', 'client',
- 'openvpn.pem'
-))
-
-eipconfig_spec = {
- 'description': 'sample eipconfig',
- 'type': 'object',
- 'properties': {
- 'provider': {
- 'type': unicode,
- 'default': u"%s" % PROVIDER_DOMAIN,
- 'required': True,
- },
- 'transport': {
- 'type': unicode,
- 'default': u"openvpn",
- },
- 'openvpn_protocol': {
- 'type': unicode,
- 'default': u"tcp"
- },
- 'openvpn_port': {
- 'type': int,
- 'default': 80
- },
- 'openvpn_ca_certificate': {
- 'type': unicode, # path
- 'default': default_provider_ca_path
- },
- 'openvpn_client_certificate': {
- 'type': unicode, # path
- 'default': default_client_cert_path
- },
- 'connect_on_login': {
- 'type': bool,
- 'default': True
- },
- 'block_cleartext_traffic': {
- 'type': bool,
- 'default': True
- },
- 'primary_gateway': {
- 'type': unicode,
- 'default': u"location_unknown",
- #'required': True
- },
- 'secondary_gateway': {
- 'type': unicode,
- 'default': u"location_unknown2"
- },
- 'management_password': {
- 'type': unicode
- }
- }
-}
-
-eipservice_config_spec = {
- 'description': 'sample eip service config',
- 'type': 'object',
- 'properties': {
- 'serial': {
- 'type': int,
- 'required': True,
- 'default': 1
- },
- 'version': {
- 'type': int,
- 'required': True,
- 'default': 1
- },
- 'clusters': {
- 'type': list,
- 'default': [
- {"label": {
- "en": "Location Unknown"},
- "name": "location_unknown"}]
- },
- 'gateways': {
- 'type': list,
- 'default': [
- {"capabilities": {
- "adblock": True,
- "filter_dns": True,
- "ports": ["80", "53", "443", "1194"],
- "protocols": ["udp", "tcp"],
- "transport": ["openvpn"],
- "user_ips": False},
- "cluster": "location_unknown",
- "host": "location.example.org",
- "ip_address": "127.0.0.1"}]
- },
- 'openvpn_configuration': {
- 'type': dict,
- 'default': {
- "auth": None,
- "cipher": None,
- "tls-cipher": None}
- }
- }
-}
diff --git a/src/leap/eip/tests/data.py b/src/leap/eip/tests/data.py
deleted file mode 100644
index a7fe1853..00000000
--- a/src/leap/eip/tests/data.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from __future__ import unicode_literals
-import os
-
-#from leap import __branding
-
-# sample data used in tests
-
-#PROVIDER = __branding.get('provider_domain')
-PROVIDER = "testprovider.example.org"
-
-EIP_SAMPLE_CONFIG = {
- "provider": "%s" % PROVIDER,
- "transport": "openvpn",
- "openvpn_protocol": "tcp",
- "openvpn_port": 80,
- "openvpn_ca_certificate": os.path.expanduser(
- "~/.config/leap/providers/"
- "%s/"
- "keys/ca/cacert.pem" % PROVIDER),
- "openvpn_client_certificate": os.path.expanduser(
- "~/.config/leap/providers/"
- "%s/"
- "keys/client/openvpn.pem" % PROVIDER),
- "connect_on_login": True,
- "block_cleartext_traffic": True,
- "primary_gateway": "location_unknown",
- "secondary_gateway": "location_unknown2",
- #"management_password": "oph7Que1othahwiech6J"
-}
-
-EIP_SAMPLE_SERVICE = {
- "serial": 1,
- "version": 1,
- "clusters": [
- {"label": {
- "en": "Location Unknown"},
- "name": "location_unknown"}
- ],
- "gateways": [
- {"capabilities": {
- "adblock": True,
- "filter_dns": True,
- "ports": ["80", "53", "443", "1194"],
- "protocols": ["udp", "tcp"],
- "transport": ["openvpn"],
- "user_ips": False},
- "cluster": "location_unknown",
- "host": "location.example.org",
- "ip_address": "192.0.43.10"}
- ]
-}
diff --git a/src/leap/eip/tests/test_checks.py b/src/leap/eip/tests/test_checks.py
deleted file mode 100644
index f42a0eeb..00000000
--- a/src/leap/eip/tests/test_checks.py
+++ /dev/null
@@ -1,372 +0,0 @@
-from BaseHTTPServer import BaseHTTPRequestHandler
-import copy
-import json
-try:
- import unittest2 as unittest
-except ImportError:
- import unittest
-import os
-import time
-import urlparse
-
-from mock import (patch, Mock)
-
-import requests
-
-from leap.base import config as baseconfig
-from leap.base import pluggableconfig
-from leap.base.constants import (DEFAULT_PROVIDER_DEFINITION,
- DEFINITION_EXPECTED_PATH)
-from leap.eip import checks as eipchecks
-from leap.eip import specs as eipspecs
-from leap.eip import exceptions as eipexceptions
-from leap.eip.tests import data as testdata
-from leap.testing.basetest import BaseLeapTest
-from leap.testing.https_server import BaseHTTPSServerTestCase
-from leap.testing.https_server import where as where_cert
-from leap.util.fileutil import mkdir_f
-
-
-class NoLogRequestHandler:
- def log_message(self, *args):
- # don't write log msg to stderr
- pass
-
- def read(self, n=None):
- return ''
-
-
-class EIPCheckTest(BaseLeapTest):
-
- __name__ = "eip_check_tests"
- provider = "testprovider.example.org"
- maxDiff = None
-
- def setUp(self):
- pass
-
- def tearDown(self):
- pass
-
- # test methods are there, and can be called from run_all
-
- def test_checker_should_implement_check_methods(self):
- checker = eipchecks.EIPConfigChecker(domain=self.provider)
-
- self.assertTrue(hasattr(checker, "check_default_eipconfig"),
- "missing meth")
- self.assertTrue(hasattr(checker, "check_is_there_default_provider"),
- "missing meth")
- self.assertTrue(hasattr(checker, "fetch_definition"), "missing meth")
- self.assertTrue(hasattr(checker, "fetch_eip_service_config"),
- "missing meth")
- self.assertTrue(hasattr(checker, "check_complete_eip_config"),
- "missing meth")
-
- def test_checker_should_actually_call_all_tests(self):
- checker = eipchecks.EIPConfigChecker(domain=self.provider)
-
- mc = Mock()
- checker.run_all(checker=mc)
- self.assertTrue(mc.check_default_eipconfig.called, "not called")
- self.assertTrue(mc.check_is_there_default_provider.called,
- "not called")
- self.assertTrue(mc.fetch_definition.called,
- "not called")
- self.assertTrue(mc.fetch_eip_service_config.called,
- "not called")
- self.assertTrue(mc.check_complete_eip_config.called,
- "not called")
-
- # test individual check methods
-
- def test_check_default_eipconfig(self):
- checker = eipchecks.EIPConfigChecker(domain=self.provider)
- # no eip config (empty home)
- eipconfig_path = checker.eipconfig.filename
- self.assertFalse(os.path.isfile(eipconfig_path))
- checker.check_default_eipconfig()
- # we've written one, so it should be there.
- self.assertTrue(os.path.isfile(eipconfig_path))
- with open(eipconfig_path, 'rb') as fp:
- deserialized = json.load(fp)
-
- # force re-evaluation of the paths
- # small workaround for evaluating home dirs correctly
- EIP_SAMPLE_CONFIG = copy.copy(testdata.EIP_SAMPLE_CONFIG)
- EIP_SAMPLE_CONFIG['openvpn_client_certificate'] = \
- eipspecs.client_cert_path(self.provider)
- EIP_SAMPLE_CONFIG['openvpn_ca_certificate'] = \
- eipspecs.provider_ca_path(self.provider)
- self.assertEqual(deserialized, EIP_SAMPLE_CONFIG)
-
- # TODO: shold ALSO run validation methods.
-
- def test_check_is_there_default_provider(self):
- checker = eipchecks.EIPConfigChecker(domain=self.provider)
- # we do dump a sample eip config, but lacking a
- # default provider entry.
- # This error will be possible catched in a different
- # place, when JSONConfig does validation of required fields.
-
- # passing direct config
- with self.assertRaises(eipexceptions.EIPMissingDefaultProvider):
- checker.check_is_there_default_provider(config={})
-
- # ok. now, messing with real files...
- # blank out default_provider
- sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG)
- sampleconfig['provider'] = None
- eipcfg_path = checker.eipconfig.filename
- mkdir_f(eipcfg_path)
- with open(eipcfg_path, 'w') as fp:
- json.dump(sampleconfig, fp)
- #with self.assertRaises(eipexceptions.EIPMissingDefaultProvider):
- # XXX we should catch this as one of our errors, but do not
- # see how to do it quickly.
- with self.assertRaises(pluggableconfig.ValidationError):
- #import ipdb;ipdb.set_trace()
- checker.eipconfig.load(fromfile=eipcfg_path)
- checker.check_is_there_default_provider()
-
- sampleconfig = testdata.EIP_SAMPLE_CONFIG
- #eipcfg_path = checker._get_default_eipconfig_path()
- with open(eipcfg_path, 'w') as fp:
- json.dump(sampleconfig, fp)
- checker.eipconfig.load()
- self.assertTrue(checker.check_is_there_default_provider())
-
- def test_fetch_definition(self):
- with patch.object(requests, "get") as mocked_get:
- mocked_get.return_value.status_code = 200
- mocked_get.return_value.headers = {
- 'last-modified': "Wed Dec 12 12:12:12 GMT 2012"}
- mocked_get.return_value.json = DEFAULT_PROVIDER_DEFINITION
- checker = eipchecks.EIPConfigChecker(fetcher=requests)
- sampleconfig = testdata.EIP_SAMPLE_CONFIG
- checker.fetch_definition(config=sampleconfig)
-
- fn = os.path.join(baseconfig.get_default_provider_path(),
- DEFINITION_EXPECTED_PATH)
- with open(fn, 'r') as fp:
- deserialized = json.load(fp)
- self.assertEqual(DEFAULT_PROVIDER_DEFINITION, deserialized)
-
- # XXX TODO check for ConnectionError, HTTPError, InvalidUrl
- # (and proper EIPExceptions are raised).
- # Look at base.test_config.
-
- def test_fetch_eip_service_config(self):
- with patch.object(requests, "get") as mocked_get:
- mocked_get.return_value.status_code = 200
- mocked_get.return_value.headers = {
- 'last-modified': "Wed Dec 12 12:12:12 GMT 2012"}
- mocked_get.return_value.json = testdata.EIP_SAMPLE_SERVICE
- checker = eipchecks.EIPConfigChecker(fetcher=requests)
- sampleconfig = testdata.EIP_SAMPLE_CONFIG
- checker.fetch_eip_service_config(config=sampleconfig)
-
- def test_check_complete_eip_config(self):
- checker = eipchecks.EIPConfigChecker()
- with self.assertRaises(eipexceptions.EIPConfigurationError):
- sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG)
- sampleconfig['provider'] = None
- checker.check_complete_eip_config(config=sampleconfig)
- with self.assertRaises(eipexceptions.EIPConfigurationError):
- sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG)
- del sampleconfig['provider']
- checker.check_complete_eip_config(config=sampleconfig)
-
- # normal case
- sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG)
- checker.check_complete_eip_config(config=sampleconfig)
-
-
-class ProviderCertCheckerTest(BaseLeapTest):
-
- __name__ = "provider_cert_checker_tests"
- provider = "testprovider.example.org"
-
- def setUp(self):
- pass
-
- def tearDown(self):
- pass
-
- # test methods are there, and can be called from run_all
-
- def test_checker_should_implement_check_methods(self):
- checker = eipchecks.ProviderCertChecker()
-
- # For MVS+
- self.assertTrue(hasattr(checker, "download_ca_cert"),
- "missing meth")
- self.assertTrue(hasattr(checker, "download_ca_signature"),
- "missing meth")
- self.assertTrue(hasattr(checker, "get_ca_signatures"), "missing meth")
- self.assertTrue(hasattr(checker, "is_there_trust_path"),
- "missing meth")
-
- # For MVS
- self.assertTrue(hasattr(checker, "is_there_provider_ca"),
- "missing meth")
- self.assertTrue(hasattr(checker, "is_https_working"), "missing meth")
- self.assertTrue(hasattr(checker, "check_new_cert_needed"),
- "missing meth")
-
- def test_checker_should_actually_call_all_tests(self):
- checker = eipchecks.ProviderCertChecker()
-
- mc = Mock()
- checker.run_all(checker=mc)
- # XXX MVS+
- #self.assertTrue(mc.download_ca_cert.called, "not called")
- #self.assertTrue(mc.download_ca_signature.called, "not called")
- #self.assertTrue(mc.get_ca_signatures.called, "not called")
- #self.assertTrue(mc.is_there_trust_path.called, "not called")
-
- # For MVS
- self.assertTrue(mc.is_there_provider_ca.called, "not called")
- self.assertTrue(mc.is_https_working.called,
- "not called")
- self.assertTrue(mc.check_new_cert_needed.called,
- "not called")
-
- # test individual check methods
-
- @unittest.skip
- def test_is_there_provider_ca(self):
- # XXX commenting out this test.
- # With the generic client this does not make sense,
- # we should dump one there.
- # or test conductor logic.
- checker = eipchecks.ProviderCertChecker()
- self.assertTrue(
- checker.is_there_provider_ca())
-
-
-class ProviderCertCheckerHTTPSTests(BaseHTTPSServerTestCase, BaseLeapTest):
- provider = "testprovider.example.org"
-
- class request_handler(NoLogRequestHandler, BaseHTTPRequestHandler):
- responses = {
- '/': ['OK', ''],
- '/client.cert': [
- # XXX get sample cert
- '-----BEGIN CERTIFICATE-----',
- '-----END CERTIFICATE-----'],
- '/badclient.cert': [
- 'BADCERT']}
-
- def do_GET(self):
- path = urlparse.urlparse(self.path)
- message = '\n'.join(self.responses.get(
- path.path, None))
- self.send_response(200)
- self.end_headers()
- self.wfile.write(message)
-
- def test_is_https_working(self):
- fetcher = requests
- uri = "https://%s/" % (self.get_server())
- # bare requests call. this should just pass (if there is
- # an https service there).
- fetcher.get(uri, verify=False)
- checker = eipchecks.ProviderCertChecker(fetcher=fetcher)
- self.assertTrue(checker.is_https_working(uri=uri, verify=False))
-
- # for local debugs, when in doubt
- #self.assertTrue(checker.is_https_working(uri="https://github.com",
- #verify=True))
-
- # for the two checks below, I know they fail because no ca
- # cert is passed to them, and I know that's the error that
- # requests return with our implementation.
- # We're receiving this because our
- # server is dying prematurely when the handshake is interrupted on the
- # client side.
- # Since we have access to the server, we could check that
- # the error raised has been:
- # SSL23_READ_BYTES: alert bad certificate
- with self.assertRaises(requests.exceptions.SSLError) as exc:
- fetcher.get(uri, verify=True)
- self.assertTrue(
- "SSL23_GET_SERVER_HELLO:unknown protocol" in exc.message)
-
- # XXX FIXME! Uncomment after #638 is done
- #with self.assertRaises(eipexceptions.EIPBadCertError) as exc:
- #checker.is_https_working(uri=uri, verify=True)
- #self.assertTrue(
- #"cert verification failed" in exc.message)
-
- # get cacert from testing.https_server
- cacert = where_cert('cacert.pem')
- fetcher.get(uri, verify=cacert)
- self.assertTrue(checker.is_https_working(uri=uri, verify=cacert))
-
- # same, but get cacert from leap.custom
- # XXX TODO!
-
- @unittest.skip
- def test_download_new_client_cert(self):
- # FIXME
- # Magick srp decorator broken right now...
- # Have to mock the decorator and inject something that
- # can bypass the authentication
-
- uri = "https://%s/client.cert" % (self.get_server())
- cacert = where_cert('cacert.pem')
- checker = eipchecks.ProviderCertChecker(domain=self.provider)
- credentials = "testuser", "testpassword"
- self.assertTrue(checker.download_new_client_cert(
- credentials=credentials, uri=uri, verify=cacert))
-
- # now download a malformed cert
- uri = "https://%s/badclient.cert" % (self.get_server())
- cacert = where_cert('cacert.pem')
- checker = eipchecks.ProviderCertChecker()
- with self.assertRaises(ValueError):
- self.assertTrue(checker.download_new_client_cert(
- credentials=credentials, uri=uri, verify=cacert))
-
- # did we write cert to its path?
- clientcertfile = eipspecs.client_cert_path()
- self.assertTrue(os.path.isfile(clientcertfile))
- certfile = eipspecs.client_cert_path()
- with open(certfile, 'r') as cf:
- certcontent = cf.read()
- self.assertEqual(certcontent,
- '\n'.join(
- self.request_handler.responses['/client.cert']))
- os.remove(clientcertfile)
-
- def test_is_cert_valid(self):
- checker = eipchecks.ProviderCertChecker()
- # TODO: better exception catching
- # should raise eipexceptions.BadClientCertificate, and give reasons
- # on msg.
- with self.assertRaises(Exception) as exc:
- self.assertFalse(checker.is_cert_valid())
- exc.message = "missing cert"
-
- def test_bad_validity_certs(self):
- checker = eipchecks.ProviderCertChecker()
- certfile = where_cert('leaptestscert.pem')
- self.assertFalse(checker.is_cert_not_expired(
- certfile=certfile,
- now=lambda: time.mktime((2038, 1, 1, 1, 1, 1, 1, 1, 1))))
- self.assertFalse(checker.is_cert_not_expired(
- certfile=certfile,
- now=lambda: time.mktime((1970, 1, 1, 1, 1, 1, 1, 1, 1))))
-
- def test_check_new_cert_needed(self):
- # check: missing cert
- checker = eipchecks.ProviderCertChecker(domain=self.provider)
- self.assertTrue(checker.check_new_cert_needed(skip_download=True))
- # TODO check: malformed cert
- # TODO check: expired cert
- # TODO check: pass test server uri instead of skip
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/eip/tests/test_config.py b/src/leap/eip/tests/test_config.py
deleted file mode 100644
index 72ab3c8e..00000000
--- a/src/leap/eip/tests/test_config.py
+++ /dev/null
@@ -1,298 +0,0 @@
-from collections import OrderedDict
-import json
-import os
-import platform
-import stat
-
-try:
- import unittest2 as unittest
-except ImportError:
- import unittest
-
-#from leap.base import constants
-#from leap.eip import config as eip_config
-#from leap import __branding as BRANDING
-from leap.eip import config as eipconfig
-from leap.eip.tests.data import EIP_SAMPLE_CONFIG, EIP_SAMPLE_SERVICE
-from leap.testing.basetest import BaseLeapTest
-from leap.util.fileutil import mkdir_p, mkdir_f
-
-_system = platform.system()
-
-#PROVIDER = BRANDING.get('provider_domain')
-#PROVIDER_SHORTNAME = BRANDING.get('short_name')
-
-
-class EIPConfigTest(BaseLeapTest):
-
- __name__ = "eip_config_tests"
- provider = "testprovider.example.org"
-
- maxDiff = None
-
- def setUp(self):
- pass
-
- def tearDown(self):
- pass
-
- #
- # helpers
- #
-
- def touch_exec(self):
- path = os.path.join(
- self.tempdir, 'bin')
- mkdir_p(path)
- tfile = os.path.join(
- path,
- 'openvpn')
- open(tfile, 'wb').close()
- os.chmod(tfile, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
-
- def write_sample_eipservice(self, vpnciphers=False, extra_vpnopts=None,
- gateways=None):
- conf = eipconfig.EIPServiceConfig()
- mkdir_f(conf.filename)
- if gateways:
- EIP_SAMPLE_SERVICE['gateways'] = gateways
- if vpnciphers:
- openvpnconfig = OrderedDict({
- "auth": "SHA1",
- "cipher": "AES-128-CBC",
- "tls-cipher": "DHE-RSA-AES128-SHA"})
- if extra_vpnopts:
- for k, v in extra_vpnopts.items():
- openvpnconfig[k] = v
- EIP_SAMPLE_SERVICE['openvpn_configuration'] = openvpnconfig
-
- with open(conf.filename, 'w') as fd:
- fd.write(json.dumps(EIP_SAMPLE_SERVICE))
-
- def write_sample_eipconfig(self):
- conf = eipconfig.EIPConfig()
- folder, f = os.path.split(conf.filename)
- if not os.path.isdir(folder):
- mkdir_p(folder)
- with open(conf.filename, 'w') as fd:
- fd.write(json.dumps(EIP_SAMPLE_CONFIG))
-
- def get_expected_openvpn_args(self, with_openvpn_ciphers=False):
- """
- yeah, this is almost as duplicating the
- code for building the command
- """
- args = []
- eipconf = eipconfig.EIPConfig(domain=self.provider)
- eipconf.load()
- eipsconf = eipconfig.EIPServiceConfig(domain=self.provider)
- eipsconf.load()
-
- username = self.get_username()
- groupname = self.get_groupname()
-
- args.append('--client')
- args.append('--dev')
- #does this have to be tap for win??
- args.append('tun')
- args.append('--persist-tun')
- args.append('--persist-key')
- args.append('--remote')
-
- args.append('%s' % eipconfig.get_eip_gateway(
- eipconfig=eipconf,
- eipserviceconfig=eipsconf))
- # XXX get port!?
- args.append('1194')
- # XXX get proto
- args.append('udp')
- args.append('--tls-client')
- args.append('--remote-cert-tls')
- args.append('server')
-
- if with_openvpn_ciphers:
- CIPHERS = [
- "--tls-cipher", "DHE-RSA-AES128-SHA",
- "--cipher", "AES-128-CBC",
- "--auth", "SHA1"]
- for opt in CIPHERS:
- args.append(opt)
-
- args.append('--user')
- args.append(username)
- args.append('--group')
- args.append(groupname)
- args.append('--management-client-user')
- args.append(username)
- args.append('--management-signal')
-
- args.append('--management')
- #XXX hey!
- #get platform switches here!
- args.append('/tmp/test.socket')
- args.append('unix')
-
- args.append('--script-security')
- args.append('2')
-
- if _system == "Linux":
- UPDOWN_SCRIPT = "/etc/leap/resolv-update"
- if os.path.isfile(UPDOWN_SCRIPT):
- args.append('--up')
- args.append('/etc/leap/resolv-update')
- args.append('--down')
- args.append('/etc/leap/resolv-update')
- args.append('--plugin')
- args.append('/usr/lib/openvpn/openvpn-down-root.so')
- args.append("'script_type=down /etc/leap/resolv-update'")
-
- # certs
- # XXX get values from specs?
- args.append('--cert')
- args.append(os.path.join(
- self.home,
- '.config', 'leap', 'providers',
- '%s' % self.provider,
- 'keys', 'client',
- 'openvpn.pem'))
- args.append('--key')
- args.append(os.path.join(
- self.home,
- '.config', 'leap', 'providers',
- '%s' % self.provider,
- 'keys', 'client',
- 'openvpn.pem'))
- args.append('--ca')
- args.append(os.path.join(
- self.home,
- '.config', 'leap', 'providers',
- '%s' % self.provider,
- 'keys', 'ca',
- 'cacert.pem'))
- return args
-
- # build command string
- # these tests are going to have to check
- # many combinations. we should inject some
- # params in the function call, to disable
- # some checks.
-
- def test_get_eip_gateway(self):
- self.write_sample_eipconfig()
- eipconf = eipconfig.EIPConfig(domain=self.provider)
-
- # default eipservice
- self.write_sample_eipservice()
- eipsconf = eipconfig.EIPServiceConfig(domain=self.provider)
-
- gateway = eipconfig.get_eip_gateway(
- eipconfig=eipconf,
- eipserviceconfig=eipsconf)
-
- # in spec is local gateway by default
- self.assertEqual(gateway, '127.0.0.1')
-
- # change eipservice
- # right now we only check that cluster == selected primary gw in
- # eip.json, and pick first matching ip
- eipconf._config.config['primary_gateway'] = "foo_provider"
- newgateways = [{"cluster": "foo_provider",
- "ip_address": "127.0.0.99"}]
- self.write_sample_eipservice(gateways=newgateways)
- eipsconf = eipconfig.EIPServiceConfig(domain=self.provider)
- # load from disk file
- eipsconf.load()
-
- gateway = eipconfig.get_eip_gateway(
- eipconfig=eipconf,
- eipserviceconfig=eipsconf)
- self.assertEqual(gateway, '127.0.0.99')
-
- # change eipservice, several gateways
- # right now we only check that cluster == selected primary gw in
- # eip.json, and pick first matching ip
- eipconf._config.config['primary_gateway'] = "bar_provider"
- newgateways = [{"cluster": "foo_provider",
- "ip_address": "127.0.0.99"},
- {'cluster': "bar_provider",
- "ip_address": "127.0.0.88"}]
- self.write_sample_eipservice(gateways=newgateways)
- eipsconf = eipconfig.EIPServiceConfig(domain=self.provider)
- # load from disk file
- eipsconf.load()
-
- gateway = eipconfig.get_eip_gateway(
- eipconfig=eipconf,
- eipserviceconfig=eipsconf)
- self.assertEqual(gateway, '127.0.0.88')
-
- def test_build_ovpn_command_empty_config(self):
- self.touch_exec()
- self.write_sample_eipservice()
- self.write_sample_eipconfig()
-
- from leap.eip import config as eipconfig
- from leap.util.fileutil import which
- path = os.environ['PATH']
- vpnbin = which('openvpn', path=path)
- #print 'path =', path
- #print 'vpnbin = ', vpnbin
- vpncommand, vpnargs = eipconfig.build_ovpn_command(
- do_pkexec_check=False, vpnbin=vpnbin,
- socket_path="/tmp/test.socket",
- provider=self.provider)
- self.assertEqual(vpncommand, self.home + '/bin/openvpn')
- self.assertEqual(vpnargs, self.get_expected_openvpn_args())
-
- def test_build_ovpn_command_openvpnoptions(self):
- self.touch_exec()
-
- from leap.eip import config as eipconfig
- from leap.util.fileutil import which
- path = os.environ['PATH']
- vpnbin = which('openvpn', path=path)
-
- self.write_sample_eipconfig()
-
- # regular run, everything normal
- self.write_sample_eipservice(vpnciphers=True)
- vpncommand, vpnargs = eipconfig.build_ovpn_command(
- do_pkexec_check=False, vpnbin=vpnbin,
- socket_path="/tmp/test.socket",
- provider=self.provider)
- self.assertEqual(vpncommand, self.home + '/bin/openvpn')
- expected = self.get_expected_openvpn_args(
- with_openvpn_ciphers=True)
- self.assertEqual(vpnargs, expected)
-
- # bad options -- illegal options
- self.write_sample_eipservice(
- vpnciphers=True,
- # WE ONLY ALLOW vpn options in auth, cipher, tls-cipher
- extra_vpnopts={"notallowedconfig": "badvalue"})
- vpncommand, vpnargs = eipconfig.build_ovpn_command(
- do_pkexec_check=False, vpnbin=vpnbin,
- socket_path="/tmp/test.socket",
- provider=self.provider)
- self.assertEqual(vpncommand, self.home + '/bin/openvpn')
- expected = self.get_expected_openvpn_args(
- with_openvpn_ciphers=True)
- self.assertEqual(vpnargs, expected)
-
- # bad options -- illegal chars
- self.write_sample_eipservice(
- vpnciphers=True,
- # WE ONLY ALLOW A-Z09\-
- extra_vpnopts={"cipher": "AES-128-CBC;FOOTHING"})
- vpncommand, vpnargs = eipconfig.build_ovpn_command(
- do_pkexec_check=False, vpnbin=vpnbin,
- socket_path="/tmp/test.socket",
- provider=self.provider)
- self.assertEqual(vpncommand, self.home + '/bin/openvpn')
- expected = self.get_expected_openvpn_args(
- with_openvpn_ciphers=True)
- self.assertEqual(vpnargs, expected)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/eip/tests/test_eipconnection.py b/src/leap/eip/tests/test_eipconnection.py
deleted file mode 100644
index 163f8d45..00000000
--- a/src/leap/eip/tests/test_eipconnection.py
+++ /dev/null
@@ -1,216 +0,0 @@
-import glob
-import logging
-import platform
-#import os
-import shutil
-
-logging.basicConfig()
-logger = logging.getLogger(name=__name__)
-
-try:
- import unittest2 as unittest
-except ImportError:
- import unittest
-
-from mock import Mock, patch # MagicMock
-
-from leap.eip.eipconnection import EIPConnection
-from leap.eip.exceptions import ConnectionRefusedError
-from leap.eip import specs as eipspecs
-from leap.testing.basetest import BaseLeapTest
-
-_system = platform.system()
-
-PROVIDER = "testprovider.example.org"
-
-
-class NotImplementedError(Exception):
- pass
-
-
-@patch('OpenVPNConnection._get_or_create_config')
-@patch('OpenVPNConnection._set_ovpn_command')
-class MockedEIPConnection(EIPConnection):
-
- def _set_ovpn_command(self):
- self.command = "mock_command"
- self.args = [1, 2, 3]
-
-
-class EIPConductorTest(BaseLeapTest):
-
- __name__ = "eip_conductor_tests"
- provider = PROVIDER
-
- def setUp(self):
- # XXX there's a conceptual/design
- # mistake here.
- # If we're testing just attrs after init,
- # init shold not be doing so much side effects.
-
- # for instance:
- # We have to TOUCH a keys file because
- # we're triggerig the key checks FROM
- # the constructor. me not like that,
- # key checker should better be called explicitelly.
-
- # XXX change to keys_checker invocation
- # (see config_checker)
-
- keyfiles = (eipspecs.provider_ca_path(domain=self.provider),
- eipspecs.client_cert_path(domain=self.provider))
- for filepath in keyfiles:
- self.touch(filepath)
- self.chmod600(filepath)
-
- # we init the manager with only
- # some methods mocked
- self.manager = Mock(name="openvpnmanager_mock")
- self.con = MockedEIPConnection()
- self.con.provider = self.provider
-
- # XXX watch out. This sometimes is throwing the following error:
- # NoSuchProcess: process no longer exists (pid=6571)
- # because of a bad implementation of _check_if_running_instance
-
- self.con.run_openvpn_checks()
-
- def tearDown(self):
- pass
-
- def doCleanups(self):
- super(BaseLeapTest, self).doCleanups()
- self.cleanupSocketDir()
- del self.con
-
- def cleanupSocketDir(self):
- ptt = ('/tmp/leap-tmp*')
- for tmpdir in glob.glob(ptt):
- shutil.rmtree(tmpdir)
-
- #
- # tests
- #
-
- def test_vpnconnection_defaults(self):
- """
- default attrs as expected
- """
- con = self.con
- self.assertEqual(con.autostart, True)
- # XXX moar!
-
- def test_ovpn_command(self):
- """
- set_ovpn_command called
- """
- self.assertEqual(self.con.command,
- "mock_command")
- self.assertEqual(self.con.args,
- [1, 2, 3])
-
- # config checks
-
- def test_config_checked_called(self):
- # XXX this single test is taking half of the time
- # needed to run tests. (roughly 3 secs for this only)
- # We should modularize and inject Mocks on more places.
-
- oldcon = self.con
- del(self.con)
- config_checker = Mock()
- self.con = MockedEIPConnection(config_checker=config_checker)
- self.assertTrue(config_checker.called)
- self.con.run_checks()
- self.con.config_checker.run_all.assert_called_with(
- skip_download=False)
-
- # XXX test for cert_checker also
- self.con = oldcon
-
- # connect/disconnect calls
-
- def test_disconnect(self):
- """
- disconnect method calls private and changes status
- """
- self.con._disconnect = Mock(
- name="_disconnect")
-
- # first we set status to connected
- self.con.status.set_current(self.con.status.CONNECTED)
- self.assertEqual(self.con.status.current,
- self.con.status.CONNECTED)
-
- # disconnect
- self.con.terminate_openvpn_connection = Mock()
- self.con.disconnect()
- self.con.terminate_openvpn_connection.assert_called_once_with(
- shutdown=False)
- self.con.terminate_openvpn_connection = Mock()
- self.con.disconnect(shutdown=True)
- self.con.terminate_openvpn_connection.assert_called_once_with(
- shutdown=True)
-
- # new status should be disconnected
- # XXX this should evolve and check no errors
- # during disconnection
- self.assertEqual(self.con.status.current,
- self.con.status.DISCONNECTED)
-
- def test_connect(self):
- """
- connect calls _launch_openvpn private
- """
- self.con._launch_openvpn = Mock()
- self.con.connect()
- self.con._launch_openvpn.assert_called_once_with()
-
- # XXX tests breaking here ...
-
- def test_good_poll_connection_state(self):
- """
- """
- #@patch --
- # self.manager.get_connection_state
-
- #XXX review this set of poll_state tests
- #they SHOULD NOT NEED TO MOCK ANYTHING IN THE
- #lower layers!! -- status, vpn_manager..
- #right now we're testing implementation, not
- #behavior!!!
- good_state = ["1345466946", "unknown_state", "ok",
- "192.168.1.1", "192.168.1.100"]
- self.con.get_connection_state = Mock(return_value=good_state)
- self.con.status.set_vpn_state = Mock()
-
- state = self.con.poll_connection_state()
- good_state[1] = "disconnected"
- final_state = tuple(good_state)
- self.con.status.set_vpn_state.assert_called_with("unknown_state")
- self.assertEqual(state, final_state)
-
- # TODO between "good" and "bad" (exception raised) cases,
- # we can still test for malformed states and see that only good
- # states do have a change (and from only the expected transition
- # states).
-
- def test_bad_poll_connection_state(self):
- """
- get connection state raises ConnectionRefusedError
- state is None
- """
- self.con.get_connection_state = Mock(
- side_effect=ConnectionRefusedError('foo!'))
- state = self.con.poll_connection_state()
- self.assertEqual(state, None)
-
-
- # XXX more things to test:
- # - called config routines during initz.
- # - raising proper exceptions with no config
- # - called proper checks on config / permissions
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/eip/tests/test_openvpnconnection.py b/src/leap/eip/tests/test_openvpnconnection.py
deleted file mode 100644
index 95bfb2f0..00000000
--- a/src/leap/eip/tests/test_openvpnconnection.py
+++ /dev/null
@@ -1,161 +0,0 @@
-import logging
-import os
-import platform
-import psutil
-import shutil
-#import socket
-
-logging.basicConfig()
-logger = logging.getLogger(name=__name__)
-
-try:
- import unittest2 as unittest
-except ImportError:
- import unittest
-
-from mock import Mock, patch # MagicMock
-
-from leap.eip import config as eipconfig
-from leap.eip import openvpnconnection
-from leap.eip import exceptions as eipexceptions
-from leap.eip.udstelnet import UDSTelnet
-from leap.testing.basetest import BaseLeapTest
-
-_system = platform.system()
-
-
-class NotImplementedError(Exception):
- pass
-
-
-mock_UDSTelnet = Mock(spec=UDSTelnet)
-# XXX cautious!!!
-# this might be fragile right now (counting a global
-# reference of calls I think.
-# investigate this other form instead:
-# http://www.voidspace.org.uk/python/mock/patch.html#start-and-stop
-
-# XXX redo after merge-refactor
-
-
-@patch('openvpnconnection.OpenVPNConnection.connect_to_management')
-class MockedOpenVPNConnection(openvpnconnection.OpenVPNConnection):
- def __init__(self, *args, **kwargs):
- self.mock_UDSTelnet = Mock()
- super(MockedOpenVPNConnection, self).__init__(
- *args, **kwargs)
- self.tn = self.mock_UDSTelnet(self.host, self.port)
-
- def connect_to_management(self):
- #print 'patched connect'
- self.tn = mock_UDSTelnet(self.host, port=self.port)
-
-
-class OpenVPNConnectionTest(BaseLeapTest):
-
- __name__ = "vpnconnection_tests"
-
- def setUp(self):
- # XXX this will have to change for win, host=localhost
- host = eipconfig.get_socket_path()
- self.host = host
- self.manager = MockedOpenVPNConnection(host=host)
-
- def tearDown(self):
- pass
-
- def doCleanups(self):
- super(BaseLeapTest, self).doCleanups()
- self.cleanupSocketDir()
-
- def cleanupSocketDir(self):
- # remove the socket folder.
- # XXX only if posix. in win, host is localhost, so nothing
- # has to be done.
- if self.host:
- folder, fpath = os.path.split(self.host)
- try:
- assert folder.startswith('/tmp/leap-tmp') # safety check
- shutil.rmtree(folder)
- except:
- self.fail("could not remove temp file")
-
- del self.manager
-
- #
- # tests
- #
-
- def test_detect_vpn(self):
- # XXX review, not sure if captured all the logic
- # while fixing. kali.
- openvpn_connection = openvpnconnection.OpenVPNConnection()
-
- with patch.object(psutil, "process_iter") as mocked_psutil:
- mocked_process = Mock()
- mocked_process.name = "openvpn"
- mocked_process.cmdline = ["openvpn", "-foo", "-bar", "-gaaz"]
- mocked_psutil.return_value = [mocked_process]
- with self.assertRaises(eipexceptions.OpenVPNAlreadyRunning):
- openvpn_connection._check_if_running_instance()
-
- openvpn_connection._check_if_running_instance()
-
- @unittest.skipIf(_system == "Windows", "lin/mac only")
- def test_lin_mac_default_init(self):
- """
- check default host for management iface
- """
- self.assertTrue(self.manager.host.startswith('/tmp/leap-tmp'))
- self.assertEqual(self.manager.port, 'unix')
-
- @unittest.skipUnless(_system == "Windows", "win only")
- def test_win_default_init(self):
- """
- check default host for management iface
- """
- # XXX should we make the platform specific switch
- # here or in the vpn command string building?
- self.assertEqual(self.manager.host, 'localhost')
- self.assertEqual(self.manager.port, 7777)
-
- def test_port_types_init(self):
- oldmanager = self.manager
- self.manager = MockedOpenVPNConnection(port="42")
- self.assertEqual(self.manager.port, 42)
- self.manager = MockedOpenVPNConnection()
- self.assertEqual(self.manager.port, "unix")
- self.manager = MockedOpenVPNConnection(port="bad")
- self.assertEqual(self.manager.port, None)
- self.manager = oldmanager
-
- def test_uds_telnet_called_on_connect(self):
- self.manager.connect_to_management()
- mock_UDSTelnet.assert_called_with(
- self.manager.host,
- port=self.manager.port)
-
- @unittest.skip
- def test_connect(self):
- raise NotImplementedError
- # XXX calls close
- # calls UDSTelnet mock.
-
- # XXX
- # tests to write:
- # UDSTelnetTest (for real?)
- # HAVE A LOOK AT CORE TESTS FOR TELNETLIB.
- # very illustrative instead...
-
- # - raise MissingSocket
- # - raise ConnectionRefusedError
- # - test send command
- # - tries connect
- # - ... tries?
- # - ... calls _seek_to_eof
- # - ... read_until --> return value
- # - ...
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/eip/udstelnet.py b/src/leap/eip/udstelnet.py
deleted file mode 100644
index 18e927c2..00000000
--- a/src/leap/eip/udstelnet.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import os
-import socket
-import telnetlib
-
-from leap.eip import exceptions as eip_exceptions
-
-
-class UDSTelnet(telnetlib.Telnet):
- """
- a telnet-alike class, that can listen
- on unix domain sockets
- """
-
- def open(self, host, port=23, timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
- """Connect to a host. If port is 'unix', it
- will open a connection over unix docmain sockets.
-
- The optional second argument is the port number, which
- defaults to the standard telnet port (23).
-
- Don't try to reopen an already connected instance.
- """
- self.eof = 0
- self.host = host
- self.port = port
- self.timeout = timeout
-
- if self.port == "unix":
- # unix sockets spoken
- if not os.path.exists(self.host):
- raise eip_exceptions.MissingSocketError
- self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- try:
- self.sock.connect(self.host)
- except socket.error:
- raise eip_exceptions.ConnectionRefusedError
- else:
- self.sock = socket.create_connection((host, port), timeout)
diff --git a/src/leap/gui/__init__.py b/src/leap/gui/__init__.py
index 804bfbc1..4b289442 100644
--- a/src/leap/gui/__init__.py
+++ b/src/leap/gui/__init__.py
@@ -1,11 +1,21 @@
-try:
- import sip
- sip.setapi('QString', 2)
- sip.setapi('QVariant', 2)
-except ValueError:
- pass
-
-import firstrun
-import firstrun.wizard
-
-__all__ = ['firstrun', 'firstrun.wizard']
+# -*- coding: utf-8 -*-
+# __init__.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+init file for leap.gui
+"""
+app = __import__("app", globals(), locals(), [], 2)
+__all__ = [app]
diff --git a/src/leap/gui/constants.py b/src/leap/gui/constants.py
deleted file mode 100644
index 07077293..00000000
--- a/src/leap/gui/constants.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import time
-
-APP_LOGO = ':/images/leap-color-small.png'
-APP_WATERMARK = ':/images/watermark.png'
-
-# bare is the username portion of a JID
-# full includes the "at" and some extra chars
-# that can be allowed for fqdn
-
-BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$"
-FULL_USERNAME_REGEX = r"^[A-Za-z\d_@.-]+$"
-
-GUI_PAUSE_FOR_USER_SECONDS = 1
-pause_for_user = lambda: time.sleep(GUI_PAUSE_FOR_USER_SECONDS)
diff --git a/src/leap/gui/firstrun/__init__.py b/src/leap/gui/firstrun/__init__.py
deleted file mode 100644
index d802fa1f..00000000
--- a/src/leap/gui/firstrun/__init__.py
+++ /dev/null
@@ -1,28 +0,0 @@
-try:
- import sip
- sip.setapi('QString', 2)
- sip.setapi('QVariant', 2)
-except ValueError:
- pass
-
-import intro
-import connect
-import last
-import login
-import mixins
-import providerinfo
-import providerselect
-import providersetup
-import register
-
-__all__ = [
- 'intro',
- 'connect',
- 'last',
- 'login',
- 'mixins',
- 'providerinfo',
- 'providerselect',
- 'providersetup',
- 'register',
-] # ,'wizard']
diff --git a/src/leap/gui/firstrun/connect.py b/src/leap/gui/firstrun/connect.py
deleted file mode 100644
index 209174a1..00000000
--- a/src/leap/gui/firstrun/connect.py
+++ /dev/null
@@ -1,218 +0,0 @@
-"""
-Provider Setup Validation Page,
-used in First Run Wizard
-"""
-import logging
-
-from PyQt4 import QtGui
-
-#import requests
-
-from leap.gui.progress import ValidationPage
-from leap.util.web import get_https_domain_and_port
-
-from leap.base import auth
-from leap.gui.constants import APP_LOGO, APP_WATERMARK
-
-logger = logging.getLogger(__name__)
-
-
-class ConnectionPage(ValidationPage):
-
- def __init__(self, parent=None):
- super(ConnectionPage, self).__init__(parent)
- self.current_page = "connect"
-
- title = self.tr("Connecting...")
- subtitle = self.tr("Setting up a encrypted "
- "connection with the provider")
-
- self.setTitle(title)
- self.setSubTitle(subtitle)
-
- self.setPixmap(
- QtGui.QWizard.WatermarkPixmap,
- QtGui.QPixmap(APP_WATERMARK))
-
- self.setPixmap(
- QtGui.QWizard.LogoPixmap,
- QtGui.QPixmap(APP_LOGO))
-
- def _do_checks(self, update_signal=None):
- """
- executes actual checks in a separate thread
-
- we initialize the srp protocol register
- and try to register user.
- """
- wizard = self.wizard()
- full_domain = self.field('provider_domain')
- domain, port = get_https_domain_and_port(full_domain)
-
- pconfig = wizard.eipconfigchecker(domain=domain)
- # this should be persisted...
- pconfig.defaultprovider.load()
- pconfig.set_api_domain()
-
- pCertChecker = wizard.providercertchecker(
- domain=domain)
- pCertChecker.set_api_domain(pconfig.apidomain)
-
- ###########################################
- # Set Credentials.
- # username and password are in different fields
- # if they were stored in log_in or sign_up pages.
- from_login = wizard.from_login
-
- unamek_base = 'userName'
- passwk_base = 'userPassword'
- unamek = 'login_%s' % unamek_base if from_login else unamek_base
- passwk = 'login_%s' % passwk_base if from_login else passwk_base
-
- username = self.field(unamek)
- password = self.field(passwk)
- credentials = username, password
-
- yield(("head_sentinel", 0), lambda: None)
-
- ##################################################
- # 1) fetching eip service config
- ##################################################
- def fetcheipconf():
- try:
- pconfig.fetch_eip_service_config()
-
- # XXX get specific exception
- except Exception as exc:
- return self.fail(exc.message)
-
- yield((self.tr("Getting EIP configuration files"), 40),
- fetcheipconf)
-
- ##################################################
- # 2) getting client certificate
- ##################################################
-
- def fetcheipcert():
- try:
- downloaded = pCertChecker.download_new_client_cert(
- credentials=credentials)
- if not downloaded:
- logger.error('Could not download client cert')
- return False
-
- except auth.SRPAuthenticationError as exc:
- return self.fail(self.tr(
- "Authentication error: %s" % exc.message))
-
- except Exception as exc:
- return self.fail(exc.message)
- else:
- return True
-
- yield((self.tr("Getting EIP certificate"), 80),
- fetcheipcert)
-
- ################
- # end !
- ################
- self.set_done()
- yield(("end_sentinel", 100), lambda: None)
-
- def on_checks_validation_ready(self):
- """
- called after _do_checks has finished
- (connected to checker thread finished signal)
- """
- # here we go! :)
- if self.is_done():
- nextbutton = self.wizard().button(QtGui.QWizard.NextButton)
- nextbutton.setFocus()
-
- full_domain = self.field('provider_domain')
- domain, port = get_https_domain_and_port(full_domain)
- _domain = u"%s:%s" % (
- domain, port) if port != 443 else unicode(domain)
- self.run_eip_checks_for_provider_and_connect(_domain)
-
- def run_eip_checks_for_provider_and_connect(self, domain):
- wizard = self.wizard()
- conductor = wizard.conductor
- start_eip_signal = getattr(
- wizard,
- 'start_eipconnection_signal', None)
-
- if conductor:
- conductor.set_provider_domain(domain)
- # we could run some of the checks to be
- # sure everything is in order, but
- # I see no point in doing it, we assume
- # we've gone thru all checks during the wizard.
- #conductor.run_checks()
- #self.conductor = conductor
- #errors = self.eip_error_check()
- #if not errors and start_eip_signal:
- if start_eip_signal:
- start_eip_signal.emit()
-
- else:
- logger.warning(
- "No conductor found. This means that "
- "probably the wizard has been launched "
- "in an stand-alone way.")
-
- self.set_done()
-
- #def eip_error_check(self):
- #"""
- #a version of the main app error checker,
- #but integrated within the connecting page of the wizard.
- #consumes the conductor error queue.
- #pops errors, and add those to the wizard page
- #"""
- # TODO handle errors.
- # We should redirect them to the log viewer
- # with a brief message.
- # XXX move to LAST PAGE instead.
- #logger.debug('eip error check from connecting page')
- #errq = self.conductor.error_queue
-
- #def _do_validation(self):
- #"""
- #called after _do_checks has finished
- #(connected to checker thread finished signal)
- #"""
- #from_login = self.wizard().from_login
- #prevpage = "login" if from_login else "signup"
-
- #wizard = self.wizard()
- #if self.errors:
- #logger.debug('going back with errors')
- #logger.error(self.errors)
- #name, first_error = self.pop_first_error()
- #wizard.set_validation_error(
- #prevpage,
- #first_error)
- #self.go_back()
-
- def nextId(self):
- wizard = self.wizard()
- return wizard.get_page_index('lastpage')
-
- def initializePage(self):
- super(ConnectionPage, self).initializePage()
- self.set_undone()
- cancelbutton = self.wizard().button(QtGui.QWizard.CancelButton)
- cancelbutton.hide()
- self.completeChanged.emit()
-
- wizard = self.wizard()
- eip_statuschange_signal = wizard.eip_statuschange_signal
- if eip_statuschange_signal:
- eip_statuschange_signal.connect(
- lambda status: self.send_status(
- status))
-
- def send_status(self, status):
- wizard = self.wizard()
- wizard.openvpn_status.append(status)
diff --git a/src/leap/gui/firstrun/constants.py b/src/leap/gui/firstrun/constants.py
deleted file mode 100644
index e69de29b..00000000
--- a/src/leap/gui/firstrun/constants.py
+++ /dev/null
diff --git a/src/leap/gui/firstrun/intro.py b/src/leap/gui/firstrun/intro.py
deleted file mode 100644
index 8e5014e6..00000000
--- a/src/leap/gui/firstrun/intro.py
+++ /dev/null
@@ -1,68 +0,0 @@
-"""
-Intro page used in first run wizard
-"""
-
-from PyQt4 import QtGui
-
-from leap.gui.constants import APP_LOGO, APP_WATERMARK
-
-
-class IntroPage(QtGui.QWizardPage):
- def __init__(self, parent=None):
- super(IntroPage, self).__init__(parent)
-
- self.setTitle(self.tr("First run wizard"))
-
- self.setPixmap(
- QtGui.QWizard.WatermarkPixmap,
- QtGui.QPixmap(APP_WATERMARK))
-
- self.setPixmap(
- QtGui.QWizard.LogoPixmap,
- QtGui.QPixmap(APP_LOGO))
-
- label = QtGui.QLabel(self.tr(
- "Now we will guide you through "
- "some configuration that is needed before you "
- "can connect for the first time.<br><br>"
- "If you ever need to modify these options again, "
- "you can find the wizard in the '<i>Settings</i>' menu from the "
- "main window.<br><br>"
- "Do you want to <b>sign up</b> for a new account, or <b>log "
- "in</b> with an already existing username?<br>"))
- label.setWordWrap(True)
-
- radiobuttonGroup = QtGui.QGroupBox()
-
- self.sign_up = QtGui.QRadioButton(
- self.tr("Sign up for a new account"))
- self.sign_up.setChecked(True)
- self.log_in = QtGui.QRadioButton(
- self.tr("Log In with my credentials"))
-
- radiobLayout = QtGui.QVBoxLayout()
- radiobLayout.addWidget(self.sign_up)
- radiobLayout.addWidget(self.log_in)
- radiobuttonGroup.setLayout(radiobLayout)
-
- layout = QtGui.QVBoxLayout()
- layout.addWidget(label)
- layout.addWidget(radiobuttonGroup)
- self.setLayout(layout)
-
- #self.registerField('is_signup', self.sign_up)
-
- def validatePage(self):
- return True
-
- def nextId(self):
- """
- returns next id
- in a non-linear wizard
- """
- if self.sign_up.isChecked():
- next_ = 'providerselection'
- if self.log_in.isChecked():
- next_ = 'login'
- wizard = self.wizard()
- return wizard.get_page_index(next_)
diff --git a/src/leap/gui/firstrun/last.py b/src/leap/gui/firstrun/last.py
deleted file mode 100644
index 6a01ba34..00000000
--- a/src/leap/gui/firstrun/last.py
+++ /dev/null
@@ -1,119 +0,0 @@
-"""
-Last Page, used in First Run Wizard
-"""
-import logging
-
-from PyQt4 import QtGui
-
-from leap.util.coroutines import coroutine
-from leap.gui.constants import APP_LOGO, APP_WATERMARK
-
-logger = logging.getLogger(__name__)
-
-
-class LastPage(QtGui.QWizardPage):
- def __init__(self, parent=None):
- super(LastPage, self).__init__(parent)
-
- self.setTitle(self.tr(
- "Connecting to Encrypted Internet Proxy service..."))
-
- self.setPixmap(
- QtGui.QWizard.WatermarkPixmap,
- QtGui.QPixmap(APP_WATERMARK))
-
- self.setPixmap(
- QtGui.QWizard.LogoPixmap,
- QtGui.QPixmap(APP_LOGO))
-
- self.label = QtGui.QLabel()
- self.label.setWordWrap(True)
-
- self.wizard_done = False
-
- # XXX REFACTOR to a Validating Page...
- self.status_line_1 = QtGui.QLabel()
- self.status_line_2 = QtGui.QLabel()
- self.status_line_3 = QtGui.QLabel()
- self.status_line_4 = QtGui.QLabel()
- self.status_line_5 = QtGui.QLabel()
-
- layout = QtGui.QVBoxLayout()
- layout.addWidget(self.label)
-
- # make loop
- layout.addWidget(self.status_line_1)
- layout.addWidget(self.status_line_2)
- layout.addWidget(self.status_line_3)
- layout.addWidget(self.status_line_4)
- layout.addWidget(self.status_line_5)
-
- self.setLayout(layout)
-
- def isComplete(self):
- return self.wizard_done
-
- def set_status_line(self, line, status):
- statusline = getattr(self, 'status_line_%s' % line)
- if statusline:
- statusline.setText(status)
-
- def set_finished_status(self):
- self.setTitle(self.tr('You are now using an encrypted connection!'))
- finishText = self.wizard().buttonText(
- QtGui.QWizard.FinishButton)
- finishText = finishText.replace('&', '')
- self.label.setText(self.tr(
- "Click '<i>%s</i>' to end the wizard and "
- "save your settings." % finishText))
- self.wizard_done = True
- self.completeChanged.emit()
-
- @coroutine
- def eip_status_handler(self):
- # XXX this can be changed to use
- # signals. See progress.py
- logger.debug('logging status in last page')
- self.validation_done = False
- status_count = 1
- try:
- while True:
- status = (yield)
- status_count += 1
- # XXX add to line...
- logger.debug('status --> %s', status)
- self.set_status_line(status_count, status)
- if status == "connected":
- self.set_finished_status()
- self.completeChanged.emit()
- break
- self.completeChanged.emit()
- except GeneratorExit:
- pass
- except StopIteration:
- pass
-
- def initializePage(self):
- super(LastPage, self).initializePage()
- wizard = self.wizard()
- wizard.button(QtGui.QWizard.FinishButton).setDisabled(True)
-
- handler = self.eip_status_handler()
-
- # get statuses done in prev page
- for st in wizard.openvpn_status:
- self.send_status(handler.send, st)
-
- # bind signal for events yet to come
- eip_statuschange_signal = wizard.eip_statuschange_signal
- if eip_statuschange_signal:
- eip_statuschange_signal.connect(
- lambda status: self.send_status(
- handler.send, status))
- self.completeChanged.emit()
-
- def send_status(self, cb, status):
- try:
- cb(status)
- except StopIteration:
- pass
diff --git a/src/leap/gui/firstrun/login.py b/src/leap/gui/firstrun/login.py
deleted file mode 100644
index 409cb9bf..00000000
--- a/src/leap/gui/firstrun/login.py
+++ /dev/null
@@ -1,341 +0,0 @@
-"""
-LogIn Page, used inf First Run Wizard
-"""
-import logging
-
-from PyQt4 import QtCore
-from PyQt4 import QtGui
-
-import requests
-
-from leap.base import auth
-from leap.gui.firstrun.mixins import UserFormMixIn
-from leap.gui.progress import InlineValidationPage
-from leap.gui import styles
-
-from leap.gui.constants import APP_LOGO, APP_WATERMARK, FULL_USERNAME_REGEX
-
-logger = logging.getLogger(__name__)
-
-
-class LogInPage(InlineValidationPage, UserFormMixIn): # InlineValidationPage
-
- def __init__(self, parent=None):
-
- super(LogInPage, self).__init__(parent)
- self.current_page = "login"
-
- self.setTitle(self.tr("Log In"))
- self.setSubTitle(self.tr("Log in with your credentials"))
- self.current_page = "login"
-
- self.setPixmap(
- QtGui.QWizard.WatermarkPixmap,
- QtGui.QPixmap(APP_WATERMARK))
-
- self.setPixmap(
- QtGui.QWizard.LogoPixmap,
- QtGui.QPixmap(APP_LOGO))
-
- self.setupSteps()
- self.setupUI()
-
- self.do_confirm_next = False
-
- def setupUI(self):
- userNameLabel = QtGui.QLabel(self.tr("User &name:"))
- userNameLineEdit = QtGui.QLineEdit()
- userNameLineEdit.cursorPositionChanged.connect(
- self.reset_validation_status)
- userNameLabel.setBuddy(userNameLineEdit)
-
- # let's add regex validator
- usernameRe = QtCore.QRegExp(FULL_USERNAME_REGEX)
- userNameLineEdit.setValidator(
- QtGui.QRegExpValidator(usernameRe, self))
-
- #userNameLineEdit.setPlaceholderText(
- #'username@provider.example.org')
- self.userNameLineEdit = userNameLineEdit
-
- userPasswordLabel = QtGui.QLabel(self.tr("&Password:"))
- self.userPasswordLineEdit = QtGui.QLineEdit()
- self.userPasswordLineEdit.setEchoMode(
- QtGui.QLineEdit.Password)
- userPasswordLabel.setBuddy(self.userPasswordLineEdit)
-
- self.registerField('login_userName*', self.userNameLineEdit)
- self.registerField('login_userPassword*', self.userPasswordLineEdit)
-
- layout = QtGui.QGridLayout()
- layout.setColumnMinimumWidth(0, 20)
-
- validationMsg = QtGui.QLabel("")
- validationMsg.setStyleSheet(styles.ErrorLabelStyleSheet)
- self.validationMsg = validationMsg
-
- layout.addWidget(validationMsg, 0, 3)
- layout.addWidget(userNameLabel, 1, 0)
- layout.addWidget(self.userNameLineEdit, 1, 3)
- layout.addWidget(userPasswordLabel, 2, 0)
- layout.addWidget(self.userPasswordLineEdit, 2, 3)
-
- # add validation frame
- self.setupValidationFrame()
- layout.addWidget(self.valFrame, 4, 2, 4, 2)
- self.valFrame.hide()
-
- self.nextText(self.tr("Log in"))
- self.setLayout(layout)
-
- #self.registerField('is_login_wizard')
-
- # actual checks
-
- def _do_checks(self):
-
- full_username = self.userNameLineEdit.text()
- ###########################
- # 0) check user@domain form
- ###########################
-
- def checkusername():
- if full_username.count('@') != 1:
- return self.fail(
- self.tr(
- "Username must be in the username@provider form."))
- else:
- return True
-
- yield(("head_sentinel", 0), checkusername)
-
- username, domain = full_username.split('@')
- password = self.userPasswordLineEdit.text()
-
- # We try a call to an authenticated
- # page here as a mean to catch
- # srp authentication errors while
- wizard = self.wizard()
- pconfig = wizard.eipconfigchecker(domain=domain)
- pconfig.defaultprovider.load()
- pconfig.set_api_domain()
-
- ########################
- # 1) try name resolution
- ########################
- # show the frame before going on...
- QtCore.QMetaObject.invokeMethod(
- self, "showStepsFrame")
-
- # Able to contact domain?
- # can get definition?
- # two-by-one
- def resolvedomain():
- try:
- pconfig.fetch_definition(domain=domain)
-
- # we're using requests here for all
- # the possible error cases that it catches.
- except requests.exceptions.ConnectionError as exc:
- return self.fail(exc.message[1])
- except requests.exceptions.HTTPError as exc:
- return self.fail(exc.message)
- except Exception as exc:
- # XXX get catchall error msg
- return self.fail(
- exc.message)
- else:
- return True
-
- yield((self.tr("Resolving domain name"), 20), resolvedomain)
-
- wizard.set_providerconfig(
- pconfig.defaultprovider.config)
-
- ########################
- # 2) do authentication
- ########################
- credentials = username, password
- pCertChecker = wizard.providercertchecker(
- domain=domain,
- apidomain=pconfig.get_api_domain())
-
- def validate_credentials():
-
- try:
- pCertChecker.download_new_client_cert(
- credentials=credentials)
-
- except auth.SRPAuthenticationError as exc:
- logger.debug('srp auth error in validate credentials step')
- return self.fail(
- self.tr("Authentication error: %s" % exc.message))
-
- except Exception as exc:
- logger.error('unknown error in validate credentials step')
- return self.fail(exc.message)
-
- else:
- return True
-
- yield(('Validating credentials', 60), validate_credentials)
-
- self.set_done()
- yield(("end_sentinel", 100), lambda: None)
-
- def green_validation_status(self):
- val = self.validationMsg
- val.setText(self.tr('Credentials validated.'))
- val.setStyleSheet(styles.GreenLineEdit)
-
- def on_checks_validation_ready(self):
- """
- after checks
- """
- if self.is_done():
- self.disableFields()
- self.cleanup_errormsg()
- self.clean_wizard_errors(self.current_page)
- # make the user confirm the transition
- # to next page.
- self.nextText('&Next')
- self.nextFocus()
- self.green_validation_status()
- self.do_confirm_next = True
-
- # ui update
-
- def nextText(self, text):
- self.setButtonText(
- QtGui.QWizard.NextButton, text)
-
- def nextFocus(self):
- self.wizard().button(
- QtGui.QWizard.NextButton).setFocus()
-
- def disableNextButton(self):
- self.wizard().button(
- QtGui.QWizard.NextButton).setDisabled(True)
-
- def onUserNamePositionChanged(self, *args):
- if self.initial_username_sample:
- self.userNameLineEdit.setText('')
- # XXX set regular color
- self.initial_username_sample = None
-
- def onUserNameTextChanged(self, *args):
- if self.initial_username_sample:
- k = args[0][-1]
- self.initial_username_sample = None
- self.userNameLineEdit.setText(k)
-
- def disableFields(self):
- for field in (self.userNameLineEdit,
- self.userPasswordLineEdit):
- field.setDisabled(True)
-
- def populateErrors(self):
- # XXX could move this to ValidationMixin
- # used in providerselect and register too
-
- errors = self.wizard().get_validation_error(
- self.current_page)
- showerr = self.validationMsg.setText
-
- if errors:
- bad_str = getattr(self, 'bad_string', None)
- cur_str = self.userNameLineEdit.text()
-
- if bad_str is None:
- # first time we fall here.
- # save the current bad_string value
- self.bad_string = cur_str
- showerr(errors)
- else:
- # not the first time
- if cur_str == bad_str:
- showerr(errors)
- else:
- self.focused_field = False
- showerr('')
-
- def cleanup_errormsg(self):
- """
- we reset bad_string to None
- should be called before leaving the page
- """
- self.bad_string = None
-
- def paintEvent(self, event):
- """
- we hook our populate errors
- on paintEvent because we need it to catch
- when user enters the page coming from next,
- and initializePage does not cover that case.
- Maybe there's a better event to hook upon.
- """
- super(LogInPage, self).paintEvent(event)
- self.populateErrors()
-
- def set_prevalidation_error(self, error):
- self.prevalidation_error = error
-
- # pagewizard methods
-
- def nextId(self):
- wizard = self.wizard()
- if not wizard:
- return
- if wizard.is_provider_setup is False:
- next_ = 'providersetupvalidation'
- if wizard.is_provider_setup is True:
- # XXX bad name, ok, gonna change that
- next_ = 'signupvalidation'
- return wizard.get_page_index(next_)
-
- def initializePage(self):
- super(LogInPage, self).initializePage()
- username = self.userNameLineEdit
- username.setText('username@provider.example.org')
- username.cursorPositionChanged.connect(
- self.onUserNamePositionChanged)
- username.textChanged.connect(
- self.onUserNameTextChanged)
- self.initial_username_sample = True
- self.validationMsg.setText('')
- self.valFrame.hide()
-
- def reset_validation_status(self):
- """
- empty the validation msg
- and clean the inline validation widget.
- """
- self.validationMsg.setText('')
- self.steps.removeAllSteps()
- self.clearTable()
-
- def validatePage(self):
- """
- if not register done, do checks.
- if done, wait for click.
- """
- self.disableNextButton()
- self.cleanup_errormsg()
- self.clean_wizard_errors(self.current_page)
-
- if self.do_confirm_next:
- full_username = self.userNameLineEdit.text()
- password = self.userPasswordLineEdit.text()
- username, domain = full_username.split('@')
- self.setField('provider_domain', domain)
- self.setField('login_userName', username)
- self.setField('login_userPassword', password)
- self.wizard().from_login = True
-
- return True
-
- if not self.is_done():
- self.reset_validation_status()
- self.do_checks()
-
- return self.is_done()
diff --git a/src/leap/gui/firstrun/mixins.py b/src/leap/gui/firstrun/mixins.py
deleted file mode 100644
index c4731893..00000000
--- a/src/leap/gui/firstrun/mixins.py
+++ /dev/null
@@ -1,18 +0,0 @@
-"""
-mixins used in First Run Wizard
-"""
-
-
-class UserFormMixIn(object):
-
- def reset_validation_status(self):
- """
- empty the validation msg
- """
- self.validationMsg.setText('')
-
- def set_validation_status(self, msg):
- """
- set generic validation status
- """
- self.validationMsg.setText(msg)
diff --git a/src/leap/gui/firstrun/providerinfo.py b/src/leap/gui/firstrun/providerinfo.py
deleted file mode 100644
index 3385e9e7..00000000
--- a/src/leap/gui/firstrun/providerinfo.py
+++ /dev/null
@@ -1,110 +0,0 @@
-"""
-Provider Info Page, used in First run Wizard
-"""
-import logging
-
-from PyQt4 import QtGui
-
-from leap.gui.constants import APP_LOGO, APP_WATERMARK
-from leap.util.translations import translate
-
-logger = logging.getLogger(__name__)
-
-
-class ProviderInfoPage(QtGui.QWizardPage):
-
- def __init__(self, parent=None):
- super(ProviderInfoPage, self).__init__(parent)
-
- self.setTitle(self.tr("Provider Information"))
- self.setSubTitle(self.tr(
- "Services offered by this provider"))
-
- self.setPixmap(
- QtGui.QWizard.WatermarkPixmap,
- QtGui.QPixmap(APP_WATERMARK))
-
- self.setPixmap(
- QtGui.QWizard.LogoPixmap,
- QtGui.QPixmap(APP_LOGO))
-
- self.create_info_panel()
-
- def create_info_panel(self):
- # Use stacked widget instead
- # of reparenting the layout.
-
- infoWidget = QtGui.QStackedWidget()
-
- info = QtGui.QWidget()
- layout = QtGui.QVBoxLayout()
-
- displayName = QtGui.QLabel("")
- description = QtGui.QLabel("")
- enrollment_policy = QtGui.QLabel("")
-
- # XXX set stylesheet...
- # prettify a little bit.
- # bigger fonts and so on...
-
- # We could use a QFrame here
-
- layout.addWidget(displayName)
- layout.addWidget(description)
- layout.addWidget(enrollment_policy)
- layout.addStretch(1)
-
- info.setLayout(layout)
- infoWidget.addWidget(info)
-
- pageLayout = QtGui.QVBoxLayout()
- pageLayout.addWidget(infoWidget)
- self.setLayout(pageLayout)
-
- # add refs to self to allow for
- # updates.
- # Watch out! Have to get rid of these references!
- # this should be better handled with signals !!
- self.displayName = displayName
- self.description = description
- self.description.setWordWrap(True)
- self.enrollment_policy = enrollment_policy
-
- def show_provider_info(self):
-
- # XXX get multilingual objects
- # directly from the config object
-
- lang = "en"
- pconfig = self.wizard().providerconfig
-
- dn = pconfig.get('name')
- display_name = dn[lang] if dn else ''
- domain_name = self.field('provider_domain')
-
- self.displayName.setText(
- "<b>%s</b> https://%s" % (display_name, domain_name))
-
- desc = pconfig.get('description')
-
- #description_text = desc[lang] if desc else ''
- description_text = translate(desc) if desc else ''
-
- self.description.setText(
- "<i>%s</i>" % description_text)
-
- # XXX should translate this...
- enroll = pconfig.get('enrollment_policy')
- if enroll:
- self.enrollment_policy.setText(
- '<b>%s</b>: <em>%s</em>' % (
- self.tr('enrollment policy'),
- enroll))
-
- def nextId(self):
- wizard = self.wizard()
- next_ = "providersetupvalidation"
- return wizard.get_page_index(next_)
-
- def initializePage(self):
- self.show_provider_info()
diff --git a/src/leap/gui/firstrun/providerselect.py b/src/leap/gui/firstrun/providerselect.py
deleted file mode 100644
index 36bb4510..00000000
--- a/src/leap/gui/firstrun/providerselect.py
+++ /dev/null
@@ -1,475 +0,0 @@
-"""
-Select Provider Page, used in First Run Wizard
-"""
-import logging
-
-import requests
-
-from PyQt4 import QtCore
-from PyQt4 import QtGui
-
-from leap.base import exceptions as baseexceptions
-#from leap.crypto import certs
-from leap.eip import exceptions as eipexceptions
-from leap.gui.progress import InlineValidationPage
-from leap.gui import styles
-from leap.gui.utils import delay
-from leap.util.web import get_https_domain_and_port
-
-from leap.gui.constants import APP_LOGO, APP_WATERMARK
-
-logger = logging.getLogger(__name__)
-
-
-class SelectProviderPage(InlineValidationPage):
-
- launchChecks = QtCore.pyqtSignal()
-
- def __init__(self, parent=None, providers=None):
- super(SelectProviderPage, self).__init__(parent)
- self.current_page = 'providerselection'
-
- self.setTitle(self.tr("Enter Provider"))
- self.setSubTitle(self.tr(
- "Please enter the domain of the provider you want "
- "to use for your connection")
- )
- self.setPixmap(
- QtGui.QWizard.WatermarkPixmap,
- QtGui.QPixmap(APP_WATERMARK))
-
- self.setPixmap(
- QtGui.QWizard.LogoPixmap,
- QtGui.QPixmap(APP_LOGO))
-
- self.did_cert_check = False
-
- self.done = False
-
- self.setupSteps()
- self.setupUI()
-
- self.launchChecks.connect(
- self.launch_checks)
-
- self.providerNameEdit.editingFinished.connect(
- lambda: self.providerCheckButton.setFocus(True))
-
- def setupUI(self):
- """
- initializes the UI
- """
- providerNameLabel = QtGui.QLabel("h&ttps://")
- # note that we expect the bare domain name
- # we will add the scheme later
- providerNameEdit = QtGui.QLineEdit()
- providerNameEdit.cursorPositionChanged.connect(
- self.reset_validation_status)
- providerNameLabel.setBuddy(providerNameEdit)
-
- # add regex validator
- providerDomainRe = QtCore.QRegExp(r"^[a-z1-9_\-\.]+$")
- providerNameEdit.setValidator(
- QtGui.QRegExpValidator(providerDomainRe, self))
- self.providerNameEdit = providerNameEdit
-
- # Eventually we will seed a list of
- # well known providers here.
-
- #providercombo = QtGui.QComboBox()
- #if providers:
- #for provider in providers:
- #providercombo.addItem(provider)
- #providerNameSelect = providercombo
-
- self.registerField("provider_domain*", self.providerNameEdit)
- #self.registerField('provider_name_index', providerNameSelect)
-
- validationMsg = QtGui.QLabel("")
- validationMsg.setStyleSheet(styles.ErrorLabelStyleSheet)
- self.validationMsg = validationMsg
- providerCheckButton = QtGui.QPushButton(self.tr("chec&k!"))
- self.providerCheckButton = providerCheckButton
-
- # cert info
-
- # this is used in the callback
- # for the checkbox changes.
- # tricky, since the first time came
- # from the exception message.
- # should get string from exception too!
- self.bad_cert_status = self.tr(
- "Server certificate could not be verified.")
-
- self.certInfo = QtGui.QLabel("")
- self.certInfo.setWordWrap(True)
- self.certWarning = QtGui.QLabel("")
- self.trustProviderCertCheckBox = QtGui.QCheckBox(
- self.tr("&Trust this provider certificate."))
-
- self.trustProviderCertCheckBox.stateChanged.connect(
- self.onTrustCheckChanged)
- self.providerNameEdit.textChanged.connect(
- self.onProviderChanged)
- self.providerCheckButton.clicked.connect(
- self.onCheckButtonClicked)
-
- layout = QtGui.QGridLayout()
- layout.addWidget(validationMsg, 0, 2)
- layout.addWidget(providerNameLabel, 1, 1)
- layout.addWidget(providerNameEdit, 1, 2)
- layout.addWidget(providerCheckButton, 1, 3)
-
- # add certinfo group
- # XXX not shown now. should move to validation box.
- #layout.addWidget(certinfoGroup, 4, 1, 4, 2)
- #self.certinfoGroup = certinfoGroup
- #self.certinfoGroup.hide()
-
- # add validation frame
- self.setupValidationFrame()
- layout.addWidget(self.valFrame, 4, 2, 4, 2)
- self.valFrame.hide()
-
- self.setLayout(layout)
-
- # certinfo
-
- def setupCertInfoGroup(self): # pragma: no cover
- # XXX not used now.
- certinfoGroup = QtGui.QGroupBox(
- self.tr("Certificate validation"))
- certinfoLayout = QtGui.QVBoxLayout()
- certinfoLayout.addWidget(self.certInfo)
- certinfoLayout.addWidget(self.certWarning)
- certinfoLayout.addWidget(self.trustProviderCertCheckBox)
- certinfoGroup.setLayout(certinfoLayout)
- self.certinfoGroup = self.certinfoGroup
-
- # progress frame
-
- def setupValidationFrame(self):
- qframe = QtGui.QFrame
- valFrame = qframe()
- valFrame.setFrameStyle(qframe.NoFrame)
- valframeLayout = QtGui.QVBoxLayout()
- zeros = (0, 0, 0, 0)
- valframeLayout.setContentsMargins(*zeros)
-
- valframeLayout.addWidget(self.stepsTableWidget)
- valFrame.setLayout(valframeLayout)
- self.valFrame = valFrame
-
- @QtCore.pyqtSlot()
- def onDisableCheckButton(self):
- #print 'CHECK BUTTON DISABLED!!!'
- self.providerCheckButton.setDisabled(True)
-
- @QtCore.pyqtSlot()
- def launch_checks(self):
- self.do_checks()
-
- def onCheckButtonClicked(self):
- QtCore.QMetaObject.invokeMethod(
- self, "onDisableCheckButton")
-
- QtCore.QMetaObject.invokeMethod(
- self, "showStepsFrame")
-
- delay(self, "launch_checks")
-
- def _do_checks(self):
- """
- generator that yields actual checks
- that are executed in a separate thread
- """
-
- wizard = self.wizard()
- full_domain = self.providerNameEdit.text()
-
- # we check if we have a port in the domain string.
- domain, port = get_https_domain_and_port(full_domain)
- _domain = u"%s:%s" % (domain, port) if port != 443 else unicode(domain)
-
- netchecker = wizard.netchecker()
- providercertchecker = wizard.providercertchecker()
- eipconfigchecker = wizard.eipconfigchecker(domain=_domain)
-
- yield(("head_sentinel", 0), lambda: None)
-
- ########################
- # 1) try name resolution
- ########################
-
- def namecheck():
- """
- in which we check if
- we are able to name resolve
- this domain
- """
- try:
- #import ipdb;ipdb.set_trace()
- netchecker.check_name_resolution(
- domain)
-
- except baseexceptions.LeapException as exc:
- logger.error(exc.message)
- return self.fail(exc.usermessage)
-
- except Exception as exc:
- return self.fail(exc.message)
-
- else:
- return True
-
- logger.debug('checking name resolution')
- yield((self.tr("Checking if it is a valid provider"), 20), namecheck)
-
- #########################
- # 2) try https connection
- #########################
-
- def httpscheck():
- """
- in which we check
- if the provider
- is offering service over
- https
- """
- try:
- providercertchecker.is_https_working(
- "https://%s" % _domain,
- verify=True)
-
- except eipexceptions.HttpsBadCertError as exc:
- logger.debug('exception')
- return self.fail(exc.usermessage)
- # XXX skipping for now...
- ##############################################
- # We had this validation logic
- # in the provider selection page before
- ##############################################
- #if self.trustProviderCertCheckBox.isChecked():
- #pass
- #else:
- #fingerprint = certs.get_cert_fingerprint(
- #domain=domain, sep=" ")
-
- # it's ok if we've trusted this fgprt before
- #trustedcrts = wizard.trusted_certs
- #if trustedcrts and \
- # fingerprint.replace(' ', '') in trustedcrts:
- #pass
- #else:
- # let your user face panick :P
- #self.add_cert_info(fingerprint)
- #self.did_cert_check = True
- #self.completeChanged.emit()
- #return False
-
- except baseexceptions.LeapException as exc:
- return self.fail(exc.usermessage)
-
- except Exception as exc:
- return self.fail(exc.message)
-
- else:
- return True
-
- logger.debug('checking https connection')
- yield((self.tr("Checking for a secure connection"), 40), httpscheck)
-
- ##################################
- # 3) try download provider info...
- ##################################
-
- def fetchinfo():
- try:
- # XXX we already set _domain in the initialization
- # so it should not be needed here.
- eipconfigchecker.fetch_definition(domain=_domain)
- wizard.set_providerconfig(
- eipconfigchecker.defaultprovider.config)
- except requests.exceptions.SSLError:
- return self.fail(self.tr(
- "Could not get info from provider."))
- except requests.exceptions.ConnectionError:
- return self.fail(self.tr(
- "Could not download provider info "
- "(refused conn.)."))
-
- except Exception as exc:
- return self.fail(
- self.tr(exc.message))
- else:
- return True
-
- yield((self.tr("Getting info from the provider"), 80), fetchinfo)
-
- # done!
-
- self.done = True
- yield(("end_sentinel", 100), lambda: None)
-
- def on_checks_validation_ready(self):
- """
- called after _do_checks has finished.
- """
- self.domain_checked = True
- self.completeChanged.emit()
- # let's set focus...
- if self.is_done():
- self.wizard().clean_validation_error(self.current_page)
- nextbutton = self.wizard().button(QtGui.QWizard.NextButton)
- nextbutton.setFocus()
- else:
- self.providerNameEdit.setFocus()
-
- # cert trust verification
- # (disabled for now)
-
- def is_insecure_cert_trusted(self):
- return self.trustProviderCertCheckBox.isChecked()
-
- def onTrustCheckChanged(self, state): # pragma: no cover XXX
- checked = False
- if state == 2:
- checked = True
-
- if checked:
- self.reset_validation_status()
- else:
- self.set_validation_status(self.bad_cert_status)
-
- # trigger signal to redraw next button
- self.completeChanged.emit()
-
- def add_cert_info(self, certinfo): # pragma: no cover XXX
- self.certWarning.setText(
- self.tr("Do you want to <b>trust this provider certificate?</b>"))
- # XXX Check if this needs to abstracted to remove certinfo
- self.certInfo.setText(
- self.tr('SHA-256 fingerprint: <i>%s</i><br>' % certinfo))
- self.certInfo.setWordWrap(True)
- self.certinfoGroup.show()
-
- def onProviderChanged(self, text):
- self.done = False
- provider = self.providerNameEdit.text()
- if provider:
- self.providerCheckButton.setDisabled(False)
- else:
- self.providerCheckButton.setDisabled(True)
- self.completeChanged.emit()
-
- def reset_validation_status(self):
- """
- empty the validation msg
- and clean the inline validation widget.
- """
- self.validationMsg.setText('')
- self.steps.removeAllSteps()
- self.clearTable()
- self.domain_checked = False
-
- # pagewizard methods
-
- def isComplete(self):
- provider = self.providerNameEdit.text()
-
- if not self.is_done():
- return False
-
- if not provider:
- return False
- else:
- if self.is_insecure_cert_trusted():
- return True
- if not self.did_cert_check:
- if self.is_done():
- # XXX sure?
- return True
- return False
-
- def populateErrors(self):
- # XXX could move this to ValidationMixin
- # with some defaults for the validating fields
- # (now it only allows one field, manually specified)
-
- #logger.debug('getting errors')
- errors = self.wizard().get_validation_error(
- self.current_page)
- if errors:
- bad_str = getattr(self, 'bad_string', None)
- cur_str = self.providerNameEdit.text()
- showerr = self.validationMsg.setText
- markred = lambda: self.providerNameEdit.setStyleSheet(
- styles.ErrorLineEdit)
- umarkrd = lambda: self.providerNameEdit.setStyleSheet(
- styles.RegularLineEdit)
- if bad_str is None:
- # first time we fall here.
- # save the current bad_string value
- self.bad_string = cur_str
- showerr(errors)
- markred()
- else:
- # not the first time
- # XXX hey, this is getting convoluted.
- # roll out this.
- # but be careful about all the possibilities
- # with going back and forth once you
- # enter a domain.
- if cur_str == bad_str:
- showerr(errors)
- markred()
- else:
- if not getattr(self, 'domain_checked', None):
- showerr('')
- umarkrd()
- else:
- self.bad_string = cur_str
- showerr(errors)
-
- def cleanup_errormsg(self):
- """
- we reset bad_string to None
- should be called before leaving the page
- """
- self.bad_string = None
- self.domain_checked = False
-
- def paintEvent(self, event):
- """
- we hook our populate errors
- on paintEvent because we need it to catch
- when user enters the page coming from next,
- and initializePage does not cover that case.
- Maybe there's a better event to hook upon.
- """
- super(SelectProviderPage, self).paintEvent(event)
- self.populateErrors()
-
- def initializePage(self):
- self.validationMsg.setText('')
- if hasattr(self, 'certinfoGroup'):
- # XXX remove ?
- self.certinfoGroup.hide()
- self.done = False
- self.providerCheckButton.setDisabled(True)
- self.valFrame.hide()
- self.steps.removeAllSteps()
- self.clearTable()
-
- def validatePage(self):
- # some cleanup before we leave the page
- self.cleanup_errormsg()
-
- # go
- return True
-
- def nextId(self):
- wizard = self.wizard()
- if not wizard:
- return
- return wizard.get_page_index('providerinfo')
diff --git a/src/leap/gui/firstrun/providersetup.py b/src/leap/gui/firstrun/providersetup.py
deleted file mode 100644
index 40a14048..00000000
--- a/src/leap/gui/firstrun/providersetup.py
+++ /dev/null
@@ -1,161 +0,0 @@
-"""
-Provider Setup Validation Page,
-used if First Run Wizard
-"""
-import logging
-
-import requests
-
-from PyQt4 import QtGui
-
-from leap.base import exceptions as baseexceptions
-from leap.gui.progress import ValidationPage
-
-from leap.gui.constants import APP_LOGO, APP_WATERMARK
-
-logger = logging.getLogger(__name__)
-
-
-class ProviderSetupValidationPage(ValidationPage):
- def __init__(self, parent=None):
- super(ProviderSetupValidationPage, self).__init__(parent)
- self.current_page = "providersetupvalidation"
-
- # XXX needed anymore?
- #is_signup = self.field("is_signup")
- #self.is_signup = is_signup
-
- self.setTitle(self.tr("Provider setup"))
- self.setSubTitle(
- self.tr("Gathering configuration options for this provider"))
-
- self.setPixmap(
- QtGui.QWizard.WatermarkPixmap,
- QtGui.QPixmap(APP_WATERMARK))
-
- self.setPixmap(
- QtGui.QWizard.LogoPixmap,
- QtGui.QPixmap(APP_LOGO))
-
- def _do_checks(self):
- """
- generator that yields actual checks
- that are executed in a separate thread
- """
-
- full_domain = self.field('provider_domain')
- wizard = self.wizard()
- pconfig = wizard.providerconfig
-
- #pCertChecker = wizard.providercertchecker
- #certchecker = pCertChecker(domain=full_domain)
- pCertChecker = wizard.providercertchecker(
- domain=full_domain)
-
- yield(("head_sentinel", 0), lambda: None)
-
- ########################
- # 1) fetch ca cert
- ########################
-
- def fetchcacert():
- if pconfig:
- ca_cert_uri = pconfig.get('ca_cert_uri').geturl()
- else:
- ca_cert_uri = None
-
- # XXX check scheme == "https"
- # XXX passing verify == False because
- # we have trusted right before.
- # We should check it's the same domain!!!
- # (Check with the trusted fingerprints dict
- # or something smart)
- try:
- pCertChecker.download_ca_cert(
- uri=ca_cert_uri,
- verify=False)
-
- except baseexceptions.LeapException as exc:
- logger.error(exc.message)
- # XXX this should be _ method
- return self.fail(self.tr(exc.usermessage))
-
- except Exception as exc:
- return self.fail(exc.message)
-
- else:
- return True
-
- yield((self.tr('Fetching CA certificate'), 30),
- fetchcacert)
-
- #########################
- # 2) check CA fingerprint
- #########################
-
- def checkcafingerprint():
- # XXX get the real thing!!!
- pass
- #ca_cert_fingerprint = pconfig.get('ca_cert_fingerprint', None)
-
- # XXX get fingerprint dict (types)
- #sha256_fpr = ca_cert_fingerprint.split('=')[1]
-
- #validate_fpr = pCertChecker.check_ca_cert_fingerprint(
- #fingerprint=sha256_fpr)
- #if not validate_fpr:
- # XXX update validationMsg
- # should catch exception
- #return False
-
- yield((self.tr("Checking CA fingerprint"), 60),
- checkcafingerprint)
-
- #########################
- # 2) check CA fingerprint
- #########################
-
- def validatecacert():
- api_uri = pconfig.get('api_uri', None)
- try:
- pCertChecker.verify_api_https(api_uri)
- except requests.exceptions.SSLError as exc:
- return self.fail("Validation Error")
- except Exception as exc:
- return self.fail(exc.message)
- else:
- return True
-
- yield((self.tr('Validating api certificate'), 90), validatecacert)
-
- self.set_done()
- yield(('end_sentinel', 100), lambda: None)
-
- def on_checks_validation_ready(self):
- """
- called after _do_checks has finished
- (connected to checker thread finished signal)
- """
- wizard = self.wizard()
- prevpage = "login" if wizard.from_login else "providerselection"
-
- if self.errors:
- logger.debug('going back with errors')
- name, first_error = self.pop_first_error()
- wizard.set_validation_error(
- prevpage,
- first_error)
-
- def nextId(self):
- wizard = self.wizard()
- from_login = wizard.from_login
- if from_login:
- next_ = 'connect'
- else:
- next_ = 'signup'
- return wizard.get_page_index(next_)
-
- def initializePage(self):
- super(ProviderSetupValidationPage, self).initializePage()
- self.set_undone()
- self.completeChanged.emit()
diff --git a/src/leap/gui/firstrun/register.py b/src/leap/gui/firstrun/register.py
deleted file mode 100644
index 2ae926d1..00000000
--- a/src/leap/gui/firstrun/register.py
+++ /dev/null
@@ -1,391 +0,0 @@
-"""
-Register User Page, used in First Run Wizard
-"""
-import json
-import logging
-import socket
-
-import requests
-
-from PyQt4 import QtCore
-from PyQt4 import QtGui
-
-from leap.gui.firstrun.mixins import UserFormMixIn
-
-logger = logging.getLogger(__name__)
-
-from leap.base import auth
-from leap.gui import styles
-from leap.gui.constants import APP_LOGO, APP_WATERMARK, BARE_USERNAME_REGEX
-from leap.gui.progress import InlineValidationPage
-from leap.gui.styles import ErrorLabelStyleSheet
-
-
-class RegisterUserPage(InlineValidationPage, UserFormMixIn):
-
- def __init__(self, parent=None):
-
- super(RegisterUserPage, self).__init__(parent)
- self.current_page = "signup"
-
- self.setTitle(self.tr("Sign Up"))
- # subtitle is set in the initializePage
-
- self.setPixmap(
- QtGui.QWizard.WatermarkPixmap,
- QtGui.QPixmap(APP_WATERMARK))
-
- self.setPixmap(
- QtGui.QWizard.LogoPixmap,
- QtGui.QPixmap(APP_LOGO))
-
- # commit page means there's no way back after this...
- # XXX should change the text on the "commit" button...
- self.setCommitPage(True)
-
- self.setupSteps()
- self.setupUI()
- self.do_confirm_next = False
- self.focused_field = False
-
- def setupUI(self):
- userNameLabel = QtGui.QLabel(self.tr("User &name:"))
- userNameLineEdit = QtGui.QLineEdit()
- userNameLineEdit.cursorPositionChanged.connect(
- self.reset_validation_status)
- userNameLabel.setBuddy(userNameLineEdit)
-
- # let's add regex validator
- usernameRe = QtCore.QRegExp(BARE_USERNAME_REGEX)
- userNameLineEdit.setValidator(
- QtGui.QRegExpValidator(usernameRe, self))
- self.userNameLineEdit = userNameLineEdit
-
- userPasswordLabel = QtGui.QLabel(self.tr("&Password:"))
- self.userPasswordLineEdit = QtGui.QLineEdit()
- self.userPasswordLineEdit.setEchoMode(
- QtGui.QLineEdit.Password)
- userPasswordLabel.setBuddy(self.userPasswordLineEdit)
-
- userPassword2Label = QtGui.QLabel(self.tr("Password (again):"))
- self.userPassword2LineEdit = QtGui.QLineEdit()
- self.userPassword2LineEdit.setEchoMode(
- QtGui.QLineEdit.Password)
- userPassword2Label.setBuddy(self.userPassword2LineEdit)
-
- rememberPasswordCheckBox = QtGui.QCheckBox(
- self.tr("&Remember username and password."))
- rememberPasswordCheckBox.setChecked(True)
-
- self.registerField('userName*', self.userNameLineEdit)
- self.registerField('userPassword*', self.userPasswordLineEdit)
- self.registerField('userPassword2*', self.userPassword2LineEdit)
-
- # XXX missing password confirmation
- # XXX validator!
-
- self.registerField('rememberPassword', rememberPasswordCheckBox)
-
- layout = QtGui.QGridLayout()
- layout.setColumnMinimumWidth(0, 20)
-
- validationMsg = QtGui.QLabel("")
- validationMsg.setStyleSheet(ErrorLabelStyleSheet)
-
- self.validationMsg = validationMsg
-
- layout.addWidget(validationMsg, 0, 3)
- layout.addWidget(userNameLabel, 1, 0)
- layout.addWidget(self.userNameLineEdit, 1, 3)
- layout.addWidget(userPasswordLabel, 2, 0)
- layout.addWidget(userPassword2Label, 3, 0)
- layout.addWidget(self.userPasswordLineEdit, 2, 3)
- layout.addWidget(self.userPassword2LineEdit, 3, 3)
- layout.addWidget(rememberPasswordCheckBox, 4, 3, 4, 4)
-
- # add validation frame
- self.setupValidationFrame()
- layout.addWidget(self.valFrame, 5, 2, 5, 2)
- self.valFrame.hide()
-
- self.setLayout(layout)
- self.commitText("Sign up!")
-
- # commit button
-
- def commitText(self, text):
- # change "commit" button text
- self.setButtonText(
- QtGui.QWizard.CommitButton, text)
-
- @property
- def commitButton(self):
- return self.wizard().button(QtGui.QWizard.CommitButton)
-
- def commitFocus(self):
- self.commitButton.setFocus()
-
- def disableCommitButton(self):
- self.commitButton.setDisabled(True)
-
- def disableFields(self):
- for field in (self.userNameLineEdit,
- self.userPasswordLineEdit,
- self.userPassword2LineEdit):
- field.setDisabled(True)
-
- # error painting
- def paintEvent(self, event):
- """
- we hook our populate errors
- on paintEvent because we need it to catch
- when user enters the page coming from next,
- and initializePage does not cover that case.
- Maybe there's a better event to hook upon.
- """
- super(RegisterUserPage, self).paintEvent(event)
- self.populateErrors()
-
- def markRedAndGetFocus(self, field):
- field.setStyleSheet(styles.ErrorLineEdit)
- if not self.focused_field:
- self.focused_field = True
- field.setFocus(QtCore.Qt.OtherFocusReason)
-
- def markRegular(self, field):
- field.setStyleSheet(styles.RegularLineEdit)
-
- def populateErrors(self):
- def showerr(text):
- self.validationMsg.setText(text)
- err_lower = text.lower()
- if "username" in err_lower:
- self.markRedAndGetFocus(
- self.userNameLineEdit)
- if "password" in err_lower:
- self.markRedAndGetFocus(
- self.userPasswordLineEdit)
-
- def unmarkred():
- for field in (self.userNameLineEdit,
- self.userPasswordLineEdit,
- self.userPassword2LineEdit):
- self.markRegular(field)
-
- errors = self.wizard().get_validation_error(
- self.current_page)
- if errors:
- bad_str = getattr(self, 'bad_string', None)
- cur_str = self.userNameLineEdit.text()
- #prev_er = getattr(self, 'prevalidation_error', None)
-
- if bad_str is None:
- # first time we fall here.
- # save the current bad_string value
- self.bad_string = cur_str
- showerr(errors)
- else:
- #if prev_er:
- #showerr(prev_er)
- #return
- # not the first time
- if cur_str == bad_str:
- showerr(errors)
- else:
- self.focused_field = False
- showerr('')
- unmarkred()
- else:
- # no errors
- self.focused_field = False
- unmarkred()
-
- def cleanup_errormsg(self):
- """
- we reset bad_string to None
- should be called before leaving the page
- """
- self.bad_string = None
-
- def green_validation_status(self):
- val = self.validationMsg
- val.setText(self.tr('Registration succeeded!'))
- val.setStyleSheet(styles.GreenLineEdit)
-
- def reset_validation_status(self):
- """
- empty the validation msg
- and clean the inline validation widget.
- """
- self.validationMsg.setText('')
- self.steps.removeAllSteps()
- self.clearTable()
-
- # actual checks
-
- def _do_checks(self):
- """
- generator that yields actual checks
- that are executed in a separate thread
- """
- wizard = self.wizard()
-
- provider = self.field('provider_domain')
- username = self.userNameLineEdit.text()
- password = self.userPasswordLineEdit.text()
- password2 = self.userPassword2LineEdit.text()
-
- pconfig = wizard.eipconfigchecker(domain=provider)
- pconfig.defaultprovider.load()
- pconfig.set_api_domain()
-
- def checkpass():
- # we better have here
- # some call to a password checker...
- # to assess strenght and avoid silly stuff.
-
- if password != password2:
- return self.fail(self.tr('Password does not match..'))
-
- if len(password) < 6:
- #self.set_prevalidation_error('Password too short.')
- return self.fail(self.tr('Password too short.'))
-
- if password == "123456":
- # joking, but not too much.
- #self.set_prevalidation_error('Password too obvious.')
- return self.fail(self.tr('Password too obvious.'))
-
- # go
- return True
-
- yield(("head_sentinel", 0), checkpass)
-
- # XXX should emit signal for .show the frame!
- # XXX HERE!
-
- ##################################################
- # 1) register user
- ##################################################
-
- # show the frame before going on...
- QtCore.QMetaObject.invokeMethod(
- self, "showStepsFrame")
-
- def register():
-
- signup = auth.LeapSRPRegister(
- schema="https",
- provider=pconfig.apidomain,
- verify=pconfig.cacert)
- try:
- ok, req = signup.register_user(
- username, password)
-
- except socket.timeout:
- return self.fail(
- self.tr("Error connecting to provider (timeout)"))
-
- except requests.exceptions.ConnectionError as exc:
- logger.error(exc.message)
- return self.fail(
- self.tr('Error Connecting to provider (connerr).'))
- except Exception as exc:
- return self.fail(exc.message)
-
- # XXX check for != OK instead???
-
- if req.status_code in (404, 500):
- return self.fail(
- self.tr(
- "Error during registration (%s)") % req.status_code)
-
- try:
- validation_msgs = json.loads(req.content)
- errors = validation_msgs.get('errors', None)
- logger.debug('validation errors: %s' % validation_msgs)
- except ValueError:
- # probably bad json returned
- return self.fail(
- self.tr(
- "Could not register (bad response)"))
-
- if errors and errors.get('login', None):
- # XXX this sometimes catch the blank username
- # but we're not allowing that (soon)
- return self.fail(
- self.tr('Username not available.'))
-
- return True
-
- logger.debug('registering user')
- yield(("Registering username", 40), register)
-
- self.set_done()
- yield(("end_sentinel", 100), lambda: None)
-
- def on_checks_validation_ready(self):
- """
- after checks
- """
- if self.is_done():
- self.disableFields()
- self.cleanup_errormsg()
- self.clean_wizard_errors(self.current_page)
- # make the user confirm the transition
- # to next page.
- self.commitText('Connect!')
- self.commitFocus()
- self.green_validation_status()
- self.do_confirm_next = True
-
- # pagewizard methods
-
- def validatePage(self):
- """
- if not register done, do checks.
- if done, wait for click.
- """
- self.disableCommitButton()
- self.cleanup_errormsg()
- self.clean_wizard_errors(self.current_page)
-
- # After a successful validation
- # (ie, success register with server)
- # we change the commit button text
- # and set this flag to True.
- if self.do_confirm_next:
- return True
-
- if not self.is_done():
- # calls checks, which after successful
- # execution will call on_checks_validation_ready
- self.reset_validation_status()
- self.do_checks()
-
- return self.is_done()
-
- def initializePage(self):
- """
- inits wizard page
- """
- provider = unicode(self.field('provider_domain'))
- if provider:
- # here we should have provider
- # but in tests we might not.
-
- # XXX this error causes a segfault on free()
- # that we might want to get fixed ...
- #self.setSubTitle(
- #self.tr("Register a new user with provider %s.") %
- #provider)
- self.setSubTitle(
- self.tr("Register a new user with provider <em>%s</em>" %
- provider))
- self.validationMsg.setText('')
- self.userPassword2LineEdit.setText('')
- self.valFrame.hide()
-
- def nextId(self):
- wizard = self.wizard()
- return wizard.get_page_index('connect')
diff --git a/src/leap/gui/firstrun/tests/integration/fake_provider.py b/src/leap/gui/firstrun/tests/integration/fake_provider.py
deleted file mode 100755
index 668db5d1..00000000
--- a/src/leap/gui/firstrun/tests/integration/fake_provider.py
+++ /dev/null
@@ -1,302 +0,0 @@
-#!/usr/bin/env python
-"""A server faking some of the provider resources and apis,
-used for testing Leap Client requests
-
-It needs that you create a subfolder named 'certs',
-and that you place the following files:
-
-[ ] certs/leaptestscert.pem
-[ ] certs/leaptestskey.pem
-[ ] certs/cacert.pem
-[ ] certs/openvpn.pem
-
-[ ] provider.json
-[ ] eip-service.json
-"""
-# XXX NOTE: intended for manual debug.
-# I intend to include this as a regular test after 0.2.0 release
-# (so we can add twisted as a dep there)
-import binascii
-import json
-import os
-import sys
-
-# python SRP LIB (! important MUST be >=1.0.1 !)
-import srp
-
-# GnuTLS Example -- is not working as expected
-#from gnutls import crypto
-#from gnutls.constants import COMP_LZO, COMP_DEFLATE, COMP_NULL
-#from gnutls.interfaces.twisted import X509Credentials
-
-# Going with OpenSSL as a workaround instead
-# But we DO NOT want to introduce this dependency.
-from OpenSSL import SSL
-
-from zope.interface import Interface, Attribute, implements
-
-from twisted.web.server import Site
-from twisted.web.static import File
-from twisted.web.resource import Resource
-from twisted.internet import reactor
-
-from leap.testing.https_server import where
-
-# See
-# http://twistedmatrix.com/documents/current/web/howto/web-in-60/index.htmln
-# for more examples
-
-"""
-Testing the FAKE_API:
-#####################
-
- 1) register an user
- >> curl -d "user[login]=me" -d "user[password_salt]=foo" \
- -d "user[password_verifier]=beef" http://localhost:8000/1/users.json
- << {"errors": null}
-
- 2) check that if you try to register again, it will fail:
- >> curl -d "user[login]=me" -d "user[password_salt]=foo" \
- -d "user[password_verifier]=beef" http://localhost:8000/1/users.json
- << {"errors": {"login": "already taken!"}}
-
-"""
-
-# Globals to mock user/sessiondb
-
-USERDB = {}
-SESSIONDB = {}
-
-
-safe_unhexlify = lambda x: binascii.unhexlify(x) \
- if (len(x) % 2 == 0) else binascii.unhexlify('0' + x)
-
-
-class IUser(Interface):
- login = Attribute("User login.")
- salt = Attribute("Password salt.")
- verifier = Attribute("Password verifier.")
- session = Attribute("Session.")
- svr = Attribute("Server verifier.")
-
-
-class User(object):
- implements(IUser)
-
- def __init__(self, login, salt, verifier):
- self.login = login
- self.salt = salt
- self.verifier = verifier
- self.session = None
-
- def set_server_verifier(self, svr):
- self.svr = svr
-
- def set_session(self, session):
- SESSIONDB[session] = self
- self.session = session
-
-
-class FakeUsers(Resource):
- def __init__(self, name):
- self.name = name
-
- def render_POST(self, request):
- args = request.args
-
- login = args['user[login]'][0]
- salt = args['user[password_salt]'][0]
- verifier = args['user[password_verifier]'][0]
-
- if login in USERDB:
- return "%s\n" % json.dumps(
- {'errors': {'login': 'already taken!'}})
-
- print login, verifier, salt
- user = User(login, salt, verifier)
- USERDB[login] = user
- return json.dumps({'errors': None})
-
-
-def get_user(request):
- login = request.args.get('login')
- if login:
- user = USERDB.get(login[0], None)
- if user:
- return user
-
- session = request.getSession()
- user = SESSIONDB.get(session, None)
- return user
-
-
-class FakeSession(Resource):
- def __init__(self, name):
- self.name = name
-
- def render_GET(self, request):
- return "%s\n" % json.dumps({'errors': None})
-
- def render_POST(self, request):
-
- user = get_user(request)
-
- if not user:
- # XXX get real error from demo provider
- return json.dumps({'errors': 'no such user'})
-
- A = request.args['A'][0]
-
- _A = safe_unhexlify(A)
- _salt = safe_unhexlify(user.salt)
- _verifier = safe_unhexlify(user.verifier)
-
- svr = srp.Verifier(
- user.login,
- _salt,
- _verifier,
- _A,
- hash_alg=srp.SHA256,
- ng_type=srp.NG_1024)
-
- s, B = svr.get_challenge()
-
- _B = binascii.hexlify(B)
-
- print 'login = %s' % user.login
- print 'salt = %s' % user.salt
- print 'len(_salt) = %s' % len(_salt)
- print 'vkey = %s' % user.verifier
- print 'len(vkey) = %s' % len(_verifier)
- print 's = %s' % binascii.hexlify(s)
- print 'B = %s' % _B
- print 'len(B) = %s' % len(_B)
-
- session = request.getSession()
- user.set_session(session)
- user.set_server_verifier(svr)
-
- # yep, this is tricky.
- # some things are *already* unhexlified.
- data = {
- 'salt': user.salt,
- 'B': _B,
- 'errors': None}
-
- return json.dumps(data)
-
- def render_PUT(self, request):
-
- # XXX check session???
- user = get_user(request)
-
- if not user:
- print 'NO USER'
- return json.dumps({'errors': 'no such user'})
-
- data = request.content.read()
- auth = data.split("client_auth=")
- M = auth[1] if len(auth) > 1 else None
- # if not H, return
- if not M:
- return json.dumps({'errors': 'no M proof passed by client'})
-
- svr = user.svr
- HAMK = svr.verify_session(binascii.unhexlify(M))
- if HAMK is None:
- print 'verification failed!!!'
- raise Exception("Authentication failed!")
- #import ipdb;ipdb.set_trace()
-
- assert svr.authenticated()
- print "***"
- print 'server authenticated user SRP!'
- print "***"
-
- return json.dumps(
- {'M2': binascii.hexlify(HAMK), 'errors': None})
-
-
-class API_Sessions(Resource):
- def getChild(self, name, request):
- return FakeSession(name)
-
-
-def get_certs_path():
- script_path = os.path.realpath(os.path.dirname(sys.argv[0]))
- certs_path = os.path.join(script_path, 'certs')
- return certs_path
-
-
-def get_TLS_credentials():
- # XXX this is giving errors
- # XXX REview! We want to use gnutls!
-
- cert = crypto.X509Certificate(
- open(where('leaptestscert.pem')).read())
- key = crypto.X509PrivateKey(
- open(where('leaptestskey.pem')).read())
- ca = crypto.X509Certificate(
- open(where('cacert.pem')).read())
- #crl = crypto.X509CRL(open(certs_path + '/crl.pem').read())
- #cred = crypto.X509Credentials(cert, key, [ca], [crl])
- cred = X509Credentials(cert, key, [ca])
- cred.verify_peer = True
- cred.session_params.compressions = (COMP_LZO, COMP_DEFLATE, COMP_NULL)
- return cred
-
-
-class OpenSSLServerContextFactory:
- # XXX workaround for broken TLS interface
- # from gnuTLS.
-
- def getContext(self):
- """Create an SSL context.
- This is a sample implementation that loads a certificate from a file
- called 'server.pem'."""
-
- ctx = SSL.Context(SSL.SSLv23_METHOD)
- #certs_path = get_certs_path()
- #ctx.use_certificate_file(certs_path + '/leaptestscert.pem')
- #ctx.use_privatekey_file(certs_path + '/leaptestskey.pem')
- ctx.use_certificate_file(where('leaptestscert.pem'))
- ctx.use_privatekey_file(where('leaptestskey.pem'))
- return ctx
-
-
-def serve_fake_provider():
- root = Resource()
- root.putChild("provider.json", File("./provider.json"))
- config = Resource()
- config.putChild(
- "eip-service.json",
- File("./eip-service.json"))
- apiv1 = Resource()
- apiv1.putChild("config", config)
- apiv1.putChild("sessions.json", API_Sessions())
- apiv1.putChild("users.json", FakeUsers(None))
- apiv1.putChild("cert", File(get_certs_path() + '/openvpn.pem'))
- root.putChild("1", apiv1)
-
- cred = get_TLS_credentials()
-
- factory = Site(root)
-
- # regular http (for debugging with curl)
- reactor.listenTCP(8000, factory)
-
- # TLS with gnutls --- seems broken :(
- #reactor.listenTLS(8003, factory, cred)
-
- # OpenSSL
- reactor.listenSSL(8443, factory, OpenSSLServerContextFactory())
-
- reactor.run()
-
-
-if __name__ == "__main__":
-
- from twisted.python import log
- log.startLogging(sys.stdout)
-
- serve_fake_provider()
diff --git a/src/leap/gui/firstrun/wizard.py b/src/leap/gui/firstrun/wizard.py
deleted file mode 100755
index f198dca0..00000000
--- a/src/leap/gui/firstrun/wizard.py
+++ /dev/null
@@ -1,309 +0,0 @@
-#!/usr/bin/env python
-import logging
-
-import sip
-try:
- sip.setapi('QString', 2)
- sip.setapi('QVariant', 2)
-except ValueError:
- pass
-
-from PyQt4 import QtCore
-from PyQt4 import QtGui
-
-from leap.base import checks as basechecks
-from leap.crypto import leapkeyring
-from leap.eip import checks as eipchecks
-
-from leap.gui import firstrun
-
-from leap.gui import mainwindow_rc
-
-try:
- from collections import OrderedDict
-except ImportError:
- # We must be in 2.6
- from leap.util.dicts import OrderedDict
-
-logger = logging.getLogger(__name__)
-
-"""
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-Work in progress!
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-This wizard still needs to be refactored out.
-
-TODO-ish:
-
-[X] Break file in wizard / pages files (and its own folder).
-[ ] Separate presentation from logic.
-[ ] Have a "manager" class for connections, that can be
- dep-injected for testing.
-[ ] Document signals used / expected.
-[ ] Separate style from widgets.
-[ ] Fix TOFU Widget for provider cert.
-[X] Refactor widgets out.
-[ ] Follow more MVC style.
-[ ] Maybe separate "first run wizard" into different wizards
- that share some of the pages?
-"""
-
-
-def get_pages_dict():
- return OrderedDict((
- ('intro', firstrun.intro.IntroPage),
- ('providerselection',
- firstrun.providerselect.SelectProviderPage),
- ('login', firstrun.login.LogInPage),
- ('providerinfo', firstrun.providerinfo.ProviderInfoPage),
- ('providersetupvalidation',
- firstrun.providersetup.ProviderSetupValidationPage),
- ('signup', firstrun.register.RegisterUserPage),
- ('connect',
- firstrun.connect.ConnectionPage),
- ('lastpage', firstrun.last.LastPage)
- ))
-
-
-class FirstRunWizard(QtGui.QWizard):
-
- def __init__(
- self,
- conductor_instance,
- parent=None,
- pages_dict=None,
- username=None,
- providers=None,
- success_cb=None, is_provider_setup=False,
- trusted_certs=None,
- netchecker=basechecks.LeapNetworkChecker,
- providercertchecker=eipchecks.ProviderCertChecker,
- eipconfigchecker=eipchecks.EIPConfigChecker,
- start_eipconnection_signal=None,
- eip_statuschange_signal=None,
- debug_server=None,
- quitcallback=None):
- super(FirstRunWizard, self).__init__(
- parent,
- QtCore.Qt.WindowStaysOnTopHint)
-
- # we keep a reference to the conductor
- # to be able to launch eip checks and connection
- # in the connection page, before the wizard has ended.
- self.conductor = conductor_instance
-
- self.username = username
- self.providers = providers
-
- # success callback
- self.success_cb = success_cb
-
- # is provider setup?
- self.is_provider_setup = is_provider_setup
-
- # a dict with trusted fingerprints
- # in the form {'nospacesfingerprint': ['host1', 'host2']}
- self.trusted_certs = trusted_certs
-
- # Checkers
- self.netchecker = netchecker
- self.providercertchecker = providercertchecker
- self.eipconfigchecker = eipconfigchecker
-
- # debug server
- self.debug_server = debug_server
-
- # Signals
- # will be emitted in connecting page
- self.start_eipconnection_signal = start_eipconnection_signal
- self.eip_statuschange_signal = eip_statuschange_signal
-
- if quitcallback is not None:
- self.button(
- QtGui.QWizard.CancelButton).clicked.connect(
- quitcallback)
-
- self.providerconfig = None
- # previously registered
- # if True, jumps to LogIn page.
- # by setting 1st page??
- #self.is_previously_registered = is_previously_registered
- # XXX ??? ^v
- self.is_previously_registered = bool(self.username)
- self.from_login = False
-
- pages_dict = pages_dict or get_pages_dict()
- self.add_pages_from_dict(pages_dict)
-
- self.validation_errors = {}
- self.openvpn_status = []
-
- self.setPixmap(
- QtGui.QWizard.BannerPixmap,
- QtGui.QPixmap(':/images/banner.png'))
- self.setPixmap(
- QtGui.QWizard.BackgroundPixmap,
- QtGui.QPixmap(':/images/background.png'))
-
- # set options
- self.setOption(QtGui.QWizard.IndependentPages, on=False)
- self.setOption(QtGui.QWizard.NoBackButtonOnStartPage, on=True)
-
- self.setWindowTitle("First Run Wizard")
-
- # TODO: set style for MAC / windows ...
- #self.setWizardStyle()
-
- #
- # setup pages in wizard
- #
-
- def add_pages_from_dict(self, pages_dict):
- """
- @param pages_dict: the dictionary with pages, where
- values are a tuple of InstanceofWizardPage, kwargs.
- @type pages_dict: dict
- """
- for name, page in pages_dict.items():
- # XXX check for is_previously registered
- # and skip adding the signup branch if so
- self.addPage(page())
- self.pages_dict = pages_dict
-
- def get_page_index(self, page_name):
- """
- returns the index of the given page
- @param page_name: the name of the desired page
- @type page_name: str
- @rparam: index of page in wizard
- @rtype: int
- """
- return self.pages_dict.keys().index(page_name)
-
- #
- # validation errors
- #
-
- def set_validation_error(self, pagename, error):
- self.validation_errors[pagename] = error
-
- def clean_validation_error(self, pagename):
- vald = self.validation_errors
- if pagename in vald:
- del vald[pagename]
-
- def get_validation_error(self, pagename):
- return self.validation_errors.get(pagename, None)
-
- def accept(self):
- """
- final step in the wizard.
- gather the info, update settings
- and call the success callback if any has been passed.
- """
- super(FirstRunWizard, self).accept()
-
- # username and password are in different fields
- # if they were stored in log_in or sign_up pages.
- from_login = self.from_login
- unamek_base = 'userName'
- passwk_base = 'userPassword'
- unamek = 'login_%s' % unamek_base if from_login else unamek_base
- passwk = 'login_%s' % passwk_base if from_login else passwk_base
-
- username = self.field(unamek)
- password = self.field(passwk)
- provider = self.field('provider_domain')
- remember_pass = self.field('rememberPassword')
-
- logger.debug('chosen provider: %s', provider)
- logger.debug('username: %s', username)
- logger.debug('remember password: %s', remember_pass)
-
- # we are assuming here that we only remember one username
- # in the form username@provider.domain
- # We probably could extend this to support some form of
- # profiles.
-
- settings = QtCore.QSettings()
-
- settings.setValue("FirstRunWizardDone", True)
- settings.setValue("provider_domain", provider)
- full_username = "%s@%s" % (username, provider)
-
- settings.setValue("remember_user_and_pass", remember_pass)
-
- if remember_pass:
- settings.setValue("username", full_username)
- seed = self.get_random_str(10)
- settings.setValue("%s_seed" % provider, seed)
-
- # XXX #744: comment out for 0.2.0 release
- # if we need to have a version of python-keyring < 0.9
- leapkeyring.leap_set_password(
- full_username, password, seed=seed)
-
- logger.debug('First Run Wizard Done.')
- cb = self.success_cb
- if cb and callable(cb):
- self.success_cb()
-
- # misc helpers
-
- def get_random_str(self, n):
- """
- returns a random string
- :param n: the length of the desired string
- :rvalue: str
- """
- from string import (ascii_uppercase, ascii_lowercase, digits)
- from random import choice
- return ''.join(choice(
- ascii_uppercase +
- ascii_lowercase +
- digits) for x in range(n))
-
- def set_providerconfig(self, providerconfig):
- """
- sets a providerconfig attribute
- used when we fetch and parse a json configuration
- """
- self.providerconfig = providerconfig
-
- def get_provider_by_index(self): # pragma: no cover
- """
- returns the value of a provider given its index.
- this was used in the select provider page,
- in the case where we were preseeding providers in a combobox
- """
- # Leaving it here for the moment when we go back at the
- # option of preseeding with known provider values.
- provider = self.field('provider_index')
- return self.providers[provider]
-
-
-if __name__ == '__main__':
- # standalone test
- # it can be (somehow) run against
- # gui/tests/integration/fake_user_signup.py
-
- import sys
- import logging
- logging.basicConfig()
- logger = logging.getLogger()
- logger.setLevel(logging.DEBUG)
-
- app = QtGui.QApplication(sys.argv)
- server = sys.argv[1] if len(sys.argv) > 1 else None
-
- trusted_certs = {
- "3DF83F316BFA0186"
- "0A11A5C9C7FC24B9"
- "18C62B941192CC1A"
- "49AE62218B2A4B7C": ['springbok']}
-
- wizard = FirstRunWizard(
- None, trusted_certs=trusted_certs,
- debug_server=server)
- wizard.show()
- sys.exit(app.exec_())
diff --git a/src/leap/gui/locale_rc.py b/src/leap/gui/locale_rc.py
deleted file mode 100644
index 7ff8b4d4..00000000
--- a/src/leap/gui/locale_rc.py
+++ /dev/null
@@ -1,1260 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Resource object code
-#
-# Created: Thu Feb 14 08:00:11 2013
-# by: The Resource Compiler for PyQt (Qt v4.8.2)
-#
-# WARNING! All changes made in this file will be lost!
-
-from PyQt4 import QtCore
-
-qt_resource_data = "\
-\x00\x00\x17\x94\
-\x3c\
-\xb8\x64\x18\xca\xef\x9c\x95\xcd\x21\x1c\xbf\x60\xa1\xbd\xdd\x42\
-\x00\x00\x01\x30\x00\x8f\x9b\xbe\x00\x00\x14\x69\x01\x23\x92\xe5\
-\x00\x00\x10\x2f\x01\x87\x64\x8e\x00\x00\x08\xbe\x01\xa8\xbe\x7e\
-\x00\x00\x0d\xf4\x02\x2c\xac\xe9\x00\x00\x0b\x9c\x02\x3a\xce\xbf\
-\x00\x00\x15\xe2\x02\x6e\x0f\xe5\x00\x00\x09\x2d\x02\x87\x60\x9e\
-\x00\x00\x06\xc6\x02\xaa\x52\x6e\x00\x00\x07\xc9\x02\xf2\xe0\x59\
-\x00\x00\x0a\x6c\x03\xec\x70\x0e\x00\x00\x10\x9c\x04\xd4\x45\xee\
-\x00\x00\x0d\x3c\x05\xb7\x8f\x59\x00\x00\x0c\x35\x06\x3e\x6a\x9e\
-\x00\x00\x06\x01\x06\x40\xa8\x7e\x00\x00\x0b\x02\x06\xee\xff\x6e\
-\x00\x00\x13\x50\x08\x13\xe8\xae\x00\x00\x0c\xc2\x08\x7a\x64\xee\
-\x00\x00\x11\x8b\x08\xe6\x98\x33\x00\x00\x05\x93\x08\xe6\x98\x33\
-\x00\x00\x0f\xb0\x09\x5c\x35\xe1\x00\x00\x0e\x96\x09\x74\x75\x4e\
-\x00\x00\x0d\x9c\x09\x98\x34\x0e\x00\x00\x12\x55\x09\xd8\x1f\x95\
-\x00\x00\x15\x19\x09\xfc\x2c\x8e\x00\x00\x05\x19\x09\xfe\x05\x90\
-\x00\x00\x0f\x06\x0a\x74\xb8\x1e\x00\x00\x00\xe6\x0a\xfd\x99\xfe\
-\x00\x00\x00\x6d\x0b\xd2\x4b\x3f\x00\x00\x07\x7d\x0c\x44\x41\xbe\
-\x00\x00\x00\x00\x0c\xc0\x94\x05\x00\x00\x09\xf2\x0d\x0d\x9d\xc5\
-\x00\x00\x06\x5f\x0d\x15\x34\x70\x00\x00\x09\x98\x0e\x36\x15\x54\
-\x00\x00\x08\x47\x0e\x7e\xf5\xee\x00\x00\x0f\x42\x0e\x91\x50\x3e\
-\x00\x00\x15\x76\x0e\xc0\xbb\x72\x00\x00\x12\xfb\x0f\x27\x0d\x6e\
-\x00\x00\x11\x22\x69\x00\x00\x16\x43\x03\x00\x00\x00\x3e\x00\x41\
-\x00\x73\x00\x73\x00\x69\x00\x73\x00\x74\x00\x65\x00\x6e\x00\x74\
-\x00\x20\x00\x66\x00\xfc\x00\x72\x00\x20\x00\x65\x00\x72\x00\x73\
-\x00\x74\x00\x6d\x00\x61\x00\x6c\x00\x69\x00\x67\x00\x65\x00\x6e\
-\x00\x20\x00\x53\x00\x74\x00\x61\x00\x72\x00\x74\x08\x00\x00\x00\
-\x00\x06\x00\x00\x00\x11\x46\x69\x72\x73\x74\x20\x72\x75\x6e\x20\
-\x77\x69\x7a\x61\x72\x64\x2e\x07\x00\x00\x00\x09\x49\x6e\x74\x72\
-\x6f\x50\x61\x67\x65\x01\x03\x00\x00\x00\x40\x00\x4d\x00\x69\x00\
-\x74\x00\x20\x00\x62\x00\x65\x00\x73\x00\x74\x00\x65\x00\x68\x00\
-\x65\x00\x6e\x00\x64\x00\x65\x00\x6e\x00\x20\x00\x44\x00\x61\x00\
-\x74\x00\x65\x00\x6e\x00\x20\x00\x65\x00\x69\x00\x6e\x00\x6c\x00\
-\x6f\x00\x67\x00\x67\x00\x65\x00\x6e\x00\x2e\x08\x00\x00\x00\x00\
-\x06\x00\x00\x00\x1b\x4c\x6f\x67\x20\x49\x6e\x20\x77\x69\x74\x68\
-\x20\x6d\x79\x20\x63\x72\x65\x64\x65\x6e\x74\x69\x61\x6c\x73\x2e\
-\x07\x00\x00\x00\x09\x49\x6e\x74\x72\x6f\x50\x61\x67\x65\x01\x03\
-\x00\x00\x02\xb8\x00\x57\x00\x69\x00\x72\x00\x20\x00\x77\x00\x65\
-\x00\x72\x00\x64\x00\x65\x00\x6e\x00\x20\x00\x64\x00\x69\x00\x63\
-\x00\x68\x00\x20\x00\x6e\x00\x75\x00\x6e\x00\x20\x00\x64\x00\x75\
-\x00\x72\x00\x63\x00\x68\x00\x20\x00\x65\x00\x69\x00\x6e\x00\x69\
-\x00\x67\x00\x65\x00\x20\x00\x4b\x00\x6f\x00\x6e\x00\x66\x00\x69\
-\x00\x67\x00\x75\x00\x72\x00\x61\x00\x74\x00\x69\x00\x6f\x00\x6e\
-\x00\x65\x00\x6e\x00\x20\x00\x66\x00\xfc\x00\x68\x00\x72\x00\x65\
-\x00\x6e\x00\x2c\x00\x20\x00\x64\x00\x69\x00\x65\x00\x20\x00\x64\
-\x00\x75\x00\x20\x00\x66\x00\xfc\x00\x72\x00\x20\x00\x64\x00\x65\
-\x00\x6e\x00\x20\x00\x65\x00\x72\x00\x73\x00\x74\x00\x65\x00\x6e\
-\x00\x20\x00\x53\x00\x74\x00\x61\x00\x72\x00\x74\x00\x20\x00\x62\
-\x00\x65\x00\x6e\x00\xf6\x00\x74\x00\x69\x00\x67\x00\x73\x00\x74\
-\x00\x2e\x00\x3c\x00\x62\x00\x72\x00\x3e\x00\x3c\x00\x62\x00\x72\
-\x00\x3e\x00\x57\x00\x65\x00\x6e\x00\x6e\x00\x20\x00\x64\x00\x75\
-\x00\x20\x00\x64\x00\x69\x00\x65\x00\x73\x00\x65\x00\x20\x00\x4b\
-\x00\x6f\x00\x6e\x00\x66\x00\x69\x00\x67\x00\x75\x00\x72\x00\x61\
-\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x65\x00\x6e\x00\x20\x00\x6a\
-\x00\x65\x00\x6d\x00\x61\x00\x6c\x00\x73\x00\x20\x00\xe4\x00\x6e\
-\x00\x64\x00\x65\x00\x72\x00\x6e\x00\x20\x00\x6d\x00\x75\x00\x73\
-\x00\x73\x00\x74\x00\x2c\x00\x20\x00\x66\x00\x69\x00\x6e\x00\x64\
-\x00\x65\x00\x73\x00\x74\x00\x20\x00\x64\x00\x75\x00\x20\x00\x64\
-\x00\x65\x00\x6e\x00\x20\x00\x41\x00\x73\x00\x73\x00\x69\x00\x73\
-\x00\x74\x00\x65\x00\x6e\x00\x74\x00\x65\x00\x6e\x00\x20\x00\x69\
-\x00\x6d\x00\x20\x00\x27\x00\x3c\x00\x69\x00\x3e\x00\x45\x00\x69\
-\x00\x6e\x00\x73\x00\x74\x00\x65\x00\x6c\x00\x6c\x00\x75\x00\x6e\
-\x00\x67\x00\x65\x00\x6e\x00\x3c\x00\x2f\x00\x69\x00\x3e\x00\x27\
-\x00\x2d\x00\x4d\x00\x65\x00\x6e\x00\xfc\x00\x20\x00\x64\x00\x65\
-\x00\x73\x00\x20\x00\x48\x00\x61\x00\x75\x00\x70\x00\x66\x00\x65\
-\x00\x6e\x00\x73\x00\x74\x00\x65\x00\x72\x00\x73\x00\x2e\x00\x3c\
-\x00\x62\x00\x72\x00\x3e\x00\x3c\x00\x62\x00\x72\x00\x3e\x00\x4d\
-\x00\xf6\x00\x63\x00\x68\x00\x74\x00\x65\x00\x73\x00\x74\x00\x20\
-\x00\x64\x00\x75\x00\x20\x00\x64\x00\x69\x00\x63\x00\x68\x00\x20\
-\x00\x66\x00\xfc\x00\x72\x00\x20\x00\x65\x00\x69\x00\x6e\x00\x65\
-\x00\x6e\x00\x20\x00\x6e\x00\x65\x00\x75\x00\x65\x00\x6e\x00\x20\
-\x00\x41\x00\x63\x00\x63\x00\x6f\x00\x75\x00\x6e\x00\x74\x00\x20\
-\x00\x3c\x00\x62\x00\x3e\x00\x61\x00\x6e\x00\x6d\x00\x65\x00\x6c\
-\x00\x64\x00\x65\x00\x6e\x00\x3c\x00\x2f\x00\x62\x00\x3e\x00\x20\
-\x00\x6f\x00\x64\x00\x65\x00\x72\x00\x20\x00\x6d\x00\x69\x00\x74\
-\x00\x20\x00\x65\x00\x69\x00\x6e\x00\x65\x00\x6d\x00\x20\x00\x62\
-\x00\x65\x00\x73\x00\x74\x00\x65\x00\x68\x00\x65\x00\x6e\x00\x64\
-\x00\x65\x00\x6e\x00\x20\x00\x55\x00\x73\x00\x65\x00\x72\x00\x6e\
-\x00\x61\x00\x6d\x00\x65\x00\x6e\x00\x20\x00\x3c\x00\x62\x00\x3e\
-\x00\x65\x00\x69\x00\x6e\x00\x6c\x00\x6f\x00\x67\x00\x67\x00\x65\
-\x00\x6e\x00\x3c\x00\x2f\x00\x62\x00\x3e\x00\x3f\x08\x00\x00\x00\
-\x00\x06\x00\x00\x01\x5d\x4e\x6f\x77\x20\x77\x65\x20\x77\x69\x6c\
-\x6c\x20\x67\x75\x69\x64\x65\x20\x79\x6f\x75\x20\x74\x68\x72\x6f\
-\x75\x67\x68\x20\x73\x6f\x6d\x65\x20\x63\x6f\x6e\x66\x69\x67\x75\
-\x72\x61\x74\x69\x6f\x6e\x20\x74\x68\x61\x74\x20\x69\x73\x20\x6e\
-\x65\x65\x64\x65\x64\x20\x62\x65\x66\x6f\x72\x65\x20\x79\x6f\x75\
-\x20\x63\x61\x6e\x20\x63\x6f\x6e\x6e\x65\x63\x74\x20\x66\x6f\x72\
-\x20\x74\x68\x65\x20\x66\x69\x72\x73\x74\x20\x74\x69\x6d\x65\x2e\
-\x3c\x62\x72\x3e\x3c\x62\x72\x3e\x49\x66\x20\x79\x6f\x75\x20\x65\
-\x76\x65\x72\x20\x6e\x65\x65\x64\x20\x74\x6f\x20\x6d\x6f\x64\x69\
-\x66\x79\x20\x74\x68\x65\x73\x65\x20\x6f\x70\x74\x69\x6f\x6e\x73\
-\x20\x61\x67\x61\x69\x6e\x2c\x20\x79\x6f\x75\x20\x63\x61\x6e\x20\
-\x66\x69\x6e\x64\x20\x74\x68\x65\x20\x77\x69\x7a\x61\x72\x64\x20\
-\x69\x6e\x20\x74\x68\x65\x20\x27\x3c\x69\x3e\x53\x65\x74\x74\x69\
-\x6e\x67\x73\x3c\x2f\x69\x3e\x27\x20\x6d\x65\x6e\x75\x20\x66\x72\
-\x6f\x6d\x20\x74\x68\x65\x20\x6d\x61\x69\x6e\x20\x77\x69\x6e\x64\
-\x6f\x77\x2e\x3c\x62\x72\x3e\x3c\x62\x72\x3e\x44\x6f\x20\x79\x6f\
-\x75\x20\x77\x61\x6e\x74\x20\x74\x6f\x20\x3c\x62\x3e\x73\x69\x67\
-\x6e\x20\x75\x70\x3c\x2f\x62\x3e\x20\x66\x6f\x72\x20\x61\x20\x6e\
-\x65\x77\x20\x61\x63\x63\x6f\x75\x6e\x74\x2c\x20\x6f\x72\x20\x3c\
-\x62\x3e\x6c\x6f\x67\x20\x69\x6e\x3c\x2f\x62\x3e\x20\x77\x69\x74\
-\x68\x20\x61\x6e\x20\x61\x6c\x72\x65\x61\x64\x79\x20\x65\x78\x69\
-\x73\x74\x69\x6e\x67\x20\x75\x73\x65\x72\x6e\x61\x6d\x65\x3f\x3c\
-\x62\x72\x3e\x07\x00\x00\x00\x09\x49\x6e\x74\x72\x6f\x50\x61\x67\
-\x65\x01\x03\x00\x00\x00\x42\x00\x46\x00\xfc\x00\x72\x00\x20\x00\
-\x65\x00\x69\x00\x6e\x00\x65\x00\x6e\x00\x20\x00\x6e\x00\x65\x00\
-\x75\x00\x65\x00\x6e\x00\x20\x00\x41\x00\x63\x00\x63\x00\x6f\x00\
-\x75\x00\x6e\x00\x74\x00\x20\x00\x61\x00\x6e\x00\x6d\x00\x65\x00\
-\x6c\x00\x64\x00\x65\x00\x6e\x00\x2e\x08\x00\x00\x00\x00\x06\x00\
-\x00\x00\x1a\x53\x69\x67\x6e\x20\x75\x70\x20\x66\x6f\x72\x20\x61\
-\x20\x6e\x65\x77\x20\x61\x63\x63\x6f\x75\x6e\x74\x2e\x07\x00\x00\
-\x00\x09\x49\x6e\x74\x72\x6f\x50\x61\x67\x65\x01\x03\x00\x00\x00\
-\x38\x00\x41\x00\x75\x00\x74\x00\x68\x00\x65\x00\x6e\x00\x74\x00\
-\x69\x00\x66\x00\x69\x00\x7a\x00\x69\x00\x65\x00\x72\x00\x75\x00\
-\x6e\x00\x67\x00\x73\x00\x66\x00\x65\x00\x68\x00\x6c\x00\x65\x00\
-\x72\x00\x3a\x00\x20\x00\x25\x00\x73\x08\x00\x00\x00\x00\x06\x00\
-\x00\x00\x18\x41\x75\x74\x68\x65\x6e\x74\x69\x63\x61\x74\x69\x6f\
-\x6e\x20\x65\x72\x72\x6f\x72\x3a\x20\x25\x73\x07\x00\x00\x00\x09\
-\x4c\x6f\x67\x49\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x2a\x00\
-\x41\x00\x6e\x00\x6d\x00\x65\x00\x6c\x00\x64\x00\x65\x00\x64\x00\
-\x61\x00\x74\x00\x65\x00\x6e\x00\x20\x00\x6b\x00\x6f\x00\x72\x00\
-\x72\x00\x65\x00\x6b\x00\x74\x00\x2e\x08\x00\x00\x00\x00\x06\x00\
-\x00\x00\x16\x43\x72\x65\x64\x65\x6e\x74\x69\x61\x6c\x73\x20\x76\
-\x61\x6c\x69\x64\x61\x74\x65\x64\x2e\x07\x00\x00\x00\x09\x4c\x6f\
-\x67\x49\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x34\x00\x41\x00\
-\x75\x00\x66\x00\x6c\x00\xf6\x00\x73\x00\x65\x00\x6e\x00\x20\x00\
-\x64\x00\x65\x00\x73\x00\x20\x00\x44\x00\x6f\x00\x6d\x00\x61\x00\
-\x69\x00\x6e\x00\x2d\x00\x4e\x00\x61\x00\x6d\x00\x65\x00\x6e\x00\
-\x73\x08\x00\x00\x00\x00\x06\x00\x00\x00\x15\x52\x65\x73\x6f\x6c\
-\x76\x69\x6e\x67\x20\x64\x6f\x6d\x61\x69\x6e\x20\x6e\x61\x6d\x65\
-\x07\x00\x00\x00\x09\x4c\x6f\x67\x49\x6e\x50\x61\x67\x65\x01\x03\
-\x00\x00\x00\x6a\x00\x44\x00\x65\x00\x72\x00\x20\x00\x55\x00\x73\
-\x00\x65\x00\x72\x00\x6e\x00\x61\x00\x6d\x00\x65\x00\x20\x00\x6d\
-\x00\x75\x00\x73\x00\x73\x00\x20\x00\x69\x00\x6e\x00\x20\x00\x64\
-\x00\x65\x00\x72\x00\x20\x00\x46\x00\x6f\x00\x72\x00\x6d\x00\x20\
-\x00\x75\x00\x73\x00\x65\x00\x72\x00\x6e\x00\x61\x00\x6d\x00\x65\
-\x00\x40\x00\x70\x00\x72\x00\x6f\x00\x76\x00\x69\x00\x64\x00\x65\
-\x00\x72\x00\x20\x00\x73\x00\x65\x00\x69\x00\x6e\x00\x2e\x08\x00\
-\x00\x00\x00\x06\x00\x00\x00\x2f\x55\x73\x65\x72\x6e\x61\x6d\x65\
-\x20\x6d\x75\x73\x74\x20\x62\x65\x20\x69\x6e\x20\x74\x68\x65\x20\
-\x75\x73\x65\x72\x6e\x61\x6d\x65\x40\x70\x72\x6f\x76\x69\x64\x65\
-\x72\x20\x66\x6f\x72\x6d\x2e\x07\x00\x00\x00\x09\x4c\x6f\x67\x49\
-\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x1a\x00\x50\x00\x72\x00\
-\x6f\x00\x76\x00\x69\x00\x64\x00\x65\x00\x72\x00\x2d\x00\x69\x00\
-\x6e\x00\x66\x00\x6f\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0d\x50\
-\x72\x6f\x76\x69\x64\x65\x72\x20\x49\x6e\x66\x6f\x07\x00\x00\x00\
-\x10\x50\x72\x6f\x76\x69\x64\x65\x72\x49\x6e\x66\x6f\x50\x61\x67\
-\x65\x01\x03\x00\x00\x00\x3e\x00\x44\x00\x61\x00\x73\x00\x20\x00\
-\x69\x00\x73\x00\x74\x00\x2c\x00\x20\x00\x77\x00\x61\x00\x73\x00\
-\x20\x00\x64\x00\x65\x00\x72\x00\x20\x00\x50\x00\x72\x00\x6f\x00\
-\x76\x00\x69\x00\x64\x00\x65\x00\x72\x00\x20\x00\x73\x00\x61\x00\
-\x67\x00\x74\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1b\x54\
-\x68\x69\x73\x20\x69\x73\x20\x77\x68\x61\x74\x20\x70\x72\x6f\x76\
-\x69\x64\x65\x72\x20\x73\x61\x79\x73\x2e\x07\x00\x00\x00\x10\x50\
-\x72\x6f\x76\x69\x64\x65\x72\x49\x6e\x66\x6f\x50\x61\x67\x65\x01\
-\x03\x00\x00\x00\x30\x00\xdc\x00\x62\x00\x65\x00\x72\x00\x70\x00\
-\x72\x00\xfc\x00\x66\x00\x65\x00\x20\x00\x43\x00\x41\x00\x2d\x00\
-\x46\x00\x69\x00\x6e\x00\x67\x00\x65\x00\x72\x00\x70\x00\x72\x00\
-\x69\x00\x6e\x00\x74\x08\x00\x00\x00\x00\x06\x00\x00\x00\x17\x43\
-\x68\x65\x63\x6b\x69\x6e\x67\x20\x43\x41\x20\x66\x69\x6e\x67\x65\
-\x72\x70\x72\x69\x6e\x74\x07\x00\x00\x00\x1b\x50\x72\x6f\x76\x69\
-\x64\x65\x72\x53\x65\x74\x75\x70\x56\x61\x6c\x69\x64\x61\x74\x69\
-\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x2e\x00\x46\x00\xfc\
-\x00\x68\x00\x72\x00\x65\x00\x20\x00\x61\x00\x75\x00\x74\x00\x6f\
-\x00\x63\x00\x6f\x00\x6e\x00\x66\x00\x69\x00\x67\x00\x20\x00\x64\
-\x00\x75\x00\x72\x00\x63\x00\x68\x00\x2e\x08\x00\x00\x00\x00\x06\
-\x00\x00\x00\x11\x44\x6f\x69\x6e\x67\x20\x61\x75\x74\x6f\x63\x6f\
-\x6e\x66\x69\x67\x2e\x07\x00\x00\x00\x1b\x50\x72\x6f\x76\x69\x64\
-\x65\x72\x53\x65\x74\x75\x70\x56\x61\x6c\x69\x64\x61\x74\x69\x6f\
-\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x24\x00\x48\x00\x6f\x00\
-\x6c\x00\x65\x00\x20\x00\x43\x00\x41\x00\x2d\x00\x5a\x00\x65\x00\
-\x72\x00\x74\x00\x69\x00\x66\x00\x69\x00\x6b\x00\x61\x00\x74\x08\
-\x00\x00\x00\x00\x06\x00\x00\x00\x17\x46\x65\x74\x63\x68\x69\x6e\
-\x67\x20\x43\x41\x20\x63\x65\x72\x74\x69\x66\x69\x63\x61\x74\x65\
-\x07\x00\x00\x00\x1b\x50\x72\x6f\x76\x69\x64\x65\x72\x53\x65\x74\
-\x75\x70\x56\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\x50\x61\x67\x65\
-\x01\x03\x00\x00\x00\x1c\x00\x50\x00\x72\x00\x6f\x00\x76\x00\x69\
-\x00\x64\x00\x65\x00\x72\x00\x2d\x00\x53\x00\x65\x00\x74\x00\x75\
-\x00\x70\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0e\x50\x72\x6f\x76\
-\x69\x64\x65\x72\x20\x73\x65\x74\x75\x70\x07\x00\x00\x00\x1b\x50\
-\x72\x6f\x76\x69\x64\x65\x72\x53\x65\x74\x75\x70\x56\x61\x6c\x69\
-\x64\x61\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x30\
-\x00\xdc\x00\x62\x00\x65\x00\x72\x00\x70\x00\x72\x00\xfc\x00\x66\
-\x00\x65\x00\x20\x00\x41\x00\x50\x00\x49\x00\x2d\x00\x5a\x00\x65\
-\x00\x72\x00\x74\x00\x69\x00\x66\x00\x69\x00\x6b\x00\x61\x00\x74\
-\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1a\x56\x61\x6c\x69\x64\x61\
-\x74\x69\x6e\x67\x20\x61\x70\x69\x20\x63\x65\x72\x74\x69\x66\x69\
-\x63\x61\x74\x65\x07\x00\x00\x00\x1b\x50\x72\x6f\x76\x69\x64\x65\
-\x72\x53\x65\x74\x75\x70\x56\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\
-\x50\x61\x67\x65\x01\x03\x00\x00\x00\x50\x00\x4b\x00\x6f\x00\x6e\
-\x00\x6e\x00\x74\x00\x65\x00\x20\x00\x6e\x00\x69\x00\x63\x00\x68\
-\x00\x74\x00\x20\x00\x72\x00\x65\x00\x67\x00\x69\x00\x73\x00\x74\
-\x00\x72\x00\x69\x00\x65\x00\x72\x00\x65\x00\x6e\x00\x20\x00\x28\
-\x00\x62\x00\x61\x00\x64\x00\x20\x00\x72\x00\x65\x00\x73\x00\x70\
-\x00\x6f\x00\x6e\x00\x73\x00\x65\x00\x29\x08\x00\x00\x00\x00\x06\
-\x00\x00\x00\x21\x43\x6f\x75\x6c\x64\x20\x6e\x6f\x74\x20\x72\x65\
-\x67\x69\x73\x74\x65\x72\x20\x28\x62\x61\x64\x20\x72\x65\x73\x70\
-\x6f\x6e\x73\x65\x29\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\
-\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x4e\
-\x00\x56\x00\x65\x00\x72\x00\x62\x00\x69\x00\x6e\x00\x64\x00\x75\
-\x00\x6e\x00\x67\x00\x73\x00\x66\x00\x65\x00\x68\x00\x6c\x00\x65\
-\x00\x72\x00\x20\x00\x7a\x00\x75\x00\x20\x00\x50\x00\x72\x00\x6f\
-\x00\x76\x00\x69\x00\x64\x00\x65\x00\x72\x00\x20\x00\x28\x00\x63\
-\x00\x6f\x00\x6e\x00\x6e\x00\x65\x00\x72\x00\x72\x00\x29\x08\x00\
-\x00\x00\x00\x06\x00\x00\x00\x27\x45\x72\x72\x6f\x72\x20\x43\x6f\
-\x6e\x6e\x65\x63\x74\x69\x6e\x67\x20\x74\x6f\x20\x70\x72\x6f\x76\
-\x69\x64\x65\x72\x20\x28\x63\x6f\x6e\x6e\x65\x72\x72\x29\x2e\x07\
-\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\
-\x50\x61\x67\x65\x01\x03\x00\x00\x00\x4e\x00\x56\x00\x65\x00\x72\
-\x00\x62\x00\x69\x00\x6e\x00\x64\x00\x75\x00\x6e\x00\x67\x00\x73\
-\x00\x66\x00\x65\x00\x68\x00\x6c\x00\x65\x00\x72\x00\x20\x00\x7a\
-\x00\x75\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x76\x00\x69\x00\x64\
-\x00\x65\x00\x72\x00\x20\x00\x28\x00\x74\x00\x69\x00\x6d\x00\x65\
-\x00\x6f\x00\x75\x00\x74\x00\x29\x08\x00\x00\x00\x00\x06\x00\x00\
-\x00\x26\x45\x72\x72\x6f\x72\x20\x63\x6f\x6e\x6e\x65\x63\x74\x69\
-\x6e\x67\x20\x74\x6f\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x20\x28\
-\x74\x69\x6d\x65\x6f\x75\x74\x29\x07\x00\x00\x00\x10\x52\x65\x67\
-\x69\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\
-\x00\x00\x4a\x00\x46\x00\x65\x00\x68\x00\x6c\x00\x65\x00\x72\x00\
-\x20\x00\x77\x00\xe4\x00\x68\x00\x72\x00\x65\x00\x6e\x00\x64\x00\
-\x20\x00\x64\x00\x65\x00\x72\x00\x20\x00\x52\x00\x65\x00\x67\x00\
-\x69\x00\x73\x00\x74\x00\x72\x00\x69\x00\x65\x00\x72\x00\x75\x00\
-\x6e\x00\x67\x00\x20\x00\x28\x00\x25\x00\x73\x00\x29\x08\x00\x00\
-\x00\x00\x06\x00\x00\x00\x1e\x45\x72\x72\x6f\x72\x20\x64\x75\x72\
-\x69\x6e\x67\x20\x72\x65\x67\x69\x73\x74\x72\x61\x74\x69\x6f\x6e\
-\x20\x28\x25\x73\x29\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\
-\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x3c\
-\x00\x50\x00\x61\x00\x73\x00\x73\x00\x77\x00\x6f\x00\x72\x00\x74\
-\x00\x20\x00\x73\x00\x74\x00\x69\x00\x6d\x00\x6d\x00\x74\x00\x20\
-\x00\x6e\x00\x69\x00\x63\x00\x68\x00\x74\x00\x20\x00\xfc\x00\x62\
-\x00\x65\x00\x72\x00\x69\x00\x65\x00\x6e\x00\x2e\x08\x00\x00\x00\
-\x00\x06\x00\x00\x00\x19\x50\x61\x73\x73\x77\x6f\x72\x64\x20\x64\
-\x6f\x65\x73\x20\x6e\x6f\x74\x20\x6d\x61\x74\x63\x68\x2e\x2e\x07\
-\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\
-\x50\x61\x67\x65\x01\x03\x00\x00\x00\x26\x00\x50\x00\x61\x00\x73\
-\x00\x73\x00\x77\x00\x6f\x00\x72\x00\x74\x00\x20\x00\x7a\x00\x75\
-\x00\x20\x00\x73\x00\x69\x00\x6d\x00\x70\x00\x65\x00\x6c\x00\x2e\
-\x08\x00\x00\x00\x00\x06\x00\x00\x00\x15\x50\x61\x73\x73\x77\x6f\
-\x72\x64\x20\x74\x6f\x6f\x20\x6f\x62\x76\x69\x6f\x75\x73\x2e\x07\
-\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\
-\x50\x61\x67\x65\x01\x03\x00\x00\x00\x20\x00\x50\x00\x61\x00\x73\
-\x00\x73\x00\x77\x00\x6f\x00\x72\x00\x74\x00\x20\x00\x7a\x00\x75\
-\x00\x20\x00\x6b\x00\x75\x00\x72\x00\x7a\x08\x00\x00\x00\x00\x06\
-\x00\x00\x00\x13\x50\x61\x73\x73\x77\x6f\x72\x64\x20\x74\x6f\x6f\
-\x20\x73\x68\x6f\x72\x74\x2e\x07\x00\x00\x00\x10\x52\x65\x67\x69\
-\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\
-\x00\x58\x00\x52\x00\x65\x00\x67\x00\x69\x00\x73\x00\x74\x00\x72\
-\x00\x69\x00\x65\x00\x72\x00\x65\x00\x20\x00\x65\x00\x69\x00\x6e\
-\x00\x65\x00\x6e\x00\x20\x00\x6e\x00\x65\x00\x75\x00\x65\x00\x6e\
-\x00\x20\x00\x55\x00\x73\x00\x65\x00\x72\x00\x20\x00\x62\x00\x65\
-\x00\x69\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x76\x00\x69\x00\x64\
-\x00\x65\x00\x72\x00\x20\x00\x25\x00\x73\x08\x00\x00\x00\x00\x06\
-\x00\x00\x00\x25\x52\x65\x67\x69\x73\x74\x65\x72\x20\x61\x20\x6e\
-\x65\x77\x20\x75\x73\x65\x72\x20\x77\x69\x74\x68\x20\x70\x72\x6f\
-\x76\x69\x64\x65\x72\x20\x25\x73\x2e\x07\x00\x00\x00\x10\x52\x65\
-\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\
-\x00\x00\x00\x34\x00\x52\x00\x65\x00\x67\x00\x69\x00\x73\x00\x74\
-\x00\x72\x00\x69\x00\x65\x00\x72\x00\x75\x00\x6e\x00\x67\x00\x20\
-\x00\x65\x00\x72\x00\x66\x00\x6f\x00\x6c\x00\x67\x00\x72\x00\x65\
-\x00\x69\x00\x63\x00\x68\x00\x21\x08\x00\x00\x00\x00\x06\x00\x00\
-\x00\x17\x52\x65\x67\x69\x73\x74\x72\x61\x74\x69\x6f\x6e\x20\x73\
-\x75\x63\x63\x65\x65\x64\x65\x64\x21\x07\x00\x00\x00\x10\x52\x65\
-\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\
-\x00\x00\x00\x10\x00\x41\x00\x6e\x00\x6d\x00\x65\x00\x6c\x00\x64\
-\x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x07\x53\x69\
-\x67\x6e\x20\x55\x70\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\
-\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x32\
-\x00\x55\x00\x73\x00\x65\x00\x72\x00\x6e\x00\x61\x00\x6d\x00\x65\
-\x00\x20\x00\x6e\x00\x69\x00\x63\x00\x68\x00\x74\x00\x20\x00\x76\
-\x00\x65\x00\x72\x00\x66\x00\xfc\x00\x67\x00\x62\x00\x61\x00\x72\
-\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x17\x55\x73\x65\x72\
-\x6e\x61\x6d\x65\x20\x6e\x6f\x74\x20\x61\x76\x61\x69\x6c\x61\x62\
-\x6c\x65\x2e\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\
-\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x38\x00\x41\
-\x00\x75\x00\x74\x00\x68\x00\x65\x00\x6e\x00\x74\x00\x69\x00\x66\
-\x00\x69\x00\x7a\x00\x69\x00\x65\x00\x72\x00\x75\x00\x6e\x00\x67\
-\x00\x73\x00\x66\x00\x65\x00\x68\x00\x6c\x00\x65\x00\x72\x00\x3a\
-\x00\x20\x00\x25\x00\x73\x08\x00\x00\x00\x00\x06\x00\x00\x00\x18\
-\x41\x75\x74\x68\x65\x6e\x74\x69\x63\x61\x74\x69\x6f\x6e\x20\x65\
-\x72\x72\x6f\x72\x3a\x20\x25\x73\x07\x00\x00\x00\x1a\x52\x65\x67\
-\x69\x73\x74\x65\x72\x55\x73\x65\x72\x56\x61\x6c\x69\x64\x61\x74\
-\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x26\x00\x48\x00\
-\x6f\x00\x6c\x00\x65\x00\x20\x00\x45\x00\x49\x00\x50\x00\x2d\x00\
-\x5a\x00\x65\x00\x72\x00\x74\x00\x69\x00\x66\x00\x69\x00\x6b\x00\
-\x61\x00\x74\x08\x00\x00\x00\x00\x06\x00\x00\x00\x18\x46\x65\x74\
-\x63\x68\x69\x6e\x67\x20\x65\x69\x70\x20\x63\x65\x72\x74\x69\x66\
-\x69\x63\x61\x74\x65\x07\x00\x00\x00\x1a\x52\x65\x67\x69\x73\x74\
-\x65\x72\x55\x73\x65\x72\x56\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\
-\x50\x61\x67\x65\x01\x03\x00\x00\x00\x3c\x00\x48\x00\x6f\x00\x6c\
-\x00\x65\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x76\x00\x69\x00\x64\
-\x00\x65\x00\x72\x00\x2d\x00\x4b\x00\x6f\x00\x6e\x00\x66\x00\x69\
-\x00\x67\x00\x75\x00\x72\x00\x61\x00\x74\x00\x69\x00\x6f\x00\x6e\
-\x00\x2e\x00\x2e\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1b\
-\x46\x65\x74\x63\x68\x69\x6e\x67\x20\x70\x72\x6f\x76\x69\x64\x65\
-\x72\x20\x63\x6f\x6e\x66\x69\x67\x2e\x2e\x2e\x07\x00\x00\x00\x1a\
-\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\x56\x61\x6c\x69\
-\x64\x61\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x2c\
-\x00\x5a\x00\x65\x00\x72\x00\x74\x00\x69\x00\x66\x00\x69\x00\x6b\
-\x00\x61\x00\x74\x00\x73\x00\xfc\x00\x62\x00\x65\x00\x72\x00\x70\
-\x00\x72\x00\xfc\x00\x66\x00\x75\x00\x6e\x00\x67\x08\x00\x00\x00\
-\x00\x06\x00\x00\x00\x16\x43\x65\x72\x74\x69\x66\x69\x63\x61\x74\
-\x65\x20\x76\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\x07\x00\x00\x00\
-\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\
-\x61\x67\x65\x01\x03\x00\x00\x00\x72\x00\x4b\x00\x6f\x00\x6e\x00\
-\x6e\x00\x74\x00\x65\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x76\x00\
-\x69\x00\x64\x00\x65\x00\x72\x00\x2d\x00\x49\x00\x6e\x00\x66\x00\
-\x6f\x00\x20\x00\x6e\x00\x69\x00\x63\x00\x68\x00\x74\x00\x20\x00\
-\x68\x00\x65\x00\x72\x00\x75\x00\x6e\x00\x74\x00\x65\x00\x72\x00\
-\x6c\x00\x61\x00\x64\x00\x65\x00\x6e\x00\x20\x00\x28\x00\x72\x00\
-\x65\x00\x66\x00\x75\x00\x73\x00\x65\x00\x64\x00\x20\x00\x63\x00\
-\x6f\x00\x6e\x00\x6e\x00\x2e\x00\x29\x00\x2e\x08\x00\x00\x00\x00\
-\x06\x00\x00\x00\x31\x43\x6f\x75\x6c\x64\x20\x6e\x6f\x74\x20\x64\
-\x6f\x77\x6e\x6c\x6f\x61\x64\x20\x70\x72\x6f\x76\x69\x64\x65\x72\
-\x20\x69\x6e\x66\x6f\x20\x28\x72\x65\x66\x75\x73\x65\x64\x20\x63\
-\x6f\x6e\x6e\x2e\x29\x2e\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\
-\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\
-\x00\x00\x5e\x00\x4b\x00\x6f\x00\x6e\x00\x6e\x00\x74\x00\x65\x00\
-\x20\x00\x6b\x00\x65\x00\x69\x00\x6e\x00\x65\x00\x20\x00\x49\x00\
-\x6e\x00\x66\x00\x6f\x00\x72\x00\x6d\x00\x61\x00\x74\x00\x69\x00\
-\x6f\x00\x6e\x00\x20\x00\x76\x00\x6f\x00\x6d\x00\x20\x00\x50\x00\
-\x72\x00\x6f\x00\x76\x00\x69\x00\x64\x00\x65\x00\x72\x00\x20\x00\
-\x62\x00\x65\x00\x6b\x00\x6f\x00\x6d\x00\x6d\x00\x65\x00\x6e\x00\
-\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x21\x43\x6f\x75\x6c\x64\
-\x20\x6e\x6f\x74\x20\x67\x65\x74\x20\x69\x6e\x66\x6f\x20\x66\x72\
-\x6f\x6d\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x2e\x07\x00\x00\x00\
-\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\
-\x61\x67\x65\x01\x03\x00\x00\x00\x20\x00\x47\x00\x69\x00\x62\x00\
-\x20\x00\x50\x00\x72\x00\x6f\x00\x76\x00\x69\x00\x64\x00\x65\x00\
-\x72\x00\x20\x00\x65\x00\x69\x00\x6e\x08\x00\x00\x00\x00\x06\x00\
-\x00\x00\x0e\x45\x6e\x74\x65\x72\x20\x50\x72\x6f\x76\x69\x64\x65\
-\x72\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\
-\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\xa6\x00\x42\
-\x00\x69\x00\x74\x00\x74\x00\x65\x00\x20\x00\x67\x00\x69\x00\x62\
-\x00\x20\x00\x64\x00\x69\x00\x65\x00\x20\x00\x44\x00\x6f\x00\x6d\
-\x00\x61\x00\x69\x00\x6e\x00\x20\x00\x64\x00\x65\x00\x73\x00\x20\
-\x00\x50\x00\x72\x00\x6f\x00\x76\x00\x69\x00\x64\x00\x65\x00\x72\
-\x00\x73\x00\x20\x00\x61\x00\x6e\x00\x2c\x00\x20\x00\x64\x00\x65\
-\x00\x6e\x00\x20\x00\x64\x00\x75\x00\x20\x00\x66\x00\xfc\x00\x72\
-\x00\x20\x00\x64\x00\x65\x00\x69\x00\x6e\x00\x65\x00\x20\x00\x56\
-\x00\x65\x00\x72\x00\x62\x00\x69\x00\x6e\x00\x64\x00\x75\x00\x6e\
-\x00\x67\x00\x20\x00\x6e\x00\x75\x00\x74\x00\x7a\x00\x65\x00\x6e\
-\x00\x20\x00\x6d\x00\xf6\x00\x63\x00\x68\x00\x74\x00\x65\x00\x73\
-\x00\x74\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x4c\x50\x6c\
-\x65\x61\x73\x65\x20\x65\x6e\x74\x65\x72\x20\x74\x68\x65\x20\x64\
-\x6f\x6d\x61\x69\x6e\x20\x6f\x66\x20\x74\x68\x65\x20\x70\x72\x6f\
-\x76\x69\x64\x65\x72\x20\x79\x6f\x75\x20\x77\x61\x6e\x74\x20\x74\
-\x6f\x20\x75\x73\x65\x20\x66\x6f\x72\x20\x79\x6f\x75\x72\x20\x63\
-\x6f\x6e\x6e\x65\x63\x74\x69\x6f\x6e\x2e\x07\x00\x00\x00\x12\x53\
-\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\
-\x65\x01\x03\x00\x00\x00\x60\x00\x53\x00\x65\x00\x72\x00\x76\x00\
-\x65\x00\x72\x00\x2d\x00\x5a\x00\x65\x00\x72\x00\x74\x00\x69\x00\
-\x66\x00\x69\x00\x6b\x00\x61\x00\x74\x00\x20\x00\x6b\x00\x6f\x00\
-\x6e\x00\x6e\x00\x74\x00\x65\x00\x20\x00\x6e\x00\x69\x00\x63\x00\
-\x68\x00\x74\x00\x20\x00\x62\x00\x65\x00\x73\x00\x74\x00\xe4\x00\
-\x74\x00\x69\x00\x67\x00\x74\x00\x20\x00\x77\x00\x65\x00\x72\x00\
-\x64\x00\x65\x00\x6e\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\
-\x29\x53\x65\x72\x76\x65\x72\x20\x63\x65\x72\x74\x69\x66\x69\x63\
-\x61\x74\x65\x20\x63\x6f\x75\x6c\x64\x20\x6e\x6f\x74\x20\x62\x65\
-\x20\x76\x65\x72\x69\x66\x69\x65\x64\x2e\x07\x00\x00\x00\x12\x53\
-\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\
-\x65\x01\x03\x00\x00\x00\x22\x00\x50\x00\x72\x00\xfc\x00\x66\x00\
-\x65\x00\x20\x00\x44\x00\x6f\x00\x6d\x00\x61\x00\x69\x00\x6e\x00\
-\x2d\x00\x4e\x00\x61\x00\x6d\x00\x65\x08\x00\x00\x00\x00\x06\x00\
-\x00\x00\x14\x63\x68\x65\x63\x6b\x69\x6e\x67\x20\x64\x6f\x6d\x61\
-\x69\x6e\x20\x6e\x61\x6d\x65\x07\x00\x00\x00\x12\x53\x65\x6c\x65\
-\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\
-\x00\x00\x00\x2c\x00\x50\x00\x72\x00\xfc\x00\x66\x00\x65\x00\x20\
-\x00\x48\x00\x54\x00\x54\x00\x50\x00\x53\x00\x2d\x00\x56\x00\x65\
-\x00\x72\x00\x62\x00\x69\x00\x6e\x00\x64\x00\x75\x00\x6e\x00\x67\
-\x08\x00\x00\x00\x00\x06\x00\x00\x00\x19\x63\x68\x65\x63\x6b\x69\
-\x6e\x67\x20\x68\x74\x74\x70\x73\x20\x63\x6f\x6e\x6e\x65\x63\x74\
-\x69\x6f\x6e\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\
-\x6f\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x24\
-\x00\x48\x00\x6f\x00\x6c\x00\x65\x00\x20\x00\x50\x00\x72\x00\x6f\
-\x00\x76\x00\x69\x00\x64\x00\x65\x00\x72\x00\x2d\x00\x49\x00\x6e\
-\x00\x66\x00\x6f\x08\x00\x00\x00\x00\x06\x00\x00\x00\x16\x66\x65\
-\x74\x63\x68\x69\x6e\x67\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x20\
-\x69\x6e\x66\x6f\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\
-\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x88\x00\x00\x00\
-\x02\x01\x01\
-\x00\x00\x34\x30\
-\x3c\
-\xb8\x64\x18\xca\xef\x9c\x95\xcd\x21\x1c\xbf\x60\xa1\xbd\xdd\x42\
-\x00\x00\x02\xb8\x00\x2b\x8c\x04\x00\x00\x2d\xfe\x00\x47\x96\xc4\
-\x00\x00\x2e\xa2\x00\x8f\x9b\xbe\x00\x00\x2c\x71\x00\xa1\xec\xce\
-\x00\x00\x31\x03\x00\xa1\xec\xee\x00\x00\x00\x6f\x00\xe0\x8f\x2e\
-\x00\x00\x15\xe4\x00\xe2\xaf\xee\x00\x00\x2f\xaa\x01\x29\x68\x74\
-\x00\x00\x03\x16\x01\x30\x21\x7e\x00\x00\x1f\x3f\x01\xa2\xd7\xb4\
-\x00\x00\x0c\xce\x01\xfb\x30\xd4\x00\x00\x1a\x99\x02\x02\xd1\x23\
-\x00\x00\x01\x27\x02\x2c\xac\xe9\x00\x00\x20\xec\x02\x6e\x0f\xe5\
-\x00\x00\x1c\xc8\x02\x73\x64\x0e\x00\x00\x0d\x30\x02\x87\x60\x9e\
-\x00\x00\x19\xb7\x02\xa7\x96\xc4\x00\x00\x2d\x67\x02\xa8\xff\xca\
-\x00\x00\x19\x7e\x02\xa8\xff\xca\x00\x00\x25\x36\x02\xcc\x9d\x3e\
-\x00\x00\x08\x17\x02\xf2\xe0\x59\x00\x00\x1f\xc0\x03\x92\xb4\xc3\
-\x00\x00\x18\x9b\x04\x20\xed\x72\x00\x00\x01\xcb\x04\x4b\xd3\xa9\
-\x00\x00\x1b\xdd\x04\x65\x87\x6e\x00\x00\x25\xde\x04\xd4\x45\xee\
-\x00\x00\x22\xe8\x04\xdf\xfa\xae\x00\x00\x23\xc6\x05\x20\x54\x63\
-\x00\x00\x0a\xff\x05\x35\x94\xfe\x00\x00\x18\x3b\x05\x35\x96\xfe\
-\x00\x00\x18\x6b\x05\xb7\x8f\x59\x00\x00\x21\x89\x06\x1c\x1c\x8e\
-\x00\x00\x1a\xe0\x06\x3e\x6a\x9e\x00\x00\x17\xd9\x06\x40\xa8\x7e\
-\x00\x00\x20\x4c\x06\x6e\xef\xee\x00\x00\x2a\xea\x06\xc1\xd8\x79\
-\x00\x00\x04\x28\x06\xc6\x14\x44\x00\x00\x2e\xe6\x06\xf6\x2e\x2e\
-\x00\x00\x2e\x3c\x06\xf9\xb6\xf5\x00\x00\x0b\xc9\x07\xea\x0d\xae\
-\x00\x00\x2d\xae\x08\x13\xe8\xae\x00\x00\x22\x6a\x08\x2f\x36\x66\
-\x00\x00\x30\x90\x08\x62\xc8\x1e\x00\x00\x26\xe9\x08\x7a\x64\xee\
-\x00\x00\x28\x0b\x08\x98\x6b\x31\x00\x00\x16\xa4\x08\xe6\x98\x33\
-\x00\x00\x00\x00\x08\xe6\x98\x33\x00\x00\x17\x6f\x09\x5c\x35\xe1\
-\x00\x00\x24\x86\x09\x5e\x19\xbe\x00\x00\x0d\xc3\x09\x6d\xef\xce\
-\x00\x00\x14\xee\x09\x74\x75\x4e\x00\x00\x23\x58\x09\x98\x34\x0e\
-\x00\x00\x28\xcf\x09\xe9\x28\x8a\x00\x00\x22\x08\x09\xeb\x5c\xb1\
-\x00\x00\x2d\x23\x09\xfe\x05\x90\x00\x00\x24\xf2\x0a\x3f\xd3\x94\
-\x00\x00\x0c\x73\x0a\x49\x3f\xe2\x00\x00\x1b\x4b\x0a\x65\x49\x04\
-\x00\x00\x02\x87\x0a\x65\x49\x04\x00\x00\x1a\x5e\x0a\x74\xb8\x1e\
-\x00\x00\x10\x92\x0a\x8f\xfb\x34\x00\x00\x02\xca\x0a\xaf\xd9\x93\
-\x00\x00\x10\x1e\x0a\xb0\xde\xfa\x00\x00\x17\x2f\x0a\xb0\xde\xfa\
-\x00\x00\x1e\xf8\x0a\xf3\x4d\x69\x00\x00\x06\x86\x0b\x17\xf6\xc8\
-\x00\x00\x0a\x26\x0b\x85\xb8\xf6\x00\x00\x30\x1c\x0b\xd6\x50\x84\
-\x00\x00\x0e\x91\x0c\x2d\xc1\xbe\x00\x00\x0f\x35\x0c\x4a\x67\x63\
-\x00\x00\x2f\x36\x0c\x8e\x7b\xb2\x00\x00\x27\x78\x0c\xc0\x94\x05\
-\x00\x00\x1e\x70\x0d\x08\xf9\xb4\x00\x00\x05\x3e\x0d\x0d\x9d\xc5\
-\x00\x00\x19\x11\x0d\x15\x34\x70\x00\x00\x1d\xfc\x0d\xa0\xe6\xa3\
-\x00\x00\x05\xee\x0d\xa2\xd3\x55\x00\x00\x00\xb9\x0d\xc4\x44\x14\
-\x00\x00\x0f\xac\x0d\xd2\xd7\x9e\x00\x00\x29\x61\x0e\x36\x15\x54\
-\x00\x00\x1c\x3b\x0e\x7e\xf5\xee\x00\x00\x25\x76\x0e\x9f\xc2\xc4\
-\x00\x00\x14\x83\x0e\xc0\xbb\x72\x00\x00\x2a\x12\x0f\x0f\x19\xc2\
-\x00\x00\x1d\x4d\x0f\x27\x0d\x6e\x00\x00\x26\x78\x0f\x95\xa0\x4e\
-\x00\x00\x2b\xe4\x0f\xe5\x67\x92\x00\x00\x2a\x6b\x69\x00\x00\x31\
-\x57\x03\x00\x00\x00\x34\x00\x45\x00\x72\x00\x72\x00\x6f\x00\x72\
-\x00\x20\x00\x64\x00\x65\x00\x20\x00\x61\x00\x75\x00\x74\x00\x65\
-\x00\x6e\x00\x74\x00\x69\x00\x63\x00\x61\x00\x63\x00\x69\x00\x6f\
-\x00\x6e\x00\x3a\x00\x20\x00\x25\x00\x73\x08\x00\x00\x00\x00\x06\
-\x00\x00\x00\x18\x41\x75\x74\x68\x65\x6e\x74\x69\x63\x61\x74\x69\
-\x6f\x6e\x20\x65\x72\x72\x6f\x72\x3a\x20\x25\x73\x07\x00\x00\x00\
-\x0e\x43\x6f\x6e\x6e\x65\x63\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\
-\x03\x00\x00\x00\x1a\x00\x43\x00\x6f\x00\x6e\x00\x65\x00\x63\x00\
-\x74\x00\x61\x00\x6e\x00\x64\x00\x6f\x00\x2e\x00\x2e\x00\x2e\x08\
-\x00\x00\x00\x00\x06\x00\x00\x00\x0d\x43\x6f\x6e\x6e\x65\x63\x74\
-\x69\x6e\x67\x2e\x2e\x2e\x07\x00\x00\x00\x0e\x43\x6f\x6e\x6e\x65\
-\x63\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x34\x00\
-\x4f\x00\x62\x00\x74\x00\x65\x00\x6e\x00\x69\x00\x65\x00\x6e\x00\
-\x64\x00\x6f\x00\x20\x00\x63\x00\x65\x00\x72\x00\x74\x00\x69\x00\
-\x66\x00\x69\x00\x63\x00\x61\x00\x64\x00\x6f\x00\x20\x00\x45\x00\
-\x49\x00\x50\x08\x00\x00\x00\x00\x06\x00\x00\x00\x17\x47\x65\x74\
-\x74\x69\x6e\x67\x20\x45\x49\x50\x20\x63\x65\x72\x74\x69\x66\x69\
-\x63\x61\x74\x65\x07\x00\x00\x00\x0e\x43\x6f\x6e\x6e\x65\x63\x74\
-\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x62\x00\x4f\x00\
-\x62\x00\x74\x00\x65\x00\x6e\x00\x69\x00\x65\x00\x6e\x00\x64\x00\
-\x6f\x00\x20\x00\x6c\x00\x6f\x00\x73\x00\x20\x00\x61\x00\x72\x00\
-\x63\x00\x68\x00\x69\x00\x76\x00\x6f\x00\x73\x00\x20\x00\x64\x00\
-\x65\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x66\x00\x69\x00\x67\x00\
-\x75\x00\x72\x00\x61\x00\x63\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\
-\x70\x00\x61\x00\x72\x00\x61\x00\x20\x00\x45\x00\x49\x00\x50\x08\
-\x00\x00\x00\x00\x06\x00\x00\x00\x1f\x47\x65\x74\x74\x69\x6e\x67\
-\x20\x45\x49\x50\x20\x63\x6f\x6e\x66\x69\x67\x75\x72\x61\x74\x69\
-\x6f\x6e\x20\x66\x69\x6c\x65\x73\x07\x00\x00\x00\x0e\x43\x6f\x6e\
-\x6e\x65\x63\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\
-\x66\x00\x45\x00\x73\x00\x74\x00\x61\x00\x62\x00\x6c\x00\x65\x00\
-\x63\x00\x69\x00\x65\x00\x6e\x00\x64\x00\x6f\x00\x20\x00\x75\x00\
-\x6e\x00\x61\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x65\x00\x78\x00\
-\x69\x00\x6f\x00\x6e\x00\x20\x00\x63\x00\x69\x00\x66\x00\x72\x00\
-\x61\x00\x64\x00\x61\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x20\x00\
-\x65\x00\x6c\x00\x20\x00\x70\x00\x72\x00\x6f\x00\x76\x00\x65\x00\
-\x65\x00\x64\x00\x6f\x00\x72\x08\x00\x00\x00\x00\x06\x00\x00\x00\
-\x33\x53\x65\x74\x74\x69\x6e\x67\x20\x75\x70\x20\x61\x20\x65\x6e\
-\x63\x72\x79\x70\x74\x65\x64\x20\x63\x6f\x6e\x6e\x65\x63\x74\x69\
-\x6f\x6e\x20\x77\x69\x74\x68\x20\x74\x68\x65\x20\x70\x72\x6f\x76\
-\x69\x64\x65\x72\x07\x00\x00\x00\x0e\x43\x6f\x6e\x6e\x65\x63\x74\
-\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x12\x00\x26\x00\
-\x43\x00\x6f\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x61\x00\x72\x08\
-\x00\x00\x00\x00\x06\x00\x00\x00\x08\x26\x43\x6f\x6e\x6e\x65\x63\
-\x74\x07\x00\x00\x00\x14\x45\x49\x50\x43\x6f\x6e\x64\x75\x63\x74\
-\x6f\x72\x41\x70\x70\x4d\x69\x78\x69\x6e\x01\x03\x00\x00\x00\x18\
-\x00\x26\x00\x44\x00\x65\x00\x73\x00\x63\x00\x6f\x00\x6e\x00\x65\
-\x00\x63\x00\x74\x00\x61\x00\x72\x08\x00\x00\x00\x00\x06\x00\x00\
-\x00\x0b\x26\x44\x69\x73\x63\x6f\x6e\x6e\x65\x63\x74\x07\x00\x00\
-\x00\x14\x45\x49\x50\x43\x6f\x6e\x64\x75\x63\x74\x6f\x72\x41\x70\
-\x70\x4d\x69\x78\x69\x6e\x01\x03\x00\x00\x00\x9e\x00\x53\x00\x65\
-\x00\x20\x00\x68\x00\x61\x00\x20\x00\x64\x00\x65\x00\x74\x00\x65\
-\x00\x63\x00\x74\x00\x61\x00\x64\x00\x6f\x00\x20\x00\x6f\x00\x74\
-\x00\x72\x00\x6f\x00\x20\x00\x70\x00\x72\x00\x6f\x00\x63\x00\x65\
-\x00\x73\x00\x6f\x00\x20\x00\x4f\x00\x70\x00\x65\x00\x6e\x00\x56\
-\x00\x50\x00\x4e\x00\x2e\x00\x20\x00\x41\x00\x63\x00\x61\x00\x62\
-\x00\x61\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x20\x00\x65\x00\x6c\
-\x00\x20\x00\x61\x00\x6e\x00\x74\x00\x65\x00\x73\x00\x20\x00\x64\
-\x00\x65\x00\x20\x00\x69\x00\x6e\x00\x69\x00\x63\x00\x69\x00\x61\
-\x00\x72\x00\x20\x00\x6c\x00\x65\x00\x61\x00\x70\x00\x2d\x00\x63\
-\x00\x6c\x00\x69\x00\x65\x00\x6e\x00\x74\x08\x00\x00\x00\x00\x06\
-\x00\x00\x00\x56\x41\x6e\x6f\x74\x68\x65\x72\x20\x4f\x70\x65\x6e\
-\x56\x50\x4e\x20\x50\x72\x6f\x63\x65\x73\x73\x20\x68\x61\x73\x20\
-\x62\x65\x65\x6e\x20\x64\x65\x74\x65\x63\x74\x65\x64\x2e\x20\x50\
-\x6c\x65\x61\x73\x65\x20\x63\x6c\x6f\x73\x65\x20\x69\x74\x20\x62\
-\x65\x66\x6f\x72\x65\x20\x73\x74\x61\x72\x74\x69\x6e\x67\x20\x6c\
-\x65\x61\x70\x2d\x63\x6c\x69\x65\x6e\x74\x07\x00\x00\x00\x09\x45\
-\x49\x50\x45\x72\x72\x6f\x72\x73\x01\x03\x00\x00\x00\xb0\x00\x4e\
-\x00\x6f\x00\x20\x00\x73\x00\x65\x00\x20\x00\x68\x00\x61\x00\x20\
-\x00\x70\x00\x6f\x00\x64\x00\x69\x00\x64\x00\x6f\x00\x20\x00\x65\
-\x00\x6e\x00\x63\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x61\x00\x72\
-\x00\x20\x00\x65\x00\x6c\x00\x20\x00\x63\x00\x6f\x00\x6d\x00\x61\
-\x00\x6e\x00\x64\x00\x6f\x00\x20\x00\x6f\x00\x70\x00\x65\x00\x6e\
-\x00\x76\x00\x70\x00\x6e\x00\x2e\x00\x20\x00\x3c\x00\x62\x00\x72\
-\x00\x2f\x00\x3e\x00\x20\x00\x28\x00\x70\x00\x6f\x00\x64\x00\x72\
-\x00\x69\x00\x61\x00\x20\x00\x73\x00\x65\x00\x72\x00\x20\x00\x75\
-\x00\x6e\x00\x20\x00\x70\x00\x72\x00\x6f\x00\x62\x00\x6c\x00\x65\
-\x00\x6d\x00\x61\x00\x20\x00\x64\x00\x65\x00\x20\x00\x70\x00\x65\
-\x00\x72\x00\x6d\x00\x69\x00\x73\x00\x6f\x00\x73\x00\x29\x08\x00\
-\x00\x00\x00\x06\x00\x00\x00\x48\x4e\x6f\x20\x73\x75\x69\x74\x61\
-\x62\x6c\x65\x20\x6f\x70\x65\x6e\x76\x70\x6e\x20\x63\x6f\x6d\x6d\
-\x61\x6e\x64\x20\x66\x6f\x75\x6e\x64\x2e\x20\x3c\x62\x72\x2f\x3e\
-\x28\x4d\x69\x67\x68\x74\x20\x62\x65\x20\x61\x20\x70\x65\x72\x6d\
-\x69\x73\x73\x69\x6f\x6e\x73\x20\x70\x72\x6f\x62\x6c\x65\x6d\x29\
-\x07\x00\x00\x00\x09\x45\x49\x50\x45\x72\x72\x6f\x72\x73\x01\x03\
-\x00\x00\x00\x6a\x00\x45\x00\x6c\x00\x20\x00\x63\x00\x65\x00\x72\
-\x00\x74\x00\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\x64\x00\x6f\
-\x00\x20\x00\x64\x00\x65\x00\x6c\x00\x20\x00\x73\x00\x65\x00\x72\
-\x00\x76\x00\x69\x00\x64\x00\x6f\x00\x72\x00\x20\x00\x6e\x00\x6f\
-\x00\x20\x00\x73\x00\x65\x00\x20\x00\x68\x00\x61\x00\x20\x00\x70\
-\x00\x6f\x00\x64\x00\x69\x00\x64\x00\x6f\x00\x20\x00\x76\x00\x65\
-\x00\x72\x00\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\x72\x08\x00\
-\x00\x00\x00\x06\x00\x00\x00\x28\x53\x65\x72\x76\x65\x72\x20\x63\
-\x65\x72\x74\x69\x66\x69\x63\x61\x74\x65\x20\x63\x6f\x75\x6c\x64\
-\x20\x6e\x6f\x74\x20\x62\x65\x20\x76\x65\x72\x69\x66\x69\x65\x64\
-\x07\x00\x00\x00\x09\x45\x49\x50\x45\x72\x72\x6f\x72\x73\x01\x03\
-\x00\x00\x00\x52\x00\x45\x00\x6c\x00\x20\x00\x73\x00\x65\x00\x72\
-\x00\x76\x00\x69\x00\x64\x00\x6f\x00\x72\x00\x20\x00\x6e\x00\x6f\
-\x00\x20\x00\x70\x00\x65\x00\x72\x00\x6d\x00\x69\x00\x74\x00\x65\
-\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x65\x00\x78\x00\x69\x00\x6f\
-\x00\x6e\x00\x65\x00\x73\x00\x20\x00\x73\x00\x65\x00\x67\x00\x75\
-\x00\x72\x00\x61\x00\x73\x08\x00\x00\x00\x00\x06\x00\x00\x00\x28\
-\x53\x65\x72\x76\x65\x72\x20\x64\x6f\x65\x73\x20\x6e\x6f\x74\x20\
-\x61\x6c\x6c\x6f\x77\x20\x73\x65\x63\x75\x72\x65\x20\x63\x6f\x6e\
-\x6e\x65\x63\x74\x69\x6f\x6e\x73\x07\x00\x00\x00\x09\x45\x49\x50\
-\x45\x72\x72\x6f\x72\x73\x01\x03\x00\x00\x00\xf8\x00\x4e\x00\x6f\
-\x00\x20\x00\x68\x00\x65\x00\x6d\x00\x6f\x00\x73\x00\x20\x00\x65\
-\x00\x6e\x00\x63\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x61\x00\x64\
-\x00\x6f\x00\x20\x00\x3c\x00\x62\x00\x3e\x00\x70\x00\x6b\x00\x65\
-\x00\x78\x00\x65\x00\x63\x00\x3c\x00\x2f\x00\x62\x00\x3e\x00\x20\
-\x00\x65\x00\x6e\x00\x20\x00\x65\x00\x6c\x00\x20\x00\x73\x00\x69\
-\x00\x73\x00\x74\x00\x65\x00\x6d\x00\x61\x00\x2e\x00\x20\x00\x3c\
-\x00\x62\x00\x72\x00\x2f\x00\x3e\x00\x20\x00\x51\x00\x75\x00\x69\
-\x00\x65\x00\x72\x00\x65\x00\x73\x00\x20\x00\x70\x00\x72\x00\x6f\
-\x00\x62\x00\x61\x00\x72\x00\x20\x00\x20\x00\x61\x00\x20\x00\x69\
-\x00\x6e\x00\x74\x00\x65\x00\x6e\x00\x74\x00\x61\x00\x72\x00\x20\
-\x00\x65\x00\x73\x00\x74\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x63\
-\x00\x65\x00\x72\x00\x20\x00\x73\x00\x65\x00\x74\x00\x75\x00\x69\
-\x00\x64\x00\x3f\x00\x20\x00\x28\x00\x4e\x00\x6f\x00\x20\x00\x68\
-\x00\x61\x00\x63\x00\x65\x00\x20\x00\x6e\x00\x61\x00\x64\x00\x61\
-\x00\x20\x00\x74\x00\x6f\x00\x64\x00\x61\x00\x76\x00\x69\x00\x61\
-\x00\x29\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x7b\x57\x65\
-\x20\x63\x6f\x75\x6c\x64\x20\x6e\x6f\x74\x20\x66\x69\x6e\x64\x20\
-\x3c\x62\x3e\x70\x6b\x65\x78\x65\x63\x3c\x2f\x62\x3e\x20\x69\x6e\
-\x20\x79\x6f\x75\x72\x20\x73\x79\x73\x74\x65\x6d\x2e\x3c\x62\x72\
-\x2f\x3e\x20\x44\x6f\x20\x79\x6f\x75\x20\x77\x61\x6e\x74\x20\x74\
-\x6f\x20\x74\x72\x79\x20\x3c\x62\x3e\x73\x65\x74\x75\x69\x64\x20\
-\x77\x6f\x72\x6b\x61\x72\x6f\x75\x6e\x64\x3c\x2f\x62\x3e\x3f\x20\
-\x28\x3c\x69\x3e\x44\x4f\x45\x53\x20\x4e\x4f\x54\x48\x49\x4e\x47\
-\x20\x59\x45\x54\x3c\x2f\x69\x3e\x29\x07\x00\x00\x00\x09\x45\x49\
-\x50\x45\x72\x72\x6f\x72\x73\x01\x03\x00\x00\x01\x5e\x00\x4e\x00\
-\x6f\x00\x20\x00\x68\x00\x65\x00\x6d\x00\x6f\x00\x73\x00\x20\x00\
-\x70\x00\x6f\x00\x64\x00\x69\x00\x64\x00\x6f\x00\x20\x00\x65\x00\
-\x6e\x00\x63\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x61\x00\x72\x00\
-\x20\x00\x6e\x00\x69\x00\x6e\x00\x67\x00\x75\x00\x6e\x00\x20\x00\
-\x61\x00\x67\x00\x65\x00\x6e\x00\x74\x00\x65\x00\x20\x00\x64\x00\
-\x65\x00\x20\x00\x61\x00\x75\x00\x74\x00\x65\x00\x6e\x00\x74\x00\
-\x69\x00\x63\x00\x61\x00\x63\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\
-\x65\x00\x6e\x00\x20\x00\x65\x00\x6c\x00\x20\x00\x73\x00\x69\x00\
-\x73\x00\x74\x00\x65\x00\x6d\x00\x61\x00\x2e\x00\x3c\x00\x62\x00\
-\x72\x00\x2f\x00\x3e\x00\x41\x00\x73\x00\x65\x00\x67\x00\x75\x00\
-\x72\x00\x61\x00\x74\x00\x65\x00\x20\x00\x64\x00\x65\x00\x20\x00\
-\x71\x00\x75\x00\x65\x00\x20\x00\x3c\x00\x62\x00\x3e\x00\x70\x00\
-\x6f\x00\x6c\x00\x6b\x00\x69\x00\x74\x00\x2d\x00\x67\x00\x6e\x00\
-\x6f\x00\x6d\x00\x65\x00\x2d\x00\x61\x00\x75\x00\x74\x00\x68\x00\
-\x65\x00\x6e\x00\x74\x00\x69\x00\x63\x00\x61\x00\x74\x00\x69\x00\
-\x6f\x00\x6e\x00\x2d\x00\x61\x00\x67\x00\x65\x00\x6e\x00\x74\x00\
-\x2d\x00\x31\x00\x3c\x00\x2f\x00\x62\x00\x3e\x00\x20\x00\x65\x00\
-\x73\x00\x74\x00\x61\x00\x20\x00\x65\x00\x6a\x00\x65\x00\x63\x00\
-\x75\x00\x74\x00\x61\x00\x6e\x00\x64\x00\x6f\x00\x73\x00\x65\x00\
-\x20\x00\x65\x00\x20\x00\x69\x00\x6e\x00\x74\x00\x65\x00\x6e\x00\
-\x74\x00\x61\x00\x6c\x00\x6f\x00\x20\x00\x64\x00\x65\x00\x20\x00\
-\x6e\x00\x75\x00\x65\x00\x76\x00\x6f\x00\x2e\x08\x00\x00\x00\x00\
-\x06\x00\x00\x00\x93\x57\x65\x20\x63\x6f\x75\x6c\x64\x20\x6e\x6f\
-\x74\x20\x66\x69\x6e\x64\x20\x61\x6e\x79\x20\x61\x75\x74\x68\x65\
-\x6e\x74\x69\x63\x61\x74\x69\x6f\x6e\x20\x61\x67\x65\x6e\x74\x20\
-\x69\x6e\x20\x79\x6f\x75\x72\x20\x73\x79\x73\x74\x65\x6d\x2e\x3c\
-\x62\x72\x2f\x3e\x4d\x61\x6b\x65\x20\x73\x75\x72\x65\x20\x79\x6f\
-\x75\x20\x68\x61\x76\x65\x20\x3c\x62\x3e\x70\x6f\x6c\x6b\x69\x74\
-\x2d\x67\x6e\x6f\x6d\x65\x2d\x61\x75\x74\x68\x65\x6e\x74\x69\x63\
-\x61\x74\x69\x6f\x6e\x2d\x61\x67\x65\x6e\x74\x2d\x31\x3c\x2f\x62\
-\x3e\x20\x72\x75\x6e\x6e\x69\x6e\x67\x20\x61\x6e\x64\x20\x74\x72\
-\x79\x20\x61\x67\x61\x69\x6e\x2e\x07\x00\x00\x00\x09\x45\x49\x50\
-\x45\x72\x72\x6f\x72\x73\x01\x03\x00\x00\x00\x86\x00\x4e\x00\x6f\
-\x00\x20\x00\x73\x00\x65\x00\x20\x00\x68\x00\x61\x00\x6e\x00\x20\
-\x00\x70\x00\x6f\x00\x64\x00\x69\x00\x64\x00\x6f\x00\x20\x00\x65\
-\x00\x6e\x00\x63\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x61\x00\x72\
-\x00\x20\x00\x6c\x00\x6f\x00\x73\x00\x20\x00\x63\x00\x65\x00\x72\
-\x00\x74\x00\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\x64\x00\x6f\
-\x00\x73\x00\x20\x00\x65\x00\x69\x00\x70\x00\x20\x00\x65\x00\x6e\
-\x00\x20\x00\x65\x00\x6c\x00\x20\x00\x70\x00\x61\x00\x74\x00\x68\
-\x00\x20\x00\x65\x00\x73\x00\x70\x00\x65\x00\x72\x00\x61\x00\x64\
-\x00\x6f\x08\x00\x00\x00\x00\x06\x00\x00\x00\x35\x57\x65\x20\x63\
-\x6f\x75\x6c\x64\x20\x6e\x6f\x74\x20\x66\x69\x6e\x64\x20\x79\x6f\
-\x75\x72\x20\x65\x69\x70\x20\x63\x65\x72\x74\x73\x20\x69\x6e\x20\
-\x74\x68\x65\x20\x65\x78\x70\x65\x63\x74\x65\x64\x20\x70\x61\x74\
-\x68\x07\x00\x00\x00\x09\x45\x49\x50\x45\x72\x72\x6f\x72\x73\x01\
-\x03\x00\x00\x00\x76\x00\x6f\x00\x63\x00\x75\x00\x72\x00\x72\x00\
-\x69\x00\x6f\x00\x20\x00\x75\x00\x6e\x00\x20\x00\x65\x00\x72\x00\
-\x72\x00\x6f\x00\x72\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x20\x00\
-\x6c\x00\x61\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x66\x00\x69\x00\
-\x67\x00\x75\x00\x72\x00\x61\x00\x63\x00\x69\x00\x6f\x00\x6e\x00\
-\x20\x00\x64\x00\x65\x00\x20\x00\x6c\x00\x6f\x00\x73\x00\x20\x00\
-\x73\x00\x65\x00\x72\x00\x76\x00\x69\x00\x63\x00\x69\x00\x6f\x00\
-\x73\x00\x20\x00\x4c\x00\x45\x00\x41\x00\x50\x08\x00\x00\x00\x00\
-\x06\x00\x00\x00\x36\x61\x6e\x20\x65\x72\x72\x6f\x72\x20\x6f\x63\
-\x63\x75\x72\x72\x65\x64\x20\x64\x75\x72\x69\x6e\x67\x20\x63\x6f\
-\x6e\x66\x69\x67\x75\x72\x61\x74\x69\x6f\x20\x6f\x66\x20\x6c\x65\
-\x61\x70\x20\x73\x65\x72\x76\x69\x63\x65\x73\x07\x00\x00\x00\x09\
-\x45\x49\x50\x45\x72\x72\x6f\x72\x73\x01\x03\x00\x00\x00\x60\x00\
-\x68\x00\x61\x00\x79\x00\x20\x00\x75\x00\x6e\x00\x20\x00\x70\x00\
-\x72\x00\x6f\x00\x62\x00\x6c\x00\x65\x00\x6d\x00\x61\x00\x20\x00\
-\x63\x00\x6f\x00\x6e\x00\x20\x00\x65\x00\x6c\x00\x20\x00\x63\x00\
-\x65\x00\x72\x00\x74\x00\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\
-\x64\x00\x6f\x00\x20\x00\x64\x00\x65\x00\x6c\x00\x20\x00\x70\x00\
-\x72\x00\x6f\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x08\
-\x00\x00\x00\x00\x06\x00\x00\x00\x2c\x74\x68\x65\x72\x65\x20\x69\
-\x73\x20\x61\x20\x70\x72\x6f\x62\x6c\x65\x6d\x20\x77\x69\x74\x68\
-\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x20\x63\x65\x72\x74\x69\x66\
-\x69\x63\x61\x74\x65\x07\x00\x00\x00\x09\x45\x49\x50\x45\x72\x72\
-\x6f\x72\x73\x01\x03\x00\x00\x00\x2a\x00\x44\x00\x6f\x00\x6d\x00\
-\x69\x00\x6e\x00\x69\x00\x6f\x00\x20\x00\x6e\x00\x6f\x00\x20\x00\
-\x65\x00\x6e\x00\x63\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x61\x00\
-\x64\x00\x6f\x08\x00\x00\x00\x00\x06\x00\x00\x00\x16\x44\x6f\x6d\
-\x61\x69\x6e\x20\x63\x61\x6e\x6e\x6f\x74\x20\x62\x65\x20\x66\x6f\
-\x75\x6e\x64\x07\x00\x00\x00\x06\x45\x72\x72\x6f\x72\x73\x01\x03\
-\x00\x00\x00\x34\x00\x4e\x00\x6f\x00\x20\x00\x73\x00\x65\x00\x20\
-\x00\x65\x00\x6e\x00\x63\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x6f\
-\x00\x20\x00\x6c\x00\x61\x00\x20\x00\x69\x00\x6e\x00\x74\x00\x65\
-\x00\x72\x00\x66\x00\x61\x00\x7a\x08\x00\x00\x00\x00\x06\x00\x00\
-\x00\x13\x49\x6e\x74\x65\x72\x66\x61\x63\x65\x20\x6e\x6f\x74\x20\
-\x66\x6f\x75\x6e\x64\x07\x00\x00\x00\x06\x45\x72\x72\x6f\x72\x73\
-\x01\x03\x00\x00\x00\x4a\x00\x50\x00\x61\x00\x72\x00\x65\x00\x63\
-\x00\x65\x00\x20\x00\x71\x00\x75\x00\x65\x00\x20\x00\x6e\x00\x6f\
-\x00\x20\x00\x68\x00\x61\x00\x79\x00\x20\x00\x63\x00\x6f\x00\x6e\
-\x00\x65\x00\x78\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\x61\x00\x20\
-\x00\x69\x00\x6e\x00\x74\x00\x65\x00\x72\x00\x6e\x00\x65\x00\x74\
-\x08\x00\x00\x00\x00\x06\x00\x00\x00\x2e\x49\x74\x20\x6c\x6f\x6f\
-\x6b\x73\x20\x6c\x69\x6b\x65\x20\x74\x68\x65\x72\x65\x20\x69\x73\
-\x20\x6e\x6f\x20\x69\x6e\x74\x65\x72\x6e\x65\x74\x20\x63\x6f\x6e\
-\x6e\x65\x63\x74\x69\x6f\x6e\x2e\x07\x00\x00\x00\x06\x45\x72\x72\
-\x6f\x72\x73\x01\x03\x00\x00\x00\x78\x00\x4f\x00\x68\x00\x20\x00\
-\x6f\x00\x68\x00\x2e\x00\x2e\x00\x2e\x00\x20\x00\x70\x00\x61\x00\
-\x72\x00\x65\x00\x63\x00\x65\x00\x20\x00\x71\x00\x75\x00\x65\x00\
-\x20\x00\x68\x00\x61\x00\x79\x00\x20\x00\x70\x00\x72\x00\x6f\x00\
-\x62\x00\x6c\x00\x65\x00\x6d\x00\x61\x00\x73\x00\x20\x00\x63\x00\
-\x6f\x00\x6e\x00\x20\x00\x6c\x00\x61\x00\x20\x00\x63\x00\x6f\x00\
-\x6e\x00\x65\x00\x78\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\x61\x00\
-\x20\x00\x69\x00\x6e\x00\x74\x00\x65\x00\x72\x00\x6e\x00\x65\x00\
-\x74\x08\x00\x00\x00\x00\x06\x00\x00\x00\x3b\x4c\x6f\x6f\x6b\x73\
-\x20\x6c\x69\x6b\x65\x20\x74\x68\x65\x72\x65\x20\x61\x72\x65\x20\
-\x70\x72\x6f\x62\x6c\x65\x6d\x73\x20\x77\x69\x74\x68\x20\x79\x6f\
-\x75\x72\x20\x69\x6e\x74\x65\x72\x6e\x65\x74\x20\x63\x6f\x6e\x6e\
-\x65\x63\x74\x69\x6f\x6e\x07\x00\x00\x00\x06\x45\x72\x72\x6f\x72\
-\x73\x01\x03\x00\x00\x00\x50\x00\x50\x00\x61\x00\x72\x00\x65\x00\
-\x63\x00\x65\x00\x20\x00\x71\x00\x75\x00\x65\x00\x20\x00\x6e\x00\
-\x6f\x00\x20\x00\x65\x00\x73\x00\x74\x00\x61\x00\x73\x00\x20\x00\
-\x63\x00\x6f\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x61\x00\x64\x00\
-\x6f\x00\x20\x00\x61\x00\x20\x00\x69\x00\x6e\x00\x74\x00\x65\x00\
-\x72\x00\x6e\x00\x65\x00\x74\x08\x00\x00\x00\x00\x06\x00\x00\x00\
-\x39\x4c\x6f\x6f\x6b\x73\x20\x6c\x69\x6b\x65\x20\x79\x6f\x75\x72\
-\x20\x63\x6f\x6d\x70\x75\x74\x65\x72\x20\x69\x73\x20\x6e\x6f\x74\
-\x20\x63\x6f\x6e\x6e\x65\x63\x74\x65\x64\x20\x74\x6f\x20\x74\x68\
-\x65\x20\x69\x6e\x74\x65\x72\x6e\x65\x74\x07\x00\x00\x00\x06\x45\
-\x72\x72\x6f\x72\x73\x01\x03\x00\x00\x00\x3a\x00\x4c\x00\x61\x00\
-\x20\x00\x43\x00\x6f\x00\x6e\x00\x65\x00\x78\x00\x69\x00\x6f\x00\
-\x6e\x00\x20\x00\x43\x00\x69\x00\x66\x00\x72\x00\x61\x00\x64\x00\
-\x61\x00\x20\x00\x73\x00\x65\x00\x20\x00\x70\x00\x65\x00\x72\x00\
-\x64\x00\x69\x00\x6f\x08\x00\x00\x00\x00\x06\x00\x00\x00\x22\x54\
-\x68\x65\x20\x45\x6e\x63\x72\x79\x70\x74\x65\x64\x20\x43\x6f\x6e\
-\x6e\x65\x63\x74\x69\x6f\x6e\x20\x77\x61\x73\x20\x6c\x6f\x73\x74\
-\x2e\x07\x00\x00\x00\x06\x45\x72\x72\x6f\x72\x73\x01\x03\x00\x00\
-\x00\x44\x00\x41\x00\x73\x00\x69\x00\x73\x00\x74\x00\x65\x00\x6e\
-\x00\x74\x00\x65\x00\x20\x00\x70\x00\x61\x00\x72\x00\x61\x00\x20\
-\x00\x6c\x00\x61\x00\x20\x00\x70\x00\x72\x00\x69\x00\x6d\x00\x65\
-\x00\x72\x00\x61\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x65\x00\x78\
-\x00\x69\x00\x6f\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x10\
-\x46\x69\x72\x73\x74\x20\x72\x75\x6e\x20\x77\x69\x7a\x61\x72\x64\
-\x07\x00\x00\x00\x09\x49\x6e\x74\x72\x6f\x50\x61\x67\x65\x01\x03\
-\x00\x00\x00\x3c\x00\x4c\x00\x6f\x00\x67\x00\x75\x00\x65\x00\x61\
-\x00\x72\x00\x6d\x00\x65\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x20\
-\x00\x6d\x00\x69\x00\x73\x00\x20\x00\x63\x00\x72\x00\x65\x00\x64\
-\x00\x65\x00\x6e\x00\x63\x00\x69\x00\x61\x00\x6c\x00\x65\x00\x73\
-\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1a\x4c\x6f\x67\x20\x49\x6e\
-\x20\x77\x69\x74\x68\x20\x6d\x79\x20\x63\x72\x65\x64\x65\x6e\x74\
-\x69\x61\x6c\x73\x07\x00\x00\x00\x09\x49\x6e\x74\x72\x6f\x50\x61\
-\x67\x65\x01\x03\x00\x00\x02\x76\x00\x56\x00\x61\x00\x6d\x00\x6f\
-\x00\x73\x00\x20\x00\x61\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x66\
-\x00\x69\x00\x67\x00\x75\x00\x72\x00\x61\x00\x72\x00\x20\x00\x61\
-\x00\x6c\x00\x67\x00\x75\x00\x6e\x00\x61\x00\x73\x00\x20\x00\x63\
-\x00\x6f\x00\x73\x00\x61\x00\x73\x00\x20\x00\x61\x00\x6e\x00\x74\
-\x00\x65\x00\x73\x00\x20\x00\x64\x00\x65\x00\x20\x00\x71\x00\x75\
-\x00\x65\x00\x20\x00\x74\x00\x65\x00\x20\x00\x70\x00\x75\x00\x65\
-\x00\x64\x00\x61\x00\x73\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x65\
-\x00\x63\x00\x74\x00\x61\x00\x72\x00\x20\x00\x70\x00\x6f\x00\x72\
-\x00\x20\x00\x70\x00\x72\x00\x69\x00\x6d\x00\x65\x00\x72\x00\x61\
-\x00\x20\x00\x76\x00\x65\x00\x7a\x00\x2e\x00\x3c\x00\x62\x00\x72\
-\x00\x3e\x00\x3c\x00\x62\x00\x72\x00\x3e\x00\x53\x00\x69\x00\x20\
-\x00\x6e\x00\x65\x00\x63\x00\x65\x00\x73\x00\x69\x00\x74\x00\x61\
-\x00\x73\x00\x20\x00\x6d\x00\x6f\x00\x64\x00\x69\x00\x66\x00\x69\
-\x00\x63\x00\x61\x00\x72\x00\x20\x00\x65\x00\x73\x00\x74\x00\x61\
-\x00\x73\x00\x20\x00\x6f\x00\x70\x00\x63\x00\x69\x00\x6f\x00\x6e\
-\x00\x65\x00\x73\x00\x20\x00\x64\x00\x65\x00\x20\x00\x6e\x00\x75\
-\x00\x65\x00\x76\x00\x6f\x00\x2c\x00\x20\x00\x70\x00\x75\x00\x65\
-\x00\x64\x00\x65\x00\x73\x00\x20\x00\x65\x00\x6e\x00\x63\x00\x6f\
-\x00\x6e\x00\x74\x00\x72\x00\x61\x00\x72\x00\x20\x00\x65\x00\x73\
-\x00\x74\x00\x65\x00\x20\x00\x61\x00\x73\x00\x69\x00\x73\x00\x74\
-\x00\x65\x00\x6e\x00\x74\x00\x65\x00\x20\x00\x65\x00\x6e\x00\x20\
-\x00\x65\x00\x6c\x00\x20\x00\x6d\x00\x65\x00\x6e\x00\x75\x00\x20\
-\x00\x64\x00\x65\x00\x20\x00\x27\x00\x3c\x00\x69\x00\x3e\x00\x4f\
-\x00\x70\x00\x63\x00\x69\x00\x6f\x00\x6e\x00\x65\x00\x73\x00\x3c\
-\x00\x2f\x00\x69\x00\x3e\x00\x27\x00\x20\x00\x65\x00\x6e\x00\x20\
-\x00\x6c\x00\x61\x00\x20\x00\x76\x00\x65\x00\x6e\x00\x74\x00\x61\
-\x00\x6e\x00\x61\x00\x20\x00\x70\x00\x72\x00\x69\x00\x6e\x00\x63\
-\x00\x69\x00\x70\x00\x61\x00\x6c\x00\x2e\x00\x3c\x00\x62\x00\x72\
-\x00\x3e\x00\x3c\x00\x62\x00\x72\x00\x3e\x00\x51\x00\x75\x00\x69\
-\x00\x65\x00\x72\x00\x65\x00\x73\x00\x20\x00\x3c\x00\x62\x00\x3e\
-\x00\x72\x00\x65\x00\x67\x00\x69\x00\x73\x00\x74\x00\x72\x00\x61\
-\x00\x72\x00\x3c\x00\x2f\x00\x62\x00\x3e\x00\x20\x00\x75\x00\x6e\
-\x00\x61\x00\x20\x00\x6e\x00\x75\x00\x65\x00\x76\x00\x61\x00\x20\
-\x00\x63\x00\x75\x00\x65\x00\x6e\x00\x74\x00\x61\x00\x2c\x00\x20\
-\x00\x6f\x00\x20\x00\x3c\x00\x62\x00\x3e\x00\x6c\x00\x6f\x00\x67\
-\x00\x75\x00\x65\x00\x61\x00\x72\x00\x74\x00\x65\x00\x3c\x00\x2f\
-\x00\x62\x00\x3e\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x20\x00\x74\
-\x00\x75\x00\x20\x00\x75\x00\x73\x00\x75\x00\x61\x00\x72\x00\x69\
-\x00\x6f\x00\x3f\x00\x3c\x00\x62\x00\x72\x00\x3e\x00\x20\x08\x00\
-\x00\x00\x00\x06\x00\x00\x01\x5d\x4e\x6f\x77\x20\x77\x65\x20\x77\
-\x69\x6c\x6c\x20\x67\x75\x69\x64\x65\x20\x79\x6f\x75\x20\x74\x68\
-\x72\x6f\x75\x67\x68\x20\x73\x6f\x6d\x65\x20\x63\x6f\x6e\x66\x69\
-\x67\x75\x72\x61\x74\x69\x6f\x6e\x20\x74\x68\x61\x74\x20\x69\x73\
-\x20\x6e\x65\x65\x64\x65\x64\x20\x62\x65\x66\x6f\x72\x65\x20\x79\
-\x6f\x75\x20\x63\x61\x6e\x20\x63\x6f\x6e\x6e\x65\x63\x74\x20\x66\
-\x6f\x72\x20\x74\x68\x65\x20\x66\x69\x72\x73\x74\x20\x74\x69\x6d\
-\x65\x2e\x3c\x62\x72\x3e\x3c\x62\x72\x3e\x49\x66\x20\x79\x6f\x75\
-\x20\x65\x76\x65\x72\x20\x6e\x65\x65\x64\x20\x74\x6f\x20\x6d\x6f\
-\x64\x69\x66\x79\x20\x74\x68\x65\x73\x65\x20\x6f\x70\x74\x69\x6f\
-\x6e\x73\x20\x61\x67\x61\x69\x6e\x2c\x20\x79\x6f\x75\x20\x63\x61\
-\x6e\x20\x66\x69\x6e\x64\x20\x74\x68\x65\x20\x77\x69\x7a\x61\x72\
-\x64\x20\x69\x6e\x20\x74\x68\x65\x20\x27\x3c\x69\x3e\x53\x65\x74\
-\x74\x69\x6e\x67\x73\x3c\x2f\x69\x3e\x27\x20\x6d\x65\x6e\x75\x20\
-\x66\x72\x6f\x6d\x20\x74\x68\x65\x20\x6d\x61\x69\x6e\x20\x77\x69\
-\x6e\x64\x6f\x77\x2e\x3c\x62\x72\x3e\x3c\x62\x72\x3e\x44\x6f\x20\
-\x79\x6f\x75\x20\x77\x61\x6e\x74\x20\x74\x6f\x20\x3c\x62\x3e\x73\
-\x69\x67\x6e\x20\x75\x70\x3c\x2f\x62\x3e\x20\x66\x6f\x72\x20\x61\
-\x20\x6e\x65\x77\x20\x61\x63\x63\x6f\x75\x6e\x74\x2c\x20\x6f\x72\
-\x20\x3c\x62\x3e\x6c\x6f\x67\x20\x69\x6e\x3c\x2f\x62\x3e\x20\x77\
-\x69\x74\x68\x20\x61\x6e\x20\x61\x6c\x72\x65\x61\x64\x79\x20\x65\
-\x78\x69\x73\x74\x69\x6e\x67\x20\x75\x73\x65\x72\x6e\x61\x6d\x65\
-\x3f\x3c\x62\x72\x3e\x07\x00\x00\x00\x09\x49\x6e\x74\x72\x6f\x50\
-\x61\x67\x65\x01\x03\x00\x00\x00\x34\x00\x52\x00\x65\x00\x67\x00\
-\x69\x00\x73\x00\x74\x00\x72\x00\x61\x00\x72\x00\x20\x00\x75\x00\
-\x6e\x00\x61\x00\x20\x00\x6e\x00\x75\x00\x65\x00\x76\x00\x61\x00\
-\x20\x00\x63\x00\x75\x00\x65\x00\x6e\x00\x74\x00\x61\x08\x00\x00\
-\x00\x00\x06\x00\x00\x00\x19\x53\x69\x67\x6e\x20\x75\x70\x20\x66\
-\x6f\x72\x20\x61\x20\x6e\x65\x77\x20\x61\x63\x63\x6f\x75\x6e\x74\
-\x07\x00\x00\x00\x09\x49\x6e\x74\x72\x6f\x50\x61\x67\x65\x01\x03\
-\x00\x00\x00\x9e\x00\x48\x00\x61\x00\x7a\x00\x20\x00\x63\x00\x6c\
-\x00\x69\x00\x63\x00\x6b\x00\x20\x00\x65\x00\x6e\x00\x20\x00\x27\
-\x00\x3c\x00\x69\x00\x3e\x00\x25\x00\x73\x00\x3c\x00\x2f\x00\x69\
-\x00\x3e\x00\x27\x00\x20\x00\x70\x00\x61\x00\x72\x00\x61\x00\x20\
-\x00\x74\x00\x65\x00\x72\x00\x6d\x00\x69\x00\x6e\x00\x61\x00\x72\
-\x00\x20\x00\x65\x00\x6c\x00\x20\x00\x61\x00\x73\x00\x69\x00\x73\
-\x00\x74\x00\x65\x00\x6e\x00\x74\x00\x65\x00\x20\x00\x79\x00\x20\
-\x00\x67\x00\x75\x00\x61\x00\x72\x00\x64\x00\x61\x00\x72\x00\x20\
-\x00\x74\x00\x75\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x66\x00\x69\
-\x00\x67\x00\x75\x00\x72\x00\x61\x00\x63\x00\x69\x00\x6f\x00\x6e\
-\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x3b\x43\x6c\x69\x63\
-\x6b\x20\x27\x3c\x69\x3e\x25\x73\x3c\x2f\x69\x3e\x27\x20\x74\x6f\
-\x20\x65\x6e\x64\x20\x74\x68\x65\x20\x77\x69\x7a\x61\x72\x64\x20\
-\x61\x6e\x64\x20\x73\x61\x76\x65\x20\x79\x6f\x75\x72\x20\x73\x65\
-\x74\x74\x69\x6e\x67\x73\x2e\x07\x00\x00\x00\x08\x4c\x61\x73\x74\
-\x50\x61\x67\x65\x01\x03\x00\x00\x00\x72\x00\x43\x00\x6f\x00\x6e\
-\x00\x65\x00\x63\x00\x74\x00\x61\x00\x6e\x00\x64\x00\x6f\x00\x20\
-\x00\x61\x00\x6c\x00\x20\x00\x73\x00\x65\x00\x72\x00\x76\x00\x69\
-\x00\x63\x00\x69\x00\x6f\x00\x20\x00\x64\x00\x65\x00\x20\x00\x50\
-\x00\x72\x00\x6f\x00\x78\x00\x79\x00\x20\x00\x43\x00\x69\x00\x66\
-\x00\x72\x00\x61\x00\x64\x00\x6f\x00\x20\x00\x64\x00\x65\x00\x20\
-\x00\x49\x00\x6e\x00\x74\x00\x65\x00\x72\x00\x6e\x00\x65\x00\x74\
-\x00\x20\x00\x28\x00\x45\x00\x49\x00\x50\x00\x29\x08\x00\x00\x00\
-\x00\x06\x00\x00\x00\x31\x43\x6f\x6e\x6e\x65\x63\x74\x69\x6e\x67\
-\x20\x74\x6f\x20\x45\x6e\x63\x72\x79\x70\x74\x65\x64\x20\x49\x6e\
-\x74\x65\x72\x6e\x65\x74\x20\x50\x72\x6f\x78\x79\x20\x73\x65\x72\
-\x76\x69\x63\x65\x2e\x2e\x2e\x07\x00\x00\x00\x08\x4c\x61\x73\x74\
-\x50\x61\x67\x65\x01\x03\x00\x00\x00\x44\x00\x45\x00\x73\x00\x74\
-\x00\x61\x00\x73\x00\x20\x00\x75\x00\x73\x00\x61\x00\x6e\x00\x64\
-\x00\x6f\x00\x20\x00\x75\x00\x6e\x00\x61\x00\x20\x00\x63\x00\x6f\
-\x00\x6e\x00\x65\x00\x78\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\x63\
-\x00\x69\x00\x66\x00\x72\x00\x61\x00\x64\x00\x61\x00\x21\x08\x00\
-\x00\x00\x00\x06\x00\x00\x00\x2a\x59\x6f\x75\x20\x61\x72\x65\x20\
-\x6e\x6f\x77\x20\x75\x73\x69\x6e\x67\x20\x61\x6e\x20\x65\x6e\x63\
-\x72\x79\x70\x74\x65\x64\x20\x63\x6f\x6e\x6e\x65\x63\x74\x69\x6f\
-\x6e\x21\x07\x00\x00\x00\x08\x4c\x61\x73\x74\x50\x61\x67\x65\x01\
-\x03\x00\x00\x00\x18\x00\x26\x00\x43\x00\x6f\x00\x6e\x00\x74\x00\
-\x72\x00\x61\x00\x73\x00\x65\x00\x6e\x00\x61\x00\x3a\x08\x00\x00\
-\x00\x00\x06\x00\x00\x00\x0a\x26\x50\x61\x73\x73\x77\x6f\x72\x64\
-\x3a\x07\x00\x00\x00\x09\x4c\x6f\x67\x49\x6e\x50\x61\x67\x65\x01\
-\x03\x00\x00\x00\x34\x00\x45\x00\x72\x00\x72\x00\x6f\x00\x72\x00\
-\x20\x00\x64\x00\x65\x00\x20\x00\x61\x00\x75\x00\x74\x00\x65\x00\
-\x6e\x00\x74\x00\x69\x00\x63\x00\x61\x00\x63\x00\x69\x00\x6f\x00\
-\x6e\x00\x3a\x00\x20\x00\x25\x00\x73\x08\x00\x00\x00\x00\x06\x00\
-\x00\x00\x18\x41\x75\x74\x68\x65\x6e\x74\x69\x63\x61\x74\x69\x6f\
-\x6e\x20\x65\x72\x72\x6f\x72\x3a\x20\x25\x73\x07\x00\x00\x00\x09\
-\x4c\x6f\x67\x49\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x2e\x00\
-\x43\x00\x72\x00\x65\x00\x64\x00\x65\x00\x6e\x00\x63\x00\x69\x00\
-\x61\x00\x6c\x00\x65\x00\x73\x00\x20\x00\x76\x00\x61\x00\x6c\x00\
-\x69\x00\x64\x00\x61\x00\x64\x00\x61\x00\x73\x00\x2e\x08\x00\x00\
-\x00\x00\x06\x00\x00\x00\x16\x43\x72\x65\x64\x65\x6e\x74\x69\x61\
-\x6c\x73\x20\x76\x61\x6c\x69\x64\x61\x74\x65\x64\x2e\x07\x00\x00\
-\x00\x09\x4c\x6f\x67\x49\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\
-\x0c\x00\x4c\x00\x6f\x00\x67\x00\x20\x00\x69\x00\x6e\x08\x00\x00\
-\x00\x00\x06\x00\x00\x00\x06\x4c\x6f\x67\x20\x49\x6e\x07\x00\x00\
-\x00\x09\x4c\x6f\x67\x49\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\
-\x0c\x00\x4c\x00\x6f\x00\x67\x00\x20\x00\x69\x00\x6e\x08\x00\x00\
-\x00\x00\x06\x00\x00\x00\x06\x4c\x6f\x67\x20\x69\x6e\x07\x00\x00\
-\x00\x09\x4c\x6f\x67\x49\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\
-\x3c\x00\x4c\x00\x6f\x00\x67\x00\x75\x00\x65\x00\x61\x00\x72\x00\
-\x74\x00\x65\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x20\x00\x74\x00\
-\x75\x00\x73\x00\x20\x00\x63\x00\x72\x00\x65\x00\x64\x00\x65\x00\
-\x6e\x00\x63\x00\x69\x00\x61\x00\x6c\x00\x65\x00\x73\x08\x00\x00\
-\x00\x00\x06\x00\x00\x00\x1c\x4c\x6f\x67\x20\x69\x6e\x20\x77\x69\
-\x74\x68\x20\x79\x6f\x75\x72\x20\x63\x72\x65\x64\x65\x6e\x74\x69\
-\x61\x6c\x73\x07\x00\x00\x00\x09\x4c\x6f\x67\x49\x6e\x50\x61\x67\
-\x65\x01\x03\x00\x00\x00\x3a\x00\x52\x00\x65\x00\x73\x00\x6f\x00\
-\x6c\x00\x76\x00\x69\x00\x65\x00\x6e\x00\x64\x00\x6f\x00\x20\x00\
-\x6e\x00\x6f\x00\x6d\x00\x62\x00\x72\x00\x65\x00\x20\x00\x64\x00\
-\x65\x00\x20\x00\x64\x00\x6f\x00\x6d\x00\x69\x00\x6e\x00\x69\x00\
-\x6f\x08\x00\x00\x00\x00\x06\x00\x00\x00\x15\x52\x65\x73\x6f\x6c\
-\x76\x69\x6e\x67\x20\x64\x6f\x6d\x61\x69\x6e\x20\x6e\x61\x6d\x65\
-\x07\x00\x00\x00\x09\x4c\x6f\x67\x49\x6e\x50\x61\x67\x65\x01\x03\
-\x00\x00\x00\x10\x00\x26\x00\x4e\x00\x6f\x00\x6d\x00\x62\x00\x72\
-\x00\x65\x00\x3a\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0b\x55\x73\
-\x65\x72\x20\x26\x6e\x61\x6d\x65\x3a\x07\x00\x00\x00\x09\x4c\x6f\
-\x67\x49\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x5a\x00\x45\x00\
-\x6c\x00\x20\x00\x75\x00\x73\x00\x75\x00\x61\x00\x72\x00\x69\x00\
-\x6f\x00\x20\x00\x74\x00\x69\x00\x65\x00\x6e\x00\x65\x00\x20\x00\
-\x71\x00\x75\x00\x65\x00\x20\x00\x73\x00\x65\x00\x72\x00\x20\x00\
-\x75\x00\x73\x00\x75\x00\x61\x00\x72\x00\x69\x00\x6f\x00\x40\x00\
-\x74\x00\x75\x00\x2e\x00\x70\x00\x72\x00\x6f\x00\x76\x00\x65\x00\
-\x65\x00\x64\x00\x6f\x00\x72\x08\x00\x00\x00\x00\x06\x00\x00\x00\
-\x2f\x55\x73\x65\x72\x6e\x61\x6d\x65\x20\x6d\x75\x73\x74\x20\x62\
-\x65\x20\x69\x6e\x20\x74\x68\x65\x20\x75\x73\x65\x72\x6e\x61\x6d\
-\x65\x40\x70\x72\x6f\x76\x69\x64\x65\x72\x20\x66\x6f\x72\x6d\x2e\
-\x07\x00\x00\x00\x09\x4c\x6f\x67\x49\x6e\x50\x61\x67\x65\x01\x03\
-\x00\x00\x00\x12\x00\x26\x00\x43\x00\x6f\x00\x6e\x00\x65\x00\x63\
-\x00\x74\x00\x61\x00\x72\x08\x00\x00\x00\x00\x06\x00\x00\x00\x08\
-\x26\x43\x6f\x6e\x6e\x65\x63\x74\x07\x00\x00\x00\x0c\x4c\x6f\x67\
-\x50\x61\x6e\x65\x4d\x69\x78\x69\x6e\x01\x03\x00\x00\x00\x1a\x00\
-\x26\x00\x44\x00\x65\x00\x73\x00\x63\x00\x6f\x00\x6e\x00\x65\x00\
-\x63\x00\x74\x00\x61\x00\x64\x00\x6f\x08\x00\x00\x00\x00\x06\x00\
-\x00\x00\x0c\x44\x69\x73\x63\x6f\x6e\x6e\x65\x63\x74\x65\x64\x07\
-\x00\x00\x00\x0c\x4c\x6f\x67\x50\x61\x6e\x65\x4d\x69\x78\x69\x6e\
-\x01\x03\x00\x00\x00\x32\x00\x49\x00\x6e\x00\x66\x00\x6f\x00\x72\
-\x00\x6d\x00\x61\x00\x63\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\x64\
-\x00\x65\x00\x6c\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x76\x00\x65\
-\x00\x65\x00\x64\x00\x6f\x00\x72\x08\x00\x00\x00\x00\x06\x00\x00\
-\x00\x14\x50\x72\x6f\x76\x69\x64\x65\x72\x20\x49\x6e\x66\x6f\x72\
-\x6d\x61\x74\x69\x6f\x6e\x07\x00\x00\x00\x10\x50\x72\x6f\x76\x69\
-\x64\x65\x72\x49\x6e\x66\x6f\x50\x61\x67\x65\x01\x03\x00\x00\x00\
-\x4c\x00\x53\x00\x65\x00\x72\x00\x76\x00\x69\x00\x63\x00\x69\x00\
-\x6f\x00\x73\x00\x20\x00\x6f\x00\x66\x00\x72\x00\x65\x00\x63\x00\
-\x69\x00\x64\x00\x6f\x00\x73\x00\x20\x00\x70\x00\x6f\x00\x72\x00\
-\x20\x00\x65\x00\x73\x00\x74\x00\x65\x00\x20\x00\x70\x00\x72\x00\
-\x6f\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x08\x00\x00\
-\x00\x00\x06\x00\x00\x00\x21\x53\x65\x72\x76\x69\x63\x65\x73\x20\
-\x6f\x66\x66\x65\x72\x65\x64\x20\x62\x79\x20\x74\x68\x69\x73\x20\
-\x70\x72\x6f\x76\x69\x64\x65\x72\x07\x00\x00\x00\x10\x50\x72\x6f\
-\x76\x69\x64\x65\x72\x49\x6e\x66\x6f\x50\x61\x67\x65\x01\x03\x00\
-\x00\x00\x28\x00\x70\x00\x6f\x00\x6c\x00\x69\x00\x74\x00\x69\x00\
-\x63\x00\x61\x00\x20\x00\x64\x00\x65\x00\x20\x00\x72\x00\x65\x00\
-\x67\x00\x69\x00\x73\x00\x74\x00\x72\x00\x6f\x08\x00\x00\x00\x00\
-\x06\x00\x00\x00\x11\x65\x6e\x72\x6f\x6c\x6c\x6d\x65\x6e\x74\x20\
-\x70\x6f\x6c\x69\x63\x79\x07\x00\x00\x00\x10\x50\x72\x6f\x76\x69\
-\x64\x65\x72\x49\x6e\x66\x6f\x50\x61\x67\x65\x01\x03\x00\x00\x00\
-\x46\x00\x43\x00\x6f\x00\x6d\x00\x70\x00\x72\x00\x6f\x00\x62\x00\
-\x61\x00\x6e\x00\x64\x00\x6f\x00\x20\x00\x65\x00\x6c\x00\x20\x00\
-\x66\x00\x69\x00\x6e\x00\x67\x00\x65\x00\x72\x00\x70\x00\x72\x00\
-\x69\x00\x6e\x00\x74\x00\x20\x00\x64\x00\x65\x00\x20\x00\x6c\x00\
-\x61\x00\x20\x00\x43\x00\x41\x08\x00\x00\x00\x00\x06\x00\x00\x00\
-\x17\x43\x68\x65\x63\x6b\x69\x6e\x67\x20\x43\x41\x20\x66\x69\x6e\
-\x67\x65\x72\x70\x72\x69\x6e\x74\x07\x00\x00\x00\x1b\x50\x72\x6f\
-\x76\x69\x64\x65\x72\x53\x65\x74\x75\x70\x56\x61\x6c\x69\x64\x61\
-\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x3e\x00\x4f\
-\x00\x62\x00\x74\x00\x65\x00\x6e\x00\x69\x00\x65\x00\x6e\x00\x64\
-\x00\x6f\x00\x20\x00\x63\x00\x65\x00\x72\x00\x74\x00\x69\x00\x66\
-\x00\x69\x00\x63\x00\x61\x00\x64\x00\x6f\x00\x20\x00\x64\x00\x65\
-\x00\x20\x00\x6c\x00\x61\x00\x20\x00\x43\x00\x41\x08\x00\x00\x00\
-\x00\x06\x00\x00\x00\x17\x46\x65\x74\x63\x68\x69\x6e\x67\x20\x43\
-\x41\x20\x63\x65\x72\x74\x69\x66\x69\x63\x61\x74\x65\x07\x00\x00\
-\x00\x1b\x50\x72\x6f\x76\x69\x64\x65\x72\x53\x65\x74\x75\x70\x56\
-\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\
-\x00\x00\x4e\x00\x4f\x00\x62\x00\x74\x00\x65\x00\x6e\x00\x69\x00\
-\x65\x00\x6e\x00\x64\x00\x6f\x00\x20\x00\x6f\x00\x70\x00\x63\x00\
-\x69\x00\x6f\x00\x6e\x00\x65\x00\x73\x00\x20\x00\x70\x00\x61\x00\
-\x72\x00\x61\x00\x20\x00\x65\x00\x73\x00\x74\x00\x65\x00\x20\x00\
-\x70\x00\x72\x00\x6f\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\
-\x72\x08\x00\x00\x00\x00\x06\x00\x00\x00\x31\x47\x61\x74\x68\x65\
-\x72\x69\x6e\x67\x20\x63\x6f\x6e\x66\x69\x67\x75\x72\x61\x74\x69\
-\x6f\x6e\x20\x6f\x70\x74\x69\x6f\x6e\x73\x20\x66\x6f\x72\x20\x74\
-\x68\x69\x73\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x07\x00\x00\x00\
-\x1b\x50\x72\x6f\x76\x69\x64\x65\x72\x53\x65\x74\x75\x70\x56\x61\
-\x6c\x69\x64\x61\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\
-\x00\x36\x00\x43\x00\x6f\x00\x6e\x00\x66\x00\x69\x00\x67\x00\x75\
-\x00\x72\x00\x61\x00\x63\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\x64\
-\x00\x65\x00\x6c\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x76\x00\x65\
-\x00\x65\x00\x64\x00\x6f\x00\x72\x08\x00\x00\x00\x00\x06\x00\x00\
-\x00\x0e\x50\x72\x6f\x76\x69\x64\x65\x72\x20\x73\x65\x74\x75\x70\
-\x07\x00\x00\x00\x1b\x50\x72\x6f\x76\x69\x64\x65\x72\x53\x65\x74\
-\x75\x70\x56\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\x50\x61\x67\x65\
-\x01\x03\x00\x00\x00\x3e\x00\x56\x00\x61\x00\x6c\x00\x69\x00\x64\
-\x00\x61\x00\x6e\x00\x64\x00\x6f\x00\x20\x00\x63\x00\x65\x00\x72\
-\x00\x74\x00\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\x64\x00\x6f\
-\x00\x20\x00\x64\x00\x65\x00\x20\x00\x6c\x00\x61\x00\x20\x00\x61\
-\x00\x70\x00\x69\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1a\x56\x61\
-\x6c\x69\x64\x61\x74\x69\x6e\x67\x20\x61\x70\x69\x20\x63\x65\x72\
-\x74\x69\x66\x69\x63\x61\x74\x65\x07\x00\x00\x00\x1b\x50\x72\x6f\
-\x76\x69\x64\x65\x72\x53\x65\x74\x75\x70\x56\x61\x6c\x69\x64\x61\
-\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x18\x00\x26\
-\x00\x43\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x61\x00\x73\x00\x65\
-\x00\x6e\x00\x61\x00\x3a\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0a\
-\x26\x50\x61\x73\x73\x77\x6f\x72\x64\x3a\x07\x00\x00\x00\x10\x52\
-\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\
-\x03\x00\x00\x00\x3c\x00\x26\x00\x52\x00\x65\x00\x63\x00\x6f\x00\
-\x72\x00\x64\x00\x61\x00\x72\x00\x20\x00\x75\x00\x73\x00\x75\x00\
-\x61\x00\x72\x00\x69\x00\x6f\x00\x20\x00\x79\x00\x20\x00\x63\x00\
-\x6f\x00\x6e\x00\x74\x00\x72\x00\x61\x00\x73\x00\x65\x00\x6e\x00\
-\x61\x08\x00\x00\x00\x00\x06\x00\x00\x00\x20\x26\x52\x65\x6d\x65\
-\x6d\x62\x65\x72\x20\x75\x73\x65\x72\x6e\x61\x6d\x65\x20\x61\x6e\
-\x64\x20\x70\x61\x73\x73\x77\x6f\x72\x64\x2e\x07\x00\x00\x00\x10\
-\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\
-\x01\x03\x00\x00\x00\x46\x00\x4e\x00\x6f\x00\x20\x00\x73\x00\x65\
-\x00\x20\x00\x70\x00\x75\x00\x64\x00\x6f\x00\x20\x00\x72\x00\x65\
-\x00\x67\x00\x69\x00\x73\x00\x74\x00\x72\x00\x61\x00\x72\x00\x20\
-\x00\x28\x00\x62\x00\x61\x00\x64\x00\x20\x00\x72\x00\x65\x00\x73\
-\x00\x70\x00\x6f\x00\x6e\x00\x73\x00\x65\x00\x29\x08\x00\x00\x00\
-\x00\x06\x00\x00\x00\x21\x43\x6f\x75\x6c\x64\x20\x6e\x6f\x74\x20\
-\x72\x65\x67\x69\x73\x74\x65\x72\x20\x28\x62\x61\x64\x20\x72\x65\
-\x73\x70\x6f\x6e\x73\x65\x29\x07\x00\x00\x00\x10\x52\x65\x67\x69\
-\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\
-\x00\x54\x00\x45\x00\x72\x00\x72\x00\x6f\x00\x72\x00\x20\x00\x63\
-\x00\x6f\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x61\x00\x6e\x00\x64\
-\x00\x6f\x00\x73\x00\x65\x00\x20\x00\x61\x00\x6c\x00\x20\x00\x70\
-\x00\x72\x00\x6f\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\
-\x00\x20\x00\x28\x00\x63\x00\x6f\x00\x6e\x00\x6e\x00\x65\x00\x72\
-\x00\x72\x00\x29\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x27\
-\x45\x72\x72\x6f\x72\x20\x43\x6f\x6e\x6e\x65\x63\x74\x69\x6e\x67\
-\x20\x74\x6f\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x20\x28\x63\x6f\
-\x6e\x6e\x65\x72\x72\x29\x2e\x07\x00\x00\x00\x10\x52\x65\x67\x69\
-\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\
-\x00\x52\x00\x45\x00\x72\x00\x72\x00\x6f\x00\x72\x00\x20\x00\x63\
-\x00\x6f\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x61\x00\x6e\x00\x64\
-\x00\x6f\x00\x73\x00\x65\x00\x20\x00\x61\x00\x6c\x00\x20\x00\x70\
-\x00\x72\x00\x6f\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\
-\x00\x20\x00\x28\x00\x74\x00\x69\x00\x6d\x00\x65\x00\x6f\x00\x75\
-\x00\x74\x00\x29\x08\x00\x00\x00\x00\x06\x00\x00\x00\x26\x45\x72\
-\x72\x6f\x72\x20\x63\x6f\x6e\x6e\x65\x63\x74\x69\x6e\x67\x20\x74\
-\x6f\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x20\x28\x74\x69\x6d\x65\
-\x6f\x75\x74\x29\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\
-\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x3c\x00\
-\x45\x00\x72\x00\x72\x00\x6f\x00\x72\x00\x20\x00\x64\x00\x75\x00\
-\x72\x00\x61\x00\x6e\x00\x74\x00\x65\x00\x20\x00\x65\x00\x6c\x00\
-\x20\x00\x72\x00\x65\x00\x67\x00\x69\x00\x73\x00\x74\x00\x72\x00\
-\x6f\x00\x20\x00\x28\x00\x25\x00\x73\x00\x29\x08\x00\x00\x00\x00\
-\x06\x00\x00\x00\x1e\x45\x72\x72\x6f\x72\x20\x64\x75\x72\x69\x6e\
-\x67\x20\x72\x65\x67\x69\x73\x74\x72\x61\x74\x69\x6f\x6e\x20\x28\
-\x25\x73\x29\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\
-\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x2c\x00\x43\
-\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x61\x00\x73\x00\x65\x00\x6e\
-\x00\x61\x00\x20\x00\x28\x00\x64\x00\x65\x00\x20\x00\x6e\x00\x75\
-\x00\x65\x00\x76\x00\x6f\x00\x29\x00\x3a\x08\x00\x00\x00\x00\x06\
-\x00\x00\x00\x11\x50\x61\x73\x73\x77\x6f\x72\x64\x20\x28\x61\x67\
-\x61\x69\x6e\x29\x3a\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\
-\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x40\
-\x00\x4c\x00\x61\x00\x73\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x74\
-\x00\x72\x00\x61\x00\x73\x00\x65\x00\x6e\x00\x61\x00\x73\x00\x20\
-\x00\x6e\x00\x6f\x00\x20\x00\x73\x00\x6f\x00\x6e\x00\x20\x00\x69\
-\x00\x67\x00\x75\x00\x61\x00\x6c\x00\x65\x00\x73\x00\x2e\x00\x2e\
-\x08\x00\x00\x00\x00\x06\x00\x00\x00\x19\x50\x61\x73\x73\x77\x6f\
-\x72\x64\x20\x64\x6f\x65\x73\x20\x6e\x6f\x74\x20\x6d\x61\x74\x63\
-\x68\x2e\x2e\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\
-\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x36\x00\x43\
-\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x61\x00\x73\x00\x65\x00\x6e\
-\x00\x61\x00\x20\x00\x64\x00\x65\x00\x6d\x00\x61\x00\x73\x00\x69\
-\x00\x61\x00\x64\x00\x6f\x00\x20\x00\x6f\x00\x62\x00\x76\x00\x69\
-\x00\x61\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x15\x50\x61\
-\x73\x73\x77\x6f\x72\x64\x20\x74\x6f\x6f\x20\x6f\x62\x76\x69\x6f\
-\x75\x73\x2e\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\
-\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x36\x00\x43\
-\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x61\x00\x73\x00\x65\x00\x6e\
-\x00\x61\x00\x20\x00\x64\x00\x65\x00\x6d\x00\x61\x00\x73\x00\x69\
-\x00\x61\x00\x64\x00\x6f\x00\x20\x00\x63\x00\x6f\x00\x72\x00\x74\
-\x00\x61\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x13\x50\x61\
-\x73\x73\x77\x6f\x72\x64\x20\x74\x6f\x6f\x20\x73\x68\x6f\x72\x74\
-\x2e\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\
-\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x6e\x00\x52\x00\x65\
-\x00\x67\x00\x69\x00\x73\x00\x74\x00\x72\x00\x61\x00\x72\x00\x20\
-\x00\x75\x00\x6e\x00\x20\x00\x6e\x00\x75\x00\x65\x00\x76\x00\x6f\
-\x00\x20\x00\x75\x00\x73\x00\x75\x00\x61\x00\x72\x00\x69\x00\x6f\
-\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x20\x00\x65\x00\x6c\x00\x20\
-\x00\x70\x00\x72\x00\x6f\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\
-\x00\x72\x00\x20\x00\x3c\x00\x65\x00\x6d\x00\x3e\x00\x25\x00\x73\
-\x00\x3c\x00\x2f\x00\x65\x00\x6d\x00\x3e\x08\x00\x00\x00\x00\x06\
-\x00\x00\x00\x2d\x52\x65\x67\x69\x73\x74\x65\x72\x20\x61\x20\x6e\
-\x65\x77\x20\x75\x73\x65\x72\x20\x77\x69\x74\x68\x20\x70\x72\x6f\
-\x76\x69\x64\x65\x72\x20\x3c\x65\x6d\x3e\x25\x73\x3c\x2f\x65\x6d\
-\x3e\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\
-\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x30\x00\x43\x00\x75\
-\x00\x65\x00\x6e\x00\x74\x00\x61\x00\x20\x00\x63\x00\x72\x00\x65\
-\x00\x61\x00\x64\x00\x61\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x20\
-\x00\x65\x00\x78\x00\x69\x00\x74\x00\x6f\x00\x21\x08\x00\x00\x00\
-\x00\x06\x00\x00\x00\x17\x52\x65\x67\x69\x73\x74\x72\x61\x74\x69\
-\x6f\x6e\x20\x73\x75\x63\x63\x65\x65\x64\x65\x64\x21\x07\x00\x00\
-\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\
-\x67\x65\x01\x03\x00\x00\x00\x18\x00\x4e\x00\x75\x00\x65\x00\x76\
-\x00\x61\x00\x20\x00\x43\x00\x75\x00\x65\x00\x6e\x00\x74\x00\x61\
-\x08\x00\x00\x00\x00\x06\x00\x00\x00\x07\x53\x69\x67\x6e\x20\x55\
-\x70\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\
-\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x10\x00\x26\x00\x4e\
-\x00\x6f\x00\x6d\x00\x62\x00\x72\x00\x65\x00\x3a\x08\x00\x00\x00\
-\x00\x06\x00\x00\x00\x0b\x55\x73\x65\x72\x20\x26\x6e\x61\x6d\x65\
-\x3a\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\
-\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x2c\x00\x55\x00\x73\
-\x00\x75\x00\x61\x00\x72\x00\x69\x00\x6f\x00\x20\x00\x6e\x00\x6f\
-\x00\x20\x00\x64\x00\x69\x00\x73\x00\x70\x00\x6f\x00\x6e\x00\x69\
-\x00\x62\x00\x6c\x00\x65\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\
-\x00\x17\x55\x73\x65\x72\x6e\x61\x6d\x65\x20\x6e\x6f\x74\x20\x61\
-\x76\x61\x69\x6c\x61\x62\x6c\x65\x2e\x07\x00\x00\x00\x10\x52\x65\
-\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\
-\x00\x00\x00\x52\x00\x43\x00\x6f\x00\x6e\x00\x26\x00\x66\x00\x69\
-\x00\x6f\x00\x20\x00\x65\x00\x6e\x00\x20\x00\x65\x00\x73\x00\x74\
-\x00\x65\x00\x20\x00\x63\x00\x65\x00\x72\x00\x74\x00\x69\x00\x66\
-\x00\x69\x00\x63\x00\x61\x00\x64\x00\x6f\x00\x20\x00\x64\x00\x65\
-\x00\x6c\x00\x20\x00\x70\x00\x72\x00\x6f\x00\x76\x00\x65\x00\x65\
-\x00\x64\x00\x6f\x00\x72\x08\x00\x00\x00\x00\x06\x00\x00\x00\x21\
-\x26\x54\x72\x75\x73\x74\x20\x74\x68\x69\x73\x20\x70\x72\x6f\x76\
-\x69\x64\x65\x72\x20\x63\x65\x72\x74\x69\x66\x69\x63\x61\x74\x65\
-\x2e\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\
-\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x34\x00\x56\
-\x00\x61\x00\x6c\x00\x69\x00\x64\x00\x61\x00\x63\x00\x69\x00\x6f\
-\x00\x6e\x00\x20\x00\x64\x00\x65\x00\x6c\x00\x20\x00\x63\x00\x65\
-\x00\x72\x00\x74\x00\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\x64\
-\x00\x6f\x08\x00\x00\x00\x00\x06\x00\x00\x00\x16\x43\x65\x72\x74\
-\x69\x66\x69\x63\x61\x74\x65\x20\x76\x61\x6c\x69\x64\x61\x74\x69\
-\x6f\x6e\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\
-\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x48\x00\
-\x43\x00\x6f\x00\x6d\x00\x70\x00\x72\x00\x6f\x00\x62\x00\x61\x00\
-\x6e\x00\x64\x00\x6f\x00\x20\x00\x73\x00\x69\x00\x20\x00\x6c\x00\
-\x61\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x65\x00\x78\x00\x69\x00\
-\x6f\x00\x6e\x00\x20\x00\x65\x00\x73\x00\x20\x00\x73\x00\x65\x00\
-\x67\x00\x75\x00\x72\x00\x61\x08\x00\x00\x00\x00\x06\x00\x00\x00\
-\x20\x43\x68\x65\x63\x6b\x69\x6e\x67\x20\x66\x6f\x72\x20\x61\x20\
-\x73\x65\x63\x75\x72\x65\x20\x63\x6f\x6e\x6e\x65\x63\x74\x69\x6f\
-\x6e\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\
-\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x4a\x00\x43\
-\x00\x6f\x00\x6d\x00\x70\x00\x72\x00\x6f\x00\x62\x00\x61\x00\x6e\
-\x00\x64\x00\x6f\x00\x20\x00\x73\x00\x69\x00\x20\x00\x65\x00\x73\
-\x00\x20\x00\x75\x00\x6e\x00\x20\x00\x70\x00\x72\x00\x6f\x00\x76\
-\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x00\x20\x00\x76\x00\x61\
-\x00\x6c\x00\x69\x00\x64\x00\x6f\x08\x00\x00\x00\x00\x06\x00\x00\
-\x00\x22\x43\x68\x65\x63\x6b\x69\x6e\x67\x20\x69\x66\x20\x69\x74\
-\x20\x69\x73\x20\x61\x20\x76\x61\x6c\x69\x64\x20\x70\x72\x6f\x76\
-\x69\x64\x65\x72\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\
-\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\
-\x6c\x00\x6e\x00\x6f\x00\x20\x00\x73\x00\x65\x00\x20\x00\x70\x00\
-\x75\x00\x64\x00\x6f\x00\x20\x00\x6f\x00\x62\x00\x74\x00\x65\x00\
-\x6e\x00\x65\x00\x72\x00\x20\x00\x69\x00\x6e\x00\x66\x00\x6f\x00\
-\x20\x00\x64\x00\x65\x00\x6c\x00\x20\x00\x70\x00\x72\x00\x6f\x00\
-\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x00\x20\x00\x28\x00\
-\x72\x00\x65\x00\x66\x00\x75\x00\x73\x00\x65\x00\x64\x00\x20\x00\
-\x63\x00\x6f\x00\x6e\x00\x6e\x00\x2e\x00\x29\x00\x2e\x08\x00\x00\
-\x00\x00\x06\x00\x00\x00\x31\x43\x6f\x75\x6c\x64\x20\x6e\x6f\x74\
-\x20\x64\x6f\x77\x6e\x6c\x6f\x61\x64\x20\x70\x72\x6f\x76\x69\x64\
-\x65\x72\x20\x69\x6e\x66\x6f\x20\x28\x72\x65\x66\x75\x73\x65\x64\
-\x20\x63\x6f\x6e\x6e\x2e\x29\x2e\x07\x00\x00\x00\x12\x53\x65\x6c\
-\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\
-\x03\x00\x00\x00\x4a\x00\x6e\x00\x6f\x00\x20\x00\x73\x00\x65\x00\
-\x20\x00\x70\x00\x75\x00\x64\x00\x6f\x00\x20\x00\x6f\x00\x62\x00\
-\x74\x00\x65\x00\x6e\x00\x65\x00\x72\x00\x20\x00\x69\x00\x6e\x00\
-\x66\x00\x6f\x00\x20\x00\x64\x00\x65\x00\x6c\x00\x20\x00\x70\x00\
-\x72\x00\x6f\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x08\
-\x00\x00\x00\x00\x06\x00\x00\x00\x21\x43\x6f\x75\x6c\x64\x20\x6e\
-\x6f\x74\x20\x67\x65\x74\x20\x69\x6e\x66\x6f\x20\x66\x72\x6f\x6d\
-\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x2e\x07\x00\x00\x00\x12\x53\
-\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\
-\x65\x01\x03\x00\x00\x00\x54\x00\x43\x00\x6f\x00\x6e\x00\x66\x00\
-\x69\x00\x61\x00\x73\x00\x20\x00\x65\x00\x6e\x00\x20\x00\x65\x00\
-\x73\x00\x74\x00\x65\x00\x20\x00\x63\x00\x65\x00\x72\x00\x74\x00\
-\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\x64\x00\x6f\x00\x20\x00\
-\x64\x00\x65\x00\x6c\x00\x20\x00\x70\x00\x72\x00\x6f\x00\x76\x00\
-\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x00\x3f\x08\x00\x00\x00\x00\
-\x06\x00\x00\x00\x36\x44\x6f\x20\x79\x6f\x75\x20\x77\x61\x6e\x74\
-\x20\x74\x6f\x20\x3c\x62\x3e\x74\x72\x75\x73\x74\x20\x74\x68\x69\
-\x73\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x20\x63\x65\x72\x74\x69\
-\x66\x69\x63\x61\x74\x65\x3f\x3c\x2f\x62\x3e\x07\x00\x00\x00\x12\
-\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\x61\
-\x67\x65\x01\x03\x00\x00\x00\x24\x00\x45\x00\x6e\x00\x74\x00\x72\
-\x00\x61\x00\x20\x00\x74\x00\x75\x00\x20\x00\x50\x00\x72\x00\x6f\
-\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x08\x00\x00\x00\
-\x00\x06\x00\x00\x00\x0e\x45\x6e\x74\x65\x72\x20\x50\x72\x6f\x76\
-\x69\x64\x65\x72\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\
-\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\
-\x3a\x00\x4f\x00\x62\x00\x74\x00\x65\x00\x6e\x00\x69\x00\x65\x00\
-\x6e\x00\x64\x00\x6f\x00\x20\x00\x69\x00\x6e\x00\x66\x00\x6f\x00\
-\x20\x00\x64\x00\x65\x00\x6c\x00\x20\x00\x70\x00\x72\x00\x6f\x00\
-\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x08\x00\x00\x00\x00\
-\x06\x00\x00\x00\x1e\x47\x65\x74\x74\x69\x6e\x67\x20\x69\x6e\x66\
-\x6f\x20\x66\x72\x6f\x6d\x20\x74\x68\x65\x20\x70\x72\x6f\x76\x69\
-\x64\x65\x72\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\
-\x6f\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x88\
-\x00\x49\x00\x6e\x00\x74\x00\x72\x00\x6f\x00\x64\x00\x75\x00\x63\
-\x00\x65\x00\x20\x00\x65\x00\x6c\x00\x20\x00\x64\x00\x6f\x00\x6d\
-\x00\x69\x00\x6e\x00\x69\x00\x6f\x00\x20\x00\x64\x00\x65\x00\x6c\
-\x00\x20\x00\x70\x00\x72\x00\x6f\x00\x76\x00\x65\x00\x65\x00\x64\
-\x00\x6f\x00\x72\x00\x20\x00\x71\x00\x75\x00\x65\x00\x20\x00\x71\
-\x00\x75\x00\x69\x00\x65\x00\x72\x00\x65\x00\x73\x00\x20\x00\x75\
-\x00\x73\x00\x61\x00\x72\x00\x20\x00\x70\x00\x61\x00\x72\x00\x61\
-\x00\x20\x00\x74\x00\x75\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x65\
-\x00\x78\x00\x69\x00\x6f\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\
-\x00\x4b\x50\x6c\x65\x61\x73\x65\x20\x65\x6e\x74\x65\x72\x20\x74\
-\x68\x65\x20\x64\x6f\x6d\x61\x69\x6e\x20\x6f\x66\x20\x74\x68\x65\
-\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x20\x79\x6f\x75\x20\x77\x61\
-\x6e\x74\x20\x74\x6f\x20\x75\x73\x65\x20\x66\x6f\x72\x20\x79\x6f\
-\x75\x72\x20\x63\x6f\x6e\x6e\x65\x63\x74\x69\x6f\x6e\x07\x00\x00\
-\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\
-\x50\x61\x67\x65\x01\x03\x00\x00\x00\x44\x00\x46\x00\x69\x00\x6e\
-\x00\x67\x00\x65\x00\x72\x00\x70\x00\x72\x00\x69\x00\x6e\x00\x74\
-\x00\x20\x00\x53\x00\x48\x00\x41\x00\x2d\x00\x32\x00\x35\x00\x36\
-\x00\x3a\x00\x20\x00\x3c\x00\x69\x00\x3e\x00\x25\x00\x73\x00\x3c\
-\x00\x2f\x00\x69\x00\x3e\x00\x3c\x00\x62\x00\x72\x00\x3e\x08\x00\
-\x00\x00\x00\x06\x00\x00\x00\x22\x53\x48\x41\x2d\x32\x35\x36\x20\
-\x66\x69\x6e\x67\x65\x72\x70\x72\x69\x6e\x74\x3a\x20\x3c\x69\x3e\
-\x25\x73\x3c\x2f\x69\x3e\x3c\x62\x72\x3e\x07\x00\x00\x00\x12\x53\
-\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\
-\x65\x01\x03\x00\x00\x00\x62\x00\x4e\x00\x6f\x00\x20\x00\x73\x00\
-\x65\x00\x20\x00\x70\x00\x75\x00\x64\x00\x6f\x00\x20\x00\x76\x00\
-\x65\x00\x72\x00\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\x72\x00\
-\x20\x00\x65\x00\x6c\x00\x20\x00\x63\x00\x65\x00\x72\x00\x74\x00\
-\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\x64\x00\x6f\x00\x20\x00\
-\x64\x00\x65\x00\x6c\x00\x20\x00\x73\x00\x65\x00\x72\x00\x76\x00\
-\x69\x00\x64\x00\x6f\x00\x72\x00\x2e\x08\x00\x00\x00\x00\x06\x00\
-\x00\x00\x29\x53\x65\x72\x76\x65\x72\x20\x63\x65\x72\x74\x69\x66\
-\x69\x63\x61\x74\x65\x20\x63\x6f\x75\x6c\x64\x20\x6e\x6f\x74\x20\
-\x62\x65\x20\x76\x65\x72\x69\x66\x69\x65\x64\x2e\x07\x00\x00\x00\
-\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\
-\x61\x67\x65\x01\x03\x00\x00\x00\x16\x00\x63\x00\x6f\x00\x6d\x00\
-\x70\x00\x72\x00\x6f\x00\x26\x00\x62\x00\x61\x00\x72\x00\x21\x08\
-\x00\x00\x00\x00\x06\x00\x00\x00\x07\x63\x68\x65\x63\x26\x6b\x21\
-\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\
-\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x14\x00\x26\x00\
-\x41\x00\x63\x00\x65\x00\x72\x00\x63\x00\x61\x00\x20\x00\x64\x00\
-\x65\x08\x00\x00\x00\x00\x06\x00\x00\x00\x06\x26\x41\x62\x6f\x75\
-\x74\x07\x00\x00\x00\x18\x53\x74\x61\x74\x75\x73\x41\x77\x61\x72\
-\x65\x54\x72\x61\x79\x49\x63\x6f\x6e\x4d\x69\x78\x69\x6e\x01\x03\
-\x00\x00\x00\x18\x00\x26\x00\x44\x00\x65\x00\x74\x00\x61\x00\x6c\
-\x00\x6c\x00\x65\x00\x73\x00\x2e\x00\x2e\x00\x2e\x08\x00\x00\x00\
-\x00\x06\x00\x00\x00\x0b\x26\x44\x65\x74\x61\x69\x6c\x73\x2e\x2e\
-\x2e\x07\x00\x00\x00\x18\x53\x74\x61\x74\x75\x73\x41\x77\x61\x72\
-\x65\x54\x72\x61\x79\x49\x63\x6f\x6e\x4d\x69\x78\x69\x6e\x01\x03\
-\x00\x00\x00\x0c\x00\x26\x00\x53\x00\x61\x00\x6c\x00\x69\x00\x72\
-\x08\x00\x00\x00\x00\x06\x00\x00\x00\x05\x26\x51\x75\x69\x74\x07\
-\x00\x00\x00\x18\x53\x74\x61\x74\x75\x73\x41\x77\x61\x72\x65\x54\
-\x72\x61\x79\x49\x63\x6f\x6e\x4d\x69\x78\x69\x6e\x01\x03\x00\x00\
-\x00\x26\x00\x3c\x00\x62\x00\x3e\x00\x64\x00\x65\x00\x73\x00\x63\
-\x00\x6f\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x61\x00\x64\x00\x6f\
-\x00\x3c\x00\x2f\x00\x62\x00\x3e\x08\x00\x00\x00\x00\x06\x00\x00\
-\x00\x13\x3c\x62\x3e\x64\x69\x73\x63\x6f\x6e\x6e\x65\x63\x74\x65\
-\x64\x3c\x2f\x62\x3e\x07\x00\x00\x00\x18\x53\x74\x61\x74\x75\x73\
-\x41\x77\x61\x72\x65\x54\x72\x61\x79\x49\x63\x6f\x6e\x4d\x69\x78\
-\x69\x6e\x01\x03\x00\x00\x00\x12\x00\x41\x00\x63\x00\x65\x00\x72\
-\x00\x63\x00\x61\x00\x20\x00\x64\x00\x65\x08\x00\x00\x00\x00\x06\
-\x00\x00\x00\x05\x41\x62\x6f\x75\x74\x07\x00\x00\x00\x18\x53\x74\
-\x61\x74\x75\x73\x41\x77\x61\x72\x65\x54\x72\x61\x79\x49\x63\x6f\
-\x6e\x4d\x69\x78\x69\x6e\x01\x03\x00\x00\x00\x1a\x00\x41\x00\x63\
-\x00\x65\x00\x72\x00\x63\x00\x61\x00\x20\x00\x64\x00\x65\x00\x20\
-\x00\x51\x00\x26\x00\x74\x08\x00\x00\x00\x00\x06\x00\x00\x00\x09\
-\x41\x62\x6f\x75\x74\x20\x51\x26\x74\x07\x00\x00\x00\x18\x53\x74\
-\x61\x74\x75\x73\x41\x77\x61\x72\x65\x54\x72\x61\x79\x49\x63\x6f\
-\x6e\x4d\x69\x78\x69\x6e\x01\x03\x00\x00\x00\x32\x00\x45\x00\x73\
-\x00\x74\x00\x61\x00\x64\x00\x6f\x00\x20\x00\x64\x00\x65\x00\x20\
-\x00\x6c\x00\x61\x00\x20\x00\x43\x00\x6f\x00\x6e\x00\x65\x00\x78\
-\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\x45\x00\x49\x00\x50\x08\x00\
-\x00\x00\x00\x06\x00\x00\x00\x15\x45\x49\x50\x20\x43\x6f\x6e\x6e\
-\x65\x63\x74\x69\x6f\x6e\x20\x53\x74\x61\x74\x75\x73\x07\x00\x00\
-\x00\x18\x53\x74\x61\x74\x75\x73\x41\x77\x61\x72\x65\x54\x72\x61\
-\x79\x49\x63\x6f\x6e\x4d\x69\x78\x69\x6e\x01\x03\x00\x00\x00\x2c\
-\x00\x43\x00\x69\x00\x66\x00\x72\x00\x61\x00\x64\x00\x6f\x00\x20\
-\x00\x4f\x00\x46\x00\x46\x00\x20\x00\x7c\x00\x20\x00\x74\x00\x75\
-\x00\x72\x00\x6e\x00\x20\x00\x26\x00\x6f\x00\x6e\x08\x00\x00\x00\
-\x00\x06\x00\x00\x00\x19\x45\x6e\x63\x72\x79\x70\x74\x69\x6f\x6e\
-\x20\x4f\x46\x46\x20\x20\x20\x74\x75\x72\x6e\x20\x26\x6f\x6e\x07\
-\x00\x00\x00\x18\x53\x74\x61\x74\x75\x73\x41\x77\x61\x72\x65\x54\
-\x72\x61\x79\x49\x63\x6f\x6e\x4d\x69\x78\x69\x6e\x01\x03\x00\x00\
-\x00\x2c\x00\x43\x00\x69\x00\x66\x00\x72\x00\x61\x00\x64\x00\x6f\
-\x00\x20\x00\x4f\x00\x4e\x00\x20\x00\x7c\x00\x20\x00\x74\x00\x75\
-\x00\x72\x00\x6e\x00\x20\x00\x26\x00\x6f\x00\x66\x00\x66\x08\x00\
-\x00\x00\x00\x06\x00\x00\x00\x1b\x45\x6e\x63\x72\x79\x70\x74\x69\
-\x6f\x6e\x20\x4f\x4e\x20\x20\x20\x20\x20\x74\x75\x72\x6e\x20\x26\
-\x6f\x66\x66\x07\x00\x00\x00\x18\x53\x74\x61\x74\x75\x73\x41\x77\
-\x61\x72\x65\x54\x72\x61\x79\x49\x63\x6f\x6e\x4d\x69\x78\x69\x6e\
-\x01\x03\x00\x00\x00\x2c\x00\x43\x00\x69\x00\x66\x00\x72\x00\x61\
-\x00\x64\x00\x6f\x00\x20\x00\x4f\x00\x4e\x00\x20\x00\x7c\x00\x20\
-\x00\x74\x00\x75\x00\x72\x00\x6e\x00\x20\x00\x6f\x00\x26\x00\x66\
-\x00\x66\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1a\x45\x6e\x63\x72\
-\x79\x70\x74\x69\x6f\x6e\x20\x4f\x4e\x20\x20\x20\x20\x74\x75\x72\
-\x6e\x20\x6f\x26\x66\x66\x07\x00\x00\x00\x18\x53\x74\x61\x74\x75\
-\x73\x41\x77\x61\x72\x65\x54\x72\x61\x79\x49\x63\x6f\x6e\x4d\x69\
-\x78\x69\x6e\x01\x03\x00\x00\x00\x1a\x00\x63\x00\x6f\x00\x6e\x00\
-\x65\x00\x63\x00\x74\x00\x61\x00\x6e\x00\x64\x00\x6f\x00\x2e\x00\
-\x2e\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0d\x63\x6f\x6e\
-\x6e\x65\x63\x74\x69\x6e\x67\x2e\x2e\x2e\x07\x00\x00\x00\x18\x53\
-\x74\x61\x74\x75\x73\x41\x77\x61\x72\x65\x54\x72\x61\x79\x49\x63\
-\x6f\x6e\x4d\x69\x78\x69\x6e\x01\x88\x00\x00\x00\x02\x01\x01\
-"
-
-qt_resource_name = "\
-\x00\x0c\
-\x0d\xfc\x11\x13\
-\x00\x74\
-\x00\x72\x00\x61\x00\x6e\x00\x73\x00\x6c\x00\x61\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x73\
-\x00\x05\
-\x00\x6a\x85\x7d\
-\x00\x64\
-\x00\x65\x00\x2e\x00\x71\x00\x6d\
-\x00\x05\
-\x00\x6c\x65\x7d\
-\x00\x65\
-\x00\x73\x00\x2e\x00\x71\x00\x6d\
-"
-
-qt_resource_struct = "\
-\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
-\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\
-\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
-\x00\x00\x00\x2e\x00\x00\x00\x00\x00\x01\x00\x00\x17\x98\
-"
-
-def qInitResources():
- QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
-
-def qCleanupResources():
- QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
-
-qInitResources()
diff --git a/src/leap/gui/loggerwindow.py b/src/leap/gui/loggerwindow.py
new file mode 100644
index 00000000..fcbdbf19
--- /dev/null
+++ b/src/leap/gui/loggerwindow.py
@@ -0,0 +1,137 @@
+# -*- coding: utf-8 -*-
+# loggerwindow.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+History log window
+"""
+import logging
+
+from PySide import QtGui
+from ui_loggerwindow import Ui_LoggerWindow
+from leap.common.check import leap_assert, leap_assert_type
+from leap.util.leap_log_handler import LeapLogHandler
+
+logger = logging.getLogger(__name__)
+
+
+class LoggerWindow(QtGui.QDialog):
+ """
+ Window that displays a history of the logged messages in the app.
+ """
+ def __init__(self, handler):
+ """
+ Initialize the widget with the custom handler.
+
+ :param handler: Custom handler that supports history and signal.
+ :type handler: LeapLogHandler.
+ """
+ QtGui.QDialog.__init__(self)
+ leap_assert(handler, "We need a handler for the logger window")
+ leap_assert_type(handler, LeapLogHandler)
+
+ # Load UI
+ self.ui = Ui_LoggerWindow()
+ self.ui.setupUi(self)
+
+ # Make connections
+ self.ui.btnSave.clicked.connect(self._save_log_to_file)
+ self.ui.btnDebug.toggled.connect(self._load_history),
+ self.ui.btnInfo.toggled.connect(self._load_history),
+ self.ui.btnWarning.toggled.connect(self._load_history),
+ self.ui.btnError.toggled.connect(self._load_history),
+ self.ui.btnCritical.toggled.connect(self._load_history)
+
+ # Load logging history and connect logger with the widget
+ self._logging_handler = handler
+ self._connect_to_handler()
+ self._load_history()
+
+ def _connect_to_handler(self):
+ """
+ This method connects the loggerwindow with the handler through a
+ signal communicate the logger events.
+ """
+ self._logging_handler.new_log.connect(self._add_log_line)
+
+ def _add_log_line(self, log):
+ """
+ Adds a line to the history, only if it's in the desired levels to show.
+
+ :param log: a log record to be inserted in the widget
+ :type log: a dict with RECORD_KEY and MESSAGE_KEY.
+ the record contains the LogRecord of the logging module,
+ the message contains the formatted message for the log.
+ """
+ html_style = {
+ logging.DEBUG: "background: #CDFFFF;",
+ logging.INFO: "background: white;",
+ logging.WARNING: "background: #FFFF66;",
+ logging.ERROR: "background: red; color: white;",
+ logging.CRITICAL: "background: red; color: white; font: bold;"
+ }
+ level = log[LeapLogHandler.RECORD_KEY].levelno
+ message = log[LeapLogHandler.MESSAGE_KEY]
+ message = message.replace('\n', '<br>\n')
+
+ if self._logs_to_display[level]:
+ open_tag = "<tr style='" + html_style[level] + "'>"
+ open_tag += "<td width='100%' style='padding: 5px;'>"
+ close_tag = "</td></tr>"
+ message = open_tag + message + close_tag
+
+ self.ui.txtLogHistory.append(message)
+
+ def _load_history(self):
+ """
+ Load the previous logged messages in the widget.
+ They are stored in the custom handler.
+ """
+ self._set_logs_to_display()
+ self.ui.txtLogHistory.clear()
+ history = self._logging_handler.log_history
+ for line in history:
+ self._add_log_line(line)
+
+ def _set_logs_to_display(self):
+ """
+ Sets the logs_to_display dict getting the toggled options from the ui
+ """
+ self._logs_to_display = {
+ logging.DEBUG: self.ui.btnDebug.isChecked(),
+ logging.INFO: self.ui.btnInfo.isChecked(),
+ logging.WARNING: self.ui.btnWarning.isChecked(),
+ logging.ERROR: self.ui.btnError.isChecked(),
+ logging.CRITICAL: self.ui.btnCritical.isChecked()
+ }
+
+ def _save_log_to_file(self):
+ """
+ Lets the user save the current log to a file
+ """
+ fileName, filtr = QtGui.QFileDialog.getSaveFileName(
+ self, self.tr("Save As"))
+
+ if fileName:
+ try:
+ with open(fileName, 'w') as output:
+ output.write(self.ui.txtLogHistory.toPlainText())
+ output.write('\n')
+ logger.debug('Log saved in %s' % (fileName, ))
+ except IOError, e:
+ logger.error("Error saving log file: %r" % (e, ))
+ else:
+ logger.debug('Log not saved!')
diff --git a/src/leap/gui/login.py b/src/leap/gui/login.py
new file mode 100644
index 00000000..de0b2d50
--- /dev/null
+++ b/src/leap/gui/login.py
@@ -0,0 +1,245 @@
+# -*- coding: utf-8 -*-
+# login.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Login widget implementation
+"""
+import logging
+
+from PySide import QtCore, QtGui
+from ui_login import Ui_LoginWidget
+
+from leap.util.keyring_helpers import has_keyring
+
+logger = logging.getLogger(__name__)
+
+
+class LoginWidget(QtGui.QWidget):
+ """
+ Login widget that emits signals to display the wizard or to
+ perform login.
+ """
+
+ # Emitted when the login button is clicked
+ login = QtCore.Signal()
+ cancel_login = QtCore.Signal()
+
+ # Emitted when the user selects "Other..." in the provider
+ # combobox or click "Create Account"
+ show_wizard = QtCore.Signal()
+
+ MAX_STATUS_WIDTH = 40
+
+ BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$"
+
+ def __init__(self, settings, parent=None):
+ """
+ Constructs the LoginWidget.
+
+ :param settings: client wide settings
+ :type settings: LeapSettings
+ :param parent: The parent widget for this widget
+ :type parent: QWidget or None
+ """
+ QtGui.QWidget.__init__(self, parent)
+
+ self._settings = settings
+ self._selected_provider_index = -1
+
+ self.ui = Ui_LoginWidget()
+ self.ui.setupUi(self)
+
+ self.ui.chkRemember.stateChanged.connect(
+ self._remember_state_changed)
+ self.ui.chkRemember.setEnabled(has_keyring())
+
+ self.ui.lnPassword.setEchoMode(QtGui.QLineEdit.Password)
+
+ self.ui.btnLogin.clicked.connect(self.login)
+ self.ui.lnPassword.returnPressed.connect(self.login)
+
+ self.ui.lnUser.returnPressed.connect(self._focus_password)
+
+ self.ui.cmbProviders.currentIndexChanged.connect(
+ self._current_provider_changed)
+ self.ui.btnCreateAccount.clicked.connect(
+ self.show_wizard)
+
+ username_re = QtCore.QRegExp(self.BARE_USERNAME_REGEX)
+ self.ui.lnUser.setValidator(
+ QtGui.QRegExpValidator(username_re, self))
+
+ def _remember_state_changed(self, state):
+ """
+ Saves the remember state in the LeapSettings
+
+ :param state: possible stats can be Checked, Unchecked and
+ PartiallyChecked
+ :type state: QtCore.Qt.CheckState
+ """
+ enable = True if state == QtCore.Qt.Checked else False
+ self._settings.set_remember(enable)
+
+ def set_providers(self, provider_list):
+ """
+ Set the provider list to provider_list plus an "Other..." item
+ that triggers the wizard
+
+ :param provider_list: list of providers
+ :type provider_list: list of str
+ """
+ self.ui.cmbProviders.blockSignals(True)
+ self.ui.cmbProviders.clear()
+ self.ui.cmbProviders.addItems(provider_list + [self.tr("Other...")])
+ self.ui.cmbProviders.blockSignals(False)
+
+ def select_provider_by_name(self, name):
+ """
+ Given a provider name/domain, it selects it in the combobox
+
+ :param name: name or domain for the provider
+ :type name: str
+ """
+ provider_index = self.ui.cmbProviders.findText(name)
+ self.ui.cmbProviders.setCurrentIndex(provider_index)
+
+ def get_selected_provider(self):
+ """
+ Returns the selected provider in the combobox
+ """
+ return self.ui.cmbProviders.currentText()
+
+ def set_remember(self, value):
+ """
+ Checks the remember user and password checkbox
+
+ :param value: True to mark it checked, False otherwise
+ :type value: bool
+ """
+ self.ui.chkRemember.setChecked(value)
+
+ def get_remember(self):
+ """
+ Returns the remember checkbox state
+
+ :rtype: bool
+ """
+ return self.ui.chkRemember.isChecked()
+
+ def set_user(self, user):
+ """
+ Sets the user and focuses on the next field, password.
+
+ :param user: user to set the field to
+ :type user: str
+ """
+ self.ui.lnUser.setText(user)
+ self._focus_password()
+
+ def get_user(self):
+ """
+ Returns the user that appears in the widget.
+
+ :rtype: str
+ """
+ return self.ui.lnUser.text()
+
+ def set_password(self, password):
+ """
+ Sets the password for the widget
+
+ :param password: password to set
+ :type password: str
+ """
+ self.ui.lnPassword.setText(password)
+
+ def get_password(self):
+ """
+ Returns the password that appears in the widget
+
+ :rtype: str
+ """
+ return self.ui.lnPassword.text()
+
+ def set_status(self, status, error=True):
+ """
+ Sets the status label at the login stage to status
+
+ :param status: status message
+ :type status: str
+ """
+ if len(status) > self.MAX_STATUS_WIDTH:
+ status = status[:self.MAX_STATUS_WIDTH] + "..."
+ if error:
+ status = "<font color='red'><b>%s</b></font>" % (status,)
+ self.ui.lblStatus.setText(status)
+
+ def set_enabled(self, enabled=False):
+ """
+ Enables or disables all the login widgets
+
+ :param enabled: wether they should be enabled or not
+ :type enabled: bool
+ """
+ self.ui.lnUser.setEnabled(enabled)
+ self.ui.lnPassword.setEnabled(enabled)
+ self.ui.chkRemember.setEnabled(enabled)
+ self.ui.cmbProviders.setEnabled(enabled)
+
+ self._set_cancel(not enabled)
+
+ def _set_cancel(self, enabled=False):
+ """
+ Enables or disables the cancel action in the "log in" process.
+
+ :param enabled: wether it should be enabled or not
+ :type enabled: bool
+ """
+ text = self.tr("Cancel")
+ login_or_cancel = self.cancel_login
+
+ if not enabled:
+ text = self.tr("Log In")
+ login_or_cancel = self.login
+
+ self.ui.btnLogin.setText(text)
+
+ self.ui.btnLogin.clicked.disconnect()
+ self.ui.btnLogin.clicked.connect(login_or_cancel)
+
+ def _focus_password(self):
+ """
+ Focuses in the password lineedit
+ """
+ self.ui.lnPassword.setFocus()
+
+ def _current_provider_changed(self, param):
+ """
+ SLOT
+ TRIGGERS: self.ui.cmbProviders.currentIndexChanged
+ """
+ if param == (self.ui.cmbProviders.count() - 1):
+ self.show_wizard.emit()
+ # Leave the previously selected provider in the combobox
+ prev_provider = 0
+ if self._selected_provider_index != -1:
+ prev_provider = self._selected_provider_index
+ self.ui.cmbProviders.blockSignals(True)
+ self.ui.cmbProviders.setCurrentIndex(prev_provider)
+ self.ui.cmbProviders.blockSignals(False)
+ else:
+ self._selected_provider_index = param
diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py
new file mode 100644
index 00000000..5ace1043
--- /dev/null
+++ b/src/leap/gui/mainwindow.py
@@ -0,0 +1,1537 @@
+# -*- coding: utf-8 -*-
+# mainwindow.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Main window for the leap client
+"""
+import logging
+import os
+import platform
+import tempfile
+from functools import partial
+
+import keyring
+
+from PySide import QtCore, QtGui
+from twisted.internet import threads
+
+from leap.common.check import leap_assert
+from leap.common.events import register
+from leap.common.events import events_pb2 as proto
+from leap.config.leapsettings import LeapSettings
+from leap.config.providerconfig import ProviderConfig
+from leap.crypto.srpauth import SRPAuth
+from leap.gui.loggerwindow import LoggerWindow
+from leap.gui.wizard import Wizard
+from leap.gui.login import LoginWidget
+from leap.gui.statuspanel import StatusPanelWidget
+from leap.services.eip.eipbootstrapper import EIPBootstrapper
+from leap.services.eip.eipconfig import EIPConfig
+from leap.services.eip.providerbootstrapper import ProviderBootstrapper
+# XXX: Soledad might not work out of the box in Windows, issue #2932
+from leap.services.soledad.soledadbootstrapper import SoledadBootstrapper
+from leap.services.mail.smtpbootstrapper import SMTPBootstrapper
+from leap.services.mail import imap
+from leap.platform_init import IS_WIN, IS_MAC
+from leap.platform_init.initializers import init_platform
+
+from leap.services.eip.vpnprocess import VPN
+from leap.services.eip.vpnprocess import OpenVPNAlreadyRunning
+from leap.services.eip.vpnprocess import AlienOpenVPNAlreadyRunning
+
+from leap.services.eip.vpnlaunchers import VPNLauncherException
+from leap.services.eip.vpnlaunchers import OpenVPNNotFoundException
+from leap.services.eip.vpnlaunchers import EIPNoPkexecAvailable
+from leap.services.eip.vpnlaunchers import EIPNoPolkitAuthAgentAvailable
+from leap.services.eip.vpnlaunchers import EIPNoTunKextLoaded
+
+from leap.util import __version__ as VERSION
+from leap.util.keyring_helpers import has_keyring
+
+from leap.services.mail.smtpconfig import SMTPConfig
+
+if IS_WIN:
+ from leap.platform_init.locks import WindowsLock
+ from leap.platform_init.locks import raise_window_ack
+
+from ui_mainwindow import Ui_MainWindow
+
+logger = logging.getLogger(__name__)
+
+
+class MainWindow(QtGui.QMainWindow):
+ """
+ Main window for login and presenting status updates to the user
+ """
+
+ # StackedWidget indexes
+ LOGIN_INDEX = 0
+ EIP_STATUS_INDEX = 1
+
+ # Keyring
+ KEYRING_KEY = "bitmask"
+
+ # SMTP
+ PORT_KEY = "port"
+ IP_KEY = "ip_address"
+
+ OPENVPN_SERVICE = "openvpn"
+ MX_SERVICE = "mx"
+
+ # Signals
+ new_updates = QtCore.Signal(object)
+ raise_window = QtCore.Signal([])
+ soledad_ready = QtCore.Signal([])
+
+ # We use this flag to detect abnormal terminations
+ user_stopped_eip = False
+
+ def __init__(self, quit_callback,
+ standalone=False,
+ openvpn_verb=1,
+ bypass_checks=False):
+ """
+ Constructor for the client main window
+
+ :param quit_callback: Function to be called when closing
+ the application.
+ :type quit_callback: callable
+
+ :param standalone: Set to true if the app should use configs
+ inside its pwd
+ :type standalone: bool
+
+ :param bypass_checks: Set to true if the app should bypass
+ first round of checks for CA
+ certificates at bootstrap
+ :type bypass_checks: bool
+ """
+ QtGui.QMainWindow.__init__(self)
+
+ # register leap events
+ register(signal=proto.UPDATER_NEW_UPDATES,
+ callback=self._new_updates_available,
+ reqcbk=lambda req, resp: None) # make rpc call async
+ register(signal=proto.RAISE_WINDOW,
+ callback=self._on_raise_window_event,
+ reqcbk=lambda req, resp: None) # make rpc call async
+
+ self._quit_callback = quit_callback
+
+ self._updates_content = ""
+
+ self.ui = Ui_MainWindow()
+ self.ui.setupUi(self)
+
+ self._settings = LeapSettings(standalone)
+
+ self._login_widget = LoginWidget(
+ self._settings,
+ self.ui.stackedWidget.widget(self.LOGIN_INDEX))
+ self.ui.loginLayout.addWidget(self._login_widget)
+
+ # Signals
+ # TODO separate logic from ui signals.
+
+ self._login_widget.login.connect(self._login)
+ self._login_widget.cancel_login.connect(self._cancel_login)
+ self._login_widget.show_wizard.connect(
+ self._launch_wizard)
+
+ self.ui.btnShowLog.clicked.connect(self._show_logger_window)
+
+ self._status_panel = StatusPanelWidget(
+ self.ui.stackedWidget.widget(self.EIP_STATUS_INDEX))
+ self.ui.statusLayout.addWidget(self._status_panel)
+
+ self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX)
+
+ self._status_panel.start_eip.connect(self._start_eip)
+ self._status_panel.stop_eip.connect(self._stop_eip)
+
+ # This is loaded only once, there's a bug when doing that more
+ # than once
+ ProviderConfig.standalone = standalone
+ EIPConfig.standalone = standalone
+ self._standalone = standalone
+ self._provider_config = ProviderConfig()
+ # Used for automatic start of EIP
+ self._provisional_provider_config = ProviderConfig()
+ self._eip_config = EIPConfig()
+
+ self._already_started_eip = False
+
+ # This is created once we have a valid provider config
+ self._srp_auth = None
+ self._logged_user = None
+
+ # This thread is always running, although it's quite
+ # lightweight when it's done setting up provider
+ # configuration and certificate.
+ self._provider_bootstrapper = ProviderBootstrapper(bypass_checks)
+
+ # Intermediate stages, only do something if there was an error
+ self._provider_bootstrapper.name_resolution.connect(
+ self._intermediate_stage)
+ self._provider_bootstrapper.https_connection.connect(
+ self._intermediate_stage)
+ self._provider_bootstrapper.download_ca_cert.connect(
+ self._intermediate_stage)
+
+ # Important stages, loads the provider config and checks
+ # certificates
+ self._provider_bootstrapper.download_provider_info.connect(
+ self._load_provider_config)
+ self._provider_bootstrapper.check_api_certificate.connect(
+ self._provider_config_loaded)
+
+ # This thread is similar to the provider bootstrapper
+ self._eip_bootstrapper = EIPBootstrapper()
+
+ self._eip_bootstrapper.download_config.connect(
+ self._eip_intermediate_stage)
+ self._eip_bootstrapper.download_client_certificate.connect(
+ self._finish_eip_bootstrap)
+
+ self._soledad_bootstrapper = SoledadBootstrapper()
+ self._soledad_bootstrapper.download_config.connect(
+ self._soledad_intermediate_stage)
+ self._soledad_bootstrapper.gen_key.connect(
+ self._soledad_bootstrapped_stage)
+
+ self._smtp_bootstrapper = SMTPBootstrapper()
+ self._smtp_bootstrapper.download_config.connect(
+ self._smtp_bootstrapped_stage)
+
+ self._vpn = VPN(openvpn_verb=openvpn_verb)
+ self._vpn.qtsigs.state_changed.connect(
+ self._status_panel.update_vpn_state)
+ self._vpn.qtsigs.status_changed.connect(
+ self._status_panel.update_vpn_status)
+ self._vpn.qtsigs.process_finished.connect(
+ self._eip_finished)
+
+ self.ui.action_log_out.setEnabled(False)
+ self.ui.action_log_out.triggered.connect(self._logout)
+ self.ui.action_about_leap.triggered.connect(self._about)
+ self.ui.action_quit.triggered.connect(self.quit)
+ self.ui.action_wizard.triggered.connect(self._launch_wizard)
+ self.ui.action_show_logs.triggered.connect(self._show_logger_window)
+ self.raise_window.connect(self._do_raise_mainwindow)
+
+ # Used to differentiate between real quits and close to tray
+ self._really_quit = False
+
+ self._systray = None
+
+ self._action_eip_provider = QtGui.QAction(
+ self.tr("No default provider"), self)
+ self._action_eip_provider.setEnabled(False)
+ self._action_eip_status = QtGui.QAction(
+ self.tr("Encrypted internet is OFF"),
+ self)
+ self._action_eip_status.setEnabled(False)
+
+ self._status_panel.set_action_eip_status(
+ self._action_eip_status)
+
+ self._action_eip_startstop = QtGui.QAction(
+ self.tr("Turn OFF"), self)
+ self._action_eip_startstop.triggered.connect(
+ self._stop_eip)
+ self._action_eip_startstop.setEnabled(False)
+ self._status_panel.set_action_eip_startstop(
+ self._action_eip_startstop)
+
+ self._action_visible = QtGui.QAction(self.tr("Hide Main Window"), self)
+ self._action_visible.triggered.connect(self._toggle_visible)
+
+ self._enabled_services = []
+
+ self._center_window()
+
+ self.ui.lblNewUpdates.setVisible(False)
+ self.ui.btnMore.setVisible(False)
+ self.ui.btnMore.clicked.connect(self._updates_details)
+
+ self.new_updates.connect(self._react_to_new_updates)
+ self.soledad_ready.connect(self._start_imap_service)
+
+ init_platform()
+
+ self._wizard = None
+ self._wizard_firstrun = False
+
+ self._logger_window = None
+
+ self._bypass_checks = bypass_checks
+
+ self._soledad = None
+ self._keymanager = None
+ self._imap_service = None
+
+ self._login_defer = None
+ self._download_provider_defer = None
+
+ self._smtp_config = SMTPConfig()
+
+ if self._first_run():
+ self._wizard_firstrun = True
+ self._wizard = Wizard(standalone=standalone,
+ bypass_checks=bypass_checks)
+ # Give this window time to finish init and then show the wizard
+ QtCore.QTimer.singleShot(1, self._launch_wizard)
+ self._wizard.accepted.connect(self._finish_init)
+ self._wizard.rejected.connect(self._rejected_wizard)
+ else:
+ self._finish_init()
+
+ def _rejected_wizard(self):
+ """
+ SLOT
+ TRIGGERS: self._wizard.rejected
+
+ Called if the wizard has been cancelled or closed before
+ finishing.
+ """
+ if self._wizard_firstrun:
+ self._settings.set_properprovider(False)
+ self.quit()
+ else:
+ self._finish_init()
+
+ def _launch_wizard(self):
+ """
+ SLOT
+ TRIGGERS:
+ self._login_widget.show_wizard
+ self.ui.action_wizard.triggered
+
+ Also called in first run.
+
+ Launches the wizard, creating the object itself if not already
+ there.
+ """
+ if self._wizard is None:
+ self._wizard = Wizard(bypass_checks=self._bypass_checks)
+ self._wizard.accepted.connect(self._finish_init)
+ self._wizard.rejected.connect(self._wizard.close)
+
+ self.setVisible(False)
+ # Do NOT use exec_, it will use a child event loop!
+ # Refer to http://www.themacaque.com/?p=1067 for funny details.
+ self._wizard.show()
+ if IS_MAC:
+ self._wizard.raise_()
+ self._wizard.finished.connect(self._wizard_finished)
+
+ def _wizard_finished(self):
+ """
+ SLOT
+ TRIGGERS
+ self._wizard.finished
+
+ Called when the wizard has finished.
+ """
+ self.setVisible(True)
+
+ def _get_leap_logging_handler(self):
+ """
+ Gets the leap handler from the top level logger
+
+ :return: a logging handler or None
+ :rtype: LeapLogHandler or None
+ """
+ from leap.util.leap_log_handler import LeapLogHandler
+ leap_logger = logging.getLogger('leap')
+ for h in leap_logger.handlers:
+ if isinstance(h, LeapLogHandler):
+ return h
+ return None
+
+ def _show_logger_window(self):
+ """
+ SLOT
+ TRIGGERS:
+ self.ui.action_show_logs.triggered
+ self.ui.btnShowLog.clicked
+
+ Displays the window with the history of messages logged until now
+ and displays the new ones on arrival.
+ """
+ if self._logger_window is None:
+ leap_log_handler = self._get_leap_logging_handler()
+ if leap_log_handler is None:
+ logger.error('Leap logger handler not found')
+ else:
+ self._logger_window = LoggerWindow(handler=leap_log_handler)
+ self._logger_window.setVisible(
+ not self._logger_window.isVisible())
+ self.ui.btnShowLog.setChecked(self._logger_window.isVisible())
+ else:
+ self._logger_window.setVisible(not self._logger_window.isVisible())
+ self.ui.btnShowLog.setChecked(self._logger_window.isVisible())
+
+ self._logger_window.finished.connect(self._uncheck_logger_button)
+
+ def _uncheck_logger_button(self):
+ """
+ SLOT
+ Sets the checked state of the loggerwindow button to false.
+ """
+ self.ui.btnShowLog.setChecked(False)
+
+ def _new_updates_available(self, req):
+ """
+ Callback for the new updates event
+
+ :param req: Request type
+ :type req: leap.common.events.events_pb2.SignalRequest
+ """
+ self.new_updates.emit(req)
+
+ def _react_to_new_updates(self, req):
+ """
+ SLOT
+ TRIGGER: self._new_updates_available
+
+ Displays the new updates label and sets the updates_content
+ """
+ self.moveToThread(QtCore.QCoreApplication.instance().thread())
+ self.ui.lblNewUpdates.setVisible(True)
+ self.ui.btnMore.setVisible(True)
+ self._updates_content = req.content
+
+ def _updates_details(self):
+ """
+ SLOT
+ TRIGGER: self.ui.btnMore.clicked
+
+ Parses and displays the updates details
+ """
+ msg = self.tr("The Bitmask app is ready to update, please"
+ " restart the application.")
+
+ # We assume that if there is nothing in the contents, then
+ # the Bitmask bundle is what needs updating.
+ if len(self._updates_content) > 0:
+ files = self._updates_content.split(", ")
+ files_str = ""
+ for f in files:
+ final_name = f.replace("/data/", "")
+ final_name = final_name.replace(".thp", "")
+ files_str += final_name
+ files_str += "\n"
+ msg += self.tr(" The following components will be updated:\n%s") \
+ % (files_str,)
+
+ QtGui.QMessageBox.information(self,
+ self.tr("Updates available"),
+ msg)
+
+ def _finish_init(self):
+ """
+ SLOT
+ TRIGGERS:
+ self._wizard.accepted
+
+ Also called at the end of the constructor if not first run,
+ and after _rejected_wizard if not first run.
+
+ Implements the behavior after either constructing the
+ mainwindow object, loading the saved user/password, or after
+ the wizard has been executed.
+ """
+ # XXX: May be this can be divided into two methods?
+
+ self._login_widget.set_providers(self._configured_providers())
+ self._show_systray()
+ self.show()
+ if IS_MAC:
+ self.raise_()
+
+ if self._wizard:
+ possible_username = self._wizard.get_username()
+ possible_password = self._wizard.get_password()
+
+ # select the configured provider in the combo box
+ domain = self._wizard.get_domain()
+ self._login_widget.select_provider_by_name(domain)
+
+ self._login_widget.set_remember(self._wizard.get_remember())
+ self._enabled_services = list(self._wizard.get_services())
+ self._settings.set_enabled_services(
+ self._login_widget.get_selected_provider(),
+ self._enabled_services)
+ if possible_username is not None:
+ self._login_widget.set_user(possible_username)
+ if possible_password is not None:
+ self._login_widget.set_password(possible_password)
+ self._login()
+ self._wizard = None
+ self._settings.set_properprovider(True)
+ else:
+ self._try_autostart_eip()
+ if not self._settings.get_remember():
+ # nothing to do here
+ return
+
+ saved_user = self._settings.get_user()
+
+ try:
+ username, domain = saved_user.split('@')
+ except (ValueError, AttributeError) as e:
+ # if the saved_user does not contain an '@' or its None
+ logger.error('Username@provider malformed. %r' % (e, ))
+ saved_user = None
+
+ if saved_user is not None and has_keyring():
+ # fill the username
+ self._login_widget.set_user(username)
+
+ # select the configured provider in the combo box
+ self._login_widget.select_provider_by_name(domain)
+
+ self._login_widget.set_remember(True)
+
+ saved_password = None
+ try:
+ saved_password = keyring.get_password(self.KEYRING_KEY,
+ saved_user
+ .encode("utf8"))
+ except ValueError, e:
+ logger.debug("Incorrect Password. %r." % (e,))
+
+ if saved_password is not None:
+ self._login_widget.set_password(
+ saved_password.decode("utf8"))
+ self._login()
+
+ def _try_autostart_eip(self):
+ """
+ Tries to autostart EIP
+ """
+ default_provider = self._settings.get_defaultprovider()
+
+ if default_provider is None:
+ logger.info("Cannot autostart Encrypted Internet because there is "
+ "no default provider configured")
+ return
+
+ self._action_eip_provider.setText(default_provider)
+
+ self._enabled_services = self._settings.get_enabled_services(
+ default_provider)
+
+ if self._provisional_provider_config.load(
+ os.path.join("leap",
+ "providers",
+ default_provider,
+ "provider.json")):
+ self._download_eip_config()
+ else:
+ # XXX: Display a proper message to the user
+ logger.error("Unable to load %s config, cannot autostart." %
+ (default_provider,))
+
+ def _show_systray(self):
+ """
+ Sets up the systray icon
+ """
+ if self._systray is not None:
+ self._systray.setVisible(True)
+ return
+
+ # Placeholder actions
+ # They are temporary to display the tray as designed
+ preferences_action = QtGui.QAction(self.tr("Preferences"), self)
+ preferences_action.setEnabled(False)
+ help_action = QtGui.QAction(self.tr("Help"), self)
+ help_action.setEnabled(False)
+
+ systrayMenu = QtGui.QMenu(self)
+ systrayMenu.addAction(self._action_visible)
+ systrayMenu.addSeparator()
+ systrayMenu.addAction(self._action_eip_provider)
+ systrayMenu.addAction(self._action_eip_status)
+ systrayMenu.addAction(self._action_eip_startstop)
+ systrayMenu.addSeparator()
+ systrayMenu.addAction(preferences_action)
+ systrayMenu.addAction(help_action)
+ systrayMenu.addSeparator()
+ systrayMenu.addAction(self.ui.action_log_out)
+ systrayMenu.addAction(self.ui.action_quit)
+ self._systray = QtGui.QSystemTrayIcon(self)
+ self._systray.setContextMenu(systrayMenu)
+ self._systray.setIcon(self._status_panel.ERROR_ICON_TRAY)
+ self._systray.setVisible(True)
+ self._systray.activated.connect(self._tray_activated)
+
+ self._status_panel.set_systray(self._systray)
+
+ def _tray_activated(self, reason=None):
+ """
+ SLOT
+ TRIGGER: self._systray.activated
+
+ Displays the context menu from the tray icon
+ """
+ self._update_hideshow_menu()
+
+ context_menu = self._systray.contextMenu()
+ if not IS_MAC:
+ # for some reason, context_menu.show()
+ # is failing in a way beyond my understanding.
+ # (not working the first time it's clicked).
+ # this works however.
+ context_menu.exec_(self._systray.geometry().center())
+
+ def _update_hideshow_menu(self):
+ """
+ Updates the Hide/Show main window menu text based on the
+ visibility of the window.
+ """
+ get_action = lambda visible: (
+ self.tr("Show Main Window"),
+ self.tr("Hide Main Window"))[int(visible)]
+
+ # set labels
+ visible = self.isVisible()
+ self._action_visible.setText(get_action(visible))
+
+ def _toggle_visible(self):
+ """
+ SLOT
+ TRIGGER: self._action_visible.triggered
+
+ Toggles the window visibility
+ """
+ if not self.isVisible():
+ self.show()
+ self.raise_()
+ else:
+ self.hide()
+
+ self._update_hideshow_menu()
+
+ def _center_window(self):
+ """
+ Centers the mainwindow based on the desktop geometry
+ """
+ geometry = self._settings.get_geometry()
+ state = self._settings.get_windowstate()
+
+ if geometry is None:
+ app = QtGui.QApplication.instance()
+ width = app.desktop().width()
+ height = app.desktop().height()
+ window_width = self.size().width()
+ window_height = self.size().height()
+ x = (width / 2.0) - (window_width / 2.0)
+ y = (height / 2.0) - (window_height / 2.0)
+ self.move(x, y)
+ else:
+ self.restoreGeometry(geometry)
+
+ if state is not None:
+ self.restoreState(state)
+
+ def _about(self):
+ """
+ SLOT
+ TRIGGERS: self.ui.action_about_leap.triggered
+
+ Display the About Bitmask dialog
+ """
+ QtGui.QMessageBox.about(
+ self, self.tr("About Bitmask - %s") % (VERSION,),
+ self.tr("Version: <b>%s</b><br>"
+ "<br>"
+ "Bitmask is the Desktop client application for "
+ "the LEAP platform, supporting encrypted internet "
+ "proxy, secure email, and secure chat (coming soon).<br>"
+ "<br>"
+ "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. <br>"
+ "<br>"
+ "<a href='https://leap.se'>More about LEAP"
+ "</a>") % (VERSION,))
+
+ def changeEvent(self, e):
+ """
+ Reimplements the changeEvent method to minimize to tray
+ """
+ if QtGui.QSystemTrayIcon.isSystemTrayAvailable() and \
+ e.type() == QtCore.QEvent.WindowStateChange and \
+ self.isMinimized():
+ self._toggle_visible()
+ e.accept()
+ return
+ QtGui.QMainWindow.changeEvent(self, e)
+
+ def closeEvent(self, e):
+ """
+ Reimplementation of closeEvent to close to tray
+ """
+ if QtGui.QSystemTrayIcon.isSystemTrayAvailable() and \
+ not self._really_quit:
+ self._toggle_visible()
+ e.ignore()
+ return
+
+ self._settings.set_geometry(self.saveGeometry())
+ self._settings.set_windowstate(self.saveState())
+
+ QtGui.QMainWindow.closeEvent(self, e)
+
+ def _configured_providers(self):
+ """
+ Returns the available providers based on the file structure
+
+ :rtype: list
+ """
+
+ # TODO: check which providers have a valid certificate among
+ # other things, not just the directories
+ providers = []
+ try:
+ providers = os.listdir(
+ os.path.join(self._provider_config.get_path_prefix(),
+ "leap",
+ "providers"))
+ except Exception as e:
+ logger.debug("Error listing providers, assume there are none. %r"
+ % (e,))
+
+ return providers
+
+ def _first_run(self):
+ """
+ Returns True if there are no configured providers. False otherwise
+
+ :rtype: bool
+ """
+ has_provider_on_disk = len(self._configured_providers()) != 0
+ is_proper_provider = self._settings.get_properprovider()
+ return not (has_provider_on_disk and is_proper_provider)
+
+ def _download_provider_config(self):
+ """
+ Starts the bootstrapping sequence. It will download the
+ provider configuration if it's not present, otherwise will
+ emit the corresponding signals inmediately
+ """
+ provider = self._login_widget.get_selected_provider()
+
+ pb = self._provider_bootstrapper
+ d = pb.run_provider_select_checks(provider, download_if_needed=True)
+ self._download_provider_defer = d
+
+ def _load_provider_config(self, data):
+ """
+ SLOT
+ TRIGGER: self._provider_bootstrapper.download_provider_info
+
+ Once the provider config has been downloaded, this loads the
+ self._provider_config instance with it and starts the second
+ part of the bootstrapping sequence
+
+ :param data: result from the last stage of the
+ run_provider_select_checks
+ :type data: dict
+ """
+ if data[self._provider_bootstrapper.PASSED_KEY]:
+ provider = self._login_widget.get_selected_provider()
+
+ # If there's no loaded provider or
+ # we want to connect to other provider...
+ if (not self._provider_config.loaded() or
+ self._provider_config.get_domain() != provider):
+ self._provider_config.load(
+ os.path.join("leap", "providers",
+ provider, "provider.json"))
+
+ if self._provider_config.loaded():
+ self._provider_bootstrapper.run_provider_setup_checks(
+ self._provider_config,
+ download_if_needed=True)
+ else:
+ self._login_widget.set_status(
+ self.tr("Unable to login: Problem with provider"))
+ logger.error("Could not load provider configuration.")
+ self._login_widget.set_enabled(True)
+ else:
+ self._login_widget.set_status(
+ self.tr("Unable to login: Problem with provider"))
+ logger.error(data[self._provider_bootstrapper.ERROR_KEY])
+ self._login_widget.set_enabled(True)
+
+ def _login(self):
+ """
+ SLOT
+ TRIGGERS:
+ self._login_widget.login
+
+ Starts the login sequence. Which involves bootstrapping the
+ selected provider if the selection is valid (not empty), then
+ start the SRP authentication, and as the last step
+ bootstrapping the EIP service
+ """
+ leap_assert(self._provider_config, "We need a provider config")
+
+ username = self._login_widget.get_user()
+ password = self._login_widget.get_password()
+ provider = self._login_widget.get_selected_provider()
+
+ self._enabled_services = self._settings.get_enabled_services(
+ self._login_widget.get_selected_provider())
+
+ if len(provider) == 0:
+ self._login_widget.set_status(
+ self.tr("Please select a valid provider"))
+ return
+
+ if len(username) == 0:
+ self._login_widget.set_status(
+ self.tr("Please provide a valid username"))
+ return
+
+ if len(password) == 0:
+ self._login_widget.set_status(
+ self.tr("Please provide a valid Password"))
+ return
+
+ self._login_widget.set_status(self.tr("Logging in..."), error=False)
+ self._login_widget.set_enabled(False)
+
+ if self._login_widget.get_remember() and has_keyring():
+ # in the keyring and in the settings
+ # we store the value 'usename@provider'
+ username_domain = (username + '@' + provider).encode("utf8")
+ try:
+ keyring.set_password(self.KEYRING_KEY,
+ username_domain,
+ password.encode("utf8"))
+ # Only save the username if it was saved correctly in
+ # the keyring
+ self._settings.set_user(username_domain)
+ except Exception as e:
+ logger.error("Problem saving data to keyring. %r"
+ % (e,))
+
+ self._download_provider_config()
+
+ def _cancel_login(self):
+ """
+ SLOT
+ TRIGGERS:
+ self._login_widget.cancel_login
+
+ Stops the login sequence.
+ """
+ logger.debug("Cancelling log in.")
+
+ if self._download_provider_defer:
+ logger.debug("Cancelling download provider defer.")
+ self._download_provider_defer.cancel()
+
+ if self._login_defer:
+ logger.debug("Cancelling login defer.")
+ self._login_defer.cancel()
+
+ def _provider_config_loaded(self, data):
+ """
+ SLOT
+ TRIGGER: self._provider_bootstrapper.check_api_certificate
+
+ Once the provider configuration is loaded, this starts the SRP
+ authentication
+ """
+ leap_assert(self._provider_config, "We need a provider config!")
+
+ if data[self._provider_bootstrapper.PASSED_KEY]:
+ username = self._login_widget.get_user().encode("utf8")
+ password = self._login_widget.get_password().encode("utf8")
+
+ if self._srp_auth is None:
+ self._srp_auth = SRPAuth(self._provider_config)
+ self._srp_auth.authentication_finished.connect(
+ self._authentication_finished)
+ self._srp_auth.logout_finished.connect(
+ self._done_logging_out)
+
+ # TODO: Add errback!
+ self._login_defer = self._srp_auth.authenticate(username, password)
+ else:
+ self._login_widget.set_status(
+ "Unable to login: Problem with provider")
+ logger.error(data[self._provider_bootstrapper.ERROR_KEY])
+ self._login_widget.set_enabled(True)
+
+ def _authentication_finished(self, ok, message):
+ """
+ SLOT
+ TRIGGER: self._srp_auth.authentication_finished
+
+ Once the user is properly authenticated, try starting the EIP
+ service
+ """
+
+ # In general we want to "filter" likely complicated error
+ # messages, but in this case, the messages make more sense as
+ # they come. Since they are "Unknown user" or "Unknown
+ # password"
+ self._login_widget.set_status(message, error=not ok)
+
+ if ok:
+ self._logged_user = self._login_widget.get_user()
+ self.ui.action_log_out.setEnabled(True)
+ # We leave a bit of room for the user to see the
+ # "Succeeded" message and then we switch to the EIP status
+ # panel
+ QtCore.QTimer.singleShot(1000, self._switch_to_status)
+ self._login_defer = None
+ else:
+ self._login_widget.set_enabled(True)
+
+ def _switch_to_status(self):
+ """
+ Changes the stackedWidget index to the EIP status one and
+ triggers the eip bootstrapping
+ """
+ if not self._already_started_eip:
+ self._status_panel.set_provider(
+ "%s@%s" % (self._login_widget.get_user(),
+ self._get_best_provider_config().get_domain()))
+
+ self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX)
+
+ self._soledad_bootstrapper.run_soledad_setup_checks(
+ self._provider_config,
+ self._login_widget.get_user(),
+ self._login_widget.get_password(),
+ download_if_needed=True,
+ standalone=self._standalone)
+
+ self._download_eip_config()
+
+ def _soledad_intermediate_stage(self, data):
+ """
+ SLOT
+ TRIGGERS:
+ self._soledad_bootstrapper.download_config
+
+ If there was a problem, displays it, otherwise it does nothing.
+ This is used for intermediate bootstrapping stages, in case
+ they fail.
+ """
+ passed = data[self._soledad_bootstrapper.PASSED_KEY]
+ if not passed:
+ # TODO: display in the GUI:
+ # should pass signal to a slot in status_panel
+ # that sets the global status
+ logger.warning("Soledad failed to start: %s" %
+ (data[self._soledad_bootstrapper.ERROR_KEY],))
+
+ def _soledad_bootstrapped_stage(self, data):
+ """
+ SLOT
+ TRIGGERS:
+ self._soledad_bootstrapper.gen_key
+
+ If there was a problem, displays it, otherwise it does nothing.
+ This is used for intermediate bootstrapping stages, in case
+ they fail.
+
+ :param data: result from the bootstrapping stage for Soledad
+ :type data: dict
+ """
+ passed = data[self._soledad_bootstrapper.PASSED_KEY]
+ if not passed:
+ logger.error(data[self._soledad_bootstrapper.ERROR_KEY])
+ return
+
+ logger.debug("Done bootstrapping Soledad")
+
+ self._soledad = self._soledad_bootstrapper.soledad
+ self._keymanager = self._soledad_bootstrapper.keymanager
+
+ # Ok, now soledad is ready, so we can allow other things that
+ # depend on soledad to start.
+
+ # this will trigger start_imap_service
+ self.soledad_ready.emit()
+
+ # TODO connect all these activations to the soledad_ready
+ # signal so the logic is clearer to follow.
+
+ if self._provider_config.provides_mx() and \
+ self._enabled_services.count(self.MX_SERVICE) > 0:
+ self._smtp_bootstrapper.run_smtp_setup_checks(
+ self._provider_config,
+ self._smtp_config,
+ True)
+ else:
+ if self._enabled_services.count(self.MX_SERVICE) > 0:
+ pass # TODO: show MX status
+ #self._status_panel.set_eip_status(
+ # self.tr("%s does not support MX") %
+ # (self._provider_config.get_domain(),),
+ # error=True)
+ else:
+ pass # TODO: show MX status
+ #self._status_panel.set_eip_status(
+ # self.tr("MX is disabled"))
+
+ # Service control methods: smtp
+
+ def _smtp_bootstrapped_stage(self, data):
+ """
+ SLOT
+ TRIGGERS:
+ self._smtp_bootstrapper.download_config
+
+ If there was a problem, displays it, otherwise it does nothing.
+ This is used for intermediate bootstrapping stages, in case
+ they fail.
+
+ :param data: result from the bootstrapping stage for Soledad
+ :type data: dict
+ """
+ passed = data[self._smtp_bootstrapper.PASSED_KEY]
+ if not passed:
+ logger.error(data[self._smtp_bootstrapper.ERROR_KEY])
+ return
+ logger.debug("Done bootstrapping SMTP")
+
+ hosts = self._smtp_config.get_hosts()
+ # TODO: handle more than one host and define how to choose
+ if len(hosts) > 0:
+ hostname = hosts.keys()[0]
+ logger.debug("Using hostname %s for SMTP" % (hostname,))
+ host = hosts[hostname][self.IP_KEY].encode("utf-8")
+ port = hosts[hostname][self.PORT_KEY]
+ # TODO: pick local smtp port in a better way
+ # TODO: Make the encrypted_only configurable
+
+ from leap.mail.smtp import setup_smtp_relay
+ client_cert = self._eip_config.get_client_cert_path(
+ self._provider_config)
+ setup_smtp_relay(port=2013,
+ keymanager=self._keymanager,
+ smtp_host=host,
+ smtp_port=port,
+ smtp_cert=client_cert,
+ smtp_key=client_cert,
+ encrypted_only=False)
+
+ def _start_imap_service(self):
+ """
+ SLOT
+ TRIGGERS:
+ soledad_ready
+ """
+ logger.debug('Starting imap service')
+
+ self._imap_service = imap.start_imap_service(
+ self._soledad,
+ self._keymanager)
+
+ def _get_socket_host(self):
+ """
+ Returns the socket and port to be used for VPN
+
+ :rtype: tuple (str, str) (host, port)
+ """
+
+ # TODO: make this properly multiplatform
+
+ if platform.system() == "Windows":
+ host = "localhost"
+ port = "9876"
+ else:
+ host = os.path.join(tempfile.mkdtemp(prefix="leap-tmp"),
+ 'openvpn.socket')
+ port = "unix"
+
+ return host, port
+
+ def _start_eip(self):
+ """
+ SLOT
+ TRIGGERS:
+ self._status_panel.start_eip
+ self._action_eip_startstop.triggered
+ or called from _finish_eip_bootstrap
+
+ Starts EIP
+ """
+ self._status_panel.eip_pre_up()
+ self.user_stopped_eip = False
+ provider_config = self._get_best_provider_config()
+
+ try:
+ host, port = self._get_socket_host()
+ self._vpn.start(eipconfig=self._eip_config,
+ providerconfig=provider_config,
+ socket_host=host,
+ socket_port=port)
+
+ self._settings.set_defaultprovider(
+ provider_config.get_domain())
+
+ provider = provider_config.get_domain()
+ if self._logged_user is not None:
+ provider = "%s@%s" % (self._logged_user, provider)
+
+ self._status_panel.set_provider(provider)
+
+ self._action_eip_provider.setText(provider_config.get_domain())
+
+ self._status_panel.eip_started()
+
+ # XXX refactor into status_panel method?
+ self._action_eip_startstop.setText(self.tr("Turn OFF"))
+ self._action_eip_startstop.disconnect(self)
+ self._action_eip_startstop.triggered.connect(
+ self._stop_eip)
+ except EIPNoPolkitAuthAgentAvailable:
+ self._status_panel.set_global_status(
+ # XXX this should change to polkit-kde where
+ # applicable.
+ self.tr("We could not find any "
+ "authentication "
+ "agent in your system.<br/>"
+ "Make sure you have "
+ "<b>polkit-gnome-authentication-"
+ "agent-1</b> "
+ "running and try again."),
+ error=True)
+ self._set_eipstatus_off()
+ except EIPNoTunKextLoaded:
+ self._status_panel.set_global_status(
+ self.tr("Encrypted Internet cannot be started because "
+ "the tuntap extension is not installed properly "
+ "in your system."))
+ self._set_eipstatus_off()
+ except EIPNoPkexecAvailable:
+ self._status_panel.set_global_status(
+ self.tr("We could not find <b>pkexec</b> "
+ "in your system."),
+ error=True)
+ self._set_eipstatus_off()
+ except OpenVPNNotFoundException:
+ self._status_panel.set_global_status(
+ self.tr("We could not find openvpn binary."),
+ error=True)
+ self._set_eipstatus_off()
+ except OpenVPNAlreadyRunning as e:
+ self._status_panel.set_global_status(
+ self.tr("Another openvpn instance is already running, and "
+ "could not be stopped."),
+ error=True)
+ self._set_eipstatus_off()
+ except AlienOpenVPNAlreadyRunning as e:
+ self._status_panel.set_global_status(
+ self.tr("Another openvpn instance is already running, and "
+ "could not be stopped because it was not launched by "
+ "Bitmask. Please stop it and try again."),
+ error=True)
+ self._set_eipstatus_off()
+ except VPNLauncherException as e:
+ # XXX We should implement again translatable exceptions so
+ # we can pass a translatable string to the panel (usermessage attr)
+ self._status_panel.set_global_status("%s" % (e,), error=True)
+ self._set_eipstatus_off()
+ else:
+ self._already_started_eip = True
+
+ def _set_eipstatus_off(self):
+ """
+ Sets eip status to off
+ """
+ self._status_panel.set_eip_status(self.tr("OFF"), error=True)
+ self._status_panel.set_eip_status_icon("error")
+ self._status_panel.set_startstop_enabled(True)
+ self._status_panel.eip_stopped()
+
+ self._set_action_eipstart_off()
+
+ def _set_action_eipstart_off(self):
+ """
+ Sets eip startstop action to OFF status.
+ """
+ self._action_eip_startstop.setText(self.tr("Turn ON"))
+ self._action_eip_startstop.disconnect(self)
+ self._action_eip_startstop.triggered.connect(
+ self._start_eip)
+
+ def _stop_eip(self, abnormal=False):
+ """
+ SLOT
+ TRIGGERS:
+ self._status_panel.stop_eip
+ self._action_eip_startstop.triggered
+ or called from _eip_finished
+
+ Stops vpn process and makes gui adjustments to reflect
+ the change of state.
+
+ :param abnormal: whether this was an abnormal termination.
+ :type abnormal: bool
+ """
+ if abnormal:
+ logger.warning("Abnormal EIP termination.")
+
+ self.user_stopped_eip = True
+ self._vpn.terminate()
+
+ self._set_eipstatus_off()
+
+ self._already_started_eip = False
+ self._settings.set_defaultprovider(None)
+ if self._logged_user:
+ self._status_panel.set_provider(
+ "%s@%s" % (self._logged_user,
+ self._get_best_provider_config().get_domain()))
+
+ def _get_best_provider_config(self):
+ """
+ Returns the best ProviderConfig to use at a moment. We may
+ have to use self._provider_config or
+ self._provisional_provider_config depending on the start
+ status.
+
+ :rtype: ProviderConfig
+ """
+ leap_assert(self._provider_config is not None or
+ self._provisional_provider_config is not None,
+ "We need a provider config")
+
+ provider_config = None
+ if self._provider_config.loaded():
+ provider_config = self._provider_config
+ elif self._provisional_provider_config.loaded():
+ provider_config = self._provisional_provider_config
+ else:
+ leap_assert(False, "We could not find any usable ProviderConfig.")
+
+ return provider_config
+
+ def _download_eip_config(self):
+ """
+ Starts the EIP bootstrapping sequence
+ """
+ leap_assert(self._eip_bootstrapper, "We need an eip bootstrapper!")
+
+ provider_config = self._get_best_provider_config()
+
+ if provider_config.provides_eip() and \
+ self._enabled_services.count(self.OPENVPN_SERVICE) > 0 and \
+ not self._already_started_eip:
+
+ self._status_panel.set_eip_status(
+ self.tr("Starting..."))
+ self._eip_bootstrapper.run_eip_setup_checks(
+ provider_config,
+ download_if_needed=True)
+ self._already_started_eip = True
+ elif not self._already_started_eip:
+ if self._enabled_services.count(self.OPENVPN_SERVICE) > 0:
+ self._status_panel.set_eip_status(
+ self.tr("Not supported"),
+ error=True)
+ else:
+ self._status_panel.set_eip_status(self.tr("Disabled"))
+ self._status_panel.set_startstop_enabled(False)
+
+ def _finish_eip_bootstrap(self, data):
+ """
+ SLOT
+ TRIGGER: self._eip_bootstrapper.download_client_certificate
+
+ Starts the VPN thread if the eip configuration is properly
+ loaded
+ """
+ leap_assert(self._eip_config, "We need an eip config!")
+ passed = data[self._eip_bootstrapper.PASSED_KEY]
+
+ if not passed:
+ error_msg = self.tr("There was a problem with the provider")
+ self._status_panel.set_eip_status(error_msg, error=True)
+ logger.error(data[self._eip_bootstrapper.ERROR_KEY])
+ self._already_started_eip = False
+ return
+
+ provider_config = self._get_best_provider_config()
+
+ domain = provider_config.get_domain()
+
+ loaded = self._eip_config.loaded()
+ if not loaded:
+ eip_config_path = os.path.join("leap", "providers",
+ domain, "eip-service.json")
+ api_version = provider_config.get_api_version()
+ self._eip_config.set_api_version(api_version)
+ loaded = self._eip_config.load(eip_config_path)
+
+ if loaded:
+ self._start_eip()
+ else:
+ self._status_panel.set_eip_status(
+ self.tr("Could not load Encrypted Internet "
+ "Configuration."),
+ error=True)
+
+ def _logout(self):
+ """
+ SLOT
+ TRIGGER: self.ui.action_log_out.triggered
+
+ Starts the logout sequence
+ """
+ # XXX: If other defers are doing authenticated stuff, this
+ # might conflict with those. CHECK!
+ threads.deferToThread(self._srp_auth.logout)
+
+ def _done_logging_out(self, ok, message):
+ """
+ SLOT
+ TRIGGER: self._srp_auth.logout_finished
+
+ Switches the stackedWidget back to the login stage after
+ logging out
+ """
+ self._logged_user = None
+ self.ui.action_log_out.setEnabled(False)
+ self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX)
+ self._login_widget.set_password("")
+ self._login_widget.set_enabled(True)
+ self._login_widget.set_status("")
+
+ def _intermediate_stage(self, data):
+ """
+ SLOT
+ TRIGGERS:
+ self._provider_bootstrapper.name_resolution
+ self._provider_bootstrapper.https_connection
+ self._provider_bootstrapper.download_ca_cert
+ self._eip_bootstrapper.download_config
+
+ If there was a problem, displays it, otherwise it does nothing.
+ This is used for intermediate bootstrapping stages, in case
+ they fail.
+ """
+ passed = data[self._provider_bootstrapper.PASSED_KEY]
+ if not passed:
+ self._login_widget.set_enabled(True)
+ self._login_widget.set_status(
+ self.tr("Unable to connect: Problem with provider"))
+ logger.error(data[self._provider_bootstrapper.ERROR_KEY])
+
+ def _eip_intermediate_stage(self, data):
+ """
+ SLOT
+ TRIGGERS:
+ self._eip_bootstrapper.download_config
+
+ If there was a problem, displays it, otherwise it does nothing.
+ This is used for intermediate bootstrapping stages, in case
+ they fail.
+ """
+ passed = data[self._provider_bootstrapper.PASSED_KEY]
+ if not passed:
+ self._login_widget.set_status(
+ self.tr("Unable to connect: Problem with provider"))
+ logger.error(data[self._provider_bootstrapper.ERROR_KEY])
+ self._already_started_eip = False
+
+ def _eip_finished(self, exitCode):
+ """
+ SLOT
+ TRIGGERS:
+ self._vpn.process_finished
+
+ Triggered when the EIP/VPN process finishes to set the UI
+ accordingly.
+ """
+ logger.info("VPN process finished with exitCode %s..."
+ % (exitCode,))
+
+ # Ideally we would have the right exit code here,
+ # but the use of different wrappers (pkexec, cocoasudo) swallows
+ # the openvpn exit code so we get zero exit in some cases where we
+ # shouldn't. As a workaround we just use a flag to indicate
+ # a purposeful switch off, and mark everything else as unexpected.
+
+ # In the near future we should trigger a native notification from here,
+ # since the user really really wants to know she is unprotected asap.
+ # And the right thing to do will be to fail-close.
+
+ # TODO we should have a way of parsing the latest lines in the vpn
+ # log buffer so we can have a more precise idea of which type
+ # of error did we have (server side, local problem, etc)
+ abnormal = True
+
+ # XXX check if these exitCodes are pkexec/cocoasudo specific
+ if exitCode in (126, 127):
+ self._status_panel.set_global_status(
+ self.tr("Encrypted Internet could not be launched "
+ "because you did not authenticate properly."),
+ error=True)
+ self._vpn.killit()
+ elif exitCode != 0 or not self.user_stopped_eip:
+ self._status_panel.set_global_status(
+ self.tr("Encrypted Internet finished in an "
+ "unexpected manner!"), error=True)
+ else:
+ abnormal = False
+ if exitCode == 0 and IS_MAC:
+ # XXX remove this warning after I fix cocoasudo.
+ logger.warning("The above exit code MIGHT BE WRONG.")
+ self._stop_eip(abnormal)
+
+ def _on_raise_window_event(self, req):
+ """
+ Callback for the raise window event
+ """
+ if IS_WIN:
+ raise_window_ack()
+ self.raise_window.emit()
+
+ def _do_raise_mainwindow(self):
+ """
+ SLOT
+ TRIGGERS:
+ self._on_raise_window_event
+
+ Triggered when we receive a RAISE_WINDOW event.
+ """
+ TOPFLAG = QtCore.Qt.WindowStaysOnTopHint
+ self.setWindowFlags(self.windowFlags() | TOPFLAG)
+ self.show()
+ self.setWindowFlags(self.windowFlags() & ~TOPFLAG)
+ self.show()
+ if IS_MAC:
+ self.raise_()
+
+ def _cleanup_pidfiles(self):
+ """
+ Removes lockfiles on a clean shutdown.
+
+ Triggered after aboutToQuit signal.
+ """
+ if IS_WIN:
+ WindowsLock.release_all_locks()
+
+ def _cleanup_and_quit(self):
+ """
+ Call all the cleanup actions in a serialized way.
+ Should be called from the quit function.
+ """
+ logger.debug('About to quit, doing cleanup...')
+
+ if self._imap_service is not None:
+ self._imap_service.stop()
+
+ if self._srp_auth is not None:
+ if self._srp_auth.get_session_id() is not None or \
+ self._srp_auth.get_token() is not None:
+ # XXX this can timeout after loong time: See #3368
+ self._srp_auth.logout()
+
+ if self._soledad:
+ logger.debug("Closing soledad...")
+ self._soledad.close()
+ else:
+ logger.error("No instance of soledad was found.")
+
+ logger.debug('Terminating vpn')
+ self._vpn.terminate(shutdown=True)
+
+ if self._login_defer:
+ logger.debug("Cancelling login defer.")
+ self._login_defer.cancel()
+
+ if self._download_provider_defer:
+ logger.debug("Cancelling download provider defer.")
+ self._download_provider_defer.cancel()
+
+ # TODO missing any more cancels?
+
+ logger.debug('Cleaning pidfiles')
+ self._cleanup_pidfiles()
+
+ def quit(self):
+ """
+ Cleanup and tidely close the main window before quitting.
+ """
+ # TODO: separate the shutting down of services from the
+ # UI stuff.
+ self._cleanup_and_quit()
+
+ self._really_quit = True
+
+ if self._wizard:
+ self._wizard.close()
+
+ if self._logger_window:
+ self._logger_window.close()
+
+ self.close()
+
+ if self._quit_callback:
+ self._quit_callback()
+
+ logger.debug('Bye.')
+
+
+if __name__ == "__main__":
+ import signal
+
+ def sigint_handler(*args, **kwargs):
+ logger.debug('SIGINT catched. shutting down...')
+ mainwindow = args[0]
+ mainwindow.quit()
+
+ import sys
+
+ logger = logging.getLogger(name='leap')
+ logger.setLevel(logging.DEBUG)
+ console = logging.StreamHandler()
+ console.setLevel(logging.DEBUG)
+ formatter = logging.Formatter(
+ '%(asctime)s '
+ '- %(name)s - %(levelname)s - %(message)s')
+ console.setFormatter(formatter)
+ logger.addHandler(console)
+
+ app = QtGui.QApplication(sys.argv)
+ mainwindow = MainWindow()
+ mainwindow.show()
+
+ timer = QtCore.QTimer()
+ timer.start(500)
+ timer.timeout.connect(lambda: None)
+
+ sigint = partial(sigint_handler, mainwindow)
+ signal.signal(signal.SIGINT, sigint)
+
+ sys.exit(app.exec_())
diff --git a/src/leap/gui/mainwindow_rc.py b/src/leap/gui/mainwindow_rc.py
deleted file mode 100644
index 9edb712a..00000000
--- a/src/leap/gui/mainwindow_rc.py
+++ /dev/null
@@ -1,2564 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Resource object code
-#
-# Created: Fri Feb 1 00:37:24 2013
-# by: The Resource Compiler for PyQt (Qt v4.8.2)
-#
-# WARNING! All changes made in this file will be lost!
-
-from PyQt4 import QtCore
-
-qt_resource_data = "\
-\x00\x00\x05\x95\
-\x89\
-\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\x40\x00\x00\x00\x40\x08\x03\x00\x00\x00\x9d\xb7\x81\xec\
-\x00\x00\x00\x03\x73\x42\x49\x54\x08\x08\x08\xdb\xe1\x4f\xe0\x00\
-\x00\x00\x09\x70\x48\x59\x73\x00\x00\x37\x5d\x00\x00\x37\x5d\x01\
-\x19\x80\x46\x5d\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\
-\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\
-\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x1f\x74\x45\x58\
-\x74\x54\x69\x74\x6c\x65\x00\x47\x6e\x6f\x6d\x65\x20\x53\x79\x6d\
-\x62\x6f\x6c\x69\x63\x20\x49\x63\x6f\x6e\x20\x54\x68\x65\x6d\x65\
-\x8e\xa4\x29\xab\x00\x00\x02\x13\x50\x4c\x54\x45\xff\xff\xff\xff\
-\x00\x00\xff\x00\x00\xaa\x00\x00\xbf\x00\x00\xbf\xbf\xbf\xd5\x00\
-\x00\xc6\x00\x00\xc4\x00\x00\xbb\xbb\xbb\xcc\x00\x00\xcf\x00\x00\
-\xcc\x00\x00\xce\x00\x00\xb9\xb9\xb9\xc2\xc2\xc2\xce\x00\x00\xca\
-\x00\x00\xcc\x00\x00\xcd\x00\x00\xcc\x00\x00\xc1\xc1\xc1\xce\x00\
-\x00\xca\x00\x00\xcb\x00\x00\xcd\x00\x00\xcb\x00\x00\xcd\x00\x00\
-\xce\x00\x00\xbe\xbe\xbe\xcc\x00\x00\xbf\xbf\xbf\xbe\xbe\xbe\xcd\
-\x00\x00\xcb\x00\x00\xcc\x00\x00\xcd\x00\x00\xcc\x00\x00\xcc\x00\
-\x00\xbe\xbe\xbe\xbf\xbf\xbf\xcb\x00\x00\xcb\x00\x00\xcb\x00\x00\
-\xcc\x00\x00\xcc\x00\x00\xbf\xbf\xbf\xcd\x00\x00\xcb\x00\x00\xcc\
-\x00\x00\xcc\x00\x00\xbf\xbf\xbf\xcc\x00\x00\xcc\x00\x00\xcc\x00\
-\x00\xbe\xbe\xbe\xcc\x00\x00\xbe\xbe\xbe\xcc\x00\x00\xcc\x00\x00\
-\xcc\x00\x00\xcc\x00\x00\xbe\xbe\xbe\xbe\xbe\xbe\xcc\x00\x00\xcc\
-\x00\x00\xcc\x00\x00\xcc\x00\x00\xbe\xb7\xb7\xbe\xb8\xb8\xbe\xba\
-\xba\xbe\xbc\xbc\xbe\xbd\xbd\xbe\xbe\xbe\xbf\xaa\xaa\xbf\xab\xab\
-\xbf\xac\xac\xbf\xad\xad\xbf\xae\xae\xbf\xb0\xb0\xbf\xb1\xb1\xbf\
-\xb4\xb4\xbf\xb6\xb6\xbf\xb7\xb7\xc0\x9c\x9c\xc0\x9d\x9d\xc0\xa1\
-\xa1\xc0\xa2\xa2\xc0\xa4\xa4\xc0\xa5\xa5\xc0\xa6\xa6\xc0\xa7\xa7\
-\xc0\xa8\xa8\xc1\x8d\x8d\xc1\x91\x91\xc1\x94\x94\xc1\x95\x95\xc1\
-\x96\x96\xc1\x99\x99\xc1\x9c\x9c\xc2\x82\x82\xc2\x87\x87\xc2\x88\
-\x88\xc2\x8d\x8d\xc2\x8e\x8e\xc3\x73\x73\xc3\x74\x74\xc3\x76\x76\
-\xc3\x79\x79\xc3\x7c\x7c\xc3\x7d\x7d\xc3\x7f\x7f\xc4\x67\x67\xc4\
-\x6c\x6c\xc4\x6d\x6d\xc4\x6e\x6e\xc4\x70\x70\xc5\x59\x59\xc5\x5d\
-\x5d\xc5\x5f\x5f\xc5\x62\x62\xc5\x63\x63\xc6\x4c\x4c\xc6\x4f\x4f\
-\xc6\x50\x50\xc6\x53\x53\xc6\x56\x56\xc6\x58\x58\xc7\x3e\x3e\xc7\
-\x41\x41\xc7\x43\x43\xc7\x45\x45\xc7\x46\x46\xc7\x47\x47\xc7\x4b\
-\x4b\xc8\x31\x31\xc8\x35\x35\xc8\x36\x36\xc8\x38\x38\xc8\x3a\x3a\
-\xc8\x3c\x3c\xc9\x22\x22\xc9\x25\x25\xc9\x26\x26\xc9\x27\x27\xc9\
-\x28\x28\xc9\x2a\x2a\xc9\x2d\x2d\xc9\x2e\x2e\xca\x16\x16\xca\x17\
-\x17\xca\x1a\x1a\xca\x1b\x1b\xca\x1c\x1c\xca\x1d\x1d\xca\x1e\x1e\
-\xca\x20\x20\xca\x21\x21\xcb\x07\x07\xcb\x09\x09\xcb\x0a\x0a\xcb\
-\x0c\x0c\xcb\x0d\x0d\xcb\x0e\x0e\xcb\x0f\x0f\xcb\x10\x10\xcb\x11\
-\x11\xcb\x12\x12\xcb\x13\x13\xcc\x00\x00\xcc\x01\x01\xcc\x02\x02\
-\xcc\x03\x03\xcc\x04\x04\xcc\x05\x05\xcc\x06\x06\xcc\x07\x07\xd4\
-\x0d\x79\xbb\x00\x00\x00\x44\x74\x52\x4e\x53\x00\x01\x02\x03\x04\
-\x04\x06\x09\x0d\x0f\x0f\x10\x14\x15\x16\x19\x1a\x1d\x1e\x24\x28\
-\x29\x2a\x30\x36\x3d\x40\x42\x43\x4b\x55\x58\x5e\x60\x63\x64\x65\
-\x6e\x73\x7d\x7f\x8a\x94\x99\x9a\xaa\xb2\xbb\xbc\xc3\xc9\xca\xd2\
-\xd5\xde\xe0\xe3\xe6\xe8\xed\xef\xf6\xf7\xfa\xfa\xfb\xfc\xfd\xef\
-\xfa\x14\xec\x00\x00\x02\x79\x49\x44\x41\x54\x58\xc3\xed\x96\xd7\
-\x5b\x13\x41\x14\x47\x47\x05\x15\x5b\x2c\xa0\x58\x62\xb0\x26\xb6\
-\xa0\x46\xc5\x28\x12\x6c\x49\xae\x0d\x62\x01\xc5\x82\x58\x10\x7b\
-\x2f\xd8\x1b\x8a\xbd\x00\x62\x41\x45\x14\xf5\x04\x51\xff\x44\x1f\
-\x76\x37\x1f\x09\xc9\x66\x37\x8f\xc8\xef\xed\xee\xf7\x9d\xb3\x33\
-\x73\x67\x66\x57\xa9\xfe\x98\x65\x94\xd3\xe3\xf5\xfb\xbd\x1e\xe7\
-\xa8\x8c\xf0\x6c\x77\x40\xf4\x04\xdc\xd9\xf6\xf9\x3c\x9f\xf4\x88\
-\x2f\xcf\x2e\x5f\x50\x22\x71\x29\x29\xb0\xf9\xfe\x04\x5e\xa4\xc4\
-\xd6\x18\xb2\x7d\xd2\x2b\x3e\x3b\xeb\xe0\x96\x24\x71\xdb\xe8\x5f\
-\x20\x99\x20\x60\xbd\x9b\x4e\x83\x29\x72\x39\x1c\xae\x22\xa3\x72\
-\x5a\x16\x78\x0c\x3e\x47\x29\xa5\x72\x0c\x83\xc7\xb2\xc0\xab\x13\
-\x2e\xad\x74\xe9\xa5\xd7\xb2\xc0\xaf\x13\x0e\xad\x74\xe8\xa5\xdf\
-\x12\x3c\x31\x57\x29\x63\xd2\xc6\xb3\x58\x3d\x60\xda\xa0\x74\x7c\
-\x6e\x71\xf1\x94\x94\x82\x21\x0b\x98\x9b\x86\x1f\xb9\x0c\x56\xcf\
-\x4c\x21\xd8\xb4\x18\x98\x65\x2e\x28\x04\xe0\x66\x38\x99\xe0\x60\
-\x0b\xc0\x9a\x7c\x53\xc1\x84\xe5\x00\x3c\xd9\xda\x7b\x1f\x1d\x6e\
-\x07\x60\xce\x40\xf3\x21\x8c\x7e\x01\xc0\xf3\x0d\x89\xfc\xd9\x2e\
-\x80\xee\xa3\x69\xbb\xb0\xed\x29\x00\x3f\x0f\xc4\xe1\xa1\x6b\x00\
-\x7c\x39\x22\x69\x05\x12\xbe\x05\xc0\xa7\x43\x3d\xf8\x48\x23\x00\
-\xad\x15\x62\x41\x20\x72\xac\x1b\x20\x7a\x26\xc6\x57\xb5\x01\xd0\
-\x54\x26\xd6\x04\x52\xd7\x01\xc0\x83\xa0\xc6\x57\x77\x02\x70\x2f\
-\x28\x56\x05\x52\xf9\x16\x80\x87\xe5\x22\x22\x27\x7f\x03\x44\xaf\
-\xc4\xb7\xd6\x5c\x20\x65\xcd\x00\xbc\xdf\x29\x72\x19\x80\x3f\xa7\
-\xc4\x96\x40\x82\x0d\x00\x7c\xdd\x7b\x17\x80\xce\x6a\xb1\x29\x10\
-\xa9\x8f\x02\x44\x01\x68\xab\x12\xfb\x02\x39\xfd\x17\x3d\x8d\x11\
-\xc9\x44\x20\x35\x5a\x33\xb8\x1a\x92\xcc\x04\x17\x7e\x69\x82\x57\
-\x9b\x33\x12\x84\x6e\x18\x33\xa0\x65\x7b\x06\x82\xc8\x63\x00\x3e\
-\x03\xd0\x5e\x6b\x5b\xb0\xe7\x19\x00\x2f\x37\x9e\xef\x02\xe8\x3a\
-\x67\x53\x50\xf3\x03\x80\xdb\xeb\x44\x6a\xb5\x7b\xe0\x7a\xc8\x8e\
-\x40\xef\xe0\xc5\xb0\x88\xc8\x8e\x37\x00\x3c\x8a\x58\x17\x68\x7b\
-\xa8\xfb\xb8\x3e\xea\x2d\xaf\x01\xf8\xb8\xdb\xa2\x20\x78\x1f\x80\
-\x8e\xba\xd8\xc2\xad\xbf\x03\xc0\xf7\xfd\x96\x04\x65\x4d\x00\xbc\
-\xab\xec\xd1\xfc\xf0\xa5\xd8\x89\x4a\x2b\xa8\x68\x05\xa0\xb9\x3c\
-\xfe\x4a\x3c\x61\x9c\xe9\x74\xfc\x64\x6d\xfb\x36\x04\x13\x2f\xd5\
-\x7d\xdf\xb4\x5b\x65\xb0\x39\x3f\xa3\x14\x20\x5a\x9f\xe4\xf7\x60\
-\xd7\x07\x00\x16\x8d\x30\x15\xcc\x03\x58\x35\x35\xf1\x9b\xa8\x94\
-\x52\x6a\xf8\x42\x80\x95\xe3\x4c\x05\x59\x85\xb0\x22\x5f\x25\x15\
-\xa8\xac\xf9\x50\x3a\x29\xcd\x1a\x0c\x5b\xba\x64\xac\x4a\x21\x50\
-\x6a\xf6\xda\xe9\x69\xbb\x30\x66\xa8\x4a\x2d\x50\xe3\xed\xfd\x2d\
-\x5a\x3e\x40\x7d\x44\x20\x36\xd3\x2f\xe8\x9b\x82\xff\x38\xff\x00\
-\xc1\x36\x30\x95\xf0\x66\xca\x60\x00\x00\x00\x00\x49\x45\x4e\x44\
-\xae\x42\x60\x82\
-\x00\x00\x04\xec\
-\x89\
-\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\x18\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0\x77\x3d\xf8\
-\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\
-\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x06\xec\x00\x00\x06\xec\
-\x01\x1e\x75\x38\x35\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\
-\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\
-\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x13\x74\x45\
-\x58\x74\x41\x75\x74\x68\x6f\x72\x00\x52\x6f\x64\x6e\x65\x79\x20\
-\x44\x61\x77\x65\x73\x0e\xd8\x7e\x1d\x00\x00\x04\x4a\x49\x44\x41\
-\x54\x48\x89\x8d\x96\x5d\x6c\x53\x65\x18\xc7\x7f\xef\x39\x6b\xbb\
-\x7e\x9c\x75\x65\xad\x2b\x9b\xfb\xd0\x31\xdd\x14\xb6\x8c\x19\x44\
-\x90\x44\x63\x82\x42\x88\x5e\x90\x98\xcc\x19\x15\x13\xd4\x18\x76\
-\x61\xd4\x18\xe3\x85\x57\xca\x05\xe1\xc2\x0c\xa3\xa8\x51\xd0\x4c\
-\x12\xe3\x85\x31\x80\x26\x6a\xe2\x85\x23\xb0\x38\xb6\xc1\x1c\xce\
-\xb1\x40\x59\xf6\xe5\xca\xda\xae\xed\xfa\x75\x7a\x5e\x2f\x4e\xd7\
-\x59\xd6\x32\xfe\xc9\x7b\xf3\x9e\xe7\xf9\xff\x9f\xe7\xff\x9e\xf3\
-\x9c\x57\x48\x29\x59\x0f\xbd\x7b\x85\x0d\x17\xed\x1e\xbb\xb2\x07\
-\x20\x94\x30\x7e\x22\xc6\x48\xcf\x59\x99\x5a\x2f\x57\x94\x12\xf8\
-\xec\x55\x61\x71\x65\x6d\x47\xfc\xbe\xda\x47\x9d\x5a\xa5\xbf\xda\
-\x69\xaf\xda\xe0\x28\x2f\x07\x58\x5c\x4e\x26\xe7\xe3\x89\x9b\xf1\
-\x68\x78\x6e\x6e\x61\xfa\x8f\x98\x9a\x7a\xfb\x95\xe3\x32\x73\xc7\
-\x02\x9f\x76\x89\x8e\xba\xda\xda\x2f\xb7\x37\xdf\xdf\xe6\x2a\x13\
-\x8a\x94\x06\x82\xc2\x38\x89\x40\x08\x85\x98\x2e\x8d\xf3\x13\xe3\
-\x97\xa6\xa6\xa7\x5f\x7e\xed\x94\x1c\x5a\x57\xa0\xef\xa0\xfd\x70\
-\x5b\xf3\x96\x03\xcd\xde\x8a\x6a\x61\x64\xd7\x73\xc0\x14\x53\x54\
-\x26\x82\x4b\xf3\x97\x26\x2e\x7f\xd5\xfd\x79\xe2\xdd\x92\x02\x27\
-\x5f\x2a\x7b\xe1\x89\xce\x1d\xc7\xbc\x76\x55\x13\xc5\x98\xac\x4e\
-\x10\x0a\xa4\xa2\x6b\x45\x80\x60\x22\x1b\xfd\x6d\xf0\xdc\xa1\x17\
-\x4f\xe8\x5f\xaf\x11\x38\xfa\x9c\xf0\x6e\xdb\xf4\xc0\xf9\x6d\xf5\
-\xfe\x26\x30\xf2\x89\xca\xc6\x76\xd4\x07\xf7\xa3\xd4\x74\x80\xd5\
-\x65\x6e\xa6\xe3\x64\x03\xfd\x64\x2f\x9e\x40\x46\x67\xff\x27\xa3\
-\x30\x70\x63\x6e\x72\xe0\xea\xd8\xf6\x37\xbf\x95\x41\x73\x27\x87\
-\x06\x8f\xa7\x6f\x6b\x7d\x4d\x01\x39\x80\x52\xff\x08\x4a\xe3\xae\
-\x55\xf2\x5c\x27\x6a\xf3\x6e\x2c\x7b\x8f\x9a\x5d\xe5\x61\xb0\xb5\
-\xbe\xa6\xa9\xc1\xe3\xe9\x5b\x95\x04\x7a\xbb\x44\x47\x5b\x53\xcb\
-\x4e\x15\xbd\x98\x31\xc8\x70\x00\xfd\xfc\xc7\x64\xce\xbc\x81\x7e\
-\xe1\x13\xc8\x75\x2d\xb4\x8d\x28\xb5\x0f\x15\xc4\xaa\xe8\xb4\x35\
-\xb5\xec\xec\xed\x12\x1d\x00\x65\x00\xee\x72\x65\x9f\x5f\x73\x38\
-\x05\x6b\x0f\x35\x3b\xf6\x03\xfa\xc0\xf1\x3c\x29\xb3\xc3\xa8\xf7\
-\x3e\x8e\xf0\xb5\x98\x22\xf6\x0d\x05\xf1\x02\xf0\x6b\x0e\xa7\xbb\
-\x5c\xd9\x07\x0c\x29\x00\x9a\xc3\xd5\x69\x55\xd5\xe2\xd5\x47\xe7\
-\x56\xc9\x01\xe1\xbe\x1b\xe1\xb9\x67\xf5\x79\x70\x7c\x4d\x8e\x55\
-\x55\xd1\x1c\xae\xce\xbc\x45\x15\x6e\x5f\x9d\x90\xc5\xed\x29\xa8\
-\xae\xa2\x06\xcb\x53\x47\xa0\xcc\x66\x76\x37\xfa\x3d\xd9\xa9\x81\
-\xb5\x71\x52\xa7\xc2\xed\xab\x83\x9c\x45\x76\xbb\x56\x25\xa5\xa4\
-\xe8\xab\xb9\x02\x9b\x86\x65\xf7\x87\x08\xcd\x6f\x92\x8f\x9f\x21\
-\xf5\xdd\xf3\xa0\xa7\x10\xe5\x6e\x44\x45\x2d\x38\x7d\x08\x21\x90\
-\xd2\xe4\xcc\x0b\x24\x12\xd1\x9b\x42\xbd\xab\x81\x6c\xba\x28\xb7\
-\x94\x06\x65\xcd\x4f\x22\x2a\x1b\x00\x30\xa6\xff\x24\xd5\xb7\x1f\
-\x74\x73\x14\xc9\x64\x04\x99\x8c\x80\xc5\x8e\xe2\xae\x03\xab\x93\
-\x44\x22\x7a\x33\x6f\xd1\x52\x64\x61\x0a\xb5\xbc\x28\xb1\xb1\x34\
-\x83\x91\xb3\xc1\x98\x1d\xc1\x98\x1d\x41\x3f\xd7\x9b\x27\x2f\x40\
-\x26\x81\x11\xfc\x07\x99\x8a\x99\x9c\x2b\x1d\x44\x97\x63\x83\xc9\
-\xe8\xfc\x33\x36\x23\x05\xaa\x05\xd2\xcb\xc8\x74\xcc\xfc\x88\x72\
-\x5d\xa5\x7f\x3c\x74\x3b\x03\x0b\x90\x52\xed\x44\x97\x63\x83\x79\
-\x81\x48\xd2\x38\x3d\x1b\xcf\xbc\x53\x1f\xb9\xe4\x44\x1a\x45\x93\
-\xac\xcf\x7e\x83\xda\xb8\xcb\x2c\xf4\xd7\xf7\xd1\x2f\x9e\x2c\xce\
-\x2e\x14\xe6\xd2\x65\xf1\x48\xd2\x38\x0d\x39\x8b\x7a\x4e\xc9\xa1\
-\xd1\xc0\xb5\xfe\xac\xb7\xb5\x64\x55\xc2\xe5\x47\x54\x36\x98\xe7\
-\x60\xd3\x4a\xc6\x65\xbd\xad\x8c\x06\xae\xf5\xf7\xe4\x26\x6b\x7e\
-\x54\x04\x42\xa1\xee\xe1\x90\x31\x29\x1c\xde\xd2\xbd\xaf\x03\xe1\
-\xf0\x32\x1c\x32\x26\x03\xa1\x50\x77\x7e\xef\xd6\x69\xfa\x58\x7b\
-\xe7\x31\x5f\x78\x54\x23\xb3\x5c\x90\xac\xf8\xdb\x10\x0e\xf3\xab\
-\x35\x82\x13\xc8\xa5\xe9\x42\x76\x8b\x83\x85\xca\xcd\xd1\xdf\x47\
-\x06\x8b\x4f\xd3\x15\xf4\x1d\xb4\x1f\xde\xd2\xd4\x7a\x60\x93\x1a\
-\xaa\x26\x74\xfd\xce\x4a\xf7\x34\x72\x35\xeb\x99\xbf\x3c\x79\xe5\
-\xf6\xff\x83\x15\x7c\xf0\xb4\xd8\xbe\xb9\xa9\xe6\x8b\x1d\x0d\xd5\
-\xad\xae\xd8\x94\x22\x13\x21\x90\xb7\xcc\x29\xa1\x22\xec\x1e\x62\
-\xae\x3a\xa3\xff\xfa\xfc\xdf\xe7\xc6\x66\x5e\x3f\xf2\x0b\xfd\x52\
-\x16\x8e\x84\x02\x01\x21\x84\x0a\x54\x01\x95\x9a\x1d\xdf\x7b\x7b\
-\xac\x6f\xdd\x57\xb7\xb1\x6d\x83\xbb\xd2\x53\xe3\x10\x2e\x9f\xcd\
-\xb0\x00\xfc\x9b\x54\xf4\x99\x84\x8c\x2d\x86\xc3\xe1\x2b\x81\xd9\
-\xbf\x0e\xff\x9c\xfe\x28\x9e\x22\x08\x84\x80\xb0\x94\x32\x5c\xb2\
-\x03\x21\x84\x13\xf0\x00\xee\xdc\xd2\x5c\x56\x3c\x5b\xeb\x69\x79\
-\xb8\x51\x74\x18\x12\xe5\xc2\x75\x39\x3c\x74\x83\xc9\x78\x86\x10\
-\x10\x03\x96\x80\x48\x6e\x2d\x4a\xb9\x7a\x01\x28\x79\xab\xc8\x89\
-\x59\x00\x2b\x60\xcb\x2d\x0b\xa0\x02\x3a\x90\x02\xd2\x40\x12\xc8\
-\x48\x79\xab\x87\x26\xfe\x03\x26\x93\xd5\x41\x51\x76\x98\xdb\x00\
-\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
-\x00\x00\x01\xaa\
-\x89\
-\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\x40\x00\x00\x00\x40\x08\x03\x00\x00\x00\x9d\xb7\x81\xec\
-\x00\x00\x00\x03\x73\x42\x49\x54\x08\x08\x08\xdb\xe1\x4f\xe0\x00\
-\x00\x00\x09\x70\x48\x59\x73\x00\x00\x37\x5d\x00\x00\x37\x5d\x01\
-\x19\x80\x46\x5d\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\
-\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\
-\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x1f\x74\x45\x58\
-\x74\x54\x69\x74\x6c\x65\x00\x47\x6e\x6f\x6d\x65\x20\x53\x79\x6d\
-\x62\x6f\x6c\x69\x63\x20\x49\x63\x6f\x6e\x20\x54\x68\x65\x6d\x65\
-\x8e\xa4\x29\xab\x00\x00\x00\x36\x50\x4c\x54\x45\xff\xff\xff\xbf\
-\xbf\xbf\xbb\xbb\xbb\xb9\xb9\xb9\xc2\xc2\xc2\xc1\xc1\xc1\xbe\xbe\
-\xbe\xbf\xbf\xbf\xbe\xbe\xbe\xbe\xbe\xbe\xbf\xbf\xbf\xbf\xbf\xbf\
-\xbf\xbf\xbf\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\
-\xbe\xbe\xf1\xb6\xe9\xa5\x00\x00\x00\x11\x74\x52\x4e\x53\x00\x04\
-\x0f\x16\x19\x29\x4b\x58\x5e\x7d\x7f\xb2\xca\xe0\xe6\xf7\xfa\x2a\
-\xb3\x5d\x53\x00\x00\x00\x9e\x49\x44\x41\x54\x58\xc3\xed\x95\xb9\
-\x12\x83\x30\x0c\x05\x31\x18\x1b\x1f\x18\xeb\xff\x7f\x36\xc9\x20\
-\xcd\x24\xe1\x92\xe8\x00\x6d\xf7\x8a\x5d\xc0\x2e\x68\x1a\x65\x8f\
-\xce\xc5\x54\x4a\x8a\xae\x3b\xa5\x9b\x50\x01\xa9\xc1\xc8\xfd\x3e\
-\xc3\x17\xb9\x97\xfa\xc3\x04\x3f\x4c\x83\xf0\xf9\x7f\xfe\xbb\x20\
-\x7a\x07\x93\x61\x41\x96\x9c\x43\x80\x15\x82\xe0\xfe\xea\x5a\xa0\
-\xf2\x6f\xd3\x91\x33\x7a\x6b\xfd\x48\xcb\xb1\x03\x91\xfc\xf6\xb3\
-\x5a\x2a\x44\x76\x20\xa1\xe1\xe7\xe9\x71\x26\x76\xa0\xa0\x61\xe7\
-\x69\x71\x16\x76\x80\x3e\x7a\x6b\xdf\x3d\x00\x07\x68\x40\x03\x1a\
-\xd0\x80\x06\x9e\x15\xd8\xfb\xc1\x88\xd1\xc0\xe5\x02\x20\x44\x03\
-\xf7\x0c\x3c\x98\x17\xb4\xcd\x62\x13\x3b\x4c\x60\xe6\x00\x00\x00\
-\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
-\x00\x00\x59\x23\
-\x89\
-\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\xce\x00\x00\x01\x39\x08\x06\x00\x00\x00\xd8\xff\x1f\xd1\
-\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\
-\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x06\xf5\x00\x00\
-\x06\xf5\x01\xa1\xac\xc2\x7e\x00\x00\x00\x07\x74\x49\x4d\x45\x07\
-\xdd\x01\x1f\x0f\x1b\x31\xa4\xda\x47\xb5\x00\x00\x20\x00\x49\x44\
-\x41\x54\x78\xda\xec\xbd\x79\x70\x1c\x59\x7e\xdf\xf9\x79\x99\x59\
-\x77\x15\xaa\x50\x85\xfb\x20\xc1\xfb\xbe\xc9\x26\xd9\x3c\x7b\xa6\
-\x7b\x0e\x5b\x33\x23\x69\x64\xcb\xa3\xf1\x58\xd2\xb8\x24\x87\x24\
-\x87\x7c\xc9\xb2\xd6\xb6\x1c\xeb\xb5\x65\xad\xb5\x11\xbb\x96\xe5\
-\x90\x65\xbb\x64\x59\xd2\x48\x11\x3b\x92\xac\xd5\x39\x1a\x1d\x1e\
-\x92\x7d\x91\xcd\xa3\x9b\xf7\x09\x10\xc4\x7d\x15\x50\xf7\x99\x99\
-\x6f\xff\x28\x90\x44\x01\x05\xa0\x00\x14\x48\x76\x23\xbf\x11\x19\
-\x40\xe5\xf1\x32\xf3\x65\x7e\xf3\x77\xbc\xdf\xef\xf7\x84\x94\x12\
-\x0b\x16\x2c\x2c\x0d\x8a\xd5\x05\x16\x2c\x58\xc4\xb1\x60\xc1\x22\
-\x8e\x05\x0b\x16\x71\x2c\x58\xf8\x04\x41\xb3\xba\xc0\x82\x85\xea\
-\x11\x0e\x87\x4f\x01\x7b\x2d\xe2\x58\xb0\x30\x3f\x49\xea\x22\x91\
-\x48\x22\x1c\x0e\x2b\xc0\xdf\x00\x7e\x1a\x38\x08\x20\x2c\x77\xb4\
-\x05\x0b\x65\x64\x11\xc0\x5b\xc0\x3f\x00\x3e\x0f\x8c\x01\x19\x60\
-\xc3\xcc\xfd\x2c\xe2\x58\xb0\xf0\x9c\x34\x8d\xc0\xb7\x81\x03\x8b\
-\xed\x6b\x11\xc7\x82\x85\x12\x69\x7c\xc0\x77\x80\x43\x96\x73\xc0\
-\x82\x85\xea\x48\xe3\x00\xfe\xa0\x1a\xd2\xd4\xdb\x6c\x74\x79\x3c\
-\x16\x71\x2c\x7c\x22\x89\xe0\x01\x7e\x08\xb8\x05\xdc\x8c\x44\x22\
-\x93\x8b\xd8\x34\xbf\x01\xbc\x31\xdf\x3e\x87\xeb\xeb\x39\x18\x08\
-\xb0\xde\xed\xc6\xa7\x69\x96\xaa\x66\xe1\x13\x6d\xe0\xa7\x00\xf7\
-\xf4\xaa\x21\xe0\xe6\xf4\x72\x1d\xb8\x01\xdc\x05\x8e\x00\xff\x1a\
-\x78\xb3\x52\x3b\x5d\x6e\x37\x5f\x6e\x6f\x67\xbd\xdb\x6d\xd9\x38\
-\x16\xd6\x0c\x79\xae\x2d\x62\xe4\x1b\x80\x5a\x69\x43\xc0\x66\xe3\
-\x8b\xad\xad\x1c\xaa\xaf\x47\x58\x36\x8e\x85\x35\x86\x7b\x8b\x10\
-\x67\x0e\x69\x6c\x42\xf0\xe9\xa6\x26\xde\x6c\x6a\xc2\xae\x28\x96\
-\x73\xc0\xc2\x9a\x44\xcf\x52\x76\x3e\x14\x08\xf0\xc5\xd6\x56\xea\
-\xed\xf6\xaa\xf6\xb7\x88\x63\xe1\x93\x66\xdb\x7c\x1a\xf8\x71\xe0\
-\x8b\xd5\x1c\xb3\xde\xe5\xe2\x7b\xdb\xdb\xd9\xe0\xf1\x2c\xe9\x5c\
-\x16\x71\x2c\x7c\x92\xb0\x0d\xf8\x8b\x85\x76\x10\x40\x97\xc7\xc3\
-\x0e\x9f\x8f\x1d\x3e\x1f\xeb\xdc\xee\x79\xed\x18\x8b\x38\x16\xd6\
-\x0a\xfa\xe6\x33\xf6\xb7\xfb\x7c\xec\xf4\xf9\xd8\xea\xf3\xe1\x56\
-\xd5\x15\x9f\xc8\xf2\xaa\x59\xf8\xa4\xa9\x6b\xa3\x40\xd3\xd3\xdf\
-\xcd\xf5\x82\xbf\xb3\xb9\x9d\xce\x42\xa8\xa6\xe7\xb1\xf2\x71\x2c\
-\x7c\xd2\xf0\x64\xe6\x8f\xbc\x2e\xf1\x1d\x1c\x60\xd0\x16\xb5\x88\
-\x63\xc1\xc2\x02\xe8\x9f\xf9\x23\x9b\x07\xbb\x03\x5c\x87\x06\x18\
-\xb2\x4d\x59\xc4\xb1\x60\x61\x1e\x7c\x87\xd2\xe0\x66\x49\xe2\x14\
-\x40\x9a\xe0\x74\x80\xf3\x60\x1f\x23\x5a\xcc\x22\x8e\x05\x0b\xb3\
-\x11\x89\x44\xfe\x13\xf0\x8d\x32\xa9\x53\x28\xfd\x75\x3a\xc1\x76\
-\xe8\x09\xa3\x5a\xdc\x22\x8e\x05\x0b\xb3\x9c\x03\x3e\xe0\xcb\x33\
-\xd7\x65\x72\xcf\xff\x77\x39\x41\x3d\xd8\xcb\xa4\x2d\x91\xb3\x88\
-\x63\xc1\xc2\x73\x6c\x01\xbc\x65\x12\x67\x16\x45\xdc\x2e\x30\xf6\
-\x3f\x76\xe6\x9c\xe9\x51\x8b\x38\x16\x2c\x94\x54\xb5\x6b\xc0\x83\
-\xd9\x0e\x82\xd9\xf0\xb8\x21\xb5\xe7\x51\xc8\x74\xe5\x9e\x58\xc4\
-\xb1\x60\xa1\x84\x5f\x9f\xf9\x23\x97\xaf\xbc\x93\xdb\x8d\x96\x3b\
-\x7c\xdf\x61\xba\xf2\xfd\x16\x71\x2c\x58\x80\xdf\x04\x64\x25\x1b\
-\xa7\x02\x03\x5a\x72\x87\xef\x29\xa6\x33\x3f\x68\x11\xc7\xc2\x5a\
-\xc7\x06\xc0\x9c\xcf\xc6\xa9\xc0\x82\xf6\xdc\x91\x7b\xa6\xe9\x28\
-\x0c\x5b\xc4\xb1\xb0\x96\xf1\x73\xcc\xc8\xb7\xc9\xe4\xab\x62\x42\
-\x67\xee\xb5\x7b\x05\xd3\x51\x18\xb5\x88\x63\x61\xcd\x21\x1c\x0e\
-\x9f\x05\x4e\x2e\xe6\x1c\xa8\xcc\x06\xb9\x3e\xf7\xda\xbd\x8c\x69\
-\x2f\x8c\x59\xc4\xb1\xb0\xd6\xf0\x13\xb3\x57\x54\x4d\x9c\x12\x79\
-\x36\xe4\x5f\xbb\x97\x94\x36\x3d\x6a\x11\xc7\xc2\x5a\xc2\xae\xd9\
-\x2b\x32\x4b\x1c\xea\x94\xaa\xdc\x94\x3b\x7a\x67\x52\x6a\xfa\xa4\
-\x45\x1c\x0b\x6b\x41\x4d\x53\x81\x4d\x2b\x92\x38\xcf\xc9\xb3\x25\
-\x77\xec\xee\xb8\xb4\x19\x31\x8b\x38\x16\x3e\xe9\xd8\x00\xcc\x29\
-\x1a\x90\x5d\x66\x70\x8d\x54\xcd\x6d\xb9\xa3\x77\x86\xa5\x66\xc4\
-\x2d\xe2\x58\xf8\x24\x63\x53\xa5\x95\xcb\x91\x38\x33\xc8\xb3\x23\
-\x7b\xf4\xee\x80\xd4\x8c\xa4\x45\x1c\x0b\x9f\x54\x14\x2b\xad\x2c\
-\x14\xc1\x34\x56\xd0\xaa\x66\xec\xca\x1d\xbd\xdb\x2b\x15\x23\x6d\
-\x11\xc7\xc2\x27\x11\xc9\xf9\x36\x64\xf2\x2b\x6b\x58\x6a\xc6\x9e\
-\xdc\xb1\xbb\x8f\x9e\x92\xc7\x22\x8e\x85\x35\x41\x9c\x6c\x7e\xe5\
-\x8d\x4b\x9b\xb1\x2f\x77\xf4\xee\x43\xa9\x98\x59\x8b\x38\xaf\x20\
-\xc2\xe1\xb0\xd3\xea\x85\xda\x12\x27\x97\xab\xcd\x09\xa4\xdd\xd8\
-\x9f\x3b\x7a\xf7\xae\x55\x1e\xea\xe5\x93\xa4\x0b\xf8\x05\x60\x2b\
-\x10\x02\x1a\x00\x67\x38\x1c\xbe\x01\xfc\x3f\xc0\x6f\x47\x22\x91\
-\x82\xd5\x53\x2b\x54\xd5\x72\xb5\x3b\x89\xb4\xeb\x07\xad\xf2\x50\
-\x2f\x8f\x30\x76\xe0\xa7\x80\x7f\xc1\xf3\xaa\xfa\x95\x30\x0c\xfc\
-\x22\xf0\x8b\x91\x48\x24\x67\xf5\xdc\x82\x7d\xaa\x30\xa3\xde\xc0\
-\x4c\xfc\xad\xcf\xc0\xfe\xed\xb5\x3b\x97\xa5\xaa\xbd\x9c\x07\x7c\
-\x94\xd2\x54\x13\x3f\xb7\x08\x69\x00\x5a\x81\xff\x13\xb8\x13\x0e\
-\x87\xbf\x68\xf5\xde\xfc\x88\x44\x22\x26\xa5\xf9\x3a\x57\xc5\xc6\
-\xb1\x88\xf3\x72\x49\xb3\x1b\xf8\x33\x4a\xe5\x5a\x97\x82\x0d\xc0\
-\x1f\x84\xc3\xe1\x3f\x0e\x87\xc3\x1b\xad\x9e\x5c\x9a\xba\x96\xb1\
-\x88\xf3\xb1\x26\xcd\xba\x69\xd2\x04\x2a\xee\xb0\xfd\x53\xac\xfb\
-\xea\x4f\xe1\xd8\x72\x04\x54\xdb\x7c\xcd\xfc\x75\xe0\x76\x38\x1c\
-\xfe\x57\xe1\x70\xd8\x6d\xf5\x6a\x75\xc4\xc9\xe6\x2c\xe2\x7c\x5c\
-\x49\x13\x9a\x26\x4d\xfb\x9c\x8d\x76\x37\x9c\xfa\x7b\xd8\xd6\xef\
-\xa3\xae\x63\x1d\x9b\xbf\xfb\x07\xf0\x9e\xfe\x0a\x6c\x3a\x0e\x5a\
-\xc5\x69\x27\x9c\x94\x66\x12\xeb\x0d\x87\xc3\x3f\x33\x5d\xd9\xc5\
-\x42\x09\x31\x4b\x55\xfb\xe4\x90\xc6\x3b\x4d\x9a\x1d\x73\x36\x06\
-\xd7\xc3\x67\x7e\x1a\xda\x76\x11\xaa\x2f\x49\x19\xa1\xa8\x74\xbd\
-\x76\x98\xd6\xc3\xc7\x11\x27\x7e\x08\x36\x9f\x04\xcd\x51\xa9\xe9\
-\x46\xe0\xe7\xa7\x09\xf4\x7f\x85\xc3\xe1\x3d\x56\x6f\x53\xb1\x68\
-\x5a\xa6\xc6\x12\xc7\x72\x47\xaf\x3e\x69\x6c\xc0\xef\x01\x87\xe7\
-\x6c\xdc\x72\x06\xf6\x7f\x09\x14\x0d\x91\x99\xa4\x61\x67\xb9\x30\
-\x0a\x75\x75\x51\xd7\x92\xa5\x87\x22\xc5\x8d\x47\xe1\xf1\x07\xa5\
-\x45\x9f\xf3\xf9\x0c\x52\xf2\xd0\xfd\x54\x38\x1c\xbe\x4e\x29\xe7\
-\xfe\xb7\x23\x91\xc8\xf0\x1a\xec\xf2\x17\x22\x71\x2c\xe2\xac\x2e\
-\x69\x04\xf0\x6b\xc0\x67\xca\x7b\xdd\x09\x47\x7f\x00\x3a\xf6\x3f\
-\x5b\xe5\x31\xa7\x4c\x08\xce\xd1\x00\x6c\x4e\x17\xdb\x8e\x1d\x60\
-\xe8\xf6\x1d\x26\x37\x1e\x85\x0d\xaf\x41\xef\x15\x78\x7c\x09\x8a\
-\x15\x3f\xa3\xfb\xa6\x97\x7f\x1f\x0e\x87\xcf\x03\x7f\x08\xfc\x71\
-\x24\x12\xe9\x5e\xcb\x12\xc7\x22\xce\xc7\x0b\xbf\x00\x7c\xb5\x6c\
-\x4d\xa0\x1d\x4e\x7c\x1d\xbc\x8d\xcf\xd7\x99\x06\xed\xdb\xd6\xa7\
-\x80\xba\xf9\x1a\x6a\xdb\xb5\x93\xc0\x54\x94\xde\x07\xc3\x98\x5b\
-\x4e\xc2\x86\x23\x25\x02\xf5\x5c\x82\x62\xb6\xd2\x21\x2a\xf0\xa9\
-\xe9\xe5\x3f\x84\xc3\xe1\xbb\xc0\x1f\x4d\x2f\xef\x47\x22\x11\xe3\
-\x13\xda\xe7\x31\x4b\x55\xfb\x78\x3b\x02\xfe\x33\xf0\x37\xca\x36\
-\x6c\x3c\x0e\x07\xbf\x6f\x8e\xc7\xcc\x66\xa6\x7a\x6c\x36\xff\xa2\
-\x2e\x66\x77\x7d\x88\x1d\xaf\xd5\xf3\xe4\xda\x0d\x52\xb6\x66\xd8\
-\x7c\x02\xba\x0e\x43\xf4\x4f\x60\xea\x21\x8c\xe8\x90\x9e\xf7\xf0\
-\x1d\xd3\xcb\x4f\x03\x93\xe1\x70\xf8\x12\x70\xed\xe9\x12\x89\x44\
-\x7a\x2d\x89\x63\x11\xe7\x65\x90\xa5\x0e\x78\x1d\x38\x06\xfc\x3d\
-\xa0\xe5\xf9\xb7\xdf\x06\x87\xbf\x1f\xba\x5e\xab\x78\x6c\x4b\xab\
-\xbf\xea\x12\xfa\x42\x28\x74\x1d\xda\xcf\x54\x7f\x3f\x43\x23\x59\
-\xa4\xcb\x0f\xf5\x9f\x87\x5d\x21\x48\xf5\x41\x66\x10\xc6\x0c\x18\
-\x01\x46\x01\xbd\x62\x33\x41\xe0\xf3\xd3\xcb\xd3\xeb\x9f\x9c\x26\
-\xd1\x87\xc0\x55\xe0\xdd\x48\x24\x32\xf0\x31\x7c\x14\x15\x5d\xf4\
-\xba\x0e\xba\x01\x9a\x5a\x9b\x93\x58\x21\x37\xb5\x21\xcd\xf7\x01\
-\xff\x65\xfa\x85\x2c\x47\x5d\x33\xbc\xfe\x75\xf0\xb7\xce\x77\x78\
-\xff\xee\x2e\x3a\x60\xe9\x53\x51\xea\xf9\x1c\x8f\x3f\xbc\x4b\xde\
-\xdb\x06\xf9\x38\xec\xb8\x01\xf6\x22\x3c\xee\x07\x6d\x08\x30\x21\
-\x4a\x89\x44\x23\xc0\x24\x33\xca\xf4\x55\x85\x7e\xe0\xdd\x19\xcb\
-\x8d\x57\x59\xc5\x0b\x87\xc3\xa7\x28\x4d\xf3\x51\x91\x1e\xff\xf2\
-\xef\x82\xd7\x63\x11\xe7\x55\x31\xfe\x23\xc0\xd7\x2b\xee\xb0\xfe\
-\x10\x1c\xfe\x5b\xf3\xb9\x92\x4b\x26\x8f\x97\x73\x1d\x0d\x9c\x5d\
-\xc9\x75\x8c\xdc\xbb\xc7\x44\xd6\x05\xc5\x24\x1c\xbc\x0b\x2e\x20\
-\x55\x80\x27\xfd\xa0\x0d\x83\x32\xfd\x8c\x8b\xd3\x52\xe8\xa9\x34\
-\x4a\x2d\xf9\x54\x29\xe0\x12\xf0\x6d\xe0\x37\x22\x91\xc8\xe8\x2b\
-\xf4\x2c\x42\xc0\x47\x40\xc7\x7c\xfb\xfc\xe3\xaf\x42\x53\xc8\x22\
-\xce\xab\xf0\xb0\x7e\x1a\xf8\xf7\x73\x36\x28\x1a\x1c\xfc\x32\x6c\
-\x3a\xb1\xa8\xd0\xd8\xd6\xc1\xa4\x4d\x7b\x3e\x67\xe5\x72\x91\x8d\
-\xc7\x65\xef\xdd\x3e\x61\x88\x02\x1c\x7b\x0c\xda\xf4\x73\x4d\xe4\
-\xa1\xbf\x0f\xec\xa3\x73\xc5\x4d\x7a\x86\x34\x1a\x65\x9e\xfc\xc9\
-\xf9\xaf\x1d\xf8\x63\xe0\x57\x81\x6f\x55\x2b\x89\xa6\xdd\xf3\xeb\
-\xa6\x97\xf5\x33\xfe\x6f\x03\x26\x28\x4d\x80\xfb\x74\xe9\x07\x7a\
-\x22\x91\x48\xb6\x8a\x0f\xd8\x1f\x03\x7f\x6d\xa1\xfd\x7e\xec\xfb\
-\x60\x7d\x9b\x45\x9c\x97\x4d\x9a\xa3\xc0\x3b\x73\xec\x44\x4f\x08\
-\x4e\xfe\x5d\x08\x74\x2c\xda\x86\x4d\xe3\xd2\xb6\x0e\x8e\xd6\xf0\
-\xb2\x64\xdf\xad\x7b\xb1\x44\x3a\x1a\xe0\xc4\xb0\x40\xcc\x78\xb6\
-\xb1\x1c\x0c\x3c\x01\xc7\x3c\xb5\xf6\xe4\xb4\x2a\xf7\x94\x48\xd1\
-\x25\xa9\x75\x43\xc0\xff\x00\xfe\x7b\x25\xb7\x77\x38\x1c\x6e\xa6\
-\x14\x2a\xf4\x5d\xc0\x5b\xcc\x9a\x86\x63\x11\xe4\x80\xf3\xc0\xb7\
-\xa6\x09\xfa\xa0\x42\xfb\x3f\x43\x69\x20\x78\x41\xfc\xd0\x17\x60\
-\xfb\x06\x8b\x38\x2f\x9b\x38\xb7\x98\x5d\xc3\xab\x63\x2f\xbc\xf6\
-\x55\xb0\xb9\xaa\x6a\xa3\xa3\x91\x2b\x01\x4f\x85\x81\xd1\x15\x22\
-\x9d\x2e\x3c\x1a\x18\xbb\x91\x2d\x6e\x7d\x3c\x37\x92\x60\x32\x03\
-\x43\x4f\xc0\x31\xb1\xb8\x3c\x19\x9d\x41\xa4\xea\xd4\x3a\x09\xfc\
-\x09\xa5\x70\xa0\x01\xe0\x87\x81\xef\x06\x8e\x2c\xc7\x86\x5b\x40\
-\x5d\xec\x05\x7a\xa6\x25\x54\x10\xf8\x52\x35\xed\x7f\xff\x5b\x70\
-\x60\x87\x45\x9c\x97\x49\x9a\x63\xc0\xfb\xe5\x7e\xaa\xf5\xf0\xd6\
-\x3f\x62\x09\x51\x4c\x83\xbb\xba\x68\x15\x55\x1e\x20\x04\xba\x82\
-\xcc\x0b\x21\x0b\x42\x50\x50\x04\x45\x45\xc8\xa2\x22\x28\x0a\x21\
-\x75\x55\xa0\x0b\x81\xa1\x0a\x53\x17\x02\x53\x48\x99\xbf\x93\xbf\
-\x55\x1c\x55\x7a\x3e\x5d\xb1\xc1\xf1\x14\x8c\x3e\x01\xc7\x64\x95\
-\x6c\x5c\xb2\x5a\xa7\xf3\x8a\x79\x6d\xdf\x3c\xee\xe5\xcd\x23\xa9\
-\x9a\xb4\x65\xb9\xa3\x97\x87\x1f\x9e\xfd\x56\x73\xe4\xfb\x17\x25\
-\x8d\x0a\x71\xaf\x41\x77\x9d\x49\xc2\xab\x48\x63\xbb\x3f\xfb\xc8\
-\xe5\x32\xea\x00\x4d\x20\x35\x21\xd0\x04\xd8\x00\x1b\x02\x9b\x28\
-\xd5\x08\xb3\x03\x8e\xe9\x67\xa5\x01\x55\xf9\x85\xee\x4d\x66\xce\
-\xdb\x0b\x0d\x67\x7d\x5a\xe6\x83\xa4\x36\x32\xd7\x0f\xde\xe8\x85\
-\xc6\x5d\x30\x9e\x2a\x30\xfa\xd8\x8e\x63\x11\x8f\xb8\x87\x52\xf1\
-\xa5\x4d\xb3\xd4\xba\xd1\xe9\xef\xbe\x7c\x75\xde\x2d\x4d\xb3\x61\
-\xb3\x39\xb0\xdb\x1d\xd8\x6c\x76\x6c\x36\x07\x9e\x40\x3b\xc9\xba\
-\xe3\xbc\x73\xff\x8f\x78\x7d\xcb\x7d\x94\x15\x46\x69\x5a\x12\x67\
-\xe9\xd2\xc6\x45\x29\x2b\xd3\xff\x6c\xe5\xb6\x37\x60\xff\xf7\x94\
-\xfb\x07\xa4\x99\xf3\x18\x66\x4f\x1d\xda\xb8\xc7\xc0\xe6\x90\xb4\
-\x4f\x1b\xc1\xcf\x55\x0a\x41\xb4\x6b\x47\xb6\x27\xd4\x52\x3c\x52\
-\x33\x23\x07\xcc\xab\xe3\xc9\x77\xe2\x39\xfd\xf4\xf4\xaa\xc2\x84\
-\xfd\xe1\xfd\xac\x32\xb5\x70\x00\x68\x36\xf7\x88\xa1\x87\x1e\xcc\
-\x58\xeb\x92\x4f\xfa\x54\xad\x7b\xaa\xda\x25\x97\x77\xed\xaa\xaa\
-\xe1\x74\xba\xb0\xd9\x1c\xd3\x8b\xbd\xec\xff\x12\x11\x66\xae\xb7\
-\xcf\x20\x88\x63\xfa\x7f\xdb\x1c\xad\xcd\x34\x0d\x9e\x44\x0d\x72\
-\x46\x89\x2d\x3e\xf3\x1a\xa7\x36\xfe\x2f\x5c\x4e\x8b\x38\x2f\x92\
-\x38\x3f\x00\xfc\xd6\xb3\x15\xee\x7a\xc4\x67\x7e\x1a\x57\x36\x8e\
-\x2b\x9b\xc1\x8d\x42\x5d\x5d\xc7\xa0\xa2\xda\x5b\xab\xd5\xdb\xbc\
-\x01\xe3\xfc\xd6\xfd\xe9\x63\x42\xc1\xb1\x92\x6b\x33\xa4\xcc\x5c\
-\x1c\x49\xde\xca\xe9\xc6\x6c\x09\x93\x18\x71\xdc\x9e\x28\x8a\xf4\
-\xe2\x09\x70\x7a\xe1\x0a\x23\xf7\x3c\x64\xe3\xcb\xb7\x06\xd2\x94\
-\xbb\xbd\x2b\x55\x4c\x10\xe0\xac\xd3\x70\xd6\x6b\x38\x03\xa5\xbf\
-\x9e\xa0\x83\x86\xa6\x56\x3a\xb3\x9f\x46\x31\x6d\x35\x7b\x66\x4f\
-\xc6\xd3\x64\x8c\xf2\xae\x75\xc8\x61\xde\xdc\xf4\xcd\x21\xa7\xbd\
-\xd8\x66\x11\xe7\xc5\x10\xe7\xcf\xa7\x3d\x43\xa5\x07\x10\xe8\x64\
-\xf3\x89\xbf\x8f\xa2\xb9\x56\xd4\xae\xa2\xc8\x07\xdb\x0e\xa5\x35\
-\xb7\xcf\x5c\x56\x76\x67\xc1\x34\x27\xde\x1b\x4e\x8c\x19\xa6\xdc\
-\x59\x51\x12\x09\x86\x47\xec\xd7\x15\x5d\xe4\x9b\xab\x6a\xb0\x98\
-\xbb\xc4\xf0\xdd\x10\xf9\xd4\xe6\x95\xf6\x99\x9a\x16\xb8\xf2\x36\
-\x7c\xaa\x9d\x80\xc7\x89\x33\x60\xc3\xe9\x57\x11\x4a\x65\x7b\x5e\
-\xcd\xd6\xb1\x3e\xf5\x39\x14\xb9\x72\xf2\x44\x53\xe6\xe0\x58\xca\
-\x6c\xaf\xa8\xd2\x89\x82\xfe\xa9\xae\x6f\xdc\xf2\x39\x26\xf7\x5b\
-\xc4\x59\x7d\xe2\x7c\x9b\x59\xd1\xce\xde\xc6\x6d\x74\x1d\xfb\x51\
-\x84\xb2\xe2\x07\x9d\x6d\x59\x9f\xbf\xd2\xbe\x29\x7f\x6a\x29\x07\
-\x4d\x25\xd2\xd1\xab\xe3\x49\xbb\xb0\x3b\x16\x4c\x68\x93\xc2\x7c\
-\x34\x68\xff\xa8\x49\x0a\xbd\xae\xca\xa6\x4d\x0a\xd9\x8b\x8c\xdc\
-\x69\x23\x9f\xe9\xaa\xde\x70\x16\x38\x34\x0d\x97\xdd\x86\xd3\xa6\
-\xe1\x54\x35\x1c\x9a\x8a\x53\xd3\xb0\xab\x5a\x55\xee\x35\x35\x5b\
-\x67\xac\x4f\x7d\x5e\x55\xe4\xf2\x4d\xa5\x74\x5e\x76\xf7\x4d\x19\
-\x9b\x16\x76\xba\x48\x0e\xb5\x7c\xfb\xda\x7a\xff\xad\x03\x4b\xf1\
-\xfc\x59\xc4\x59\x3a\x71\xce\x00\xe7\x66\xaf\xf7\x35\xef\x62\xfd\
-\x6b\x61\x84\xb2\xf2\x60\x28\x87\xcb\xbc\xb8\xfd\x70\x66\x9b\x66\
-\x33\xeb\x17\xdb\xb7\xbb\xa7\x9f\xc7\xa6\x0d\xe1\xa8\x4e\xcb\x33\
-\x15\xfd\xa3\x41\xfb\x47\x3b\xc1\xb4\x2f\x45\x0b\x54\x73\xc9\x1b\
-\xe6\xe8\xfd\x75\xb2\x90\x0d\x09\xd0\x1d\x9a\x96\x77\xda\x34\xc5\
-\xa5\x6a\x2e\xa7\x4d\xc3\xa1\x6a\xcf\x48\xa2\x88\xda\x78\x9e\x1d\
-\x7a\x60\xaa\x7d\xea\xb3\xf5\xcb\x21\x4f\xc1\xa0\xbb\x67\x42\xdf\
-\x54\xed\xeb\xbd\xde\x7f\xfb\xe1\xe1\xd6\x6f\xb5\x55\xeb\x7c\xb1\
-\x88\xb3\x3c\xf2\xbc\x03\xcc\x09\x0b\xf0\xb7\xee\x63\xdd\x91\x1f\
-\x06\x51\x8b\xc4\x5a\x63\x62\xdd\x96\xa9\x74\x63\xa7\x7d\xfd\x7c\
-\x7b\xdc\xbc\xfd\x88\x51\x4f\x00\xa1\x2e\x8d\xac\x85\x42\xfc\xce\
-\x68\xdd\x83\x1d\x20\x2b\xbd\xe1\x52\x45\xa6\x6c\xd2\xc8\x3b\xa4\
-\x8e\x4b\x16\x5d\x4e\x59\xf4\x38\xa4\x81\x43\x16\x71\xe8\x59\x6c\
-\x8a\xfa\xc2\xfa\xda\xa1\xd7\x8f\x77\x4c\x7d\xae\x51\xc8\xea\xfb\
-\xd4\x94\x0c\x3c\x1a\xd3\xdb\x0d\xb9\xb4\xb1\xa3\x80\x63\x6c\xf2\
-\x8d\xf5\xbf\x9d\x57\x14\xbd\xd5\x22\xce\xea\x10\xe7\xf3\xc0\x9f\
-\x56\xec\xfc\x8e\x43\x74\x1e\xfa\x1a\xb5\xc9\x4a\x37\x41\x7f\xc0\
-\xfe\x4f\xb7\xa0\xaa\xe5\xed\x5d\xbc\x76\x97\x74\x63\xcb\xd2\xbd\
-\x6e\xc5\x22\x32\x9f\xa2\xa8\x3f\x4a\x14\x82\xc9\x98\x5b\x35\x71\
-\x9b\x85\x7a\x87\xd4\x7d\x0e\x74\x1c\xa6\x81\xe0\xd5\x7a\x27\xec\
-\x7a\x70\xb8\x73\xea\xb3\xad\xd5\x90\x47\xc2\x54\xcf\xb8\xee\x28\
-\x18\x2c\xab\x90\x89\x43\xcd\xea\x9f\x5a\xff\x8d\x1e\xb7\x3d\xbe\
-\xd5\x22\xce\xea\x90\xe7\x22\x54\x0e\x97\xa9\x5f\x77\x8c\x8e\x03\
-\x5f\xa1\x56\x83\xe5\xc5\xcc\x10\x5b\x0f\x49\x82\xad\x3e\x4c\xd3\
-\xe4\xc2\x87\xf7\x30\x9a\xe6\x7e\x14\xa5\x69\x62\xa4\x92\x18\x89\
-\x24\x46\x32\x81\xd7\x9d\x46\x53\xb3\x68\x5a\x01\xa7\x37\x8f\xcb\
-\x5b\x40\x59\x62\x40\xda\xab\x02\x87\x1e\xec\xeb\x98\xfa\xec\xba\
-\x45\xc8\x93\xef\x9b\x34\x26\xd3\x05\xd9\xba\x92\x73\x29\xc2\xe4\
-\x68\xdb\x1f\xdd\x6b\xf3\x3d\xdc\x6e\x11\xa7\xf6\xc4\xf1\x03\xdf\
-\x9c\xed\x28\x78\xf6\xa0\xbd\xcd\xa8\x36\x17\x8a\x6a\x43\xa8\x36\
-\x14\xc5\x86\x50\x35\x84\x62\x9b\x5e\xa7\x95\xd6\x29\x36\x94\xb2\
-\xf5\x36\x14\x45\x43\xa8\x36\x84\xa2\x3d\x5b\x87\x34\x69\x68\x8d\
-\x32\x90\x8b\x92\x93\x0a\x7a\x22\x81\x91\x4c\xa0\x27\xa7\x89\x92\
-\x48\x60\x64\x32\x30\xe3\x79\xda\x1c\xb0\xe7\x14\x1c\xf8\x14\x38\
-\xbd\x60\xe8\x90\x4a\xa9\xa4\x93\x0a\xd9\xac\x4a\x2e\xaf\x52\x28\
-\x2a\x14\x4d\x15\xd5\xa9\x8d\xda\xea\x6c\xa3\x52\x51\x9d\x52\x12\
-\x90\x52\x84\x98\x27\x3c\xff\x25\x92\xa7\xa7\x73\xea\x73\x1b\x2b\
-\x6b\x98\xc8\xe1\x84\x71\x3f\x96\x91\x35\xab\xd7\xb9\x25\x78\xf5\
-\xc9\xde\xc6\xef\xb4\x21\xb0\x59\xc4\xa9\x2d\x79\x34\x4a\xd1\xd1\
-\x61\x16\x48\x7b\x7e\xd9\xd0\xec\x25\x02\x1d\xfc\x74\x89\x40\xd5\
-\x6a\x75\xba\xa1\x4c\x15\x0d\x75\x32\xaf\x6b\xc9\x82\xae\x65\x0b\
-\x86\x5a\x2c\xe8\x9a\x2c\x18\xaa\x6a\x98\x8a\xd3\x30\x15\x8f\x29\
-\x45\x40\x4a\x11\xa4\x54\xb2\xea\x45\x90\xe7\x5e\xe7\xe4\xe7\xb6\
-\xcf\x96\xe6\xf1\xac\xbc\x32\x14\x37\x6a\x1e\xf7\xd7\xe0\xea\x8f\
-\x9f\xee\xfc\x5d\x43\x28\x46\x50\x11\xe4\xb0\xa9\x83\xc2\xa1\x46\
-\x2d\xe2\xd4\x86\x40\x4e\xe0\x37\x98\x9d\x2a\xfd\xc9\x20\x50\x75\
-\xae\x0c\x53\x24\x75\x43\x8d\xe6\x75\x2d\x51\x34\xb4\x4c\x4e\xd7\
-\x0a\x45\x5d\x33\x0b\x86\xaa\x14\x0d\xc5\x6e\x48\xc5\x23\x4d\x51\
-\x67\x96\x48\xe6\x5b\x21\x79\x6e\x74\x4e\x7e\x7e\xef\xd3\xdf\xd9\
-\xa2\xfc\xa0\x37\x6a\xbc\xb6\x92\xcb\x07\x26\x4c\x29\xa7\x0c\x29\
-\x53\x45\xd3\xcc\x15\x4c\x53\x2f\x18\xa6\xc8\x19\xba\xdd\x6f\x4f\
-\x25\x7e\xf4\x8b\xdf\x5a\xaf\x6a\xe6\xb6\xa7\xfa\xb7\x45\x9c\xda\
-\x91\xa7\x15\xb8\x5f\xc5\x4b\x51\xab\xe0\x47\x83\x52\x84\xf0\xd0\
-\xf4\x32\x48\x29\x6a\xac\x83\xd2\xcc\x07\x5b\x28\xe5\xbb\x28\xf3\
-\x11\xe8\xc0\xa7\xc0\xf5\x12\x4a\x19\x4a\x53\xe4\x0a\xa6\x1a\x2d\
-\xea\x6a\x2c\x6f\x68\x99\xa2\xae\xe5\xf2\xba\x6a\x14\x75\x4d\x14\
-\x4c\xc5\xa6\x9b\xaa\xdb\x34\x85\x4f\x4a\x51\x0f\xd4\x57\x32\x16\
-\x1d\x7a\xf0\x4a\xe7\xd4\xe7\x0e\x19\x86\xb8\xf9\x70\x5c\xdf\x25\
-\x65\x45\xb5\x32\x26\xa5\x9c\x34\x90\x09\xdd\x34\x33\x05\x43\x16\
-\xf3\xa6\x41\xc1\xd0\xb5\xbc\x29\x9d\x39\xc3\xf0\x15\x4c\x33\x58\
-\x34\xcd\x20\xcc\x63\x3c\x99\xca\x0d\xe2\xa1\x9d\xbb\x36\xf6\xdf\
-\x0e\x7f\xe9\xcf\x77\x3d\x7d\x76\x16\x71\x6a\x4b\x9e\x7f\x48\x69\
-\x6a\x8e\xf9\xf0\x0b\xc0\xbf\x05\xde\xa0\x14\x7d\x70\x9c\x52\x29\
-\xa7\x6a\x89\x64\x02\x17\xa6\x6d\xab\xdf\x8b\x44\x22\x63\x8b\x5c\
-\x8f\x03\x38\x08\xfc\x2c\x33\xea\x0b\x3c\x23\x90\x6d\x9a\x40\x9f\
-\x7e\x39\x04\xaa\x8e\x65\xe8\x45\x53\x8d\x16\x74\x35\x5e\x34\xb4\
-\x64\xbe\xa8\xe5\xf2\x86\x5a\xd4\x75\xad\xa8\x26\xb6\x27\x7a\xef\
-\x1c\xaa\xcf\xea\xba\x92\xd7\x4d\x35\x6f\x18\x8e\x9c\x69\x7a\x0a\
-\x86\x51\x5f\x90\x66\x48\x4a\xe9\x58\xe1\xd9\xa3\x24\x82\x3a\x86\
-\xda\x0c\xf0\xdd\x67\xde\xbf\x70\xe6\xe0\x9d\xd3\x16\x71\x6a\x4f\
-\x1c\x95\x52\xa1\x8b\x7d\x15\x36\xff\x21\xf0\x3d\xd3\x15\xf5\x67\
-\x1e\xe3\x02\x0e\x51\x2a\xf2\xf1\x74\x69\x9b\x96\x22\xdd\xb3\x96\
-\xef\x2c\x37\x5d\x39\x1c\x0e\x9f\x00\xfe\x1d\x70\xba\x12\x81\x76\
-\x9f\x84\x83\x6f\xbe\x72\x04\x92\x36\x18\x70\xc3\x88\x13\x32\xce\
-\x92\xb0\x0c\x9a\x86\xda\xf2\xc7\x37\x3f\x73\xfd\x76\x7a\xab\x39\
-\xfa\x30\xf6\x06\xb5\xcb\xf5\x29\xff\x48\xa5\x7d\x1f\x51\x70\x1e\
-\x9c\xb9\xf2\x9f\x7e\xed\xf7\xdf\x69\x6b\x98\x3c\x69\x11\xa7\xf6\
-\xe4\xd9\x44\x69\x26\x82\x29\x4a\x35\xbe\x62\x40\x3c\x12\x89\x64\
-\x96\xd0\x86\x2d\x12\x89\x14\x57\xe9\xfa\x3e\x4b\x69\x7a\x91\x43\
-\xf3\x12\xe8\xd3\xe0\x7a\x81\xae\x0e\x01\xa6\x06\x83\x6e\x18\x76\
-\x42\xd6\x05\x9a\x06\x21\xb5\x14\x4d\x5e\x36\x1e\xf3\x68\xac\xeb\
-\xc3\x3f\xbd\xf9\x99\xa0\x61\x2a\xeb\x07\x5a\x9b\xc7\x8a\xd7\x9e\
-\x64\x31\xe5\xfa\x9a\x5f\x54\xc1\x71\x8e\x74\xdd\xd9\x39\x7d\xa4\
-\x19\xb9\x7f\xfb\xa3\xdf\x78\x6c\x11\x67\x6d\x92\x5b\x00\x5f\xa3\
-\x94\x6e\xdc\xf6\xa2\x08\x24\xc0\xb4\xc1\x80\x0b\x46\xdc\x25\x09\
-\x62\x53\x4b\x04\x59\x4f\xa9\xc4\xc8\xbc\xc8\x15\x1c\x53\xbf\xf7\
-\xe1\x17\x6e\x8d\x25\x1a\x4e\x01\x48\x45\xe9\xee\xed\x68\xdb\xc4\
-\x47\xbd\xef\x93\x2d\x1e\xaf\xad\x9c\x13\xd7\x88\x87\xf6\x23\x2b\
-\x87\x80\x04\xfd\xa9\x21\x8b\x38\x6b\x9b\x40\x1e\xe0\x9f\x51\xaa\
-\x3b\xed\xaa\x44\xa0\x5d\x27\x4a\x2a\x9c\xbb\x6e\xc9\x04\xe9\x77\
-\xc1\xe8\x53\x82\x68\x10\x52\xaa\x20\x48\x25\x5c\x7b\xb2\xf7\xdd\
-\xb7\x1f\x1e\xdf\x6a\x4a\xf1\xac\xfc\x69\xc2\xe7\xbd\x1e\xad\x0f\
-\xec\xa3\x7b\xf4\x3c\x63\x89\x33\x35\xec\x96\x51\xe2\x21\x15\x53\
-\x69\x58\xf0\x1e\x2d\xe2\x58\x08\x87\xc3\x9d\x94\x66\x7d\xab\x18\
-\xee\x30\x2f\x81\x4c\xc8\xc5\xc0\x48\x80\x5a\x04\xa7\x0a\x01\x2f\
-\x66\x63\x03\x86\xa2\xb0\xe2\x50\xf1\x78\xc6\x37\xf8\xcd\x2b\x5f\
-\x1a\x4c\xe5\xbd\x73\x5c\xcd\xfd\x6d\x2d\x79\x5d\xd3\x1c\x44\x93\
-\x1f\xf2\x60\xe4\x40\x8d\xba\xc2\x20\x15\xb8\x45\xd1\xb6\x6f\xd1\
-\x8f\x83\x45\x1c\x0b\x33\x08\x74\x88\x92\xe7\xef\x53\x95\xb6\x6b\
-\x36\xd8\x73\x02\x8e\xef\x83\x80\x17\x42\x41\xb0\xd9\x6a\x7f\x1d\
-\xa6\xc4\x3c\xff\xe0\xe4\x85\xeb\xfd\xbb\x8e\x48\x29\xe6\x44\x2b\
-\x9b\xaa\xd2\xfb\xa4\xbd\xad\x0b\x00\xdd\x9c\xe2\x72\x77\x7d\x4d\
-\x4e\x9c\x77\x9d\x23\xe3\x3d\x5b\x95\x54\xb5\x88\x63\xa1\x02\x81\
-\x3e\x3f\x2d\x81\xf6\x56\xda\xbe\x6f\x1f\xfc\xe0\x0f\x96\x2c\xf8\
-\x5a\x63\x24\xde\x78\xff\xf7\xae\x7d\xb1\x58\xd0\x6d\xbb\xe7\xdb\
-\x27\xe6\xaf\xeb\x9d\xf2\xd7\x75\x3d\x5b\x71\xf1\xd1\x00\x52\x76\
-\xac\x8c\xad\xea\x65\xe2\xc1\xc3\x54\xe9\xa1\xb3\x88\x63\x61\x3e\
-\xf2\x28\xd3\x0e\x84\x7f\x4d\xc9\x36\x29\xc3\x96\x2d\xf0\x23\x3f\
-\x02\xce\x1a\x05\xda\xe8\x86\x9a\xfd\x93\x9b\x9f\xb9\xd8\x33\xbe\
-\xfe\x24\x2c\xa8\xe6\xc9\xbe\xb6\x56\x61\xcc\x2c\x02\x7d\xad\xf7\
-\x22\xf9\xe2\xb1\x15\x9c\x7e\x88\x78\x83\x0b\x53\x54\x2d\xb9\x2c\
-\xe2\x58\x58\x8c\x40\xda\xb4\xed\xf3\x33\x40\x59\x5a\x76\x67\x27\
-\xfc\xd8\x8f\x81\x77\x85\xe1\x3b\xdd\x63\x5d\xd7\xfe\xe4\xe6\x67\
-\xea\x0d\x53\x59\xb4\x5c\xa0\xae\xa9\xfd\xfd\x6d\xad\x9d\x65\x2b\
-\x1f\x8d\x9e\x63\x3c\x71\x76\x99\xa7\x2f\x92\x0c\x3c\x40\xb7\xed\
-\x5a\xca\x41\x16\x71\x2c\x54\x4b\x20\x41\xa9\xb8\xe0\xff\x46\xa9\
-\xc0\x20\x00\xcd\xcd\xf0\xe3\x3f\x0e\xf5\xcb\xb0\x32\xa6\x5d\xcc\
-\x37\xa7\x5d\xcc\x55\xa9\x48\x93\xf5\xfe\xa9\xb8\xcf\x57\x7e\xb6\
-\x89\xe4\x15\x1e\x8e\x2c\x2f\xc0\x33\xef\x3a\x4f\xc6\xbb\x64\xaf\
-\x9c\x45\x1c\x0b\xcb\x75\x22\x7c\x1e\xf8\x02\xf0\x5a\x20\x00\x3f\
-\xf1\x13\x25\x12\x55\x8b\x0f\xfb\xf6\xbc\x7b\xfe\xc1\xeb\x5b\xa4\
-\x14\x4b\xa9\x9b\x2d\xfb\xda\x5b\x85\x31\x3b\xe3\xb5\x68\x44\xb9\
-\xd2\xb3\xf4\x72\xea\x86\x7a\x91\x44\x70\x59\x2a\x9e\x45\x1c\x0b\
-\x2b\x21\x90\x1b\x18\x07\xdc\x1e\x4f\x49\x6d\x5b\xb7\x6e\xe1\x63\
-\xe2\x19\xdf\xe0\x37\xaf\x7e\x69\x20\x95\xf3\x2e\xb9\x66\x76\xd1\
-\x6e\x1f\x19\x68\x69\xaa\x9c\xf6\x7a\xf1\xd1\x30\x72\x09\x09\x6c\
-\x52\xf4\x91\x08\xd6\x63\x2a\xcb\x0a\x32\xb2\x66\x9d\xb6\xb0\x6c\
-\x4c\x87\x11\x7d\x0b\x20\x9d\x86\x5f\xfa\x25\x78\xf0\xa0\xf2\xbe\
-\xa6\xc4\x38\x77\xff\xc4\x77\x7e\xed\xdd\xaf\xfa\x97\x43\x1a\x80\
-\x84\xd7\x1d\x9c\x77\xa3\x5d\xeb\x5b\x8a\x96\x48\xca\x9f\x59\x2e\
-\x69\x2c\xe2\x58\xa8\x05\x3e\x7c\x66\x2e\xe4\xe1\x57\x7e\x05\xae\
-\x5f\x2f\xdf\x61\x34\xde\x78\xff\x3f\x9f\xfb\xfa\x9d\x0f\xfb\xf6\
-\xbc\x21\x97\x36\x53\xc1\x4c\x11\x61\xa6\x5c\xee\xf9\x2b\xf3\x78\
-\x9d\xd9\xea\x69\xe3\xbe\x8c\x6e\x5b\x51\xa6\xa8\x55\x3b\xda\xc2\
-\x4a\xf1\x68\xe6\x0f\x5d\x87\xff\xfe\xdf\xe1\x2b\x5f\x81\xfd\x07\
-\x54\x7e\xe7\xc2\x91\xf8\x04\xfb\x37\xc2\xca\x22\x09\x72\x0e\x67\
-\xdc\x54\x95\xf9\x5d\x10\x01\xb7\x87\x68\x15\xb5\x77\x0d\xed\x5d\
-\xb2\x9e\x53\x2b\xbd\x69\x8b\x38\x16\x6a\x4a\x1c\x28\x95\x3d\xf8\
-\x9f\xdf\x6e\xe5\xdd\xb1\x33\x28\xae\x80\xbf\x16\x27\x49\x7a\x3d\
-\x0b\xfb\xed\x02\xee\x75\x8b\x0b\x2d\x7a\x48\x05\xf6\xd7\xe2\x7a\
-\x2c\xe2\x58\xa8\x39\x71\x42\xbb\x8e\x53\xb7\x71\x2f\x35\x4b\x93\
-\x11\x18\x69\x97\x73\xe1\xc2\x21\x76\xad\x19\xc1\x18\x72\xde\xd9\
-\xed\x32\xa4\xea\x4d\x4c\x51\x93\x59\x40\x2d\x1b\xc7\xc2\x4a\x51\
-\x66\x77\x08\x4d\xa3\xeb\xe4\x69\x8a\xc9\x68\xcd\x4e\x90\x75\x38\
-\x0a\xb2\x9a\x79\x39\x6c\xda\x93\xf9\x1b\xf1\x7c\x88\xae\x6d\xae\
-\xd5\x35\x59\xc4\xb1\xb0\x52\x94\x19\xd9\xce\x40\x08\x9b\xc7\x43\
-\xc7\xc1\xbd\xc8\xfc\x14\x48\x73\xc5\xe3\x1d\x29\x8f\xa7\xba\x54\
-\x04\xaf\x33\x5d\x71\xbd\x6e\x7b\x9b\x9c\xfb\x44\x0d\xee\x35\xa5\
-\xc9\xe2\xa5\x50\x61\xf2\x2f\x2d\x55\xcd\xc2\x4a\xf1\xfa\xcc\x1f\
-\xae\xc0\x73\x8f\x71\xcb\xee\x5d\xa4\xc7\xc6\xb2\xe9\xa4\x3e\x2e\
-\x4d\x96\x97\xa5\x29\x84\x9e\x76\xb9\xaa\x7b\x4f\x03\x6e\x17\x93\
-\xb3\x66\x5c\x93\xe2\x01\x29\xff\x72\xe7\x1f\x2a\xa8\xd2\xbc\xe9\
-\x33\x52\xd1\x50\x21\x1a\xf0\xe9\xa9\x3d\x02\x79\xd4\xb2\x71\x2c\
-\xd4\x02\x65\x29\x08\xce\xfa\xf2\xfc\x2f\x4f\x53\x93\xdb\x15\x32\
-\x43\x53\x03\x13\xef\xe9\x05\xfd\xf5\x25\xab\x69\x2e\x67\x5e\x2a\
-\xa2\x4a\xe2\x78\x3a\x67\x4b\x08\x92\xf5\x76\xa4\xa8\x36\x14\xd5\
-\x14\x98\xb7\x3d\x7a\x66\x38\x54\x9c\x74\x05\x8a\x89\xdd\x0a\xe6\
-\x21\xcb\x39\x60\xa1\xa6\x08\x87\xc3\x76\xe0\x64\x99\xc4\xa9\x9f\
-\x1b\xf9\xa2\xa8\x8a\x37\xb4\xbe\xe9\xf5\x54\x34\xf1\x4e\x7a\x32\
-\x75\x10\xaa\xaf\xeb\x9c\x74\xbb\xab\x37\xe6\x1d\x5a\x1b\xa5\x49\
-\x16\x4b\x62\x2f\xe3\xbd\x89\xa1\x2e\x92\x56\x2d\x1f\xb9\xcd\x5c\
-\x4f\xb0\x30\xa5\xd6\x17\x63\x5b\x35\xa9\xef\x01\xf6\x2c\x76\x2a\
-\x8b\x38\x16\x56\x82\x63\xb3\x49\xe0\xac\x9f\x3f\x64\xcc\x1b\xaa\
-\x3b\xe9\xf0\x3a\x7b\x62\x03\x51\xc3\x34\xe5\x96\x2a\x2c\xf0\x7c\
-\xc6\xe5\x5c\x5a\x89\x27\xbb\xf6\x98\x82\x1e\xc4\xd0\x2e\x90\x77\
-\x9d\xae\xb0\xc7\x80\x43\x16\xee\xd5\x17\x26\xf5\x50\x21\xd6\x65\
-\x97\x85\x6d\xc0\x92\x9d\x06\x16\x71\x2c\xd4\x4c\x4d\x5b\x8c\x38\
-\x00\x36\x87\x7d\x63\xc3\x86\x96\xdc\xd4\xe0\xc4\xdb\xc5\x5c\x71\
-\xc1\x81\xc8\xac\xdd\x19\x95\x42\x2c\x6d\xaa\x41\x8f\x33\x49\x3e\
-\x7d\x87\x64\xe0\x69\xf0\xe6\x84\x4d\x16\x6f\x06\x8a\x89\x74\xa8\
-\x10\x6d\x73\x99\xb9\x3d\x94\x8a\x36\xae\x08\x16\x71\x2c\xac\x04\
-\x65\x2f\xbe\xea\x70\x60\x73\x2f\x1e\x51\x23\x14\xe1\x0c\x76\x36\
-\x9e\xca\xc4\xd3\xef\x27\xc7\xe2\xbb\x98\xa7\xee\x76\xd2\xe7\x09\
-\x2e\xf9\x8a\x82\x1e\xc5\xf6\x58\xf4\x78\x0b\xf1\xee\x86\x42\xb4\
-\xde\x6b\x64\xf6\x83\x7c\xa3\xd6\x37\x6e\x11\xc7\xc2\x4a\x50\x36\
-\x0a\xef\x0c\x2c\x2d\xb2\xdf\xed\xf7\x1c\x77\xb8\x1d\xfd\x93\xfd\
-\xe3\xfd\xa6\x21\xcb\x13\xc9\x04\xe9\xb4\xd3\x59\x8d\x7d\x63\x0a\
-\xdd\xbc\xe9\x9d\x48\x0c\x84\xba\x87\x9d\x81\x81\xe8\x1e\x21\xcd\
-\xa6\xd5\xbe\x71\x8b\x38\x16\x96\xeb\x18\xe8\x7c\x66\x84\x2f\x93\
-\x38\x00\xaa\x4d\xeb\x6c\xd8\xd0\x5a\x8c\x0d\x47\xcf\x17\xd2\xf9\
-\xd3\x4c\x87\x1b\xe4\x1c\xf6\x1e\x84\xa8\x6c\xa4\x4b\xd9\xe7\x8c\
-\x67\x1e\x86\x7a\x47\xcd\x60\xcf\xe8\x66\xad\x50\xdc\x47\xe5\xea\
-\xa9\x16\x71\x2c\xbc\x72\x98\xf3\xa2\xba\xea\x43\xcb\x6a\x48\x08\
-\x6c\xf5\x6d\xa1\x33\xb9\x44\xe6\x72\x7c\x34\xb6\x09\x08\x26\xbc\
-\xbe\x19\xd2\x46\x26\x6d\x99\xc2\xad\xc0\xc0\x44\x3a\xd4\x3d\xd2\
-\xe6\x8a\xa7\x77\x50\xaa\xf2\xf9\xd2\x60\x11\xc7\x42\x4d\xd4\xb4\
-\x92\x63\xa0\x61\x45\x0d\x3a\xeb\xdc\x47\x6c\x6e\xc7\xe8\xe4\xc0\
-\xc4\xfb\x39\x4d\xf3\x06\xfa\x27\xbe\x13\xea\x19\x0e\xf8\x46\x62\
-\xbb\x84\x94\xc7\x5f\xa5\x9b\xb7\x88\x63\xa1\x66\x12\xc7\xb9\x4c\
-\x89\x33\x13\x8a\x2a\xd2\x13\x76\x33\xb5\xe5\xdc\xf5\x5c\x20\x93\
-\xd9\x8e\x24\x29\x6d\xe2\x96\x29\xc9\x49\x29\x74\xa4\x34\xa5\x44\
-\x91\x52\xd8\xa4\x94\x2e\x4a\xf9\x3d\x7e\x4a\xd3\x81\x68\x16\x71\
-\x2c\x7c\x0c\x25\xce\xca\x88\x93\x2c\xe4\xde\xbe\x3b\x35\x72\xc8\
-\x4c\x33\x1e\x6f\x6e\x38\xda\x92\x4c\x5d\xec\x8a\x4e\xee\x51\xc0\
-\xa3\x3c\x8b\xb4\x16\x65\x1e\x84\x32\xcb\x07\x62\x52\x12\x43\x92\
-\x34\x4d\xb2\x12\xf2\xd2\x34\xcd\xd2\xd4\x37\x52\x35\x4d\xe9\x00\
-\xc5\x0d\xb2\x6e\x9a\x68\x5e\x8b\x38\x16\x5e\xa4\x63\xc0\x0b\x6c\
-\x2a\x7b\x91\x5c\x1e\x34\xc7\xf2\x8a\xac\x49\x29\x27\x7b\x12\xe3\
-\x8f\xc6\xb3\xa9\x92\x7b\xdb\x54\x8a\x00\x23\x3e\xef\xb1\x84\xd3\
-\xde\xb3\x77\x68\x54\x08\xc9\x62\xa5\xa3\x84\x80\x80\x10\x04\x10\
-\xf0\x3c\x98\x5a\x99\x41\xb2\x72\xf2\x49\xc8\x23\x99\x42\xca\x84\
-\x94\x22\x65\x4a\x59\xb5\x54\xb3\x88\x63\x61\x39\x98\x93\x6c\xb3\
-\x5c\xc7\x40\xd1\x30\xae\xde\x9c\x1c\xec\x28\x18\xfa\xf3\xfa\xd0\
-\x86\x30\x9e\xfe\x9b\xb1\xd9\x37\x5e\x5e\xd7\x9e\xda\x3b\x38\x7a\
-\xc9\xa9\xeb\x47\x6b\x79\x13\x02\x1c\x08\x5a\x10\xa2\x45\x00\x4b\
-\x91\x6a\x56\x5a\x81\x85\x97\x65\xdf\xe4\x47\xd2\x89\xf3\x57\xc7\
-\x9f\x1c\x2c\x18\x7a\x79\x61\x29\x29\xca\x52\x11\x0c\xa1\x78\x3f\
-\xec\x68\x79\x6d\xdc\xeb\x3e\x47\x69\x56\xba\x97\x05\x21\x20\xa0\
-\x08\xba\x2c\x89\x63\xe1\x85\xdb\x37\xa6\x29\xbb\xef\x4c\x0d\x1b\
-\xa9\x62\xae\x72\x21\x40\x5d\x8a\xb9\xd9\xa3\x42\x3c\x6a\x08\x9d\
-\x9d\x72\xbb\xae\x6e\x1d\x8f\x6e\x40\x12\x7c\x99\x1d\x60\x49\x1c\
-\x0b\xcb\x55\xd5\xca\x89\x53\xe5\xe0\x67\xb2\x90\xbb\x70\x79\xfc\
-\x49\x5b\xaa\x98\xdb\x3a\x3f\xb3\xe6\x37\x21\xa2\x6e\xf7\xa1\x0f\
-\xdb\x5b\x33\xa6\x10\xf7\x2c\xe2\x58\xf8\xb8\xa1\x71\xa9\x12\x47\
-\x22\xa3\xdd\xb1\x89\xcb\xb7\x27\x87\x4e\x4b\x69\xba\x16\xd9\x79\
-\xc1\x8a\x38\x39\x4d\xeb\xf8\x60\x7d\x7b\x57\xc6\x6e\x7b\xc7\x22\
-\x8e\x85\x8f\x13\xe6\xa8\x49\x0b\x39\x07\x0a\x86\x71\xe5\xda\x58\
-\x9f\x39\x9e\x4b\x54\x97\x89\x69\xb2\x68\x2a\x81\x44\x38\xaf\xb7\
-\xb5\x9c\x1c\xf2\xfb\x2e\x00\xc5\x17\xdd\x01\x96\x8d\x63\x61\x49\
-\x98\x9e\xfe\xa3\xac\xe4\x93\xdd\xe7\x47\xd1\x2a\x0a\x89\xdc\x48\
-\x26\x71\xa9\x37\x31\xf1\x2c\x06\xad\x2a\xc8\xea\x13\xdd\x9e\xd4\
-\x07\x4e\xc7\x5c\xce\x5b\x3b\x46\xc7\x1b\x85\xa4\xf9\x45\xf5\x83\
-\x25\x71\x2c\x2c\x15\xfe\xd9\xef\x8d\x33\x30\xd7\x4e\x37\xa4\xf9\
-\xe8\xe6\xc4\x60\x7f\x6f\x62\xe2\x0c\x4b\xae\x13\xb5\xb4\x12\x4e\
-\x71\xa7\x73\xf7\xd5\x8e\x76\xa1\xab\xe2\xc6\x6a\xdf\xbc\x09\xdd\
-\x13\x45\xe7\x39\x4b\xe2\x58\xa8\x81\x9a\x56\x1e\xa3\x96\x28\xe4\
-\xce\xdf\x9d\x1c\x39\x2a\x31\x97\x3b\x22\x5a\xb7\x54\xae\x15\x55\
-\xa5\xe9\x4a\x67\x7b\x70\xe7\xc8\xf8\xf9\xba\x5c\xbe\x96\x93\xe9\
-\x22\xa1\x7f\xaa\x68\xef\x1e\xcc\x79\xda\x53\x86\x6d\x0b\xb0\xc9\
-\x22\x8e\x85\x15\x6b\x29\x4f\x1d\x03\xc5\x4c\x9a\xee\xe1\x5e\x23\
-\xe6\x71\x9d\x59\xc1\x5b\x6a\x80\xf0\x2e\xef\x50\xa1\xdd\x6e\x69\
-\x3a\xd3\x9e\x48\xbe\xbb\x6e\x32\x76\x80\x25\xd4\x36\xa8\x40\x96\
-\x91\x84\x6e\xbb\x3f\x90\xf3\x34\x24\x74\xfb\x2e\xa0\xd3\xb2\x71\
-\x2c\xac\x04\x03\x95\x88\x33\x35\xd4\x47\xb7\x9e\x43\xf7\xb8\xd4\
-\x15\xea\x42\x09\x4a\xa1\x2d\xcb\xc6\x60\x9d\xef\x44\xdc\x61\x7f\
-\xb0\x7b\x64\xcc\x25\x24\x9d\x4b\x20\xcb\x64\xca\xd0\x6e\x0d\x66\
-\xbd\xfe\x29\xdd\xbe\x07\x68\xb1\x9c\x03\x16\x6a\x85\xcf\x02\xfa\
-\xcc\x77\x67\x34\x16\x25\xd6\xd6\x02\xf6\x1a\xbc\x4e\x26\xa9\x95\
-\x12\x07\x20\xe5\x70\x6c\xbd\xd2\xd1\x1e\xdb\x37\x3c\x7c\xc5\xae\
-\x9b\x87\x17\x20\x4b\x22\x6b\x68\x37\x06\x73\x6e\x57\xb4\xe8\xd8\
-\x27\x11\xa7\xab\xb2\xc2\xac\x89\xa5\x2c\x54\x83\x70\x38\xac\x02\
-\xbf\x0d\xfc\xcd\xb2\x0d\x01\x37\xf8\x77\x43\x20\x00\x1b\x83\xe0\
-\x5a\xe1\x6c\xba\x79\x71\x97\x51\x75\x47\x2d\xed\xf9\x6d\xe3\x13\
-\x17\x82\xe9\xec\x4c\x27\x45\x26\x6b\xa8\xd7\x87\x0b\x6e\x65\x2c\
-\xef\xdc\x2f\x11\x8e\xa5\x36\x6a\x49\x1c\x0b\xd5\xe2\x1f\xcd\x21\
-\x0d\xc0\xba\x10\xb8\x52\x30\xd8\x05\x53\x06\x84\x86\x60\x63\x03\
-\x38\xec\xcb\x3b\x8b\x21\x72\xb5\xb6\xc9\xee\x37\x36\x9c\x6d\x72\
-\xa5\xde\x6f\x1f\x8b\x8b\x91\xbc\xdb\x1c\xcd\xbb\xf6\x99\x88\x15\
-\x25\xc6\x59\xc4\xb1\x50\x8d\xb4\xd9\x0c\xfc\x1f\x73\x36\x78\x1c\
-\xd0\xe0\x2d\x29\x3c\xe6\x28\x88\x16\x98\xa8\x87\xc9\x3c\xac\xcb\
-\x3f\xa0\xd3\xd7\x0e\x2c\x6d\x76\x00\x83\x7c\x0d\x2f\xdd\x40\x97\
-\xd7\x99\x30\x32\x63\x13\xf6\x3d\x63\x7a\x83\xbf\x56\x0d\x5b\xc4\
-\xb1\x50\x0d\x7e\x19\x98\x1b\x26\xd3\x39\xc3\x33\x1d\x8a\x42\x6c\
-\xda\x96\x36\x35\xe8\x65\x2b\x43\xd9\x28\xdb\x1c\x57\x08\x28\xc7\
-\x80\xea\xd4\x21\x73\xc5\x51\x00\x12\x43\xde\x24\x6a\xc6\x18\x33\
-\x76\xa2\xcb\x83\xab\xd1\x21\x16\x71\x2c\x2c\x26\x6d\x0e\x01\x6f\
-\xcd\xd9\x60\xd7\xa0\x65\xc6\x07\xbc\xd1\x80\xc9\x54\x1e\xc5\xfb\
-\x9c\x20\x05\x42\xdc\xcc\x9f\xc1\xa9\x0c\xb1\xc3\xde\x8d\x57\xbc\
-\x0e\xa8\x8b\xa8\x6a\xcb\x4b\x1b\x30\xe4\x5d\x62\xe6\x18\xa3\xc6\
-\x56\x0a\x72\xef\x6a\xf7\x8b\x45\x1c\x0b\x8b\xe1\x1f\x57\x5c\xdb\
-\x59\x0f\xca\xac\x41\xca\x86\xcc\x65\x26\xbd\x27\xe7\xec\x9b\x33\
-\xdb\xf8\x30\xd7\x86\x5b\x79\xcc\x4e\xfb\x08\x2e\x71\x8c\xf9\x46\
-\x38\x0d\xaa\xf7\x56\x49\x1e\x11\x37\x07\x18\xd6\x37\x90\x97\x3b\
-\x80\x1d\x2f\xaa\x53\x2c\xaf\x9a\x85\x85\xa4\x4d\x3d\x30\x3c\x47\
-\xcd\x52\x05\xbc\xbe\x05\x6c\xb3\x84\x87\x54\xb2\x3c\xda\x9d\x63\
-\x31\x77\x72\x9d\x7a\x8f\xed\xf6\x14\x0e\xe6\xba\x89\x47\xd5\x0b\
-\xe4\x17\x70\x09\x4b\xfa\x48\x98\x8f\x19\xd1\xdb\xc9\xca\xcd\x2f\
-\xab\x6f\x2c\x89\x63\x61\x21\x7c\xa5\xa2\x6d\xd2\x1a\x98\x4b\x1a\
-\x00\x61\xba\x70\x27\x3e\x20\x53\xb7\x70\xe4\x40\xc2\xd8\xce\x07\
-\x59\x08\x2a\x37\xd8\x6a\x07\x9b\xd8\x3b\xc3\xc6\xa9\xd0\xb0\x1c\
-\x21\xc5\x7d\x46\x8a\x8d\xa4\xe4\x4e\x5e\x72\x4d\x35\x8b\x38\x16\
-\x16\xc3\x0f\xcf\x25\x07\x25\x17\xf4\x7c\x68\x1a\xda\x4c\x6f\x9d\
-\xb1\xa8\x2d\x03\x30\x69\xee\xe5\x62\x0e\x9a\xb5\xcb\x6c\xb2\xd5\
-\xa1\xb2\x0d\x29\x9e\xfa\xb1\xa3\xa4\xe5\x6d\xc6\x74\x3f\x71\x73\
-\x2f\x0b\x8c\xe2\xbf\x0c\x58\xaa\x9a\x85\xf9\xd4\xb4\x5d\xc0\xad\
-\x39\x1b\x9a\xeb\x60\x57\xfb\xc2\x07\xf7\x6e\xbf\x44\xd1\xbe\xd4\
-\xc2\x1a\x92\x76\xdb\x45\x21\xb5\x31\x39\x48\x3b\x71\x63\x3f\xf2\
-\xd5\xfd\xb0\x5b\x69\x05\x16\xe6\xc3\x0f\x56\x5c\xbb\xae\x8a\x14\
-\xe9\x86\x21\xc7\x32\xce\x27\x18\x2c\xee\xa7\x67\xf4\xf5\xc6\xb1\
-\xfe\x29\x5f\x21\xf1\xae\x22\xcd\x5b\x80\xb4\x88\x63\xe1\xe3\x84\
-\xd7\xe6\xac\xa9\xf7\x80\xaf\x8a\x90\x1a\x6f\x62\x3f\x98\x8f\x96\
-\x7c\x46\x29\x6f\x4b\x68\x1c\xf7\xdb\xdf\xd2\xb4\x84\xba\x21\xdd\
-\xbd\x61\x7d\xba\x77\xcc\x5f\x8c\x5d\x50\xa5\xfe\x11\x60\xbc\x2a\
-\x9d\x63\xd9\x38\x16\xe6\xc3\x5c\x8f\xd5\xba\x25\x14\x96\xa9\x9f\
-\x18\x66\xaa\x69\x69\x5e\x2f\xa3\xf8\x6c\xd6\xe8\x29\xb7\xf3\x64\
-\xca\x6e\xef\xed\x88\x25\xb2\x0d\xf9\x89\xd3\x0d\xf9\x09\x0c\xa1\
-\x4e\xc6\x6d\xfe\xdb\x09\x9b\xdf\x69\x08\x75\x1f\xb3\xa6\x8a\xb7\
-\x6c\x1c\x0b\x2f\xdb\xbe\x71\x02\x19\x66\x8e\xb5\x78\x1c\x70\x74\
-\x63\xf5\x8d\x98\x4a\x86\xee\xdd\x05\x20\x50\xf5\x31\x85\xd4\x63\
-\x4c\x73\x76\xc5\xce\x62\x6b\x3c\xfd\x9e\xbb\x50\x2c\x4b\xbf\x36\
-\x51\x92\x09\x5b\xdd\x8d\xb8\xcd\xaf\xea\x8a\x6d\x2f\x2b\xc8\xbd\
-\xb1\x24\x8e\x85\x5a\x61\x13\xb3\x07\x28\xd7\x2d\xb1\x8c\x99\x62\
-\xba\x71\x27\x2f\x93\xf1\x55\x9b\xd4\x36\x50\x81\x34\x00\xb6\x61\
-\xbf\xe7\x8c\x37\x5f\xbc\xda\x94\x48\x77\x0a\x68\x2a\xd9\x18\xa6\
-\x2f\x50\x8c\x9d\x08\x14\x63\x98\x88\x4c\xd2\x56\x77\x31\x6e\xab\
-\x33\x8b\x8a\x7d\x0f\x08\x9f\x65\xe3\x58\x78\x19\xf8\xee\xb2\x5f\
-\x8e\x59\xe1\x35\xd5\xa2\x69\x68\x23\xd5\x56\xde\x34\x8d\xc7\x0b\
-\x6d\x4e\x39\x6c\x87\x9e\x34\xf8\x15\x5d\x51\xae\xcc\x7d\x89\xa5\
-\xdb\x5f\x8c\x1f\x5b\x97\xe9\x7f\x7d\x63\xaa\xc7\xd1\x94\x1b\xb9\
-\xec\x30\xf2\x6f\x23\xe5\xa4\xa5\xaa\x59\x78\x51\x6a\x9a\x06\xf4\
-\x02\xcf\x7d\xce\xdb\x80\x40\x03\xb8\x1b\x4a\x45\x60\x97\x82\xc7\
-\xdb\x3f\x40\xb7\xbf\xb6\xe8\x7e\xc5\xdc\x25\x8c\x42\x35\x2e\x6c\
-\xd9\x90\xca\x5e\xf0\x67\xf3\xc7\x17\xb3\x71\x24\x18\x19\xd5\x7b\
-\x33\x66\x0f\x24\x72\xaa\x63\x1b\x88\x9a\x55\xc1\xb1\x54\x35\x0b\
-\xb3\xf1\x3d\x65\xa4\x81\xd2\x1c\xcd\x0d\x13\x30\x91\x86\x5c\xdb\
-\xd2\x72\x6d\x1a\x86\x6d\x8c\xac\x5f\x94\x36\x98\xc5\x5d\xd5\x7e\
-\xec\x27\xbc\xae\x33\x29\xa7\xed\x5e\xdb\x54\xca\x21\x98\x7f\x16\
-\x03\x01\xaa\xc7\x48\xed\xf7\x64\x53\x00\x32\xa3\xba\x6f\xc5\x6c\
-\x81\x68\x56\x75\x6d\x44\x88\xce\x95\x74\x92\xa5\xaa\x59\x98\x8d\
-\x9f\x28\xfb\xe5\x04\x9e\x16\xb1\x69\xc8\x42\xcb\x63\x89\x92\xbb\
-\x58\x75\x6b\xbe\xf8\x01\x84\xec\x5e\x58\x86\xc8\x3b\x48\xb9\xa4\
-\x02\x1d\x39\x4d\xdb\xfe\xb8\x21\xd0\x94\xd7\xd4\x6a\xab\x79\x0a\
-\xb7\x91\xd9\xdd\x96\x1b\x3a\xb3\x29\xdd\xdd\xd9\x9e\x1d\xb8\xef\
-\xd1\xd3\xe7\x04\x8b\x5c\x9b\xa5\xaa\x59\xa8\x42\x4d\xdb\x02\xdc\
-\x2f\x73\x0c\x6c\x02\x2a\x65\xec\xe7\x82\xef\x93\x6a\xda\x89\x14\
-\x8b\x1b\x3f\xd1\x96\x0b\x4c\x36\xcd\x1f\xb8\x69\x16\xcf\x51\xc8\
-\x9e\x5d\xee\x75\xfb\xb3\xf9\xf7\x43\xa9\xcc\x2e\x81\xa8\x5b\xce\
-\xf1\x79\xe1\xe8\x89\xdb\xeb\xfb\x52\x9a\xbb\x45\x0a\x65\xbb\x25\
-\x71\x2c\x2c\x99\x3b\xcc\xf6\xa6\xcd\x17\x5d\xe3\x9c\x3c\x4e\x7d\
-\x77\x1a\x45\xbf\xbe\x68\xab\xf5\x63\x87\x80\xf8\xbc\xdb\xf5\x62\
-\xeb\x4a\x2e\x3a\xee\x72\x1c\xef\x0f\x05\x12\x86\x10\x37\x97\x73\
-\xbc\x43\xe6\x37\x36\xe5\x47\xce\x6e\x4c\xf7\x6c\xef\x4c\xf7\x0e\
-\xd4\x15\xe3\xe7\x15\x69\xde\x64\x81\xa8\x05\x4b\xe2\x58\x78\x2a\
-\x6d\x6c\x94\x4a\x3f\x35\x95\x59\xc0\xdf\xb3\xe8\xe7\xd5\x24\xd9\
-\x7a\x81\x5c\xe0\xe4\x82\x36\xf3\xe0\xc6\xf3\x64\xbc\x67\x2a\x58\
-\xf0\x63\xc8\x7b\xad\xe4\x00\x00\x20\x00\x49\x44\x41\x54\xe4\x13\
-\x8d\x2c\xb9\xda\x67\x65\x0a\x36\x27\xd2\xef\x78\xf3\xc5\xd3\xb5\
-\x10\x0a\xba\xd0\x46\xe3\xf6\xc0\xfd\xa4\xe6\xf3\x19\x42\xdd\x33\
-\xf3\xfe\x2c\xe7\x80\x85\xa7\xf8\x52\x19\x69\x00\x5a\xab\x7a\xfd\
-\x14\x7c\xc3\x67\x71\xc6\xef\x10\x5f\xe7\x41\x8a\xca\x9e\x80\xa6\
-\x81\x8d\xf4\x6e\x37\xe7\xb4\x28\xcd\x07\x73\xce\xbb\x7c\x68\xa3\
-\x75\x9e\xb3\xc9\x42\xf1\x7a\x4b\x3c\xdd\x24\x4a\x77\xb0\xfc\xc6\
-\xa4\xde\x1c\xca\x4f\x34\x87\x9e\x46\x2d\x68\xfe\x3b\x09\xbb\xdf\
-\x61\x08\x75\xaf\xa5\xaa\x59\x78\x8a\x1f\x98\xb3\xa6\x63\x09\x47\
-\xdb\x32\x3b\x09\xde\x6f\x40\xcb\x55\x36\xd6\x6d\x85\x4e\xb4\xc2\
-\xd5\x0a\xf6\x4d\xcd\xdf\xc1\x8c\xdd\xb6\xaf\xb7\x21\xe0\xd4\x55\
-\xf5\x52\xad\xda\x54\xa5\x11\x0c\x16\x27\x4f\x76\xa5\x1f\x1f\xd9\
-\x90\xea\x29\x58\xc4\xb1\x40\x38\x1c\xb6\x33\xbb\xae\x80\x02\xb4\
-\x2d\xb1\x21\x45\x7a\xa8\x7f\x7c\x12\xef\xf0\x45\x04\x53\x73\xb6\
-\x37\x8e\xa8\x73\xd4\x3c\xa3\xb0\x7d\x35\xee\xc9\x14\xd4\x3f\x09\
-\xfa\x8e\xc6\x5c\xce\x0b\x40\xb6\x96\x6d\x2b\x98\x3e\x8b\x38\x16\
-\x00\xce\x32\x7b\xea\xf2\xa6\x15\x28\xf2\xae\xd8\x31\x82\x8f\x72\
-\x08\xe3\xa3\xb2\xf5\xde\xd8\x01\x84\xec\x99\xb1\xe6\x2e\x52\xae\
-\xea\x94\x84\x51\xaf\xf3\xf4\x40\xbd\x6f\xd0\x84\x47\xb5\x25\x8f\
-\x05\x0b\xf0\x85\x15\xa9\x69\x15\xdf\xac\x62\x2b\x0d\x0f\xf6\xe2\
-\x8c\x9d\xe7\xf9\xc4\x4f\x02\xff\xc4\xf3\xda\xd3\xa6\x3e\xfe\x22\
-\x6e\x2e\xaf\xa9\x9b\x7b\x1b\x03\x1d\x39\x9b\x7a\xc1\x22\x8e\x85\
-\x5a\x62\x6e\x48\x4c\x7b\x8d\xde\x2f\xdf\xf0\x19\xea\x7b\xbb\x11\
-\xb2\x14\x8b\x16\x1a\x3d\x04\x24\x01\xd0\x8b\x0d\x2f\xea\x06\x25\
-\x38\x07\x03\xbe\xd3\x63\x3e\xf7\x25\x09\x93\x16\x71\x2c\xd4\x02\
-\xbf\x5f\xf6\x4b\xd4\xb8\x75\x2d\xbb\x9d\x86\xfb\xcd\xd8\xb2\x6f\
-\xa3\x98\x1e\xdc\xa9\x6b\x48\xa6\x90\xfa\x8e\x17\x7d\xa3\x49\xa7\
-\xfd\x68\x5f\xc8\x5f\x30\x14\xf1\x91\x45\x1c\x0b\x2b\xc5\xff\x60\
-\xe6\x60\x9f\x04\x7a\x6a\xfe\xcd\x77\x13\xe8\x3d\x85\x6f\xf8\x12\
-\x8d\x83\x41\xa4\x79\x17\x29\xd5\x97\x71\xb3\xba\x22\x5a\x7a\x43\
-\xfe\xbd\x49\x87\xfd\x1c\xa5\x99\x17\x2c\xe2\x58\x58\x16\x62\x73\
-\xe4\x4c\xcf\x2a\x9d\xc9\x19\x3b\x4a\xeb\x2d\x5f\x47\x67\xef\x98\
-\xaa\xca\xfe\x97\x78\xcf\xca\x58\x9d\xfb\xec\x50\xc0\x7b\x4f\xc2\
-\x92\xaf\xc3\x1a\x00\xb5\xf0\x54\xc6\x64\x98\x99\x45\x99\x06\x46\
-\xa8\x75\x51\xa6\x5c\xab\xd3\x71\xe9\xcd\x50\x68\x47\x68\x53\x5f\
-\x70\x63\xdd\xe5\xce\x44\xda\x11\xbd\xdb\xdb\xd0\x73\xb3\xa7\x39\
-\xdd\x3b\x1c\xf0\x25\xd2\x8e\x4d\x2c\x25\x6b\x74\x85\xc8\xda\xb4\
-\xdd\xbd\x0d\xfe\x44\xfb\x54\xea\x3d\xbb\x61\xbc\x5e\xed\x71\x56\
-\xc8\x8d\x05\x00\xc2\xe1\xf0\x9f\x51\x9a\x34\xaa\xdc\x41\x70\xb2\
-\x26\xcd\x17\x9a\x1d\x8e\x8b\x6f\x86\x82\x5b\xbd\xaa\xd6\x32\x6d\
-\x46\x0d\xec\x0c\xf6\xcf\xf1\xdd\x49\x89\x1c\x9d\xf4\x3e\xb9\xd9\
-\xd3\x3c\x78\xa7\xb7\xd1\x18\x18\xaf\x0b\xe5\xf3\xea\x16\x5e\x40\
-\x7d\x81\xfa\x4c\xee\x9d\x60\x3a\x77\x80\x2a\x66\x58\xb0\x88\x63\
-\xe1\x29\x71\xfe\x29\xf0\x0b\x73\x9c\x04\x5f\xa0\xd2\x3c\x05\xd5\
-\xa2\xd8\x68\xb7\x5f\x7c\x2b\x14\xda\xe4\xd3\xb4\x39\xc3\xa9\x9b\
-\xfc\x23\x8f\x9d\x6a\x71\xc3\xa2\x36\x89\x21\x0a\x3d\x43\xf5\x0f\
-\x6f\xf5\x34\x47\xef\x3d\x69\x50\x27\x62\x9e\x76\xdd\x10\xeb\x57\
-\xc1\x8d\x81\x4d\x37\x7b\x3b\xa6\x12\x39\x05\xb6\x5b\xaa\x9a\x85\
-\x6a\xf0\x57\x15\x15\xb8\x1e\x60\xd7\x92\xdb\x32\x42\x36\xdb\xfb\
-\x6f\x86\x42\xeb\x03\x36\xdb\xa9\x79\x0d\xab\xbc\xa7\xaf\xc5\x1d\
-\x5b\x94\x38\x9a\x2a\xed\x5b\x3b\x27\x77\x6d\xed\x7c\xee\x45\x4e\
-\xe7\x6c\xb1\xfb\x4f\x1a\xba\x6f\xf4\x34\x27\x7b\x86\xea\x3d\xf1\
-\x94\x73\x83\x34\x59\xb1\x7b\xbb\xa8\x29\x5d\x8f\x1b\x03\x85\xd6\
-\x78\xea\xbc\xbb\xa0\x9f\x9e\x8f\x9c\x96\xc4\xb1\xf0\x54\xe2\x28\
-\xc0\x38\xb3\xa7\x63\x77\x4d\x4b\x9d\xea\xbe\xed\x66\xbd\x4d\x7b\
-\xff\x33\x0d\x0d\xed\x01\xcd\xd6\xb5\x28\x21\x14\xe3\xca\xb6\xc0\
-\xd0\xe1\x5a\xdd\xc3\x78\xcc\x3d\x70\xab\xa7\xa9\xff\xce\xe3\xc6\
-\x42\xdf\xa8\x3f\x98\xcd\xdb\x36\xaf\x44\x5e\x7a\x73\xc5\xab\x4d\
-\xc9\xf4\x3a\x01\x8d\x16\x71\x2c\x2c\x44\x9e\xdf\x05\xbe\x3c\x67\
-\xc3\x29\x16\x8b\x5b\x93\x7e\x4d\xbb\xf8\x56\x43\x43\x73\xc8\x66\
-\x5b\x42\x0d\x29\xb2\x3b\xea\x07\x84\x22\xa4\x73\x35\xee\xc7\x30\
-\x85\xfe\x64\x24\xf0\xe8\x56\x77\xd3\xf8\xdd\xbe\x46\x31\x3a\xe9\
-\x69\xd1\x75\x65\x23\x4b\xf0\x26\x6b\xd2\x1c\x6f\x9f\x4c\xf5\x69\
-\xa6\x79\xc8\x22\x8e\x85\x4a\xa4\xd9\x0d\xfc\x39\x95\x42\xf1\x0f\
-\x02\x5b\x2a\x13\xc6\xa7\xa9\x1f\xbc\x15\x6a\x08\x36\xda\xed\x5b\
-\x96\x73\xde\x0e\x6f\xf4\xaa\xdf\x9e\x39\xf4\xa2\xee\x33\x57\xd0\
-\x52\x0f\xfa\x42\x8f\x6e\xf4\x34\xc7\xba\x07\x82\xae\xc9\xa4\xab\
-\x4b\x9a\x2c\x56\xc4\x43\x36\xa4\xb2\x17\xea\xb2\xb9\x53\x02\xa1\
-\x58\xc4\xb1\xf0\x94\x34\x7b\xa6\x6d\x9c\x39\x2a\x09\xbb\x80\xdd\
-\x73\x8f\xf1\xa8\xea\xe5\x37\x1b\x42\xbe\x16\xbb\x63\x45\xd1\xcd\
-\x5e\x2d\x7f\x7e\x7d\xdd\xd8\x99\x97\x79\xff\x93\x09\xe7\xc8\xdd\
-\xde\xc6\x27\x37\xbb\x9b\x73\x7d\xa3\x7e\x7f\x2a\x67\xdf\x8c\x64\
-\x4e\x0d\x04\x47\x3e\x69\x34\x25\x32\xaa\x1d\x87\x45\x1c\x8b\x34\
-\xe1\xbd\xc0\x5f\x56\x24\xcd\x1e\x60\x67\xf9\x2a\x67\x4e\x19\x7d\
-\x6b\x5d\x68\xa2\xcd\xe1\xdc\x55\x8b\xf3\x0b\x21\x7b\x76\xd6\x0f\
-\x6c\x7c\x95\xfa\xc4\x34\x31\x07\x27\xfc\x3d\x37\xbb\x9b\x46\xee\
-\xf6\x36\xc8\xe1\x68\x5d\x53\xa1\xa8\x6c\x06\x54\x61\xea\x34\xc4\
-\x46\x2c\xe2\xac\x71\xd2\xec\x9f\x26\xcd\xdc\x29\x08\xf6\x53\xaa\
-\xa7\xf6\xf4\x6b\x1b\x17\xbc\xde\x5c\x1f\xdb\xda\xe8\xa9\xf9\xe0\
-\xe4\x16\xff\xf0\xa0\x5d\xd5\xdb\x5f\xe5\xbe\xca\xe5\xd5\xfc\xe5\
-\x5b\x75\x8e\xbb\x8f\x03\x0c\x8c\xfb\x2d\x77\xf4\x1a\xc7\xcf\x57\
-\x24\xcd\x0c\x9b\xc6\x96\x10\x1c\xf1\xfb\xd9\xb3\xdb\x07\xab\x34\
-\xa2\x3f\x95\xf7\xf6\x34\xbb\x63\xaf\x34\x71\x9c\x0e\xc3\x71\x74\
-\xef\x14\x1b\x5b\xa6\xc8\xc6\x84\x45\x9c\x35\x2c\x6d\x8e\x00\x6f\
-\xcc\xd9\x70\x18\xd8\x54\x22\xcc\x21\x5f\x1d\xfb\x76\xd5\xad\xfa\
-\xb5\xc4\x8b\x2e\x7b\x33\xb1\x57\xb1\x9b\xfa\xc9\xd1\x4f\x52\xd1\
-\xe3\x23\x62\x8f\xd7\x54\xea\xdb\x05\x3c\x9a\x52\x2c\x55\x6d\x8d\
-\x92\xa6\x01\xb8\xca\xec\xb9\x34\x8f\x82\xd6\x20\xd8\xef\xaa\xe3\
-\x50\x57\xdd\x0b\xbb\x1e\x21\x64\x6a\x47\xfd\x80\x43\x80\xed\x25\
-\x76\x4b\x06\x9d\x87\x64\x44\x8c\x84\xe2\x24\x21\x36\xa2\x8b\x32\
-\xbb\xcf\x94\x70\x23\xee\xc0\xb5\xb5\xd9\x92\x38\x6b\x14\xdf\x28\
-\x23\x8d\x00\xf7\x21\x95\x6d\x1d\x6e\x5e\xdb\x10\x78\xe1\x17\x23\
-\xa5\xf0\xa6\x0a\xae\xeb\x3e\x7b\x76\xdf\x8b\xf3\x00\xd0\x47\x81\
-\x7e\x92\x8a\x41\x42\x69\x22\x23\x36\x23\x99\xf7\xfc\xe9\xbc\x34\
-\xef\x2b\x01\xc5\xb3\xb5\x54\x7f\xd1\x22\xce\xda\x93\x36\x47\x99\
-\x15\xcc\x19\x3a\xea\x61\xfb\x46\x17\x5e\xa1\x30\x39\x55\xc4\xef\
-\xd3\x50\x35\xf1\x42\xaf\x6b\xaa\xe0\x99\xf2\xd9\xb3\xab\xc4\x4c\
-\xd2\x18\xcf\xa4\x89\x9b\xb8\xd8\x84\x21\xd6\x51\xe5\xec\xd5\x86\
-\x50\xee\xf4\x04\x1a\x77\x7a\x7c\xa5\x19\x1a\x0d\xc3\xb4\x88\xb3\
-\x06\xf1\xe3\x73\x0c\xdf\x23\x6e\x06\x82\xcf\x73\xca\x84\xd4\xb1\
-\x25\x4c\x9c\x09\x13\x77\x1e\x7c\x42\x14\x83\x1e\x2d\xe9\x73\xab\
-\x9a\xa6\x8a\x55\xd1\xe1\xd2\x45\x47\x6b\xcd\x1a\x33\xe9\xa5\xc0\
-\x00\x09\xc5\x24\xa1\xb4\x90\x11\x9b\x28\xf9\x09\x97\x8c\x9c\xcd\
-\xfe\x76\x5f\x7d\xf3\x31\xfb\x8c\x98\xa3\x78\x9a\x61\xcb\xc6\x59\
-\x5b\xd2\xa6\x1e\x18\xa2\x54\x4a\xbd\xa4\x72\xd4\xab\xb4\xff\x64\
-\x63\xd5\x6d\xd8\x24\x89\x3a\x53\xe9\x0f\x98\x62\x2a\x60\xa2\x7b\
-\x4d\xe1\x72\x98\xa2\x51\x81\x4e\x56\x68\xa3\x6c\x0d\x0c\x8d\xda\
-\x14\x63\x69\x53\x71\x48\x92\x18\x3c\x24\x2d\x12\xc4\x15\x0f\x29\
-\xb1\x19\x5d\xd4\xd7\xa0\xbb\x72\x13\x1e\xff\x95\xa8\xc7\x3f\x27\
-\xb1\x62\xcc\xe6\x7b\xcf\x92\x38\x6b\x0b\x5f\x9d\x49\x1a\x00\xd7\
-\xe6\xa5\x4d\x10\x5d\x14\xd4\x45\x55\x73\x57\x74\x56\xd2\xb3\x00\
-\xdd\x6b\x88\x5e\xbf\xa9\x8c\xd5\x9b\x64\xeb\x4c\xa1\xba\x24\x7e\
-\x0d\x3a\x90\xd5\xbd\xc8\x53\x05\xcf\xc3\x26\x67\xa2\x79\x41\x9a\
-\x98\x3c\x26\xc7\x10\x29\x45\xce\x90\x26\x07\x6b\xab\xd9\xd1\xdf\
-\x1f\x6c\x4e\x67\x35\x47\xa5\x6c\xa4\x4c\x4c\xb8\xf6\x59\xc4\x59\
-\x5b\xf8\xfa\xec\x15\x4b\x25\xce\x02\x2f\x9b\x96\x54\x65\x57\x52\
-\x35\xba\x06\x66\x6d\xb3\x4b\xa2\x7e\x53\x0c\xd6\x9b\x4a\xdc\x6f\
-\x60\x7a\xa5\x70\xdb\x4d\xd1\x2c\x4a\x45\xa8\x9e\x05\x5c\x26\x72\
-\x1e\xb5\xc9\x99\x98\x79\x68\x02\x9d\x87\xa4\x44\x92\x84\xe2\x25\
-\x29\xb6\x60\x88\x8d\xc0\xaa\x45\x1a\xe8\x8a\x7a\xe5\x49\xb0\x75\
-\x93\xae\x28\x9d\x95\x3f\x1c\xca\x75\x13\x71\xdc\x22\xce\xda\x51\
-\xd3\x0e\x00\x07\xca\x56\xaa\xe0\xec\x5a\xfd\x89\x9b\x0b\x82\xd0\
-\xb8\x2a\x43\xe3\xaa\x51\xa6\xcc\x29\x90\xf7\x1a\xa2\xbf\xde\x14\
-\x13\x7e\x53\xe4\x9a\x74\x8a\x64\xc4\xdb\xa4\x14\x88\x8b\x36\xb2\
-\x62\x23\xf0\xa2\x02\x40\x65\xca\xe1\x3e\x3f\xe4\x6f\x38\x2d\x17\
-\x88\x9e\x9e\x54\x3d\x0a\x58\x5e\xb5\xb5\x84\xbf\x3b\xc7\x29\xb0\
-\xce\x8e\xb0\x8b\x97\x75\x3d\x52\x91\xf2\x49\x5d\x41\x1f\xd9\x94\
-\xc8\xdb\xb7\xa6\x0a\xbb\x3d\xba\xd9\xf0\x92\x5e\xc9\xf8\x48\x5d\
-\xe8\x41\xdc\xe9\x39\xbb\xc8\x7e\xa9\x98\xe2\xda\x67\x11\x67\x6d\
-\xe1\xf3\xab\xa5\xa6\x55\x09\xc3\x66\xca\x07\xad\x39\x63\x6c\x5b\
-\x22\xe7\xdc\x92\x2a\x6c\x75\x19\x72\x2b\xb0\xf5\x65\x76\x8a\x21\
-\xc4\x83\x27\xc1\x16\x57\x51\xb5\x1d\x59\x5c\x72\x6a\xd7\x25\xe2\
-\x84\x45\x9c\xb5\xa3\xa6\x79\xa8\x30\x57\xe6\x2a\x13\xa7\xe8\x34\
-\xe4\xfd\xd6\xac\x1e\xdd\x9e\xca\x79\x36\x25\xf5\x6d\x0e\xd3\xdc\
-\x01\xec\x78\x55\xfa\x25\xaf\xd9\xde\xed\xab\x6f\x3e\x68\x0a\xa5\
-\xaa\x2c\xd1\xa8\xe6\x7e\xa6\xd7\x5a\xc4\x59\x1b\xd8\xce\xec\xe4\
-\x67\x01\xb2\x58\xd3\xa1\x88\xbc\xcb\x90\xf7\x3a\x32\xc5\xa9\xed\
-\xc9\x7c\xdd\x86\x74\x71\xbb\xcd\x94\xbb\x5f\xd1\xfe\x28\x4e\xba\
-\xeb\xde\x1f\xf7\x06\x4e\x2f\xe1\x98\x58\x42\x38\xf7\x5b\xc4\x59\
-\x5b\xc8\xcf\xb5\x30\x60\xe4\xd7\x27\x69\xf8\x72\x00\xf7\xb6\x65\
-\x49\x9e\x8c\x5b\x97\xf7\xd6\x65\x8b\x89\xed\x89\x5c\x7d\x57\xba\
-\xb8\x5d\x5d\x20\x64\xe5\x55\x81\x84\x91\x01\x7f\xe3\x44\xc6\xe1\
-\x3a\xbd\xa4\x0e\x54\x6c\x37\x25\xe2\x94\x45\x9c\xb5\x85\xdb\x94\
-\x0a\x71\x94\x8d\x74\xca\xa2\x64\xfc\xff\x9d\x22\xf8\xb9\x3a\x7c\
-\xaf\xb9\x17\x6c\x40\x48\x92\x1e\x43\xde\xef\x4a\x17\xd2\xdb\x93\
-\xf9\x50\x47\xba\xb8\x4d\xad\xf1\xf8\xc9\xea\xdb\x33\xea\x47\xbd\
-\xa1\x96\x0e\x5d\x51\x97\x2c\x09\xa3\x8a\xdb\x53\xde\x1f\x56\xe4\
-\xc0\x5a\xb1\x73\x2a\x17\xe2\x98\x46\xdd\x71\x0f\xf5\x6f\xf9\x9e\
-\x29\x74\x66\x56\x62\x1f\x2b\xc8\x5d\x7e\x79\x61\x7b\xbc\xd0\xd4\
-\x96\x2b\x6e\x55\x40\xfd\xb8\xde\x7f\xda\xee\x3c\x3f\x18\x68\x3c\
-\x29\x11\xcb\xb9\x87\xe8\x7d\x7b\x73\x40\xce\xb8\x7f\x4b\xe2\xac\
-\x1d\xfc\x57\xe0\x73\xcc\x53\xa5\x32\xf1\x7e\x1a\x91\xd0\xd9\xb1\
-\x59\x63\x9f\x62\xb0\xc3\x6e\xa0\x08\x04\x39\xce\x7c\x9c\x6f\x5a\
-\x42\x7a\xcc\x17\xb8\x19\x73\xd5\x2d\xfb\x3e\x72\x8a\xed\xb6\x84\
-\x32\xd5\xce\x2a\xba\xbe\x46\x10\x89\x44\xfe\x1c\x38\x0a\x3c\x9c\
-\x6f\x9f\xf8\xed\x3c\x3b\x5d\x69\x62\x23\x79\xfa\xa7\x3e\xfe\x9a\
-\x88\x14\x4a\x4f\x5f\xb0\x65\x34\xe6\xaa\x3b\xb6\x92\x76\x26\x54\
-\x8f\x7f\xf6\x3a\x8b\x38\x6b\x8b\x3c\xb7\x29\x4d\x22\xf5\xcb\x40\
-\xbc\xd2\x3e\xa3\x51\xd8\x78\x44\xd2\x97\xd1\xb9\xd8\xa7\xc7\x53\
-\x05\xf9\x0e\x90\xfb\xb8\xdd\x6b\x41\xd5\x2e\x3e\x6a\x68\x6b\xca\
-\x69\xf6\x95\x86\xe7\x8c\xa6\x84\x7d\xcf\x5c\x9b\xcf\xb2\x71\xd6\
-\xaa\xcd\xe3\x02\x7e\x0d\xf8\xfe\x99\xeb\x5b\x9a\xe0\x9f\xff\x43\
-\x88\x4e\xc2\xa3\x2b\x24\xf4\x02\x75\x36\x95\xc9\x0d\x01\xf5\x46\
-\x83\x5b\xd9\x2e\x44\x8d\xe7\x2f\x58\x05\x1f\x40\xcc\xe9\x79\x67\
-\xb4\x2e\x54\x13\x15\x33\xa3\xd8\xce\xf7\x69\xc1\x33\x96\xc4\xb1\
-\xf0\x54\xfa\x64\x29\x4d\xe4\x51\x86\x91\x31\x18\x1c\x81\x50\x10\
-\xf6\x9c\xc2\xee\xf5\xf3\xb0\x68\x10\x7c\x10\x35\xce\xbe\xdf\x5f\
-\x6c\x78\x1c\x33\xde\xd3\x4d\x79\xeb\x15\xb5\x67\x26\x06\xfd\x0d\
-\x37\x6b\x45\x1a\x80\xa8\xea\x0d\x55\x5a\x6f\x11\x67\x6d\xa3\xe2\
-\x00\xce\xd5\xe9\x49\xfe\xdc\x2e\x9c\xbb\x8f\xd3\x5e\xdf\xc4\xcd\
-\xe9\x17\x53\x1b\x4a\x98\xaf\x5f\x1a\xd0\x77\xdf\x1a\xd5\xef\x64\
-\x0a\xf2\x5d\xa0\xf0\x4a\x88\x19\xa1\xdc\x7a\x1c\x6a\x2b\xa6\x1c\
-\xee\xfd\x35\x6c\x76\x30\x23\xec\xbb\x2c\xe2\x58\x98\x8d\x8a\xa1\
-\xd1\x57\x6f\xc0\x53\x0d\x5e\xd5\x70\xef\x38\xc2\x96\xe6\x4e\x2e\
-\x97\x39\x12\xf2\x72\xe7\x87\x23\xfa\x89\x2b\x03\xc5\x58\x34\x2b\
-\xcf\xc9\xd2\x38\xd1\x4b\x41\xd6\x6e\xbf\xd0\xdd\xd8\xbe\xb5\xa8\
-\x6a\xad\xb5\x6c\x37\x2d\xec\x8f\xe4\x3c\xe5\xe6\x2d\xe2\xac\x6d\
-\xfc\x06\x15\xe6\xc0\x9c\x8a\xc1\xe3\xbe\x19\x86\xb0\xc0\xb9\x79\
-\x1f\x07\x3a\x37\xf3\xee\xec\x7d\xf3\x26\x4d\xf7\xc6\xf5\xb3\xef\
-\xf7\x17\xfd\x4f\x62\xc6\xbb\x86\x29\xef\xbe\x48\xce\x4c\x78\xfc\
-\xef\xf6\x05\x5a\x4e\x4b\x44\xcd\xf3\x23\x26\x34\xef\xbc\x49\x75\
-\x96\x73\xc0\x72\x12\xfc\x24\xf0\x8b\xb3\xd7\x7f\xe1\xb3\xf0\xd6\
-\x5c\x4b\x41\x8e\xf6\x71\xe1\xd1\x8d\x85\xc7\x76\xea\x5d\xe2\xe6\
-\x86\x80\x9a\x72\xd9\xc4\x11\x56\x69\xac\xd0\x94\xf4\x0f\x04\x5b\
-\x32\x59\x9b\x7d\xdb\xaa\xd8\x4b\x82\x27\xf7\x6d\xcd\xeb\xe7\xdb\
-\x6e\x49\x1c\xcb\x49\xf0\x1f\x81\xde\xd9\xeb\xf7\x54\x8e\x61\x16\
-\xcd\xeb\x38\xb3\xe3\x35\x2e\x2c\xd4\xe6\x54\x56\xee\xb9\x36\xac\
-\x1f\xbf\x32\x54\x1c\x9f\xcc\xca\x73\xc0\x64\x2d\xaf\x59\x57\xd4\
-\xcb\x3d\x8d\x1d\xbe\xd5\x22\xcd\xb4\x9a\xf6\x78\xa1\xed\x16\x71\
-\x2c\x89\xb3\x11\xe8\x9a\xb9\x6e\x7d\x47\xc9\x2d\x3d\x1f\x82\x4d\
-\x9c\xde\x77\x8a\x77\x85\xc0\x58\xa8\xed\xbc\x4e\xeb\xdd\x71\xfd\
-\xec\xc5\xfe\xa2\xab\x3f\x6e\xbc\x6d\x48\xf9\x70\xa5\x82\x20\xe1\
-\x74\x9f\xeb\x6e\x68\x3f\x6c\x28\xca\xaa\x16\x80\x9b\x50\xbd\x1d\
-\x16\x71\x2c\x2c\x84\xef\x9b\xbd\xe2\x58\x15\xc9\xca\x5e\x3f\x27\
-\x0e\xbe\xc1\x55\x45\x59\x7c\x70\xd4\x90\xb8\xfa\xe2\xe6\xa9\x8b\
-\xfd\xfa\x96\x7b\xe3\xfa\x47\x39\x5d\x5e\x02\xcc\x25\x5e\x67\x6c\
-\xb8\x2e\x74\x65\xb8\xae\xe1\x2c\xab\x30\xf7\x67\xb9\x9a\x26\xba\
-\x73\xc2\xb6\x79\xa1\x7d\x2c\x1b\x67\x6d\x4b\x1b\x1b\xd0\x4d\xa9\
-\xb4\x13\x00\x9a\x06\x3f\xf7\xcf\xc1\x55\xe5\x1c\x69\x7a\x81\xeb\
-\x57\xbf\xc3\x46\xbd\x88\x6f\x29\xe7\x76\x69\xf4\x6f\x0c\x69\xdd\
-\x01\xbb\x38\x80\xc0\xbf\xa0\x3d\x23\xc4\xfd\xbe\x60\x8b\x3b\xaf\
-\xda\x3a\x5f\x44\xbf\x24\x55\xe7\xb9\x41\xd5\x7f\xd6\x92\x38\x16\
-\xe6\xc3\x0f\xcd\x24\x0d\xc0\xbe\x5d\xd5\x93\x06\x40\xb3\xb3\xef\
-\xf0\xa7\x19\xb0\xd9\x97\xe6\x8e\xce\xea\x74\xde\x1e\xd5\xcf\x5e\
-\x1c\x2c\x6a\x03\x71\xf3\x82\x29\xe9\xa9\xa8\xee\x69\xf6\x77\xba\
-\x1b\xda\xd7\xbd\x28\xd2\x00\x8c\x2b\x9e\xae\xc5\xf6\xb1\x24\xce\
-\xda\x95\x36\x01\xe0\x3e\x50\x66\xcd\xfc\xf8\xd7\x61\xfb\xe6\x2a\
-\x1b\x29\x72\x9b\x2c\x13\x18\xec\xcc\xe5\xb8\xf7\xd1\x47\x74\x19\
-\x06\xcb\x7b\xc1\x05\xb2\xc1\x2d\xae\x6e\x08\xa8\xd2\xae\x8a\xc3\
-\x94\xb2\x34\x2f\x2e\x31\x4b\x73\xe5\x6a\x1a\xe2\xde\x7d\x7b\xd3\
-\xa2\xb3\xcc\x59\x69\x05\x6b\x17\x5f\x99\x4d\x9a\x80\x1f\xb6\x2e\
-\x16\x12\x69\xd0\x4b\x96\x5e\x8a\x74\x21\x9f\x4f\xe4\xee\x74\xd0\
-\xd0\xd6\xc1\x07\xc3\x43\x64\xf5\xe2\x32\x0a\x70\x48\xc4\x44\x5a\
-\x1e\x9e\x48\xeb\xb8\x6c\xf4\x06\x5a\x03\x77\x8a\x2f\x98\x34\x00\
-\x09\xc5\x31\x4a\x29\xd5\xdc\x22\x8e\x85\x8a\x98\x33\xd9\xed\x6b\
-\x07\x40\xa9\xa4\xbc\x9b\x8c\x91\xe3\x2e\x05\x1a\x31\xd9\xc9\x2c\
-\x2f\xdc\x53\x99\xd1\xde\x8a\x2b\x93\xa1\x25\x19\xe7\x66\xa1\xc0\
-\x9e\xe5\x5e\x58\xb6\x48\x57\xb6\x2f\xd6\x25\x06\xe3\x49\x4f\x7b\
-\xe8\xbc\xb3\xa9\x6e\x03\x88\x75\x2f\xa2\x53\xa2\xaa\x77\x53\x55\
-\x2a\xaa\xf5\xfe\xac\x59\xcc\x19\xa9\x39\x7a\xa8\xcc\xe8\x2f\x6a\
-\x06\x1f\x50\xc0\x89\xc1\x7e\x58\x3c\xa1\x4d\x55\xd8\x1b\xf0\x73\
-\x11\xd8\x9f\x4a\x72\x39\x9f\xe3\xc8\x4a\x2e\x50\x1a\xd2\x97\xea\
-\x9b\x38\x93\xea\x9f\x30\x9d\x21\xdf\x07\x9e\x8e\x90\x26\x34\x75\
-\xd5\xd2\xb5\x0d\xc4\xed\x82\x50\xab\x98\xdb\x54\x18\x16\x71\xd6\
-\x2e\xca\xe6\xd4\xd8\xd4\x05\xc1\x00\x3c\xe9\x06\x33\x0f\xeb\x9a\
-\x01\xc1\x89\xa5\x36\xda\xd4\x48\x7b\x3c\x81\xe2\xf5\x71\x50\x51\
-\x78\x37\x9b\x59\x7a\x1b\x15\xd4\x38\x25\x37\x91\x7c\x2d\x37\x91\
-\x44\x73\x3b\x1e\x79\xd7\x37\x0c\x6b\x1e\xe7\x21\xc0\x5d\xcb\x0e\
-\x49\xaa\xce\x89\xc5\xf6\xd1\xa5\xab\x67\x2c\xdf\x95\xb5\xbc\x6a\
-\x6b\x17\x65\x5e\xac\xa6\x90\x93\xec\x08\xac\xaf\x87\x0d\x2d\xa0\
-\x8a\xe5\xcd\x3c\xa0\x08\x3a\x9b\x9b\x78\x1f\x50\xdd\x1e\x4e\x78\
-\xbc\x0b\x47\x19\x2c\x15\x7a\x26\xbf\x39\x76\x77\xf0\xd4\xe4\x47\
-\x8f\x0b\xb9\x68\xe2\x1c\x52\x0e\xd6\xca\x2f\x10\x55\x3c\x0b\x45\
-\x22\x98\xb1\x62\xfb\xf9\xb1\xfc\xa6\x36\x50\x77\x59\xc4\xb1\x88\
-\x53\x32\x8a\xf5\x16\x3e\x18\xd8\xc8\x64\x72\xe5\x63\x8b\x81\x3a\
-\xf6\x2b\x4a\x29\xcc\xc6\xe9\xe2\xb4\xcf\x5f\x5b\xf2\x00\x98\xba\
-\x19\x48\x3d\x1e\x3f\x3b\x71\xad\xa7\x25\xd5\x37\x71\x51\xea\xc6\
-\xf5\x15\xa9\x69\x42\xdc\x2c\x0a\xb5\xa5\xb2\x94\x71\x3c\x1e\xc9\
-\xed\xb8\x93\x31\xea\xcf\x30\x3d\xdb\x83\x45\x1c\x8b\x38\xa5\x97\
-\xc3\x30\x90\xce\x16\xae\x45\x0f\x71\xbb\x6f\xc5\x15\x3e\xfd\x1d\
-\x6d\x3c\x4b\x76\xb3\xdb\x39\xed\xaf\xe7\x5d\x16\x09\xd1\x59\xa6\
-\x1a\xa7\xe6\xc6\xe2\xc7\xa2\x1f\xf5\xee\x8b\xdf\x1d\xb8\xaf\x67\
-\xf3\xef\x50\xa9\x8e\xdc\x22\x88\x0b\x57\xac\xb2\x94\x69\x3b\x3f\
-\x96\xdf\xd2\x6a\x52\x5e\x52\xca\x22\xce\xda\x45\x59\x10\xa3\xa1\
-\x97\x22\x60\x84\x6a\x67\x44\x1c\xe2\xed\x87\xad\x86\x84\xc4\x72\
-\x1b\x77\xbb\x78\xdd\x61\x7f\x4e\x4e\x4d\xe3\x44\xa0\x9e\xab\x42\
-\xac\x5e\xfd\x82\x62\x3a\xbf\x2d\x76\x7b\xe0\x64\xf4\x7a\x6f\x2a\
-\x1f\x4d\x9e\x43\xca\xd1\x6a\x05\xce\xa4\xe6\xd9\x39\x4b\xca\xf4\
-\x8e\xe4\x76\xdc\xce\x18\xc1\x67\x52\xc6\x22\x8e\x05\x28\x45\x44\
-\x3f\x1b\xfd\xd6\xf5\x72\x61\x50\x70\x6e\x50\xdf\x1f\x3e\x90\x2a\
-\x9a\xda\xcd\x65\xb6\xaf\x75\xb4\x13\x9d\xb9\x42\x55\x79\x2d\x10\
-\xe4\xbe\x22\x96\x4f\xc8\xaa\x84\x50\xd1\x08\x25\x1f\x8f\x9d\x8d\
-\x5e\xeb\x09\xa6\xfb\x27\xde\x93\xa6\xb9\x60\xaa\xb7\x81\xb8\xa1\
-\xa3\x34\x3c\x95\x32\xf1\x62\xeb\xf9\xb1\xfc\x96\x66\x13\x75\x5e\
-\x97\xba\x45\x9c\x35\x8a\x48\x24\x92\x07\x06\x9f\x4b\x9c\xb9\x5a\
-\x54\x56\x77\xb5\x5d\x18\x3c\xb2\x73\x34\xdb\x70\x8e\x0a\x09\x6f\
-\x8b\xc1\xa6\x71\xc4\xe7\xe5\x5a\xd9\x0b\xa7\xb0\xcf\x1f\x62\x44\
-\x51\x56\x3f\x63\x54\x4a\x6c\xd9\xd1\xf8\xeb\xd1\x6b\x8f\x77\xc7\
-\xef\x0f\xdd\x36\x72\x85\x77\x81\xe2\xec\xfd\x62\xaa\x3b\x35\x2d\
-\x65\x9e\x8c\xe4\x76\xdc\x4a\x1b\xa1\x33\xc0\x82\x85\xd8\x2d\xe2\
-\xac\x6d\xdc\x7d\x2e\x71\xe6\x0b\x56\x16\xea\xad\x89\xad\x67\xaf\
-\x8d\xef\xba\x67\x22\xfa\x97\x7a\x82\xd6\x16\xbc\xcc\x8a\x84\x56\
-\x04\x5b\x03\x21\x72\xaa\x42\xff\x8b\xba\xd1\x62\x32\xbb\x6b\xea\
-\x56\xff\x89\xc9\x1b\x4f\x26\x8b\xb1\xd4\x39\xe0\xa9\xeb\xb9\x38\
-\xa9\xba\x77\xc6\xf5\xd6\xf3\x63\xf9\x2d\x8d\x26\xea\xde\xaa\xc4\
-\xa9\xf5\xee\xac\x69\xdc\x01\xde\x9a\x4f\xe2\xcc\xc4\x54\xce\xbf\
-\xfb\xc2\xc0\x6b\xa9\xc3\xcd\x37\xdf\xf5\xda\x32\x55\x8f\xcd\x28\
-\x82\xad\x0d\x21\xde\x9e\x88\x72\xaa\x8c\x8e\xd0\x19\x08\x31\x16\
-\x8f\xf1\x60\x59\x21\x3a\xcb\x84\x59\xd0\x9b\xe3\x8f\x46\x9b\x85\
-\x18\xcb\xbb\x5a\x02\xef\xc8\x86\x60\x6a\x20\xbe\x5e\xd5\x0d\xb5\
-\x41\xe8\xf1\x9b\xa6\x6e\xe8\xe8\xa6\x21\x0d\xc3\xc4\x30\x91\xba\
-\xa9\x60\x18\x2a\x86\xb4\x49\x69\xda\x31\xa5\x43\x4a\xe9\xb2\x88\
-\x63\x49\x9c\x8a\x36\x4e\x45\x5b\x40\xaa\xde\x4b\x23\xfb\x4f\x74\
-\xfa\x86\xde\xdf\x1a\xe8\xdd\x09\x0b\xa7\x03\x3c\x45\x43\x90\xed\
-\xd1\x29\xd2\xd2\x9c\x53\x7e\xb7\xc9\x1f\xc0\x99\x88\x73\xa3\x58\
-\x60\xef\x8b\xba\x69\x29\x19\x1a\x35\xe4\xc3\xdb\x3d\xb1\x86\x58\
-\x77\x71\x43\x87\x67\xbc\x8d\x25\xe6\xf8\x58\xaa\x9a\x45\x9c\x69\
-\x1b\xa7\xfa\xbc\xb2\xfe\x64\xdb\xf1\xf7\x86\x0f\xa6\x0b\xa6\x56\
-\xed\xd8\x49\x63\x7b\x4b\x79\x95\x9c\x19\xa8\xab\xf3\xb3\xd5\xe1\
-\x98\x77\x7b\x6d\xc8\x02\xa3\xe3\x06\xe7\xcf\x67\xc5\xcd\x6f\xa6\
-\x94\xd6\x73\x19\xe5\xcc\xb8\x2e\x76\x15\x8d\x4c\xbb\x61\xe6\x6f\
-\x2c\xb5\x3d\x8b\x38\x96\xaa\x56\x22\x8e\xb1\xb4\x21\x96\xac\xee\
-\x6c\x7b\x7b\xe8\xc8\x9e\x91\x4c\x75\x8e\x03\xaf\x87\x63\x36\x1b\
-\x43\xf3\x6c\x76\x7a\xeb\x38\xe8\x72\xf3\x4e\x8d\xc9\x32\x3e\x61\
-\x70\xe1\x42\x56\x5c\xff\x66\x52\x69\xfc\x5f\x19\xe5\xcc\x88\x2e\
-\xf6\xcc\x96\x2e\x93\x85\xee\x25\x87\xee\x58\xf9\x38\x6b\x1c\xe1\
-\x70\xf8\x21\xb0\xd9\xe9\xb2\xf3\xd9\x2f\x1e\x5e\x56\x1b\xf5\xce\
-\xf8\xed\xfd\x8d\x77\xbc\x0a\x72\xfd\x42\xfb\xe5\x0b\xbc\xf7\xf8\
-\x09\xaf\x2f\xb8\x4f\x8e\xf3\xa9\xe4\x0a\x66\x48\x90\x4c\x46\x4d\
-\x6e\xdf\x2d\x08\xdf\xa0\x2e\xf6\x56\x27\x1c\x04\x5d\xbe\x53\x63\
-\x42\x28\x4d\x96\xc4\xb1\x50\x2d\xfe\xa0\x5a\x1b\x67\x01\xc7\xc1\
-\xae\x0b\x83\xaf\x35\x24\x8b\x9e\x05\x25\x86\xc3\xce\x71\xb7\x8b\
-\xdb\x0b\xee\xe3\xe4\x8c\xcf\xcf\xf9\xa5\x5e\x42\xcc\xe0\xed\xf7\
-\xb2\xe2\xda\x37\xd3\x4a\xdd\x5f\x66\x94\x53\x83\xba\xd8\x5f\xfd\
-\xfb\x2d\x89\x25\x9f\x34\x58\xaa\x9a\x85\xa5\xe0\x2f\x97\x6a\xe3\
-\x54\x74\x1c\x98\xaa\xe7\x83\x91\x7d\x27\xef\x4f\x6d\xb8\x08\xc4\
-\xe6\xfb\xb4\xb7\xb7\x2d\x5e\xa4\xc3\x6e\xe7\x4c\x15\x21\x3a\xf1\
-\xb8\xc9\x3b\x17\x73\xe2\xca\x37\x93\x8a\xf7\xdb\x19\xe5\x54\xbf\
-\x2e\x0e\x4a\xb9\x3c\x4f\x71\x52\x0c\x2b\x98\xa6\x45\x1c\x0b\x55\
-\xa3\x19\x40\x4a\x89\x69\xae\x5c\x6d\x1f\x48\xb5\x1e\x7b\x77\xf8\
-\x50\xb6\x20\x6d\x1f\x55\xda\xae\x2a\xec\xa9\x0f\xf0\xfe\x62\xed\
-\xcc\x13\xa2\x93\x4a\x98\xbc\xfb\x41\x5e\x7c\xf0\x3b\x49\xc5\xf5\
-\x67\x69\xe5\xe4\x93\xa2\x38\x2c\x59\x5e\x24\x77\x19\xf1\x65\x81\
-\x74\x66\xa2\xea\xfd\x2d\x77\xb4\x85\x67\xc1\x8b\xba\x6e\x60\xb7\
-\xaf\xfc\x95\xc8\xe9\x8e\xd6\xb7\x07\x0f\x37\xef\x0c\x76\x9f\x6b\
-\x75\x8f\x9d\x60\xd6\x8b\xdd\xd4\x48\x47\x2c\x4e\x5e\x4a\x16\x8c\
-\x26\x9d\x0e\xd1\xf9\xe8\xd1\x14\xb9\xc7\x39\xa1\xf5\x16\xc4\x6e\
-\x43\xd6\x20\xbf\x67\x1e\x24\xc4\x20\x1e\x2a\x9b\x39\xc5\x6c\x86\
-\x42\x3a\x49\x3e\x55\x5a\x2c\xe2\x58\xd8\x53\x6b\xe2\x94\xcc\x06\
-\xa1\xdc\x89\x6e\x3e\x3b\x94\x6e\xbe\x7b\xa0\xe1\x96\x4b\x11\xb2\
-\xeb\xb9\x29\x4e\x67\x73\x13\xe7\x47\x46\x17\x77\x02\x5c\x2d\x12\
-\x9f\x74\x71\x06\x97\x64\x9d\x94\x19\xdd\x20\x55\xd4\xf1\x14\x4d\
-\x21\x74\x1d\x8a\x06\x14\x4d\xd0\x0d\x81\x34\x57\x48\x78\x23\x4e\
-\x21\x9f\x42\x18\x0a\xf9\x74\x92\x42\x2a\x41\x7e\x9a\x2c\x72\x96\
-\xd7\xd1\x22\x8e\x85\x67\x12\x67\xa5\x76\x4e\x25\xc4\x72\xbe\x1d\
-\xe7\x87\x8e\xa6\x0f\x35\xdd\x7a\xbb\xce\x96\x7a\x16\x3d\x10\xa8\
-\xe3\xc0\x78\x94\xa8\xa1\x13\x9a\xef\xd8\x0f\x0b\x9c\x9b\x34\x38\
-\xfb\xcc\xae\x10\xb8\xed\x1a\x94\xb8\x2d\xe7\x18\xf8\x86\x09\xba\
-\x51\x5a\x8a\xa6\xa0\x02\xb1\x0a\x14\xa5\x46\x11\x85\x02\xa5\xa8\
-\xb5\x22\x50\x94\xcf\xfe\x1f\x34\x3e\x98\xdb\x74\x39\x74\xa0\xcf\
-\x72\x47\xaf\x61\x84\xc3\x61\x1f\x33\x52\x07\xce\xbc\xb5\x97\x40\
-\xd0\xbb\x6a\xe7\x6b\xf7\x8e\x5e\xda\x1e\xec\xde\x8a\xa4\x1e\x20\
-\x93\xe3\x42\x5f\x3f\x15\x2b\xd9\xdc\x2c\x70\x6e\x48\x7f\x4e\x9a\
-\x5a\xc0\xd4\xc9\x17\x33\x38\xf4\x1c\x14\x33\x90\x9d\x2a\x2d\xb2\
-\xb2\x0b\xc2\x04\xce\x03\xef\x51\xca\x5d\x7a\x3c\xbd\xf4\x47\x22\
-\x11\xab\xe6\xc0\x1a\x47\x79\x0e\xca\x2a\x48\x9c\x99\x18\x4c\x35\
-\x1f\x9d\xc8\x05\x46\x8e\x34\xdf\xf8\xd0\xa1\x14\x0f\xb8\x9d\x9c\
-\x70\x38\xe8\xce\xe7\x29\xab\x2c\x73\xb7\xc0\xf9\x1a\x91\xc6\x04\
-\xfa\x80\x47\x85\x14\x46\x7a\x8c\xcf\x9a\x05\x90\x26\x08\xb3\xe4\
-\x19\xab\x40\x9a\xdb\xc0\x6f\x02\xbf\x15\x89\x44\x06\x3e\xb1\xce\
-\x81\xe9\x32\xae\x9d\xc0\x46\xa0\x03\xf8\xab\x48\x24\xd2\x6f\x71\
-\xa2\x2a\x94\xc5\x87\x2d\x35\x7a\x60\x39\xc8\xeb\x8e\x96\x77\x06\
-\x8f\x34\xef\x08\x75\x9f\x6f\x73\x8f\x1e\xef\x68\x63\xaa\x7b\x46\
-\x4a\xdd\xc3\x02\x6f\xf7\xe9\x9c\x5e\x06\x39\xba\xa5\x64\xd4\x2c\
-\x92\x90\x45\x0c\x0a\x68\x46\x9e\x00\x92\xf5\x12\xf6\x08\x68\x76\
-\xbb\x78\x96\x2c\x20\x81\xb1\x47\x73\xda\xfa\xd5\x48\x24\x12\xae\
-\xe6\xa4\xda\xc7\x90\x28\x02\x38\x0e\xfc\x4d\xe0\xbb\x28\xd5\xf8\
-\x52\x67\x2a\xbb\xe1\x70\xf8\x02\xf0\x5b\xc0\x37\xa6\xe7\xba\xb4\
-\x50\x05\x71\x56\x5b\xe2\xcc\x80\xb8\x1b\xdd\x74\x66\x28\xd5\x78\
-\xef\x60\xd3\xed\xa0\xcf\x27\xaf\x26\x93\x1c\x7a\xac\xf3\x6e\x8f\
-\xce\x09\xe6\x06\x5c\x96\xc8\x21\x19\x30\x4d\xa2\x14\xc9\x9a\x45\
-\x30\x0b\xb8\xcd\x22\x41\x21\xe9\x02\xde\x60\x9e\xe1\x95\x4a\xd1\
-\x9b\xa9\x09\x28\x96\xe7\xa2\x8e\x00\x3f\x55\xf5\x0d\x7c\x5c\x6c\
-\x9c\x70\x38\xbc\x17\xf8\x3a\xf0\xe5\x69\xc9\x52\x0d\xee\x02\x5f\
-\x89\x44\x22\xd7\x2d\x8e\x54\xec\xd3\xf3\xf0\xfc\xeb\x7e\xe0\xb5\
-\xcd\xac\xdb\xd0\xf4\x42\xaf\x41\x15\x66\x66\x5b\xe0\xe1\xf9\x5b\
-\xfd\x51\xff\x9d\x02\xad\x48\x26\x4c\x9d\xb8\xd4\xd1\xcd\x22\xaa\
-\x2c\x50\x27\x75\x9a\x91\x74\xd4\xea\x43\x6f\xe8\x30\xf2\x80\xd9\
-\x5e\xb8\xef\x8b\x44\x22\xbf\x57\x6d\x1b\xda\xc7\xe0\xe1\x9e\x02\
-\x7e\x06\xf8\x6b\xcb\x38\x7c\x07\xf0\x41\x38\x1c\xfe\xdb\x91\x48\
-\xe4\x77\x2c\xaa\xcc\x41\x59\x01\x8a\xa9\x44\x9c\x3d\x4d\x9d\x34\
-\xb6\x94\xb2\x05\x9c\x6e\x5b\x4c\x51\x14\x7d\x81\x8f\x77\xd9\x6f\
-\x21\x66\x6f\x96\xa2\x9a\x63\x7b\x63\x75\xf6\x6b\x97\xff\xaa\x4d\
-\xe4\x8d\x2e\x60\xc3\x6a\xdf\x74\x6c\x78\x0e\x69\xfe\xe7\x52\x48\
-\xf3\xca\x4a\x9c\x69\x75\xec\xbb\xa6\x09\xb3\x60\x50\x60\x43\xc3\
-\x0e\xf2\xf9\x24\xa9\xd4\x20\x0b\xdc\x4b\x1c\xd8\x17\x89\x44\x9e\
-\x58\x5c\x79\xd6\xc7\x75\xd3\xfd\xf2\x0c\x5b\xf6\xb5\x60\x77\xa8\
-\x6c\xd9\xde\xc1\xde\xc3\x5d\x2f\xe2\x32\xf4\xf7\x06\xfb\xde\xfb\
-\xcb\xde\x07\xa7\xdb\xee\x4d\xc6\x15\x43\xfa\x57\xfb\x84\xb9\x14\
-\x4c\xf4\x96\xad\x9a\x02\x76\x46\x22\x91\x91\xa5\xb4\xa3\xbd\x62\
-\x0f\x53\x05\xfe\xd6\x34\x61\x76\x2f\xb4\xaf\xcb\x15\xc4\xe7\x6b\
-\xa3\xb9\x79\x0f\x3e\x5f\x3b\x86\x51\x20\x1e\xef\x23\x16\x7b\xcc\
-\xd4\x54\x2f\xf9\x7c\xd9\x3b\xe1\x07\x7e\x2b\x1c\x0e\x9f\x8a\x44\
-\x22\x96\xff\xbd\x84\x39\xcc\x70\xb8\x34\x14\x45\xd0\xfd\x70\x90\
-\xb1\xd1\x78\xe1\xcd\xbf\xbe\xcf\xbe\x5a\x27\x37\x25\x93\xdf\xbc\
-\x7f\xbd\xef\x41\x74\xfc\x34\x60\x2a\xa6\xac\x5b\xed\x1b\x96\x12\
-\x62\x73\x13\x1b\xfe\xc9\x52\x49\xf3\xca\x10\x27\x1c\x0e\x3b\x80\
-\x1f\x04\xfe\x19\x25\xef\x58\x65\xf1\x28\x14\x3c\x9e\x26\x7c\xbe\
-\x56\x34\xad\xe4\x1e\x31\xcd\x92\x27\x48\x55\xed\x04\x83\x9b\x09\
-\x06\x4b\x73\x54\x74\x77\xff\x05\xa3\xa3\x65\xa6\xcd\x09\xe0\x14\
-\xd4\xbe\x38\xde\xc7\x14\x65\x2a\x91\xcd\xa1\xa2\x28\xe2\x99\xca\
-\x95\x4a\xa6\xed\x7f\xf1\x47\xd7\x2e\x9e\xfd\xdc\xbe\xdd\x36\x9b\
-\x5a\xd3\xc1\x9d\xa2\x61\x3c\xf8\x95\x8f\x2e\xb9\xa7\x72\x99\xfd\
-\xd3\xab\x92\x48\x56\x5d\xda\x24\xc6\x41\x2f\x94\xad\xfa\xcb\x48\
-\x24\xf2\x6b\xcb\x69\x4b\x7b\x49\x24\xa9\x9f\x36\xf0\xf7\x03\xfb\
-\x80\xef\x05\xda\xe6\x3b\x46\x51\x6c\xf8\x7c\x2d\x78\xbd\x2d\x28\
-\x4a\x79\x3c\xdf\x53\xe2\xcc\x46\x47\xc7\x31\xc6\xc6\x6e\x21\xcb\
-\x1d\xf5\x7f\xdb\x22\x4e\x65\x89\xe3\x74\xcd\x8d\x93\x4c\xa7\x73\
-\xc7\xbe\xf5\xfb\x97\x7b\xce\x7c\x66\xef\x98\x3f\xe0\xde\x58\x8b\
-\x93\x4e\xe6\xb2\x17\xff\xcb\x47\x97\xf6\x14\x0d\xfd\x79\x1a\xb5\
-\x24\x01\xab\x4b\x1c\xbd\x00\xc9\xf2\xba\x3a\x79\xe0\x47\x97\xdb\
-\x9e\xb6\x02\x02\x6c\xa5\x34\x7e\xe2\x9d\x5e\x7c\xb3\xfe\xaf\x07\
-\x82\x15\xfe\xba\xaa\xbe\x38\xcd\x85\xcf\xd7\x86\xc7\xd3\x88\x10\
-\xca\x3c\xe2\xb7\x32\x71\x1c\x0e\x1f\xcd\xcd\xbb\x19\x19\x29\x93\
-\x3a\xdf\x17\x0e\x87\xff\x7e\x24\x12\x29\x58\xbc\x29\x27\x8e\x63\
-\x9e\xf2\x13\xa6\x69\x6e\x3c\xf7\xed\x0f\x53\x7b\x0f\x6d\xba\xb4\
-\x61\x73\xcb\xd1\x95\x68\x4a\xb7\x27\x46\xcf\xff\xde\xfd\x5b\x67\
-\x66\x39\x0d\x10\xc8\xf4\xaa\x3b\x04\x86\x98\x1d\x4a\xf3\x1f\x22\
-\x91\xc8\xe3\x55\x23\x4e\x38\x1c\x6e\x9f\x16\xeb\x5b\xa6\xd5\xa8\
-\x2d\x94\xfc\xff\x3b\x56\xeb\x26\x1d\x8e\x3a\x7c\xbe\x36\x5c\xae\
-\xe0\xe2\xba\xb2\x39\x7f\xd6\x6e\x7b\xfb\x31\x46\x47\xcb\xa4\x4e\
-\x3d\xf0\xd7\x81\xdf\xb7\x78\x53\x3e\x73\x9a\xc3\x65\x5b\xc0\x36\
-\x10\xde\xeb\x57\x7a\x8e\x8e\x0e\x4d\x9d\x3b\x7a\x6a\xc7\x69\x21\
-\x96\x96\x8e\x22\x25\xe9\x3f\xed\xb9\x77\xeb\xea\xc8\xc0\xd9\x8a\
-\x2a\xb8\x21\x57\x75\xac\x2d\x1b\x2f\x39\x05\x66\x60\x0c\xf8\x77\
-\x2b\x69\x53\x5b\x80\x30\x4e\xe0\x3f\x02\x3f\xf2\x42\x1e\xa3\xcd\
-\x8d\xcb\xee\xa1\xce\xdb\x8a\xdd\x5e\xbd\x4a\x6d\x9a\xc5\x05\x08\
-\x58\x51\xea\x7c\xd5\x22\x4e\xe9\xbb\x52\xae\xaa\x2d\xae\x7c\x8c\
-\x0c\x4d\x9d\xfd\xb3\x3f\xb8\x7c\xed\xd3\x9f\x3f\xd0\x65\x77\x68\
-\xc1\xaa\x9e\x8f\x94\xfd\xbf\x7a\xe3\x83\xdc\x70\x2a\x39\xaf\xb4\
-\x12\xa6\xcc\xaf\xd6\x4d\x4a\xb3\xe4\x7e\x9e\x85\x7f\x15\x89\x44\
-\x12\x35\x27\xce\xb4\x1a\xf6\xcd\x69\xfb\xa3\x26\xa4\xc0\xee\x03\
-\x87\x6f\xee\x5f\x47\x1d\xd8\xbd\x20\x14\x72\xd9\x49\x3c\x13\x0f\
-\x58\xca\x4c\xde\x8b\x85\x89\xb4\x85\x3a\x19\x1d\xb9\x3e\x53\x4a\
-\x7f\x57\x38\x1c\x0e\x44\x22\x91\x98\x45\x9c\xe7\xb0\xbb\xaa\xcb\
-\x05\xcb\xe7\x8a\x07\xbf\xf5\x07\x97\x07\x4f\xbe\xb1\xeb\x5e\xa8\
-\xb1\x6e\xc1\x29\xff\xd2\x85\xc2\x87\xbf\xfc\xd1\xfb\xeb\xb3\xc5\
-\xe2\x82\xf3\x82\x2a\x26\xc5\xd5\xba\xc9\xf8\x58\x69\xc0\x73\x06\
-\x6e\x01\x91\x95\xb6\xab\x55\x20\xcd\x31\xe0\xcf\xa7\xed\x94\x72\
-\xa8\x1a\x6c\x39\x53\xba\x92\xf1\x47\x25\xc5\xd1\xe6\x9a\x9f\x14\
-\x76\x5f\x89\x14\x8a\x5a\xdd\xd7\xc1\x15\x64\xa2\xfd\x30\x9e\xd1\
-\x9b\x04\xf5\xea\xa4\xb7\x94\xf3\xf7\x79\xb1\x90\xe2\xbb\x1b\x9e\
-\x90\x6d\xf0\x72\x77\xe2\x99\xac\x76\x50\x0a\xd7\xf9\xaf\x6b\x95\
-\x31\xe1\x70\x58\x01\x5a\x9f\x3d\x56\x55\x60\xb3\xab\x55\x1f\x2f\
-\x4d\xd9\xfe\xf6\x5f\xdd\xca\x6d\xdb\xdd\xf9\xce\x8e\xdd\x9d\x27\
-\x2b\xed\xd3\x97\x88\x9d\xff\xf5\x5b\x57\x4f\x48\x29\x17\x15\x65\
-\x8a\x6e\xae\x4a\xac\x4f\x31\x57\x0a\xad\x99\x85\x7f\x1c\x89\x44\
-\x8c\x9a\x12\x67\x5a\x3d\xfb\x1f\x15\x49\xd3\xba\x0b\x0e\x7e\x19\
-\xbc\x33\x6a\x1a\xe8\x39\x48\xc5\xa0\xa0\x83\xa9\x42\xb1\x06\x1f\
-\x0e\x45\x23\xdd\x7a\x80\x7c\x72\x64\xa4\x39\xde\x5b\xa7\x48\xd3\
-\xbd\x1c\x89\x23\xa5\x64\x97\xb8\x4a\xc0\x6e\x72\xa2\xbd\x7e\x26\
-\x71\xa0\x34\x4d\xf9\x9a\x25\x0e\xd0\x32\xf3\xd9\x57\x2b\x6d\x66\
-\xc1\x79\xff\x56\xff\xc9\xf1\xe1\xd8\x85\x93\x9f\xda\x75\x4c\x51\
-\x95\xa7\x63\x3e\x85\x0b\xfd\xbd\x97\xce\xf5\x3d\xaa\xba\x52\x8d\
-\x62\x98\xab\x32\xb6\x36\x35\x77\xcc\xe6\x4f\x23\x91\xc8\x5f\xd4\
-\xa2\xed\xd9\x46\xde\xff\x0e\x94\xcf\x4a\xe5\x09\xc1\xc9\x1f\x81\
-\xd3\x7f\xaf\x9c\x34\x00\x9a\x13\x02\x2d\xd0\xd4\x01\x2d\xad\xd0\
-\xda\x0c\x5e\x1b\xd8\x25\xd8\x34\x10\xcb\x9f\xa4\x48\xf7\xb5\xb4\
-\x0c\xb6\x1f\x9d\xc8\x3b\xfc\x77\x17\xd1\xa2\x01\x39\x67\xea\x08\
-\x7b\xfa\x16\x07\x03\x19\x00\x76\x35\xfa\xf0\x3b\xca\xbe\x11\xc7\
-\xa7\xd5\xd1\xb5\x8a\x2f\x95\xdb\x37\xcb\x4f\xd9\x9f\x8c\x26\x4f\
-\x7f\xeb\xff\xbb\xf2\x20\x93\xce\x8f\x9a\x52\x8e\xff\xd6\x9d\x0f\
-\xef\x9d\xeb\x7b\x74\x6a\x29\x6d\xa8\xba\x59\xf3\xda\x17\xe9\x29\
-\x28\x64\xca\x5f\x29\xe0\x9f\xd4\xaa\x7d\x6d\x86\xb4\x39\xcc\xec\
-\xe8\xd0\xb6\x5d\xf0\xfa\x0f\x83\x5a\xe5\x00\xb2\xe6\x80\xfa\xd6\
-\x19\x97\x5a\x80\xe4\x04\xe4\xf3\x80\xad\x94\x69\xc7\x12\x3e\x2e\
-\x42\xac\x1b\x6b\xda\x55\xf0\xa6\x46\xce\xd7\x4f\xf5\x9c\x59\xc0\
-\x6b\x93\x14\xe2\xf9\x1c\x26\x1a\xc5\xfb\x5f\x6c\x1a\x7c\xf6\x01\
-\x50\x85\xe0\x58\x5b\x80\x6f\x3f\x2e\x93\xdb\x3f\x08\xfc\x8b\x35\
-\x4a\x9c\xb2\xf1\x0b\xc7\xb6\x1c\xb9\x2d\xd3\x9f\x67\x09\x8a\x46\
-\xd2\xd5\x24\x6e\x3d\x7d\x58\x52\x82\x00\x39\xed\x45\x96\xd3\xbf\
-\x91\x08\x29\x00\x53\xc2\x5f\x0c\x4f\x3e\x7e\x12\xf7\x1d\x8c\xe5\
-\xb2\x4b\x2e\x65\xab\x16\x65\x4d\xc7\x13\x4d\x03\xe2\x73\x63\x01\
-\x7e\x25\x12\x89\xdc\xab\x29\x71\xc2\xe1\xb0\x1d\xf8\x35\xca\xc2\
-\xf3\x05\xec\xfb\x52\xf5\xa4\xa9\xd8\xba\x1d\xea\x67\x8e\x6b\xca\
-\x1c\xc5\xe2\x00\xf9\x7c\x81\x4c\xb6\x9d\x42\xde\xcf\xe2\xb1\x72\
-\xf6\x94\xb7\xe5\x4c\xd6\x15\xbc\xdc\x3c\x72\x7d\x93\x6a\x16\x83\
-\x73\x89\x63\xa4\x85\xd0\x1a\xa7\x1f\x7c\xfa\x2d\xf7\x45\xa7\x4d\
-\x94\xb7\x7b\xbc\xbd\x9e\x3f\x7f\x3c\x31\x93\xb6\x5f\x0b\x87\xc3\
-\x3f\x1b\x89\x44\xcc\xb5\xc4\x98\x70\x38\x7c\x84\xd2\xc0\xf3\x33\
-\x04\x8f\x18\x38\x1a\xcb\x76\xf3\x51\x4a\xdd\x78\xfa\x26\x94\x7f\
-\xcf\x66\xfd\xcd\xe7\xd4\xf7\xee\xf6\x6b\x9f\x96\x66\x76\x59\x93\
-\xd9\x2a\x86\x59\xd3\xd0\x9e\xf8\x48\x89\x3c\x33\x10\x9b\xd6\xa6\
-\x6a\x86\xa7\x22\xf2\x67\x99\x1d\x1b\xb6\xee\x00\xd4\xb5\xd4\xf8\
-\xb1\x09\x27\x36\xfb\x66\xbc\xbe\x9d\x34\x35\xf9\xe9\xec\xcc\xd1\
-\xd6\x76\x8f\xfa\xe0\x75\x1c\xce\xd1\x85\x54\x3b\x43\xb5\x1f\x19\
-\x6a\x3f\x5c\xc8\x3a\xeb\xaf\xcf\x25\x8e\xf9\xcc\x93\xb0\xdb\xfe\
-\xf0\x43\xbf\x92\x9a\x53\x51\x32\xe8\xb2\xb3\x3d\x54\xe6\xe6\xee\
-\x04\x7e\x75\x3a\x7d\x78\xed\x4a\x9b\x7a\x66\x93\x66\x49\x1f\xf7\
-\xc9\x49\xc7\xb9\x3b\xf7\x03\xaf\x4b\x73\xf9\x33\x40\xab\xba\xe9\
-\xac\xd5\xcd\xe5\xb3\x25\x35\x6d\x16\xfe\x4d\x24\x12\x89\xae\x06\
-\x71\xe6\x4e\x2f\xd7\xb4\x79\xf5\x1f\xa1\xc4\x89\xaa\x6d\xc7\xeb\
-\xdd\x47\x53\x53\x33\x1d\x9d\x79\xda\xda\xef\x12\x0c\x5d\xc5\xe9\
-\x7a\x82\x50\x66\x89\x23\xd1\x32\xd1\xb8\x63\xf7\x44\x68\xfb\xb9\
-\x52\xf2\xeb\xf4\xd3\x33\x8d\x1c\x80\x4f\xa4\xdf\xdf\x6d\x7f\x74\
-\x72\xbe\xd3\xbd\xde\x51\x3f\x7b\xd5\x0f\x01\x37\xc2\xe1\xf0\x99\
-\xb5\xc0\x98\xe9\x8f\xc4\x57\x66\xae\x5b\xd8\xa1\xbc\xe0\xb3\x4b\
-\xf6\xf5\x7b\xaf\x3c\xe9\xf7\x9e\x5d\xe9\x75\xa9\xba\xe9\xae\xcd\
-\xeb\x04\xb1\xb9\x73\x50\x3f\x02\xfe\x53\xad\xfb\xf2\x29\x71\x7e\
-\x1e\x66\x95\x1d\xbd\xfa\xbb\xd0\xfb\xc1\x8b\x7e\xb6\x0e\x54\x75\
-\x07\x1e\xcf\x21\x1a\x1b\xd7\xd3\xd1\x51\xa4\xbd\xfd\x2e\xa1\xd0\
-\x45\x5c\xee\xfb\x08\x45\x07\xd4\xac\x3b\x78\x76\xb0\xfd\xc8\x4d\
-\x5d\x75\x8c\x94\x24\x8e\x5e\x50\x90\x43\x6f\xba\xde\xdf\xb9\x50\
-\xe3\xbb\x1b\x7d\xd4\x39\xe6\xa8\xd3\x5d\xc0\x77\xc2\xe1\xf0\x37\
-\xc3\xe1\xf0\xf1\x4f\x38\x77\xbe\x07\xca\xa7\xda\xa8\xdb\xb6\x8c\
-\x17\xd4\xe4\xc9\xbd\x87\xfe\xb1\xe8\xa4\xe3\xb5\x9a\xbc\x84\xba\
-\x59\x93\x20\xd2\x74\x74\x4e\x56\x27\xc0\xcf\xac\x46\x88\xd5\xb3\
-\x7c\x9c\x70\x38\xdc\x01\x7c\x04\xb3\xca\xf5\xec\xff\x1e\xd8\xf6\
-\xc6\xab\xf2\xe0\x8b\x98\xc6\x43\xf2\xf9\x49\x32\x59\x1f\xd9\x6c\
-\x43\x70\xe2\xc1\x50\x97\x3b\x98\xfb\x4c\x7d\x8f\xbf\x4d\x1d\x5f\
-\xd4\x30\xbd\x32\x1c\xe7\x0f\x1f\x8e\x10\xcb\xcf\x1b\xaa\x73\x11\
-\xf8\xbf\x29\x25\x37\x19\x9f\x24\xd6\x84\xc3\xe1\xdf\x9e\x29\x71\
-\x84\x0d\xf6\xfd\x2c\x88\x25\x98\xe6\xba\x2e\xae\xde\xb9\x5f\xbf\
-\xc9\xd0\x45\xa0\x56\xd7\xd5\x7e\x27\x9a\x17\x8b\x14\x27\x5c\x0c\
-\xf3\x64\x75\x5e\x8a\x44\x22\xc7\x56\xa3\x2f\xcb\x12\xd9\xc2\xe1\
-\xf0\x17\x80\x3f\x9c\xb3\xd7\x8e\x37\x61\xd3\x89\xd2\x55\x49\x09\
-\xd2\x44\x55\x35\x6c\x76\x07\xaa\xaa\x82\x94\x25\xf3\x44\x96\xbc\
-\x66\x62\xda\x04\x17\x20\x85\xaa\x66\x15\xcd\x91\x12\xaa\x96\x07\
-\x4c\x51\x92\xa8\x72\xfa\xaf\x29\x40\x0a\x81\x14\x60\x0a\x81\x89\
-\x2c\xfd\x66\xda\xcf\xac\x3c\x5f\x67\x0a\x30\x11\x48\x21\x4b\xfb\
-\x2a\x26\xaa\x2f\xaf\xc7\xba\x06\xde\x8b\x7f\xca\x7e\x6b\xa7\xa2\
-\xa8\xbb\xa9\x62\x82\x20\x43\x4a\x6e\x8c\x25\xb8\xd0\x37\x49\x77\
-\x2c\x33\xdf\x6e\x4f\x28\x85\x1c\x45\x56\x1a\x9e\xf1\x8a\x90\x46\
-\x01\x46\x81\x67\x63\x0a\xfe\xed\xb0\xf1\xef\x54\xdf\x46\x32\x65\
-\x3b\xff\xa8\xa7\xee\x24\x12\xb5\x86\x97\x96\xeb\xb8\x1d\x5d\xb1\
-\x8d\x13\xed\x2f\xc5\xa4\xcd\xc2\x1b\x91\x48\xe4\xdc\xaa\x13\x67\
-\xba\x83\x7f\x11\xf8\xc9\x6a\x1b\x68\xec\xd8\xc4\xfa\x1d\x87\xe8\
-\xdc\x76\x00\xbb\xd3\xb3\x5a\xb6\x50\xc9\x0b\x9f\xc7\xa0\xc0\x18\
-\x79\x74\x74\x1a\x00\x17\x48\x06\xfa\xde\xe9\x75\x75\xff\xfe\x93\
-\xdd\xa6\xc8\x74\xad\x0b\xba\x14\x7f\xc0\x87\xa6\xed\xa5\x8a\x9a\
-\xc2\x83\xc9\x1c\x17\xfa\xa3\x5c\x19\x8e\x53\xac\x3c\x0e\x97\xa2\
-\x14\x7e\xf4\xdf\x22\x91\xc8\xc5\x8f\xb9\x37\xad\x4c\xf7\xee\xfc\
-\x12\x34\x54\x17\xef\x9c\x1f\x1e\x71\x5d\x1e\x19\x75\x9f\x5c\x85\
-\x4b\x1b\xed\xb8\x1d\x6d\x5e\x11\xf3\xe6\x66\x75\x02\x7c\x2b\x12\
-\x89\xfc\xb5\xd5\xea\xcf\x4a\xc4\x71\x00\x6f\x03\x47\x96\xd4\x90\
-\xa2\xd2\xba\x61\x07\x9b\xf6\xbe\x4e\xeb\xc6\x9d\xf3\xa6\x01\x54\
-\x45\x92\x42\x69\x11\x45\xa4\xcc\x93\xc3\xc0\x81\xac\x1c\x91\x9b\
-\x4e\x8f\xdd\x9f\x9c\x7c\xb8\x4d\x1d\xbf\x76\x41\x8d\x3d\x5c\xdf\
-\x18\x63\x64\xa3\x1b\xa3\xbd\x5e\xdd\x89\xbf\xfe\x36\x81\x7a\x55\
-\xda\xed\x7b\x05\x0b\x7b\x7d\xd2\x45\x83\x8b\x83\x53\xbc\xdd\x3f\
-\xc9\x64\x6e\xde\x08\x88\xdb\x94\xe2\x9c\x7e\xb3\xd6\x5e\x9a\x17\
-\x40\x9c\x7f\x09\xfc\x9b\x32\x9b\xef\xa7\xc1\x16\x58\xf4\x79\x8c\
-\x3e\xea\xf1\x8d\x27\x53\xf6\xdd\xab\xf3\x51\x94\xdd\x1d\x0e\x6f\
-\x31\x90\x00\x00\x18\x98\x49\x44\x41\x54\x77\x26\x37\x2d\xff\x70\
-\x18\x7d\x04\x7a\x7e\xce\x5b\x74\x60\x35\x8b\xb4\x54\xac\x39\x30\
-\x1d\x7a\xf3\x4b\x40\x78\x39\x8d\xba\x7c\x01\x36\xed\x7d\x9d\x4d\
-\x7b\x8f\xe3\xf4\x2c\x90\x9f\x64\x52\x4a\x27\x2a\x4e\x93\xa5\xc8\
-\x92\x27\x05\x1f\x1a\xba\x7a\xd7\x30\x72\x3b\x90\x66\xde\xd6\xf3\
-\xbb\x51\x21\xa5\xcf\x97\xe0\x9e\x2f\x8f\x77\x4b\x90\x7c\xc8\xcd\
-\x7e\x84\xc8\x52\xe7\xbf\x11\xf3\xf8\x83\x76\x97\x6b\xb3\xdb\xa6\
-\xce\xab\xce\x99\x52\x72\x6b\x22\xc9\x85\xbe\x49\x1e\x4c\xce\x9b\
-\x26\x92\xa7\x14\x61\xfd\xdf\x80\xef\x7c\x1c\xd2\xb1\xc3\xe1\xf0\
-\x3b\xf0\xbc\x60\xb9\xb3\x19\x76\xfc\x83\x85\x8f\x31\x0d\xee\xdc\
-\xb9\x5f\x1f\x2c\x16\x95\x96\xd5\xba\x2e\xc5\x90\x37\xdb\xee\x4d\
-\xee\x59\xee\xf1\x89\x71\x48\x8c\xce\x59\xfd\x8d\x48\x24\xf2\xb5\
-\xd5\xec\xcf\x05\x8b\x75\x84\xc3\xe1\xaf\x01\xff\x79\xb6\x27\xa6\
-\xea\xc6\x85\x42\xfb\xe6\x3d\x6c\xde\x7f\x82\xe6\xf5\xdb\x20\x2b\
-\x9e\x13\xa5\x08\xac\xd0\xf4\xd6\xf5\x4c\x7e\x78\xf8\xc3\x67\x46\
-\xa5\x12\xbd\xfe\xb6\x36\x75\xef\x14\x60\x3a\x73\x5c\xf0\x27\x38\
-\xeb\x50\xb9\xbc\xbd\x91\xa0\xd7\xf6\xac\x5a\xa4\x3e\x6a\xf7\xe4\
-\x92\x6e\x9f\xb7\xb1\xbe\x0e\xbf\x63\x7e\x6d\x6e\x24\x95\xe7\x42\
-\x7f\x94\xcb\xc3\x71\xf2\xc6\xbc\xe3\xa4\x3d\xc0\xaf\x02\xbf\x16\
-\x89\x44\x86\x5f\x51\xd2\xf8\x29\x4d\x4f\xfe\xcc\x0d\xd0\x7c\x0a\
-\xda\x3e\xbf\x80\xfa\x93\x53\xdf\xbd\xf7\x20\x70\x48\x4a\x9c\xab\
-\x79\x6d\xaa\x6e\x5e\x6e\xbd\x3f\x75\x64\x59\xcf\xbf\x08\xa3\x0f\
-\x98\x3d\x86\x5e\x00\xb6\x45\x22\x91\xde\x97\x46\x9c\xe9\x4e\xdf\
-\x0e\x7c\x6d\xfa\x55\x7f\xee\xdd\x2a\x51\x60\x1f\xf0\x05\xaa\x48\
-\x7b\xf5\x06\x1a\xd8\xba\xff\x0c\x9d\x1d\xfb\x51\x72\x36\xec\xea\
-\xca\x5d\xf7\xd1\xe8\xfd\x07\x99\xcc\xc4\xf3\x98\x33\x69\x16\xed\
-\x3d\xbf\x3b\x82\x94\x9d\x00\xaa\xc1\xfb\x0d\x51\xf6\x03\x76\xaf\
-\x9d\xf7\xb6\x87\xd8\xea\xd0\x68\x9e\x29\xcf\x47\x55\x17\x49\x8f\
-\x8f\xfa\x40\x1d\x0d\xee\xca\x8e\x9d\xac\x6e\x70\x71\x28\xc6\xdb\
-\x7d\x93\x4c\x64\xe7\xf5\x6c\x1a\xc0\x9f\x4c\xab\x72\x7f\xfa\x2a\
-\x79\xe4\xc2\xe1\xf0\xf7\x02\x65\xe5\x8f\xb6\xfc\x08\x78\x2b\x17\
-\x62\x32\xa3\x51\xc7\x85\xbe\x81\x95\x8f\xcf\xcc\xff\xd6\x91\x46\
-\x15\x3d\x38\xd4\x29\x47\x34\x35\xd5\xd8\x9d\xfe\xd2\x72\x9a\x99\
-\x78\x02\xb9\xe4\x9c\xd5\xbf\x18\x89\x44\xfe\xe1\x6a\xf7\xe9\x8a\
-\xcb\x43\x4d\xdb\x44\x9f\x9b\x76\x73\x7e\x37\x54\xe7\x56\x0c\x06\
-\xd7\xb1\x71\xe3\x31\xd6\xad\x3b\x84\xcd\xe6\x5a\xf2\x79\x4d\xd3\
-\x60\x68\xe8\x52\x5a\x4a\x59\x26\x0d\xd5\xa9\xdb\xef\xa8\xd1\x5b\
-\x27\x9f\xdf\x20\xf7\x1a\xa2\xd4\x29\x26\x6d\x40\x3a\xe4\xe2\xf2\
-\x96\x10\x47\x54\x31\x57\x8a\x4e\x28\x0e\xa6\x5c\x3e\x7c\x81\x3a\
-\x5a\x7c\xae\x8a\xe6\xd7\xdd\x89\x14\xe7\xfb\xa3\xb3\xa3\xad\xe7\
-\x68\x90\x94\xa2\xcc\x7f\x3d\x12\x89\x3c\x78\x05\x88\xf3\x5f\x99\
-\x91\x90\xa8\x3a\x61\xef\xbf\x80\x0a\xbe\xb1\x44\x6f\x9f\xf7\xc1\
-\xd4\x94\xe3\x70\x0d\x4f\x6f\xa2\x88\x01\x1c\xca\x24\x6e\x4d\xc1\
-\xad\xb5\x61\x57\x1a\x90\x32\x45\xdf\xc0\x3d\x47\xc2\xc8\x34\x3e\
-\x5e\x52\xc9\xdb\xd2\xc7\x2c\x01\xd1\xbe\xb9\x8e\x3f\x60\x53\x24\
-\x12\x19\x7f\xe5\x89\x33\xeb\x01\x05\x81\xbf\x33\xfd\x90\x76\x56\
-\x25\xaa\x55\x1b\x1d\x1d\xfb\xd8\xb0\xe1\x18\xcd\xcd\x5b\xa8\x76\
-\xba\xf9\x54\x6a\x28\x3e\x35\xf5\xd8\x5f\xc1\x5a\x34\x6c\x3d\xbf\
-\xd3\x2f\xe4\xf3\xf9\x58\x80\x89\x40\x8c\x21\x47\xa1\x54\xf2\x55\
-\xc0\x78\x9b\x8f\xff\xbf\xbd\x33\x0d\x6e\xeb\xba\xee\xf8\xef\x61\
-\x07\x01\x82\x20\x41\x4a\x14\x05\x49\xa1\x16\xcb\xb2\x44\xc5\x96\
-\x2c\x5b\x96\x62\xcb\x69\xc6\xd9\x3a\x53\x27\x9e\x2c\x8d\x9b\xb4\
-\x9e\x84\x6d\x6c\xd7\x99\x36\x4d\x9b\x66\xd2\xe9\x24\xd3\x49\xa7\
-\x4d\x26\x49\xd3\x69\xb3\x75\x6e\xdb\x24\x1f\xdc\x38\x8e\xd3\x64\
-\x3a\xd3\xc9\x34\x4d\xa3\xc5\xda\x25\xd3\xd6\x2e\x8a\x92\x28\x12\
-\xe0\x02\x12\x00\x01\x62\xdf\x5e\x3f\x3c\x48\x24\x80\x07\x10\x00\
-\x77\xf2\xfd\x67\xf8\x41\x78\x18\x08\xb8\xf7\xfe\xef\x39\xf7\xdc\
-\x73\xfe\xe7\xea\xc6\x26\x0e\xe8\x24\xf5\x62\xbe\x90\xce\xc8\x98\
-\xa5\x11\x8b\xc3\x41\x47\x53\x03\xba\xa2\x74\x20\x5f\x2c\xc9\xb1\
-\xc1\x20\xa7\x87\x82\x24\x2a\x4b\xc7\x1e\x07\xfe\x0d\xf8\x89\x10\
-\x22\xb2\x48\xc4\xb9\x0e\xdc\xb3\xcc\xce\x2e\xe8\xfc\x58\xd1\xb0\
-\xe5\xb8\x7d\xad\xcf\x49\x22\xae\x9f\x9d\x20\xa0\x24\x85\x30\xe9\
-\x7c\x58\xf5\x60\x35\xb4\x61\xd1\x3b\x29\x3e\x52\xa6\xd3\x43\x0c\
-\x7a\xa3\xe4\xe4\x6d\xd6\x49\x0e\xbb\x06\x6a\x13\x58\x97\x65\xe5\
-\xce\x26\x5b\x1a\xc3\xf9\x92\x10\xe2\x6f\x16\x62\x4c\xe7\x4d\x90\
-\xb0\xbb\xbb\xfb\x60\x9e\x40\x1f\xa1\x4a\x81\x0e\x9b\xcd\x45\x67\
-\xe7\xa3\x74\x76\x3e\x42\x43\x43\xcb\x0c\x41\x81\xb3\x63\xd9\x6c\
-\x4a\x35\xcb\x4a\x3f\x71\xed\x84\x7e\xfc\xad\x62\x21\xc3\x94\x2d\
-\xca\x69\x7b\x94\x7b\x29\xef\x3a\xb8\xd5\xd9\xcc\x58\xbb\x9d\x8a\
-\x41\xd9\x28\x7a\x46\x2c\x8d\x18\x1c\x8d\x74\x38\xed\x18\x75\x53\
-\x01\xbe\x64\x26\xcb\x99\xe1\x10\x47\x07\x03\x8c\x46\x93\x95\x3f\
-\x06\x5e\x05\xfe\x45\x08\x71\x72\x01\x49\xe3\xca\x9f\x6f\xee\x61\
-\xd3\x87\xa0\x65\xcf\xb4\x48\x47\x54\x77\xe5\xfa\x6d\xe7\xfa\x6c\
-\x56\x6a\xaa\x91\x24\x69\xf4\xd2\x18\x56\x7d\x86\x06\x43\x0b\x16\
-\xbd\x1d\xe3\x0c\xd1\xd4\x78\xe2\x2a\x43\xc3\x2e\x64\xa5\xf5\x99\
-\x2d\xc0\x91\xe6\xe1\xda\xba\x4c\x4f\x8c\xa8\x16\xa8\x8d\xe6\xad\
-\x4d\x74\x59\x13\xa7\x68\xe2\x3e\x03\xbc\x04\xe5\x9b\x08\x15\x3b\
-\xc1\xed\xed\xdb\xe9\xec\xdc\x8f\xdb\xbd\x1b\x9d\xae\xd0\x28\x24\
-\x93\x61\x7c\xbe\x8b\x95\x42\xa8\x39\xd3\xad\x9f\xde\x42\xce\x96\
-\x24\xdc\x19\xd3\x1c\x6d\x09\x72\x60\xfa\x41\xd9\xa0\xe3\xc2\xfd\
-\x2e\xf4\x4d\x16\x76\xce\xf4\xcd\x92\x48\x0c\x9b\xec\xe4\x1a\x1d\
-\x74\x34\x37\x62\x31\x4c\xf9\x3b\xd7\x03\x11\x8e\x0c\x04\xb8\x3c\
-\x36\x39\x53\xf1\xc4\x45\xe0\xfb\xf9\xe8\x4f\x68\x9e\xc6\xbd\x01\
-\xa5\xdb\xda\x07\x51\xf4\xea\xee\xa1\xeb\x8b\x60\xc8\x27\xb9\xf8\
-\xae\x1a\x32\xde\x74\x93\xae\x5c\xb8\xbf\x70\x47\x92\x26\x30\xeb\
-\x93\x34\x18\x1c\x58\xf4\x56\xcc\xfa\x6a\x1d\x84\xbc\x19\x0f\x9f\
-\x62\xcc\xbf\x9b\x69\x57\x03\x8d\x63\xbc\xde\xe4\xa3\xea\xfb\xa1\
-\x74\x52\x09\x3f\xab\x0c\xf0\x0b\x42\x88\xef\x2d\xd4\x86\xb4\x60\
-\x12\xb8\xdd\xdd\xdd\x36\x94\xf0\xf6\xe7\x28\x52\x58\xa9\x04\x93\
-\xa9\x81\x4d\x9b\xf6\xb2\x79\xf3\x63\x38\x9d\x8a\xd6\xfa\xf0\x70\
-\x0f\x99\x4c\xac\x72\x98\x33\x7c\xe3\x94\xc1\xf7\x86\x6a\xba\x85\
-\x94\xe3\xcd\x56\x3f\x1b\x75\x32\x05\x66\xcd\x62\xe0\xe4\x8e\x56\
-\xd6\x37\x18\xd9\x58\xcd\x77\xcb\x02\x5e\xa3\x9d\x94\xbd\x91\xb5\
-\xcd\x0e\x1a\xf3\x6d\x00\xfd\xf1\x14\xaf\x7b\x02\x9c\xf0\x4c\x10\
-\xaf\xdc\x06\x3d\x06\xfc\x18\xf8\xbe\x10\xe2\xcc\x2c\xc6\x76\x63\
-\x9e\x24\x5d\x4c\x69\xd5\xdd\x87\x8a\x7a\x7f\x83\x1b\xb6\xbf\x08\
-\x72\x06\xee\x5c\xb0\x10\xd4\xdb\xca\x59\x93\x24\x66\x7d\x0c\xab\
-\xce\x86\xc5\x60\xc2\xa2\x07\x7d\xfd\x85\x89\xf8\xc6\x8f\x10\x9e\
-\x7c\xa2\xd8\x17\x77\x0e\x71\xda\x1e\xa4\x6a\xd9\xa9\xb1\xdb\x90\
-\x2c\xb5\x29\x67\x81\xfd\x0b\x59\x22\xb2\xe0\xda\xd1\xf9\x7e\x36\
-\xcf\xe6\x77\xc1\x9a\x24\xa6\x9c\x4e\x37\x5b\xb6\xec\xc7\x64\xd2\
-\x57\x53\x5c\x2a\x9b\x6e\xfd\xb4\x97\x5c\x76\xbb\xfa\x0f\x67\xb0\
-\x25\x48\xc2\x90\x61\x5b\xb1\x4b\xd7\x64\xe6\xe4\x7d\x2e\xba\x4c\
-\x7a\x5a\xaa\xfd\x6e\x32\x30\x6c\xb0\x12\x6b\x70\xd0\xd2\xec\xa0\
-\xc5\x6a\x22\x95\xcb\x71\x6e\x38\xc4\xd1\x81\x00\x43\x91\xc4\x4c\
-\x1f\xf1\x26\xf0\x1d\xe0\x47\x42\x88\x64\x99\xb1\x6b\x42\x29\xff\
-\xd8\x3d\x8d\x28\x5d\xd4\x20\xe6\xb7\xee\x5d\xe0\x7a\x04\x6e\xf5\
-\xda\x89\x19\xcc\x53\x5f\xdf\xa8\x8b\xd1\x60\x30\x63\xd1\x1b\x30\
-\xeb\xc1\x34\x47\x45\x99\xb2\x9c\xc1\x3b\x7c\x92\x44\x52\xb5\x2a\
-\xd4\x35\xc0\x9b\xd6\xc9\xc2\xfa\xa0\xb2\xbe\xee\x04\x04\x3d\xaa\
-\xfb\xd7\x3e\x21\x44\xcf\x42\xae\xe3\x45\x13\x5d\xcf\x0b\xab\x3f\
-\x9d\x27\x50\xd5\x89\x78\x6e\xf7\x4e\x36\x6e\xac\xee\x12\x5b\x3f\
-\x79\xfb\x8c\x7e\xf4\x4c\xa5\x0c\xde\x68\x63\x98\x4b\x0d\x09\xd5\
-\x1d\x2f\xbc\xc6\x46\xcf\x96\x66\x1e\xd5\x49\xb5\xdf\x65\xf8\xf4\
-\x66\x42\x56\x07\x0e\xa7\x83\xb5\x76\x0b\x7d\xc1\x28\x47\x07\x03\
-\x5c\xf0\x85\x99\xa1\xc2\x7e\x14\x25\x0d\xfe\x2c\xb0\x15\x45\xc7\
-\x6e\x6b\x3e\xd8\x52\xdf\xc1\x5d\x0f\xb4\x99\xa0\xdd\x8c\xfb\xf1\
-\x34\xa3\xa1\x06\xd2\x06\x23\x58\x0c\x60\xd1\x81\x45\x3f\xab\x32\
-\xf7\xf2\x26\x39\x17\x66\xc0\xd3\x47\x36\xbb\xa7\xdc\x5b\xda\x6e\
-\x71\xdd\x1c\x67\xc6\x1c\xed\x5c\x16\x46\x6e\x80\x8a\x8c\xde\xb7\
-\x84\x10\x9f\x5d\xe8\xf5\xbb\x24\xba\x15\x74\x77\x77\x3f\x09\xbc\
-\x98\x27\x52\xc5\x6a\xc0\x8e\x8e\xed\x6c\xd8\xb0\x0b\xbd\xbe\xba\
-\x94\x5e\xd3\xad\xd7\x2e\x93\xcb\x54\x3a\xbb\xc8\x96\x04\x47\x9a\
-\xc2\x1c\x52\x0b\xe9\x49\x30\xb4\xb1\x89\xdb\x6e\x07\x07\xa8\xcd\
-\xa3\x9f\x3a\xcc\xea\x8c\x8c\x5b\x1d\x58\x1d\x8d\x58\x4d\x46\x8e\
-\x7b\x83\x9c\xf4\x04\x89\xa4\xe7\xe1\xaa\xc7\x20\x29\x24\x59\xa7\
-\x10\x85\x75\x66\x68\x33\xde\x73\xb3\xa4\x6c\x0e\x59\xaf\x9b\xff\
-\x49\x4d\x67\x3c\x0c\x78\x92\xc8\x72\xc5\x74\x9a\xf6\x3e\xbc\x86\
-\x64\xa1\x54\x95\x1a\x82\x43\x10\x0d\x94\xbc\xec\x01\x76\x2c\x46\
-\xb4\x72\x49\xb5\xf9\xc8\x07\x12\x3e\x0e\x7c\x8a\x69\x6d\xc4\x4b\
-\xd6\x86\xc1\x8c\xdb\xbd\x83\xf6\xf6\xad\xe8\x66\x90\x9e\x4a\x07\
-\x6f\x78\x6d\xfe\x37\x66\x9c\x18\x7d\x96\x53\xad\x01\xba\x90\xd5\
-\xb3\x24\xf4\x12\xd7\xb7\xb6\x10\x6d\x6d\x60\xcf\x6c\x7e\x63\x44\
-\xd2\x33\x6a\x6e\x44\xd7\xd8\xc8\x70\x3c\xc3\x71\x4f\x90\xc1\xc9\
-\x44\xfd\x24\x59\x6b\x52\xc8\xd1\x9e\x27\x4a\x9b\x11\x74\xd2\xe2\
-\x4e\x64\x3c\x7e\x89\xa1\x91\x76\x64\x5a\x67\x74\x1d\xaf\x11\xd6\
-\x67\xa9\xd8\xa9\x20\x15\x07\xdf\x4d\xd5\x47\xcf\x08\x21\x16\x45\
-\x5c\x72\xc9\x76\x64\xcb\x8b\x87\x7c\x0a\xe5\x62\x55\xd5\x87\x37\
-\x1a\x2d\xb8\xdd\x0f\xb0\x76\xed\x16\x74\xba\xf2\xbb\x68\xf6\xc6\
-\x6b\x31\xab\x94\x69\x98\x79\x30\xe8\x75\x05\x68\xd0\x67\xcb\x77\
-\x7c\x33\xe9\x39\xbf\xa3\x95\x46\xbb\x89\x59\xab\xe4\x24\xd1\xe1\
-\x35\x37\x32\x86\x81\xcb\xa1\x24\x17\xc7\x23\x64\xcb\x4d\x87\x51\
-\x52\x88\xd1\x3e\x8d\x28\xae\x25\x40\x92\x12\xf3\x1a\x3a\xc9\x78\
-\xe0\x21\xa8\xca\xbd\xcd\xb9\xaf\x20\x21\x57\xb6\xe4\xa3\x7d\xaa\
-\x05\x6a\xff\x25\x84\xf8\x9d\xc5\xfa\x99\x4b\xbe\x95\x61\x77\x77\
-\xb7\x15\xa5\x9b\xc1\xef\x02\xef\x56\x73\xe5\xcc\x66\x2b\x6e\xf7\
-\x4e\xd6\xac\xe9\x54\xcd\xca\x8e\x8d\xf7\xe2\x9c\xa8\xfa\xec\x18\
-\x70\x86\x18\x30\x27\x2b\x1e\x58\x73\x76\x23\x27\xee\x6f\x65\xab\
-\xd9\xc0\x9c\x24\x40\x66\x90\xb8\x23\x99\xb9\x38\x99\xe1\x4c\x3c\
-\x47\x64\xad\xa9\x90\x28\x2d\xc6\x3a\x1d\xc5\x05\xc4\xe8\xd8\x61\
-\x26\x23\x87\x6a\x70\x69\x27\xdc\x97\xa9\x98\x9f\x1d\xf1\xab\x4a\
-\xd8\x46\x51\x9a\x41\x0d\x68\xc4\xa9\x8e\x44\x4d\x28\x69\x3d\x1f\
-\x06\x9e\x2a\x26\x91\xc5\x62\xc3\xed\xde\x45\x5b\xdb\xa6\x92\xb6\
-\x7a\xc9\xde\x9f\xd1\xa8\xab\x5a\x30\x31\x63\x8b\x71\xc2\x1e\x99\
-\x31\x15\x24\xde\x62\xe1\xf4\x36\x17\x7b\x0d\x3a\xe6\x4c\xf4\xe3\
-\xb8\xd9\xc8\x2b\x1f\x5a\x3f\xbb\xf0\xef\x42\x42\x96\xd3\x78\xbc\
-\xa7\x49\xa6\x6b\xab\xd7\x91\x19\x74\x5f\x29\x7f\x35\x91\xcd\x28\
-\x01\x01\x95\x86\x14\x7f\x21\x84\xf8\xfa\x62\xfe\xe4\x65\x45\x9c\
-\x22\x12\xd9\xf3\x16\xe8\x45\xe0\x5d\xd3\x9f\x59\xad\x8d\x6c\xd8\
-\xb0\x8b\xd6\xd6\xa9\xeb\x98\x78\xe0\x26\x4d\x81\x73\xb5\x1d\x21\
-\xd2\x1c\x73\x05\xd9\xcf\xcc\x05\x71\xfe\x0e\x3b\x97\x37\x39\x79\
-\x4c\x27\x61\x9c\xed\x6f\x0b\x64\x40\xec\x68\xc6\xf3\x98\x73\xe9\
-\x4f\x44\x36\x3b\xc1\x80\xb7\x9f\x6c\xf6\xc1\x9a\x17\x5f\x8e\x6b\
-\xeb\xaf\x52\x56\x2e\xc4\xef\x81\x78\xa9\xc2\xf7\x05\x60\xaf\x10\
-\x22\xa3\x11\x67\xf6\x24\xfa\x43\xe0\x5b\x14\x15\xab\x35\x34\x34\
-\xb1\x71\xe3\x2e\x5a\x5a\x94\x23\x4b\xa2\xf7\xe7\x38\x74\xc9\x1a\
-\x07\x88\x0b\xad\xe3\x74\xe8\xaa\x38\xe8\xea\xa0\xbf\xd3\xc9\x48\
-\x7b\x23\xb3\xae\x73\x3f\x13\x91\x78\xf9\x59\x37\x39\xe7\x12\xee\
-\x6f\x9c\x4a\x0f\x30\xe8\xc9\x22\xd7\x17\x26\xd7\x65\xe8\xe9\xb8\
-\xce\x43\xaa\xe7\xbf\xa8\x72\xd9\x59\x62\xa3\xe0\xc0\x52\xa8\xc4\
-\x5d\x11\xc4\xc9\x93\xe7\x7e\xe0\x65\x28\x9d\x08\x9b\xad\x99\x8d\
-\x1b\xbb\xb0\x90\xa4\xc9\x7f\xba\x0e\x57\x84\x21\x57\x90\xb0\x21\
-\x43\x55\x62\x4a\x06\x89\x4b\xdb\x5d\xc8\x4e\x2b\x75\x17\x68\x5d\
-\xca\x90\xea\x77\xda\xa2\xff\xf3\xcc\x9a\xe6\x25\x39\xe0\xd1\xf8\
-\x45\x86\x47\xd6\x43\xf5\x97\xc4\xc5\xd0\xa7\x38\xb5\xee\x46\xe9\
-\x26\x53\xa6\xaa\x13\x94\x0c\x8b\xe7\x97\xc2\xcf\xd7\xb1\x42\x90\
-\x97\x37\xdd\x0f\x7c\x83\xa2\x3e\x21\xd1\x68\x90\xab\x57\x8f\xd2\
-\xe7\xe9\x63\x2c\x55\x87\xce\x84\x44\x87\xbf\x85\x8d\x31\x2b\x55\
-\x25\x67\x66\x64\x76\x5d\x1e\xa7\xeb\xfc\x10\xa7\x62\x69\xfa\xeb\
-\xf9\x3d\x9b\xcd\x9c\x7c\xaa\x3f\x7a\xc1\x19\xce\x9c\x5d\x72\x83\
-\x1d\x0c\x9d\x60\x78\xe4\xbe\xd9\x90\x06\xca\xb7\xf7\x88\x8c\xab\
-\x92\x66\x14\xa5\xa9\xf2\x92\xc0\x8a\xb1\x38\x45\xd6\x67\x2f\x4a\
-\x12\xe5\x5e\xb5\xe7\x1b\x1a\x60\xb3\xad\xbe\xcb\x72\x73\x92\xc3\
-\xce\x10\xb5\x44\x8e\x32\x0e\x33\x27\xb6\xbb\xd8\x69\xd2\x57\x9b\
-\xe4\x0a\x59\x38\x37\x06\x2e\x5d\xd4\x68\xf8\xca\x0b\xee\xbc\x30\
-\xc9\x12\xc0\xa8\xef\x30\x93\xd1\x27\xe7\xe2\xa3\xcc\x51\x8e\xb4\
-\xf5\x17\x66\x46\x97\xa9\xea\x04\xf8\x3d\x21\xc4\xcb\x4b\x65\x8d\
-\xe9\x58\x81\x10\x42\x9c\x07\x1e\x05\xfe\x04\xa5\xb8\xa9\x00\x83\
-\x31\x38\x1f\x84\x58\x1d\x17\xf7\x49\x33\x4f\x8e\xbb\x38\x2b\x4b\
-\x54\x7b\x5b\x6d\x08\x27\x79\xe2\xec\x10\xe6\xde\x71\x0e\x67\x65\
-\xaa\x6a\xfc\xa3\x87\xcd\x29\x33\x9d\xae\x50\x7a\x7c\xef\xa5\xc8\
-\x99\x45\x1f\x54\x59\x4e\x31\xe8\x39\x3e\x57\xa4\x01\xd0\xa7\x4b\
-\xd7\xdf\xc4\x90\x2a\x69\xfe\x77\x29\x91\x66\xc5\x5a\x9c\x22\xeb\
-\xd3\x01\xfc\x23\xf0\xa1\x92\x89\x93\x60\xab\x1d\xd6\x59\xeb\x19\
-\x38\x6e\xb6\x04\x30\x1a\xb2\xd5\x65\x52\x4f\x79\x7d\x8c\xb8\x9b\
-\xe8\xdb\xe0\xe0\x80\x34\xc3\xc6\x35\x6c\xc2\x63\x0e\xe3\x75\x04\
-\xa4\x87\x3e\xff\xe7\x9b\x86\x33\x06\x69\xd3\xa2\x0c\x62\x36\x17\
-\xe0\xce\xa0\x87\x5c\x6e\xf7\x5c\x7e\xac\xed\x0e\xa3\xcd\x91\xa9\
-\x52\xf6\xf8\x24\xf8\xef\x94\xbc\x2d\x01\x74\x09\x21\xfa\x34\xe2\
-\x2c\x0e\x81\xde\x0f\x7c\x9b\xa2\x6e\xcb\x00\xad\x66\xd8\xde\xc8\
-\x8c\x35\x58\x2a\x41\x83\x89\xa6\x30\xb7\x2c\xc9\xda\xd3\x70\xf4\
-\x12\x37\xb6\xb4\x10\x6a\x6b\xa0\x6c\x99\xf2\x84\x81\x93\x93\x3a\
-\x1e\x71\xdf\xc4\xd7\xb3\xc3\x36\xf2\x83\x67\xd6\x3c\xb4\xe0\x03\
-\x97\x4a\xf5\x33\xe8\x95\x90\x99\x73\xd2\xda\x7b\xc1\x99\x9e\x0a\
-\x08\x2c\x76\x55\xe7\xaa\x77\xd5\xca\xb8\x6f\xff\x0d\xec\xcc\x9f\
-\x7d\x0a\x30\x9e\x84\x73\x01\x08\xd6\xaa\x30\x2c\xe1\x0c\x35\xf1\
-\xf6\x70\x63\x91\xee\x76\x35\x9b\xb8\xcc\xb6\x5e\x3f\x0f\x9f\xf1\
-\xd2\x13\x4e\xa2\xda\xb7\xc5\x92\x23\x2d\x4b\xe8\xe3\x76\xae\xef\
-\xb9\x1a\x7d\xa8\xcd\x9f\x5e\xd8\x30\x6c\x34\xf6\x16\x03\xde\xa6\
-\xf9\x20\x4d\x3e\x38\x70\x0f\x61\x9f\x2a\x69\xce\xa2\xe8\x9a\x2f\
-\x39\xac\x1a\x8b\x53\x64\x7d\x9e\x46\x51\xa3\x69\x55\x0b\x1c\x74\
-\xda\x6a\x4f\x01\x33\xa4\x79\xdd\x15\x64\x1f\xd4\xa5\x81\x2c\x37\
-\x18\x39\xb1\xa3\x95\x4e\x8b\x81\x7b\x0d\x85\x72\x70\xc9\x63\x66\
-\x97\x3e\xc3\xd8\xfa\x7e\x9a\x02\x0e\x43\xf0\xcb\x2f\xb9\xed\x48\
-\x92\x6d\xde\x07\x29\x38\x71\x1c\x7f\x70\x1f\x33\x64\xab\xcf\x06\
-\xcd\xd7\xc0\x96\x2d\x5b\xd5\x39\x09\xec\x59\x6a\x2e\xda\xaa\xb3\
-\x38\x45\xd6\xe7\x17\x28\xc5\x60\xbf\x52\x0b\x1c\xbc\x11\x84\x68\
-\x8d\x81\x83\x8c\x91\x77\xf8\x5a\xb9\x91\xd3\xe1\xab\x67\x03\x8b\
-\xa5\x39\x78\x7e\x98\x96\x2b\x63\x1c\xc9\xe4\x08\xe5\x27\x67\xab\
-\x04\x99\xac\x81\xb6\xb4\x91\x73\x2d\xe1\xcc\xda\x03\x3d\x91\x73\
-\xf3\x3c\x3c\x32\x23\xbe\xc3\xf8\x83\x07\xe7\x93\x34\x00\xfa\xdc\
-\x54\x40\x40\xa5\x14\xfa\xa5\xa5\x4a\x9a\x55\x4b\x9c\x3c\x79\x86\
-\x81\xf7\xa0\xb4\x6f\x2c\x70\xd2\x22\x19\x38\x1f\x80\xa1\x78\x8d\
-\x2b\x4e\xc7\xae\xb1\x56\x72\x29\x23\x57\xea\xfc\x5a\x96\x60\x82\
-\x43\x67\xbc\x64\x6f\x06\x39\x92\x93\xd1\x99\x72\xdc\x04\x08\xb6\
-\x62\x07\xf8\xf0\x2f\xc7\x0f\x1a\xd3\xf2\xfc\x2c\x28\x59\x4e\x30\
-\xe0\x39\x49\x64\xee\x22\x67\x15\x89\x03\xc4\x26\x54\x4b\xa1\x5f\
-\x16\x42\xfc\x68\x29\xaf\x9f\x55\x4b\x9c\x3c\x79\x64\x21\xc4\x37\
-\x50\x2e\x4e\xfb\xa7\x3f\xcb\xc9\xd0\x3b\x09\x97\x42\x90\xae\xad\
-\x92\xbd\x3d\xd0\xcc\xe6\x68\x03\xc7\xeb\xdf\xf2\x69\x19\x89\x70\
-\xe8\xb4\x87\xd1\x68\x8c\x21\x80\x84\x8d\xdd\xb2\x44\xaf\x5e\xc6\
-\xf0\xc9\xd7\x46\x63\x73\x3e\x18\xd9\xac\x9f\xfe\x81\x3e\x52\xe9\
-\x03\x0b\xb6\xf8\x32\x8a\x62\x4d\x11\x6e\x01\x2f\x2c\xf5\xb5\xb3\
-\xaa\x89\x33\x8d\x40\x3d\x28\xf7\x3e\xa7\xd4\x02\x07\x67\x6b\x0c\
-\x1c\x48\x60\x89\xd8\x39\x38\xe1\xe4\x08\x45\x59\x0c\xb5\x20\x07\
-\x1b\xc6\x83\x53\xee\x52\xd8\xc9\x28\xc0\xce\x9b\xf1\xdd\x1d\xbe\
-\xd4\xf1\x39\x1b\x80\x64\xea\x16\xfd\x03\x31\xb2\xb9\x5d\x0b\x36\
-\xe8\x59\x98\xf4\x95\x94\x42\x67\x80\x67\x97\x43\x5b\x15\x8d\x38\
-\x53\xe4\xf1\x01\xef\x04\x7e\x58\xfc\x2c\x95\x83\xb7\x26\xa0\x6f\
-\x92\x99\xf4\x02\x0a\xd7\xa3\x89\x43\xe3\xad\xbc\x21\x4b\xd4\xbd\
-\x10\x32\xa9\xa9\x7a\x9f\xc9\x66\xf6\x80\xf2\x59\x2f\xfe\xc7\xc8\
-\xfd\x20\xcf\x5e\x5a\x2a\x12\xeb\x61\xd0\xeb\x42\xae\x5e\x79\x68\
-\x56\x48\x01\x3e\xa0\x4f\xa9\xb5\x29\xc2\x97\x84\x10\xa7\x97\xc3\
-\x7a\xd1\x88\x53\x48\x9e\x84\x10\xe2\x39\x94\x52\x85\x92\xe0\xa8\
-\x27\x5e\x7b\xe0\x20\xab\xe3\xe1\xb1\x56\xfc\x19\x03\xb7\xeb\x3b\
-\x76\xd0\x29\xe7\x88\x02\xe4\x74\xd8\x92\x56\x7a\x00\x1c\x91\xac\
-\xeb\x9d\xa7\x42\x17\x67\xf5\x83\x03\xc1\x63\x8c\x8c\xee\xa2\x06\
-\x95\x9c\x3a\xcc\xa6\x1f\x1f\x39\xee\xe4\x9d\x61\x1f\x4a\xbc\xec\
-\x72\xc9\x3b\x7f\x03\xfc\xfd\x72\x59\x2b\x1a\x71\xd4\x09\xf4\x5d\
-\xe0\xb7\x50\x12\x0b\x29\x09\x1c\xf8\xc1\x5b\x43\xe0\x40\x96\xe8\
-\xf4\xb7\xe0\x4a\x58\xa8\x27\x22\xa6\x4b\xc6\xb8\x17\x0c\x08\xb6\
-\x4e\x65\x2a\x3c\xfd\x7f\xc1\x03\xe6\x64\xee\x5a\x5d\xc7\xa8\xe1\
-\x91\x23\x04\x26\x1e\x87\xd9\xd7\x0f\x15\x7d\xb2\x97\x14\xc7\x09\
-\x73\x8c\x61\xee\xe0\xc5\x45\x12\x1d\xba\x7c\x34\x20\x07\x5c\x2f\
-\xd9\x96\xfc\xc0\x27\x16\x52\x17\x4d\x23\xce\xfc\x91\xe7\x75\x94\
-\x24\xd1\x33\x2a\x67\x0f\x6e\x4c\xc2\xc5\xda\x02\x07\x8e\x90\x83\
-\x3d\xf5\x5c\x96\xc6\xa2\xdc\x73\xc9\x52\x66\x3a\xb3\x7a\xc5\xea\
-\xe8\x64\x74\x7f\xf4\xea\xa8\x5c\xd3\x39\x4a\x96\xe3\xdc\x19\x3c\
-\x4d\x34\x7e\x68\x8e\x88\xd2\x4f\x8a\x63\x84\x38\xce\x10\x43\x78\
-\x58\xcf\x28\x07\x09\xf1\x38\x19\x95\x8b\xd3\x7e\x54\xb2\x07\xf9\
-\x94\x10\xc2\xbb\x9c\xd6\x87\x46\x9c\xca\xe4\xf1\x02\x4f\x00\xff\
-\xae\xf6\xdc\x9f\x0f\x1c\x04\xaa\x0f\x1c\xe8\xe2\x56\x0e\xf9\x5d\
-\x1c\x47\xc9\xc1\xaa\xee\xac\x94\x28\xbc\x54\x0d\xb5\x4c\x85\xcf\
-\xb7\xdd\x49\xec\xd8\xe4\x4d\x56\x17\x28\xc8\x64\xc6\xe8\x1f\xb8\
-\x45\x3a\xb3\xbf\x6e\x9a\xc8\xf4\x91\xe4\x28\x13\x9c\xc4\x8b\x0f\
-\x0f\x6f\x63\x94\xc7\x09\x73\x90\xec\xd4\xe5\xad\x2a\xc6\x80\x52\
-\xfd\x80\xef\xe6\xef\xd5\x96\x15\x56\x65\xe6\x40\x3d\xe8\xee\xee\
-\xfe\x34\x4a\x95\xa9\xaa\x7a\x8b\xdb\x0a\x9b\xed\x35\x64\x1c\xc8\
-\x5c\x6d\xf5\xd3\xac\xcf\x55\x25\xf6\x31\xb2\x61\xeb\xd4\xfb\x24\
-\x99\xac\xfb\x26\x3e\x09\xd6\x01\x44\xad\xba\xd0\x17\x3f\xbb\x31\
-\x2b\x4b\x52\xf9\xfa\x98\x44\xaa\x0f\x8f\xd7\x0a\x33\x6b\x98\x4d\
-\x3f\xa2\x91\xa3\x97\x14\x3e\x62\x58\x88\xb3\x8d\x5c\x9d\x35\x38\
-\x31\x94\xa2\xe7\x42\xdb\x78\x19\x45\x85\x33\xbe\xdc\xd6\x83\x66\
-\x71\xaa\xb7\x3e\xdf\x07\x1e\x03\x6e\xa8\x3d\xf7\xc4\x95\x52\x85\
-\x68\xb5\x95\xf0\x12\x3b\xc6\x5d\xe8\x92\x26\x2e\x55\xf1\xee\xf6\
-\x5c\x66\x2a\x23\xe1\x6e\xfe\xda\xdd\x7f\xdb\xe2\xb9\xa6\xf7\x1e\
-\x9b\x28\x7f\xd6\x89\x44\xce\xe3\xf5\xae\xad\x82\x34\x69\x72\x5c\
-\x24\xc1\x61\x02\x9c\xc3\x43\x0c\x2f\x3b\x18\xe3\x10\x51\x1e\xad\
-\x9b\x34\x59\xe0\x5a\x09\x69\x12\xc0\xc7\x96\x23\x69\x34\x8b\x53\
-\x9f\xe5\x69\x44\xe9\xfd\xf9\xd1\x72\x3b\xd1\x16\x3b\xac\xaf\xbe\
-\xe1\x5c\xd2\x1e\xe5\xac\x2d\x5a\x59\xb1\xbf\xa5\x8d\x33\xb6\x26\
-\xee\xc9\xf9\xe6\xf3\xd7\x9c\x77\x0f\xf7\x32\xf0\x85\xcf\x6d\xba\
-\x14\xb7\xe8\x0a\xef\x62\xc6\x03\x47\x99\x08\x15\x74\x67\x98\x86\
-\x38\x39\xae\x93\x60\x82\x38\x4d\xc4\xd9\x8e\x4c\xc3\x9c\x0f\xda\
-\x55\x20\x58\xf2\xea\x4b\x42\x88\x6f\x2f\xd7\x75\xa0\x11\xa7\x7e\
-\x02\x3d\x9f\x77\xdd\x54\x93\x3a\x5d\x26\xd8\xee\xa8\x5e\xbb\xdc\
-\x98\xe2\x48\xcb\x04\xef\x40\xad\x4f\x1a\x60\xb5\x71\xa4\x75\x5d\
-\x61\xb5\xe4\xba\x3b\x9c\x30\xa6\xb9\x77\xd3\xdf\xbf\xde\xdc\xf7\
-\xcd\xe7\x3a\x3a\x01\x3d\x32\x39\x86\x47\x8e\x11\x2b\x08\x02\x44\
-\xc8\x72\x9d\x24\x93\x44\x70\x91\x62\x3b\xf2\xfc\xe6\xa3\x31\x98\
-\xff\x2b\xc4\xa2\x8a\x09\x6a\xae\xda\xe2\xba\x6e\xdf\x43\x49\xd5\
-\x51\xcd\x1b\xf3\xa7\x94\xc0\x81\xbf\xca\xc0\x41\xda\xc4\xa1\xb1\
-\x56\xde\xca\x49\x2a\x7b\xb3\x12\x20\x28\xb9\x6b\xb9\x9b\xbf\x76\
-\x17\x6f\xf3\x26\xb7\x6e\xeb\x8f\x9f\x40\x96\x63\x0c\x0c\x9e\x25\
-\x16\x7f\x3b\x19\xce\x10\xe5\x08\x3e\xae\xe0\xc1\xca\x10\x7b\xf1\
-\xf3\x24\x49\xba\xe6\x9d\x34\x41\x55\xd2\x0c\x01\x9f\x5c\xee\xf3\
-\xaf\x59\x9c\x79\x76\xdd\x00\xd6\xe7\x03\x07\x55\xea\x0b\x0e\xb8\
-\x02\xa4\x0d\x19\xb6\x14\x1e\x89\x98\x74\x6f\xc5\x4e\x91\xd6\xc1\
-\x86\x9b\xf4\x4a\xf2\x94\x1c\x6f\xdc\x2c\x45\xbf\xf2\x94\xe9\xf0\
-\x64\x34\xb9\x8d\x14\xdb\x66\x92\x97\x9d\x37\x24\x81\xb7\x50\x92\
-\x68\xa6\x90\x03\xde\x2d\x84\xf8\xb5\x46\x1c\x0d\x77\x09\xf4\x02\
-\xf0\x0f\xe5\x5c\x37\x9b\x01\x76\x38\xc0\x5e\x85\x4c\x9a\x0c\x91\
-\xa6\x30\x57\xac\x09\x0a\x5a\x94\xb4\x6f\xe0\xb6\xd1\x5c\xa8\x61\
-\xe6\xf4\x73\xcc\x11\xa4\xa0\xf7\x8c\xcf\xc2\xe9\xaf\xec\xa9\xbe\
-\x59\xd3\x9c\x23\x87\xd2\x73\xae\x34\xeb\xf9\xab\x42\x88\x2f\xac\
-\x84\xf9\xd6\x5c\xb5\xb9\x73\xdd\xbe\x8b\x12\x75\x53\x75\xdd\xa2\
-\xf9\x52\x05\x4f\x15\x79\xcd\x12\xd8\xc3\x0e\xf6\x85\x1c\x1c\x9e\
-\xfe\x7a\x22\x5a\x7a\x0b\x12\x9e\x96\xbf\x76\x17\x6b\x12\x3c\xfa\
-\xa0\x9f\x37\x17\x6d\x30\x6e\xaa\x92\xe6\x28\xf0\xd7\x2b\x65\xbe\
-\x35\xe2\xcc\x2d\x79\x7a\x50\xb2\x0d\x7e\x52\xc6\x92\xd0\x17\x81\
-\x0b\x13\x4a\xe2\xe8\x4c\xfc\x49\x58\x78\x72\xdc\xc5\x49\x59\x22\
-\x06\x10\x8f\x51\x92\x25\x37\x3d\x7f\x6d\x3a\x7e\xbf\x97\x66\x43\
-\x8e\xd4\x82\x0f\xc2\x08\xca\x45\x67\x21\x3c\xc0\x47\x84\x10\xe9\
-\x95\x32\xd7\x9a\xab\x36\x7f\xae\xdb\x8b\x28\xe2\x88\xaa\x17\xa6\
-\x46\x09\xee\x77\x80\xab\x9a\x42\x6b\x99\xeb\xad\x01\x1a\x0d\x32\
-\x61\xf7\xe6\x52\x35\x51\x53\x92\x5b\xed\x83\x6c\x2e\x7e\xfd\x5c\
-\x1b\x47\x7f\xb4\x6d\x46\xe1\xf8\xb9\xc3\x24\x70\x89\xe2\x6a\xce\
-\x24\xf0\xc4\x6c\x7a\x9c\x6a\x16\x67\x75\x59\x9f\xef\xa0\xf4\x38\
-\xfd\x99\xda\xf3\xb4\xac\xe4\xba\xdd\x98\xa4\x7c\x4f\x9c\x29\xdf\
-\x6d\xfb\xb8\x0b\x73\xc2\x40\x1c\xb9\xd4\x8a\xa4\xcc\x6c\xbe\x9b\
-\xbf\x36\x1d\x0f\x8f\xb1\xaf\x2d\xce\xc2\xe4\x80\xa5\x51\x92\x37\
-\x4b\x7f\xcb\x1f\xaf\x34\xd2\x68\x16\x67\xe1\xac\xcf\x13\x28\x21\
-\xd8\x0f\x42\x69\xf7\xb1\x06\x3d\x3c\xd0\x54\x55\xe0\x20\xbd\xdd\
-\xca\x8d\x46\x23\x0f\x14\x3f\xb0\x87\x38\xdd\x32\x56\x1a\x10\x08\
-\x98\x39\xf7\xe5\xbd\xe5\x25\xa8\xe6\x04\x32\x4a\xf2\x4c\x69\xd5\
-\xd1\xf7\x84\x10\x2f\xac\xc4\x39\xd5\x2c\xce\xc2\x58\x9f\xa3\xf9\
-\x3a\x9f\xb5\x79\x02\x15\xa8\x80\xc6\xb2\x4a\xe0\x60\x70\xe6\xc0\
-\x81\x31\x90\x55\x2f\x8a\x8b\x3a\x78\x58\x56\x49\xa1\x6c\x49\xf2\
-\xf0\x7e\x1f\xf3\x2b\xf0\x71\x47\x95\x34\x27\x50\x94\x54\x57\x24\
-\x34\x8b\xb3\x38\x16\x68\x17\xf0\x0b\x28\x3d\x97\x34\x9b\x94\xb3\
-\x8f\xb9\xcc\x96\x66\xd2\x71\x79\xb7\x1d\xd5\x66\xc0\xad\x23\x1c\
-\x6e\x88\xf0\x64\xf1\xeb\x59\x09\xef\xe7\x1f\xa1\x25\xad\x9f\x07\
-\xfd\x69\x7f\xde\x45\x2b\xc4\x6d\xe0\x31\x21\xc4\xe8\x4a\x9d\x43\
-\xcd\xe2\x2c\x8e\x05\xba\x04\xec\x43\x45\x9e\x2a\x98\x82\x73\x7e\
-\x45\xeb\x40\x0d\xc9\x2c\xf7\x95\xdb\xea\x82\xad\xec\x44\xa5\x72\
-\x55\x2f\xb3\xfe\xb9\x1b\xcc\x7d\xd7\x83\x18\x6a\xc1\xf7\x20\xf0\
-\xfe\x95\x4c\x1a\x8d\x38\x8b\x4b\x9e\x00\xf0\x5e\x94\x32\xed\x70\
-\x71\xe0\xe0\x52\x48\x51\xd9\x29\x0e\x1c\x48\x12\xc6\x70\x06\x55\
-\x5a\xe5\xf5\xd7\x54\x09\xd2\x15\x60\xff\xba\x58\x7d\x2d\x47\x54\
-\x71\x37\xe3\xb9\x30\x40\x9e\x04\x9e\xce\xb7\x5c\x59\xd1\xd0\x5c\
-\xb5\xa5\xe1\xba\x75\x00\xff\x84\xd2\x24\xb8\x24\x70\xb0\xa3\x09\
-\x1a\xa7\x05\x0e\x6c\x3a\xd8\x61\x57\xff\x2c\x4b\x94\x0b\x6b\x86\
-\x51\x15\x47\x9f\x34\xd2\xf3\x57\xfb\x98\x1b\xfd\xe9\x6b\x40\xa0\
-\x24\x44\xf0\xac\x10\xe2\xc7\xab\x61\xce\x34\xe2\x2c\x2d\x02\x7d\
-\x00\xf8\x67\x8a\xea\x66\x24\x94\x5c\xb7\x0d\xf9\x84\x7f\x59\x86\
-\x7d\x15\xe4\x35\x8a\xf3\xd7\xa6\xe3\x67\x9d\x9c\x3a\xbc\x6e\x96\
-\xad\x16\xbd\xf9\x80\x40\x21\xfe\x52\x08\xf1\xb5\xd5\x32\x57\x9a\
-\xab\xb6\xb4\xdc\xb7\x9f\x03\x0f\x00\xbf\x2c\xde\xca\x6f\x46\xe0\
-\xcd\x20\x24\x73\x0a\x93\x2a\xed\x77\x93\x4e\xca\x9e\x2f\x3e\x70\
-\x9b\xcd\x96\x6c\xd5\xbd\x7d\x4a\x11\x52\x25\xcd\xbf\xae\x26\xd2\
-\x68\xc4\x59\x9a\xe4\x09\xa3\xf4\xf2\x29\xd1\x17\x9b\x48\xc3\x59\
-\x3f\x4c\xa4\x2a\x4b\x54\xa9\xe5\xaf\x4d\x9b\xf0\x35\xdd\xd7\x78\
-\xa3\xae\x2f\x97\x44\x2d\x82\x76\x32\x7f\x4e\x43\x23\x8e\x86\xc5\
-\x26\x4f\x14\xf8\xed\xfc\x49\xa2\x00\x19\x59\x11\x47\xcc\x54\xb0\
-\x38\xe5\xf2\xd7\xee\xe2\xbe\x10\x07\x37\x45\xd4\x4b\xc0\xcb\x7f\
-\x68\x9e\x34\x99\x12\xa7\xed\x19\x21\x44\x4a\x23\x8e\x86\xa5\x42\
-\x1e\x3f\x8a\x28\xbc\x6a\xca\x8c\x6f\x86\xae\xf3\xc1\xd6\x8a\xca\
-\x9c\xfa\x17\x2e\x93\x40\x2d\x41\xa6\x1c\x6e\x53\x74\x6d\x4b\x32\
-\x4f\x9a\x91\xd5\x38\x3f\x1a\x71\x96\x36\x79\x06\xf2\xe4\x99\x28\
-\x7e\x36\x3c\x83\xb8\x54\xb9\xfc\xb5\xbb\x68\xc8\xd2\xf5\xbe\xc1\
-\x2a\x85\xe1\x47\x51\x91\x66\xe4\xd3\x2b\x31\x07\x4d\x23\xce\xca\
-\x21\xcf\x65\xe0\x03\xf9\x1d\xfe\x1e\xae\x4c\xc2\x9d\x19\x52\x74\
-\xa6\xeb\xaf\xa9\xe1\x3d\x83\x3c\x60\x4b\x97\x92\xb2\x00\x11\x94\
-\xfe\x01\x85\xf8\x8e\x10\xe2\x87\xab\x79\x5e\x34\xe2\x2c\x0f\xf2\
-\x1c\x01\xfe\x60\xba\x6b\x15\xcd\xc2\xab\x43\xf0\xab\xb1\xf2\x6a\
-\xa2\xe5\xf2\xd7\xa6\x4d\x7e\xcb\xf3\x57\x29\xaf\x3f\xad\x9e\xf1\
-\x7c\x15\xa5\xa7\x10\x1a\x71\x34\x2c\x07\xf2\xbc\x02\x7c\xbe\xf8\
-\xf5\xb7\x42\xf0\x83\x01\x18\x54\x51\x27\x93\x25\xf4\x61\x7b\xe5\
-\x6c\x81\x4d\x11\x0e\x6e\x0b\xa9\x34\xc2\x92\x81\xde\x62\x3b\x47\
-\x1a\xf8\xf8\x72\xd5\x42\xd3\x88\xb3\x7a\xc9\xf3\x75\x94\x0c\x83\
-\x42\x97\x2c\x03\xaf\x78\xe1\xd7\x63\xa5\xd1\xb6\xb1\x66\xf6\xe7\
-\xa0\x52\x7f\x05\xdd\x47\xaf\xb2\x5e\x2a\x0e\x13\x0c\x00\xa5\x4d\
-\x44\xbe\x24\x84\x78\x43\x9b\x09\x8d\x38\xcb\x11\x7f\x8a\x4a\x0f\
-\x1f\x80\x9e\xbc\xf5\xf1\x4c\xb3\x07\x26\x33\xd2\x4d\x83\xba\x56\
-\xdb\x5d\xac\xc9\xd1\xf4\x8e\xde\x69\x2f\xf8\x51\x8b\xe5\x1d\x06\
-\xbe\xaa\x0d\xbf\x02\x2d\xe5\x66\x99\xa2\xbb\xbb\xfb\xcf\x80\xaf\
-\xa1\x22\x60\x28\x01\x7b\x9c\xf0\xb8\x0b\x0c\x12\x84\x43\xb0\x6b\
-\xac\xf2\xe7\xc5\x80\xbf\x7d\x10\x26\x25\x14\x8d\xe7\x42\x1b\x15\
-\x00\x76\x2f\xb7\x8e\x02\x1a\x71\x34\x94\x23\xcf\x53\xc0\x2b\x40\
-\xb3\xda\xf3\x66\x23\xbc\x6f\x2d\x74\x58\xc0\x7c\x13\xd6\xce\x30\
-\xd5\xbf\xb1\xc0\x7f\x4a\x40\xe9\x09\xe6\x83\xf9\x74\x20\x0d\x1a\
-\x71\x56\x0c\x79\xb6\x00\x3f\x07\x54\xfb\x77\x4a\xc0\xc3\x4e\xe8\
-\xca\xc0\x83\xf9\x0b\xcc\x2c\xca\xb5\xcc\x50\xde\x23\xbb\xfb\x37\
-\xa9\xfe\x5f\xfc\x9d\x10\xe2\x8b\xda\x48\x6b\xc4\x59\x89\xe4\xb1\
-\xe4\xcf\x1f\x9f\x01\x75\xe5\x4e\x97\x01\xb6\x66\x14\x82\x8c\x50\
-\x9c\x39\x53\x16\xaf\x02\x1f\x15\x42\x68\x8b\x44\x23\xce\x8a\x77\
-\xdd\x7e\x00\x33\x34\x78\xaa\x0e\xa7\x81\x77\x6a\xa1\x67\x75\x68\
-\x51\xb5\x15\x04\x21\xc4\xaf\x80\x2e\xe0\xc7\x54\x0e\x41\x57\x82\
-\x8c\xa2\x85\xfd\x3e\x8d\x34\x9a\xc5\x59\x8d\xd6\xa7\x1d\xf8\x04\
-\xf0\x1c\x94\xca\x49\x95\xc1\x45\xe0\x79\x21\xc4\x09\x6d\x04\x35\
-\xe2\x68\x24\xea\xee\x7e\x14\xa5\x8b\xf6\x96\xfc\xdf\x66\x94\x2a\
-\xd3\x01\x14\x45\xb4\xcb\x40\x0f\xf0\x9a\x10\x22\xa3\x8d\x98\x46\
-\x1c\x0d\xe5\xc9\x24\x69\x87\x7e\x8d\x38\x1a\x34\x68\xc1\x01\x0d\
-\x1a\x34\xe2\x68\xd0\xa0\x11\x47\x83\x06\x0d\x00\xff\x0f\x78\x5e\
-\xe6\x58\xdb\x50\x81\xdf\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\
-\x60\x82\
-\x00\x00\x02\xc8\
-\x89\
-\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\x40\x00\x00\x00\x40\x08\x03\x00\x00\x00\x9d\xb7\x81\xec\
-\x00\x00\x00\x03\x73\x42\x49\x54\x08\x08\x08\xdb\xe1\x4f\xe0\x00\
-\x00\x00\x09\x70\x48\x59\x73\x00\x00\x37\x5d\x00\x00\x37\x5d\x01\
-\x19\x80\x46\x5d\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\
-\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\
-\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x1f\x74\x45\x58\
-\x74\x54\x69\x74\x6c\x65\x00\x47\x6e\x6f\x6d\x65\x20\x53\x79\x6d\
-\x62\x6f\x6c\x69\x63\x20\x49\x63\x6f\x6e\x20\x54\x68\x65\x6d\x65\
-\x8e\xa4\x29\xab\x00\x00\x00\xb4\x50\x4c\x54\x45\xff\xff\xff\xff\
-\xff\xff\x80\x80\x80\xbf\xbf\xbf\xcc\xcc\xcc\xbf\xbf\xbf\xc6\xc6\
-\xc6\xb3\xb3\xb3\xc8\xc8\xc8\xc3\xc3\xc3\xba\xba\xba\xc4\xc4\xc4\
-\xbd\xbd\xbd\xb9\xb9\xb9\xb9\xb9\xb9\xbf\xbf\xbf\xbc\xbc\xbc\xbd\
-\xbd\xbd\xbe\xbe\xbe\xbd\xbd\xbd\xbc\xbc\xbc\xbe\xbe\xbe\xbe\xbe\
-\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbf\xbf\xbf\xbd\xbd\xbd\xbe\xbe\xbe\
-\xbf\xbf\xbf\xbd\xbd\xbd\xbf\xbf\xbf\xbe\xbe\xbe\xbe\xbe\xbe\xbe\
-\xbe\xbe\xbe\xbe\xbe\xbd\xbd\xbd\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\
-\xbe\xbe\xbe\xbe\xbd\xbd\xbd\xbf\xbf\xbf\xbe\xbe\xbe\xbe\xbe\xbe\
-\xbe\xbe\xbe\xbf\xbf\xbf\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\
-\xbe\xbe\xbe\xbe\xbe\xbd\xbd\xbd\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\
-\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\
-\xe4\x72\x0e\xe3\x00\x00\x00\x3b\x74\x52\x4e\x53\x00\x01\x02\x04\
-\x05\x08\x09\x0a\x0e\x11\x1a\x1a\x1f\x21\x2c\x2c\x35\x36\x3b\x3e\
-\x41\x43\x47\x4b\x4e\x50\x55\x56\x57\x59\x63\x66\x71\x86\x89\x90\
-\x95\x96\x9d\x9e\xa7\xaa\xad\xb5\xb8\xbe\xc0\xc3\xc5\xc9\xcd\xd6\
-\xe0\xe6\xef\xf5\xf7\xfc\xfd\xec\xba\xa4\x27\x00\x00\x01\x14\x49\
-\x44\x41\x54\x58\xc3\xed\x95\x5b\x57\x82\x40\x14\x85\x9d\x44\xb4\
-\x40\x50\x29\x92\x64\xb2\xd4\xee\xf7\xbc\x54\xf2\xff\xff\x97\xb2\
-\x98\x91\xe2\x20\xed\xe3\x4b\x2b\xe4\x7b\xfb\xd6\x39\x7b\x73\x99\
-\x87\xa9\xd5\x2a\x8a\x68\x58\xae\x17\x04\x9e\x6b\x35\x76\x8a\x0b\
-\x27\x94\x8a\xd0\x11\xfc\x7c\xcb\x97\xdf\xf0\x5b\xdc\x7c\x7b\x20\
-\x7f\x30\x68\x33\x9f\x9f\xc9\xaf\x1b\x58\xef\x20\x7c\x49\xf0\x39\
-\xff\xc1\x91\x39\x38\x8c\xf3\x0b\xf3\x0a\x42\xfc\x34\x2d\x9d\xe9\
-\xdb\xa6\x69\xf7\xb5\x59\x70\x81\xab\xf3\x46\x6c\x86\x6e\x70\xe1\
-\x02\x4f\x25\xec\x44\x6d\xa5\x1e\x5c\x10\xa8\x84\x99\xa8\xa9\x34\
-\x80\x0b\xf4\x47\x6f\xf3\xb2\x17\xc8\x5f\xa8\x0a\xfe\x45\xc1\xc9\
-\x9a\x6d\xe1\x78\x56\xdd\x76\xc5\x74\xc7\x6f\xef\x93\x63\xdc\x33\
-\x18\x97\x5f\x51\xcc\x55\x13\x73\xc2\x28\x52\xdc\x60\x9e\xa5\xb7\
-\xd4\x0b\xd1\x29\xe2\x84\xeb\xcd\x3c\xba\x47\x9c\x30\x4d\x17\xe6\
-\x88\x13\x66\xe9\xc2\x42\x00\x4e\xb8\x4d\x17\x1e\x11\x27\x9c\xa5\
-\x0b\x43\xc4\x29\x77\x7a\xfe\x24\x20\x27\x1c\x3e\x24\xf3\x97\x23\
-\xcc\x73\x38\x7f\xfe\xf8\x7c\xbd\x38\x80\x3d\x87\xba\xc1\xf3\x3d\
-\xa0\xa3\xd8\xb9\x80\x7d\xad\xff\x6d\x81\x64\x52\x15\x94\xb3\x60\
-\x8f\x59\x01\x25\xba\xb5\x2a\xd7\xa3\x29\x75\x00\x00\x00\x00\x49\
-\x45\x4e\x44\xae\x42\x60\x82\
-\x00\x00\x27\x74\
-\x89\
-\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\x80\x00\x00\x00\x66\x08\x06\x00\x00\x00\x03\x23\x99\x54\
-\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\
-\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x02\x3a\x00\x00\x02\x3a\
-\x01\xfe\x36\x29\x51\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\
-\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\
-\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x26\xf1\x49\x44\
-\x41\x54\x78\xda\xed\x9d\x05\x58\x54\x59\xff\xc7\x7f\x77\x86\x14\
-\x90\x50\x14\x41\x94\x10\x51\x09\x45\xb0\x50\xb0\x10\x10\x13\x6b\
-\x2d\x6c\xb0\xdd\xb5\xdb\x35\xd7\x5a\x5b\x09\xdb\xb5\xdb\xb5\x0b\
-\x19\x42\xba\x15\x24\x0c\x14\x29\x15\x29\x25\x06\x66\x7e\xff\x73\
-\x86\xb9\x38\xb0\x60\xbc\xaf\xfe\x5f\x70\x67\x9e\xe7\xf3\xec\xaa\
-\xc0\x1d\xe6\xfb\xb9\xe7\x9e\x3e\x80\x88\x20\xe5\xdf\x8b\xf4\x43\
-\xf8\x9a\x0f\x09\x40\x51\x2a\xc0\xbf\x27\x6c\x5d\x42\x77\xc2\x28\
-\x42\x34\x21\x9f\xc0\x27\xcc\x21\x34\x94\xf8\x3a\x19\xa9\x00\x3f\
-\x4f\xe8\xfa\x84\x49\x84\xab\x84\x62\x02\xd6\x40\x1e\xe1\x00\x21\
-\x96\xb0\x59\x2a\x40\xdd\x0f\xbe\x1d\xe1\x2d\x1b\x70\x23\x79\x79\
-\xec\xa7\xa5\x85\xab\x5b\xb7\x46\x2f\x73\x6b\xf4\x6c\xb0\x12\x97\
-\xa8\x4e\xc6\xd1\x4a\x4e\x68\x2d\xdf\x0e\x75\xb8\x8d\x90\x03\x1c\
-\x56\x86\x9b\x52\x01\xea\x5e\xe0\xc3\x08\xbb\x08\xd3\x09\xf3\x08\
-\x6f\x54\x64\x64\x70\x93\xa9\x29\xbe\x74\x74\x44\x1c\x32\xa4\x82\
-\xe2\xc1\x83\x05\x99\xba\xde\xc2\x77\xba\xfe\x28\x49\x82\xf6\x55\
-\x6c\x29\xdb\x9c\xfe\xb0\xd4\x9f\xe1\x31\xf0\x6f\x13\x60\x11\x7b\
-\xb7\x73\x18\x06\x27\xe9\xe9\x61\x66\xbf\x7e\x95\x82\x97\x24\xdb\
-\xe4\x68\x5c\x55\x01\x28\x8f\xb5\x2f\xa3\x9e\x8c\x0e\xfd\x39\x01\
-\x04\x6d\xa9\x00\x75\x23\x7c\x2f\x1a\xbc\xa5\x66\x7d\xf4\xb0\xb0\
-\xc0\x94\x2a\x77\x7c\x75\x14\x77\x5f\xe9\x53\x9d\x00\x94\xa8\x26\
-\xe7\x45\x8f\x04\xf2\x33\x93\x08\x06\x52\x01\x6a\xbf\x00\x9a\x84\
-\xe7\x2b\x26\x70\x30\xca\xa5\xfd\x17\xc3\xa7\x94\x0d\x18\x17\x56\
-\x93\x00\x94\xb0\x26\xa7\xb1\x31\xb7\x01\xfd\xe1\x45\x84\x38\x82\
-\x8e\x54\x80\xda\x2b\x00\x43\xc8\x98\x3b\x06\x30\xf5\x06\x60\xc9\
-\x1c\x9b\xf8\x2f\x4a\xe0\x3c\x24\xff\x5d\x33\xbf\xb2\xcf\x49\xf0\
-\x50\xeb\x38\x36\xe0\xa8\xb2\x15\xc3\x35\x3f\xf8\xfd\xdb\x12\x8e\
-\x88\x1f\x3d\x43\xe9\xdf\x49\x05\xf8\xb6\x0f\xd1\x63\xe2\x40\xc0\
-\x1c\x1e\xe0\xfb\x07\x90\x57\x3a\xab\xfb\x17\x25\xc8\x6b\x75\xe6\
-\xc9\xe7\x04\xa0\xb8\x6b\xac\x60\x05\x08\xfb\x01\xef\xd9\x98\x70\
-\x81\xca\x4b\xaf\xa1\xa4\xda\x00\xad\xec\x7f\x41\x0e\x87\x4b\xff\
-\xf1\x11\x15\x41\x2a\xc0\xd7\x7f\x98\x9e\x43\x7a\x95\x0b\x20\xc2\
-\x07\xde\x97\xce\xe8\x91\xf0\x39\x01\x8a\xba\xad\xe1\x7d\x49\x80\
-\xe9\x03\xf7\xa3\x4d\xa7\xc9\xac\x04\x7f\x7e\xc7\xf7\x3b\x88\xf6\
-\x3d\x34\x92\xd7\xc2\xfa\xb6\xe3\xb1\xdb\xbe\x68\xec\xeb\x1e\x8e\
-\x93\x4e\x25\x09\xe7\x1c\x8b\xc2\x96\x96\x3d\xd8\x6b\x3e\x24\x58\
-\x48\x05\xf8\xfc\x87\x29\x4f\xc8\xe9\x69\x25\x21\x00\xe5\x01\x64\
-\x97\x4e\xef\x95\x54\x63\x3d\xa0\xdf\xc4\xe0\xcf\x85\xef\x6d\x1b\
-\xf0\x68\xd3\xf2\xe7\xb8\xfc\xd7\x60\x36\x8c\xb4\xef\x54\xdc\xaf\
-\xe6\x32\x5c\xe1\x9e\xd6\x1e\xd8\x65\xf8\x45\x64\xd6\x3d\xc1\x4e\
-\x07\x9e\xa6\x39\xdd\x42\x64\x71\xbc\xf2\x11\x3b\xad\xb9\x86\x5c\
-\x05\x25\x51\x8b\x44\x2a\xc0\xe7\x3f\x54\x2b\x1a\x90\x85\x71\x15\
-\x01\x44\x25\x01\xf3\xb6\x6c\x6a\xaf\xa7\xd5\x09\x20\x1c\x32\x34\
-\xbb\xa6\xf0\xdf\x36\x0b\x28\xfb\x73\xc9\xf3\x64\x2a\xc0\xba\xc5\
-\xf1\x58\xaf\xe5\x50\x7a\xa1\xa7\xff\xed\xb8\x03\xe1\x9c\x22\x47\
-\x11\xcf\xb7\xbb\x82\xa3\x06\xdd\x10\x02\x09\x9f\xc2\xac\x4f\x10\
-\xd8\x9c\xc9\x8e\x1b\x72\xb2\x18\x6d\x3c\x62\xb1\xdd\xa2\x13\xa8\
-\xd0\xb0\x29\xfd\xa6\x77\x84\x66\x75\x52\x00\x71\x30\x46\xdf\xa3\
-\x42\xf3\x85\xeb\xb4\xa1\x02\xe8\x6b\x57\x23\x00\x21\x97\xc7\x64\
-\x09\xa6\xf4\x7e\x5e\x9d\x04\x39\x2d\x2f\x3d\xab\x4e\x80\xdb\xfd\
-\xa3\xfd\x69\xf8\x94\x51\x2e\x97\xb1\xe9\xcc\xb7\xf4\x42\x31\xff\
-\xc5\x7b\xec\x4a\x08\x57\x97\x55\x47\x9f\x8e\x01\xb8\xbc\x9f\x3f\
-\x1f\xd6\xc6\x0b\x68\xf8\xf2\x6b\x13\x0b\x7a\xce\x7b\x19\x36\x7b\
-\xd2\x8b\x0f\x33\xc6\xc4\xb0\xa5\x0d\x5f\xdc\x2d\xdd\xb5\x4e\xd6\
-\x01\xc8\x6b\x3e\x41\x20\xd1\xcf\xbe\x91\x50\xef\x07\x5d\xab\x19\
-\xbd\x8e\x7a\xfd\xea\x05\x10\x49\xf0\x80\xc9\x14\x4c\xb6\x4b\xa9\
-\x2a\x40\x61\x97\x3f\xfc\xaa\x86\x9f\xa5\x17\x50\xb4\x79\xe9\xf3\
-\x74\x1a\xfe\x8a\x25\x2f\x22\xf5\xe6\x95\x60\xb3\x05\x02\x64\x38\
-\x72\xb1\xdf\xf8\xbe\x5a\xd3\xe2\x9e\xf0\x44\x51\x46\x19\xed\x0d\
-\xc7\xe0\x8d\x8e\xf7\xf1\xa8\x53\x5c\x21\x09\xbe\x50\x6f\xd5\xcb\
-\xc7\xe3\xa6\xa7\x3e\x5a\x31\xb3\x50\xb0\xe6\x57\xc4\xd9\xe3\x12\
-\xb0\x6d\x2b\x17\xfa\x8d\xf7\xea\x6c\x33\x90\xbc\x38\x84\xbd\xa2\
-\xe0\x3b\x8f\xc3\xb6\xee\xbe\xa8\xda\x67\x12\x82\xbc\xe8\x59\x96\
-\x46\x58\x49\x68\xfc\x9d\xaf\x59\x4f\xd4\x03\xc8\x21\x2d\x00\x9f\
-\x9a\x25\x20\x8f\x83\x74\xc1\x64\xfb\x57\x92\x02\x94\x3a\x4d\x79\
-\x58\x55\x80\x8b\x23\x1e\xf1\x68\xf8\x6b\x97\xa7\x3c\x36\x58\x50\
-\x56\xd8\x6c\x81\x10\x29\x5c\x65\x6d\x7a\x31\xcb\x2f\xbc\x17\x5a\
-\xe2\xad\xa0\xb5\x78\x86\xe1\xa0\xa5\x76\x2f\x5c\x6a\x73\x08\x6f\
-\x8c\x79\x83\xde\xe3\x3f\xe2\x61\xc7\xa0\xd7\xc3\x17\xbd\x09\x5a\
-\x31\xb7\x38\x83\x86\xbe\x62\x56\x2e\x4e\x1e\xf5\x37\xea\x36\xe9\
-\xc8\xde\xf9\x2f\x69\x93\xb0\x4e\x0a\x40\x5e\xb2\x84\xd3\x20\xab\
-\x88\x30\xee\x10\xca\xac\x7d\x54\x36\x80\x14\x9a\xfd\x22\x05\xa8\
-\xb5\xe8\x14\x32\xdd\x5d\x59\x11\x4a\x08\xe7\x09\xd3\xe8\x5d\xf2\
-\x9d\xae\x5d\x4a\x3f\xc0\xf4\x3b\x50\x52\xa3\x00\x14\x6f\x26\x4d\
-\x30\xc1\xfe\xf5\xa7\xfe\x80\xa1\xe9\x92\xe1\xa7\xb5\x78\xf8\x7e\
-\xd3\xb2\xe7\xb9\x1b\x96\xbd\x78\x66\xbc\xb0\xec\x3d\x1b\x3e\x45\
-\xae\x91\x05\xbd\xd0\xe2\x1a\x46\x1c\x97\x10\xa2\xe8\x7b\xd0\x53\
-\x6b\x8d\x6e\x96\xeb\xf1\xd0\xc0\x20\x3c\xe4\x74\x1d\x8f\xd9\x6d\
-\x29\xe2\x39\x4c\x49\xf5\x1b\xec\x70\x6f\xd1\x8c\x5d\x89\xdb\xb6\
-\x79\xe7\x78\x79\x46\xe0\x86\xf5\x37\xb1\xad\xb9\x2d\x8a\x47\x29\
-\xb7\x10\xfa\x10\xb8\x75\xb2\x23\x48\x3c\x18\x13\x00\x8d\x8d\x11\
-\x56\xc5\x22\x78\x21\xb6\xba\xfc\x21\x82\x0a\xc0\x62\x71\x2c\x86\
-\x2f\x33\xfd\x28\xc2\x5c\x62\xbb\xbd\x0c\x82\x2e\x31\x9e\x11\x59\
-\x9f\x25\x6e\x0f\x53\x21\x9a\xfe\x87\xe3\xfc\x54\x2a\x4c\xbc\x04\
-\x85\x9f\x15\x40\xd4\x3a\x60\x5e\x09\x26\x3a\xa6\xb1\x12\xbc\x37\
-\xba\xf6\x8a\x15\xe0\xf8\xc4\x78\xde\x1f\xcb\x5e\xa4\x9b\x2c\x2c\
-\x49\x93\x0c\x9f\xa2\xa0\xe7\x40\x2f\x36\x5d\x7c\x4d\x6d\xc2\x6f\
-\x84\x60\xd1\x68\xa3\x0a\xe0\xf8\xe6\x4d\x71\x6f\x47\x07\xbc\xd5\
-\xdd\x09\x53\xed\x74\x05\xe8\x20\x87\x94\x8f\x4e\xb2\x61\x27\xc6\
-\x37\xbb\xe3\xe6\xba\x9b\xef\xe6\xe6\x85\xe3\xc7\xff\x89\xfd\xfa\
-\xb9\xa2\xac\xac\x3c\xfd\x41\x7e\xb4\x0e\x53\xa7\x7b\x02\xc5\x95\
-\x30\x3e\x74\x18\x85\xb0\xbb\x40\x14\x3e\x78\x0a\x72\xfb\x45\x61\
-\xb1\xa4\x00\x94\xbe\x61\xfc\x22\xcd\x75\xa7\xb2\x80\x37\x49\x08\
-\x3b\xda\x22\x6c\xe1\x22\x4c\x21\x12\x58\x13\xd4\x2a\xc6\xe6\xe9\
-\x64\x8d\xf5\xb4\xb8\xa5\x77\x04\xc1\x84\x30\x9e\xb0\x9b\x70\x99\
-\xb0\x96\xe0\x2c\xee\x48\xd1\x20\xf8\xb3\x03\x41\xbc\xfd\x5f\x08\
-\xbf\x42\x02\xce\x4b\xc1\x38\xc7\x0c\x51\x3d\xa0\xd3\x36\x7f\x1a\
-\xfe\x8b\xd6\x81\x69\xa4\xe8\xcf\x6c\xbf\x20\xef\x65\xd5\xf0\xb5\
-\xa7\x24\xa3\x7c\x93\xce\xf4\x1a\x0b\x09\xf7\x69\xfd\xa6\x21\xa9\
-\x73\xb8\x3a\x01\xde\xdb\x4c\xc4\x3b\x08\xc8\x06\xce\x22\x70\x90\
-\x7b\xee\x33\x9a\x1b\xba\x6a\x7c\xdb\xc8\xa9\x6e\x1e\x48\xc3\xb7\
-\xb3\x73\x43\x15\x15\x51\x17\x33\xed\xfc\x99\xf0\x23\x2b\xc8\xff\
-\x9f\xdd\xb0\x7e\xa2\xf0\x49\xf0\xdc\xbd\x25\x02\xcd\x3d\xa5\xf1\
-\x1d\x0e\x09\xaf\xad\x7f\x9f\xeb\xb3\x29\x37\xc7\xe7\x4f\xc2\x8e\
-\xdc\xf7\xbc\x5d\xf9\xd9\xbe\xfb\x3e\x64\xfb\xed\xfd\xf0\xd6\xd7\
-\x32\xfa\xde\x35\x48\x9c\x2d\x80\x08\xd7\x7c\xf0\x34\x7f\x0f\x1e\
-\x1c\xf2\xfd\x24\xc4\xd5\x84\x11\x04\x53\x82\xbc\x28\xd4\xec\xea\
-\x26\x70\x70\xb9\x5c\x54\x56\x56\xc3\x46\x8d\x9a\xa2\x9e\x5e\x6b\
-\x6c\xdd\xba\x03\x5a\x5a\xf5\xc1\x3f\x16\xb7\xfd\x3a\x01\x44\x8f\
-\x03\xce\x0b\xc1\x78\xa7\xcc\x32\x87\xe9\xa2\x8a\xe0\x81\xe9\x09\
-\xbc\x8e\xb3\x52\xd2\xd9\xd0\x75\xa6\xa5\xa2\x7a\xcf\x6d\x28\xa7\
-\xd5\xa1\xe2\xba\xea\xca\x80\x13\x1c\x00\x6f\x6e\x00\x2c\xba\x01\
-\x58\x7a\xbb\x9c\x9c\xd3\xf0\x4c\x22\xfc\xdc\xb8\x61\x32\xbc\xfd\
-\xae\x50\x30\x63\x84\xc3\x1b\x1a\xbc\xb3\xf3\x32\xd4\xd2\x6a\xc1\
-\x36\xeb\xdc\x68\x5d\xe9\xa7\x18\x0b\x20\x2f\x17\xfa\xc1\x18\x8d\
-\x3a\x8a\xbd\x97\x3d\x2f\x1a\xbc\x15\x4b\x09\x48\x19\xb9\x4f\x18\
-\x71\xa4\xe0\x4d\xe6\x19\x61\x06\xb2\x1c\xe3\xbf\x2e\xe8\x16\x1e\
-\x1c\x6b\x19\x1e\x8a\xc6\x31\xd7\x78\x90\x38\x0b\x45\x3c\x99\x91\
-\x0a\x67\xdb\xc5\x81\x17\x23\x14\x89\x40\xd9\x07\x28\xb7\x82\x8b\
-\x4d\x5c\x95\xb1\xfd\xb4\x26\xd8\x6b\xbd\xbe\x70\xd8\x49\xb3\xd2\
-\x95\x07\x37\xe1\xc9\x93\x8f\xfe\xc1\xdc\xdf\xef\x65\x8d\x5b\x1e\
-\x86\x2b\x96\x8e\xc5\xb7\xf7\x81\xff\x35\x12\xe4\xde\xe7\x3c\x13\
-\x8c\x1d\x18\x93\xd0\x36\x24\xa9\xcf\xbc\x77\x91\xda\x0b\x32\x51\
-\x65\xfa\x29\x94\xd7\xed\x4e\x1e\x4f\xe5\x13\x44\x54\x95\x00\x5d\
-\xec\x00\xaf\xae\x03\x2c\xbc\xfe\x29\xf4\x4a\x9c\x67\x82\x49\xf0\
-\x65\x59\x03\x65\x7d\x8f\x4d\x81\xec\x3d\x93\x98\x82\x09\x23\x26\
-\x08\xc6\x8c\xd9\x8c\x2d\x5b\xd2\x92\x83\xa1\x2d\x22\x77\x5a\x62\
-\xfd\x54\x83\x41\xf4\x99\x4d\x6b\xae\x8a\x6a\xba\x68\xbf\xf4\x39\
-\xb2\xe1\xb3\x0c\xdc\x54\xf4\x61\xd1\x9d\xd8\x34\x1a\xfe\xb6\x94\
-\x48\x61\xdb\x2b\x17\x91\x86\xcf\xa2\x7b\x6f\x47\xbc\x6c\xc2\xf4\
-\x97\xca\x4f\x5c\x1f\x37\x79\xe2\x12\x6d\x1e\xda\x27\xca\xfa\xa6\
-\xee\x8b\x7e\xde\x46\x85\xa3\x03\xcd\xd0\x35\xcc\x12\xdd\xc2\x2b\
-\xe3\x1a\x6a\xc5\xdf\x72\xce\x2b\x53\x32\xfc\xcd\x7b\xc2\x22\x68\
-\xf8\x2c\x8b\xd7\x79\x66\xbd\x7b\x20\x93\xfe\x45\x01\xfc\x98\x92\
-\xfc\x20\xe5\x80\x81\xee\x81\x17\x65\x02\x62\xa3\x21\x2a\x0a\x99\
-\x07\x0f\xb0\x37\xf9\xf8\xfa\xc8\x00\x2e\xe9\x08\x78\x71\x26\xe0\
-\xf5\x79\x80\xb7\x17\x41\x49\xc4\x06\x78\x99\xe1\x0e\x21\xef\x0f\
-\xc1\x43\x42\x60\xee\x61\xc2\x51\x08\xcc\xda\xc8\x5c\xb9\x34\x81\
-\x49\xf2\x72\x03\x5c\x37\x4a\xb6\x64\xdc\xc8\x05\x68\x65\x35\x00\
-\x65\x64\x2a\x9e\xf3\xed\x7e\xda\xd1\x40\xf1\xb3\xf8\x8d\xa2\x5a\
-\x33\x34\xe8\x3a\x0b\x5b\x74\x5f\x80\xc6\x76\x2b\xb1\x8d\xe3\x06\
-\x34\x1d\xb0\x0d\xcd\x9d\xf7\xa2\xdd\xcc\x95\xa8\x37\xd5\x15\x35\
-\x47\x0c\x47\x55\x5b\x1b\xd4\xed\x6d\x8c\x63\x3d\xb4\x71\xe6\x05\
-\x4d\x74\xbb\xa8\xfd\xe1\xb7\xc0\x16\xb1\xf3\x1e\x99\x06\xcd\x8c\
-\x6a\x17\x33\x2d\xd2\xe2\xc5\xd4\x70\xcb\xdc\xaa\xc1\x57\xa6\x7d\
-\xf1\xae\xbf\x8f\x24\xd0\xf0\xbd\x0e\xc5\x04\x8f\x5f\xf1\x29\x7c\
-\x96\x99\x6b\xee\xe4\xbf\xba\xab\x11\x47\x42\x2e\xcc\x8b\x90\x4b\
-\xfe\x98\xa4\x14\x5c\x98\xde\x80\x57\x9c\xaf\xe5\xc7\x2f\xd1\x89\
-\x2c\x15\xea\xbe\x2e\xc5\x66\x82\x37\x45\xda\x41\xbd\xa2\xa6\x05\
-\x40\x4c\x0c\x52\x34\x3d\x3c\x8a\x4e\x93\x8f\xef\x73\x5c\x56\x81\
-\x17\x7e\x66\xc0\x8b\x71\x84\xb0\x27\x83\xa1\xd0\xcf\x19\x42\x68\
-\xf8\xf3\x07\xab\x62\x4f\xdb\x91\xa8\xa4\xa4\xce\xce\x2c\x1a\xf9\
-\xaf\x18\x0e\xa6\x03\x16\x6c\x53\x8c\xe0\x4d\x7b\xbd\x24\xfe\xcc\
-\x7e\x18\x67\xc5\x1d\x23\xb4\x9d\xfc\x17\xc3\x40\x5e\x37\x67\x40\
-\xaf\x28\xc0\xfb\xf8\x4f\xee\x0a\x99\x92\xbf\x8b\xe5\xd3\x4f\x17\
-\x28\x3f\x39\xf4\xae\x41\xd8\xee\xf4\x26\xfe\x1b\x53\x9a\xf3\x56\
-\x26\x19\xf1\x16\xc6\xb5\x09\x98\x1b\x63\x11\xb4\xfb\xe2\x5f\xb7\
-\x7f\x59\x12\x14\x3a\x70\x61\x40\x80\xdd\x5c\x3f\x5e\x97\xd9\xde\
-\x3c\xd3\xe9\xb7\x02\xf4\xa7\x5c\x0f\x6f\x38\xe1\x52\x62\x83\x91\
-\x57\xef\xbe\x2b\x68\xf5\x88\x04\x8d\x35\x90\xbb\x3b\x86\x9b\xb5\
-\x3e\x8c\xc1\x7a\x31\xbe\x31\x54\x80\x11\xae\xae\xe1\x55\x03\x3f\
-\xcb\x85\x8f\x77\x74\x21\x24\xcc\x06\x78\xf1\x83\x20\x85\x84\x8e\
-\x92\x78\xf6\x02\x81\x4b\x2f\x0d\x6c\xa4\xa9\xc7\x36\xeb\x68\x25\
-\x56\xe9\x5f\x35\x1f\x40\xdc\x2c\x72\xa8\xd2\xf7\xdd\x91\x36\xd3\
-\x6a\xf8\xfa\x06\x74\x84\x8d\x36\xe1\xac\x07\x01\x7a\x44\x54\x2f\
-\xc2\x03\x84\xe2\x50\x52\x53\x48\x44\x78\xf8\x06\xc1\xa7\x10\x21\
-\xb8\x90\x2f\x9f\xb0\xe0\xfc\x6a\x1f\x9d\x8d\xf7\x6f\xc2\xc8\xd3\
-\x58\x2d\xc3\xcf\x47\x42\xb7\x50\x81\xae\xf3\xcd\xcc\x62\xa1\x5e\
-\x56\x75\x02\xf8\xa4\xa9\xf9\x6f\x08\x03\xa4\xcc\x0a\x33\x7e\x4e\
-\x04\x28\x76\xd7\xd4\xcc\xa5\xa1\x5f\x52\x81\x67\xfe\xe6\xf0\x20\
-\xd6\x11\x42\xe9\x5d\x4e\xe0\xc7\x3b\x43\x46\xf4\x00\x48\xf2\xb5\
-\x87\xe4\xd3\x5d\xe1\xfd\x36\x0b\xf2\x7d\x2d\x01\xad\x1a\x71\x59\
-\xd1\xaf\xd4\x96\x59\x44\x75\x69\x30\xa7\x05\xe1\x22\xfd\x00\x3b\
-\xf7\x07\xdc\x43\x2a\x5a\x97\x7d\x00\x03\x23\xa0\xa4\xb0\x98\x8e\
-\xc0\x41\x99\xe4\xb7\xf0\x12\xad\xa3\x7a\x6c\xbd\x9a\x62\xbd\xf1\
-\x56\x1e\x73\x3e\x39\xa5\x7a\x01\xce\x64\x82\x6d\xd0\x1b\xe8\x1a\
-\x86\x14\xfb\x79\xfb\x63\x49\xe0\x7c\xc9\xf0\xf3\x4a\x75\x22\xd9\
-\xf0\x59\xfa\xde\x72\xbb\x7a\x51\x1b\x6e\xdd\xb4\x82\xbb\xb7\xac\
-\x21\xf0\x82\x15\x3c\x3a\x6a\x06\x19\x07\xdb\x40\xd1\x01\x63\xc0\
-\xfd\x84\xe5\xcd\x01\xfb\x37\x00\xd4\x95\xaf\xd4\x32\xa1\x4d\xd1\
-\x3e\xd2\x19\x41\xff\x9d\x08\xd6\x84\x4b\xea\xea\xc0\x0f\x0a\xfa\
-\xe7\x97\xe4\x16\xd6\xcf\x99\x74\x64\x4f\x40\xd7\x4d\x37\x91\x62\
-\xbe\xd7\x2f\x1c\xee\x65\xd0\xb0\x73\xab\x08\x50\x06\xbd\x7d\xa2\
-\xd9\xf0\x59\x36\x9d\x9a\xe5\x2b\x21\x40\xa1\xc7\x23\xd9\x97\x6c\
-\xf0\x0b\xbc\x8c\xe2\x06\x19\xac\x0c\x18\x2c\xbf\xde\x9f\x06\x2d\
-\x89\x3b\xb9\xc3\x67\xe9\x00\xda\xa8\x92\x16\x81\x4c\xa5\xd0\xe9\
-\x80\xcd\x49\x3a\xe8\x25\x9d\x12\xf6\x7d\x45\x68\xa4\xac\x0c\xaf\
-\xee\xdd\xfb\xf4\xd7\x67\x43\x07\x05\xd9\x6c\xbe\xfe\x96\x0d\x9f\
-\xd2\xe0\xdc\x93\xd7\x22\x01\x26\x9c\x8f\xaa\x24\x40\xff\x9b\x3e\
-\x55\xc3\x67\x09\x4b\xec\xee\x4f\x05\x08\x7b\xdb\x80\xb7\x3e\x98\
-\x53\x3a\x7d\x6e\xaf\x87\x7d\x95\xff\x7c\xd4\x07\xf6\x21\xc5\x41\
-\x66\x51\x1a\x0d\x7d\xab\x21\x69\xfa\x35\x06\x34\x27\xed\x7e\x59\
-\xa6\x22\xf0\x5c\x82\x0f\x61\x1b\x61\x6c\x6d\x9f\x35\x5c\xe7\x67\
-\xf8\xc8\x93\x22\x76\xa7\x97\xa6\x70\xf0\x9e\xc3\x51\x92\xc1\x53\
-\xac\x37\xdf\xca\x65\xee\xa6\xa3\x48\x80\x05\x77\x78\x9f\x9e\xfb\
-\x17\x42\x49\xd0\xc2\x9a\x04\x90\xef\x11\x58\x1c\xff\xdc\xe2\xbe\
-\x4b\xdf\xf1\x3e\xf6\x9c\x3d\x19\x6c\xf0\x2c\xa6\xdc\x5e\xa8\xa7\
-\x00\xc8\x94\x07\xfe\x4c\xdc\xfb\x48\xbb\xb9\x0d\x7e\xf4\xb0\xb6\
-\x54\x80\xca\x02\x4c\x94\x51\x50\xc4\xf6\x73\xfe\xc4\xaa\xe1\x53\
-\x4c\x3c\x02\x9e\x88\xc2\xa7\x6c\x0f\x7d\x28\x16\x20\x0d\x6c\x42\
-\xde\x57\x13\xfc\x47\x85\x2e\x01\x61\x7a\xed\x2f\x3d\xe8\xd9\x6a\
-\x5b\xdc\xd0\xa6\xab\x5f\xf6\x01\xf7\x5c\xc9\xe0\x7b\xc3\x4e\xd4\
-\x02\x4b\xf6\x4e\x4f\xa1\xd7\xff\x9e\x03\x33\x52\x01\xbe\x5d\x80\
-\x4d\x1a\x46\x6d\x70\x52\x50\x2a\x0e\x38\x19\x14\x42\x42\xff\x20\
-\x29\x80\xfa\xc5\xa4\xdc\x0a\x01\xce\x3e\x7b\x41\xc2\xe7\x43\x2f\
-\xbf\xc7\xe2\xc0\xf9\xb2\xd6\x41\xd1\x3a\xed\xaf\xde\xef\xda\x7a\
-\x6f\xf8\x30\xfd\x95\x45\x23\xf4\x97\xa3\x24\x83\x1a\x6c\x08\x92\
-\x14\xa0\x09\x54\x0c\xc7\x7a\xd0\x29\x66\xd2\xa5\x61\xff\x7b\x01\
-\x22\xf4\x7b\xf5\x17\x09\x40\x19\x73\x3f\xf9\x79\xb7\xcd\xb7\x12\
-\xc5\xc5\x7f\x36\x73\x4f\x5c\xfc\x53\xee\x66\x94\xc9\x0d\xb8\x7a\
-\xb9\x91\xd5\xad\x7b\x1d\x4c\x3c\x03\x9c\x0d\x57\xbf\xaf\x1a\x78\
-\x75\x38\xca\xef\xf0\xa3\xe1\x77\x83\x35\xa4\xc8\x17\x75\xfb\xae\
-\x94\x2e\x0f\xaf\x25\x6b\xf6\x09\x65\x6d\x27\xcc\xae\x10\x80\x32\
-\xf1\xe1\xab\xa2\xde\x1e\xbe\xbe\x66\xfb\x7c\x83\xc8\xf3\x3f\x53\
-\xf5\x68\x9c\xaf\xd9\xf4\x4b\xbc\x41\x6d\xff\x48\xfd\x9a\xc0\xab\
-\x32\x5c\x6f\x45\xa1\x0d\xb3\xb6\xb4\x29\xd8\xb0\x13\x31\xe4\xa4\
-\x02\xd4\x0e\x01\x3a\xd0\xe2\xd8\x76\xd5\x8e\x4a\x02\x50\x5c\x1e\
-\xa6\xc4\x1b\xfd\x7a\xe1\x82\x8b\xe9\x2a\xff\xb1\x6d\x56\xf8\x8d\
-\x36\x5e\xc6\x1b\x65\x44\x59\xee\x3f\xd2\x60\x45\xe8\x2f\x06\xcb\
-\x1f\x8f\xd0\x5f\xf6\x92\x04\x9c\xf7\x35\x12\x74\x6b\xe0\x82\x1c\
-\x90\xa5\x17\x5d\x22\xdd\x20\xa2\xf6\x08\xe0\x4a\x05\xe8\x7f\xe0\
-\x8a\x64\xf8\x02\xbb\x7b\xb1\x3e\x9c\xc3\xe7\xf8\x30\xe7\x52\x54\
-\xc3\xf1\x27\x92\x27\x59\xae\x4c\x99\xd2\x7e\x05\x7e\x06\xfe\xa4\
-\x76\x2b\x32\x27\xb6\x5d\x91\x34\xc1\x74\x79\xd4\x78\x93\x15\x81\
-\x63\x5b\x2f\xf7\x1d\x63\xbc\xc2\x67\xa4\xd1\x72\x1f\x6b\xad\xd1\
-\x69\xb2\x1c\x79\xb6\xeb\xb6\xa1\x54\x80\xda\x23\x00\x9d\x40\x8a\
-\x63\x6e\xc7\x8a\xc2\x9f\x10\xf8\xea\x75\xd3\x73\x77\x62\xe0\xd0\
-\x59\x14\x31\xfb\x72\x02\xcc\xb8\x8c\x72\x6e\x17\x72\x47\x76\x59\
-\x13\xfa\x05\x09\x24\x29\x1d\xd5\x6e\x75\x58\x3b\x83\x9d\x01\xb2\
-\x8d\x8f\x15\x80\xa2\x93\x50\x5c\xf1\x3b\x26\xdd\x22\xa6\x76\x09\
-\xb0\x55\xbe\xbe\x9a\x28\x7c\xe7\x07\x4f\x82\xe4\x8e\x5e\xc8\xab\
-\x08\x9f\x32\xe3\xca\x6b\x2a\x40\x39\x97\x84\xbd\xfb\x6c\xf7\x21\
-\xe1\x0a\x6b\x08\x5d\x30\xb6\xed\xef\x51\x1d\x8c\xb6\xfb\x29\x68\
-\x1d\xcd\x06\xcd\xe3\x58\x81\x7c\x45\xb3\xcf\x46\x2a\x40\xed\x12\
-\x60\x87\x9a\xb1\x29\x1a\x6e\xf2\xc0\x4a\xc1\xb3\x4c\xbf\x94\xff\
-\x49\x80\x72\x8c\x46\x1c\xa2\x25\x41\x2e\x1b\xfc\xb8\x76\xab\x1e\
-\x5b\x1b\x6f\xe5\x29\x69\x1d\xce\xac\x14\xba\x24\x32\xba\xec\x28\
-\x25\x23\x15\xa0\xf6\x84\xdf\x5d\xb4\x79\x53\x27\x13\x04\xa7\x65\
-\x08\xbb\x8f\xa5\x57\x0a\xff\xe0\xd9\xb2\xaa\xe1\xb3\x68\x8c\x39\
-\xf1\xd0\xc6\x78\xab\x8f\x6a\x93\xc3\xaf\x6a\x0c\x5d\x12\x46\x81\
-\x5e\x70\xa7\x74\x97\xb0\xda\x13\x3e\x9d\xdf\xff\x1e\xe4\x49\xad\
-\x7c\x8e\x1d\x42\xa7\xf9\xaf\xc1\xca\x4b\x00\x53\x8f\xf9\xc3\x81\
-\x33\xaf\x45\x02\x78\x9d\xcb\xae\x14\xfc\xb4\xcb\x2f\x60\xe0\x79\
-\x1e\x18\x9d\x7c\xf6\x55\xa1\xb3\x34\x70\x67\x8b\xff\x61\x52\x01\
-\x6a\x8f\x00\x83\x45\xa1\x74\x32\x40\x58\xe0\x88\x30\x75\x54\x12\
-\x58\x7a\xa1\x88\x8e\x5e\x25\xf0\xdb\x5f\x3c\xd8\x75\x2e\x86\x3c\
-\x02\xd2\x60\x18\x09\xbd\xcd\xc9\x27\xdf\x14\xba\x24\xf5\x67\xb0\
-\xa3\x79\x9a\x52\x01\x6a\x8f\x00\xbb\x80\xcb\x21\xcf\xf8\x9e\xe5\
-\x02\x50\x3a\xed\x8e\xae\x90\x80\xd2\xc1\x33\x48\xad\xe1\xb6\x6b\
-\x8a\x1a\xfb\x83\x48\x90\x1f\xfe\x63\x01\x64\x8d\xe9\x05\x4f\x4a\
-\x77\x0a\xad\x5d\x02\x3c\x06\xb3\xa6\x9f\xc2\xa7\x8c\x9e\x11\x54\
-\x49\x00\xf3\x6d\xbe\xd0\x72\x0d\xea\x68\x2e\x78\x68\xcd\x4c\xcb\
-\x32\x52\x5c\x19\x5c\x4f\xdd\x33\x80\x84\x9a\xfb\xd5\xe1\xab\x6f\
-\x62\x8b\xff\x8e\x52\x01\x6a\x59\xef\x1f\x8c\x53\x25\xcf\xff\x9e\
-\xf9\x9f\x24\xe8\x5b\x46\x82\x4f\xab\x10\xc0\x78\xc3\x0b\x2a\x00\
-\x45\x51\x7f\x55\x6a\x17\x8e\xdb\x23\x1b\x98\x82\xdd\xc0\x95\xdf\
-\x4a\x61\x69\xb8\x8a\xba\xbb\x1f\x09\xf9\xed\x67\x05\x50\xb4\xa3\
-\x17\x8c\x97\xee\x15\x5c\xbb\x04\x38\x06\xaa\x44\x00\x4f\x82\xbb\
-\xcc\x47\x58\xd6\x2d\xa0\x42\x82\x7e\x2b\x7c\xca\x05\xf0\x7c\xc5\
-\x86\xcf\xc2\x18\xad\x29\x6d\xad\x3c\xdb\x87\x48\x20\xa0\x22\x88\
-\x11\x98\xc8\x2d\x89\x56\x53\xdb\xc3\x63\x34\x8f\xa5\x55\x0a\xbf\
-\xe1\x41\x52\xfb\x57\xa4\x17\x9c\x23\x15\xa0\xf6\x84\xaf\x44\xf8\
-\x08\xb6\xe2\x05\x21\x2c\x9b\xf4\x1e\x12\x01\xf2\xe0\xd7\x41\xd9\
-\x44\x80\x22\xb0\xd8\xe3\x5b\x55\x00\x16\x0d\x9d\x25\xd1\xa4\x14\
-\x48\x97\x90\xa0\x02\x33\xd9\x05\x71\xea\xaa\xbb\x78\x8c\xc6\x4e\
-\x3e\xa8\x4c\xa2\x17\x2c\x20\xa8\x49\x05\xa8\x6d\xb5\xff\x39\x55\
-\x04\xa0\xec\xad\x97\x0a\x8b\xed\x62\xa1\xc7\x66\x7f\x30\xd9\x12\
-\x52\x93\x00\x14\x19\xc3\xd5\xef\x3b\xca\x4e\x0d\xae\x4e\x02\x16\
-\x65\x68\x8c\x3f\xcb\x5e\xc0\x3f\x93\x00\x5d\x45\x6b\x08\xa6\x56\
-\x23\x00\xc5\x93\x29\xe3\xce\xed\x7d\x9d\xd3\x6a\xed\xeb\xcf\x09\
-\xc0\xa2\xaf\x31\xd7\x8f\x84\x5d\x58\x35\xfc\x8e\x30\x0a\x45\x25\
-\xcd\x4f\xdc\xf4\xab\xcb\x75\x80\x67\xd0\xaa\x5a\x01\x3e\x5a\x9c\
-\x52\xe1\x9d\x08\x36\x7f\xf7\x21\x5d\xe1\x65\x7c\x58\xc3\x94\xcd\
-\x3b\xba\x05\xd8\x8e\x9c\xc8\x53\x6d\xbf\xec\x11\x09\xbc\xb8\x3a\
-\x09\x94\x9b\x2f\x7f\x6a\xcd\xb8\x26\x49\x0a\x60\x00\x9d\xbf\xeb\
-\x4e\x5f\x52\x01\xbe\xaf\x00\x0f\x44\x8f\x81\xf5\x50\x26\x0e\xbe\
-\xc8\xfc\xb4\x0a\xef\x78\x90\xf9\x9b\x1b\xe1\x96\x48\x89\x4f\x6a\
-\xea\x8b\x6f\xc8\x97\x4b\xc0\xcf\xe0\xf2\x7d\xef\x36\x8f\x9f\xb7\
-\xca\xc1\xd7\xbc\xef\xf4\x00\x79\xd3\x55\xcf\x88\x00\x42\x2a\x01\
-\xc7\x68\x75\x91\xb9\xe2\x4c\x5f\x56\x80\xfa\xe5\xc5\xbf\x41\x0d\
-\x13\x50\xe8\x32\x73\x2d\xf1\x76\x33\x2d\xc4\x4b\xde\xe9\x6a\xa7\
-\x5e\x84\xe1\xe2\xbd\x0b\x56\x8a\x17\x7e\x3c\x15\xcf\x0e\x3e\x21\
-\xde\x0d\x65\xad\x78\x51\x0c\x5d\xc2\x3e\x44\xbc\xd1\x83\xd5\xff\
-\x7a\x8c\xa1\x2e\x85\x6f\x4a\x67\x00\x81\x31\x09\x7e\x3b\x60\xeb\
-\x03\x4a\xe1\x47\x03\xcd\x32\xd9\xe0\x59\xbc\xa3\xcc\x43\xaa\x0a\
-\x50\x1d\x39\xcf\x14\xf2\xce\x9d\x6d\x13\x31\x76\xf6\x50\x9f\x66\
-\x36\xf3\x42\x74\x1a\x8f\xce\x6d\xcf\x0c\xa3\x17\x12\x8a\x17\x70\
-\x44\x88\x43\x7c\x2b\xee\x0d\xc4\xaf\x45\x56\x59\x09\x1b\xb4\x6f\
-\x8b\x6d\x46\x4f\x47\xdb\xa1\xd3\xd0\xa2\xa7\x33\x1a\xb6\xed\x8a\
-\x8d\x74\x5b\xa0\x82\x52\xfd\xaa\x5f\xef\x4b\xd7\x4d\x4a\x05\xf8\
-\x7c\xf8\xa2\x0d\x1e\xb8\x26\x0c\xea\x2d\x55\x44\xf7\x7b\x6d\xf2\
-\xab\x06\x2f\xc1\x07\x41\x16\x53\xf2\x35\x12\x88\xc8\x84\x14\x7c\
-\xc6\x04\xf8\xb8\xcb\xf0\x17\x2e\x6b\x8d\x93\xcf\x0d\xc7\x89\x67\
-\x86\xe1\x84\x53\x43\x71\xdc\xf1\x21\xe8\x72\x6c\x30\x8e\x3d\x3c\
-\x08\x47\x1f\x1c\x88\xa3\xf6\x0f\xc0\x5f\x3c\xfa\xe3\x88\x7d\x4e\
-\x38\x7c\x77\x5f\x1c\xba\xd3\x11\x87\x6c\x77\x40\xe7\xad\xf6\xe8\
-\xf0\x7b\x1f\x6c\xbb\x68\x16\xda\xfe\xe5\x89\x8e\xf7\xff\x46\xcd\
-\x33\xe7\x0a\x06\x5c\x0a\x2b\xf6\x0a\x47\xac\xca\xde\xc0\x22\xb4\
-\x58\xf8\x17\x82\xe9\x04\x04\x35\x03\xfa\x0b\x66\xfe\xaf\xb6\x9e\
-\xaf\x2b\x02\xf8\xa9\xb4\x51\xc0\x31\xbb\x9a\xe1\xce\x73\x6d\xf0\
-\x82\x9f\x45\xe9\x67\x04\xc0\x8c\x97\x6a\x91\xd5\x86\x9d\x05\x05\
-\xf8\x9a\x89\xc4\x58\xae\x0f\xde\x96\x0d\xc5\xa3\x0a\xd9\xb8\x5f\
-\x11\xf9\x5e\xf5\x9e\xfd\x7d\xd2\xbc\x6c\xc1\xb9\x01\x25\x8b\xc2\
-\xdd\xf0\x5b\x19\x7b\x73\x59\x92\xc3\x9d\x8b\xd9\x8e\xde\x57\x91\
-\xd2\x34\x24\x24\x12\x22\x9f\xe0\x9a\x88\x92\x77\xd5\x09\xe0\xf2\
-\xe0\x59\x02\x2c\x79\x16\x07\x83\x6e\xbe\x03\x7b\x2f\x04\xc5\x86\
-\xf4\x97\xec\x2b\x15\xa0\xfa\xf0\xe9\xd6\x2f\xd8\x7c\xa5\x16\x76\
-\x7a\x6e\x52\x41\xef\xa7\x26\x6f\x5d\x12\xcd\xa2\x97\xc6\xb5\xf5\
-\xdb\x17\xd3\x8e\x77\x26\xd2\x22\xec\x5a\x84\xe5\x2b\x22\x80\x20\
-\xf4\x91\x11\x8f\x04\x2e\xc4\x0c\x78\x8a\x49\x8c\x3f\x06\xc8\xf8\
-\xe1\x59\xf9\x64\x3c\xa0\x28\xa0\x81\x4b\xf2\xf1\x88\xfa\x43\x1e\
-\xaf\xeb\xc7\x7b\xbe\x36\x69\x8b\xc3\xdd\x84\xdf\x12\xfc\x82\xb0\
-\x99\xb9\x83\x79\x27\xfd\xd9\xe0\x29\x7a\x41\x41\x31\x34\x7c\x42\
-\x91\x67\x35\xe1\x2f\x0e\xcb\x4f\x80\xa0\xd0\x5c\x98\x23\xcc\x81\
-\xd9\xa5\x6f\xc1\xe1\x00\x82\xac\x32\xfd\x45\xb7\x49\x05\xa8\x5e\
-\x00\xba\x32\x18\xdb\xde\x6f\x51\x49\x80\x6a\xe0\xf7\x4e\x6e\x15\
-\xbb\x3c\x58\xcf\xfb\xe9\x85\x06\xa1\x78\x44\x21\xaf\x6a\xd8\x55\
-\xe0\xa7\x9e\xd7\xe3\xdd\xf7\xb5\x41\xca\x59\x7f\x47\xde\xb7\x84\
-\xef\x16\xb8\x26\xcc\xd1\xfb\xef\x0c\xc9\xf0\x0d\x03\x03\xe3\xc4\
-\xe1\xa3\x62\x44\x62\x42\xd5\xf0\x37\x86\x17\xa7\x71\x82\x82\x33\
-\x95\xce\x3e\x8a\xa2\x7d\x8c\x22\xfa\x9e\x10\x02\x47\x86\xfe\xa2\
-\x7b\xa5\x02\x54\x2f\xc0\x19\x86\xcb\xa0\xf9\x9d\xca\x02\x74\x7c\
-\x6e\x52\x6c\x9f\xd8\x2a\x7a\x71\xb0\x9e\x8f\xf7\x95\x86\x91\x45\
-\x87\x94\x3e\x7e\x21\xf0\x4f\x1c\xa8\x97\x19\x75\xd3\x3c\x86\x0d\
-\x9f\xb2\x31\x78\x4c\xfc\xd7\x04\x3f\x2f\x6c\x46\xde\x60\x9f\xe3\
-\x95\xee\x7a\x4a\xcb\x87\x01\x09\x6c\xf8\x14\xfd\x88\x94\x20\xc9\
-\xf0\x77\x86\x97\xe5\xc9\x06\x85\x24\x43\x50\x10\x26\xa4\xf4\x0d\
-\x4b\x7a\xe4\xec\xab\xb7\xfc\x75\x22\x8c\x89\x65\x2b\x83\x9e\x52\
-\x01\xaa\x17\x60\x8f\x68\xc3\xa7\xfa\x5c\xb4\xd8\xa3\x8d\x13\x8f\
-\x68\xe7\xfa\x5f\x6a\x18\x5d\x72\xb0\x5e\xd1\x57\x07\x2e\x01\xff\
-\xa0\x4a\x8c\xbf\x77\xe7\x2c\xc9\xf0\xef\xfa\xd9\xbe\xfc\x9a\xf0\
-\x5d\x83\xd6\x84\x3b\xde\xbf\x92\x5e\x35\xfc\xd6\x81\x7e\xc9\x24\
-\x74\xa1\xa4\x00\x76\x91\x59\x81\x6c\xf8\xee\xe1\x42\x7e\xfd\xa0\
-\xf0\x48\x1a\xbe\x6a\xf0\x83\x22\x4c\xb3\x2c\x26\xa0\x20\xd5\xb2\
-\x70\xc5\xfa\xbe\xa8\xd0\x40\xb4\xdf\x6f\xa9\x78\xc3\x08\x39\xa9\
-\x00\xd5\x6f\x9b\x5e\xd8\xc3\x1a\xf0\xdc\x06\xe6\x4d\xf6\x2e\x05\
-\x7f\x12\x66\xf1\xb7\x86\x9f\x73\xb2\x31\xcf\x9b\xd7\xad\x54\x32\
-\x7c\xca\xe9\xc0\xbe\x3e\x9f\x7d\xd6\x87\xce\xc8\x77\xe6\x1d\xf7\
-\xab\x1a\x3c\xc5\x24\x80\x97\x42\x02\x17\x48\x86\x4f\x99\x19\x51\
-\x10\xc7\x0a\xa0\x13\x1c\x13\x40\xc3\xa7\x2c\x7d\xbc\x26\x84\x86\
-\x2f\xc9\x8b\x60\x33\x1c\xda\x4f\x9d\x2d\x09\x62\xfe\x3f\xf7\x0a\
-\xaa\x4b\xfd\x00\xf7\xb8\x5c\xc0\xb4\x58\x10\x86\x9c\x86\xe0\xe3\
-\x33\x98\xb7\xc9\x9b\x64\x7d\x84\xfb\x15\x33\xbf\x22\xfc\x0f\xc9\
-\x57\x5a\x3e\xac\x1a\x3c\xcb\xfa\x90\x71\x49\x35\x3e\xeb\x83\x7e\
-\x8f\xe8\xeb\x7d\x25\xad\xba\xf0\xcd\xfd\x7d\x52\x49\xd8\xa5\x55\
-\xc3\xa7\x6c\x8f\x28\x2b\xa0\xe1\x5b\x86\x26\xf9\xb0\xe1\x53\x96\
-\x4f\x33\xf6\x7e\xb9\x59\xfb\x9a\xf0\x6e\xab\x20\x0c\x34\xf1\xc3\
-\xc7\xe6\x91\xfc\x64\x8b\x3c\x2a\x82\xcf\x85\x96\x68\xde\x5a\x91\
-\x9d\x85\xb4\xea\x7b\x37\x0d\xc5\xd3\xe9\x74\xea\xaa\x00\x4b\xe9\
-\x1d\x12\x78\xb3\xbc\x49\x97\x74\x1f\x7c\xe9\x86\x4b\x07\xa6\x02\
-\x3f\x68\x95\x4c\x00\xdf\x5d\xe1\x71\x75\xe1\x0b\x0f\xd6\x7b\x11\
-\x72\xa7\x7d\x72\x4d\xe1\xdf\xf5\xb5\x7d\x56\x5d\xf0\xf3\xc3\x67\
-\x14\x38\xf3\xfe\xaa\xf6\xae\xa7\xb4\xbf\xb6\x2b\x9d\x7b\xf7\x98\
-\x3f\x44\xc6\xa7\x55\x0d\x9f\x13\xf1\x24\x83\x86\x3f\x20\xe4\xb5\
-\xbf\x64\xf8\x1a\xb7\xce\xe7\xae\x19\x02\xc2\xdd\x7d\x74\x7c\x22\
-\x5c\x14\xee\x15\x1c\xd6\x7d\x8c\x37\x5a\x22\x45\x78\xc3\x28\xf7\
-\xd5\x09\xc3\x77\x01\xee\x7a\xb8\x75\xa6\x16\xb6\x6c\x2a\x87\xe2\
-\xce\x28\xd3\xef\xf0\xd9\xa9\x11\x22\xc5\x9d\x5c\x74\xc7\x55\xd5\
-\xba\x28\xc0\x1d\x83\xe6\x80\xc2\xac\x4f\xed\xfa\xf4\x10\xf0\xf3\
-\x9a\x0a\x42\x2a\x02\xe5\xfa\x7c\x4e\xec\xfb\x5d\x0a\x0f\x69\x0d\
-\x9f\x86\x5f\x74\x44\x35\x98\xf7\xc0\x3a\xbf\xa6\xf0\x29\xa7\x02\
-\xfa\xfd\xa3\xf8\x9f\x1a\xb4\x2a\x92\xdc\xf5\xaf\x6b\x0a\xbf\xf3\
-\x8d\xfd\x6f\xc0\xab\xef\x47\xf0\x72\x44\xf0\x72\x2a\x83\x4b\xeb\
-\x83\x20\x24\x3c\x9a\x15\x40\x2d\xf2\x69\x98\x5b\xd8\xbb\x08\x12\
-\x7a\xa9\xa4\x00\x43\x56\xf5\x8d\x20\x02\xe0\xba\xc1\x72\xa9\xbb\
-\x7b\x19\xe6\x06\x3b\xcb\x06\x26\xcd\x50\xf4\x2d\xbb\xd4\x22\x97\
-\x15\x81\xa5\xf8\x8a\x11\xf6\xb5\xaa\xd8\x33\x79\xda\x7f\xb9\x49\
-\xe7\xd5\x7a\x5a\xda\x68\x7f\xec\x0a\xfb\x98\x59\x5b\xd7\xea\x00\
-\xb4\xef\x5d\xb8\x6e\x89\x38\xfc\x34\x48\xc3\x27\xc0\xc3\x48\x48\
-\x4c\x3c\x09\x37\xf7\x4f\x85\x52\x56\x02\xca\x89\x59\x4c\x46\xd4\
-\x8e\xfa\x37\xef\xf3\x6c\xde\x7e\x2e\x7c\xca\xba\xb0\x71\xcf\x2b\
-\xee\xfa\xb0\xe9\x1f\x86\xfa\x1e\xf3\x25\x21\x0b\x6b\x0a\xdf\xe6\
-\xf6\x5f\xef\x18\xaf\xbe\xef\xcb\xc3\xaf\xc2\x09\xb7\x24\xf0\xbf\
-\xe3\xa7\x1d\x96\x78\x8b\x04\x9e\x2f\x19\x3e\x13\xf8\x10\x57\x0e\
-\x97\xcf\xa2\x02\x50\xb6\xda\x37\xe1\xed\xe8\xd9\xa2\xcc\x7f\x90\
-\x82\x5f\xd4\x30\x78\x9b\xb5\xb1\x61\x40\x55\x09\x3e\x5e\x6a\x85\
-\xc3\xac\x0d\xd8\xd0\x8e\xfe\x27\x87\x58\x93\xd7\x64\xfa\xfd\x03\
-\x2e\xf3\xd0\x2d\x0b\x51\xd5\xc0\x88\xfe\xe5\x8d\xba\x26\x40\x7f\
-\xfa\x4b\x5c\xdc\xc7\x60\xb6\x0f\xa4\x60\x38\x08\x09\x28\x26\xeb\
-\xce\x16\x08\x22\x12\x14\x4a\x4a\x40\xd9\x3f\x8d\x53\x74\x7e\x4f\
-\x33\xbf\x7b\xde\x5d\x13\xaa\x0b\xff\xb6\x6f\xf7\xc4\x8a\xbb\x3e\
-\x70\x55\x54\x5f\xef\xbf\x53\x6b\x0a\x9e\xd2\xe3\xce\xe9\x5c\xce\
-\xfe\x7e\x69\xd5\x86\x4f\x60\x3c\xec\xcb\x1a\x4d\x33\x0e\x30\x1c\
-\x62\x98\xac\xb7\x65\x6b\x68\xf3\x9d\x1e\x48\x69\x46\xff\xbb\xc3\
-\x1d\x7f\x99\x65\x1b\x4b\xc2\x2f\x13\x49\x30\x98\x93\x47\x04\xc8\
-\x26\xe0\xbd\x01\x4a\x3e\x51\x43\x01\x1f\x8d\x96\x89\x2a\x3c\xd1\
-\xfc\x99\xa4\x04\xd9\xa7\xac\x70\x56\xef\xa1\xa8\x28\x5b\x8f\xad\
-\x20\xb6\xff\x86\xcf\xad\x3e\x2d\xf2\x9b\x3b\x0c\x14\x85\x4f\x69\
-\xe9\xe2\x4a\xff\xc1\xa7\xae\x09\x20\xea\x0d\xfc\x63\xa3\x1d\x66\
-\xdd\xe3\xbe\x91\x08\x5f\xc4\x9b\xdb\xe0\x73\x6b\x13\x3c\x3e\x38\
-\x1d\x72\xaa\x4a\x50\x51\x2a\xac\xd1\x88\xba\x7d\xa3\x63\x30\x09\
-\xbe\x8c\x15\xe0\xaf\xc0\x01\x3e\x0b\xc2\x67\x92\xbb\xfe\xe8\x67\
-\xef\x7a\x8a\xdd\xdd\xf3\x1f\xb8\xfb\x07\x24\xd7\x14\xbe\xca\x72\
-\xab\x44\x43\xa7\x86\xa9\x2d\x1c\x35\x50\x8c\xd0\x60\xf6\x48\x6f\
-\x12\x7c\x3e\x0d\x9f\xc5\x7a\xe5\xac\xcc\x95\x43\xb9\xb4\x2e\x80\
-\x9b\x1c\x1b\xfa\x53\x01\x28\x57\x9d\xd4\x7c\x89\x04\x42\x42\xc9\
-\xf3\xb9\xca\x3e\x82\xab\x46\x85\xac\x04\x39\x67\x2c\x84\x2b\x9d\
-\x96\xa2\x89\x56\x3b\x14\x0d\x88\x01\xec\x24\xa8\x7c\xc5\xe7\xb6\
-\x96\x23\x23\x83\x23\x1e\x26\x60\xdf\x98\x8c\xcc\x46\x17\x1f\x16\
-\x28\xfc\xf6\x3b\xfd\x87\xb8\xba\x26\x00\x1d\x8a\x15\xae\xde\x3e\
-\x0e\xb7\x7a\x4d\x2c\x4b\xf1\xd1\xe1\x55\x29\x05\x0a\xfd\x76\x41\
-\xfa\xed\xcd\x90\x7c\x70\x26\x64\xd6\x24\x01\xe5\xf0\x7c\xc5\xd4\
-\xab\x67\x4d\x79\xe4\xf1\x90\xb3\x30\x68\x81\x8f\xe3\x83\x2b\xaf\
-\x68\xc0\x0e\x77\xaf\x7c\x74\xb8\x76\x3e\xd3\xfe\xf2\x99\xe7\x76\
-\x67\x8e\xc7\xd9\x1d\x3d\x14\xd9\x6b\xbf\x57\x70\xcf\xbd\x7b\xfd\
-\x7b\x6c\xdb\xce\x93\xdb\x3f\x38\xba\xba\xe0\xe5\x36\xdb\xe4\xeb\
-\xf6\x6b\xf4\x86\x0d\xde\xa0\xb7\xba\xd0\xb0\x8f\xba\x90\xfd\xb3\
-\xfe\x40\x83\x2c\xdd\xd5\x6b\x93\x25\x25\x68\xbd\x71\x03\x4e\xee\
-\xa3\x80\xbf\x3b\x33\xb8\xd1\x56\xff\x2d\x2b\xc1\x05\x27\xcd\x87\
-\x44\x00\x3e\x2d\x0d\xa2\x87\x33\xa9\xd9\x3b\xb5\x42\x59\x09\xf2\
-\xcf\xb5\x13\xac\xed\xbf\x01\xc7\x58\x4d\x45\x25\x39\x51\xd7\xf1\
-\x6b\x3a\xac\xfc\x99\xcf\x8b\x4a\x82\xda\xd6\x63\xd0\x72\xd7\x89\
-\x8f\xca\xa1\x19\xa5\x04\x94\x9b\x30\x87\x6d\x65\xc8\xd5\xb5\x4a\
-\xe0\xb3\x65\x1b\x47\xe3\x16\x2f\x37\x11\xd7\xce\xdb\x85\x0b\xc3\
-\x98\x4c\x56\x82\x7c\x5f\x08\x20\x8f\x02\x24\xbc\x3e\x3c\x0b\x52\
-\x6a\x12\xc0\xd3\x0d\xf2\x56\x4d\x82\x80\x4e\x2e\x72\xc1\x5d\xa6\
-\x2d\x88\xef\x3a\x75\x41\x0e\xa1\x8c\x80\x35\xd1\x74\xce\xf0\x7f\
-\x14\xfb\xdc\xdd\xbd\x51\xd3\x59\x57\xa8\xd7\x4b\x0d\xb5\x2c\x94\
-\x51\xad\xb9\x02\xca\xab\x70\x45\x67\x1b\x30\x1c\x40\x45\x0d\x19\
-\xd4\x68\xa1\x88\x3a\x1d\x55\xd0\xa0\x8f\x06\x36\x1e\x6c\x87\xcd\
-\x36\xef\x14\x09\x50\xbf\x57\x1f\xe4\x90\xaf\xeb\x63\x0a\x38\xa3\
-\x9b\x12\xb2\x02\x50\x4e\xd8\x6b\x45\x44\x0e\x85\x0f\x54\x02\x4a\
-\xdc\x04\xd9\xe0\x92\x33\xfa\x69\x54\x82\xc2\x4b\xe6\xa5\x1b\x06\
-\xae\x2f\x59\xe3\xb4\x1b\xcd\xb5\xad\xd8\xba\xc1\x55\xf6\xc0\x28\
-\x71\x91\x4f\x0f\xa5\xc8\xaa\x27\xd7\x0c\x5b\xb6\xde\x89\xcc\xaf\
-\xe1\xc2\xdd\x57\x03\x72\x56\xde\x7f\x94\xde\xe8\xef\xd0\x62\xb9\
-\x21\x2e\xec\xee\xea\x75\x4e\x80\x93\x0b\x56\x8f\xa8\x10\x80\xb2\
-\xe7\xd0\xb8\xec\xbc\x20\x95\x60\xb1\x04\xc2\x90\xfd\x10\x2f\x96\
-\xe0\xdd\xd1\x39\xf0\x44\x22\xf4\x82\xd5\x93\x21\xa0\xdb\x28\x6e\
-\x88\x8c\xb3\x4c\x31\x0c\x96\x41\x4a\x8b\xa1\xf6\xaf\xab\x0b\xdc\
-\xda\x6d\x3e\x5a\x0c\x9f\x80\x46\x3d\x1c\xb1\xf1\xb0\x5e\x08\x1e\
-\x0e\x08\xfb\xec\x11\xf6\xf4\x41\xd8\x65\x87\x32\x0b\x3b\xa0\x72\
-\x5b\x75\x94\x53\xe6\x4a\x8e\xeb\x17\x88\xb7\xbe\xa5\x5b\xdd\x9e\
-\x13\xef\x1e\x26\xfa\x37\x56\x08\x15\xa3\x26\xa8\xd4\xbe\x03\x5b\
-\x8c\xff\x45\x8f\xb1\x33\x6e\x02\xf8\x9b\x95\x4e\x25\x09\x0e\xf5\
-\xd6\x49\x88\x1a\x02\xd9\xac\x04\x84\x0f\xaf\x96\xa9\xf2\x84\xd7\
-\x8c\x4a\x4b\x2e\x99\xf1\xb7\x0d\xdd\xf2\x66\xbb\xf3\x51\x9c\xd4\
-\x79\x0e\x1a\x36\x34\x66\xa7\xb0\xd1\x93\x48\x72\x64\x38\xca\x68\
-\xd2\x64\x25\x76\xea\x1c\x82\xcc\xfc\xd8\x32\x58\xf1\x12\xbd\xee\
-\x86\x17\x12\x70\xcb\xd9\xbb\xd8\xb0\x45\x2b\xfa\xf5\x7b\xea\x62\
-\x33\xd0\x6b\xce\x32\xe7\x4a\x02\xb0\x04\x5f\xb7\xf0\x23\x02\x7c\
-\x2c\x09\x82\x68\xb1\x00\x78\x67\x33\x14\xac\x9c\x0b\xf7\xba\x4f\
-\xe0\x44\x28\x0c\x95\x29\x62\x43\x97\x44\x61\x80\x06\x5a\xbb\xce\
-\xc5\x76\xc3\xc6\x8b\xc2\x6e\x62\x62\x81\x2a\x8d\x9b\x20\x7d\x6e\
-\x56\x33\xd1\xa3\x4c\x7c\xe7\xd0\x49\x22\x61\xb4\x63\x4a\x7c\x8c\
-\xeb\x2c\x71\x1d\x85\xa9\xe1\x94\x92\xf6\xe2\x23\x5f\x27\x11\x42\
-\xc4\xb3\x9a\x4c\x25\xef\x58\x55\x05\xee\x9b\x3e\xcd\xd5\x71\x59\
-\xe7\xe6\x9f\x44\xe8\xde\x34\x37\x7c\x08\xa4\x4b\x48\x80\xd1\x23\
-\x98\xe4\x7c\x2f\x9d\x58\xfe\x15\x93\xd2\x7d\x23\x77\x24\x52\x09\
-\x28\x8b\xed\xfe\x40\x0b\xdd\x49\xd8\xc3\xe8\x2e\x8e\x68\x5f\x8c\
-\x1d\xba\x45\xf1\x61\xe9\xd3\x22\x1a\xbe\xcc\xaa\x97\x1f\x68\xf8\
-\x7f\x9e\xbb\x8b\x4d\x9a\xe9\xd3\x8b\x26\x4b\x9e\xc7\x54\x97\x04\
-\x08\x9c\x36\x7f\x40\xb5\x02\x50\x8e\xfc\x35\xec\x39\x3f\x44\x36\
-\x3e\xee\x04\x04\x53\x01\x96\xad\x01\x3f\xdb\x65\x20\x24\x20\xa5\
-\xdb\x12\x28\xea\xb8\x10\xde\x58\xcc\x65\x22\x9a\x3a\x32\xe9\x4a\
-\x2d\x18\x04\x25\x06\x19\x2e\xf7\x99\x78\xd7\xaf\x75\x84\x79\xe2\
-\xa0\xe8\x49\x23\x3d\x68\x97\x2c\xa1\xb9\x38\x28\xe6\x07\xfe\x6e\
-\x0a\x84\x99\x74\xeb\x39\x7d\x55\x05\xfc\xa5\x55\x23\xdc\x68\x63\
-\x80\x9b\xbb\xe9\x0a\xef\xf7\x67\xde\x4b\x4a\x40\x2b\x8a\x09\x6e\
-\x0a\xfe\x85\x67\x8d\xdf\x1c\x18\xbb\x27\x8a\x95\x60\xa0\x99\x6f\
-\xc1\x48\x4b\x44\x4b\xfb\xe7\x2f\x49\xf0\x1f\x69\xf8\x14\xb5\xa5\
-\x51\x1f\x69\xf8\xda\xcd\x0d\xd8\x3d\x8e\x9a\x7f\xb7\x9e\x40\xf2\
-\xea\x26\x3e\xe0\x60\x9f\x78\x20\x43\xfd\x07\x9e\x38\x52\x30\x69\
-\xb6\x63\x8d\x02\x50\xfe\xdc\x3f\x85\x1f\x7e\xad\xd5\xed\x75\x6b\
-\x18\x9e\xed\x12\x88\xb7\x59\x08\x01\x5d\x7f\x85\xbb\x9d\xa7\xc0\
-\xdd\x0e\x63\xc1\xcf\x72\x28\x3c\x6e\xd7\x1f\x0a\x2d\x06\x02\x2a\
-\x37\xac\xd8\xb4\x99\xa9\x45\x92\xcb\x88\x0f\xd7\x48\x90\xe5\x30\
-\xd8\xbe\xb1\x0a\xba\x99\x35\xc2\x33\x76\x9c\xb2\x2a\x12\x60\xf4\
-\x30\xc8\x4e\x5d\xa3\xe5\x7b\x68\xdc\x8e\x30\x2a\xc0\xe8\x0e\xfc\
-\xc7\x96\xce\xf9\x41\x24\xf4\x12\x36\x7c\x91\x00\xb3\xef\xa0\xb6\
-\x9e\x21\x7b\xcc\xbd\xee\x7f\x3d\x16\x20\xde\xbd\x7b\x66\xf9\xc1\
-\xc5\x0c\x6a\x69\x59\x60\x8b\x16\x0e\xa8\xa0\x20\x3a\x41\x3b\x94\
-\xa0\xfc\x03\x3e\x18\x1d\x5a\x0c\xf7\x18\xd4\x06\xef\x25\xad\xc9\
-\x7b\x8a\x5e\x58\x13\x87\x5e\x2d\xe2\x99\x8d\x94\x7b\x4d\x43\xae\
-\x89\xe6\xed\x45\xe1\xbf\xa7\x13\x3c\x6b\x69\x69\x47\x8f\xd5\x1b\
-\x45\x97\xa6\xd1\xdf\x5b\x43\x81\x8b\x03\xf4\xf5\xd0\xa5\x8d\x7d\
-\xe9\x62\xab\xee\x2f\xf7\xf7\xe8\x18\x7a\xc9\xb1\x45\xec\x83\x81\
-\x1a\xa9\x61\x23\x95\x22\x0e\x8c\xdc\x16\x60\x3a\xea\xfd\x03\x12\
-\x78\x99\x64\xf8\x22\x86\xba\xb3\x67\x32\xea\x7c\xf5\x60\x10\x0d\
-\x51\xfc\xec\xa2\x73\xf1\x1d\xc5\x56\xd2\xe2\xf1\x06\x1d\xb6\xe4\
-\x70\x64\x50\x53\xb3\x0d\x9a\x9a\xfe\x82\xd6\xd6\x0b\xd0\xcd\x2d\
-\x1c\x47\x8d\xba\x8a\x9c\xf2\x89\x0d\x93\x7f\xd0\x06\xd1\xe8\x34\
-\xae\x2d\x0e\x76\xb5\x2c\x3c\x13\x38\x3f\xb4\x9a\xf0\x8b\xa6\x06\
-\x8f\x0e\x00\x2f\x03\x34\x1b\xca\xc9\xab\x29\x7c\xf3\xbe\x80\x32\
-\xe5\x3b\x78\x4f\xad\x03\x8f\x3d\x2a\xc2\x48\xda\x6e\xa7\xbf\x7f\
-\x13\x65\x23\x1c\x6c\x3c\x1f\x47\x99\xac\xae\x84\x8b\xf9\xea\x57\
-\x8a\x2e\x57\x7d\x60\x46\xa0\x1f\x2c\x4a\x0c\x83\x15\x29\x39\x15\
-\x02\xb4\xe9\x2f\xea\x45\xfc\xe2\x68\xa0\xb8\xf8\x99\x2a\xee\x6d\
-\x12\x56\x54\x7e\x68\x9b\x53\x9d\x3c\x3f\x9a\x76\x41\x68\x35\x08\
-\x65\x4c\x47\xa3\x99\xb9\x0b\x5a\x5a\xba\x89\xe8\xd2\x65\x6e\x19\
-\x15\x80\x62\x6a\x64\x57\xa9\x97\xe9\x3b\x7e\x10\xa3\x64\xe5\xb9\
-\xe8\xec\x66\xc9\x22\xfc\xdd\x73\xb8\x4f\xb2\xd0\x53\x40\xc3\x4f\
-\x14\x7a\x64\x74\xba\xd6\x3d\x9e\x86\x4f\x10\x58\x0c\x02\x61\x4d\
-\x02\x68\xea\x57\x0c\xb9\x72\xeb\x50\xfd\x87\x8a\x30\x82\x96\xba\
-\x72\x5c\x05\xec\xa2\x33\xa4\x92\x00\x13\x2d\x36\xa4\xd9\x0f\x3c\
-\x14\x0c\x03\xbc\xb0\x82\xc1\x47\xf2\x60\xd0\x1e\x92\x9f\x68\x3c\
-\xc1\xe9\x6b\x04\x58\x29\x3a\xcc\x71\xd0\x7a\x84\x9d\xb9\x08\x8b\
-\x83\x11\xa6\xdd\x44\x70\xf5\x21\xff\x8d\x40\x20\x01\xb3\x70\xc7\
-\xdc\x7a\x61\xda\x79\x5e\x06\x15\xa0\x43\x87\xe9\xa2\xf0\x27\xba\
-\x5c\xcf\xf3\x73\x11\x1d\x70\x28\xa8\x5a\xd1\xf8\x0e\x1f\xc0\x2a\
-\x8d\x26\xf5\xb0\xff\x74\x13\x1c\xfc\x9b\x79\xce\x90\xb9\xe6\xd9\
-\xce\x73\xdb\xbe\x9f\xb4\xa1\x9b\xbf\x6f\xee\xc6\x50\xad\x13\x6d\
-\xb3\xc4\xe1\x23\x78\x1a\xe4\xd6\x14\xbe\xb1\x6d\xc5\x19\x84\xbd\
-\xeb\xe8\xd6\x38\x8c\x78\x53\xea\x94\xa6\x2a\xad\xd0\xd9\x78\x81\
-\x48\x80\xf1\x16\xab\xd2\xb7\x38\x1f\x2d\xe3\x0c\xd8\xff\xa6\x92\
-\x04\x66\xa3\xd8\xdd\xcb\xe5\x3e\x2b\x80\xf8\x28\xd3\x12\x70\xde\
-\x58\x7e\x9e\x5f\x55\x3c\x05\x7c\xd8\x51\x90\x0c\x1b\xdf\x06\xc3\
-\xe2\xa7\x71\x30\x2d\x92\x4f\x44\x78\xdf\xdc\x61\x47\x30\x95\xc0\
-\xcd\x35\x2c\x37\xf1\xb7\x89\x69\xb8\xb4\x1f\x5a\x6a\x89\xea\x02\
-\x17\xbe\xe7\x9e\x7a\xb4\x6e\x61\x3c\x03\xf0\x97\x37\x95\x71\x7c\
-\x2a\xef\xcf\x39\xa0\x5f\x5c\x11\x3e\x81\xf1\x34\x48\xad\x56\x80\
-\x01\x80\xf5\xca\xcf\x1c\xbc\x52\xe7\x57\xf3\x94\x0f\xef\x5e\x92\
-\xe7\xd6\xc3\xae\xba\xc3\x71\xb8\xe9\x6f\xa2\x96\x80\x75\x7f\xaf\
-\xa8\x8a\xf0\x9d\x76\x23\xc8\x8b\xd6\x20\x1c\xff\xe2\x84\x10\xf1\
-\x71\xae\x61\xd0\xdc\x12\x61\x6b\x06\x0d\x1c\xc1\xa3\x14\x19\x0f\
-\xbe\x90\xf1\x2c\x2b\x61\xbc\xf0\x23\xb3\x1f\xf3\x09\xb9\x1c\x2f\
-\x7c\xcf\xf1\x12\x66\x2a\x7a\xf0\x43\xd4\xb6\xe7\xdc\x6f\x35\xcf\
-\xef\xda\xcd\xd9\x6b\xee\xd3\xf0\x29\x57\x87\x59\x61\x63\x25\x79\
-\xb6\xe2\x41\x7b\xa5\x14\xbe\xc3\x48\x20\xf6\xb8\x58\x29\xfc\x32\
-\xab\x10\x55\x9e\x64\xf0\x2c\x1c\x77\xfd\x84\xea\x04\xd0\x35\x07\
-\x76\xda\x95\xd1\x4f\xb3\xac\xab\xbc\x0f\xa2\xd8\xb0\x81\x29\xce\
-\xef\xb5\x16\xd7\x0f\x3e\x84\x4c\xbf\x7d\x7c\x91\x00\xe4\x71\x2d\
-\x7e\x8c\xdb\x7f\xd5\x8c\x20\xf2\x6a\x29\x3e\xa9\x52\xf4\x8d\x0d\
-\x9a\x34\x47\xa7\x49\xcb\x71\xc3\xd5\xe7\x9f\x66\xb6\x86\x10\x78\
-\x28\xf4\xba\x8e\xaf\xbd\x4e\xe1\x63\xcf\x83\xf8\x6c\xe7\xce\x9c\
-\xd7\xcb\x7a\xf6\xbc\x9e\x60\xa7\x7f\x25\xdb\xb5\x4b\x6c\xde\x5c\
-\x87\xb2\x92\x45\x7d\xf1\xf8\x80\x76\xd8\x49\x5b\x8d\x3d\xfd\x72\
-\xc3\x7f\x7a\x46\x0e\x5d\x4e\x25\xa3\x0c\x38\x3c\xad\x22\xfc\xdc\
-\x66\x37\x1a\x87\x57\x17\x3e\x45\x66\x4f\xf3\xe8\xaa\xe1\x9b\x39\
-\x00\x72\x65\x45\xbf\x97\xfb\x4f\xb7\xb6\xaf\x7c\xc5\x14\xed\x98\
-\x42\xfd\x06\x2d\xb1\xa1\x95\x2b\x42\x97\xb9\x28\x7a\x9c\x03\x2c\
-\xff\xa6\x29\x61\xb4\x87\x48\x7c\x5a\x17\x6d\xd3\x1f\xa7\x77\x31\
-\xc3\x30\x68\xd2\xc5\x01\xe7\xfc\x7e\x0b\xb7\xef\x78\x87\x5e\xe4\
-\x91\x20\xc9\xc2\x85\xb7\x43\xa6\x8e\xdf\xfa\x76\xae\x31\x93\xe2\
-\xae\x0a\xde\xcf\x0c\x99\xd0\xb7\x76\x8d\x53\x82\x86\x98\x15\x65\
-\xcc\xee\x8d\xa1\x13\xba\xa2\x8b\xa9\x0e\xca\x71\x39\x42\xf1\x49\
-\x61\xa3\xbe\xe5\xa4\x2c\xba\x22\xa8\xa9\x53\x79\xf8\xc3\x32\x99\
-\x67\xaa\xa7\x75\x5f\x54\x1b\xfe\x7e\xc3\xb7\x70\xac\x65\x98\xf2\
-\x6e\xad\xbf\xab\x0a\xa0\xa1\x2b\x0a\xff\x43\x6d\x6d\xf6\x7d\x07\
-\x09\xb8\xe2\xd2\xb6\x58\xa2\xe7\xf2\xab\x16\xb8\x7e\xcd\xa6\x0c\
-\x13\xc4\x7d\xdc\xa4\x99\xc7\x25\x4d\xbf\xbe\xa4\xd2\x77\x0e\xdd\
-\xdd\xf9\x42\x4f\xcf\x52\xf2\xff\xfb\xf3\xe9\xb1\xa7\xb3\xbb\x98\
-\xf2\x7e\x33\x86\xe2\x0d\x0d\xc1\xff\xb8\x2c\x84\x67\xb5\x82\x24\
-\xb4\x04\x61\x72\x37\xf5\x22\x9f\x7e\xad\x30\x6c\x5c\x17\x5c\x67\
-\x6b\x8c\x3a\x2a\xa2\x7d\xf7\xf2\x09\x07\xe8\x96\x2f\x5f\xb8\xbe\
-\x21\xed\x82\xed\xb0\x1d\xb0\x97\x1f\xf7\x8d\xec\x61\xbd\x7c\x71\
-\xe0\xc5\x70\xc4\x30\x09\xce\xb7\x0e\x03\xef\x76\x8f\x21\xaa\x63\
-\x2e\xc4\x5b\x0b\xe0\x9c\x1a\x4f\x71\x0b\x04\x48\x86\x6f\xd4\x15\
-\xfe\x31\x0b\xe6\xa7\x5e\xed\x0b\x20\x2f\x39\xe5\xeb\xbb\x4d\x0a\
-\x25\x2f\x3b\xf1\x81\xc8\xa2\x0f\x54\x49\x49\x03\x1d\x1d\xe7\x89\
-\x0e\x3b\x16\x31\x7e\x4b\x1e\x11\xe0\x03\x01\x97\xe9\x02\xcf\x93\
-\x54\x2a\x2f\x2a\x82\x7f\x81\x19\x64\x10\x11\x90\xf2\xbc\xb3\xb2\
-\xc0\xdb\xde\x10\x77\xd9\x18\x62\xb7\xa6\x1a\x6c\x30\xe1\xe2\x93\
-\x37\x7a\x8b\x9b\xa1\x7b\xc5\xa7\x68\xe6\x92\xc6\x0f\x82\x61\x3d\
-\x34\x3a\x5c\x1f\x99\xc3\x46\x69\x70\xc7\x2c\x19\x42\x3a\x7c\x24\
-\x61\x23\x3c\x91\x20\xae\xf3\x47\x38\xa6\x10\x4c\x57\x0c\xab\xfc\
-\x01\x3c\x36\xfc\x76\xfd\x01\x15\x54\x2a\x4e\xf7\xa8\xf7\x6f\x59\
-\xf2\xfd\x43\x67\x05\x93\x57\x6b\xf1\x81\x48\x6f\x15\x15\xeb\xa3\
-\xb5\xf5\x2f\x38\x79\xf2\x3e\x91\x04\x63\x2d\x5a\xbd\xa4\x02\x50\
-\x16\x18\x42\xc4\x3e\x2e\xe4\x78\x01\x14\xde\x56\x05\x9f\x92\xb6\
-\x90\xcf\x8a\x40\x49\xeb\xa8\x88\x17\x2c\x35\x70\x54\xd3\x7a\xa8\
-\xc0\x65\x10\x28\xc6\xf5\x10\x9c\x1b\x21\xac\xd0\x47\x38\x65\x86\
-\x10\xd5\xb9\x72\xd0\xd5\x11\x63\x95\x09\x07\x39\xf1\xec\x5e\x01\
-\xea\xab\x3e\x09\xa0\xdd\xa6\xe2\xee\xef\x2f\x0d\xfb\x3b\x4f\x0b\
-\xa7\x6d\x4b\xda\xc1\x40\x38\xa4\xac\xac\x2e\xb0\xb5\x1d\x8b\xe3\
-\x46\xaf\xc3\x99\x46\x20\x60\x25\x98\xdb\x12\x52\x76\xc9\xc3\x53\
-\x22\x01\x12\xde\x05\x36\x02\x5f\x41\x7b\x28\x95\x14\x81\x12\x66\
-\x29\x83\x1c\x5f\xab\x2f\x87\x5d\x95\x50\xf3\x44\x38\xc0\xa4\x49\
-\x6e\x16\xa1\xb9\x04\x7c\x69\xf8\x26\x7d\xc8\x23\x8b\x2b\x0a\xff\
-\x92\x34\xe8\x1f\xbc\x2e\x80\xee\xa4\x4d\x78\x51\xbf\xbe\x26\xda\
-\xea\xaa\x21\x2b\x00\xe5\x57\x63\x28\xd8\xa2\x0a\x21\x62\x09\xf0\
-\x20\x03\x2f\xe2\x9a\x43\x50\x15\x09\x84\x23\x16\xeb\x46\x7c\x53\
-\xf8\xbe\x46\xa1\x24\xf0\x82\xaa\xbb\x85\xe8\xcc\x81\x10\x2a\x80\
-\x5a\x93\x8a\x31\xfa\xa6\xd2\xa0\xff\x1f\x16\x86\x88\xc7\x0f\xe8\
-\xa8\xa0\xd0\x88\x3c\x77\xa7\xb7\x00\x49\x11\x84\xbf\x37\x01\x1f\
-\x56\x02\xca\x51\x19\x88\x4d\x35\x82\x47\xac\x04\x59\xe6\x70\x53\
-\x25\xac\xd3\xe3\xaf\x0a\xff\x7a\x13\x5f\x12\x76\x59\x75\x7b\x05\
-\xe9\x8c\x86\xf7\x86\x9d\x2a\x8a\xfe\x79\xd2\x90\xff\x9f\x57\x06\
-\x89\x77\xf2\x7e\xaa\x2c\x43\x9a\x6d\xba\x95\x24\xc0\x45\xfa\x10\
-\xe8\xc1\x81\x8f\x92\x22\x9c\x51\x80\xa0\x1c\x13\x48\x79\xd7\x09\
-\x1e\xdc\xb2\x51\xe3\x91\x80\x05\x35\x06\x4f\x6b\xfa\x67\x69\x07\
-\x50\x0d\x1b\x45\x11\x9a\x0e\x06\x94\xab\x27\x0a\xff\x61\x5d\x3f\
-\xd2\xad\xce\x2e\x0d\x13\x2f\x41\xf2\xa4\x07\x2b\x5a\x69\x00\xce\
-\x69\xf9\x49\x02\x52\x2f\x48\xd8\x23\x07\x69\x92\x12\x10\xf8\x57\
-\x35\xe0\x46\x96\x2e\xf8\x5a\x9f\x32\xf3\xad\x36\xfc\x47\x1d\x3f\
-\xc0\x11\xb9\xd0\xcf\x85\x2f\x6a\x05\x94\xf7\xf8\xe5\x7c\xef\xf1\
-\x08\xa9\x00\xff\x99\x08\x74\xd3\x24\x7e\x23\x05\xc0\xf1\xfa\x95\
-\x4a\x83\x77\x7f\x2a\x41\x4c\x25\x09\x18\xc8\x88\xea\x06\x05\xef\
-\xea\xcb\xa4\x71\xe2\xac\xdf\x56\x0a\x3f\xca\x32\x9d\xd4\xf4\x13\
-\x6a\x08\xfd\x03\xec\x81\x48\x98\x49\x9e\xf7\x83\xc8\x9f\xcb\x2b\
-\x7e\xc3\xa5\xe1\xd6\x92\xc5\xa1\xe2\x39\x05\x19\x32\xa4\x4d\xdf\
-\xbb\x71\x25\x09\xf8\x6b\x35\xc1\x4f\x52\x82\xc0\x4e\x90\x5e\xa0\
-\x0a\xbe\x1b\xa6\xea\x04\x54\x84\x1f\x6c\xf6\x04\xbc\x98\xcc\x4f\
-\xfb\x01\x42\x2e\xec\x85\x50\xd8\x00\x3c\x98\x0f\x8f\xc1\x0d\x4a\
-\x09\x08\x2e\x04\x25\x51\xf8\x07\xa4\xc1\xd6\xb2\xd5\xc1\xe2\xad\
-\xd5\x68\x37\x30\x1a\x2a\x03\x4e\x93\xa8\x20\x2e\x6b\x06\xbe\xe4\
-\x59\xc1\xa7\x02\xdc\x34\x82\xe0\xf8\xf6\x90\x4c\xbe\x43\xa8\x11\
-\xdc\x31\x06\x1e\xb4\x08\x21\x81\xbf\x82\xdd\x10\x04\xeb\x48\xe0\
-\x73\x21\x81\x84\x2d\x10\x05\x2e\x89\x2b\x41\x5b\x14\xfe\x13\x69\
-\x87\x4f\x2d\x5d\x1e\x2e\x9e\xd4\xb0\x86\xce\x17\x50\x22\x15\xc4\
-\x21\x4d\x3f\x49\x30\xdf\x08\xa2\xf7\x72\xe1\xdd\x19\x75\xf0\x0d\
-\xef\x0e\xc8\x97\x85\xe8\x64\x1d\xb9\x64\x99\x39\x90\xf4\x8f\xb0\
-\xab\xa3\xfc\xb9\x5f\x44\x68\x2b\x0d\xb5\x96\xef\x0f\x20\xee\x52\
-\x7e\x45\x4b\x83\xf6\xea\x80\xb3\xc5\x15\x44\x52\x51\xcc\x70\xd7\
-\x00\x6f\x2a\x40\x6a\x0b\x08\xa4\x6f\xcd\xab\x35\xb9\xeb\xbf\x14\
-\xbe\x5d\x45\x93\x6f\xbc\x34\xd0\x3a\xb2\x41\x84\x78\x90\x69\x0a\
-\xad\x1b\x68\xca\x03\x8e\x13\x57\x10\x7f\x6d\x09\x85\xa1\xdd\x41\
-\x10\x61\x0b\x7c\x64\x20\x53\x08\xf0\xa1\xd9\x28\xc8\xa8\x31\xfc\
-\xe1\x84\xf2\x61\xde\xdd\xd2\x30\xeb\xe0\x0e\x21\xe2\xd9\xbe\x77\
-\x64\x18\xc0\x9e\xe2\x0a\xe2\xc5\x4e\x90\x49\x4b\x81\x9c\x86\xe0\
-\x43\xdf\x5e\x52\x7d\x08\xac\x36\xfc\x09\x84\xf2\x19\x3e\xb7\xfe\
-\x57\x9b\x2c\x4a\x05\xf8\x7e\x22\xd0\xd2\xa0\x4c\x9f\xd4\xe2\x97\
-\x1b\x83\x80\x0a\x10\xdb\x09\x32\xc8\xbf\x94\xd2\xb7\x38\xa2\x37\
-\x44\xfc\x43\x00\x3d\x51\xf8\x8f\x7e\xc4\x34\x74\xa9\x00\xff\x3b\
-\x09\x70\x38\xa9\xcd\x53\x01\x28\x45\x8a\x10\x44\xdf\x62\x31\x07\
-\x5e\xc8\x4d\x81\x92\x8a\xf0\x3b\x8a\xc2\x7f\xf7\x9f\xce\x32\x92\
-\x52\x4b\xf7\x08\x22\xaf\xdf\xe5\x38\xa4\x42\x68\x00\x48\xea\x02\
-\x18\x6c\x0c\xa9\xa2\xb7\x48\xc7\x0e\x0c\x21\x40\x14\x7e\xbf\x8a\
-\x99\xbd\x4e\xd2\x00\x7f\xc2\x5d\xc2\xd8\x9d\x41\xcd\xea\x03\x9e\
-\xef\x00\xf8\x9c\x53\xfe\x18\x20\x6d\x3c\xa1\x5e\x6f\x12\xbc\x02\
-\x54\x5a\xdd\x2a\xe5\x27\xdc\x26\x4e\xbc\x1a\x26\x9d\x96\x06\x6e\
-\x24\xf0\x25\xe4\x6d\xf6\xa3\x4b\xa4\x98\x8a\x4a\x9f\x82\x34\xbc\
-\x9f\x7c\x9f\x40\xf1\x80\x12\x3d\x7c\x21\x5d\xbc\xb8\x81\x72\x50\
-\x5a\xe3\xff\x17\x9f\x18\x22\x45\x2a\x80\x94\xef\xcc\xff\x01\x19\
-\x47\x8e\x78\xd3\x1b\x66\xf0\x00\x00\x00\x00\x49\x45\x4e\x44\xae\
-\x42\x60\x82\
-\x00\x00\x05\x24\
-\x89\
-\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\x18\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0\x77\x3d\xf8\
-\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\
-\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x06\xec\x00\x00\x06\xec\
-\x01\x1e\x75\x38\x35\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\
-\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\
-\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x13\x74\x45\
-\x58\x74\x41\x75\x74\x68\x6f\x72\x00\x52\x6f\x64\x6e\x65\x79\x20\
-\x44\x61\x77\x65\x73\x0e\xd8\x7e\x1d\x00\x00\x04\x82\x49\x44\x41\
-\x54\x48\x89\x8d\x96\x6d\x88\x54\x55\x18\xc7\x7f\xe7\xbe\xcd\xdc\
-\xb9\xb3\xb3\x33\xfb\x1a\xeb\xbe\xe9\x6e\xad\x2f\x69\xea\x4a\x68\
-\x8a\x22\x24\x94\x58\x7e\xaa\x48\x23\x30\x41\xa1\xec\x5b\x51\x41\
-\xf8\x31\xa3\x3e\x44\x94\x44\x92\x09\xa5\x46\x54\x84\x21\x59\x44\
-\x18\x96\x19\xe8\xb2\x5a\x9b\xba\xea\xa4\xfb\x66\xee\xce\xce\xec\
-\xec\xcc\xdc\xbd\xf3\x76\xef\xe9\x43\xb3\x63\xe6\xbe\x3d\xf0\x7c\
-\xfb\x9f\xff\xef\x3c\xcf\x3d\xf7\x39\x47\x48\x29\x99\x2d\x36\xbf\
-\x27\x7c\x6a\x40\x7f\xc0\x6f\xaa\x8f\x02\x64\x1d\xf7\x84\x3b\x51\
-\xb8\xf0\xed\x8b\x32\x37\xdb\x5a\x31\x1d\x60\xf7\x01\xa1\x67\x2a\
-\x42\x6f\x37\xd7\x76\xac\xaf\x0a\xd6\xd5\x07\x2c\x7f\xb5\x69\x05\
-\x7c\x52\xba\x38\xb6\x93\xb3\xed\x6c\x62\xdc\x8e\xdd\xea\x8b\x5d\
-\x39\x15\x4c\xa7\x5e\xfe\x70\x97\x2c\xcc\x19\xb0\xf5\xa0\xb1\xa2\
-\xb9\x7e\xfe\xa1\x65\x1d\xab\x96\x7a\x5a\x56\xc9\xbb\x0e\x92\x3b\
-\x75\x02\x81\xa1\x9a\x50\x50\xbd\x9e\x2b\xe7\xff\xe8\x1f\xbe\xb1\
-\xe3\xd8\xce\x7c\xf7\xac\x80\x6d\x9f\x87\xf7\x2d\x69\x59\xf9\x5c\
-\x5d\x7d\x4d\x9d\xe3\xa6\x67\xeb\x00\x00\x01\x35\x44\x7c\x78\x74\
-\xe4\x42\x5f\xd7\xc7\x47\x9f\x4a\xbe\x36\x2d\xe0\x89\xc3\xe6\xb3\
-\x6b\x96\xae\xdf\x6f\x04\xf5\xa0\x27\xdd\x39\x99\x87\xb4\x7a\xaa\
-\xb4\x05\xa4\x8a\x43\x8c\x24\xaf\x65\xce\xf4\xfc\xfa\xc2\x17\xcf\
-\x38\x9f\xdc\x05\x78\xfc\x80\xa8\x59\xdc\xb6\xfc\xb7\xc6\xd6\x79\
-\x6d\x73\x33\x17\x6c\xa8\xdc\x43\x83\xb1\x0c\x9f\x08\x72\xc9\xf9\
-\x9e\x73\x99\x4f\x19\xb8\xd1\x17\xbd\x18\xed\x59\xfd\xcd\x2e\x39\
-\x0a\xa0\x4c\xca\xab\xab\xea\x8e\x34\x35\x37\xcc\xd1\x1c\x56\x5a\
-\x4f\xd2\xea\x5b\x8d\x5f\x54\x10\x2b\x46\x39\x97\x39\x8c\x2b\x5d\
-\x5a\x9a\x5b\xdb\xaa\xab\x6a\x8f\x4c\xea\x14\xf8\xf7\xa3\xde\xd7\
-\xd2\xb1\xd6\x15\x73\x33\xaf\xd1\xdb\xb9\xd7\xdc\x88\x82\x4a\xda\
-\x1d\xe1\x97\xd4\x07\xb8\xb2\x08\x40\x51\xb8\xb4\x37\xcf\x5f\xbb\
-\xf5\xa0\xb1\xa2\x0c\xf0\xfb\xd4\x2d\x56\xd0\xb4\x40\x20\xc4\x54\
-\xa9\x94\x53\x53\xfc\xac\x0b\xed\x22\xa0\x86\xc9\x4b\x87\x4b\xce\
-\x09\xd2\xee\x2d\x14\xa1\xa2\x08\x0d\x21\x54\x02\x96\xdf\xf2\xfb\
-\xd4\x2d\x00\x1a\x80\x65\x55\x76\xaa\x86\x86\xbc\xdd\xb1\x52\x97\
-\xc5\x5d\xbb\x5f\x57\xb1\x9b\x88\xd6\x02\xc0\xad\x42\x0f\x97\xb3\
-\x3f\xa2\x08\xed\x0e\x8d\x66\xe8\x58\x66\xa8\xb3\x0c\x08\x9b\x91\
-\x26\x4f\x4a\x54\x45\x9d\xd1\x7c\x81\xff\x21\x1a\x7d\xcb\xf1\x3c\
-\x8f\x91\x42\x2f\xc7\xe3\x7b\x31\xd5\x10\x86\x12\xb8\x43\x57\x14\
-\x0a\xe1\x40\xa8\xa9\x0c\x30\x0d\xb3\x5a\x11\x3a\x0a\x0a\x4c\x61\
-\x0c\x10\x50\x23\x2c\x31\x1f\x43\xb8\x3a\x49\xf7\x26\xc7\xe3\x7b\
-\x89\xe5\xa3\x00\x58\x6a\x15\x11\xa3\x11\x53\x0d\x23\x10\xe4\xbd\
-\x1c\xa6\xe1\xab\x2e\x03\x9c\xfc\x44\x5c\x57\x8c\x96\xff\x9a\x77\
-\x98\x9b\x08\x28\x61\xba\xed\x2f\xf1\x64\x91\x4e\x73\x1b\x16\xb5\
-\x14\x65\x8e\x0b\xf6\x57\x0c\xe5\xcf\x97\xb5\xb6\x9b\xc0\x76\x12\
-\xf8\x14\x8b\xb0\xde\x48\xde\x4b\xe1\xe4\xb3\xf1\x32\x20\x69\x8f\
-\x0d\x08\xe9\xad\x14\xc2\x00\x60\x51\xe0\x11\x96\x04\x36\x23\x50\
-\x18\x2b\x0c\x52\x70\x73\xdc\xa3\x2d\x06\xa0\x3f\x77\x96\x53\xa9\
-\xfd\x53\x56\x99\xf3\x6c\x86\x73\xbd\x54\xa9\xb5\x24\x27\xc6\x07\
-\xca\x00\xdb\x49\x75\x25\x9d\xa1\xad\xe8\x3a\xa6\x5a\x89\x81\x85\
-\xf0\x34\x3c\xcf\xa3\xcd\xd8\x80\x2e\x4c\x14\xa1\x13\x2b\x5c\xe5\
-\x58\xe2\x55\x60\xe6\x09\xac\xba\x3a\xb6\x93\xe9\x2a\x03\xb2\x39\
-\xf7\x78\x2e\x53\x7c\x65\x3c\xd0\x67\x49\x3c\xbc\xa2\xa4\xb5\x76\
-\x0d\x9a\xf0\x53\x55\x3a\x31\xb6\x1b\xe7\x64\xea\x1d\x26\xbc\xc4\
-\x8c\xe6\x02\x85\x82\x2d\xed\x6c\xce\x3d\x0e\xa5\xff\xe0\xd8\xce\
-\x7c\x77\x74\xa0\xff\x74\x8d\xd2\x00\xc0\x60\xbe\x9b\x44\xb1\xbf\
-\xbc\xc8\x93\x2e\x97\x9c\xef\xb8\xea\xfc\x34\xa3\x39\x40\x8d\x52\
-\x47\x74\x70\xf0\xf4\xe4\x64\x2d\x1f\xfc\x78\x22\xb6\x3d\x31\xe4\
-\x44\x4d\xc5\xc2\xc3\xe5\x77\xfb\x6b\x3c\x59\xa4\x20\x1d\x06\xf2\
-\xe7\xf8\x21\xf9\xe6\xac\xe6\xa6\x12\x20\x31\xe4\x44\xe3\x89\xf8\
-\xf6\x72\x45\xff\x9f\xa6\x9d\x0b\xef\x7f\x3f\xe9\xff\xbb\xc2\x95\
-\x1e\x0b\xcd\x87\xc9\xca\x34\xd7\xb3\x67\x98\xad\xef\x9a\xd0\x08\
-\x3b\xb5\xe9\xae\xde\xde\x3d\x53\x4e\xd3\xc9\xd8\xf6\x59\x78\x5f\
-\xc7\xbc\x96\x1d\x6a\x4d\xa1\x3e\xe9\xc6\x67\xdd\x35\x40\x58\x8d\
-\xe0\x8e\x6a\xc3\xbd\x43\xfd\x87\x8e\x3e\x9d\x9a\xfe\x3e\x98\x8c\
-\x4d\x6f\x19\xab\xdb\xda\xeb\x3f\x5a\xd0\x5e\xb7\x28\xad\x24\x94\
-\xac\x9c\xfa\x46\xf3\x0b\x3f\x15\x6e\xd8\xfb\xeb\xda\xc8\xe5\x8b\
-\x5d\xb1\xe7\x7f\xde\x57\x3c\x2d\x65\x69\xea\x4d\x05\x10\x42\xa8\
-\x40\x35\x10\x36\x2b\xa8\xdd\xf8\x7a\xf0\xa5\x96\xe6\xea\x65\xa1\
-\x50\x20\x12\x08\x2b\x41\xa3\x42\xd1\x01\x72\x29\xaf\x38\x91\x2c\
-\x66\xd2\xa9\x6c\xf2\xfa\xf5\xd8\x9f\x27\xdf\x98\x78\x37\x67\x33\
-\x0a\x8c\x01\x49\x29\x65\x72\xda\x0a\x84\x10\x16\x10\x01\x2a\x4b\
-\x59\x61\x04\x89\x34\x3c\xa8\x2c\x6c\x5a\xa5\xad\x90\x12\x65\xe0\
-\x6c\xf1\xfc\xcd\xb3\x5e\xb4\x60\x33\x06\x64\x80\x14\x30\x5e\xca\
-\x84\x94\xb7\x1f\x00\xd3\xbe\x2a\x4a\x30\x1d\x30\x00\x5f\x29\x75\
-\x40\x05\x8a\x40\x0e\xc8\x03\x59\xa0\x20\xe5\xd4\x37\xd5\x3f\x13\
-\x05\x02\x8c\xec\xcf\x7e\xae\x00\x00\x00\x00\x49\x45\x4e\x44\xae\
-\x42\x60\x82\
-\x00\x00\x05\x64\
-\x89\
-\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\x18\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0\x77\x3d\xf8\
-\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\
-\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\x0d\xd7\
-\x01\x42\x28\x9b\x78\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\
-\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\
-\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x04\xe1\x49\x44\
-\x41\x54\x48\x89\xb5\x95\x4b\x6c\x54\x55\x18\xc7\x7f\xe7\x9c\x3b\
-\xd3\x99\x32\xb5\xf4\x41\x1f\x38\x02\xa5\x8d\x4c\x54\xb0\x25\x48\
-\xa2\x12\x62\x14\x17\x46\x12\x64\x81\x26\xb0\x31\x18\x12\x36\x04\
-\xd2\x45\xe9\xca\x84\x5d\x29\x1a\x43\x80\x6e\x0c\x2b\xdc\x88\x09\
-\xa0\x21\x04\x35\x18\xa2\x26\x3e\xa3\x62\xa8\x4f\x7c\x61\x95\x81\
-\xb6\x4c\xcb\xbc\x67\xee\x39\x9f\x8b\xdb\xde\x4e\x2b\x10\x37\xde\
-\xe4\x4b\xee\xf3\xf7\xff\xfe\xdf\xf9\xee\x77\x94\x88\xf0\x7f\x1e\
-\xde\xdd\x1e\x9e\x53\xaa\x23\x6a\xcc\x33\xf1\x58\xec\xc9\xc6\xae\
-\x15\x2b\xeb\x97\x76\x2e\xb5\xc5\x62\x29\xfb\xd7\x5f\x63\xb9\x6b\
-\xd7\x7f\x2c\x55\x2a\xe7\x2b\xd6\x7e\xb0\x59\xa4\x70\x27\x86\xba\
-\xad\x03\xa5\xd4\x87\xb1\xd8\xcb\xc9\xc7\x1e\xdd\xdd\xd9\xde\xd6\
-\x11\xab\x8b\xa3\x4a\x45\xc8\xe5\xc1\x18\x48\x2c\xc2\x79\x86\x5c\
-\x21\xef\x7e\x1f\xbd\x7c\x65\xe2\xa7\x5f\xf7\x3c\x59\xad\xbe\xf7\
-\x9f\x04\xce\x2b\xb5\xa2\x75\x69\xe7\x1b\xa9\x0d\x8f\xaf\x4f\x54\
-\x6d\x84\x52\x29\x7c\x26\xc0\xec\xfb\x32\x13\x34\x24\xb8\x91\x9f\
-\x9e\xfa\xe5\xe3\x4f\xdf\xb6\xb7\xb2\xbb\x9f\x10\x29\xd5\xf2\xe6\
-\x09\xbc\xa7\x54\x6a\xc5\xda\xde\xf7\x7b\xba\xbb\x93\x3a\x57\x98\
-\x83\x2c\x80\x2e\x14\x11\xcf\xc3\xc6\x23\xf2\xdd\xa7\x9f\x7d\x91\
-\x1f\xbb\xf6\xf8\x13\x22\xfe\x2c\x53\xcf\x9e\x1c\x50\x4a\xb7\x2e\
-\x5f\x76\xa2\x67\xf9\xb2\x24\xd9\x3c\x4e\x24\x0c\x0b\xd8\x9a\x6b\
-\x07\xc1\x3d\xc0\x01\xce\xf7\x21\x5b\x54\x3d\x0f\xaf\x5e\xa7\xeb\
-\xeb\x5f\xab\x75\x10\x0a\x6c\x8a\xc7\x87\xee\x7f\xe8\x81\x3e\x29\
-\x56\x02\x80\x08\x95\xed\xdb\xa9\x6e\xd9\x32\x27\x52\x03\xb5\x9e\
-\x87\x1d\x18\xc0\xef\xed\x0d\xc5\x8d\x55\x3a\xd9\xf7\xe0\x0b\xef\
-\x46\xa3\xeb\x67\xb9\x1e\xc0\x05\xa5\xee\x5f\xf9\xd8\xfa\x17\xeb\
-\x7c\x67\x1c\x0a\x01\xec\x8e\x1d\xf8\xcf\x3d\x17\x64\xa1\x14\xfa\
-\xf4\xe9\xb0\x3c\x2e\x12\x81\xfd\xfb\xa1\xb7\x17\xfa\xfa\x70\x43\
-\x43\xf0\xf5\xd7\x88\x15\x9a\x1b\x9b\x96\x34\x25\xdb\x5f\x47\xa9\
-\x5e\x44\x44\x03\x78\x9e\xf7\x52\xc7\xe2\xa6\x25\x0e\x15\x64\xe9\
-\x79\xd8\x9e\x9e\xd0\xa6\xdb\xbe\x1d\x7f\xeb\xd6\x20\xd3\x68\x14\
-\x06\x07\x51\x7d\x7d\x28\xa5\x50\x91\x08\x74\x77\x63\x95\xc2\x29\
-\x85\xad\x38\x5a\xdb\x5a\x97\x9d\x85\x9e\xd0\xc1\xa2\xd6\x96\x07\
-\x11\x70\x5a\x07\x59\x3a\x07\x43\x43\xa8\xc1\x41\x58\xbd\x3a\x50\
-\xd9\xb1\x03\x17\x89\xa0\x52\x29\xd4\x9a\x35\xa1\xb8\x3d\x79\x12\
-\xff\xcc\x19\xc4\x18\x10\x41\xb4\xa6\x2e\x16\x5b\x1c\xd1\xfa\x29\
-\xe0\x67\x0d\x10\x6b\x6d\xba\xcf\x39\xc1\x69\x3d\x17\xd6\xe2\x86\
-\x87\x91\xcb\x97\xe7\x16\xec\xf9\xe7\x43\xb8\x88\xe0\xbf\xf9\x26\
-\xd5\x53\xa7\x82\xc4\xb4\xc6\x79\x1e\x62\x0c\x26\x12\xa5\x2e\x11\
-\xdf\x00\xa0\xdf\x52\x2a\x51\xb7\x28\xde\xee\xbc\xc8\x7c\x01\xad\
-\x71\xce\xe1\xbf\xf2\x0a\x32\x3a\x3a\x57\x2e\xe7\xf0\x7d\x9f\xe2\
-\xa1\x43\x64\x07\x06\x28\x5f\xb9\x82\x2d\x95\x70\xc6\x04\xdf\x18\
-\x83\xad\x5a\xe2\x4b\x9a\x97\x87\x25\xc2\x98\x60\xe1\x6a\x7b\x7b\
-\x06\x28\xc6\x20\x4a\x81\x13\x9c\xb8\xf0\x1f\x70\x80\xb5\x16\x3f\
-\x9d\x46\xd2\x69\x74\x4b\x0b\x5e\x32\x89\x6e\x6e\xc6\x96\xcb\x88\
-\xb8\xa0\x8b\xb6\x89\xe4\xbe\x79\x68\xd5\x75\xb9\xd7\xb4\x8b\x95\
-\x00\xae\x54\x00\x8f\x44\x88\xec\xdb\x07\xa9\x14\xd6\xd9\xda\xf6\
-\x26\xd6\xdf\x8f\x03\x0a\x87\x0f\x07\x5d\x37\x31\x41\x65\x7c\x1c\
-\xe2\x71\xbc\xce\x26\xf2\x13\x53\x7f\x84\xff\x41\x71\x32\xf3\xa7\
-\x3f\x39\x81\xd3\x26\xb4\xea\x97\xca\xe8\x5d\xbb\x90\x55\xab\x70\
-\x2e\xc8\xa6\xf0\xea\xab\x54\x3f\xfa\x28\x14\xa9\xef\xef\x27\xb6\
-\x77\x2f\x56\x24\x08\xc0\x2f\x14\xa8\x7a\x9a\x72\xae\xf8\x71\x58\
-\xa2\xc2\x44\x66\xb4\x9c\xcd\x3c\xeb\x46\x7f\x40\x25\x12\x38\xdf\
-\x27\x3e\x32\x82\x5e\xbb\x36\x84\xe5\x87\x87\x29\x1c\x3b\x86\x44\
-\xa3\x34\x1e\x3f\x4e\xdd\xc6\x8d\x00\x24\xfa\xfb\x71\xbe\x4f\xf6\
-\xc8\x91\xb0\xb4\x3e\x4c\x39\xe7\x2e\x84\x0e\xf0\xfd\xe3\x19\xe7\
-\xc6\x2d\x8e\xca\xf8\x38\xd5\x4c\x06\xff\xd2\xa5\x10\x9e\x3b\x78\
-\x90\xdc\xd1\xa3\x41\x96\xe5\x32\x93\x3b\x77\x52\xba\x78\x31\x28\
-\x63\xb9\x4c\xe9\xdb\x6f\xf1\x67\x5c\xe8\xf6\x26\xa6\x27\x32\x57\
-\x37\xc3\x15\xa8\x19\x76\x17\xea\xeb\x87\xbb\xfa\x52\xfd\xf6\xf2\
-\x2f\x46\x24\x58\x8b\xc4\xe0\x20\xf6\xd6\x2d\x72\x23\x23\x41\x8f\
-\xd7\x36\x41\x34\x4a\xeb\xc8\x08\xd9\x13\x27\x28\x5c\xbc\x18\x0c\
-\xc4\xa8\x87\xac\xec\x18\xff\xfb\xab\x9f\x36\x6f\x16\xf9\x7c\x9e\
-\xc0\x01\xa5\xf4\xa6\x64\xfb\x67\x1d\x0d\x0d\xeb\x2a\x63\xd7\x91\
-\x05\xc0\xd9\xde\x9f\x27\x52\x73\x0d\x50\x97\xba\xcf\xa5\xbf\xff\
-\x7d\xe4\xe9\x42\x69\xcf\xac\xfb\x79\xe3\xfa\xac\x52\xa9\xb6\x54\
-\xd7\xfb\x8d\xc6\x24\x2b\x7f\xa4\xff\x05\x58\x08\x0d\x47\xb7\x67\
-\x88\x76\x77\xca\x74\xfa\xe6\x17\x92\x9e\x9c\x37\xae\x6f\xbb\xe1\
-\xc4\xda\x9a\xdf\x58\xb2\x62\xe9\x23\xfe\x6f\xd7\xa2\x36\x5f\x9c\
-\x2f\xb0\x40\xc4\x6b\x6b\x44\x9a\x13\x99\xc9\x1f\xc7\xde\x89\x15\
-\x4a\x77\xdf\x70\x00\x94\x52\x0d\x31\x68\x39\x12\x31\x03\xa9\x55\
-\xcb\xb7\x35\x18\xd3\xaa\x7c\x8b\x9d\xce\x53\x9d\xce\x83\xd1\x98\
-\xc5\x09\x74\x43\x0c\xab\x95\x4c\x66\x72\x63\x17\xae\x5e\x3f\x78\
-\x08\xde\x05\xa6\x80\x29\xb9\x93\x03\xa5\x54\x04\x68\x01\x16\x03\
-\x4d\xbd\xd0\xb5\x49\xeb\x8d\x5d\x9e\xe9\x6b\x69\x69\x68\x6b\x6c\
-\x5c\xd4\x58\xf5\xad\xbd\x99\xc9\x4d\xdf\x98\xce\x5f\x1b\xf5\xed\
-\x97\xe7\xe0\x93\x71\x48\xcf\xc2\x81\x8c\x88\x64\xef\xe8\x60\x46\
-\xa8\x1e\xb8\x67\x26\xea\x81\x38\x50\x07\x18\x40\x11\xec\x3b\x15\
-\xa0\x0c\xe4\x81\x1c\x70\x0b\xc8\xca\xec\x8c\x98\x39\xfe\x01\x76\
-\x95\xba\xf1\x06\x3a\xff\x81\x00\x00\x00\x00\x49\x45\x4e\x44\xae\
-\x42\x60\x82\
-"
-
-qt_resource_name = "\
-\x00\x06\
-\x07\x03\x7d\xc3\
-\x00\x69\
-\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\
-\x00\x0e\
-\x05\xcd\xf4\xe7\
-\x00\x63\
-\x00\x6f\x00\x6e\x00\x6e\x00\x5f\x00\x65\x00\x72\x00\x72\x00\x6f\x00\x72\x00\x2e\x00\x70\x00\x6e\x00\x67\
-\x00\x13\
-\x09\xd2\x6c\x67\
-\x00\x45\
-\x00\x6d\x00\x62\x00\x6c\x00\x65\x00\x6d\x00\x2d\x00\x71\x00\x75\x00\x65\x00\x73\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x2e\x00\x70\
-\x00\x6e\x00\x67\
-\x00\x12\
-\x04\xe4\x91\x47\
-\x00\x63\
-\x00\x6f\x00\x6e\x00\x6e\x00\x5f\x00\x63\x00\x6f\x00\x6e\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\
-\x00\x67\
-\x00\x0d\
-\x03\x9b\xc4\xc7\
-\x00\x77\
-\x00\x61\x00\x74\x00\x65\x00\x72\x00\x6d\x00\x61\x00\x72\x00\x6b\x00\x2e\x00\x70\x00\x6e\x00\x67\
-\x00\x13\
-\x0d\x76\x37\xc7\
-\x00\x63\
-\x00\x6f\x00\x6e\x00\x6e\x00\x5f\x00\x63\x00\x6f\x00\x6e\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x69\x00\x6e\x00\x67\x00\x2e\x00\x70\
-\x00\x6e\x00\x67\
-\x00\x14\
-\x00\xe9\x23\x87\
-\x00\x6c\
-\x00\x65\x00\x61\x00\x70\x00\x2d\x00\x63\x00\x6f\x00\x6c\x00\x6f\x00\x72\x00\x2d\x00\x73\x00\x6d\x00\x61\x00\x6c\x00\x6c\x00\x2e\
-\x00\x70\x00\x6e\x00\x67\
-\x00\x11\
-\x06\x1a\x44\xa7\
-\x00\x44\
-\x00\x69\x00\x61\x00\x6c\x00\x6f\x00\x67\x00\x2d\x00\x61\x00\x63\x00\x63\x00\x65\x00\x70\x00\x74\x00\x2e\x00\x70\x00\x6e\x00\x67\
-\
-\x00\x10\
-\x0f\xc3\x90\x67\
-\x00\x44\
-\x00\x69\x00\x61\x00\x6c\x00\x6f\x00\x67\x00\x2d\x00\x65\x00\x72\x00\x72\x00\x6f\x00\x72\x00\x2e\x00\x70\x00\x6e\x00\x67\
-"
-
-qt_resource_struct = "\
-\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
-\x00\x00\x00\x00\x00\x02\x00\x00\x00\x08\x00\x00\x00\x02\
-\x00\x00\x00\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x68\x2a\
-\x00\x00\x00\x8a\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x37\
-\x00\x00\x00\x60\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x89\
-\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
-\x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x00\x8f\xa2\
-\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x05\x99\
-\x00\x00\x00\xaa\x00\x00\x00\x00\x00\x01\x00\x00\x65\x5e\
-\x00\x00\x01\x2c\x00\x00\x00\x00\x00\x01\x00\x00\x94\xca\
-"
-
-def qInitResources():
- QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
-
-def qCleanupResources():
- QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
-
-qInitResources()
diff --git a/src/leap/gui/progress.py b/src/leap/gui/progress.py
deleted file mode 100644
index ca4f6cc3..00000000
--- a/src/leap/gui/progress.py
+++ /dev/null
@@ -1,488 +0,0 @@
-"""
-classes used in progress pages
-from first run wizard
-"""
-try:
- from collections import OrderedDict
-except ImportError: # pragma: no cover
- # We must be in 2.6
- from leap.util.dicts import OrderedDict
-
-import logging
-
-from PyQt4 import QtCore
-from PyQt4 import QtGui
-
-from leap.gui.threads import FunThread
-
-from leap.gui import mainwindow_rc
-
-ICON_CHECKMARK = ":/images/Dialog-accept.png"
-ICON_FAILED = ":/images/Dialog-error.png"
-ICON_WAITING = ":/images/Emblem-question.png"
-
-logger = logging.getLogger(__name__)
-
-
-class ImgWidget(QtGui.QWidget):
-
- # XXX move to widgets
-
- def __init__(self, parent=None, img=None):
- super(ImgWidget, self).__init__(parent)
- self.pic = QtGui.QPixmap(img)
-
- def paintEvent(self, event):
- painter = QtGui.QPainter(self)
- painter.drawPixmap(0, 0, self.pic)
-
-
-class ProgressStep(object):
- """
- Data model for sequential steps
- to be used in a progress page in
- connection wizard
- """
- NAME = 0
- DONE = 1
-
- def __init__(self, stepname, done, index=None):
- """
- @param step: the name of the step
- @type step: str
- @param done: whether is completed or not
- @type done: bool
- """
- self.index = int(index) if index else 0
- self.name = unicode(stepname)
- self.done = bool(done)
-
- @classmethod
- def columns(self):
- return ('name', 'done')
-
-
-class ProgressStepContainer(object):
- """
- a container for ProgressSteps objects
- access data in the internal dict
- """
-
- def __init__(self):
- self.dirty = False
- self.steps = {}
-
- def step(self, identity):
- return self.steps.get(identity, None)
-
- def addStep(self, step):
- self.steps[step.index] = step
-
- def removeStep(self, step):
- if step and self.steps.get(step.index, None):
- del self.steps[step.index]
- del step
- self.dirty = True
-
- def removeAllSteps(self):
- for item in iter(self):
- self.removeStep(item)
-
- @property
- def columns(self):
- return ProgressStep.columns()
-
- def __len__(self):
- return len(self.steps)
-
- def __iter__(self):
- for step in self.steps.values():
- yield step
-
-
-class StepsTableWidget(QtGui.QTableWidget):
- """
- initializes a TableWidget
- suitable for our display purposes, like removing
- header info and grid display
- """
-
- def __init__(self, parent=None):
- super(StepsTableWidget, self).__init__(parent=parent)
-
- # remove headers and all edit/select behavior
- self.horizontalHeader().hide()
- self.verticalHeader().hide()
- self.setEditTriggers(
- QtGui.QAbstractItemView.NoEditTriggers)
- self.setSelectionMode(
- QtGui.QAbstractItemView.NoSelection)
- width = self.width()
-
- # WTF? Here init width is 100...
- # but on populating is 456... :(
- #logger.debug('init table. width=%s' % width)
-
- # XXX do we need this initial?
- self.horizontalHeader().resizeSection(0, width * 0.7)
-
- # this disables the table grid.
- # we should add alignment to the ImgWidget (it's top-left now)
- self.setShowGrid(False)
- self.setFocusPolicy(QtCore.Qt.NoFocus)
- #self.setStyleSheet("QTableView{outline: 0;}")
-
- # XXX change image for done to rc
-
- # Note about the "done" status painting:
- #
- # XXX currently we are setting the CellWidget
- # for the whole table on a per-row basis
- # (on add_status_line method on ValidationPage).
- # However, a more generic solution might be
- # to implement a custom Delegate that overwrites
- # the paint method (so it paints a checked tickmark if
- # done is True and some other thing if checking or false).
- # What we have now is quick and works because
- # I'm supposing that on first fail we will
- # go back to previous wizard page to signal the failure.
- # A more generic solution could be used for
- # some failing tests if they are not critical.
-
-
-class WithStepsMixIn(object):
- """
- This Class is a mixin that can be inherited
- by InlineValidation pages (which will display
- a progress steps widget in the same page as the form)
- or by Validation Pages (which will only display
- the progress steps in the page, below a progress bar widget)
- """
- STEPS_TIMER_MS = 100
-
- #
- # methods related to worker threads
- # launched for individual checks
- #
-
- def setupStepsProcessingQueue(self):
- """
- should be called from the init method
- of the derived classes
- """
- self.steps_queue = Queue.Queue()
- self.stepscheck_timer = QtCore.QTimer()
- self.stepscheck_timer.timeout.connect(self.processStepsQueue)
- self.stepscheck_timer.start(self.STEPS_TIMER_MS)
- # we need to keep a reference to child threads
- self.threads = []
-
- def do_checks(self):
- """
- main entry point for checks.
- it calls _do_checks in derived classes,
- and it expects it to be a generator
- yielding a tuple in the form (("message", progress_int), checkfunction)
- """
-
- # yo dawg, I heard you like checks
- # so I put a __do_checks in your do_checks
- # for calling others' _do_checks
-
- def __do_checks(fun=None, queue=None):
-
- for checkcase in fun(): # pragma: no cover
- checkmsg, checkfun = checkcase
-
- queue.put(checkmsg)
- if checkfun() is False:
- queue.put("failed")
- break
-
- t = FunThread(fun=partial(
- __do_checks,
- fun=self._do_checks,
- queue=self.steps_queue))
- if hasattr(self, 'on_checks_validation_ready'):
- t.finished.connect(self.on_checks_validation_ready)
- t.begin()
- self.threads.append(t)
-
- def processStepsQueue(self):
- """
- consume steps queue
- and pass messages
- to the ui updater functions
- """
- while self.steps_queue.qsize():
- try:
- status = self.steps_queue.get(0)
- if status == "failed":
- self.set_failed_icon()
- else:
- self.onStepStatusChanged(*status)
- except Queue.Empty: # pragma: no cover
- pass
-
- def fail(self, err=None):
- """
- return failed state
- and send error notification as
- a nice side effect. this function is called from
- the _do_checks check functions returned in the
- generator.
- """
- wizard = self.wizard()
- senderr = lambda err: wizard.set_validation_error(
- self.current_page, err)
- self.set_undone()
- if err:
- senderr(err)
- return False
-
- @QtCore.pyqtSlot()
- def launch_checks(self):
- self.do_checks()
-
- # (gui) presentation stuff begins #####################
-
- # slot
- #@QtCore.pyqtSlot(str, int)
- def onStepStatusChanged(self, status, progress=None):
- status = unicode(status)
- if status not in ("head_sentinel", "end_sentinel"):
- self.add_status_line(status)
- if status in ("end_sentinel"):
- #self.checks_finished = True
- self.set_checked_icon()
- if progress and hasattr(self, 'progress'):
- self.progress.setValue(progress)
- self.progress.update()
-
- def setupSteps(self):
- self.steps = ProgressStepContainer()
- # steps table widget
- if isinstance(self, QtCore.QObject):
- parent = self
- else:
- parent = None
- self.stepsTableWidget = StepsTableWidget(parent=parent)
- zeros = (0, 0, 0, 0)
- self.stepsTableWidget.setContentsMargins(*zeros)
- self.errors = OrderedDict()
-
- def set_error(self, name, error):
- self.errors[name] = error
-
- def pop_first_error(self):
- errkey, errval = list(reversed(self.errors.items())).pop()
- del self.errors[errkey]
- return errkey, errval
-
- def clean_errors(self):
- self.errors = OrderedDict()
-
- def clean_wizard_errors(self, pagename=None):
- if pagename is None: # pragma: no cover
- pagename = getattr(self, 'prev_page', None)
- if pagename is None: # pragma: no cover
- return
- #logger.debug('cleaning wizard errors for %s' % pagename)
- self.wizard().set_validation_error(pagename, None)
-
- def populateStepsTable(self):
- # from examples,
- # but I guess it's not needed to re-populate
- # the whole table.
- table = self.stepsTableWidget
- table.setRowCount(len(self.steps))
- columns = self.steps.columns
- table.setColumnCount(len(columns))
-
- for row, step in enumerate(self.steps):
- item = QtGui.QTableWidgetItem(step.name)
- item.setData(QtCore.Qt.UserRole,
- long(id(step)))
- table.setItem(row, columns.index('name'), item)
- table.setItem(row, columns.index('done'),
- QtGui.QTableWidgetItem(step.done))
- self.resizeTable()
- self.update()
-
- def clearTable(self):
- # ??? -- not sure what's the difference
- #self.stepsTableWidget.clear()
- self.stepsTableWidget.clearContents()
-
- def resizeTable(self):
- # resize first column to ~80%
- table = self.stepsTableWidget
- FIRST_COLUMN_PERCENT = 0.70
- width = table.width()
- #logger.debug('populate table. width=%s' % width)
- table.horizontalHeader().resizeSection(0, width * FIRST_COLUMN_PERCENT)
-
- def set_item_icon(self, img=ICON_CHECKMARK, current=True):
- """
- mark the last item
- as done
- """
- # setting cell widget.
- # see note on StepsTableWidget about plans to
- # change this for a better solution.
- if not hasattr(self, 'steps'):
- return
- index = len(self.steps)
- table = self.stepsTableWidget
- _index = index - 1 if current else index - 2
- table.setCellWidget(
- _index,
- ProgressStep.DONE,
- ImgWidget(img=img))
- table.update()
-
- def set_failed_icon(self):
- self.set_item_icon(img=ICON_FAILED, current=True)
-
- def set_checking_icon(self):
- self.set_item_icon(img=ICON_WAITING, current=True)
-
- def set_checked_icon(self, current=True):
- self.set_item_icon(current=current)
-
- def add_status_line(self, message):
- """
- adds a new status line
- and mark the next-to-last item
- as done
- """
- index = len(self.steps)
- step = ProgressStep(message, False, index=index)
- self.steps.addStep(step)
- self.populateStepsTable()
- self.set_checking_icon()
- self.set_checked_icon(current=False)
-
- # Sets/unsets done flag
- # for isComplete checks
-
- def set_done(self):
- self.done = True
- self.completeChanged.emit()
-
- def set_undone(self):
- self.done = False
- self.completeChanged.emit()
-
- def is_done(self):
- return self.done
-
- # convenience for going back and forth
- # in the wizard pages.
-
- def go_back(self):
- self.wizard().back()
-
- def go_next(self):
- self.wizard().next()
-
-
-"""
-We will use one base class for the intermediate pages
-and another one for the in-page validations, both sharing the creation
-of the tablewidgets.
-The logic of this split comes from where I was trying to solve
-the ui update using signals, but now that it's working well with
-queues I could join them again.
-"""
-
-import Queue
-from functools import partial
-
-
-class InlineValidationPage(QtGui.QWizardPage, WithStepsMixIn):
-
- def __init__(self, parent=None):
- super(InlineValidationPage, self).__init__(parent)
- self.setupStepsProcessingQueue()
- self.done = False
-
- # slot
-
- @QtCore.pyqtSlot()
- def showStepsFrame(self):
- self.valFrame.show()
- self.update()
-
- # progress frame
-
- def setupValidationFrame(self):
- qframe = QtGui.QFrame
- valFrame = qframe()
- valFrame.setFrameStyle(qframe.NoFrame)
- valframeLayout = QtGui.QVBoxLayout()
- zeros = (0, 0, 0, 0)
- valframeLayout.setContentsMargins(*zeros)
-
- valframeLayout.addWidget(self.stepsTableWidget)
- valFrame.setLayout(valframeLayout)
- self.valFrame = valFrame
-
-
-class ValidationPage(QtGui.QWizardPage, WithStepsMixIn):
- """
- class to be used as an intermediate
- between two pages in a wizard.
- shows feedback to the user and goes back if errors,
- goes forward if ok.
- initializePage triggers a one shot timer
- that calls do_checks.
- Derived classes should implement
- _do_checks and
- _do_validation
- """
-
- # signals
- stepChanged = QtCore.pyqtSignal([str, int])
-
- def __init__(self, parent=None):
- super(ValidationPage, self).__init__(parent)
- self.setupSteps()
- #self.connect_step_status()
-
- layout = QtGui.QVBoxLayout()
- self.progress = QtGui.QProgressBar(self)
- layout.addWidget(self.progress)
- layout.addWidget(self.stepsTableWidget)
-
- self.setLayout(layout)
- self.layout = layout
-
- self.timer = QtCore.QTimer()
- self.done = False
-
- self.setupStepsProcessingQueue()
-
- def isComplete(self):
- return self.is_done()
-
- ########################
-
- def show_progress(self):
- self.progress.show()
- self.stepsTableWidget.show()
-
- def hide_progress(self):
- self.progress.hide()
- self.stepsTableWidget.hide()
-
- # pagewizard methods.
- # if overriden, child classes should call super.
-
- def initializePage(self):
- self.clean_errors()
- self.clean_wizard_errors()
- self.steps.removeAllSteps()
- self.clearTable()
- self.resizeTable()
- self.timer.singleShot(0, self.do_checks)
diff --git a/src/leap/gui/statuspanel.py b/src/leap/gui/statuspanel.py
new file mode 100644
index 00000000..f3424c7c
--- /dev/null
+++ b/src/leap/gui/statuspanel.py
@@ -0,0 +1,459 @@
+# -*- coding: utf-8 -*-
+# statuspanel.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Status Panel widget implementation
+"""
+import logging
+
+from datetime import datetime
+from functools import partial
+from PySide import QtCore, QtGui
+
+from ui_statuspanel import Ui_StatusPanel
+
+from leap.common.check import leap_assert_type
+from leap.services.eip.vpnprocess import VPNManager
+from leap.platform_init import IS_WIN, IS_LINUX
+from leap.util import first
+
+logger = logging.getLogger(__name__)
+
+
+class RateMovingAverage(object):
+ """
+ Moving window average for calculating
+ upload and download rates.
+ """
+ SAMPLE_SIZE = 5
+
+ def __init__(self):
+ """
+ Initializes an empty array of fixed size
+ """
+ self.reset()
+
+ def reset(self):
+ self._data = [None for i in xrange(self.SAMPLE_SIZE)]
+
+ def append(self, x):
+ """
+ Appends a new data point to the collection.
+
+ :param x: A tuple containing timestamp and traffic points
+ in the form (timestamp, traffic)
+ :type x: tuple
+ """
+ self._data.pop(0)
+ self._data.append(x)
+
+ def get(self):
+ """
+ Gets the collection.
+ """
+ return self._data
+
+ def get_average(self):
+ """
+ Gets the moving average.
+ """
+ data = filter(None, self.get())
+ traff = [traffic for (ts, traffic) in data]
+ times = [ts for (ts, traffic) in data]
+
+ try:
+ deltatraffic = traff[-1] - first(traff)
+ deltat = (times[-1] - first(times)).seconds
+ except IndexError:
+ deltatraffic = 0
+ deltat = 0
+
+ try:
+ rate = float(deltatraffic) / float(deltat) / 1024
+ except ZeroDivisionError:
+ rate = 0
+
+ # In some cases we get negative rates
+ if rate < 0:
+ rate = 0
+
+ return rate
+
+ def get_total(self):
+ """
+ Gets the total accumulated throughput.
+ """
+ try:
+ return self._data[-1][1] / 1024
+ except TypeError:
+ return 0
+
+
+class StatusPanelWidget(QtGui.QWidget):
+ """
+ Status widget that displays the current state of the LEAP services
+ """
+
+ start_eip = QtCore.Signal()
+ stop_eip = QtCore.Signal()
+
+ DISPLAY_TRAFFIC_RATES = True
+ RATE_STR = "%14.2f KB/s"
+ TOTAL_STR = "%14.2f Kb"
+
+ def __init__(self, parent=None):
+ QtGui.QWidget.__init__(self, parent)
+
+ self._systray = None
+ self._action_eip_status = None
+
+ self.ui = Ui_StatusPanel()
+ self.ui.setupUi(self)
+
+ self.ui.btnEipStartStop.setEnabled(False)
+ self.ui.btnEipStartStop.clicked.connect(
+ self.start_eip)
+
+ self.hide_status_box()
+
+ # Set the EIP status icons
+ self.CONNECTING_ICON = None
+ self.CONNECTED_ICON = None
+ self.ERROR_ICON = None
+ self.CONNECTING_ICON_TRAY = None
+ self.CONNECTED_ICON_TRAY = None
+ self.ERROR_ICON_TRAY = None
+ self._set_eip_icons()
+
+ self._set_traffic_rates()
+ self._make_status_clickable()
+
+ def _make_status_clickable(self):
+ """
+ Makes upload and download figures clickable.
+ """
+ onclicked = self._on_VPN_status_clicked
+ self.ui.btnUpload.clicked.connect(onclicked)
+ self.ui.btnDownload.clicked.connect(onclicked)
+
+ def _on_VPN_status_clicked(self):
+ """
+ SLOT
+ TRIGGER: self.ui.btnUpload.clicked
+ self.ui.btnDownload.clicked
+
+ Toggles between rate and total throughput display for vpn
+ status figures.
+ """
+ self.DISPLAY_TRAFFIC_RATES = not self.DISPLAY_TRAFFIC_RATES
+ self.update_vpn_status(None) # refresh
+
+ def _set_traffic_rates(self):
+ """
+ Initializes up and download rates.
+ """
+ self._up_rate = RateMovingAverage()
+ self._down_rate = RateMovingAverage()
+
+ self.ui.btnUpload.setText(self.RATE_STR % (0,))
+ self.ui.btnDownload.setText(self.RATE_STR % (0,))
+
+ def _reset_traffic_rates(self):
+ """
+ Resets up and download rates, and cleans up the labels.
+ """
+ self._up_rate.reset()
+ self._down_rate.reset()
+ self.update_vpn_status(None)
+
+ def _update_traffic_rates(self, up, down):
+ """
+ Updates up and download rates.
+
+ :param up: upload total.
+ :type up: int
+ :param down: download total.
+ :type down: int
+ """
+ ts = datetime.now()
+ self._up_rate.append((ts, up))
+ self._down_rate.append((ts, down))
+
+ def _get_traffic_rates(self):
+ """
+ Gets the traffic rates (in KB/s).
+
+ :returns: a tuple with the (up, down) rates
+ :rtype: tuple
+ """
+ up = self._up_rate
+ down = self._down_rate
+
+ return (up.get_average(), down.get_average())
+
+ def _get_traffic_totals(self):
+ """
+ Gets the traffic total throughput (in Kb).
+
+ :returns: a tuple with the (up, down) totals
+ :rtype: tuple
+ """
+ up = self._up_rate
+ down = self._down_rate
+
+ return (up.get_total(), down.get_total())
+
+ def _set_eip_icons(self):
+ """
+ Sets the EIP status icons for the main window and for the tray
+
+ MAC : dark icons
+ LINUX : dark icons in window, light icons in tray
+ WIN : light icons
+ """
+ EIP_ICONS = EIP_ICONS_TRAY = (
+ ":/images/conn_connecting-light.png",
+ ":/images/conn_connected-light.png",
+ ":/images/conn_error-light.png")
+
+ if IS_LINUX:
+ EIP_ICONS_TRAY = (
+ ":/images/conn_connecting.png",
+ ":/images/conn_connected.png",
+ ":/images/conn_error.png")
+ elif IS_WIN:
+ EIP_ICONS = EIP_ICONS_TRAY = (
+ ":/images/conn_connecting.png",
+ ":/images/conn_connected.png",
+ ":/images/conn_error.png")
+
+ self.CONNECTING_ICON = QtGui.QPixmap(EIP_ICONS[0])
+ self.CONNECTED_ICON = QtGui.QPixmap(EIP_ICONS[1])
+ self.ERROR_ICON = QtGui.QPixmap(EIP_ICONS[2])
+
+ self.CONNECTING_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[0])
+ self.CONNECTED_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[1])
+ self.ERROR_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[2])
+
+ def set_systray(self, systray):
+ """
+ Sets the systray object to use.
+
+ :param systray: Systray object
+ :type systray: QtGui.QSystemTrayIcon
+ """
+ leap_assert_type(systray, QtGui.QSystemTrayIcon)
+ self._systray = systray
+
+ def set_action_eip_startstop(self, action_eip_startstop):
+ """
+ Sets the action_eip_startstop to use.
+
+ :param action_eip_startstop: action_eip_status to be used
+ :type action_eip_startstop: QtGui.QAction
+ """
+ self._action_eip_startstop = action_eip_startstop
+
+ def set_action_eip_status(self, action_eip_status):
+ """
+ Sets the action_eip_status to use.
+
+ :param action_eip_status: action_eip_status to be used
+ :type action_eip_status: QtGui.QAction
+ """
+ leap_assert_type(action_eip_status, QtGui.QAction)
+ self._action_eip_status = action_eip_status
+
+ def set_global_status(self, status, error=False):
+ """
+ Sets the global status label.
+
+ :param status: status message
+ :type status: str or unicode
+ :param error: if the status is an erroneous one, then set this
+ to True
+ :type error: bool
+ """
+ leap_assert_type(error, bool)
+ if error:
+ status = "<font color='red'><b>%s</b></font>" % (status,)
+ self.ui.lblGlobalStatus.setText(status)
+ self.ui.globalStatusBox.show()
+
+ def hide_status_box(self):
+ """
+ Hide global status box.
+ """
+ self.ui.globalStatusBox.hide()
+
+ def set_eip_status(self, status, error=False):
+ """
+ Sets the status label at the VPN stage to status
+
+ :param status: status message
+ :type status: str or unicode
+ :param error: if the status is an erroneous one, then set this
+ to True
+ :type error: bool
+ """
+ leap_assert_type(error, bool)
+
+ self._systray.setToolTip(status)
+ if error:
+ status = "<font color='red'>%s</font>" % (status,)
+ self.ui.lblEIPStatus.setText(status)
+
+ def set_startstop_enabled(self, value):
+ """
+ Enable or disable btnEipStartStop and _action_eip_startstop
+ based on value
+
+ :param value: True for enabled, False otherwise
+ :type value: bool
+ """
+ leap_assert_type(value, bool)
+ self.ui.btnEipStartStop.setEnabled(value)
+ self._action_eip_startstop.setEnabled(value)
+
+ def eip_pre_up(self):
+ """
+ Triggered when the app activates eip.
+ Hides the status box and disables the start/stop button.
+ """
+ self.hide_status_box()
+ self.set_startstop_enabled(False)
+
+ def eip_started(self):
+ """
+ Sets the state of the widget to how it should look after EIP
+ has started
+ """
+ self.ui.btnEipStartStop.setText(self.tr("Turn OFF"))
+ self.ui.btnEipStartStop.disconnect(self)
+ self.ui.btnEipStartStop.clicked.connect(
+ self.stop_eip)
+
+ def eip_stopped(self):
+ """
+ Sets the state of the widget to how it should look after EIP
+ has stopped
+ """
+ self._reset_traffic_rates()
+ self.ui.btnEipStartStop.setText(self.tr("Turn ON"))
+ self.ui.btnEipStartStop.disconnect(self)
+ self.ui.btnEipStartStop.clicked.connect(
+ self.start_eip)
+
+ def set_icon(self, icon):
+ """
+ Sets the icon to display for EIP
+
+ :param icon: icon to display
+ :type icon: QPixmap
+ """
+ self.ui.lblVPNStatusIcon.setPixmap(icon)
+
+ def update_vpn_status(self, data):
+ """
+ SLOT
+ TRIGGER: VPN.status_changed
+
+ Updates the download/upload labels based on the data provided
+ by the VPN thread.
+
+ :param data: a dictionary with the tcp/udp write and read totals.
+ If data is None, we just will refresh the display based
+ on the previous data.
+ :type data: dict
+ """
+ if data:
+ upload = float(data[VPNManager.TCPUDP_WRITE_KEY] or "0")
+ download = float(data[VPNManager.TCPUDP_READ_KEY] or "0")
+ self._update_traffic_rates(upload, download)
+
+ if self.DISPLAY_TRAFFIC_RATES:
+ uprate, downrate = self._get_traffic_rates()
+ upload_str = self.RATE_STR % (uprate,)
+ download_str = self.RATE_STR % (downrate,)
+
+ else: # display total throughput
+ uptotal, downtotal = self._get_traffic_totals()
+ upload_str = self.TOTAL_STR % (uptotal,)
+ download_str = self.TOTAL_STR % (downtotal,)
+
+ self.ui.btnUpload.setText(upload_str)
+ self.ui.btnDownload.setText(download_str)
+
+ def update_vpn_state(self, data):
+ """
+ SLOT
+ TRIGGER: VPN.state_changed
+
+ Updates the displayed VPN state based on the data provided by
+ the VPN thread
+ """
+ status = data[VPNManager.STATUS_STEP_KEY]
+ self.set_eip_status_icon(status)
+ if status == "CONNECTED":
+ self.set_eip_status(self.tr("ON"))
+ # Only now we can properly enable the button.
+ self.set_startstop_enabled(True)
+ elif status == "AUTH":
+ self.set_eip_status(self.tr("Authenticating..."))
+ elif status == "GET_CONFIG":
+ self.set_eip_status(self.tr("Retrieving configuration..."))
+ elif status == "WAIT":
+ self.set_eip_status(self.tr("Waiting to start..."))
+ elif status == "ASSIGN_IP":
+ self.set_eip_status(self.tr("Assigning IP"))
+ elif status == "ALREADYRUNNING":
+ # Put the following calls in Qt's event queue, otherwise
+ # the UI won't update properly
+ QtCore.QTimer.singleShot(0, self.stop_eip)
+ QtCore.QTimer.singleShot(0, partial(self.set_global_status,
+ self.tr("Unable to start VPN, "
+ "it's already "
+ "running.")))
+ else:
+ self.set_eip_status(status)
+
+ def set_eip_status_icon(self, status):
+ """
+ Given a status step from the VPN thread, set the icon properly
+
+ :param status: status step
+ :type status: str
+ """
+ selected_pixmap = self.ERROR_ICON
+ selected_pixmap_tray = self.ERROR_ICON_TRAY
+ tray_message = self.tr("Encryption is OFF")
+ if status in ("WAIT", "AUTH", "GET_CONFIG",
+ "RECONNECTING", "ASSIGN_IP"):
+ selected_pixmap = self.CONNECTING_ICON
+ selected_pixmap_tray = self.CONNECTING_ICON_TRAY
+ tray_message = self.tr("Turning ON")
+ elif status in ("CONNECTED"):
+ tray_message = self.tr("Encryption is ON")
+ selected_pixmap = self.CONNECTED_ICON
+ selected_pixmap_tray = self.CONNECTED_ICON_TRAY
+
+ self.set_icon(selected_pixmap)
+ self._systray.setIcon(QtGui.QIcon(selected_pixmap_tray))
+ self._action_eip_status.setText(tray_message)
+
+ def set_provider(self, provider):
+ self.ui.lblProvider.setText(provider)
diff --git a/src/leap/gui/styles.py b/src/leap/gui/styles.py
deleted file mode 100644
index b482922e..00000000
--- a/src/leap/gui/styles.py
+++ /dev/null
@@ -1,16 +0,0 @@
-GreenLineEdit = "QLabel {color: green; font-weight: bold}"
-ErrorLabelStyleSheet = """QLabel { color: red; font-weight: bold }"""
-ErrorLineEdit = """QLineEdit { border: 1px solid red; }"""
-
-
-# XXX this is bad.
-# and you should feel bad for it.
-# The original style has a sort of box color
-# white/beige left-top/right-bottom or something like
-# that.
-
-RegularLineEdit = """
-QLineEdit {
- border: 1px solid black;
-}
-"""
diff --git a/src/leap/gui/tests/integration/fake_user_signup.py b/src/leap/gui/tests/integration/fake_user_signup.py
deleted file mode 100644
index 78873749..00000000
--- a/src/leap/gui/tests/integration/fake_user_signup.py
+++ /dev/null
@@ -1,84 +0,0 @@
-"""
-simple server to test registration and
-authentication
-
-To test:
-
-curl -d login=python_test_user -d password_salt=54321\
- -d password_verifier=12341234 \
- http://localhost:8000/users.json
-
-"""
-from BaseHTTPServer import HTTPServer
-from BaseHTTPServer import BaseHTTPRequestHandler
-import cgi
-import json
-import urlparse
-
-HOST = "localhost"
-PORT = 8000
-
-LOGIN_ERROR = """{"errors":{"login":["has already been taken"]}}"""
-
-from leap.base.tests.test_providers import EXPECTED_DEFAULT_CONFIG
-
-
-class request_handler(BaseHTTPRequestHandler):
- responses = {
- '/': ['ok\n'],
- '/users.json': ['ok\n'],
- '/timeout': ['ok\n'],
- '/provider.json': ['%s\n' % json.dumps(EXPECTED_DEFAULT_CONFIG)]
- }
-
- def do_GET(self):
- path = urlparse.urlparse(self.path)
- message = '\n'.join(
- self.responses.get(
- path.path, None))
- self.send_response(200)
- self.end_headers()
- self.wfile.write(message)
-
- def do_POST(self):
- form = cgi.FieldStorage(
- fp=self.rfile,
- headers=self.headers,
- environ={'REQUEST_METHOD': 'POST',
- 'CONTENT_TYPE': self.headers['Content-Type'],
- })
- data = dict(
- (key, form[key].value) for key in form.keys())
- path = urlparse.urlparse(self.path)
- message = '\n'.join(
- self.responses.get(
- path.path, ''))
-
- login = data.get('login', None)
- #password_salt = data.get('password_salt', None)
- #password_verifier = data.get('password_verifier', None)
-
- if path.geturl() == "/timeout":
- print 'timeout'
- self.send_response(200)
- self.end_headers()
- self.wfile.write(message)
- import time
- time.sleep(10)
- return
-
- ok = True if (login == "python_test_user") else False
- if ok:
- self.send_response(200)
- self.end_headers()
- self.wfile.write(message)
-
- else:
- self.send_response(500)
- self.end_headers()
- self.wfile.write(LOGIN_ERROR)
-
-
-if __name__ == "__main__":
- server = HTTPServer((HOST, PORT), request_handler)
- server.serve_forever()
diff --git a/src/leap/gui/tests/test_firstrun_login.py b/src/leap/gui/tests/test_firstrun_login.py
deleted file mode 100644
index 6c45b8ef..00000000
--- a/src/leap/gui/tests/test_firstrun_login.py
+++ /dev/null
@@ -1,212 +0,0 @@
-import sys
-import unittest
-
-import mock
-
-from leap.testing import qunittest
-#from leap.testing import pyqt
-
-from PyQt4 import QtGui
-#from PyQt4 import QtCore
-#import PyQt4.QtCore # some weirdness with mock module
-
-from PyQt4.QtTest import QTest
-from PyQt4.QtCore import Qt
-
-from leap.gui import firstrun
-
-try:
- from collections import OrderedDict
-except ImportError:
- # We must be in 2.6
- from leap.util.dicts import OrderedDict
-
-
-class TestPage(firstrun.login.LogInPage):
- pass
-
-
-class LogInPageLogicTestCase(qunittest.TestCase):
-
- # XXX can spy on signal connections
- __name__ = "register user page logic tests"
-
- def setUp(self):
- self.app = QtGui.QApplication(sys.argv)
- QtGui.qApp = self.app
- self.page = TestPage(None)
- self.page.wizard = mock.MagicMock()
-
- def tearDown(self):
- QtGui.qApp = None
- self.app = None
- self.page = None
-
- def test__do_checks(self):
- eq = self.assertEqual
-
- self.page.userNameLineEdit.setText('testuser@domain')
- self.page.userPasswordLineEdit.setText('testpassword')
-
- # fake register process
- with mock.patch('leap.base.auth.LeapSRPRegister') as mockAuth:
- mockSignup = mock.MagicMock()
-
- reqMockup = mock.Mock()
- # XXX should inject bad json to get error
- reqMockup.content = '{"errors": null}'
- mockSignup.register_user.return_value = (True, reqMockup)
- mockAuth.return_value = mockSignup
- checks = [x for x in self.page._do_checks()]
-
- eq(len(checks), 4)
- labels = [str(x) for (x, y), z in checks]
- eq(labels, ['head_sentinel',
- 'Resolving domain name',
- 'Validating credentials',
- 'end_sentinel'])
- progress = [y for (x, y), z in checks]
- eq(progress, [0, 20, 60, 100])
-
- # normal run, ie, no exceptions
-
- checkfuns = [z for (x, y), z in checks]
- checkusername, resolvedomain, valcreds = checkfuns[:-1]
-
- self.assertTrue(checkusername())
- #self.mocknetchecker.check_name_resolution.assert_called_with(
- #'test_provider1')
-
- self.assertTrue(resolvedomain())
- #self.mockpcertchecker.is_https_working.assert_called_with(
- #"https://test_provider1", verify=True)
-
- self.assertTrue(valcreds())
-
- # XXX missing: inject failing exceptions
- # XXX TODO make it break
-
-
-class RegisterUserPageUITestCase(qunittest.TestCase):
-
- # XXX can spy on signal connections
- __name__ = "Register User Page UI tests"
-
- def setUp(self):
- self.app = QtGui.QApplication(sys.argv)
- QtGui.qApp = self.app
-
- self.pagename = "signup"
- pages = OrderedDict((
- (self.pagename, TestPage),
- ('providersetupvalidation',
- firstrun.connect.ConnectionPage)))
- self.wizard = firstrun.wizard.FirstRunWizard(None, pages_dict=pages)
- self.page = self.wizard.page(self.wizard.get_page_index(self.pagename))
-
- self.page.do_checks = mock.Mock()
-
- # wizard would do this for us
- self.page.initializePage()
-
- def tearDown(self):
- QtGui.qApp = None
- self.app = None
- self.wizard = None
-
- # XXX refactor out
- def fill_field(self, field, text):
- """
- fills a field (line edit) that is passed along
- :param field: the qLineEdit
- :param text: the text to be filled
- :type field: QLineEdit widget
- :type text: str
- """
- keyp = QTest.keyPress
- field.setFocus(True)
- for c in text:
- keyp(field, c)
- self.assertEqual(field.text(), text)
-
- def del_field(self, field):
- """
- deletes entried text in
- field line edit
- :param field: the QLineEdit
- :type field: QLineEdit widget
- """
- keyp = QTest.keyPress
- for c in range(len(field.text())):
- keyp(field, Qt.Key_Backspace)
- self.assertEqual(field.text(), "")
-
- def test_buttons_disabled_until_textentry(self):
- # it's a commit button this time
- nextbutton = self.wizard.button(QtGui.QWizard.CommitButton)
-
- self.assertFalse(nextbutton.isEnabled())
-
- f_username = self.page.userNameLineEdit
- f_password = self.page.userPasswordLineEdit
-
- self.fill_field(f_username, "testuser")
- self.fill_field(f_password, "testpassword")
-
- # commit should be enabled
- # XXX Need a workaround here
- # because the isComplete is not being evaluated...
- # (no event loop running??)
- #import ipdb;ipdb.set_trace()
- #self.assertTrue(nextbutton.isEnabled())
- self.assertTrue(self.page.isComplete())
-
- self.del_field(f_username)
- self.del_field(f_password)
-
- # after rm fields commit button
- # should be disabled again
- #self.assertFalse(nextbutton.isEnabled())
- self.assertFalse(self.page.isComplete())
-
- def test_validate_page(self):
- self.assertFalse(self.page.validatePage())
- # XXX TODO MOAR CASES...
- # add errors, False
- # change done, False
- # not done, do_checks called
- # click confirm, True
- # done and do_confirm, True
-
- def test_next_id(self):
- self.assertEqual(self.page.nextId(), 1)
-
- def test_paint_event(self):
- self.page.populateErrors = mock.Mock()
- self.page.paintEvent(None)
- self.page.populateErrors.assert_called_with()
-
- def test_validation_ready(self):
- f_username = self.page.userNameLineEdit
- f_password = self.page.userPasswordLineEdit
-
- self.fill_field(f_username, "testuser")
- self.fill_field(f_password, "testpassword")
-
- self.page.done = True
- self.page.on_checks_validation_ready()
- self.assertFalse(f_username.isEnabled())
- self.assertFalse(f_password.isEnabled())
-
- self.assertEqual(self.page.validationMsg.text(),
- "Credentials validated.")
- self.assertEqual(self.page.do_confirm_next, True)
-
- def test_regex(self):
- # XXX enter invalid username with key presses
- # check text is not updated
- pass
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/gui/tests/test_firstrun_providerselect.py b/src/leap/gui/tests/test_firstrun_providerselect.py
deleted file mode 100644
index 18d89010..00000000
--- a/src/leap/gui/tests/test_firstrun_providerselect.py
+++ /dev/null
@@ -1,203 +0,0 @@
-import sys
-import unittest
-
-import mock
-
-from leap.testing import qunittest
-#from leap.testing import pyqt
-
-from PyQt4 import QtGui
-#from PyQt4 import QtCore
-#import PyQt4.QtCore # some weirdness with mock module
-
-from PyQt4.QtTest import QTest
-from PyQt4.QtCore import Qt
-
-from leap.gui import firstrun
-
-try:
- from collections import OrderedDict
-except ImportError:
- # We must be in 2.6
- from leap.util.dicts import OrderedDict
-
-
-class TestPage(firstrun.providerselect.SelectProviderPage):
- pass
-
-
-class SelectProviderPageLogicTestCase(qunittest.TestCase):
-
- # XXX can spy on signal connections
-
- def setUp(self):
- self.app = QtGui.QApplication(sys.argv)
- QtGui.qApp = self.app
- self.page = TestPage(None)
- self.page.wizard = mock.MagicMock()
-
- mocknetchecker = mock.Mock()
- self.page.wizard().netchecker.return_value = mocknetchecker
- self.mocknetchecker = mocknetchecker
-
- mockpcertchecker = mock.Mock()
- self.page.wizard().providercertchecker.return_value = mockpcertchecker
- self.mockpcertchecker = mockpcertchecker
-
- mockeipconfchecker = mock.Mock()
- self.page.wizard().eipconfigchecker.return_value = mockeipconfchecker
- self.mockeipconfchecker = mockeipconfchecker
-
- def tearDown(self):
- QtGui.qApp = None
- self.app = None
- self.page = None
-
- def test__do_checks(self):
- eq = self.assertEqual
-
- self.page.providerNameEdit.setText('test_provider1')
-
- checks = [x for x in self.page._do_checks()]
- eq(len(checks), 5)
- labels = [str(x) for (x, y), z in checks]
- eq(labels, ['head_sentinel',
- 'Checking if it is a valid provider',
- 'Checking for a secure connection',
- 'Getting info from the provider',
- 'end_sentinel'])
- progress = [y for (x, y), z in checks]
- eq(progress, [0, 20, 40, 80, 100])
-
- # normal run, ie, no exceptions
-
- checkfuns = [z for (x, y), z in checks]
- namecheck, httpscheck, fetchinfo = checkfuns[1:-1]
-
- self.assertTrue(namecheck())
- self.mocknetchecker.check_name_resolution.assert_called_with(
- 'test_provider1')
-
- self.assertTrue(httpscheck())
- self.mockpcertchecker.is_https_working.assert_called_with(
- "https://test_provider1", verify=True)
-
- self.assertTrue(fetchinfo())
- self.mockeipconfchecker.fetch_definition.assert_called_with(
- domain="test_provider1")
-
- # XXX missing: inject failing exceptions
- # XXX TODO make it break
-
-
-class SelectProviderPageUITestCase(qunittest.TestCase):
-
- # XXX can spy on signal connections
- __name__ = "Select Provider Page UI tests"
-
- def setUp(self):
- self.app = QtGui.QApplication(sys.argv)
- QtGui.qApp = self.app
-
- self.pagename = "providerselection"
- pages = OrderedDict((
- (self.pagename, TestPage),
- ('providerinfo',
- firstrun.providerinfo.ProviderInfoPage)))
- self.wizard = firstrun.wizard.FirstRunWizard(None, pages_dict=pages)
- self.page = self.wizard.page(self.wizard.get_page_index(self.pagename))
-
- self.page.do_checks = mock.Mock()
-
- # wizard would do this for us
- self.page.initializePage()
-
- def tearDown(self):
- QtGui.qApp = None
- self.app = None
- self.wizard = None
-
- def fill_provider(self):
- """
- fills provider line edit
- """
- keyp = QTest.keyPress
- pedit = self.page.providerNameEdit
- pedit.setFocus(True)
- for c in "testprovider":
- keyp(pedit, c)
- self.assertEqual(pedit.text(), "testprovider")
-
- def del_provider(self):
- """
- deletes entried provider in
- line edit
- """
- keyp = QTest.keyPress
- pedit = self.page.providerNameEdit
- for c in range(len("testprovider")):
- keyp(pedit, Qt.Key_Backspace)
- self.assertEqual(pedit.text(), "")
-
- def test_buttons_disabled_until_textentry(self):
- nextbutton = self.wizard.button(QtGui.QWizard.NextButton)
- checkbutton = self.page.providerCheckButton
-
- self.assertFalse(nextbutton.isEnabled())
- self.assertFalse(checkbutton.isEnabled())
-
- self.fill_provider()
- # checkbutton should be enabled
- self.assertTrue(checkbutton.isEnabled())
- self.assertFalse(nextbutton.isEnabled())
-
- self.del_provider()
- # after rm provider checkbutton disabled again
- self.assertFalse(checkbutton.isEnabled())
- self.assertFalse(nextbutton.isEnabled())
-
- def test_check_button_triggers_tests(self):
- checkbutton = self.page.providerCheckButton
- self.assertFalse(checkbutton.isEnabled())
- self.assertFalse(self.page.do_checks.called)
-
- self.fill_provider()
-
- self.assertTrue(checkbutton.isEnabled())
- mclick = QTest.mouseClick
- # click!
- mclick(checkbutton, Qt.LeftButton)
- self.waitFor(seconds=0.1)
- self.assertTrue(self.page.do_checks.called)
-
- # XXX
- # can play with different side_effects for do_checks mock...
- # so we can see what happens with errors and so on
-
- def test_page_completed_after_checks(self):
- nextbutton = self.wizard.button(QtGui.QWizard.NextButton)
- self.assertFalse(nextbutton.isEnabled())
-
- self.assertFalse(self.page.isComplete())
- self.fill_provider()
- # simulate checks done
- self.page.done = True
- self.page.on_checks_validation_ready()
- self.assertTrue(self.page.isComplete())
- # cannot test for nexbutton enabled
- # cause it's the the wizard loop
- # that would do that I think
-
- def test_validate_page(self):
- self.assertTrue(self.page.validatePage())
-
- def test_next_id(self):
- self.assertEqual(self.page.nextId(), 1)
-
- def test_paint_event(self):
- self.page.populateErrors = mock.Mock()
- self.page.paintEvent(None)
- self.page.populateErrors.assert_called_with()
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/gui/tests/test_firstrun_register.py b/src/leap/gui/tests/test_firstrun_register.py
deleted file mode 100644
index 9d62f808..00000000
--- a/src/leap/gui/tests/test_firstrun_register.py
+++ /dev/null
@@ -1,244 +0,0 @@
-import sys
-import unittest
-
-import mock
-
-from leap.testing import qunittest
-#from leap.testing import pyqt
-
-from PyQt4 import QtGui
-#from PyQt4 import QtCore
-#import PyQt4.QtCore # some weirdness with mock module
-
-from PyQt4.QtTest import QTest
-from PyQt4.QtCore import Qt
-
-from leap.gui import firstrun
-
-try:
- from collections import OrderedDict
-except ImportError:
- # We must be in 2.6
- from leap.util.dicts import OrderedDict
-
-
-class TestPage(firstrun.register.RegisterUserPage):
-
- def field(self, field):
- if field == "provider_domain":
- return "testprovider"
-
-
-class RegisterUserPageLogicTestCase(qunittest.TestCase):
-
- # XXX can spy on signal connections
- __name__ = "register user page logic tests"
-
- def setUp(self):
- self.app = QtGui.QApplication(sys.argv)
- QtGui.qApp = self.app
- self.page = TestPage(None)
- self.page.wizard = mock.MagicMock()
-
- #mocknetchecker = mock.Mock()
- #self.page.wizard().netchecker.return_value = mocknetchecker
- #self.mocknetchecker = mocknetchecker
-#
- #mockpcertchecker = mock.Mock()
- #self.page.wizard().providercertchecker.return_value = mockpcertchecker
- #self.mockpcertchecker = mockpcertchecker
-#
- #mockeipconfchecker = mock.Mock()
- #self.page.wizard().eipconfigchecker.return_value = mockeipconfchecker
- #self.mockeipconfchecker = mockeipconfchecker
-
- def tearDown(self):
- QtGui.qApp = None
- self.app = None
- self.page = None
-
- def test__do_checks(self):
- eq = self.assertEqual
-
- self.page.userNameLineEdit.setText('testuser')
- self.page.userPasswordLineEdit.setText('testpassword')
- self.page.userPassword2LineEdit.setText('testpassword')
-
- # fake register process
- with mock.patch('leap.base.auth.LeapSRPRegister') as mockAuth:
- mockSignup = mock.MagicMock()
-
- reqMockup = mock.Mock()
- # XXX should inject bad json to get error
- reqMockup.content = '{"errors": null}'
- mockSignup.register_user.return_value = (True, reqMockup)
- mockAuth.return_value = mockSignup
- checks = [x for x in self.page._do_checks()]
-
- eq(len(checks), 3)
- labels = [str(x) for (x, y), z in checks]
- eq(labels, ['head_sentinel',
- 'Registering username',
- 'end_sentinel'])
- progress = [y for (x, y), z in checks]
- eq(progress, [0, 40, 100])
-
- # normal run, ie, no exceptions
-
- checkfuns = [z for (x, y), z in checks]
- passcheck, register = checkfuns[:-1]
-
- self.assertTrue(passcheck())
- #self.mocknetchecker.check_name_resolution.assert_called_with(
- #'test_provider1')
-
- self.assertTrue(register())
- #self.mockpcertchecker.is_https_working.assert_called_with(
- #"https://test_provider1", verify=True)
-
- # XXX missing: inject failing exceptions
- # XXX TODO make it break
-
-
-class RegisterUserPageUITestCase(qunittest.TestCase):
-
- # XXX can spy on signal connections
- __name__ = "Register User Page UI tests"
-
- def setUp(self):
- self.app = QtGui.QApplication(sys.argv)
- QtGui.qApp = self.app
-
- self.pagename = "signup"
- pages = OrderedDict((
- (self.pagename, TestPage),
- ('connect',
- firstrun.connect.ConnectionPage)))
- self.wizard = firstrun.wizard.FirstRunWizard(None, pages_dict=pages)
- self.page = self.wizard.page(self.wizard.get_page_index(self.pagename))
-
- self.page.do_checks = mock.Mock()
-
- # wizard would do this for us
- self.page.initializePage()
-
- def tearDown(self):
- QtGui.qApp = None
- self.app = None
- self.wizard = None
-
- def fill_field(self, field, text):
- """
- fills a field (line edit) that is passed along
- :param field: the qLineEdit
- :param text: the text to be filled
- :type field: QLineEdit widget
- :type text: str
- """
- keyp = QTest.keyPress
- field.setFocus(True)
- for c in text:
- keyp(field, c)
- self.assertEqual(field.text(), text)
-
- def del_field(self, field):
- """
- deletes entried text in
- field line edit
- :param field: the QLineEdit
- :type field: QLineEdit widget
- """
- keyp = QTest.keyPress
- for c in range(len(field.text())):
- keyp(field, Qt.Key_Backspace)
- self.assertEqual(field.text(), "")
-
- def test_buttons_disabled_until_textentry(self):
- # it's a commit button this time
- nextbutton = self.wizard.button(QtGui.QWizard.CommitButton)
-
- self.assertFalse(nextbutton.isEnabled())
-
- f_username = self.page.userNameLineEdit
- f_password = self.page.userPasswordLineEdit
- f_passwor2 = self.page.userPassword2LineEdit
-
- self.fill_field(f_username, "testuser")
- self.fill_field(f_password, "testpassword")
- self.fill_field(f_passwor2, "testpassword")
-
- # commit should be enabled
- # XXX Need a workaround here
- # because the isComplete is not being evaluated...
- # (no event loop running??)
- #import ipdb;ipdb.set_trace()
- #self.assertTrue(nextbutton.isEnabled())
- self.assertTrue(self.page.isComplete())
-
- self.del_field(f_username)
- self.del_field(f_password)
- self.del_field(f_passwor2)
-
- # after rm fields commit button
- # should be disabled again
- #self.assertFalse(nextbutton.isEnabled())
- self.assertFalse(self.page.isComplete())
-
- @unittest.skip
- def test_check_button_triggers_tests(self):
- checkbutton = self.page.providerCheckButton
- self.assertFalse(checkbutton.isEnabled())
- self.assertFalse(self.page.do_checks.called)
-
- self.fill_provider()
-
- self.assertTrue(checkbutton.isEnabled())
- mclick = QTest.mouseClick
- # click!
- mclick(checkbutton, Qt.LeftButton)
- self.waitFor(seconds=0.1)
- self.assertTrue(self.page.do_checks.called)
-
- # XXX
- # can play with different side_effects for do_checks mock...
- # so we can see what happens with errors and so on
-
- def test_validate_page(self):
- self.assertFalse(self.page.validatePage())
- # XXX TODO MOAR CASES...
- # add errors, False
- # change done, False
- # not done, do_checks called
- # click confirm, True
- # done and do_confirm, True
-
- def test_next_id(self):
- self.assertEqual(self.page.nextId(), 1)
-
- def test_paint_event(self):
- self.page.populateErrors = mock.Mock()
- self.page.paintEvent(None)
- self.page.populateErrors.assert_called_with()
-
- def test_validation_ready(self):
- f_username = self.page.userNameLineEdit
- f_password = self.page.userPasswordLineEdit
- f_passwor2 = self.page.userPassword2LineEdit
-
- self.fill_field(f_username, "testuser")
- self.fill_field(f_password, "testpassword")
- self.fill_field(f_passwor2, "testpassword")
-
- self.page.done = True
- self.page.on_checks_validation_ready()
- self.assertFalse(f_username.isEnabled())
- self.assertFalse(f_password.isEnabled())
- self.assertFalse(f_passwor2.isEnabled())
-
- self.assertEqual(self.page.validationMsg.text(),
- "Registration succeeded!")
- self.assertEqual(self.page.do_confirm_next, True)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/gui/tests/test_firstrun_wizard.py b/src/leap/gui/tests/test_firstrun_wizard.py
deleted file mode 100644
index 395604d3..00000000
--- a/src/leap/gui/tests/test_firstrun_wizard.py
+++ /dev/null
@@ -1,137 +0,0 @@
-import sys
-import unittest
-
-import mock
-
-from leap.testing import qunittest
-from leap.testing import pyqt
-
-from PyQt4 import QtGui
-#from PyQt4 import QtCore
-import PyQt4.QtCore # some weirdness with mock module
-
-from PyQt4.QtTest import QTest
-#from PyQt4.QtCore import Qt
-
-from leap.gui import firstrun
-
-
-class TestWizard(firstrun.wizard.FirstRunWizard):
- pass
-
-
-PAGES_DICT = dict((
- ('intro', firstrun.intro.IntroPage),
- ('providerselection',
- firstrun.providerselect.SelectProviderPage),
- ('login', firstrun.login.LogInPage),
- ('providerinfo', firstrun.providerinfo.ProviderInfoPage),
- ('providersetupvalidation',
- firstrun.providersetup.ProviderSetupValidationPage),
- ('signup', firstrun.register.RegisterUserPage),
- ('connect',
- firstrun.connect.ConnectionPage),
- ('lastpage', firstrun.last.LastPage)
-))
-
-
-mockQSettings = mock.MagicMock()
-mockQSettings().setValue.return_value = True
-
-#PyQt4.QtCore.QSettings = mockQSettings
-
-
-class FirstRunWizardTestCase(qunittest.TestCase):
-
- # XXX can spy on signal connections
-
- def setUp(self):
- self.app = QtGui.QApplication(sys.argv)
- QtGui.qApp = self.app
- self.wizard = TestWizard(None)
-
- def tearDown(self):
- QtGui.qApp = None
- self.app = None
- self.wizard = None
-
- def test_defaults(self):
- self.assertEqual(self.wizard.pages_dict, PAGES_DICT)
-
- @mock.patch('PyQt4.QtCore.QSettings', mockQSettings)
- def test_accept(self):
- """
- test the main accept method
- that gets called when user has gone
- thru all the wizard and click on finish button
- """
-
- self.wizard.success_cb = mock.Mock()
- self.wizard.success_cb.return_value = True
-
- # dummy values; we inject them in the field
- # mocks (where wizard gets them) and then
- # we check that they are passed to QSettings.setValue
- field_returns = ["testuser", "1234", "testprovider", True]
-
- def field_side_effects(*args):
- return field_returns.pop(0)
-
- self.wizard.field = mock.Mock(side_effect=field_side_effects)
- self.wizard.get_random_str = mock.Mock()
- RANDOMSTR = "thisisarandomstringTM"
- self.wizard.get_random_str.return_value = RANDOMSTR
-
- # mocked settings (see decorator on this method)
- mqs = PyQt4.QtCore.QSettings
-
- # go! call accept...
- self.wizard.accept()
-
- # did settings().setValue get called with the proper
- # arguments?
- call = mock.call
- calls = [call("FirstRunWizardDone", True),
- call("provider_domain", "testprovider"),
- call("remember_user_and_pass", True),
- call("username", "testuser@testprovider"),
- call("testprovider_seed", RANDOMSTR)]
- mqs().setValue.assert_has_calls(calls, any_order=True)
-
- # assert success callback is success oh boy
- self.wizard.success_cb.assert_called_with()
-
- def test_random_str(self):
- r = self.wizard.get_random_str(42)
- self.assertTrue(len(r) == 42)
-
- def test_page_index(self):
- """
- we test both the get_page_index function
- and the correct ordering of names
- """
- # remember it's implemented as an ordered dict
-
- pagenames = ('intro', 'providerselection', 'login', 'providerinfo',
- 'providersetupvalidation', 'signup', 'connect',
- 'lastpage')
- eq = self.assertEqual
- w = self.wizard
- for index, name in enumerate(pagenames):
- eq(w.get_page_index(name), index)
-
- def test_validation_errors(self):
- """
- tests getters and setters for validation errors
- """
- page = "testpage"
- eq = self.assertEqual
- w = self.wizard
- eq(w.get_validation_error(page), None)
- w.set_validation_error(page, "error")
- eq(w.get_validation_error(page), "error")
- w.clean_validation_error(page)
- eq(w.get_validation_error(page), None)
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/gui/tests/test_mainwindow_rc.py b/src/leap/gui/tests/test_mainwindow_rc.py
deleted file mode 100644
index 5004b0ac..00000000
--- a/src/leap/gui/tests/test_mainwindow_rc.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import unittest
-import hashlib
-
-try:
- import sip
- sip.setapi('QVariant', 2)
-except ValueError:
- pass
-
-from leap.gui import mainwindow_rc
-
-# I have to admit that there's something
-# perverse in testing this.
-# Even though, I still think that it _is_ a good idea
-# to put a check to avoid non-updated resources files.
-
-# so, if you came here because an updated resource
-# did break a test, what you have to do is getting
-# the md5 hash of your qt_resource_data and change it here.
-
-# annoying? yep. try making a script for that :P
-
-
-class MainWindowResourcesTest(unittest.TestCase):
-
- def test_mainwindow_resources_hash(self):
- self.assertEqual(
- hashlib.md5(mainwindow_rc.qt_resource_data).hexdigest(),
- 'ff331dc5ab50df1572b4f5c5a2691ce5')
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/gui/tests/test_progress.py b/src/leap/gui/tests/test_progress.py
deleted file mode 100644
index 1f9f9e38..00000000
--- a/src/leap/gui/tests/test_progress.py
+++ /dev/null
@@ -1,449 +0,0 @@
-from collections import namedtuple
-import sys
-import unittest
-import Queue
-
-import mock
-
-from leap.testing import qunittest
-from leap.testing import pyqt
-
-from PyQt4 import QtGui
-from PyQt4 import QtCore
-from PyQt4.QtTest import QTest
-from PyQt4.QtCore import Qt
-
-from leap.gui import progress
-
-
-class ProgressStepTestCase(unittest.TestCase):
-
- def test_step_attrs(self):
- ps = progress.ProgressStep
- step = ps('test', False, 1)
- # instance
- self.assertEqual(step.index, 1)
- self.assertEqual(step.name, "test")
- self.assertEqual(step.done, False)
- step = ps('test2', True, 2)
- self.assertEqual(step.index, 2)
- self.assertEqual(step.name, "test2")
- self.assertEqual(step.done, True)
-
- # class methods and attrs
- self.assertEqual(ps.columns(), ('name', 'done'))
- self.assertEqual(ps.NAME, 0)
- self.assertEqual(ps.DONE, 1)
-
-
-class ProgressStepContainerTestCase(unittest.TestCase):
- def setUp(self):
- self.psc = progress.ProgressStepContainer()
-
- def addSteps(self, number):
- Step = progress.ProgressStep
- for n in range(number):
- self.psc.addStep(Step("%s" % n, False, n))
-
- def test_attrs(self):
- self.assertEqual(self.psc.columns,
- ('name', 'done'))
-
- def test_add_steps(self):
- Step = progress.ProgressStep
- self.assertTrue(len(self.psc) == 0)
- self.psc.addStep(Step('one', False, 0))
- self.assertTrue(len(self.psc) == 1)
- self.psc.addStep(Step('two', False, 1))
- self.assertTrue(len(self.psc) == 2)
-
- def test_del_all_steps(self):
- self.assertTrue(len(self.psc) == 0)
- self.addSteps(5)
- self.assertTrue(len(self.psc) == 5)
- self.psc.removeAllSteps()
- self.assertTrue(len(self.psc) == 0)
-
- def test_del_step(self):
- Step = progress.ProgressStep
- self.addSteps(5)
- self.assertTrue(len(self.psc) == 5)
- self.psc.removeStep(self.psc.step(4))
- self.assertTrue(len(self.psc) == 4)
- self.psc.removeStep(self.psc.step(4))
- self.psc.removeStep(Step('none', False, 5))
- self.psc.removeStep(self.psc.step(4))
-
- def test_iter(self):
- self.addSteps(10)
- self.assertEqual(
- [x.index for x in self.psc],
- [x for x in range(10)])
-
-
-class StepsTableWidgetTestCase(unittest.TestCase):
-
- def setUp(self):
- self.app = QtGui.QApplication(sys.argv)
- QtGui.qApp = self.app
- self.stw = progress.StepsTableWidget()
-
- def tearDown(self):
- QtGui.qApp = None
- self.app = None
-
- def test_defaults(self):
- self.assertTrue(isinstance(self.stw, QtGui.QTableWidget))
- self.assertEqual(self.stw.focusPolicy(), 0)
-
-
-class TestWithStepsClass(QtGui.QWidget, progress.WithStepsMixIn):
-
- def __init__(self, parent=None):
- super(TestWithStepsClass, self).__init__(parent=parent)
- self.setupStepsProcessingQueue()
- self.statuses = []
- self.current_page = "testpage"
-
- def onStepStatusChanged(self, *args):
- """
- blank out this gui method
- that will add status lines
- """
- self.statuses.append(args)
-
-
-class WithStepsMixInTestCase(qunittest.TestCase):
-
- TIMER_WAIT = 2 * progress.WithStepsMixIn.STEPS_TIMER_MS / 1000.0
-
- # XXX can spy on signal connections
-
- def setUp(self):
- self.app = QtGui.QApplication(sys.argv)
- QtGui.qApp = self.app
- self.stepy = TestWithStepsClass()
- #self.connects = []
- #pyqt.enableSignalDebugging(
- #connectCall=lambda *args: self.connects.append(args))
- #self.assertEqual(self.connects, [])
- #self.stepy.stepscheck_timer.timeout.disconnect(
- #self.stepy.processStepsQueue)
-
- def tearDown(self):
- QtGui.qApp = None
- self.app = None
-
- def test_has_queue(self):
- s = self.stepy
- self.assertTrue(hasattr(s, 'steps_queue'))
- self.assertTrue(isinstance(s.steps_queue, Queue.Queue))
- self.assertTrue(isinstance(s.stepscheck_timer, QtCore.QTimer))
-
- def test_do_checks_delegation(self):
- s = self.stepy
-
- _do_checks = mock.Mock()
- _do_checks.return_value = (
- (("test", 0), lambda: None),
- (("test", 0), lambda: None))
- s._do_checks = _do_checks
- s.do_checks()
- self.waitFor(seconds=self.TIMER_WAIT)
- _do_checks.assert_called_with()
- self.assertEqual(len(s.statuses), 2)
-
- # test that a failed test interrupts the run
-
- s.statuses = []
- _do_checks = mock.Mock()
- _do_checks.return_value = (
- (("test", 0), lambda: None),
- (("test", 0), lambda: False),
- (("test", 0), lambda: None))
- s._do_checks = _do_checks
- s.do_checks()
- self.waitFor(seconds=self.TIMER_WAIT)
- _do_checks.assert_called_with()
- self.assertEqual(len(s.statuses), 2)
-
- def test_process_queue(self):
- s = self.stepy
- q = s.steps_queue
- s.set_failed_icon = mock.MagicMock()
- with self.assertRaises(AssertionError):
- q.put('foo')
- self.waitFor(seconds=self.TIMER_WAIT)
- s.set_failed_icon.assert_called_with()
- q.put("failed")
- self.waitFor(seconds=self.TIMER_WAIT)
- s.set_failed_icon.assert_called_with()
-
- def test_on_checks_validation_ready_called(self):
- s = self.stepy
- s.on_checks_validation_ready = mock.MagicMock()
-
- _do_checks = mock.Mock()
- _do_checks.return_value = (
- (("test", 0), lambda: None),)
- s._do_checks = _do_checks
- s.do_checks()
-
- self.waitFor(seconds=self.TIMER_WAIT)
- s.on_checks_validation_ready.assert_called_with()
-
- def test_fail(self):
- s = self.stepy
-
- s.wizard = mock.Mock()
- wizard = s.wizard.return_value
- wizard.set_validation_error.return_value = True
- s.completeChanged = mock.Mock()
- s.completeChanged.emit.return_value = True
-
- self.assertFalse(s.fail(err="foo"))
- self.waitFor(seconds=self.TIMER_WAIT)
- wizard.set_validation_error.assert_called_with('testpage', 'foo')
- s.completeChanged.emit.assert_called_with()
-
- # with no args
- s.wizard = mock.Mock()
- wizard = s.wizard.return_value
- wizard.set_validation_error.return_value = True
- s.completeChanged = mock.Mock()
- s.completeChanged.emit.return_value = True
-
- self.assertFalse(s.fail())
- self.waitFor(seconds=self.TIMER_WAIT)
- with self.assertRaises(AssertionError):
- wizard.set_validation_error.assert_called_with()
- s.completeChanged.emit.assert_called_with()
-
- def test_done(self):
- s = self.stepy
- s.done = False
-
- s.completeChanged = mock.Mock()
- s.completeChanged.emit.return_value = True
-
- self.assertFalse(s.is_done())
- s.set_done()
- self.assertTrue(s.is_done())
- s.completeChanged.emit.assert_called_with()
-
- s.completeChanged = mock.Mock()
- s.completeChanged.emit.return_value = True
- s.set_undone()
- self.assertFalse(s.is_done())
-
- def test_back_and_next(self):
- s = self.stepy
- s.wizard = mock.Mock()
- wizard = s.wizard.return_value
- wizard.back.return_value = True
- wizard.next.return_value = True
- s.go_back()
- wizard.back.assert_called_with()
- s.go_next()
- wizard.next.assert_called_with()
-
- def test_on_step_statuschanged_slot(self):
- s = self.stepy
- s.onStepStatusChanged = progress.WithStepsMixIn.onStepStatusChanged
- s.add_status_line = mock.Mock()
- s.set_checked_icon = mock.Mock()
- s.progress = mock.Mock()
- s.progress.setValue.return_value = True
- s.progress.update.return_value = True
-
- s.onStepStatusChanged(s, "end_sentinel")
- s.set_checked_icon.assert_called_with()
-
- s.onStepStatusChanged(s, "foo")
- s.add_status_line.assert_called_with("foo")
-
- s.onStepStatusChanged(s, "bar", 42)
- s.progress.setValue.assert_called_with(42)
- s.progress.update.assert_called_with()
-
- def test_steps_and_errors(self):
- s = self.stepy
- s.setupSteps()
- self.assertTrue(isinstance(s.steps, progress.ProgressStepContainer))
- self.assertEqual(s.errors, {})
- s.set_error('fooerror', 'barerror')
- self.assertEqual(s.errors, {'fooerror': 'barerror'})
- s.set_error('2', 42)
- self.assertEqual(s.errors, {'fooerror': 'barerror', '2': 42})
- fe = s.pop_first_error()
- self.assertEqual(fe, ('fooerror', 'barerror'))
- self.assertEqual(s.errors, {'2': 42})
- s.clean_errors()
- self.assertEqual(s.errors, {})
-
- def test_launch_chechs_slot(self):
- s = self.stepy
- s.do_checks = mock.Mock()
- s.launch_checks()
- s.do_checks.assert_called_with()
-
- def test_clean_wizard_errors(self):
- s = self.stepy
- s.wizard = mock.Mock()
- wizard = s.wizard.return_value
- wizard.set_validation_error.return_value = True
- s.clean_wizard_errors(pagename="foopage")
- wizard.set_validation_error.assert_called_with("foopage", None)
-
- def test_clear_table(self):
- s = self.stepy
- s.stepsTableWidget = mock.Mock()
- s.stepsTableWidget.clearContents.return_value = True
- s.clearTable()
- s.stepsTableWidget.clearContents.assert_called_with()
-
- def test_populate_steps_table(self):
- s = self.stepy
- Step = namedtuple('Step', ['name', 'done'])
-
- class Steps(object):
- columns = ("name", "done")
- _items = (Step('step1', False), Step('step2', False))
-
- def __len__(self):
- return 2
-
- def __iter__(self):
- for i in self._items:
- yield i
-
- s.steps = Steps()
-
- s.stepsTableWidget = mock.Mock()
- s.stepsTableWidget.setItem.return_value = True
- s.resizeTable = mock.Mock()
- s.update = mock.Mock()
- s.populateStepsTable()
- s.update.assert_called_with()
- s.resizeTable.assert_called_with()
-
- # assert stepsTableWidget.setItem called ...
- # we do not want to get into the actual
- # <QTableWidgetItem object at 0x92a565c>
- call_list = s.stepsTableWidget.setItem.call_args_list
- indexes = [(y, z) for y, z, xx in [x[0] for x in call_list]]
- self.assertEqual(indexes,
- [(0, 0), (0, 1), (1, 0), (1, 1)])
-
- def test_add_status_line(self):
- s = self.stepy
- s.steps = progress.ProgressStepContainer()
- s.stepsTableWidget = mock.Mock()
- s.stepsTableWidget.width.return_value = 100
- s.set_item = mock.Mock()
- s.set_item_icon = mock.Mock()
- s.add_status_line("new status")
- s.set_item_icon.assert_called_with(current=False)
-
- def test_set_item_icon(self):
- s = self.stepy
- s.steps = progress.ProgressStepContainer()
- s.stepsTableWidget = mock.Mock()
- s.stepsTableWidget.setCellWidget.return_value = True
- s.stepsTableWidget.width.return_value = 100
- #s.set_item = mock.Mock()
- #s.set_item_icon = mock.Mock()
- s.add_status_line("new status")
- s.add_status_line("new 2 status")
- s.add_status_line("new 3 status")
- call_list = s.stepsTableWidget.setCellWidget.call_args_list
- indexes = [(y, z) for y, z, xx in [x[0] for x in call_list]]
- self.assertEqual(
- indexes,
- [(0, 1), (-1, 1), (1, 1), (0, 1), (2, 1), (1, 1)])
-
-
-class TestInlineValidationPage(progress.InlineValidationPage):
- pass
-
-
-class InlineValidationPageTestCase(unittest.TestCase):
-
- def setUp(self):
- self.app = QtGui.QApplication(sys.argv)
- QtGui.qApp = self.app
- self.page = TestInlineValidationPage()
-
- def tearDown(self):
- QtGui.qApp = None
- self.app = None
-
- def test_defaults(self):
- self.assertFalse(self.page.done)
- # if setupProcessingQueue was called
- self.assertTrue(isinstance(self.page.stepscheck_timer, QtCore.QTimer))
- self.assertTrue(isinstance(self.page.steps_queue, Queue.Queue))
-
- def test_validation_frame(self):
- # test frame creation
- self.page.stepsTableWidget = progress.StepsTableWidget(
- parent=self.page)
- self.page.setupValidationFrame()
- self.assertTrue(isinstance(self.page.valFrame, QtGui.QFrame))
-
- # test show steps calls frame.show
- self.page.valFrame = mock.Mock()
- self.page.valFrame.show.return_value = True
- self.page.showStepsFrame()
- self.page.valFrame.show.assert_called_with()
-
-
-class TestValidationPage(progress.ValidationPage):
- pass
-
-
-class ValidationPageTestCase(unittest.TestCase):
-
- def setUp(self):
- self.app = QtGui.QApplication(sys.argv)
- QtGui.qApp = self.app
- self.page = TestValidationPage()
-
- def tearDown(self):
- QtGui.qApp = None
- self.app = None
-
- def test_defaults(self):
- self.assertFalse(self.page.done)
- # if setupProcessingQueue was called
- self.assertTrue(isinstance(self.page.timer, QtCore.QTimer))
- self.assertTrue(isinstance(self.page.stepscheck_timer, QtCore.QTimer))
- self.assertTrue(isinstance(self.page.steps_queue, Queue.Queue))
-
- def test_is_complete(self):
- self.assertFalse(self.page.isComplete())
- self.page.done = True
- self.assertTrue(self.page.isComplete())
- self.page.done = False
- self.assertFalse(self.page.isComplete())
-
- def test_show_hide_progress(self):
- p = self.page
- p.progress = mock.Mock()
- p.progress.show.return_code = True
- p.show_progress()
- p.progress.show.assert_called_with()
- p.progress.hide.return_code = True
- p.hide_progress()
- p.progress.hide.assert_called_with()
-
- def test_initialize_page(self):
- p = self.page
- p.timer = mock.Mock()
- p.timer.singleShot.return_code = True
- p.initializePage()
- p.timer.singleShot.assert_called_with(0, p.do_checks)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/gui/tests/test_threads.py b/src/leap/gui/tests/test_threads.py
deleted file mode 100644
index 06c19606..00000000
--- a/src/leap/gui/tests/test_threads.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import unittest
-
-import mock
-from leap.gui import threads
-
-
-class FunThreadTestCase(unittest.TestCase):
-
- def setUp(self):
- self.fun = mock.MagicMock()
- self.fun.return_value = "foo"
- self.t = threads.FunThread(fun=self.fun)
-
- def test_thread(self):
- self.t.begin()
- self.t.wait()
- self.fun.assert_called()
- del self.t
-
- def test_run(self):
- # this is called by PyQt
- self.t.run()
- del self.t
- self.fun.assert_called()
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/gui/threads.py b/src/leap/gui/threads.py
deleted file mode 100644
index 8aad8866..00000000
--- a/src/leap/gui/threads.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from PyQt4 import QtCore
-
-
-class FunThread(QtCore.QThread):
-
- def __init__(self, fun=None, parent=None):
-
- QtCore.QThread.__init__(self, parent)
- self.exiting = False
- self.fun = fun
-
- def __del__(self):
- self.exiting = True
- self.wait()
-
- def run(self):
- if self.fun:
- self.fun()
-
- def begin(self):
- self.start()
diff --git a/src/leap/gui/twisted_main.py b/src/leap/gui/twisted_main.py
new file mode 100644
index 00000000..c7add3ee
--- /dev/null
+++ b/src/leap/gui/twisted_main.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# twisted_main.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Main functions for integration of twisted reactor
+"""
+import logging
+
+from twisted.internet import error
+
+# Resist the temptation of putting the import reactor here,
+# it will raise an "reactor already imported" error.
+
+logger = logging.getLogger(__name__)
+
+
+def start(app):
+ """
+ Start the mainloop.
+
+ :param app: the main qt QApplication instance.
+ :type app: QtCore.QApplication
+ """
+ from twisted.internet import reactor
+ logger.debug('starting twisted reactor')
+
+ # this seems to be troublesome under some
+ # unidentified settings.
+ #reactor.run()
+
+ reactor.runReturn()
+ app.exec_()
+
+
+def quit(app):
+ """
+ Stop the mainloop.
+
+ :param app: the main qt QApplication instance.
+ :type app: QtCore.QApplication
+ """
+ from twisted.internet import reactor
+ logger.debug('stopping twisted reactor')
+ try:
+ reactor.stop()
+ except error.ReactorNotRunning:
+ logger.debug('reactor not running')
diff --git a/src/leap/gui/ui/loggerwindow.ui b/src/leap/gui/ui/loggerwindow.ui
new file mode 100644
index 00000000..b08428a9
--- /dev/null
+++ b/src/leap/gui/ui/loggerwindow.ui
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>LoggerWindow</class>
+ <widget class="QWidget" name="LoggerWindow">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>648</width>
+ <height>469</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Logs</string>
+ </property>
+ <property name="windowIcon">
+ <iconset resource="../../../../data/resources/mainwindow.qrc">
+ <normaloff>:/images/mask-icon.png</normaloff>:/images/mask-icon.png</iconset>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="2" column="0" colspan="2">
+ <widget class="QTextBrowser" name="txtLogHistory"/>
+ </item>
+ <item row="0" column="0" colspan="2">
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QPushButton" name="btnDebug">
+ <property name="text">
+ <string>Debug</string>
+ </property>
+ <property name="icon">
+ <iconset resource="../../../../data/resources/loggerwindow.qrc">
+ <normaloff>:/images/oxygen-icons/script-error.png</normaloff>:/images/oxygen-icons/script-error.png</iconset>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btnInfo">
+ <property name="text">
+ <string>Info</string>
+ </property>
+ <property name="icon">
+ <iconset resource="../../../../data/resources/loggerwindow.qrc">
+ <normaloff>:/images/oxygen-icons/dialog-information.png</normaloff>:/images/oxygen-icons/dialog-information.png</iconset>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btnWarning">
+ <property name="text">
+ <string>Warning</string>
+ </property>
+ <property name="icon">
+ <iconset resource="../../../../data/resources/loggerwindow.qrc">
+ <normaloff>:/images/oxygen-icons/dialog-warning.png</normaloff>:/images/oxygen-icons/dialog-warning.png</iconset>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btnError">
+ <property name="text">
+ <string>Error</string>
+ </property>
+ <property name="icon">
+ <iconset resource="../../../../data/resources/loggerwindow.qrc">
+ <normaloff>:/images/oxygen-icons/dialog-error.png</normaloff>:/images/oxygen-icons/dialog-error.png</iconset>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btnCritical">
+ <property name="text">
+ <string>Critical</string>
+ </property>
+ <property name="icon">
+ <iconset resource="../../../../data/resources/loggerwindow.qrc">
+ <normaloff>:/images/oxygen-icons/edit-bomb.png</normaloff>:/images/oxygen-icons/edit-bomb.png</iconset>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btnSave">
+ <property name="text">
+ <string>Save to file</string>
+ </property>
+ <property name="icon">
+ <iconset resource="../../../../data/resources/loggerwindow.qrc">
+ <normaloff>:/images/oxygen-icons/document-save-as.png</normaloff>:/images/oxygen-icons/document-save-as.png</iconset>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>btnDebug</tabstop>
+ <tabstop>btnInfo</tabstop>
+ <tabstop>btnWarning</tabstop>
+ <tabstop>btnError</tabstop>
+ <tabstop>btnCritical</tabstop>
+ <tabstop>btnSave</tabstop>
+ <tabstop>txtLogHistory</tabstop>
+ </tabstops>
+ <resources>
+ <include location="../../../../data/resources/loggerwindow.qrc"/>
+ <include location="../../../../data/resources/mainwindow.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/src/leap/gui/ui/login.ui b/src/leap/gui/ui/login.ui
new file mode 100644
index 00000000..42a6897a
--- /dev/null
+++ b/src/leap/gui/ui/login.ui
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>LoginWidget</class>
+ <widget class="QWidget" name="LoginWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>356</width>
+ <height>223</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="5" column="2">
+ <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>
+ <item row="1" column="1" colspan="2">
+ <widget class="QComboBox" name="cmbProviders"/>
+ </item>
+ <item row="5" column="0">
+ <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="6" column="1">
+ <widget class="QPushButton" name="btnCreateAccount">
+ <property name="text">
+ <string>Create a new account</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>&lt;b&gt;Provider:&lt;/b&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1" colspan="2">
+ <widget class="QLineEdit" name="lnPassword">
+ <property name="inputMask">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1" colspan="2">
+ <widget class="QLineEdit" name="lnUser"/>
+ </item>
+ <item row="4" column="1" colspan="2">
+ <widget class="QCheckBox" name="chkRemember">
+ <property name="text">
+ <string>Remember username and password</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>&lt;b&gt;Username:&lt;/b&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>&lt;b&gt;Password:&lt;/b&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <widget class="QPushButton" name="btnLogin">
+ <property name="text">
+ <string>Log In</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="3">
+ <widget class="QLabel" name="lblStatus">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>cmbProviders</tabstop>
+ <tabstop>lnUser</tabstop>
+ <tabstop>lnPassword</tabstop>
+ <tabstop>chkRemember</tabstop>
+ <tabstop>btnLogin</tabstop>
+ <tabstop>btnCreateAccount</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/leap/gui/ui/mainwindow.ui b/src/leap/gui/ui/mainwindow.ui
new file mode 100644
index 00000000..ecd3cbe9
--- /dev/null
+++ b/src/leap/gui/ui/mainwindow.ui
@@ -0,0 +1,315 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>429</width>
+ <height>579</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Bitmask</string>
+ </property>
+ <property name="windowIcon">
+ <iconset resource="../../../../data/resources/mainwindow.qrc">
+ <normaloff>:/images/mask-icon.png</normaloff>:/images/mask-icon.png</iconset>
+ </property>
+ <property name="inputMethodHints">
+ <set>Qt::ImhHiddenText</set>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>128</width>
+ <height>128</height>
+ </size>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0" colspan="5">
+ <layout class="QGridLayout" name="gridLayout_4">
+ <item row="2" column="0">
+ <spacer name="horizontalSpacer_8">
+ <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="1">
+ <spacer name="horizontalSpacer_7">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="lblNewUpdates">
+ <property name="text">
+ <string>There are new updates available, please restart.</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QPushButton" name="btnMore">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>More...</string>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="3">
+ <spacer name="horizontalSpacer_9">
+ <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="6" column="2">
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="10" column="0" colspan="5">
+ <widget class="QStackedWidget" name="stackedWidget">
+ <property name="currentIndex">
+ <number>1</number>
+ </property>
+ <widget class="QWidget" name="loginPage">
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="1">
+ <layout class="QHBoxLayout" name="loginLayout"/>
+ </item>
+ <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="0">
+ <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>
+ </widget>
+ <widget class="QWidget" name="page_2">
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="0" column="0">
+ <layout class="QVBoxLayout" name="statusLayout"/>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item row="7" column="2">
+ <widget class="QLabel" name="label">
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="pixmap">
+ <pixmap resource="../../../../data/resources/mainwindow.qrc">:/images/mask-launcher.png</pixmap>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="3" colspan="2">
+ <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="17" column="2">
+ <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>
+ <item row="7" column="0" colspan="2">
+ <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>
+ <item row="18" column="2">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <spacer name="horizontalSpacer_10">
+ <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>
+ <widget class="QPushButton" name="btnShowLog">
+ <property name="text">
+ <string>Show Log</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QMenuBar" name="menubar">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>429</width>
+ <height>21</height>
+ </rect>
+ </property>
+ <widget class="QMenu" name="menuSession">
+ <property name="title">
+ <string>&amp;Session</string>
+ </property>
+ <addaction name="action_log_out"/>
+ <addaction name="separator"/>
+ <addaction name="action_quit"/>
+ </widget>
+ <widget class="QMenu" name="menuHelp">
+ <property name="title">
+ <string>Help</string>
+ </property>
+ <addaction name="action_help"/>
+ <addaction name="separator"/>
+ <addaction name="action_about_leap"/>
+ </widget>
+ <addaction name="menuSession"/>
+ <addaction name="menuHelp"/>
+ </widget>
+ <widget class="QStatusBar" name="statusbar"/>
+ <action name="action_log_out">
+ <property name="text">
+ <string>Log &amp;out</string>
+ </property>
+ </action>
+ <action name="action_quit">
+ <property name="text">
+ <string>&amp;Quit</string>
+ </property>
+ </action>
+ <action name="action_about_leap">
+ <property name="text">
+ <string>About &amp;Bitmask</string>
+ </property>
+ </action>
+ <action name="action_help">
+ <property name="text">
+ <string>&amp;Help</string>
+ </property>
+ </action>
+ <action name="action_wizard">
+ <property name="text">
+ <string>&amp;Wizard</string>
+ </property>
+ </action>
+ <action name="action_show_logs">
+ <property name="text">
+ <string>Show &amp;logs</string>
+ </property>
+ </action>
+ </widget>
+ <resources>
+ <include location="../../../../data/resources/mainwindow.qrc"/>
+ <include location="../../../../data/resources/locale.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/src/leap/gui/ui/statuspanel.ui b/src/leap/gui/ui/statuspanel.ui
new file mode 100644
index 00000000..3482ac7c
--- /dev/null
+++ b/src/leap/gui/ui/statuspanel.ui
@@ -0,0 +1,289 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>StatusPanel</class>
+ <widget class="QWidget" name="StatusPanel">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>384</width>
+ <height>477</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="lblProvider">
+ <property name="styleSheet">
+ <string notr="true">font: bold;</string>
+ </property>
+ <property name="text">
+ <string>user@domain.org</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="status_rows" native="true">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="1">
+ <layout class="QHBoxLayout" name="eip_controls">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Encrypted Internet: </string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="lblEIPStatus">
+ <property name="styleSheet">
+ <string notr="true">font: bold;</string>
+ </property>
+ <property name="text">
+ <string>Off</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::AutoText</enum>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="wordWrap">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <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>
+ <item>
+ <widget class="QPushButton" name="btnEipStartStop">
+ <property name="text">
+ <string>Turn On</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0">
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Preferred</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>11</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="0" column="0" rowspan="2">
+ <widget class="QLabel" name="lblVPNStatusIcon">
+ <property name="maximumSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="pixmap">
+ <pixmap resource="../../../../data/resources/icons.qrc">:/images/light/64/network-eip-down.png</pixmap>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <layout class="QHBoxLayout" name="eip_bandwidth">
+ <property name="spacing">
+ <number>4</number>
+ </property>
+ <property name="sizeConstraint">
+ <enum>QLayout::SetDefaultConstraint</enum>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_5">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="pixmap">
+ <pixmap resource="../../../../data/resources/icons.qrc">:/images/light/16/down-arrow.png</pixmap>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btnDownload">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>100</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>120</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="cursor">
+ <cursorShape>PointingHandCursor</cursorShape>
+ </property>
+ <property name="text">
+ <string>0.0 KB/s</string>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_7">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="pixmap">
+ <pixmap resource="../../../../data/resources/icons.qrc">:/images/light/16/up-arrow.png</pixmap>
+ </property>
+ </widget>
+ </item>
+ <item alignment="Qt::AlignLeft">
+ <widget class="QPushButton" name="btnUpload">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>100</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>120</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="cursor">
+ <cursorShape>PointingHandCursor</cursorShape>
+ </property>
+ <property name="text">
+ <string>0.0 KB/s</string>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <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>
+ </layout>
+ </item>
+ <item row="3" column="0" colspan="2">
+ <widget class="QGroupBox" name="globalStatusBox">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="0">
+ <widget class="QLabel" name="lblGlobalStatus">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>...</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </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>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources>
+ <include location="../../../../data/resources/icons.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/src/leap/gui/ui/wizard.ui b/src/leap/gui/ui/wizard.ui
new file mode 100644
index 00000000..a8f66bbc
--- /dev/null
+++ b/src/leap/gui/ui/wizard.ui
@@ -0,0 +1,846 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Wizard</class>
+ <widget class="QWizard" name="Wizard">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>536</width>
+ <height>452</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Bitmask first run</string>
+ </property>
+ <property name="windowIcon">
+ <iconset resource="../../../../data/resources/mainwindow.qrc">
+ <normaloff>:/images/mask-icon.png</normaloff>:/images/mask-icon.png</iconset>
+ </property>
+ <property name="modal">
+ <bool>true</bool>
+ </property>
+ <property name="wizardStyle">
+ <enum>QWizard::ModernStyle</enum>
+ </property>
+ <property name="options">
+ <set>QWizard::IndependentPages</set>
+ </property>
+ <widget class="QWizardPage" name="introduction_page">
+ <property name="title">
+ <string>Welcome</string>
+ </property>
+ <property name="subTitle">
+ <string>This is the Bitmask first run wizard</string>
+ </property>
+ <attribute name="pageId">
+ <string notr="true">0</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="3" column="0">
+ <widget class="QRadioButton" name="rdoLogin">
+ <property name="text">
+ <string>Log In with my credentials</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Now we will guide you through some configuration that is needed before you can connect for the first time.&lt;/p&gt;&lt;p&gt;If you ever need to modify these options again, you can find the wizard in the &lt;span style=&quot; font-style:italic;&quot;&gt;'Settings'&lt;/span&gt; menu from the main window.&lt;/p&gt;&lt;p&gt;Do you want to &lt;span style=&quot; font-weight:600;&quot;&gt;sign up&lt;/span&gt; for a new account, or &lt;span style=&quot; font-weight:600;&quot;&gt;log in&lt;/span&gt; with an already existing username?&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QRadioButton" name="rdoRegister">
+ <property name="text">
+ <string>Sign up for a new account</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <spacer name="verticalSpacer_11">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="4" column="0">
+ <spacer name="verticalSpacer_12">
+ <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="WizardPage" name="select_provider_page">
+ <property name="title">
+ <string>Provider selection</string>
+ </property>
+ <property name="subTitle">
+ <string>Please enter the domain of the provider you want to use for your connection</string>
+ </property>
+ <attribute name="pageId">
+ <string notr="true">1</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="1">
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>60</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="lnProvider"/>
+ </item>
+ <item row="1" column="2">
+ <widget class="QPushButton" name="btnCheck">
+ <property name="text">
+ <string>Check</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <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>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>https://</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0" colspan="3">
+ <widget class="QGroupBox" name="grpCheckProvider">
+ <property name="title">
+ <string>Checking for a valid provider</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_5">
+ <property name="text">
+ <string>Getting provider information</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Can we stablish a secure connection?</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLabel" name="lblProviderInfo">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>24</width>
+ <height>24</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="pixmap">
+ <pixmap resource="../../../../data/resources/mainwindow.qrc">:/images/Emblem-question.png</pixmap>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="lblHTTPS">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>24</width>
+ <height>24</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="pixmap">
+ <pixmap resource="../../../../data/resources/mainwindow.qrc">:/images/Emblem-question.png</pixmap>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="lblNameResolution">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>24</width>
+ <height>24</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="pixmap">
+ <pixmap resource="../../../../data/resources/mainwindow.qrc">:/images/Emblem-question.png</pixmap>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Can we reach this provider?</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item row="2" column="1" colspan="2">
+ <widget class="QLabel" name="lblProviderSelectStatus">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWizardPage" name="provider_info_page">
+ <property name="title">
+ <string>Provider Information</string>
+ </property>
+ <property name="subTitle">
+ <string>Description of services offered by this provider</string>
+ </property>
+ <attribute name="pageId">
+ <string notr="true">2</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_4">
+ <item row="1" column="0" colspan="2">
+ <widget class="QLabel" name="lblProviderName">
+ <property name="text">
+ <string>Name</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="1">
+ <spacer name="verticalSpacer_15">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="3" column="1" colspan="2">
+ <widget class="QLabel" name="lblProviderDesc">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>200</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Desc</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLabel" name="lblServ">
+ <property name="text">
+ <string>&lt;b&gt;Services offered:&lt;/b&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <widget class="QLabel" name="lblServicesOffered">
+ <property name="text">
+ <string>services</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="2">
+ <spacer name="horizontalSpacer_6">
+ <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="5" column="0">
+ <widget class="QLabel" name="label_12">
+ <property name="text">
+ <string>&lt;b&gt;Enrollment policy:&lt;/b&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <widget class="QLabel" name="lblProviderPolicy">
+ <property name="text">
+ <string>policy</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <spacer name="verticalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_6">
+ <property name="text">
+ <string>&lt;b&gt;URL:&lt;/b&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1" colspan="2">
+ <widget class="QLabel" name="lblProviderURL">
+ <property name="text">
+ <string>URL</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_7">
+ <property name="text">
+ <string>&lt;b&gt;Description:&lt;/b&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="WizardPage" name="setup_provider_page">
+ <property name="title">
+ <string>Provider setup</string>
+ </property>
+ <property name="subTitle">
+ <string>Gathering configuration options for this provider</string>
+ </property>
+ <attribute name="pageId">
+ <string notr="true">3</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <spacer name="verticalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>60</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="lblSetupProviderExpl">
+ <property name="text">
+ <string>We are downloading some bits that we need to establish a secure connection with the provider for the first time.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_6">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_2">
+ <property name="title">
+ <string>Setting up provider</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_6">
+ <item row="2" column="1">
+ <widget class="QLabel" name="lblCheckCaFpr">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>24</width>
+ <height>24</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="pixmap">
+ <pixmap resource="../../../../data/resources/mainwindow.qrc">:/images/Emblem-question.png</pixmap>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="lblDownloadCaCert">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>24</width>
+ <height>24</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="pixmap">
+ <pixmap resource="../../../../data/resources/mainwindow.qrc">:/images/Emblem-question.png</pixmap>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_9">
+ <property name="text">
+ <string>Getting info from the Certificate Authority</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_10">
+ <property name="text">
+ <string>Do we trust this Certificate Authority?</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_11">
+ <property name="text">
+ <string>Establishing a trust relationship with this provider</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLabel" name="lblCheckApiCert">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>24</width>
+ <height>24</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="pixmap">
+ <pixmap resource="../../../../data/resources/mainwindow.qrc">:/images/Emblem-question.png</pixmap>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_8">
+ <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="WizardPage" name="register_user_page">
+ <property name="title">
+ <string>Register new user</string>
+ </property>
+ <property name="subTitle">
+ <string>Register a new user with provider</string>
+ </property>
+ <attribute name="pageId">
+ <string notr="true">4</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_7">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetDefaultConstraint</enum>
+ </property>
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_16">
+ <property name="text">
+ <string>&lt;b&gt;Password:&lt;/b&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1" colspan="2">
+ <widget class="QLineEdit" name="lblPassword"/>
+ </item>
+ <item row="4" column="1" colspan="2">
+ <widget class="QLineEdit" name="lblPassword2"/>
+ </item>
+ <item row="2" column="1" colspan="2">
+ <widget class="QLineEdit" name="lblUser"/>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLabel" name="label_17">
+ <property name="text">
+ <string>&lt;b&gt;Re-enter password:&lt;/b&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="1">
+ <widget class="QPushButton" name="btnRegister">
+ <property name="text">
+ <string>Register</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <spacer name="verticalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>60</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="7" column="1">
+ <spacer name="verticalSpacer_7">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_15">
+ <property name="text">
+ <string>&lt;b&gt;Username:&lt;/b&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1" colspan="2">
+ <widget class="QCheckBox" name="chkRemember">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Remember my username and password</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1" colspan="2">
+ <widget class="QLabel" name="lblRegisterStatus">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::AutoText</enum>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWizardPage" name="service_selection">
+ <property name="title">
+ <string>Service selection</string>
+ </property>
+ <property name="subTitle">
+ <string>Please select the services you would like to have</string>
+ </property>
+ <attribute name="pageId">
+ <string notr="true">5</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_8">
+ <item row="0" column="0">
+ <widget class="QGroupBox" name="grpServices">
+ <property name="title">
+ <string notr="true">Services by PROVIDER</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_9">
+ <item row="0" column="0">
+ <layout class="QVBoxLayout" name="serviceListLayout"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWizardPage" name="finish_page">
+ <property name="title">
+ <string>Congratulations!</string>
+ </property>
+ <property name="subTitle">
+ <string>You have successfully configured Bitmask.</string>
+ </property>
+ <attribute name="pageId">
+ <string notr="true">6</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_10">
+ <item row="1" column="0">
+ <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">
+ <spacer name="verticalSpacer_9">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="label_23">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="pixmap">
+ <pixmap resource="../../../../data/resources/mainwindow.qrc">:/images/leap-color-big.png</pixmap>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QLabel" name="label_25">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="pixmap">
+ <pixmap resource="../../../../data/resources/mainwindow.qrc">:/images/Globe.png</pixmap>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <spacer name="verticalSpacer_10">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="1" column="3">
+ <spacer name="horizontalSpacer_5">
+ <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>
+ </widget>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>WizardPage</class>
+ <extends>QWizardPage</extends>
+ <header>wizardpage.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>lblUser</tabstop>
+ <tabstop>lblPassword</tabstop>
+ <tabstop>lblPassword2</tabstop>
+ <tabstop>btnRegister</tabstop>
+ <tabstop>rdoRegister</tabstop>
+ <tabstop>rdoLogin</tabstop>
+ <tabstop>lnProvider</tabstop>
+ <tabstop>btnCheck</tabstop>
+ </tabstops>
+ <resources>
+ <include location="../../../../data/resources/mainwindow.qrc"/>
+ <include location="../../../../data/resources/locale.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/src/leap/gui/utils.py b/src/leap/gui/utils.py
deleted file mode 100644
index f91ac3ef..00000000
--- a/src/leap/gui/utils.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""
-utility functions to work with gui objects
-"""
-from PyQt4 import QtCore
-
-
-def layout_widgets(layout):
- """
- return a generator with all widgets in a layout
- """
- return (layout.itemAt(i) for i in range(layout.count()))
-
-
-DELAY_MSECS = 50
-
-
-def delay(obj, method_str=None, call_args=None):
- """
- Triggers a function or slot with a small delay.
- this is a mainly a hack to get responsiveness in the ui
- in cases in which the event loop freezes and the task
- is not heavy enough to setup a processing queue.
- """
- if callable(obj) and not method_str:
- fun = lambda: obj()
-
- if method_str:
- invoke = QtCore.QMetaObject.invokeMethod
- if call_args:
- fun = lambda: invoke(obj, method_str, call_args)
- else:
- fun = lambda: invoke(obj, method_str)
-
- QtCore.QTimer().singleShot(DELAY_MSECS, fun)
diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py
new file mode 100644
index 00000000..2b48fc81
--- /dev/null
+++ b/src/leap/gui/wizard.py
@@ -0,0 +1,626 @@
+# -*- coding: utf-8 -*-
+# wizard.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+First run wizard
+"""
+import os
+import logging
+import json
+
+from PySide import QtCore, QtGui
+from functools import partial
+from twisted.internet import threads
+
+from ui_wizard import Ui_Wizard
+from leap.config.providerconfig import ProviderConfig
+from leap.crypto.srpregister import SRPRegister
+from leap.util.privilege_policies import is_missing_policy_permissions
+from leap.util.request_helpers import get_content
+from leap.util.keyring_helpers import has_keyring
+from leap.services.eip.providerbootstrapper import ProviderBootstrapper
+from leap.services import get_supported
+
+logger = logging.getLogger(__name__)
+
+
+class Wizard(QtGui.QWizard):
+ """
+ First run wizard to register a user and setup a provider
+ """
+
+ INTRO_PAGE = 0
+ SELECT_PROVIDER_PAGE = 1
+ PRESENT_PROVIDER_PAGE = 2
+ SETUP_PROVIDER_PAGE = 3
+ REGISTER_USER_PAGE = 4
+ SERVICES_PAGE = 5
+ FINISH_PAGE = 6
+
+ WEAK_PASSWORDS = ("123456", "qweasd", "qwerty",
+ "password")
+
+ BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$"
+
+ def __init__(self, standalone=False, bypass_checks=False):
+ """
+ Constructor for the main Wizard.
+
+ :param standalone: If True, the application is running as standalone
+ and the wizard should display some messages according to this.
+ :type standalone: bool
+ :param bypass_checks: Set to true if the app should bypass
+ first round of checks for CA certificates at bootstrap
+ :type bypass_checks: bool
+ """
+ QtGui.QWizard.__init__(self)
+
+ self.standalone = standalone
+
+ self.ui = Ui_Wizard()
+ self.ui.setupUi(self)
+
+ self.setPixmap(QtGui.QWizard.LogoPixmap,
+ QtGui.QPixmap(":/images/leap-color-small.png"))
+
+ self.QUESTION_ICON = QtGui.QPixmap(":/images/Emblem-question.png")
+ self.ERROR_ICON = QtGui.QPixmap(":/images/Dialog-error.png")
+ self.OK_ICON = QtGui.QPixmap(":/images/Dialog-accept.png")
+
+ # Correspondence for services and their name to display
+ EIP_LABEL = self.tr("Encrypted Internet")
+ MX_LABEL = self.tr("Encrypted Mail")
+
+ if self._is_need_eip_password_warning():
+ EIP_LABEL += " " + self.tr(
+ "(will need admin password to start)")
+
+ self.SERVICE_DISPLAY = [
+ EIP_LABEL,
+ MX_LABEL
+ ]
+ self.SERVICE_CONFIG = [
+ "openvpn",
+ "mx"
+ ]
+
+ self._selected_services = set()
+ self._shown_services = set()
+
+ self._show_register = False
+
+ self.ui.grpCheckProvider.setVisible(False)
+ self.ui.btnCheck.clicked.connect(self._check_provider)
+ self.ui.lnProvider.returnPressed.connect(self._check_provider)
+
+ self._provider_bootstrapper = ProviderBootstrapper(bypass_checks)
+ self._provider_bootstrapper.name_resolution.connect(
+ self._name_resolution)
+ self._provider_bootstrapper.https_connection.connect(
+ self._https_connection)
+ self._provider_bootstrapper.download_provider_info.connect(
+ self._download_provider_info)
+
+ self._provider_bootstrapper.download_ca_cert.connect(
+ self._download_ca_cert)
+ self._provider_bootstrapper.check_ca_fingerprint.connect(
+ self._check_ca_fingerprint)
+ self._provider_bootstrapper.check_api_certificate.connect(
+ self._check_api_certificate)
+
+ self._domain = None
+ self._provider_config = ProviderConfig()
+
+ # We will store a reference to the defers for eventual use
+ # (eg, to cancel them) but not doing anything with them right now.
+ self._provider_select_defer = None
+ self._provider_setup_defer = None
+
+ self.currentIdChanged.connect(self._current_id_changed)
+
+ self.ui.lblPassword.setEchoMode(QtGui.QLineEdit.Password)
+ self.ui.lblPassword2.setEchoMode(QtGui.QLineEdit.Password)
+
+ self.ui.lnProvider.textChanged.connect(
+ self._enable_check)
+
+ self.ui.lblUser.returnPressed.connect(
+ self._focus_password)
+ self.ui.lblPassword.returnPressed.connect(
+ self._focus_second_password)
+ self.ui.lblPassword2.returnPressed.connect(
+ self._register)
+ self.ui.btnRegister.clicked.connect(
+ self._register)
+
+ usernameRe = QtCore.QRegExp(self.BARE_USERNAME_REGEX)
+ self.ui.lblUser.setValidator(
+ QtGui.QRegExpValidator(usernameRe, self))
+
+ self.page(self.REGISTER_USER_PAGE).setCommitPage(True)
+
+ self._username = None
+ self._password = None
+
+ self.page(self.REGISTER_USER_PAGE).setButtonText(
+ QtGui.QWizard.CommitButton, self.tr("&Next >"))
+ self.page(self.FINISH_PAGE).setButtonText(
+ QtGui.QWizard.FinishButton, self.tr("Connect"))
+
+ # XXX: Temporary removal for enrollment policy
+ # https://leap.se/code/issues/2922
+ self.ui.label_12.setVisible(False)
+ self.ui.lblProviderPolicy.setVisible(False)
+
+ def get_domain(self):
+ return self._domain
+
+ def get_username(self):
+ return self._username
+
+ def get_password(self):
+ return self._password
+
+ def get_remember(self):
+ return has_keyring() and self.ui.chkRemember.isChecked()
+
+ def get_services(self):
+ return self._selected_services
+
+ def _enable_check(self, text):
+ self.ui.btnCheck.setEnabled(len(self.ui.lnProvider.text()) != 0)
+ self._reset_provider_check()
+
+ def _focus_password(self):
+ """
+ Focuses at the password lineedit for the registration page
+ """
+ self.ui.lblPassword.setFocus()
+
+ def _focus_second_password(self):
+ """
+ Focuses at the second password lineedit for the registration page
+ """
+ self.ui.lblPassword2.setFocus()
+
+ def _basic_password_checks(self, username, password, password2):
+ """
+ Performs basic password checks to avoid really easy passwords.
+
+ :param username: username provided at the registrarion form
+ :type username: str
+ :param password: password from the registration form
+ :type password: str
+ :param password2: second password from the registration form
+ :type password: str
+
+ :return: returns True if all the checks pass, False otherwise
+ :rtype: bool
+ """
+ message = None
+
+ if message is None and password != password2:
+ message = self.tr("Passwords don't match")
+
+ if message is None and len(password) < 6:
+ message = self.tr("Password too short")
+
+ if message is None and password in self.WEAK_PASSWORDS:
+ message = self.tr("Password too easy")
+
+ if message is None and username == password:
+ message = self.tr("Password equal to username")
+
+ if message is not None:
+ self._set_register_status(message, error=True)
+ self._focus_password()
+ return False
+
+ return True
+
+ def _register(self):
+ """
+ Performs the registration based on the values provided in the form
+ """
+ self.ui.btnRegister.setEnabled(False)
+
+ username = self.ui.lblUser.text()
+ password = self.ui.lblPassword.text()
+ password2 = self.ui.lblPassword2.text()
+
+ if self._basic_password_checks(username, password, password2):
+ register = SRPRegister(provider_config=self._provider_config)
+ register.registration_finished.connect(
+ self._registration_finished)
+
+ threads.deferToThread(
+ partial(register.register_user,
+ username.encode("utf8"),
+ password.encode("utf8")))
+
+ self._username = username
+ self._password = password
+ self._set_register_status(self.tr("Starting registration..."))
+ else:
+ self.ui.btnRegister.setEnabled(True)
+
+ def _set_registration_fields_visibility(self, visible):
+ """
+ This method hides the username and password labels and inputboxes.
+
+ :param visible: sets the visibility of the widgets
+ True: widgets are visible or False: are not
+ :type visible: bool
+ """
+ # username and password inputs
+ self.ui.lblUser.setVisible(visible)
+ self.ui.lblPassword.setVisible(visible)
+ self.ui.lblPassword2.setVisible(visible)
+
+ # username and password labels
+ self.ui.label_15.setVisible(visible)
+ self.ui.label_16.setVisible(visible)
+ self.ui.label_17.setVisible(visible)
+
+ # register button
+ self.ui.btnRegister.setVisible(visible)
+
+ def _registration_finished(self, ok, req):
+ if ok:
+ user_domain = self._username + "@" + self._domain
+ message = "<font color='green'><h3>"
+ message += self.tr("User %s successfully registered.") % (
+ user_domain, )
+ message += "</h3></font>"
+ self._set_register_status(message)
+
+ self.ui.lblPassword2.clearFocus()
+ self._set_registration_fields_visibility(False)
+
+ # Allow the user to remember his password
+ if has_keyring():
+ self.ui.chkRemember.setVisible(True)
+ self.ui.chkRemember.setEnabled(True)
+
+ self.page(self.REGISTER_USER_PAGE).set_completed()
+ self.button(QtGui.QWizard.BackButton).setEnabled(False)
+ else:
+ old_username = self._username
+ self._username = None
+ self._password = None
+ error_msg = self.tr("Unknown error")
+ try:
+ content, _ = get_content(req)
+ json_content = json.loads(content)
+ error_msg = json_content.get("errors").get("login")[0]
+ if not error_msg.istitle():
+ error_msg = "%s %s" % (old_username, error_msg)
+ except Exception as e:
+ logger.error("Unknown error: %r" % (e,))
+
+ self._set_register_status(error_msg, error=True)
+ self.ui.btnRegister.setEnabled(True)
+
+ def _set_register_status(self, status, error=False):
+ """
+ Sets the status label in the registration page to status
+
+ :param status: status message to display, can be HTML
+ :type status: str
+ """
+ if error:
+ status = "<font color='red'><b>%s</b></font>" % (status,)
+ self.ui.lblRegisterStatus.setText(status)
+
+ def _reset_provider_check(self):
+ """
+ Resets the UI for checking a provider. Also resets the domain
+ in this object.
+ """
+ self.ui.lblNameResolution.setPixmap(None)
+ self.ui.lblHTTPS.setPixmap(None)
+ self.ui.lblProviderInfo.setPixmap(None)
+ self.ui.lblProviderSelectStatus.setText("")
+ self._domain = None
+ self.button(QtGui.QWizard.NextButton).setEnabled(False)
+ self.page(self.SELECT_PROVIDER_PAGE).set_completed(False)
+
+ def _reset_provider_setup(self):
+ """
+ Resets the UI for setting up a provider.
+ """
+ self.ui.lblDownloadCaCert.setPixmap(None)
+ self.ui.lblCheckCaFpr.setPixmap(None)
+ self.ui.lblCheckApiCert.setPixmap(None)
+
+ def _check_provider(self):
+ """
+ SLOT
+ TRIGGERS:
+ self.ui.btnCheck.clicked
+ self.ui.lnProvider.returnPressed
+
+ Starts the checks for a given provider
+ """
+ if len(self.ui.lnProvider.text()) == 0:
+ return
+
+ self.ui.grpCheckProvider.setVisible(True)
+ self.ui.btnCheck.setEnabled(False)
+ self.ui.lnProvider.setEnabled(False)
+ self.button(QtGui.QWizard.BackButton).clearFocus()
+ self._domain = self.ui.lnProvider.text()
+
+ self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON)
+ self._provider_select_defer = self._provider_bootstrapper.\
+ run_provider_select_checks(self._domain)
+
+ def _complete_task(self, data, label, complete=False, complete_page=-1):
+ """
+ Checks a task and completes a page if specified
+
+ :param data: data as it comes from the bootstrapper thread for
+ a specific check
+ :type data: dict
+ :param label: label that displays the status icon for a
+ specific check that corresponds to the data
+ :type label: QtGui.QLabel
+ :param complete: if True, it completes the page specified,
+ which must be of type WizardPage
+ :type complete: bool
+ :param complete_page: page id to complete
+ :type complete_page: int
+ """
+ passed = data[self._provider_bootstrapper.PASSED_KEY]
+ error = data[self._provider_bootstrapper.ERROR_KEY]
+ if passed:
+ label.setPixmap(self.OK_ICON)
+ if complete:
+ self.page(complete_page).set_completed()
+ self.button(QtGui.QWizard.NextButton).setFocus()
+ else:
+ label.setPixmap(self.ERROR_ICON)
+ logger.error(error)
+
+ def _name_resolution(self, data):
+ """
+ SLOT
+ TRIGGER: self._provider_bootstrapper.name_resolution
+
+ Sets the status for the name resolution check
+ """
+ self._complete_task(data, self.ui.lblNameResolution)
+ status = ""
+ passed = data[self._provider_bootstrapper.PASSED_KEY]
+ if not passed:
+ status = self.tr("<font color='red'><b>Non-existent "
+ "provider</b></font>")
+ else:
+ self.ui.lblHTTPS.setPixmap(self.QUESTION_ICON)
+ self.ui.lblProviderSelectStatus.setText(status)
+ self.ui.btnCheck.setEnabled(not passed)
+ self.ui.lnProvider.setEnabled(not passed)
+
+ def _https_connection(self, data):
+ """
+ SLOT
+ TRIGGER: self._provider_bootstrapper.https_connection
+
+ Sets the status for the https connection check
+ """
+ self._complete_task(data, self.ui.lblHTTPS)
+ status = ""
+ passed = data[self._provider_bootstrapper.PASSED_KEY]
+ if not passed:
+ status = self.tr("<font color='red'><b>%s</b></font>") \
+ % (data[self._provider_bootstrapper.ERROR_KEY])
+ self.ui.lblProviderSelectStatus.setText(status)
+ else:
+ self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON)
+ self.ui.btnCheck.setEnabled(not passed)
+ self.ui.lnProvider.setEnabled(not passed)
+
+ def _download_provider_info(self, data):
+ """
+ SLOT
+ TRIGGER: self._provider_bootstrapper.download_provider_info
+
+ Sets the status for the provider information download
+ check. Since this check is the last of this set, it also
+ completes the page if passed
+ """
+ if self._provider_config.load(os.path.join("leap",
+ "providers",
+ self._domain,
+ "provider.json")):
+ self._complete_task(data, self.ui.lblProviderInfo,
+ True, self.SELECT_PROVIDER_PAGE)
+ else:
+ new_data = {
+ self._provider_bootstrapper.PASSED_KEY: False,
+ self._provider_bootstrapper.ERROR_KEY:
+ self.tr("Unable to load provider configuration")
+ }
+ self._complete_task(new_data, self.ui.lblProviderInfo)
+
+ status = ""
+ if not data[self._provider_bootstrapper.PASSED_KEY]:
+ status = self.tr("<font color='red'><b>Not a valid provider"
+ "</b></font>")
+ self.ui.lblProviderSelectStatus.setText(status)
+ self.ui.btnCheck.setEnabled(True)
+ self.ui.lnProvider.setEnabled(True)
+
+ def _download_ca_cert(self, data):
+ """
+ SLOT
+ TRIGGER: self._provider_bootstrapper.download_ca_cert
+
+ Sets the status for the download of the CA certificate check
+ """
+ self._complete_task(data, self.ui.lblDownloadCaCert)
+ passed = data[self._provider_bootstrapper.PASSED_KEY]
+ if passed:
+ self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON)
+
+ def _check_ca_fingerprint(self, data):
+ """
+ SLOT
+ TRIGGER: self._provider_bootstrapper.check_ca_fingerprint
+
+ Sets the status for the CA fingerprint check
+ """
+ self._complete_task(data, self.ui.lblCheckCaFpr)
+ passed = data[self._provider_bootstrapper.PASSED_KEY]
+ if passed:
+ self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON)
+
+ def _check_api_certificate(self, data):
+ """
+ SLOT
+ TRIGGER: self._provider_bootstrapper.check_api_certificate
+
+ Sets the status for the API certificate check. Also finishes
+ the provider bootstrapper thread since it's not needed anymore
+ from this point on, unless the whole check chain is restarted
+ """
+ self._complete_task(data, self.ui.lblCheckApiCert,
+ True, self.SETUP_PROVIDER_PAGE)
+
+ def _service_selection_changed(self, service, state):
+ """
+ SLOT
+ TRIGGER: service_checkbox.stateChanged
+ Adds the service to the state if the state is checked, removes
+ it otherwise
+
+ :param service: service to handle
+ :type service: str
+ :param state: state of the checkbox
+ :type state: int
+ """
+ if state == QtCore.Qt.Checked:
+ self._selected_services = \
+ self._selected_services.union(set([service]))
+ else:
+ self._selected_services = \
+ self._selected_services.difference(set([service]))
+
+ def _populate_services(self):
+ """
+ Loads the services that the provider provides into the UI for
+ the user to enable or disable.
+ """
+ self.ui.grpServices.setTitle(
+ self.tr("Services by %s") %
+ (self._provider_config.get_name(),))
+
+ services = get_supported(
+ self._provider_config.get_services())
+
+ for service in services:
+ try:
+ if service not in self._shown_services:
+ checkbox = QtGui.QCheckBox(self)
+ service_index = self.SERVICE_CONFIG.index(service)
+ checkbox.setText(self.SERVICE_DISPLAY[service_index])
+ self.ui.serviceListLayout.addWidget(checkbox)
+ checkbox.stateChanged.connect(
+ partial(self._service_selection_changed, service))
+ checkbox.setChecked(True)
+ self._shown_services.add(service)
+ except ValueError:
+ logger.error(
+ self.tr("Something went wrong while trying to "
+ "load service %s" % (service,)))
+
+ def _current_id_changed(self, pageId):
+ """
+ SLOT
+ TRIGGER: self.currentIdChanged
+
+ Prepares the pages when they appear
+ """
+ if pageId == self.SELECT_PROVIDER_PAGE:
+ self._reset_provider_check()
+ self._enable_check("")
+
+ if pageId == self.SETUP_PROVIDER_PAGE:
+ self._reset_provider_setup()
+ self.page(pageId).setSubTitle(self.tr("Gathering configuration "
+ "options for %s") %
+ (self._provider_config
+ .get_name(),))
+ self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON)
+ self._provider_setup_defer = self._provider_bootstrapper.\
+ run_provider_setup_checks(self._provider_config)
+
+ if pageId == self.PRESENT_PROVIDER_PAGE:
+ self.page(pageId).setSubTitle(self.tr("Description of services "
+ "offered by %s") %
+ (self._provider_config
+ .get_name(),))
+
+ lang = QtCore.QLocale.system().name()
+ self.ui.lblProviderName.setText(
+ "<b>%s</b>" %
+ (self._provider_config.get_name(lang=lang),))
+ self.ui.lblProviderURL.setText(
+ "https://%s" % (self._provider_config.get_domain(),))
+ self.ui.lblProviderDesc.setText(
+ "<i>%s</i>" %
+ (self._provider_config.get_description(lang=lang),))
+
+ self.ui.lblServicesOffered.setText(self._provider_config
+ .get_services_string())
+ self.ui.lblProviderPolicy.setText(self._provider_config
+ .get_enrollment_policy())
+
+ if pageId == self.REGISTER_USER_PAGE:
+ self.page(pageId).setSubTitle(self.tr("Register a new user with "
+ "%s") %
+ (self._provider_config
+ .get_name(),))
+ self.ui.chkRemember.setVisible(False)
+
+ if pageId == self.SERVICES_PAGE:
+ self._populate_services()
+
+ def _is_need_eip_password_warning(self):
+ """
+ Returns True if we need to add a warning about eip needing
+ administrative permissions to start. That can be either
+ because we are running in standalone mode, or because we could
+ not find the needed privilege escalation mechanisms being operative.
+ """
+ return self.standalone or is_missing_policy_permissions()
+
+ def nextId(self):
+ """
+ Sets the next page id for the wizard based on wether the user
+ wants to register a new identity or uses an existing one
+ """
+ if self.currentPage() == self.page(self.INTRO_PAGE):
+ self._show_register = self.ui.rdoRegister.isChecked()
+
+ if self.currentPage() == self.page(self.SETUP_PROVIDER_PAGE):
+ if self._show_register:
+ return self.REGISTER_USER_PAGE
+ else:
+ return self.SERVICES_PAGE
+
+ return QtGui.QWizard.nextId(self)
diff --git a/src/leap/gui/wizardpage.py b/src/leap/gui/wizardpage.py
new file mode 100644
index 00000000..b2a00028
--- /dev/null
+++ b/src/leap/gui/wizardpage.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# wizardpage.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from PySide import QtGui
+
+
+class WizardPage(QtGui.QWizardPage):
+ """
+ Simple wizard page helper
+ """
+
+ def __init__(self):
+ QtGui.QWizardPage.__init__(self)
+ self._completed = False
+
+ def set_completed(self, val=True):
+ self._completed = val
+ if val:
+ self.completeChanged.emit()
+
+ def isComplete(self):
+ return self._completed
+
+ def cleanupPage(self):
+ self._completed = False
+ QtGui.QWizardPage.cleanupPage(self)
diff --git a/src/leap/platform_init/__init__.py b/src/leap/platform_init/__init__.py
new file mode 100644
index 00000000..2a262a30
--- /dev/null
+++ b/src/leap/platform_init/__init__.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# __init__.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+System constants
+"""
+import platform
+
+_system = platform.system()
+
+IS_WIN = True if _system == "Windows" else False
+IS_MAC = True if _system == "Darwin" else False
+IS_LINUX = True if _system == "Linux" else False
+IS_UNIX = IS_MAC or IS_LINUX
diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py
new file mode 100644
index 00000000..46488250
--- /dev/null
+++ b/src/leap/platform_init/initializers.py
@@ -0,0 +1,409 @@
+# -*- coding: utf-8 -*-
+# initializers.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Platform dependant initializing code
+"""
+
+import logging
+import os
+import platform
+import stat
+import subprocess
+import tempfile
+
+from PySide import QtGui
+
+from leap.config.leapsettings import LeapSettings
+from leap.services.eip import vpnlaunchers
+from leap.util import first
+from leap.util import privilege_policies
+
+
+logger = logging.getLogger(__name__)
+
+# NOTE we could use a deferToThread here, but should
+# be aware of this bug: http://www.themacaque.com/?p=1067
+
+__all__ = ["init_platform"]
+
+_system = platform.system()
+
+
+def init_platform():
+ """
+ Returns the right initializer for the platform we are running in, or
+ None if no proper initializer is found
+ """
+ initializer = None
+ try:
+ initializer = globals()[_system + "Initializer"]
+ except:
+ pass
+ if initializer:
+ logger.debug("Running initializer for %s" % (platform.system(),))
+ initializer()
+ else:
+ logger.debug("Initializer not found for %s" % (platform.system(),))
+
+
+#
+# common utils
+#
+
+NOTFOUND_MSG = ("Tried to install %s, but %s "
+ "not found inside this bundle.")
+BADEXEC_MSG = ("Tried to install %s, but %s "
+ "failed to %s.")
+
+UPDOWN_NOTFOUND_MSG = NOTFOUND_MSG % (
+ "updown scripts", "those were")
+UPDOWN_BADEXEC_MSG = BADEXEC_MSG % (
+ "updown scripts", "they", "be copied")
+
+
+def get_missing_updown_dialog():
+ """
+ Creates a dialog for notifying of missing updown scripts.
+ Returns that dialog.
+
+ :rtype: QtGui.QMessageBox instance
+ """
+ WE_NEED_POWERS = ("To better protect your privacy, "
+ "Bitmask needs administrative privileges "
+ "to install helper files. "
+ "Do you want to proceed?")
+ msg = QtGui.QMessageBox()
+ msg.setWindowTitle(msg.tr("Missing up/down scripts"))
+ msg.setText(msg.tr(WE_NEED_POWERS))
+ # but maybe the user really deserve to know more
+ #msg.setInformativeText(msg.tr(BECAUSE))
+ msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)
+ msg.addButton("No, don't ask again", QtGui.QMessageBox.RejectRole)
+ msg.setDefaultButton(QtGui.QMessageBox.Yes)
+ return msg
+
+
+def check_missing():
+ """
+ Checks for the need of installing missing scripts, and
+ raises a dialog to ask user for permission to do it.
+ """
+ config = LeapSettings()
+ alert_missing = config.get_alert_missing_scripts()
+
+ launcher = vpnlaunchers.get_platform_launcher()
+ missing_scripts = launcher.missing_updown_scripts
+ missing_other = launcher.missing_other_files
+
+ if alert_missing and (missing_scripts() or missing_other()):
+ msg = get_missing_updown_dialog()
+ ret = msg.exec_()
+
+ if ret == QtGui.QMessageBox.Yes:
+ install_missing_fun = globals().get(
+ "_%s_install_missing_scripts" % (_system.lower(),),
+ None)
+ if not install_missing_fun:
+ logger.warning(
+ "Installer not found for platform %s." % (_system,))
+ return
+
+ # XXX maybe move constants to fun
+ ok = install_missing_fun(UPDOWN_BADEXEC_MSG, UPDOWN_NOTFOUND_MSG)
+ if not ok:
+ msg = QtGui.QMessageBox()
+ msg.setWindowTitle(msg.tr("Problem installing files"))
+ msg.setText(msg.tr('Some of the files could not be copied.'))
+ msg.setIcon(QtGui.QMessageBox.Warning)
+ msg.exec_()
+
+ elif ret == QtGui.QMessageBox.No:
+ logger.debug("Not installing missing scripts, "
+ "user decided to ignore our warning.")
+
+ elif ret == QtGui.QMessageBox.Rejected:
+ logger.debug(
+ "Setting alert_missing_scripts to False, we will not "
+ "ask again")
+ config.set_alert_missing_scripts(False)
+#
+# windows initializers
+#
+
+
+def _windows_has_tap_device():
+ """
+ Loops over the windows registry trying to find if the tap0901 tap driver
+ has been installed on this machine.
+ """
+ import _winreg as reg
+
+ adapter_key = 'SYSTEM\CurrentControlSet\Control\Class' \
+ '\{4D36E972-E325-11CE-BFC1-08002BE10318}'
+ with reg.OpenKey(reg.HKEY_LOCAL_MACHINE, adapter_key) as adapters:
+ try:
+ for i in xrange(10000):
+ key_name = reg.EnumKey(adapters, i)
+ with reg.OpenKey(adapters, key_name) as adapter:
+ try:
+ component_id = reg.QueryValueEx(adapter,
+ 'ComponentId')[0]
+ if component_id.startswith("tap0901"):
+ return True
+ except WindowsError:
+ pass
+ except WindowsError:
+ pass
+ return False
+
+
+def WindowsInitializer():
+ """
+ Raises a dialog in case that the windows tap driver has not been found
+ in the registry, asking the user for permission to install the driver
+ """
+ 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.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")
+
+#
+# Darwin initializer functions
+#
+
+
+def _darwin_has_tun_kext():
+ """
+ Returns True only if we found a directory under the system kext folder
+ containing a kext named tun.kext, AND we found a startup item named 'tun'
+ """
+ # XXX we should be smarter here and use kextstats output.
+
+ has_kext = os.path.isdir("/System/Library/Extensions/tun.kext")
+ has_startup = os.path.isdir("/System/Library/StartupItems/tun")
+ has_tun_and_startup = has_kext and has_startup
+ logger.debug(
+ 'platform initializer check: has tun_and_startup = %s' %
+ (has_tun_and_startup,))
+ return has_tun_and_startup
+
+
+def _darwin_install_missing_scripts(badexec, notfound):
+ """
+ Tries to install the missing up/down scripts.
+
+ :param badexec: error for notifying execution error during command.
+ :type badexec: str
+ :param notfound: error for notifying missing path.
+ :type notfound: str
+ :returns: True if the files could be copied successfully.
+ :rtype: bool
+ """
+ # We expect to execute this from some way of bundle, since
+ # the up/down scripts should be put in place by the installer.
+ success = False
+ installer_path = os.path.join(
+ os.getcwd(),
+ "..",
+ "Resources",
+ "openvpn")
+ launcher = vpnlaunchers.DarwinVPNLauncher
+
+ if os.path.isdir(installer_path):
+ fd, tempscript = tempfile.mkstemp(prefix="leap_installer-")
+ try:
+ scriptlines = launcher.cmd_for_missing_scripts(installer_path)
+ with os.fdopen(fd, 'w') as f:
+ f.write(scriptlines)
+ st = os.stat(tempscript)
+ os.chmod(tempscript, st.st_mode | stat.S_IEXEC | stat.S_IXUSR |
+ stat.S_IXGRP | stat.S_IXOTH)
+
+ cmd, args = launcher().get_cocoasudo_installmissing_cmd()
+ args.append(tempscript)
+ cmdline = " ".join([cmd] + args)
+ ret = subprocess.call(
+ cmdline, stdout=subprocess.PIPE,
+ shell=True)
+ success = ret == 0
+ if not success:
+ logger.error("Install missing scripts failed.")
+ except Exception as exc:
+ logger.error(badexec)
+ logger.error("Error was: %r" % (exc,))
+ finally:
+ try:
+ os.remove(tempscript)
+ except OSError as exc:
+ logger.error("%r" % (exc,))
+ else:
+ logger.error(notfound)
+ logger.debug('path searched: %s' % (installer_path,))
+
+ return success
+
+
+def DarwinInitializer():
+ """
+ Raises 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
+ """
+ # XXX split this function into several
+
+ TUNTAP_NOTFOUND_MSG = NOTFOUND_MSG % (
+ "tuntaposx kext", "the installer")
+ TUNTAP_BADEXEC_MSG = BADEXEC_MSG % (
+ "tuntaposx kext", "the installer", "be launched")
+
+ # TODO DRY this with other cases, and
+ # factor out to _should_install() function.
+ # Leave the dialog as a more generic thing.
+
+ if not _darwin_has_tun_kext():
+ msg = QtGui.QMessageBox()
+ msg.setWindowTitle(msg.tr("TUN Driver"))
+ msg.setText(msg.tr("Bitmask needs to install the necessary drivers "
+ "for Encrypted Internet to work. Would you like to "
+ "proceed?"))
+ msg.setInformativeText(msg.tr("Encrypted Internet uses VPN, which "
+ "needs a kernel extension for a TUN "
+ "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:
+ installer_path = os.path.abspath(
+ os.path.join(
+ os.getcwd(),
+ "..",
+ "Resources",
+ "tuntap-installer.app"))
+ if os.path.isdir(installer_path):
+ cmd = ["open '%s'" % (installer_path,)]
+ try:
+ ret = subprocess.call(
+ cmd, stdout=subprocess.PIPE,
+ shell=True)
+ except:
+ logger.error(TUNTAP_BADEXEC_MSG)
+ else:
+ logger.error(TUNTAP_NOTFOUND_MSG)
+
+ # Second check, for missing scripts.
+ check_missing()
+
+
+#
+# Linux initializers
+#
+def _linux_install_missing_scripts(badexec, notfound):
+ """
+ Tries to install the missing up/down scripts.
+
+ :param badexec: error for notifying execution error during command.
+ :type badexec: str
+ :param notfound: error for notifying missing path.
+ :type notfound: str
+ :returns: True if the files could be copied successfully.
+ :rtype: bool
+ """
+ success = False
+ installer_path = os.path.join(os.getcwd(), "apps", "eip", "files")
+ launcher = vpnlaunchers.LinuxVPNLauncher
+
+ # XXX refactor with darwin, same block.
+
+ if os.path.isdir(installer_path):
+ fd, tempscript = tempfile.mkstemp(prefix="leap_installer-")
+ polfd, pol_tempfile = tempfile.mkstemp(prefix="leap_installer-")
+ try:
+ path = launcher.OPENVPN_BIN_PATH
+ policy_contents = privilege_policies.get_policy_contents(path)
+
+ with os.fdopen(polfd, 'w') as f:
+ f.write(policy_contents)
+
+ pkexec = first(launcher.maybe_pkexec())
+ scriptlines = launcher.cmd_for_missing_scripts(installer_path,
+ pol_tempfile)
+ with os.fdopen(fd, 'w') as f:
+ f.write(scriptlines)
+
+ st = os.stat(tempscript)
+ os.chmod(tempscript, st.st_mode | stat.S_IEXEC | stat.S_IXUSR |
+ stat.S_IXGRP | stat.S_IXOTH)
+ cmdline = ["%s %s" % (pkexec, tempscript)]
+ ret = subprocess.call(
+ cmdline, stdout=subprocess.PIPE,
+ shell=True)
+ success = ret == 0
+ if not success:
+ logger.error("Install missing scripts failed.")
+ except Exception as exc:
+ logger.error(badexec)
+ logger.error("Error was: %r" % (exc,))
+ finally:
+ try:
+ os.remove(tempscript)
+ except OSError as exc:
+ logger.error("%r" % (exc,))
+ else:
+ logger.error(notfound)
+ logger.debug('path searched: %s' % (installer_path,))
+
+ return success
+
+
+def LinuxInitializer():
+ """
+ Raises a dialog in case that either updown scripts or policykit file
+ are missing or they have incorrect permissions.
+ """
+ check_missing()
diff --git a/src/leap/platform_init/locks.py b/src/leap/platform_init/locks.py
new file mode 100644
index 00000000..39b18648
--- /dev/null
+++ b/src/leap/platform_init/locks.py
@@ -0,0 +1,405 @@
+# -*- coding: utf-8 -*-
+# locks.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Utilities for handling multi-platform file locking mechanisms
+"""
+import logging
+import errno
+import os
+import platform
+
+from leap.common.events import signal as signal_event
+from leap.common.events import events_pb2 as proto
+from leap import platform_init
+
+if platform_init.IS_UNIX:
+ from fcntl import flock, LOCK_EX, LOCK_NB
+else: # WINDOWS
+ import datetime
+ import glob
+ import shutil
+ import time
+
+ from tempfile import gettempdir
+
+ from leap.util import get_modification_ts, update_modification_ts
+
+logger = logging.getLogger(__name__)
+
+if platform_init.IS_UNIX:
+
+ class UnixLock(object):
+ """
+ Uses flock to get an exclusive lock over a file.
+ See man 2 flock
+ """
+
+ def __init__(self, path):
+ """
+ iniializes t he UnixLock with the path of the
+ desired lockfile
+ """
+
+ self._fd = None
+ self.path = path
+
+ def get_lock(self):
+ """
+ Tries to get a lock, and writes the running pid there if successful
+ """
+ gotit, pid = self._get_lock_and_pid()
+ return gotit
+
+ def get_pid(self):
+ """
+ Returns the pid of the locking process
+ """
+ gotit, pid = self._get_lock_and_pid()
+ return pid
+
+ def _get_lock(self):
+ """
+ Tries to get a lock, returning True if successful
+
+ :rtype: bool
+ """
+ self._fd = os.open(self.path, os.O_CREAT | os.O_RDWR)
+
+ try:
+ flock(self._fd, LOCK_EX | LOCK_NB)
+ except IOError as exc:
+ # could not get the lock
+ #import ipdb; ipdb.set_trace()
+
+ if exc.args[0] in (errno.EDEADLK, errno.EAGAIN):
+ # errno 11 or 35
+ # Resource temporarily unavailable
+ return False
+ else:
+ raise
+ return True
+
+ @property
+ def locked_by_us(self):
+ """
+ Returns True if the pid in the pidfile
+ is ours.
+
+ :rtype: bool
+ """
+ gotit, pid = self._get_lock_and_pid()
+ return pid == os.getpid()
+
+ def _get_lock_and_pid(self):
+ """
+ Tries to get a lock over the file.
+ Returns (locked, pid) tuple.
+
+ :rtype: tuple
+ """
+
+ if self._get_lock():
+ self._write_to_pidfile()
+ return True, None
+
+ return False, self._read_from_pidfile()
+
+ def _read_from_pidfile(self):
+ """
+ Tries to read pid from the pidfile,
+ returns False if no content found.
+ """
+
+ pidfile = os.read(
+ self._fd, 16)
+ if not pidfile:
+ return False
+
+ try:
+ return int(pidfile.strip())
+ except Exception as exc:
+ exc.args += (pidfile, self.lock_file)
+ raise
+
+ def _write_to_pidfile(self):
+ """
+ Writes the pid of the running process
+ to the pidfile
+ """
+ fd = self._fd
+ os.ftruncate(fd, 0)
+ os.write(fd, '%d\n' % os.getpid())
+ os.fsync(fd)
+
+
+if platform_init.IS_WIN:
+
+ # Time to wait (in secs) before assuming a raise window signal has not been
+ # ack-ed.
+
+ RAISE_WINDOW_TIMEOUT = 2
+
+ # How many steps to do while checking lockfile ts update.
+
+ RAISE_WINDOW_WAIT_STEPS = 10
+
+ def _release_lock(name):
+ """
+ Tries to remove a folder path.
+
+ :param name: folder lock to remove
+ :type name: str
+ """
+ try:
+ shutil.rmtree(name)
+ return True
+ except WindowsError as exc:
+ if exc.errno in (errno.EPIPE, errno.ENOENT,
+ errno.ESRCH, errno.EACCES):
+ logger.warning(
+ 'exception while trying to remove the lockfile dir')
+ logger.warning('errno %s: %s' % (exc.errno, exc.args[1]))
+ # path does not exist
+ return False
+ else:
+ logger.debug('errno = %s' % (exc.errno,))
+ # we did not foresee this error, better add it explicitely
+ raise
+
+ class WindowsLock(object):
+ """
+ Creates a lock based on the atomic nature of mkdir on Windows
+ system calls.
+ """
+ LOCKBASE = os.path.join(gettempdir(), "leap-client-lock")
+
+ def __init__(self):
+ """
+ Initializes the lock.
+ Sets the lock name to basename plus the process pid.
+ """
+ self._fd = None
+ pid = os.getpid()
+ self.name = "%s-%s" % (self.LOCKBASE, pid)
+ self.pid = pid
+
+ def get_lock(self):
+ """
+ Tries to get a lock, and writes the running pid there if successful
+ """
+ gotit = self._get_lock()
+ return gotit
+
+ def _get_lock(self):
+ """
+ Tries to write to a file with the current pid as part of the name
+ """
+ try:
+ self._fd = os.makedirs(self.name)
+ except OSError as exc:
+ # could not create the dir
+ if exc.args[0] == 183:
+ logger.debug('cannot create dir')
+ # cannot create dir with existing name
+ return False
+ else:
+ raise
+ return self._is_one_pidfile()[0]
+
+ def _is_one_pidfile(self):
+ """
+ Returns True, pid if there is only one pidfile with the expected
+ base path
+
+ :rtype: tuple
+ """
+ pidfiles = glob.glob(self.LOCKBASE + '-*')
+ if len(pidfiles) == 1:
+ pid = pidfiles[0].split('-')[-1]
+ return True, int(pid)
+ else:
+ return False, None
+
+ def get_pid(self):
+ """
+ Returns the pid of the locking process.
+
+ :rtype: int
+ """
+ # XXX assert there is only one?
+ _, pid = self._is_one_pidfile()
+ return pid
+
+ def get_locking_path(self):
+ """
+ Returns the pid path of the locking process.
+
+ :rtype: str
+ """
+ pid = self.get_pid()
+ if pid:
+ return "%s-%s" % (self.LOCKBASE, pid)
+
+ def release_lock(self, name=None):
+ """
+ Releases the pidfile dir for this process, by removing it.
+ """
+ if not name:
+ name = self.name
+ _release_lock(name)
+
+ @classmethod
+ def release_all_locks(self):
+ """
+ Releases all locks. Used for clean shutdown.
+ """
+ for lockdir in glob.glob("%s-%s" % (self.LOCKBASE, '*')):
+ _release_lock(lockdir)
+
+ @property
+ def locked_by_us(self):
+ """
+ Returns True if the pid in the pidfile
+ is ours.
+
+ :rtype: bool
+ """
+ _, pid = self._is_one_pidfile()
+ return pid == self.pid
+
+ def update_ts(self):
+ """
+ Updates the timestamp of the lock.
+ """
+ if self.locked_by_us:
+ update_modification_ts(self.name)
+
+ def write_port(self, port):
+ """
+ Writes the port for windows control to the pidfile folder
+ Returns True if successful.
+
+ :rtype: bool
+ """
+ if not self.locked_by_us:
+ logger.warning("Tried to write control port to a "
+ "non-unique pidfile folder")
+ return False
+ port_file = os.path.join(self.name, "port")
+ with open(port_file, 'w') as f:
+ f.write("%s" % port)
+ return True
+
+ def get_control_port(self):
+ """
+ Reads control port of the main instance from the port file
+ in the pidfile dir
+
+ :rtype: int
+ """
+ pid = self.get_pid()
+ port_file = os.path.join(self.LOCKBASE + "-%s" % pid, "port")
+ port = None
+ try:
+ with open(port_file) as f:
+ port_str = f.read()
+ port = int(port_str.strip())
+ except IOError as exc:
+ if exc.errno == errno.ENOENT:
+ logger.error("Tried to read port from non-existent file")
+ else:
+ # we did not know explicitely about this error
+ raise
+ return port
+
+ def raise_window_ack():
+ """
+ This function is called from the windows callback that is registered
+ with the raise_window event. It just updates the modification time
+ of the lock file so we can signal an ack to the instance that tried
+ to raise the window.
+ """
+ lock = WindowsLock()
+ lock.update_ts()
+
+
+def we_are_the_one_and_only():
+ """
+ Returns True if we are the only instance running, False otherwise.
+ If we came later, send a raise signal to the main instance of the
+ application.
+
+ Under windows we are not using flock magic, so we wait during
+ RAISE_WINDOW_TIMEOUT time, if not ack is
+ received, we assume it was a stalled lock, so we remove it and continue
+ with initialization.
+
+ :rtype: bool
+ """
+ _sys = platform.system()
+
+ if _sys in ("Linux", "Darwin"):
+ locker = UnixLock('/tmp/leap-client.lock')
+ locker.get_lock()
+ we_are_the_one = locker.locked_by_us
+ if not we_are_the_one:
+ signal_event(proto.RAISE_WINDOW)
+ return we_are_the_one
+
+ elif _sys == "Windows":
+ locker = WindowsLock()
+ locker.get_lock()
+ we_are_the_one = locker.locked_by_us
+
+ if not we_are_the_one:
+ locker.release_lock()
+ lock_path = locker.get_locking_path()
+ ts = get_modification_ts(lock_path)
+
+ nowfun = datetime.datetime.now
+ t0 = nowfun()
+ pause = RAISE_WINDOW_TIMEOUT / float(RAISE_WINDOW_WAIT_STEPS)
+ timeout_delta = datetime.timedelta(0, RAISE_WINDOW_TIMEOUT)
+ check_interval = lambda: nowfun() - t0 < timeout_delta
+
+ # let's assume it's a stalled lock
+ we_are_the_one = True
+ signal_event(proto.RAISE_WINDOW)
+
+ while check_interval():
+ if get_modification_ts(lock_path) > ts:
+ # yay! someone claimed their control over the lock.
+ # so the lock is alive
+ logger.debug('Raise window ACK-ed')
+ we_are_the_one = False
+ break
+ else:
+ time.sleep(pause)
+
+ if we_are_the_one:
+ # ok, it really was a stalled lock. let's remove all
+ # that is left, and put only ours there.
+ WindowsLock.release_all_locks()
+ WindowsLock().get_lock()
+
+ return we_are_the_one
+
+ else:
+ logger.warning("Multi-instance checker "
+ "not implemented for %s" % (_sys))
+ # lies, lies, lies...
+ return True
diff --git a/src/leap/base/tests/__init__.py b/src/leap/provider/__init__.py
index e69de29b..e69de29b 100644
--- a/src/leap/base/tests/__init__.py
+++ b/src/leap/provider/__init__.py
diff --git a/src/leap/provider/supportedapis.py b/src/leap/provider/supportedapis.py
new file mode 100644
index 00000000..3e650ba2
--- /dev/null
+++ b/src/leap/provider/supportedapis.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# supportedapis.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+API Support check.
+"""
+
+
+class SupportedAPIs(object):
+ """
+ Class responsible of checking for API compatibility.
+ """
+ SUPPORTED_APIS = ["1"]
+
+ @classmethod
+ def supports(self, api_version):
+ """
+ :param api_version: the version number of the api that we need to check
+ :type api_version: str
+
+ :returns: if that version is supported or not.
+ :return type: bool
+ """
+ return api_version in self.SUPPORTED_APIS
diff --git a/src/leap/services/__init__.py b/src/leap/services/__init__.py
new file mode 100644
index 00000000..253359cd
--- /dev/null
+++ b/src/leap/services/__init__.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# __init__.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Services module.
+"""
+DEPLOYED = ["openvpn", "mx"]
+
+
+def get_supported(services):
+ """
+ Returns a list of the available services.
+
+ :param services: a list containing the services to be filtered.
+ :type services: list of str
+
+ :returns: a list of the available services
+ :rtype: list of str
+ """
+ return filter(lambda s: s in DEPLOYED, services)
diff --git a/src/leap/services/abstractbootstrapper.py b/src/leap/services/abstractbootstrapper.py
new file mode 100644
index 00000000..9e50948c
--- /dev/null
+++ b/src/leap/services/abstractbootstrapper.py
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+# abstractbootstrapper.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Abstract bootstrapper implementation
+"""
+import logging
+
+import requests
+
+from functools import partial
+
+from PySide import QtCore
+from twisted.internet import threads
+from leap.common.check import leap_assert, leap_assert_type
+
+logger = logging.getLogger(__name__)
+
+
+class AbstractBootstrapper(QtCore.QObject):
+ """
+ Abstract Bootstrapper that implements the needed deferred callbacks
+ """
+
+ PASSED_KEY = "passed"
+ ERROR_KEY = "error"
+
+ def __init__(self, bypass_checks=False):
+ """
+ Constructor for the abstract bootstrapper
+
+ :param bypass_checks: Set to true if the app should bypass
+ first round of checks for CA
+ certificates at bootstrap
+ :type bypass_checks: bool
+ """
+ QtCore.QObject.__init__(self)
+
+ leap_assert(self._gui_errback.im_func ==
+ AbstractBootstrapper._gui_errback.im_func,
+ "Cannot redefine _gui_errback")
+ leap_assert(self._errback.im_func ==
+ AbstractBootstrapper._errback.im_func,
+ "Cannot redefine _errback")
+ leap_assert(self._gui_notify.im_func ==
+ AbstractBootstrapper._gui_notify.im_func,
+ "Cannot redefine _gui_notify")
+
+ # **************************************************** #
+ # Dependency injection helpers, override this for more
+ # granular testing
+ self._fetcher = requests
+ # **************************************************** #
+
+ self._session = self._fetcher.session()
+ self._bypass_checks = bypass_checks
+ self._signal_to_emit = None
+ self._err_msg = None
+
+ def _gui_errback(self, failure):
+ """
+ Errback used to notify the GUI of a problem, it should be used
+ as the last errback of the whole chain.
+
+ Traps all exceptions if a signal is defined, otherwise it just
+ lets it continue.
+
+ NOTE: This method is final, it should not be redefined.
+
+ :param failure: failure object that Twisted generates
+ :type failure: twisted.python.failure.Failure
+ """
+ if self._signal_to_emit:
+ err_msg = self._err_msg \
+ if self._err_msg is not None \
+ else str(failure.value)
+ self._signal_to_emit.emit({
+ self.PASSED_KEY: False,
+ self.ERROR_KEY: err_msg
+ })
+ failure.trap(Exception)
+
+ def _errback(self, failure, signal=None):
+ """
+ Regular errback used for the middle of the chain. If it's
+ executed, the first one will set the signal to emit as
+ failure.
+
+ NOTE: This method is final, it should not be redefined.
+
+ :param failure: failure object that Twisted generates
+ :type failure: twisted.python.failure.Failure
+ :param signal: Signal to emit if it fails here first
+ :type signal: QtCore.SignalInstance
+
+ :returns: failure object that Twisted generates
+ :rtype: twisted.python.failure.Failure
+ """
+ if self._signal_to_emit is None:
+ self._signal_to_emit = signal
+ return failure
+
+ def _gui_notify(self, _, signal=None):
+ """
+ Callback used to notify the GUI of a success. Will emit signal
+ if specified
+
+ NOTE: This method is final, it should not be redefined.
+
+ :param _: IGNORED. Returned from the previous callback
+ :type _: IGNORED
+ :param signal: Signal to emit if it fails here first
+ :type signal: QtCore.SignalInstance
+ """
+ if signal:
+ logger.debug("Emitting %s" % (signal,))
+ signal.emit({self.PASSED_KEY: True, self.ERROR_KEY: ""})
+
+ def _callback_threader(self, cb, res, *args, **kwargs):
+ return threads.deferToThread(cb, res, *args, **kwargs)
+
+ def addCallbackChain(self, callbacks):
+ """
+ Creates a callback/errback chain on another thread using
+ deferToThread and adds the _gui_errback to the end to notify
+ the GUI on an error.
+
+ :param callbacks: List of tuples of callbacks and the signal
+ associated to that callback
+ :type callbacks: list(tuple(func, func))
+
+ :returns: the defer with the callback chain
+ :rtype: deferred
+ """
+ leap_assert_type(callbacks, list)
+
+ self._signal_to_emit = None
+ self._err_msg = None
+
+ d = None
+ for cb, sig in callbacks:
+ if d is None:
+ d = threads.deferToThread(cb)
+ else:
+ d.addCallback(partial(self._callback_threader, cb))
+ d.addErrback(self._errback, signal=sig)
+ d.addCallback(self._gui_notify, signal=sig)
+ d.addErrback(self._gui_errback)
+ return d
diff --git a/src/leap/baseapp/__init__.py b/src/leap/services/eip/__init__.py
index e69de29b..e69de29b 100644
--- a/src/leap/baseapp/__init__.py
+++ b/src/leap/services/eip/__init__.py
diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py
new file mode 100644
index 00000000..1d7bc342
--- /dev/null
+++ b/src/leap/services/eip/eipbootstrapper.py
@@ -0,0 +1,183 @@
+# -*- coding: utf-8 -*-
+# eipbootstrapper.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+EIP bootstrapping
+"""
+
+import logging
+import os
+
+from PySide import QtCore
+
+from leap.common.check import leap_assert, leap_assert_type
+from leap.common import certs
+from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p
+from leap.config.providerconfig import ProviderConfig
+from leap.crypto.srpauth import SRPAuth
+from leap.services.eip.eipconfig import EIPConfig
+from leap.util.request_helpers import get_content
+from leap.util.constants import REQUEST_TIMEOUT
+from leap.services.abstractbootstrapper import AbstractBootstrapper
+
+logger = logging.getLogger(__name__)
+
+
+class EIPBootstrapper(AbstractBootstrapper):
+ """
+ Sets up EIP for a provider a series of checks and emits signals
+ after they are passed.
+ If a check fails, the subsequent checks are not executed
+ """
+
+ # All dicts returned are of the form
+ # {"passed": bool, "error": str}
+ download_config = QtCore.Signal(dict)
+ download_client_certificate = QtCore.Signal(dict)
+
+ def __init__(self):
+ AbstractBootstrapper.__init__(self)
+
+ self._provider_config = None
+ self._eip_config = None
+ self._download_if_needed = False
+
+ def _download_config(self, *args):
+ """
+ Downloads the EIP config for the given provider
+ """
+
+ leap_assert(self._provider_config,
+ "We need a provider configuration!")
+
+ logger.debug("Downloading EIP config for %s" %
+ (self._provider_config.get_domain(),))
+
+ api_version = self._provider_config.get_api_version()
+ self._eip_config = EIPConfig()
+ self._eip_config.set_api_version(api_version)
+
+ headers = {}
+ mtime = get_mtime(os.path.join(self._eip_config
+ .get_path_prefix(),
+ "leap",
+ "providers",
+ self._provider_config.get_domain(),
+ "eip-service.json"))
+
+ if self._download_if_needed and mtime:
+ headers['if-modified-since'] = mtime
+
+ # there is some confusion with this uri,
+ # it's in 1/config/eip, config/eip and config/1/eip...
+ config_uri = "%s/%s/config/eip-service.json" % (
+ self._provider_config.get_api_uri(),
+ api_version)
+ logger.debug('Downloading eip config from: %s' % config_uri)
+
+ res = self._session.get(config_uri,
+ verify=self._provider_config
+ .get_ca_cert_path(),
+ headers=headers,
+ timeout=REQUEST_TIMEOUT)
+ res.raise_for_status()
+
+ # Not modified
+ if res.status_code == 304:
+ logger.debug("EIP definition has not been modified")
+ else:
+ eip_definition, mtime = get_content(res)
+
+ self._eip_config.load(data=eip_definition, mtime=mtime)
+ self._eip_config.save(["leap",
+ "providers",
+ self._provider_config.get_domain(),
+ "eip-service.json"])
+
+ def _download_client_certificates(self, *args):
+ """
+ Downloads the EIP client certificate for the given provider
+ """
+ leap_assert(self._provider_config, "We need a provider configuration!")
+ leap_assert(self._eip_config, "We need an eip configuration!")
+
+ logger.debug("Downloading EIP client certificate for %s" %
+ (self._provider_config.get_domain(),))
+
+ client_cert_path = self._eip_config.\
+ get_client_cert_path(self._provider_config,
+ about_to_download=True)
+
+ # For re-download if something is wrong with the cert
+ self._download_if_needed = self._download_if_needed and \
+ not certs.should_redownload(client_cert_path)
+
+ if self._download_if_needed and \
+ os.path.exists(client_cert_path):
+ check_and_fix_urw_only(client_cert_path)
+ return
+
+ srp_auth = SRPAuth(self._provider_config)
+ session_id = srp_auth.get_session_id()
+ cookies = None
+ if session_id:
+ cookies = {"_session_id": session_id}
+ cert_uri = "%s/%s/cert" % (
+ self._provider_config.get_api_uri(),
+ self._provider_config.get_api_version())
+ logger.debug('getting cert from uri: %s' % cert_uri)
+ res = self._session.get(cert_uri,
+ verify=self._provider_config
+ .get_ca_cert_path(),
+ cookies=cookies,
+ timeout=REQUEST_TIMEOUT)
+ res.raise_for_status()
+ client_cert = res.content
+
+ if not certs.is_valid_pemfile(client_cert):
+ raise Exception(self.tr("The downloaded certificate is not a "
+ "valid PEM file"))
+
+ mkdir_p(os.path.dirname(client_cert_path))
+
+ with open(client_cert_path, "w") as f:
+ f.write(client_cert)
+
+ check_and_fix_urw_only(client_cert_path)
+
+ def run_eip_setup_checks(self,
+ provider_config,
+ download_if_needed=False):
+ """
+ Starts the checks needed for a new eip setup
+
+ :param provider_config: Provider configuration
+ :type provider_config: ProviderConfig
+ """
+ leap_assert(provider_config, "We need a provider config!")
+ leap_assert_type(provider_config, ProviderConfig)
+
+ self._provider_config = provider_config
+ self._download_if_needed = download_if_needed
+
+ cb_chain = [
+ (self._download_config, self.download_config),
+ (self._download_client_certificates,
+ self.download_client_certificate)
+ ]
+
+ return self.addCallbackChain(cb_chain)
diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py
new file mode 100644
index 00000000..d69e1fd8
--- /dev/null
+++ b/src/leap/services/eip/eipconfig.py
@@ -0,0 +1,263 @@
+# -*- coding: utf-8 -*-
+# eipconfig.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Provider configuration
+"""
+import logging
+import os
+import re
+import time
+
+import ipaddr
+
+from leap.common.check import leap_assert, leap_assert_type
+from leap.common.config.baseconfig import BaseConfig
+from leap.config.providerconfig import ProviderConfig
+from leap.services.eip.eipspec import get_schema
+
+logger = logging.getLogger(__name__)
+
+
+class VPNGatewaySelector(object):
+ """
+ VPN Gateway selector.
+ """
+ # http://www.timeanddate.com/time/map/
+ equivalent_timezones = {13: -11, 14: -10}
+
+ def __init__(self, eipconfig, tz_offset=None):
+ '''
+ Constructor for VPNGatewaySelector.
+
+ :param eipconfig: a valid EIP Configuration.
+ :type eipconfig: EIPConfig
+ :param tz_offset: use this offset as a local distance to GMT.
+ :type tz_offset: int
+ '''
+ leap_assert_type(eipconfig, EIPConfig)
+
+ self._local_offset = tz_offset
+ if tz_offset is None:
+ tz_offset = self._get_local_offset()
+
+ if tz_offset in self.equivalent_timezones:
+ tz_offset = self.equivalent_timezones[tz_offset]
+
+ self._local_offset = tz_offset
+
+ self._eipconfig = eipconfig
+
+ def get_gateways(self):
+ """
+ Returns the 4 best gateways, sorted by timezone proximity.
+
+ :rtype: list of IPv4Address or IPv6Address object.
+ """
+ gateways_timezones = []
+ locations = self._eipconfig.get_locations()
+ gateways = self._eipconfig.get_gateways()
+
+ for idx, gateway in enumerate(gateways):
+ gateway_location = gateway.get('location')
+ gateway_distance = 99 # if hasn't location -> should go last
+
+ if gateway_location is not None:
+ gw_offset = int(locations[gateway['location']]['timezone'])
+ if gw_offset in self.equivalent_timezones:
+ gw_offset = self.equivalent_timezones[gw_offset]
+
+ gateway_distance = self._get_timezone_distance(gw_offset)
+
+ ip = self._eipconfig.get_gateway_ip(idx)
+ gateways_timezones.append((ip, gateway_distance))
+
+ gateways_timezones = sorted(gateways_timezones,
+ key=lambda gw: gw[1])[:4]
+
+ gateways = [ip for ip, dist in gateways_timezones]
+ return gateways
+
+ def _get_timezone_distance(self, offset):
+ '''
+ Returns the distance between the local timezone and
+ the one with offset 'offset'.
+
+ :param offset: the distance of a timezone to GMT.
+ :type offset: int
+ :returns: distance between local offset and param offset.
+ :rtype: int
+ '''
+ timezones = range(-11, 13)
+ tz1 = offset
+ tz2 = self._local_offset
+ distance = abs(timezones.index(tz1) - timezones.index(tz2))
+ if distance > 12:
+ if tz1 < 0:
+ distance = timezones.index(tz1) + timezones[::-1].index(tz2)
+ else:
+ distance = timezones[::-1].index(tz1) + timezones.index(tz2)
+
+ return distance
+
+ def _get_local_offset(self):
+ '''
+ Returns the distance between GMT and the local timezone.
+
+ :rtype: int
+ '''
+ local_offset = time.timezone
+ if time.daylight:
+ local_offset = time.altzone
+
+ return local_offset / 3600
+
+
+class EIPConfig(BaseConfig):
+ """
+ Provider configuration abstraction class
+ """
+ OPENVPN_ALLOWED_KEYS = ("auth", "cipher", "tls-cipher")
+ OPENVPN_CIPHERS_REGEX = re.compile("[A-Z0-9\-]+")
+
+ def __init__(self):
+ BaseConfig.__init__(self)
+ self._api_version = None
+
+ def _get_schema(self):
+ """
+ Returns the schema corresponding to the version given.
+
+ :rtype: dict or None if the version is not supported.
+ """
+ return get_schema(self._api_version)
+
+ def get_clusters(self):
+ # TODO: create an abstraction for clusters
+ return self._safe_get_value("clusters")
+
+ def get_gateways(self):
+ # TODO: create an abstraction for gateways
+ return self._safe_get_value("gateways")
+
+ def get_locations(self):
+ '''
+ Returns a list of locations
+
+ :rtype: dict
+ '''
+ return self._safe_get_value("locations")
+
+ def get_openvpn_configuration(self):
+ """
+ Returns a dictionary containing the openvpn configuration
+ parameters.
+
+ These are sanitized with alphanumeric whitelist.
+
+ :returns: openvpn configuration dict
+ :rtype: C{dict}
+ """
+ ovpncfg = self._safe_get_value("openvpn_configuration")
+ config = {}
+ for key, value in ovpncfg.items():
+ if key in self.OPENVPN_ALLOWED_KEYS and value is not None:
+ sanitized_val = self.OPENVPN_CIPHERS_REGEX.findall(value)
+ if len(sanitized_val) != 0:
+ _val = sanitized_val[0]
+ config[str(key)] = str(_val)
+ return config
+
+ def get_serial(self):
+ return self._safe_get_value("serial")
+
+ def get_version(self):
+ return self._safe_get_value("version")
+
+ def get_gateway_ip(self, index=0):
+ """
+ Returns the ip of the gateway.
+
+ :rtype: An IPv4Address or IPv6Address object.
+ """
+ gateways = self.get_gateways()
+ leap_assert(len(gateways) > 0, "We don't have any gateway!")
+ if index > len(gateways):
+ index = 0
+ logger.warning("Provided an unknown gateway index %s, " +
+ "defaulting to 0")
+ ip_addr_str = gateways[index]["ip_address"]
+
+ try:
+ ipaddr.IPAddress(ip_addr_str)
+ return ip_addr_str
+ except ValueError:
+ logger.error("Invalid ip address in config: %s" % (ip_addr_str,))
+ return None
+
+ def get_client_cert_path(self,
+ providerconfig=None,
+ about_to_download=False):
+ """
+ Returns the path to the certificate used by openvpn
+ """
+
+ leap_assert(providerconfig, "We need a provider")
+ leap_assert_type(providerconfig, ProviderConfig)
+
+ cert_path = os.path.join(self.get_path_prefix(),
+ "leap",
+ "providers",
+ providerconfig.get_domain(),
+ "keys",
+ "client",
+ "openvpn.pem")
+
+ if not about_to_download:
+ leap_assert(os.path.exists(cert_path),
+ "You need to download the certificate first")
+ logger.debug("Using OpenVPN cert %s" % (cert_path,))
+
+ return cert_path
+
+
+if __name__ == "__main__":
+ logger = logging.getLogger(name='leap')
+ logger.setLevel(logging.DEBUG)
+ console = logging.StreamHandler()
+ console.setLevel(logging.DEBUG)
+ formatter = logging.Formatter(
+ '%(asctime)s '
+ '- %(name)s - %(levelname)s - %(message)s')
+ console.setFormatter(formatter)
+ logger.addHandler(console)
+
+ eipconfig = EIPConfig('1')
+
+ try:
+ eipconfig.get_clusters()
+ except Exception as e:
+ assert isinstance(e, AssertionError), "Expected an assert"
+ print "Safe value getting is working"
+
+ if eipconfig.load("leap/providers/bitmask.net/eip-service.json"):
+ print eipconfig.get_clusters()
+ print eipconfig.get_gateways()
+ print eipconfig.get_locations()
+ print eipconfig.get_openvpn_configuration()
+ print eipconfig.get_serial()
+ print eipconfig.get_version()
diff --git a/src/leap/services/eip/eipspec.py b/src/leap/services/eip/eipspec.py
new file mode 100644
index 00000000..9cc56be3
--- /dev/null
+++ b/src/leap/services/eip/eipspec.py
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+# eipspec.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+# Schemas dict
+# To add a schema for a version you should follow the form:
+# { '1': schema_v1, '2': schema_v2, ... etc }
+# so for instance, to add the '2' version, you should do:
+# eipservice_config_spec['2'] = schema_v2
+eipservice_config_spec = {}
+
+eipservice_config_spec['1'] = {
+ 'description': 'sample eip service config',
+ 'type': 'object',
+ 'properties': {
+ 'serial': {
+ 'type': int,
+ 'default': 1,
+ 'required': ["True"]
+ },
+ 'version': {
+ 'type': int,
+ 'default': 1,
+ 'required': ["True"]
+ },
+ 'clusters': {
+ 'type': list,
+ 'default': [
+ {"label": {
+ "en": "Location Unknown"},
+ "name": "location_unknown"}]
+ },
+ 'gateways': {
+ 'type': list,
+ 'default': [
+ {"capabilities": {
+ "adblock": True,
+ "filter_dns": True,
+ "ports": ["80", "53", "443", "1194"],
+ "protocols": ["udp", "tcp"],
+ "transport": ["openvpn"],
+ "user_ips": False},
+ "cluster": "location_unknown",
+ "host": "location.example.org",
+ "ip_address": "127.0.0.1"}]
+ },
+ 'locations': {
+ 'type': dict,
+ 'default': {}
+ },
+ 'openvpn_configuration': {
+ 'type': dict,
+ 'default': {
+ "auth": None,
+ "cipher": None,
+ "tls-cipher": None}
+ }
+ }
+}
+
+
+def get_schema(version):
+ """
+ Returns the schema corresponding to the version given.
+
+ :param version: the version of the schema to get.
+ :type version: str
+ :rtype: dict or None if the version is not supported.
+ """
+ schema = eipservice_config_spec.get(version, None)
+ return schema
diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py
new file mode 100644
index 00000000..bf5938dc
--- /dev/null
+++ b/src/leap/services/eip/providerbootstrapper.py
@@ -0,0 +1,340 @@
+# -*- coding: utf-8 -*-
+# providerbootstrapper.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Provider bootstrapping
+"""
+import logging
+import socket
+import os
+
+import requests
+
+from PySide import QtCore
+
+from leap.common.certs import get_digest
+from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p
+from leap.common.check import leap_assert, leap_assert_type, leap_check
+from leap.config.providerconfig import ProviderConfig, MissingCACert
+from leap.util.request_helpers import get_content
+from leap.util.constants import REQUEST_TIMEOUT
+from leap.services.abstractbootstrapper import AbstractBootstrapper
+from leap.provider.supportedapis import SupportedAPIs
+
+
+logger = logging.getLogger(__name__)
+
+
+class UnsupportedProviderAPI(Exception):
+ """
+ Raised when attempting to use a provider with an incompatible API.
+ """
+ pass
+
+
+class WrongFingerprint(Exception):
+ """
+ Raised when a fingerprint comparison does not match.
+ """
+ pass
+
+
+class ProviderBootstrapper(AbstractBootstrapper):
+ """
+ Given a provider URL performs a series of checks and emits signals
+ after they are passed.
+ If a check fails, the subsequent checks are not executed
+ """
+
+ # All dicts returned are of the form
+ # {"passed": bool, "error": str}
+ name_resolution = QtCore.Signal(dict)
+ https_connection = QtCore.Signal(dict)
+ download_provider_info = QtCore.Signal(dict)
+
+ download_ca_cert = QtCore.Signal(dict)
+ check_ca_fingerprint = QtCore.Signal(dict)
+ check_api_certificate = QtCore.Signal(dict)
+
+ def __init__(self, bypass_checks=False):
+ """
+ Constructor for provider bootstrapper object
+
+ :param bypass_checks: Set to true if the app should bypass
+ first round of checks for CA certificates at bootstrap
+ :type bypass_checks: bool
+ """
+ AbstractBootstrapper.__init__(self, bypass_checks)
+
+ self._domain = None
+ self._provider_config = None
+ self._download_if_needed = False
+
+ def _check_name_resolution(self):
+ """
+ Checks that the name resolution for the provider name works
+ """
+ leap_assert(self._domain, "Cannot check DNS without a domain")
+
+ logger.debug("Checking name resolution for %s" % (self._domain))
+
+ # We don't skip this check, since it's basic for the whole
+ # system to work
+ socket.gethostbyname(self._domain)
+
+ def _check_https(self, *args):
+ """
+ Checks that https is working and that the provided certificate
+ checks out
+ """
+
+ leap_assert(self._domain, "Cannot check HTTPS without a domain")
+
+ logger.debug("Checking https for %s" % (self._domain))
+
+ # We don't skip this check, since it's basic for the whole
+ # system to work
+
+ try:
+ res = self._session.get("https://%s" % (self._domain,),
+ verify=not self._bypass_checks,
+ timeout=REQUEST_TIMEOUT)
+ res.raise_for_status()
+ except requests.exceptions.SSLError:
+ self._err_msg = self.tr("Provider certificate could "
+ "not be verified")
+ raise
+ except Exception:
+ self._err_msg = self.tr("Provider does not support HTTPS")
+ raise
+
+ def _download_provider_info(self, *args):
+ """
+ Downloads the provider.json defition
+ """
+ leap_assert(self._domain,
+ "Cannot download provider info without a domain")
+
+ logger.debug("Downloading provider info for %s" % (self._domain))
+
+ headers = {}
+
+ provider_json = os.path.join(
+ ProviderConfig().get_path_prefix(), "leap", "providers",
+ self._domain, "provider.json")
+ mtime = get_mtime(provider_json)
+
+ if self._download_if_needed and mtime:
+ headers['if-modified-since'] = mtime
+
+ uri = "https://%s/%s" % (self._domain, "provider.json")
+ verify = not self._bypass_checks
+
+ if mtime: # the provider.json exists
+ provider_config = ProviderConfig()
+ provider_config.load(provider_json)
+ try:
+ verify = provider_config.get_ca_cert_path()
+ uri = provider_config.get_api_uri() + '/provider.json'
+ except MissingCACert:
+ # get_ca_cert_path fails if the certificate does not exists.
+ pass
+
+ logger.debug("Requesting for provider.json... "
+ "uri: {0}, verify: {1}, headers: {2}".format(
+ uri, verify, headers))
+ res = self._session.get(uri, verify=verify,
+ headers=headers, timeout=REQUEST_TIMEOUT)
+ res.raise_for_status()
+ logger.debug("Request status code: {0}".format(res.status_code))
+
+ # Not modified
+ if res.status_code == 304:
+ logger.debug("Provider definition has not been modified")
+ else:
+ provider_definition, mtime = get_content(res)
+
+ provider_config = ProviderConfig()
+ provider_config.load(data=provider_definition, mtime=mtime)
+ provider_config.save(["leap",
+ "providers",
+ self._domain,
+ "provider.json"])
+
+ api_version = provider_config.get_api_version()
+ if SupportedAPIs.supports(api_version):
+ logger.debug("Provider definition has been modified")
+ else:
+ api_supported = ', '.join(SupportedAPIs.SUPPORTED_APIS)
+ error = ('Unsupported provider API version. '
+ 'Supported versions are: {}. '
+ 'Found: {}.').format(api_supported, api_version)
+
+ logger.error(error)
+ raise UnsupportedProviderAPI(error)
+
+ def run_provider_select_checks(self, domain, download_if_needed=False):
+ """
+ Populates the check queue.
+
+ :param domain: domain to check
+ :type domain: str
+
+ :param download_if_needed: if True, makes the checks do not
+ overwrite already downloaded data
+ :type download_if_needed: bool
+ """
+ leap_assert(domain and len(domain) > 0, "We need a domain!")
+
+ self._domain = ProviderConfig.sanitize_path_component(domain)
+ self._download_if_needed = download_if_needed
+
+ cb_chain = [
+ (self._check_name_resolution, self.name_resolution),
+ (self._check_https, self.https_connection),
+ (self._download_provider_info, self.download_provider_info)
+ ]
+
+ return self.addCallbackChain(cb_chain)
+
+ def _should_proceed_cert(self):
+ """
+ Returns False if the certificate already exists for the given
+ provider. True otherwise
+
+ :rtype: bool
+ """
+ leap_assert(self._provider_config, "We need a provider config!")
+
+ if not self._download_if_needed:
+ return True
+
+ return not os.path.exists(self._provider_config
+ .get_ca_cert_path(about_to_download=True))
+
+ def _download_ca_cert(self, *args):
+ """
+ Downloads the CA cert that is going to be used for the api URL
+ """
+
+ leap_assert(self._provider_config, "Cannot download the ca cert "
+ "without a provider config!")
+
+ logger.debug("Downloading ca cert for %s at %s" %
+ (self._domain, self._provider_config.get_ca_cert_uri()))
+
+ if not self._should_proceed_cert():
+ check_and_fix_urw_only(
+ self._provider_config
+ .get_ca_cert_path(about_to_download=True))
+ return
+
+ res = self._session.get(self._provider_config.get_ca_cert_uri(),
+ verify=not self._bypass_checks,
+ timeout=REQUEST_TIMEOUT)
+ res.raise_for_status()
+
+ cert_path = self._provider_config.get_ca_cert_path(
+ about_to_download=True)
+ cert_dir = os.path.dirname(cert_path)
+ mkdir_p(cert_dir)
+ with open(cert_path, "w") as f:
+ f.write(res.content)
+
+ check_and_fix_urw_only(cert_path)
+
+ def _check_ca_fingerprint(self, *args):
+ """
+ Checks the CA cert fingerprint against the one provided in the
+ json definition
+ """
+ leap_assert(self._provider_config, "Cannot check the ca cert "
+ "without a provider config!")
+
+ logger.debug("Checking ca fingerprint for %s and cert %s" %
+ (self._domain,
+ self._provider_config.get_ca_cert_path()))
+
+ if not self._should_proceed_cert():
+ return
+
+ parts = self._provider_config.get_ca_cert_fingerprint().split(":")
+
+ error_msg = "Wrong fingerprint format"
+ leap_check(len(parts) == 2, error_msg, WrongFingerprint)
+
+ method = parts[0].strip()
+ fingerprint = parts[1].strip()
+ cert_data = None
+ with open(self._provider_config.get_ca_cert_path()) as f:
+ cert_data = f.read()
+
+ leap_assert(len(cert_data) > 0, "Could not read certificate data")
+ digest = get_digest(cert_data, method)
+
+ error_msg = "Downloaded certificate has a different fingerprint!"
+ leap_check(digest == fingerprint, error_msg, WrongFingerprint)
+
+ def _check_api_certificate(self, *args):
+ """
+ Tries to make an API call with the downloaded cert and checks
+ if it validates against it
+ """
+ leap_assert(self._provider_config, "Cannot check the ca cert "
+ "without a provider config!")
+
+ logger.debug("Checking api certificate for %s and cert %s" %
+ (self._provider_config.get_api_uri(),
+ self._provider_config.get_ca_cert_path()))
+
+ if not self._should_proceed_cert():
+ return
+
+ test_uri = "%s/%s/cert" % (self._provider_config.get_api_uri(),
+ self._provider_config.get_api_version())
+ res = self._session.get(test_uri,
+ verify=self._provider_config
+ .get_ca_cert_path(),
+ timeout=REQUEST_TIMEOUT)
+ res.raise_for_status()
+
+ def run_provider_setup_checks(self,
+ provider_config,
+ download_if_needed=False):
+ """
+ Starts the checks needed for a new provider setup.
+
+ :param provider_config: Provider configuration
+ :type provider_config: ProviderConfig
+
+ :param download_if_needed: if True, makes the checks do not
+ overwrite already downloaded data.
+ :type download_if_needed: bool
+ """
+ leap_assert(provider_config, "We need a provider config!")
+ leap_assert_type(provider_config, ProviderConfig)
+
+ self._provider_config = provider_config
+ self._download_if_needed = download_if_needed
+
+ cb_chain = [
+ (self._download_ca_cert, self.download_ca_cert),
+ (self._check_ca_fingerprint, self.check_ca_fingerprint),
+ (self._check_api_certificate, self.check_api_certificate)
+ ]
+
+ return self.addCallbackChain(cb_chain)
diff --git a/src/leap/eip/__init__.py b/src/leap/services/eip/tests/__init__.py
index e69de29b..e69de29b 100644
--- a/src/leap/eip/__init__.py
+++ b/src/leap/services/eip/tests/__init__.py
diff --git a/src/leap/services/eip/tests/test_eipbootstrapper.py b/src/leap/services/eip/tests/test_eipbootstrapper.py
new file mode 100644
index 00000000..f2331eca
--- /dev/null
+++ b/src/leap/services/eip/tests/test_eipbootstrapper.py
@@ -0,0 +1,347 @@
+# -*- coding: utf-8 -*-
+# test_eipbootstrapper.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+"""
+Tests for the EIP Boostrapper checks
+
+These will be whitebox tests since we want to make sure the private
+implementation is checking what we expect.
+"""
+
+import os
+import mock
+import tempfile
+import time
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+from nose.twistedtools import deferred, reactor
+from twisted.internet import threads
+from requests.models import Response
+
+from leap.common.testing.basetest import BaseLeapTest
+from leap.services.eip.eipbootstrapper import EIPBootstrapper
+from leap.services.eip.eipconfig import EIPConfig
+from leap.config.providerconfig import ProviderConfig
+from leap.crypto.tests import fake_provider
+from leap.common.files import mkdir_p
+from leap.crypto.srpauth import SRPAuth
+
+
+class EIPBootstrapperActiveTest(BaseLeapTest):
+ @classmethod
+ def setUpClass(cls):
+ BaseLeapTest.setUpClass()
+ factory = fake_provider.get_provider_factory()
+ http = reactor.listenTCP(0, factory)
+ https = reactor.listenSSL(
+ 0, factory,
+ fake_provider.OpenSSLServerContextFactory())
+ get_port = lambda p: p.getHost().port
+ cls.http_port = get_port(http)
+ cls.https_port = get_port(https)
+
+ def setUp(self):
+ self.eb = EIPBootstrapper()
+ self.old_pp = EIPConfig.get_path_prefix
+ self.old_save = EIPConfig.save
+ self.old_load = EIPConfig.load
+ self.old_si = SRPAuth.get_session_id
+
+ def tearDown(self):
+ EIPConfig.get_path_prefix = self.old_pp
+ EIPConfig.save = self.old_save
+ EIPConfig.load = self.old_load
+ SRPAuth.get_session_id = self.old_si
+
+ def _download_config_test_template(self, ifneeded, new):
+ """
+ All download config tests have the same structure, so this is
+ a parametrized test for that.
+
+ :param ifneeded: sets _download_if_needed
+ :type ifneeded: bool
+ :param new: if True uses time.time() as mtime for the mocked
+ eip-service file, otherwise it uses 100 (a really
+ old mtime)
+ :type new: float or int (will be coersed)
+ """
+ pc = ProviderConfig()
+ pc.get_domain = mock.MagicMock(
+ return_value="localhost:%s" % (self.https_port))
+ self.eb._provider_config = pc
+
+ pc.get_api_uri = mock.MagicMock(
+ return_value="https://%s" % (pc.get_domain()))
+ pc.get_api_version = mock.MagicMock(return_value="1")
+
+ # This is to ignore https checking, since it's not the point
+ # of this test
+ pc.get_ca_cert_path = mock.MagicMock(return_value=False)
+
+ path_prefix = tempfile.mkdtemp()
+ EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix)
+ EIPConfig.save = mock.MagicMock()
+ EIPConfig.load = mock.MagicMock()
+
+ self.eb._download_if_needed = ifneeded
+
+ provider_dir = os.path.join(EIPConfig.get_path_prefix(),
+ "leap",
+ "providers",
+ pc.get_domain())
+ mkdir_p(provider_dir)
+ eip_config_path = os.path.join(provider_dir,
+ "eip-service.json")
+
+ with open(eip_config_path, "w") as ec:
+ ec.write("A")
+
+ # set mtime to something really new
+ if new:
+ os.utime(eip_config_path, (-1, time.time()))
+ else:
+ os.utime(eip_config_path, (-1, 100))
+
+ @deferred()
+ def test_download_config_not_modified(self):
+ self._download_config_test_template(True, True)
+
+ d = threads.deferToThread(self.eb._download_config)
+
+ def check(*args):
+ self.assertFalse(self.eb._eip_config.save.called)
+ d.addCallback(check)
+ return d
+
+ @deferred()
+ def test_download_config_modified(self):
+ self._download_config_test_template(True, False)
+
+ d = threads.deferToThread(self.eb._download_config)
+
+ def check(*args):
+ self.assertTrue(self.eb._eip_config.save.called)
+ d.addCallback(check)
+ return d
+
+ @deferred()
+ def test_download_config_ignores_mtime(self):
+ self._download_config_test_template(False, True)
+
+ d = threads.deferToThread(self.eb._download_config)
+
+ def check(*args):
+ self.eb._eip_config.save.assert_called_once_with(
+ ["leap",
+ "providers",
+ self.eb._provider_config.get_domain(),
+ "eip-service.json"])
+ d.addCallback(check)
+ return d
+
+ def _download_certificate_test_template(self, ifneeded, createcert):
+ """
+ All download client certificate tests have the same structure,
+ so this is a parametrized test for that.
+
+ :param ifneeded: sets _download_if_needed
+ :type ifneeded: bool
+ :param createcert: if True it creates a dummy file to play the
+ part of a downloaded certificate
+ :type createcert: bool
+
+ :returns: the temp eip cert path and the dummy cert contents
+ :rtype: tuple of str, str
+ """
+ pc = ProviderConfig()
+ ec = EIPConfig()
+ self.eb._provider_config = pc
+ self.eb._eip_config = ec
+
+ pc.get_domain = mock.MagicMock(
+ return_value="localhost:%s" % (self.https_port))
+ pc.get_api_uri = mock.MagicMock(
+ return_value="https://%s" % (pc.get_domain()))
+ pc.get_api_version = mock.MagicMock(return_value="1")
+ pc.get_ca_cert_path = mock.MagicMock(return_value=False)
+
+ path_prefix = tempfile.mkdtemp()
+ EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix)
+ EIPConfig.save = mock.MagicMock()
+ EIPConfig.load = mock.MagicMock()
+
+ self.eb._download_if_needed = ifneeded
+
+ provider_dir = os.path.join(EIPConfig.get_path_prefix(),
+ "leap",
+ "providers",
+ "somedomain")
+ mkdir_p(provider_dir)
+ eip_cert_path = os.path.join(provider_dir,
+ "cert")
+
+ ec.get_client_cert_path = mock.MagicMock(
+ return_value=eip_cert_path)
+
+ cert_content = "A"
+ if createcert:
+ with open(eip_cert_path, "w") as ec:
+ ec.write(cert_content)
+
+ return eip_cert_path, cert_content
+
+ def test_download_client_certificate_not_modified(self):
+ cert_path, old_cert_content = self._download_certificate_test_template(
+ True, True)
+
+ with mock.patch('leap.common.certs.should_redownload',
+ new_callable=mock.MagicMock,
+ return_value=False):
+ self.eb._download_client_certificates()
+ with open(cert_path, "r") as c:
+ self.assertEqual(c.read(), old_cert_content)
+
+ @deferred()
+ def test_download_client_certificate_old_cert(self):
+ cert_path, old_cert_content = self._download_certificate_test_template(
+ True, True)
+
+ def wrapper(*args):
+ with mock.patch('leap.common.certs.should_redownload',
+ new_callable=mock.MagicMock,
+ return_value=True):
+ with mock.patch('leap.common.certs.is_valid_pemfile',
+ new_callable=mock.MagicMock,
+ return_value=True):
+ self.eb._download_client_certificates()
+
+ def check(*args):
+ with open(cert_path, "r") as c:
+ self.assertNotEqual(c.read(), old_cert_content)
+ d = threads.deferToThread(wrapper)
+ d.addCallback(check)
+
+ return d
+
+ @deferred()
+ def test_download_client_certificate_no_cert(self):
+ cert_path, _ = self._download_certificate_test_template(
+ True, False)
+
+ def wrapper(*args):
+ with mock.patch('leap.common.certs.should_redownload',
+ new_callable=mock.MagicMock,
+ return_value=False):
+ with mock.patch('leap.common.certs.is_valid_pemfile',
+ new_callable=mock.MagicMock,
+ return_value=True):
+ self.eb._download_client_certificates()
+
+ def check(*args):
+ self.assertTrue(os.path.exists(cert_path))
+ d = threads.deferToThread(wrapper)
+ d.addCallback(check)
+
+ return d
+
+ @deferred()
+ def test_download_client_certificate_force_not_valid(self):
+ cert_path, old_cert_content = self._download_certificate_test_template(
+ True, True)
+
+ def wrapper(*args):
+ with mock.patch('leap.common.certs.should_redownload',
+ new_callable=mock.MagicMock,
+ return_value=True):
+ with mock.patch('leap.common.certs.is_valid_pemfile',
+ new_callable=mock.MagicMock,
+ return_value=True):
+ self.eb._download_client_certificates()
+
+ def check(*args):
+ with open(cert_path, "r") as c:
+ self.assertNotEqual(c.read(), old_cert_content)
+ d = threads.deferToThread(wrapper)
+ d.addCallback(check)
+
+ return d
+
+ @deferred()
+ def test_download_client_certificate_invalid_download(self):
+ cert_path, _ = self._download_certificate_test_template(
+ False, False)
+
+ def wrapper(*args):
+ with mock.patch('leap.common.certs.should_redownload',
+ new_callable=mock.MagicMock,
+ return_value=True):
+ with mock.patch('leap.common.certs.is_valid_pemfile',
+ new_callable=mock.MagicMock,
+ return_value=False):
+ with self.assertRaises(Exception):
+ self.eb._download_client_certificates()
+ d = threads.deferToThread(wrapper)
+
+ return d
+
+ @deferred()
+ def test_download_client_certificate_uses_session_id(self):
+ _, _ = self._download_certificate_test_template(
+ False, False)
+
+ SRPAuth.get_session_id = mock.MagicMock(return_value="1")
+
+ def check_cookie(*args, **kwargs):
+ cookies = kwargs.get("cookies", None)
+ self.assertEqual(cookies, {'_session_id': '1'})
+ return Response()
+
+ def wrapper(*args):
+ with mock.patch('leap.common.certs.should_redownload',
+ new_callable=mock.MagicMock,
+ return_value=False):
+ with mock.patch('leap.common.certs.is_valid_pemfile',
+ new_callable=mock.MagicMock,
+ return_value=True):
+ with mock.patch('requests.sessions.Session.get',
+ new_callable=mock.MagicMock,
+ side_effect=check_cookie):
+ with mock.patch('requests.models.Response.content',
+ new_callable=mock.PropertyMock,
+ return_value="A"):
+ self.eb._download_client_certificates()
+
+ d = threads.deferToThread(wrapper)
+
+ return d
+
+ @deferred()
+ def test_run_eip_setup_checks(self):
+ self.eb._download_config = mock.MagicMock()
+ self.eb._download_client_certificates = mock.MagicMock()
+
+ d = self.eb.run_eip_setup_checks(ProviderConfig())
+
+ def check(*args):
+ self.eb._download_config.assert_called_once_with()
+ self.eb._download_client_certificates.assert_called_once_with(None)
+ d.addCallback(check)
+ return d
diff --git a/src/leap/services/eip/tests/test_eipconfig.py b/src/leap/services/eip/tests/test_eipconfig.py
new file mode 100644
index 00000000..87ce04c2
--- /dev/null
+++ b/src/leap/services/eip/tests/test_eipconfig.py
@@ -0,0 +1,324 @@
+# -*- coding: utf-8 -*-
+# test_eipconfig.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Tests for eipconfig
+"""
+import copy
+import json
+import os
+import unittest
+
+from leap.common.testing.basetest import BaseLeapTest
+from leap.services.eip.eipconfig import EIPConfig
+from leap.config.providerconfig import ProviderConfig
+
+from mock import Mock
+
+
+sample_config = {
+ "gateways": [
+ {
+ "capabilities": {
+ "adblock": False,
+ "filter_dns": True,
+ "limited": True,
+ "ports": [
+ "1194",
+ "443",
+ "53",
+ "80"],
+ "protocols": [
+ "tcp",
+ "udp"],
+ "transport": ["openvpn"],
+ "user_ips": False},
+ "host": "host.dev.example.org",
+ "ip_address": "11.22.33.44",
+ "location": "cyberspace"
+ }, {
+ "capabilities": {
+ "adblock": False,
+ "filter_dns": True,
+ "limited": True,
+ "ports": [
+ "1194",
+ "443",
+ "53",
+ "80"],
+ "protocols": [
+ "tcp",
+ "udp"],
+ "transport": ["openvpn"],
+ "user_ips": False},
+ "host": "host2.dev.example.org",
+ "ip_address": "22.33.44.55",
+ "location": "cyberspace"
+ }
+ ],
+ "locations": {
+ "ankara": {
+ "country_code": "XX",
+ "hemisphere": "S",
+ "name": "Antarctica",
+ "timezone": "+2"
+ },
+ "cyberspace": {
+ "country_code": "XX",
+ "hemisphere": "X",
+ "name": "outer space",
+ "timezone": ""
+ }
+ },
+ "openvpn_configuration": {
+ "auth": "SHA1",
+ "cipher": "AES-128-CBC",
+ "tls-cipher": "DHE-RSA-AES128-SHA"
+ },
+ "serial": 1,
+ "version": 1
+}
+
+
+class EIPConfigTest(BaseLeapTest):
+
+ __name__ = "eip_config_tests"
+
+ maxDiff = None
+
+ def setUp(self):
+ self._old_ospath_exists = os.path.exists
+
+ def tearDown(self):
+ os.path.exists = self._old_ospath_exists
+
+ def _write_config(self, data):
+ """
+ Helper to write some data to a temp config file.
+
+ :param data: data to be used to save in the config file.
+ :data type: dict (valid json)
+ """
+ self.configfile = os.path.join(self.tempdir, "eipconfig.json")
+ conf = open(self.configfile, "w")
+ conf.write(json.dumps(data))
+ conf.close()
+
+ def _get_eipconfig(self, fromfile=True, data=sample_config, api_ver='1'):
+ """
+ Helper that returns an EIPConfig object using the data parameter
+ or a sample data.
+
+ :param fromfile: sets if we should use a file or a string
+ :type fromfile: bool
+ :param data: sets the data to be used to load in the EIPConfig object
+ :type data: dict (valid json)
+ :param api_ver: the api_version schema to use.
+ :type api_ver: str
+ :rtype: EIPConfig
+ """
+ config = EIPConfig()
+ config.set_api_version(api_ver)
+
+ loaded = False
+ if fromfile:
+ self._write_config(data)
+ loaded = config.load(self.configfile, relative=False)
+ else:
+ json_string = json.dumps(data)
+ loaded = config.load(data=json_string)
+
+ if not loaded:
+ return None
+
+ return config
+
+ def test_loads_from_file(self):
+ config = self._get_eipconfig()
+ self.assertIsNotNone(config)
+
+ def test_loads_from_data(self):
+ config = self._get_eipconfig(fromfile=False)
+ self.assertIsNotNone(config)
+
+ def test_load_valid_config_from_file(self):
+ config = self._get_eipconfig()
+ self.assertIsNotNone(config)
+
+ self.assertEqual(
+ config.get_openvpn_configuration(),
+ sample_config["openvpn_configuration"])
+
+ sample_ip = sample_config["gateways"][0]["ip_address"]
+ self.assertEqual(
+ config.get_gateway_ip(),
+ sample_ip)
+ self.assertEqual(config.get_version(), sample_config["version"])
+ self.assertEqual(config.get_serial(), sample_config["serial"])
+ self.assertEqual(config.get_gateways(), sample_config["gateways"])
+ self.assertEqual(config.get_locations(), sample_config["locations"])
+ self.assertEqual(config.get_clusters(), None)
+
+ def test_load_valid_config_from_data(self):
+ config = self._get_eipconfig(fromfile=False)
+ self.assertIsNotNone(config)
+
+ self.assertEqual(
+ config.get_openvpn_configuration(),
+ sample_config["openvpn_configuration"])
+
+ sample_ip = sample_config["gateways"][0]["ip_address"]
+ self.assertEqual(
+ config.get_gateway_ip(),
+ sample_ip)
+
+ self.assertEqual(config.get_version(), sample_config["version"])
+ self.assertEqual(config.get_serial(), sample_config["serial"])
+ self.assertEqual(config.get_gateways(), sample_config["gateways"])
+ self.assertEqual(config.get_locations(), sample_config["locations"])
+ self.assertEqual(config.get_clusters(), None)
+
+ def test_sanitize_extra_parameters(self):
+ data = copy.deepcopy(sample_config)
+ data['openvpn_configuration']["extra_param"] = "FOO"
+ config = self._get_eipconfig(data=data)
+
+ self.assertEqual(
+ config.get_openvpn_configuration(),
+ sample_config["openvpn_configuration"])
+
+ def test_sanitize_non_allowed_chars(self):
+ data = copy.deepcopy(sample_config)
+ data['openvpn_configuration']["auth"] = "SHA1;"
+ config = self._get_eipconfig(data=data)
+
+ self.assertEqual(
+ config.get_openvpn_configuration(),
+ sample_config["openvpn_configuration"])
+
+ data = copy.deepcopy(sample_config)
+ data['openvpn_configuration']["auth"] = "SHA1>`&|"
+ config = self._get_eipconfig(data=data)
+
+ self.assertEqual(
+ config.get_openvpn_configuration(),
+ sample_config["openvpn_configuration"])
+
+ def test_sanitize_lowercase(self):
+ data = copy.deepcopy(sample_config)
+ data['openvpn_configuration']["auth"] = "shaSHA1"
+ config = self._get_eipconfig(data=data)
+
+ self.assertEqual(
+ config.get_openvpn_configuration(),
+ sample_config["openvpn_configuration"])
+
+ def test_all_characters_invalid(self):
+ data = copy.deepcopy(sample_config)
+ data['openvpn_configuration']["auth"] = "sha&*!@#;"
+ config = self._get_eipconfig(data=data)
+
+ self.assertEqual(
+ config.get_openvpn_configuration(),
+ {'cipher': 'AES-128-CBC',
+ 'tls-cipher': 'DHE-RSA-AES128-SHA'})
+
+ def test_sanitize_bad_ip(self):
+ data = copy.deepcopy(sample_config)
+ data['gateways'][0]["ip_address"] = "11.22.33.44;"
+ config = self._get_eipconfig(data=data)
+
+ self.assertEqual(config.get_gateway_ip(), None)
+
+ data = copy.deepcopy(sample_config)
+ data['gateways'][0]["ip_address"] = "11.22.33.44`"
+ config = self._get_eipconfig(data=data)
+
+ self.assertEqual(config.get_gateway_ip(), None)
+
+ def test_default_gateway_on_unknown_index(self):
+ config = self._get_eipconfig()
+ sample_ip = sample_config["gateways"][0]["ip_address"]
+ self.assertEqual(config.get_gateway_ip(999), sample_ip)
+
+ def test_get_gateway_by_index(self):
+ config = self._get_eipconfig()
+ sample_ip_0 = sample_config["gateways"][0]["ip_address"]
+ sample_ip_1 = sample_config["gateways"][1]["ip_address"]
+ self.assertEqual(config.get_gateway_ip(0), sample_ip_0)
+ self.assertEqual(config.get_gateway_ip(1), sample_ip_1)
+
+ def test_get_client_cert_path_as_expected(self):
+ config = self._get_eipconfig()
+ config.get_path_prefix = Mock(return_value='test')
+
+ provider_config = ProviderConfig()
+
+ # mock 'get_domain' so we don't need to load a config
+ provider_domain = 'test.provider.com'
+ provider_config.get_domain = Mock(return_value=provider_domain)
+
+ expected_path = os.path.join('test', 'leap', 'providers',
+ provider_domain, 'keys', 'client',
+ 'openvpn.pem')
+
+ # mock 'os.path.exists' so we don't get an error for unexisting file
+ os.path.exists = Mock(return_value=True)
+ cert_path = config.get_client_cert_path(provider_config)
+
+ self.assertEqual(cert_path, expected_path)
+
+ def test_get_client_cert_path_about_to_download(self):
+ config = self._get_eipconfig()
+ config.get_path_prefix = Mock(return_value='test')
+
+ provider_config = ProviderConfig()
+
+ # mock 'get_domain' so we don't need to load a config
+ provider_domain = 'test.provider.com'
+ provider_config.get_domain = Mock(return_value=provider_domain)
+
+ expected_path = os.path.join('test', 'leap', 'providers',
+ provider_domain, 'keys', 'client',
+ 'openvpn.pem')
+
+ cert_path = config.get_client_cert_path(
+ provider_config, about_to_download=True)
+
+ self.assertEqual(cert_path, expected_path)
+
+ def test_get_client_cert_path_fails(self):
+ config = self._get_eipconfig()
+ provider_config = ProviderConfig()
+
+ # mock 'get_domain' so we don't need to load a config
+ provider_domain = 'test.provider.com'
+ provider_config.get_domain = Mock(return_value=provider_domain)
+
+ with self.assertRaises(AssertionError):
+ config.get_client_cert_path(provider_config)
+
+ def test_fails_without_api_set(self):
+ config = EIPConfig()
+ with self.assertRaises(AssertionError):
+ config.load('non-relevant-path')
+
+ def test_fails_with_api_without_schema(self):
+ with self.assertRaises(AssertionError):
+ self._get_eipconfig(api_ver='123')
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/leap/services/eip/tests/test_providerbootstrapper.py b/src/leap/services/eip/tests/test_providerbootstrapper.py
new file mode 100644
index 00000000..b24334a2
--- /dev/null
+++ b/src/leap/services/eip/tests/test_providerbootstrapper.py
@@ -0,0 +1,531 @@
+# -*- coding: utf-8 -*-
+# test_providerbootstrapper.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+"""
+Tests for the Provider Boostrapper checks
+
+These will be whitebox tests since we want to make sure the private
+implementation is checking what we expect.
+"""
+
+import os
+import mock
+import socket
+import stat
+import tempfile
+import time
+import requests
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+from nose.twistedtools import deferred, reactor
+from twisted.internet import threads
+from requests.models import Response
+
+from leap.common.testing.https_server import where
+from leap.common.testing.basetest import BaseLeapTest
+from leap.services.eip.providerbootstrapper import ProviderBootstrapper
+from leap.services.eip.providerbootstrapper import UnsupportedProviderAPI
+from leap.services.eip.providerbootstrapper import WrongFingerprint
+from leap.provider.supportedapis import SupportedAPIs
+from leap.config.providerconfig import ProviderConfig
+from leap.crypto.tests import fake_provider
+from leap.common.files import mkdir_p
+
+
+class ProviderBootstrapperTest(BaseLeapTest):
+ def setUp(self):
+ self.pb = ProviderBootstrapper()
+
+ def tearDown(self):
+ pass
+
+ def test_name_resolution_check(self):
+ # Something highly likely to success
+ self.pb._domain = "google.com"
+ self.pb._check_name_resolution()
+ # Something highly likely to fail
+ self.pb._domain = "uquhqweuihowquie.abc.def"
+ with self.assertRaises(socket.gaierror):
+ self.pb._check_name_resolution()
+
+ @deferred()
+ def test_run_provider_select_checks(self):
+ self.pb._check_name_resolution = mock.MagicMock()
+ self.pb._check_https = mock.MagicMock()
+ self.pb._download_provider_info = mock.MagicMock()
+
+ d = self.pb.run_provider_select_checks("somedomain")
+
+ def check(*args):
+ self.pb._check_name_resolution.assert_called_once_with()
+ self.pb._check_https.assert_called_once_with(None)
+ self.pb._download_provider_info.assert_called_once_with(None)
+ d.addCallback(check)
+ return d
+
+ @deferred()
+ def test_run_provider_setup_checks(self):
+ self.pb._download_ca_cert = mock.MagicMock()
+ self.pb._check_ca_fingerprint = mock.MagicMock()
+ self.pb._check_api_certificate = mock.MagicMock()
+
+ d = self.pb.run_provider_setup_checks(ProviderConfig())
+
+ def check(*args):
+ self.pb._download_ca_cert.assert_called_once_with()
+ self.pb._check_ca_fingerprint.assert_called_once_with(None)
+ self.pb._check_api_certificate.assert_called_once_with(None)
+ d.addCallback(check)
+ return d
+
+ def test_should_proceed_cert(self):
+ self.pb._provider_config = mock.Mock()
+ self.pb._provider_config.get_ca_cert_path = mock.MagicMock(
+ return_value=where("cacert.pem"))
+
+ self.pb._download_if_needed = False
+ self.assertTrue(self.pb._should_proceed_cert())
+
+ self.pb._download_if_needed = True
+ self.assertFalse(self.pb._should_proceed_cert())
+
+ self.pb._provider_config.get_ca_cert_path = mock.MagicMock(
+ return_value=where("somefilethatdoesntexist.pem"))
+ self.assertTrue(self.pb._should_proceed_cert())
+
+ def _check_download_ca_cert(self, should_proceed):
+ """
+ Helper to check different paths easily for the download ca
+ cert check
+
+ :param should_proceed: sets the _should_proceed_cert in the
+ provider bootstrapper being tested
+ :type should_proceed: bool
+
+ :returns: The contents of the certificate, the expected
+ content depending on should_proceed, and the mode of
+ the file to be checked by the caller
+ :rtype: tuple of str, str, int
+ """
+ old_content = "NOT THE NEW CERT"
+ new_content = "NEW CERT"
+ new_cert_path = os.path.join(tempfile.mkdtemp(),
+ "mynewcert.pem")
+
+ with open(new_cert_path, "w") as c:
+ c.write(old_content)
+
+ self.pb._provider_config = mock.Mock()
+ self.pb._provider_config.get_ca_cert_path = mock.MagicMock(
+ return_value=new_cert_path)
+ self.pb._domain = "somedomain"
+
+ self.pb._should_proceed_cert = mock.MagicMock(
+ return_value=should_proceed)
+
+ read = None
+ content_to_check = None
+ mode = None
+
+ with mock.patch('requests.models.Response.content',
+ new_callable=mock.PropertyMock) as \
+ content:
+ content.return_value = new_content
+ response_obj = Response()
+ response_obj.raise_for_status = mock.MagicMock()
+
+ self.pb._session.get = mock.MagicMock(return_value=response_obj)
+ self.pb._download_ca_cert()
+ with open(new_cert_path, "r") as nc:
+ read = nc.read()
+ if should_proceed:
+ content_to_check = new_content
+ else:
+ content_to_check = old_content
+ mode = stat.S_IMODE(os.stat(new_cert_path).st_mode)
+
+ os.unlink(new_cert_path)
+ return read, content_to_check, mode
+
+ def test_download_ca_cert_no_saving(self):
+ read, expected_read, mode = self._check_download_ca_cert(False)
+ self.assertEqual(read, expected_read)
+ self.assertEqual(mode, int("600", 8))
+
+ def test_download_ca_cert_saving(self):
+ read, expected_read, mode = self._check_download_ca_cert(True)
+ self.assertEqual(read, expected_read)
+ self.assertEqual(mode, int("600", 8))
+
+ def test_check_ca_fingerprint_skips(self):
+ self.pb._provider_config = mock.Mock()
+ self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock(
+ return_value="")
+ self.pb._domain = "somedomain"
+
+ self.pb._should_proceed_cert = mock.MagicMock(return_value=False)
+
+ self.pb._check_ca_fingerprint()
+ self.assertFalse(self.pb._provider_config.
+ get_ca_cert_fingerprint.called)
+
+ def test_check_ca_cert_fingerprint_raises_bad_format(self):
+ self.pb._provider_config = mock.Mock()
+ self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock(
+ return_value="wrongfprformat!!")
+ self.pb._domain = "somedomain"
+
+ self.pb._should_proceed_cert = mock.MagicMock(return_value=True)
+
+ with self.assertRaises(WrongFingerprint):
+ self.pb._check_ca_fingerprint()
+
+ # This two hashes different in the last byte, but that's good enough
+ # for the tests
+ KNOWN_BAD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034efe" \
+ "7dd1b910062ca323eb4da5c7f"
+ KNOWN_GOOD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034ef" \
+ "e7dd1b910062ca323eb4da5c7e"
+ KNOWN_GOOD_CERT = """
+-----BEGIN CERTIFICATE-----
+MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt
+YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v
+Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw
+FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV
+BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
+ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai
+dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB
+7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84
+CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+
+znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4
+MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4
+lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0
+bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl
+DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB
+lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy
+YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw
+XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE
+MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w
+DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl
+cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY
+k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj
+RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG
+htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX
+EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J
+aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l
+mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK
+G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co
+Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d
+69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e
+yV8e
+-----END CERTIFICATE-----
+"""
+
+ def _prepare_provider_config_with(self, cert_path, cert_hash):
+ """
+ Mocks the provider config to give the cert_path and cert_hash
+ specified
+
+ :param cert_path: path for the certificate
+ :type cert_path: str
+ :param cert_hash: hash for the certificate as it would appear
+ in the provider config json
+ :type cert_hash: str
+ """
+ self.pb._provider_config = mock.Mock()
+ self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock(
+ return_value=cert_hash)
+ self.pb._provider_config.get_ca_cert_path = mock.MagicMock(
+ return_value=cert_path)
+ self.pb._domain = "somedomain"
+
+ def test_check_ca_fingerprint_checksout(self):
+ cert_path = os.path.join(tempfile.mkdtemp(),
+ "mynewcert.pem")
+
+ with open(cert_path, "w") as c:
+ c.write(self.KNOWN_GOOD_CERT)
+
+ self._prepare_provider_config_with(cert_path, self.KNOWN_GOOD_HASH)
+
+ self.pb._should_proceed_cert = mock.MagicMock(return_value=True)
+
+ self.pb._check_ca_fingerprint()
+
+ os.unlink(cert_path)
+
+ def test_check_ca_fingerprint_fails(self):
+ cert_path = os.path.join(tempfile.mkdtemp(),
+ "mynewcert.pem")
+
+ with open(cert_path, "w") as c:
+ c.write(self.KNOWN_GOOD_CERT)
+
+ self._prepare_provider_config_with(cert_path, self.KNOWN_BAD_HASH)
+
+ self.pb._should_proceed_cert = mock.MagicMock(return_value=True)
+
+ with self.assertRaises(WrongFingerprint):
+ self.pb._check_ca_fingerprint()
+
+ os.unlink(cert_path)
+
+
+###############################################################################
+# Tests with a fake provider #
+###############################################################################
+
+class ProviderBootstrapperActiveTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ factory = fake_provider.get_provider_factory()
+ http = reactor.listenTCP(8002, factory)
+ https = reactor.listenSSL(
+ 0, factory,
+ fake_provider.OpenSSLServerContextFactory())
+ get_port = lambda p: p.getHost().port
+ cls.http_port = get_port(http)
+ cls.https_port = get_port(https)
+
+ def setUp(self):
+ self.pb = ProviderBootstrapper()
+
+ # At certain points we are going to be replacing these methods
+ # directly in ProviderConfig to be able to catch calls from
+ # new ProviderConfig objects inside the methods tested. We
+ # need to save the old implementation and restore it in
+ # tearDown so we are sure everything is as expected for each
+ # test. If we do it inside each specific test, a failure in
+ # the test will leave the implementation with the mock.
+ self.old_gpp = ProviderConfig.get_path_prefix
+ self.old_load = ProviderConfig.load
+ self.old_save = ProviderConfig.save
+ self.old_api_version = ProviderConfig.get_api_version
+
+ def tearDown(self):
+ ProviderConfig.get_path_prefix = self.old_gpp
+ ProviderConfig.load = self.old_load
+ ProviderConfig.save = self.old_save
+ ProviderConfig.get_api_version = self.old_api_version
+
+ def test_check_https_succeeds(self):
+ # XXX: Need a proper CA signed cert to test this
+ pass
+
+ @deferred()
+ def test_check_https_fails(self):
+ self.pb._domain = "localhost:%s" % (self.https_port,)
+
+ def check(*args):
+ with self.assertRaises(requests.exceptions.SSLError):
+ self.pb._check_https()
+ return threads.deferToThread(check)
+
+ @deferred()
+ def test_second_check_https_fails(self):
+ self.pb._domain = "localhost:1234"
+
+ def check(*args):
+ with self.assertRaises(Exception):
+ self.pb._check_https()
+ return threads.deferToThread(check)
+
+ @deferred()
+ def test_check_https_succeeds_if_danger(self):
+ self.pb._domain = "localhost:%s" % (self.https_port,)
+ self.pb._bypass_checks = True
+
+ def check(*args):
+ self.pb._check_https()
+
+ return threads.deferToThread(check)
+
+ def _setup_provider_config_with(self, api, path_prefix):
+ """
+ Sets up the ProviderConfig with mocks for the path prefix, the
+ api returned and load/save methods.
+ It modifies ProviderConfig directly instead of an object
+ because the object used is created in the method itself and we
+ cannot control that.
+
+ :param api: API to return
+ :type api: str
+ :param path_prefix: path prefix to be used when calculating
+ paths
+ :type path_prefix: str
+ """
+ ProviderConfig.get_path_prefix = mock.MagicMock(
+ return_value=path_prefix)
+ ProviderConfig.get_api_version = mock.MagicMock(
+ return_value=api)
+ ProviderConfig.load = mock.MagicMock()
+ ProviderConfig.save = mock.MagicMock()
+
+ def _setup_providerbootstrapper(self, ifneeded):
+ """
+ Sets the provider bootstrapper's domain to
+ localhost:https_port, sets it to bypass https checks and sets
+ the download if needed based on the ifneeded value.
+
+ :param ifneeded: Value for _download_if_needed
+ :type ifneeded: bool
+ """
+ self.pb._domain = "localhost:%s" % (self.https_port,)
+ self.pb._bypass_checks = True
+ self.pb._download_if_needed = ifneeded
+
+ def _produce_dummy_provider_json(self):
+ """
+ Creates a dummy provider json on disk in order to test
+ behaviour around it (download if newer online, etc)
+
+ :returns: the provider.json path used
+ :rtype: str
+ """
+ provider_dir = os.path.join(ProviderConfig()
+ .get_path_prefix(),
+ "leap",
+ "providers",
+ self.pb._domain)
+ mkdir_p(provider_dir)
+ provider_path = os.path.join(provider_dir,
+ "provider.json")
+
+ with open(provider_path, "w") as p:
+ p.write("A")
+ return provider_path
+
+ def test_download_provider_info_new_provider(self):
+ self._setup_provider_config_with("1", tempfile.mkdtemp())
+ self._setup_providerbootstrapper(True)
+
+ self.pb._download_provider_info()
+ self.assertTrue(ProviderConfig.save.called)
+
+ @mock.patch('leap.config.providerconfig.ProviderConfig.get_ca_cert_path',
+ lambda x: where('cacert.pem'))
+ def test_download_provider_info_not_modified(self):
+ self._setup_provider_config_with("1", tempfile.mkdtemp())
+ self._setup_providerbootstrapper(True)
+ provider_path = self._produce_dummy_provider_json()
+
+ # set mtime to something really new
+ os.utime(provider_path, (-1, time.time()))
+
+ with mock.patch.object(
+ ProviderConfig, 'get_api_uri',
+ return_value="https://localhost:%s" % (self.https_port,)):
+ self.pb._download_provider_info()
+ # we check that it doesn't save the provider
+ # config, because it's new enough
+ self.assertFalse(ProviderConfig.save.called)
+
+ @mock.patch('leap.config.providerconfig.ProviderConfig.get_ca_cert_path',
+ lambda x: where('cacert.pem'))
+ def test_download_provider_info_modified(self):
+ self._setup_provider_config_with("1", tempfile.mkdtemp())
+ self._setup_providerbootstrapper(True)
+ provider_path = self._produce_dummy_provider_json()
+
+ # set mtime to something really old
+ os.utime(provider_path, (-1, 100))
+
+ with mock.patch.object(
+ ProviderConfig, 'get_api_uri',
+ return_value="https://localhost:%s" % (self.https_port,)):
+ self.pb._download_provider_info()
+ self.assertTrue(ProviderConfig.load.called)
+ self.assertTrue(ProviderConfig.save.called)
+
+ @mock.patch('leap.config.providerconfig.ProviderConfig.get_ca_cert_path',
+ lambda x: where('cacert.pem'))
+ def test_download_provider_info_unsupported_api_raises(self):
+ self._setup_provider_config_with("9999999", tempfile.mkdtemp())
+ self._setup_providerbootstrapper(False)
+ self._produce_dummy_provider_json()
+
+ with mock.patch.object(
+ ProviderConfig, 'get_api_uri',
+ return_value="https://localhost:%s" % (self.https_port,)):
+ with self.assertRaises(UnsupportedProviderAPI):
+ self.pb._download_provider_info()
+
+ @mock.patch('leap.config.providerconfig.ProviderConfig.get_ca_cert_path',
+ lambda x: where('cacert.pem'))
+ def test_download_provider_info_unsupported_api(self):
+ self._setup_provider_config_with(SupportedAPIs.SUPPORTED_APIS[0],
+ tempfile.mkdtemp())
+ self._setup_providerbootstrapper(False)
+ self._produce_dummy_provider_json()
+
+ with mock.patch.object(
+ ProviderConfig, 'get_api_uri',
+ return_value="https://localhost:%s" % (self.https_port,)):
+ self.pb._download_provider_info()
+
+ @mock.patch('leap.config.providerconfig.ProviderConfig.get_api_uri',
+ lambda x: 'api.uri')
+ @mock.patch('leap.config.providerconfig.ProviderConfig.get_ca_cert_path',
+ lambda x: '/cert/path')
+ def test_check_api_certificate_skips(self):
+ self.pb._provider_config = ProviderConfig()
+ self.pb._session.get = mock.MagicMock(return_value=Response())
+
+ self.pb._should_proceed_cert = mock.MagicMock(return_value=False)
+ self.pb._check_api_certificate()
+ self.assertFalse(self.pb._session.get.called)
+
+ @deferred()
+ def test_check_api_certificate_fails(self):
+ self.pb._provider_config = ProviderConfig()
+ self.pb._provider_config.get_api_uri = mock.MagicMock(
+ return_value="https://localhost:%s" % (self.https_port,))
+ self.pb._provider_config.get_ca_cert_path = mock.MagicMock(
+ return_value=os.path.join(
+ os.path.split(__file__)[0],
+ "wrongcert.pem"))
+ self.pb._provider_config.get_api_version = mock.MagicMock(
+ return_value="1")
+
+ self.pb._should_proceed_cert = mock.MagicMock(return_value=True)
+
+ def check(*args):
+ with self.assertRaises(requests.exceptions.SSLError):
+ self.pb._check_api_certificate()
+ d = threads.deferToThread(check)
+ return d
+
+ @deferred()
+ def test_check_api_certificate_succeeds(self):
+ self.pb._provider_config = ProviderConfig()
+ self.pb._provider_config.get_api_uri = mock.MagicMock(
+ return_value="https://localhost:%s" % (self.https_port,))
+ self.pb._provider_config.get_ca_cert_path = mock.MagicMock(
+ return_value=where('cacert.pem'))
+ self.pb._provider_config.get_api_version = mock.MagicMock(
+ return_value="1")
+
+ self.pb._should_proceed_cert = mock.MagicMock(return_value=True)
+
+ def check(*args):
+ self.pb._check_api_certificate()
+ d = threads.deferToThread(check)
+ return d
diff --git a/src/leap/services/eip/tests/test_vpngatewayselector.py b/src/leap/services/eip/tests/test_vpngatewayselector.py
new file mode 100644
index 00000000..c90681d7
--- /dev/null
+++ b/src/leap/services/eip/tests/test_vpngatewayselector.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+# test_vpngatewayselector.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+tests for vpngatewayselector
+"""
+
+import unittest
+
+from leap.services.eip.eipconfig import EIPConfig, VPNGatewaySelector
+from leap.common.testing.basetest import BaseLeapTest
+from mock import Mock
+
+
+sample_gateways = [
+ {u'host': u'gateway1.com',
+ u'ip_address': u'1.2.3.4',
+ u'location': u'location1'},
+ {u'host': u'gateway2.com',
+ u'ip_address': u'2.3.4.5',
+ u'location': u'location2'},
+ {u'host': u'gateway3.com',
+ u'ip_address': u'3.4.5.6',
+ u'location': u'location3'},
+ {u'host': u'gateway4.com',
+ u'ip_address': u'4.5.6.7',
+ u'location': u'location4'}
+]
+
+sample_gateways_no_location = [
+ {u'host': u'gateway1.com',
+ u'ip_address': u'1.2.3.4'},
+ {u'host': u'gateway2.com',
+ u'ip_address': u'2.3.4.5'},
+ {u'host': u'gateway3.com',
+ u'ip_address': u'3.4.5.6'}
+]
+
+sample_locations = {
+ u'location1': {u'timezone': u'2'},
+ u'location2': {u'timezone': u'-7'},
+ u'location3': {u'timezone': u'-4'},
+ u'location4': {u'timezone': u'+13'}
+}
+
+# 0 is not used, only for indexing from 1 in tests
+ips = (0, u'1.2.3.4', u'2.3.4.5', u'3.4.5.6', u'4.5.6.7')
+
+
+class VPNGatewaySelectorTest(BaseLeapTest):
+ """
+ VPNGatewaySelector's tests.
+ """
+ def setUp(self):
+ self.eipconfig = EIPConfig()
+ self.eipconfig.get_gateways = Mock(return_value=sample_gateways)
+ self.eipconfig.get_locations = Mock(return_value=sample_locations)
+
+ def tearDown(self):
+ pass
+
+ def test_get_no_gateways(self):
+ gateway_selector = VPNGatewaySelector(self.eipconfig)
+ self.eipconfig.get_gateways = Mock(return_value=[])
+ gateways = gateway_selector.get_gateways()
+ self.assertEqual(gateways, [])
+
+ def test_get_gateway_with_no_locations(self):
+ gateway_selector = VPNGatewaySelector(self.eipconfig)
+ self.eipconfig.get_gateways = Mock(
+ return_value=sample_gateways_no_location)
+ self.eipconfig.get_locations = Mock(return_value=[])
+ gateways = gateway_selector.get_gateways()
+ gateways_default_order = [
+ sample_gateways[0]['ip_address'],
+ sample_gateways[1]['ip_address'],
+ sample_gateways[2]['ip_address']
+ ]
+ self.assertEqual(gateways, gateways_default_order)
+
+ def test_correct_order_gmt(self):
+ gateway_selector = VPNGatewaySelector(self.eipconfig, 0)
+ gateways = gateway_selector.get_gateways()
+ self.assertEqual(gateways, [ips[1], ips[3], ips[2], ips[4]])
+
+ def test_correct_order_gmt_minus_3(self):
+ gateway_selector = VPNGatewaySelector(self.eipconfig, -3)
+ gateways = gateway_selector.get_gateways()
+ self.assertEqual(gateways, [ips[3], ips[2], ips[1], ips[4]])
+
+ def test_correct_order_gmt_minus_7(self):
+ gateway_selector = VPNGatewaySelector(self.eipconfig, -7)
+ gateways = gateway_selector.get_gateways()
+ self.assertEqual(gateways, [ips[2], ips[3], ips[4], ips[1]])
+
+ def test_correct_order_gmt_plus_5(self):
+ gateway_selector = VPNGatewaySelector(self.eipconfig, 5)
+ gateways = gateway_selector.get_gateways()
+ self.assertEqual(gateways, [ips[1], ips[4], ips[3], ips[2]])
+
+ def test_correct_order_gmt_plus_12(self):
+ gateway_selector = VPNGatewaySelector(self.eipconfig, 12)
+ gateways = gateway_selector.get_gateways()
+ self.assertEqual(gateways, [ips[4], ips[2], ips[3], ips[1]])
+
+ def test_correct_order_gmt_minus_11(self):
+ gateway_selector = VPNGatewaySelector(self.eipconfig, -11)
+ gateways = gateway_selector.get_gateways()
+ self.assertEqual(gateways, [ips[4], ips[2], ips[3], ips[1]])
+
+ def test_correct_order_gmt_plus_14(self):
+ gateway_selector = VPNGatewaySelector(self.eipconfig, 14)
+ gateways = gateway_selector.get_gateways()
+ self.assertEqual(gateways, [ips[4], ips[2], ips[3], ips[1]])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/leap/services/eip/tests/wrongcert.pem b/src/leap/services/eip/tests/wrongcert.pem
new file mode 100644
index 00000000..e6cff38a
--- /dev/null
+++ b/src/leap/services/eip/tests/wrongcert.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFtTCCA52gAwIBAgIJAIWZus5EIXNtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTMwNjI1MTc0NjExWhcNMTgwNjI1MTc0NjExWjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEA2ObM7ESjyuxFZYD/Y68qOPQgjgggW+cdXfBpU2p4n7clsrUeMhWdW40Y
+77Phzor9VOeqs3ZpHuyLzsYVp/kFDm8tKyo2ah5fJwzL0VCSLYaZkUQQ7GNUmTCk
+furaxl8cQx/fg395V7/EngsS9B3/y5iHbctbA4MnH3jaotO5EGeo6hw7/eyCotQ9
+KbBV9GJMcY94FsXBCmUB+XypKklWTLhSaS6Cu4Fo8YLW6WmcnsyEOGS2F7WVf5at
+7CBWFQZHaSgIBLmc818/mDYCnYmCVMFn/6Ndx7V2NTlz+HctWrQn0dmIOnCUeCwS
+wXq9PnBR1rSx/WxwyF/WpyjOFkcIo7vm72kS70pfrYsXcZD4BQqkXYj3FyKnPt3O
+ibLKtCxL8/83wOtErPcYpG6LgFkgAAlHQ9MkUi5dbmjCJtpqQmlZeK1RALdDPiB3
+K1KZimrGsmcE624dJxUIOJJpuwJDy21F8kh5ZAsAtE1prWETrQYNElNFjQxM83rS
+ZR1Ql2MPSB4usEZT57+KvpEzlOnAT3elgCg21XrjSFGi14hCEao4g2OEZH5GAwm5
+frf6UlSRZ/g3tLTfI8Hv1prw15W2qO+7q7SBAplTODCRk+Yb0YoA2mMM/QXBUcXs
+vKEDLSSxzNIBi3T62l39RB/ml+gPKo87ZMDivex1ZhrcJc3Yu3sCAwEAAaOBpzCB
+pDAdBgNVHQ4EFgQUPjE+4pun+8FreIdpoR8v6N7xKtUwdQYDVR0jBG4wbIAUPjE+
+4pun+8FreIdpoR8v6N7xKtWhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT
+b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCF
+mbrORCFzbTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCpvCPdtvXJ
+muTj379TZuCJs7/l0FhA7AHa1WAlHjsXHaA7N0+3ZWAbdtXDsowal6S+ldgU/kfV
+Lq7NrRq+amJWC7SYj6cvVwhrSwSvu01fe/TWuOzHrRv1uTfJ/VXLonVufMDd9opo
+bhqYxMaxLdIx6t/MYmZH4Wpiq0yfZuv//M8i7BBl/qvaWbLhg0yVAKRwjFvf59h6
+6tRFCLddELOIhLDQtk8zMbioPEbfAlKdwwP8kYGtDGj6/9/YTd/oTKRdgHuwyup3
+m0L20Y6LddC+tb0WpK5EyrNbCbEqj1L4/U7r6f/FKNA3bx6nfdXbscaMfYonKAKg
+1cRrRg45sErmCz0QyTnWzXyvbjR4oQRzyW3kJ1JZudZ+AwOi00J5FYa3NiLuxl1u
+gIGKWSrASQWhEdpa1nlCgX7PhdaQgYjEMpQvA0GCA0OF5JDu8en1yZqsOt1hCLIN
+lkz/5jKPqrclY5hV99bE3hgCHRmIPNHCZG3wbZv2yJKxJX1YLMmQwAmSh2N7YwGG
+yXRvCxQs5ChPHyRairuf/5MZCZnSVb45ppTVuNUijsbflKRUgfj/XvfqQ22f+C9N
+Om2dmNvAiS2TOIfuP47CF2OUa5q4plUwmr+nyXQGM0SIoHNCj+MBdFfb3oxxAtI+
+SLhbnzQv5e84Doqz3YF0XW8jyR7q8GFLNA==
+-----END CERTIFICATE-----
diff --git a/src/leap/services/eip/udstelnet.py b/src/leap/services/eip/udstelnet.py
new file mode 100644
index 00000000..e6c82350
--- /dev/null
+++ b/src/leap/services/eip/udstelnet.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# udstelnet.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import socket
+import telnetlib
+
+
+class ConnectionRefusedError(Exception):
+ pass
+
+
+class MissingSocketError(Exception):
+ pass
+
+
+class UDSTelnet(telnetlib.Telnet):
+ """
+ A telnet-alike class, that can listen on unix domain sockets
+ """
+
+ def open(self, host, port=23, timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
+ """
+ Connect to a host. If port is 'unix', it will open a
+ connection over unix docmain sockets.
+
+ The optional second argument is the port number, which
+ defaults to the standard telnet port (23).
+ Don't try to reopen an already connected instance.
+ """
+ self.eof = 0
+ self.host = host
+ self.port = port
+ self.timeout = timeout
+
+ if self.port == "unix":
+ # unix sockets spoken
+ if not os.path.exists(self.host):
+ raise MissingSocketError()
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ try:
+ self.sock.connect(self.host)
+ except socket.error:
+ raise ConnectionRefusedError()
+ else:
+ self.sock = socket.create_connection((host, port), timeout)
diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py
new file mode 100644
index 00000000..17950a25
--- /dev/null
+++ b/src/leap/services/eip/vpnlaunchers.py
@@ -0,0 +1,927 @@
+# -*- coding: utf-8 -*-
+# vpnlaunchers.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Platform dependant VPN launchers
+"""
+import commands
+import logging
+import getpass
+import os
+import platform
+import subprocess
+import stat
+try:
+ import grp
+except ImportError:
+ pass # ignore, probably windows
+
+from abc import ABCMeta, abstractmethod
+from functools import partial
+
+from leap.common.check import leap_assert, leap_assert_type
+from leap.common.files import which
+from leap.config.providerconfig import ProviderConfig
+from leap.services.eip.eipconfig import EIPConfig, VPNGatewaySelector
+from leap.util import first
+from leap.util.privilege_policies import LinuxPolicyChecker
+from leap.util import privilege_policies
+
+logger = logging.getLogger(__name__)
+
+
+class VPNLauncherException(Exception):
+ pass
+
+
+class OpenVPNNotFoundException(VPNLauncherException):
+ pass
+
+
+class EIPNoPolkitAuthAgentAvailable(VPNLauncherException):
+ pass
+
+
+class EIPNoPkexecAvailable(VPNLauncherException):
+ pass
+
+
+class EIPNoTunKextLoaded(VPNLauncherException):
+ pass
+
+
+class VPNLauncher(object):
+ """
+ Abstract launcher class
+ """
+ __metaclass__ = ABCMeta
+
+ UPDOWN_FILES = None
+ OTHER_FILES = None
+
+ @abstractmethod
+ def get_vpn_command(self, eipconfig=None, providerconfig=None,
+ socket_host=None, socket_port=None):
+ """
+ Returns the platform dependant vpn launching command
+
+ :param eipconfig: eip configuration object
+ :type eipconfig: EIPConfig
+ :param providerconfig: provider specific configuration
+ :type providerconfig: ProviderConfig
+ :param socket_host: either socket path (unix) or socket IP
+ :type socket_host: str
+ :param socket_port: either string "unix" if it's a unix
+ socket, or port otherwise
+ :type socket_port: str
+
+ :return: A VPN command ready to be launched
+ :rtype: list
+ """
+ return []
+
+ @abstractmethod
+ def get_vpn_env(self, providerconfig):
+ """
+ Returns a dictionary with the custom env for the platform.
+ This is mainly used for setting LD_LIBRARY_PATH to the correct
+ path when distributing a standalone client
+
+ :param providerconfig: provider specific configuration
+ :type providerconfig: ProviderConfig
+
+ :rtype: dict
+ """
+ return {}
+
+ @classmethod
+ def missing_updown_scripts(kls):
+ """
+ Returns what updown scripts are missing.
+ :rtype: list
+ """
+ leap_assert(kls.UPDOWN_FILES is not None,
+ "Need to define UPDOWN_FILES for this particular "
+ "auncher before calling this method")
+ file_exist = partial(_has_updown_scripts, warn=False)
+ zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES))
+ missing = filter(lambda (path, exists): exists is False, zipped)
+ return [path for path, exists in missing]
+
+ @classmethod
+ def missing_other_files(kls):
+ """
+ Returns what other important files are missing during startup.
+ Same as missing_updown_scripts but does not check for exec bit.
+ :rtype: list
+ """
+ leap_assert(kls.UPDOWN_FILES is not None,
+ "Need to define OTHER_FILES for this particular "
+ "auncher before calling this method")
+ file_exist = partial(_has_other_files, warn=False)
+ zipped = zip(kls.OTHER_FILES, map(file_exist, kls.OTHER_FILES))
+ missing = filter(lambda (path, exists): exists is False, zipped)
+ return [path for path, exists in missing]
+
+
+def get_platform_launcher():
+ launcher = globals()[platform.system() + "VPNLauncher"]
+ leap_assert(launcher, "Unimplemented platform launcher: %s" %
+ (platform.system(),))
+ return launcher()
+
+
+def _is_pkexec_in_system():
+ """
+ Checks the existence of the pkexec binary in system.
+ """
+ pkexec_path = which('pkexec')
+ if len(pkexec_path) == 0:
+ return False
+ return True
+
+
+def _has_updown_scripts(path, warn=True):
+ """
+ Checks the existence of the up/down scripts and its
+ exec bit if applicable.
+
+ :param path: the path to be checked
+ :type path: str
+
+ :param warn: whether we should log the absence
+ :type warn: bool
+
+ :rtype: bool
+ """
+ is_file = os.path.isfile(path)
+ if warn and not is_file:
+ logger.error("Could not find up/down script %s. "
+ "Might produce DNS leaks." % (path,))
+
+ # XXX check if applies in win
+ is_exe = False
+ try:
+ is_exe = (stat.S_IXUSR & os.stat(path)[stat.ST_MODE] != 0)
+ except OSError as e:
+ logger.warn("%s" % (e,))
+ if warn and not is_exe:
+ logger.error("Up/down script %s is not executable. "
+ "Might produce DNS leaks." % (path,))
+ return is_file and is_exe
+
+
+def _has_other_files(path, warn=True):
+ """
+ Checks the existence of other important files.
+
+ :param path: the path to be checked
+ :type path: str
+
+ :param warn: whether we should log the absence
+ :type warn: bool
+
+ :rtype: bool
+ """
+ is_file = os.path.isfile(path)
+ if warn and not is_file:
+ logger.warning("Could not find file during checks: %s. " % (
+ path,))
+ return is_file
+
+
+def _is_auth_agent_running():
+ """
+ Checks if a polkit daemon is running.
+
+ :return: True if it's running, False if it's not.
+ :rtype: boolean
+ """
+ ps = 'ps aux | grep polkit-%s-authentication-agent-1'
+ opts = (ps % case for case in ['[g]nome', '[k]de'])
+ is_running = map(lambda l: commands.getoutput(l), opts)
+ return any(is_running)
+
+
+def _try_to_launch_agent():
+ """
+ Tries to launch a polkit daemon.
+ """
+ opts = [
+ "/usr/lib/policykit-1-gnome/polkit-gnome-authentication-agent-1",
+ # XXX add kde thing here
+ ]
+ for cmd in opts:
+ try:
+ subprocess.Popen([cmd], shell=True)
+ except:
+ pass
+
+
+class LinuxVPNLauncher(VPNLauncher):
+ """
+ VPN launcher for the Linux platform
+ """
+
+ PKEXEC_BIN = 'pkexec'
+ OPENVPN_BIN = 'openvpn'
+ OPENVPN_BIN_PATH = os.path.join(
+ ProviderConfig().get_path_prefix(),
+ "..", "apps", "eip", OPENVPN_BIN)
+
+ SYSTEM_CONFIG = "/etc/leap"
+ UP_DOWN_FILE = "resolv-update"
+ UP_DOWN_PATH = "%s/%s" % (SYSTEM_CONFIG, UP_DOWN_FILE)
+
+ # We assume this is there by our openvpn dependency, and
+ # we will put it there on the bundle too.
+ # TODO adapt to the bundle path.
+ OPENVPN_DOWN_ROOT_BASE = "/usr/lib/openvpn/"
+ OPENVPN_DOWN_ROOT_FILE = "openvpn-plugin-down-root.so"
+ OPENVPN_DOWN_ROOT_PATH = "%s/%s" % (
+ OPENVPN_DOWN_ROOT_BASE,
+ OPENVPN_DOWN_ROOT_FILE)
+
+ UPDOWN_FILES = (UP_DOWN_PATH,)
+ POLKIT_PATH = LinuxPolicyChecker.get_polkit_path()
+ OTHER_FILES = (POLKIT_PATH, )
+
+ def missing_other_files(self):
+ """
+ 'Extend' the VPNLauncher's missing_other_files to check if the polkit
+ files is outdated. If the polkit file that is in OTHER_FILES exists but
+ is not up to date, it is added to the missing list.
+
+ :returns: a list of missing files
+ :rtype: list of str
+ """
+ missing = VPNLauncher.missing_other_files.im_func(self)
+ polkit_file = LinuxPolicyChecker.get_polkit_path()
+ if polkit_file not in missing:
+ if privilege_policies.is_policy_outdated(self.OPENVPN_BIN_PATH):
+ missing.append(polkit_file)
+
+ return missing
+
+ @classmethod
+ def cmd_for_missing_scripts(kls, frompath, pol_file):
+ """
+ Returns a sh script that can copy the missing files.
+
+ :param frompath: The path where the up/down scripts live
+ :type frompath: str
+ :param pol_file: The path where the dynamically generated
+ policy file lives
+ :type pol_file: str
+
+ :rtype: str
+ """
+ to = kls.SYSTEM_CONFIG
+
+ cmd = '#!/bin/sh\n'
+ cmd += 'mkdir -p "%s"\n' % (to, )
+ cmd += 'cp "%s/%s" "%s"\n' % (frompath, kls.UP_DOWN_FILE, to)
+ cmd += 'cp "%s" "%s"\n' % (pol_file, kls.POLKIT_PATH)
+ cmd += 'chmod 644 "%s"\n' % (kls.POLKIT_PATH, )
+
+ return cmd
+
+ @classmethod
+ def maybe_pkexec(kls):
+ """
+ Checks whether pkexec is available in the system, and
+ returns the path if found.
+
+ Might raise EIPNoPkexecAvailable or EIPNoPolkitAuthAgentAvailable
+
+ :returns: a list of the paths where pkexec is to be found
+ :rtype: list
+ """
+ if _is_pkexec_in_system():
+ if not _is_auth_agent_running():
+ _try_to_launch_agent()
+ if _is_auth_agent_running():
+ pkexec_possibilities = which(kls.PKEXEC_BIN)
+ leap_assert(len(pkexec_possibilities) > 0,
+ "We couldn't find pkexec")
+ return pkexec_possibilities
+ else:
+ logger.warning("No polkit auth agent found. pkexec " +
+ "will use its own auth agent.")
+ raise EIPNoPolkitAuthAgentAvailable()
+ else:
+ logger.warning("System has no pkexec")
+ raise EIPNoPkexecAvailable()
+
+ @classmethod
+ def maybe_down_plugin(kls):
+ """
+ Returns the path of the openvpn down-root-plugin, searching first
+ in the relative path for the standalone bundle, and then in the system
+ path where the debian package puts it.
+
+ :returns: the path where the plugin was found, or None
+ :rtype: str or None
+ """
+ cwd = os.getcwd()
+ rel_path_in_bundle = os.path.join(
+ 'apps', 'eip', 'files', kls.OPENVPN_DOWN_ROOT_FILE)
+ abs_path_in_bundle = os.path.join(cwd, rel_path_in_bundle)
+ if os.path.isfile(abs_path_in_bundle):
+ return abs_path_in_bundle
+ abs_path_in_system = kls.OPENVPN_DOWN_ROOT_FILE
+ if os.path.isfile(abs_path_in_system):
+ return abs_path_in_system
+
+ logger.warning("We could not find the down-root-plugin, so no updown "
+ "scripts will be run. DNS leaks are likely!")
+ return None
+
+ def get_vpn_command(self, eipconfig=None, providerconfig=None,
+ socket_host=None, socket_port="unix", openvpn_verb=1):
+ """
+ Returns the platform dependant vpn launching command. It will
+ look for openvpn in the regular paths and algo in
+ path_prefix/apps/eip/ (in case standalone is set)
+
+ Might raise:
+ VPNLauncherException,
+ OpenVPNNotFoundException.
+
+ :param eipconfig: eip configuration object
+ :type eipconfig: EIPConfig
+
+ :param providerconfig: provider specific configuration
+ :type providerconfig: ProviderConfig
+
+ :param socket_host: either socket path (unix) or socket IP
+ :type socket_host: str
+
+ :param socket_port: either string "unix" if it's a unix
+ socket, or port otherwise
+ :type socket_port: str
+
+ :param openvpn_verb: openvpn verbosity wanted
+ :type openvpn_verb: int
+
+ :return: A VPN command ready to be launched
+ :rtype: list
+ """
+ leap_assert(eipconfig, "We need an eip config")
+ leap_assert_type(eipconfig, EIPConfig)
+ leap_assert(providerconfig, "We need a provider config")
+ leap_assert_type(providerconfig, ProviderConfig)
+ leap_assert(socket_host, "We need a socket host!")
+ leap_assert(socket_port, "We need a socket port!")
+
+ kwargs = {}
+ if ProviderConfig.standalone:
+ kwargs['path_extension'] = os.path.join(
+ providerconfig.get_path_prefix(),
+ "..", "apps", "eip")
+
+ openvpn_possibilities = which(self.OPENVPN_BIN, **kwargs)
+
+ if len(openvpn_possibilities) == 0:
+ raise OpenVPNNotFoundException()
+
+ openvpn = first(openvpn_possibilities)
+ args = []
+
+ pkexec = self.maybe_pkexec()
+ if pkexec:
+ args.append(openvpn)
+ openvpn = first(pkexec)
+
+ if openvpn_verb is not None:
+ args += ['--verb', '%d' % (openvpn_verb,)]
+
+ gateway_selector = VPNGatewaySelector(eipconfig)
+ gateways = gateway_selector.get_gateways()
+
+ if not gateways:
+ logger.error('No gateway was found!')
+ raise VPNLauncherException(self.tr('No gateway was found!'))
+
+ logger.debug("Using gateways ips: {}".format(', '.join(gateways)))
+
+ for gw in gateways:
+ args += ['--remote', gw, '1194', 'udp']
+
+ args += [
+ '--client',
+ '--dev', 'tun',
+ ##############################################################
+ # persist-tun makes ping-restart fail because it leaves a
+ # broken routing table
+ ##############################################################
+ # '--persist-tun',
+ '--persist-key',
+ '--tls-client',
+ '--remote-cert-tls',
+ 'server'
+ ]
+
+ openvpn_configuration = eipconfig.get_openvpn_configuration()
+
+ for key, value in openvpn_configuration.items():
+ args += ['--%s' % (key,), value]
+
+ ##############################################################
+ # The down-root plugin fails in some situations, so we don't
+ # drop privs for the time being
+ ##############################################################
+ # args += [
+ # '--user', getpass.getuser(),
+ # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name
+ # ]
+
+ if socket_port == "unix": # that's always the case for linux
+ args += [
+ '--management-client-user', getpass.getuser()
+ ]
+
+ args += [
+ '--management-signal',
+ '--management', socket_host, socket_port,
+ '--script-security', '2'
+ ]
+
+ plugin_path = self.maybe_down_plugin()
+ # If we do not have the down plugin neither in the bundle
+ # nor in the system, we do not do updown scripts. The alternative
+ # is leaving the user without the ability to restore dns and routes
+ # to its original state.
+
+ if plugin_path and _has_updown_scripts(self.UP_DOWN_PATH):
+ args += [
+ '--up', self.UP_DOWN_PATH,
+ '--down', self.UP_DOWN_PATH,
+ ##############################################################
+ # For the time being we are disabling the usage of the
+ # down-root plugin, because it doesn't quite work as
+ # expected (i.e. it doesn't run route -del as root
+ # when finishing, so it fails to properly
+ # restart/quit)
+ ##############################################################
+ # '--plugin', plugin_path,
+ # '\'script_type=down %s\'' % self.UP_DOWN_PATH
+ ]
+
+ args += [
+ '--cert', eipconfig.get_client_cert_path(providerconfig),
+ '--key', eipconfig.get_client_cert_path(providerconfig),
+ '--ca', providerconfig.get_ca_cert_path()
+ ]
+
+ logger.debug("Running VPN with command:")
+ logger.debug("%s %s" % (openvpn, " ".join(args)))
+
+ return [openvpn] + args
+
+ def get_vpn_env(self, providerconfig):
+ """
+ Returns a dictionary with the custom env for the platform.
+ This is mainly used for setting LD_LIBRARY_PATH to the correct
+ path when distributing a standalone client
+
+ :param providerconfig: provider specific configuration
+ :type providerconfig: ProviderConfig
+
+ :rtype: dict
+ """
+ leap_assert(providerconfig, "We need a provider config")
+ leap_assert_type(providerconfig, ProviderConfig)
+
+ return {"LD_LIBRARY_PATH": os.path.join(
+ providerconfig.get_path_prefix(),
+ "..", "lib")}
+
+
+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.\"")
+
+ INSTALL_PATH = os.path.realpath(os.getcwd() + "/../../")
+ INSTALL_PATH_ESCAPED = os.path.realpath(os.getcwd() + "/../../")
+ OPENVPN_BIN = 'openvpn.leap'
+ OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,)
+ OPENVPN_PATH_ESCAPED = "%s/Contents/Resources/openvpn" % (
+ INSTALL_PATH_ESCAPED,)
+
+ 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 = []
+
+ @classmethod
+ def cmd_for_missing_scripts(kls, frompath):
+ """
+ Returns a command that can copy the missing scripts.
+ :rtype: str
+ """
+ to = kls.OPENVPN_PATH_ESCAPED
+ cmd = "#!/bin/sh\nmkdir -p %s\ncp \"%s/\"* %s\nchmod 744 %s/*" % (
+ to, frompath, to, to)
+ return cmd
+
+ @classmethod
+ def maybe_kextloaded(kls):
+ """
+ Checks if the needed kext is loaded before launching openvpn.
+ """
+ return bool(commands.getoutput('kextstat | grep "leap.tun"'))
+
+ def _get_resource_path(self):
+ """
+ Returns the absolute path to the app resources directory
+
+ :rtype: str
+ """
+ return os.path.abspath(
+ os.path.join(
+ os.getcwd(),
+ "../../Contents/Resources"))
+
+ def _get_icon_path(self):
+ """
+ Returns the absolute path to the app icon
+
+ :rtype: str
+ """
+ return os.path.join(self._get_resource_path(),
+ "leap-client.tiff")
+
+ def get_cocoasudo_ovpn_cmd(self):
+ """
+ Returns a string with the cocoasudo command needed to run openvpn
+ as admin with a nice password prompt. The actual command needs to be
+ appended.
+
+ :rtype: (str, list)
+ """
+ iconpath = self._get_icon_path()
+ has_icon = os.path.isfile(iconpath)
+ args = ["--icon=%s" % iconpath] if has_icon else []
+ args.append("--prompt=%s" % (self.SUDO_MSG,))
+
+ return self.COCOASUDO, args
+
+ def get_cocoasudo_installmissing_cmd(self):
+ """
+ Returns a string with the cocoasudo command needed to install missing
+ files as admin with a nice password prompt. The actual command needs to
+ be appended.
+
+ :rtype: (str, list)
+ """
+ iconpath = self._get_icon_path()
+ has_icon = os.path.isfile(iconpath)
+ args = ["--icon=%s" % iconpath] if has_icon else []
+ args.append("--prompt=%s" % (self.INSTALL_MSG,))
+
+ return self.COCOASUDO, args
+
+ def get_vpn_command(self, eipconfig=None, providerconfig=None,
+ socket_host=None, socket_port="unix", openvpn_verb=1):
+ """
+ Returns the platform dependant vpn launching command
+
+ Might raise VPNException.
+
+ :param eipconfig: eip configuration object
+ :type eipconfig: EIPConfig
+
+ :param providerconfig: provider specific configuration
+ :type providerconfig: ProviderConfig
+
+ :param socket_host: either socket path (unix) or socket IP
+ :type socket_host: str
+
+ :param socket_port: either string "unix" if it's a unix
+ socket, or port otherwise
+ :type socket_port: str
+
+ :param openvpn_verb: openvpn verbosity wanted
+ :type openvpn_verb: int
+
+ :return: A VPN command ready to be launched
+ :rtype: list
+ """
+ leap_assert(eipconfig, "We need an eip config")
+ leap_assert_type(eipconfig, EIPConfig)
+ leap_assert(providerconfig, "We need a provider config")
+ leap_assert_type(providerconfig, ProviderConfig)
+ leap_assert(socket_host, "We need a socket host!")
+ leap_assert(socket_port, "We need a socket port!")
+
+ if not self.maybe_kextloaded():
+ raise EIPNoTunKextLoaded
+
+ kwargs = {}
+ if ProviderConfig.standalone:
+ kwargs['path_extension'] = os.path.join(
+ providerconfig.get_path_prefix(),
+ "..", "apps", "eip")
+
+ openvpn_possibilities = which(
+ self.OPENVPN_BIN,
+ **kwargs)
+ if len(openvpn_possibilities) == 0:
+ raise OpenVPNNotFoundException()
+
+ openvpn = first(openvpn_possibilities)
+ args = [openvpn]
+
+ if openvpn_verb is not None:
+ args += ['--verb', '%d' % (openvpn_verb,)]
+
+ gateway_selector = VPNGatewaySelector(eipconfig)
+ gateways = gateway_selector.get_gateways()
+
+ logger.debug("Using gateways ips: {gw}".format(
+ gw=', '.join(gateways)))
+
+ for gw in gateways:
+ args += ['--remote', gw, '1194', 'udp']
+
+ args += [
+ '--client',
+ '--dev', 'tun',
+ ##############################################################
+ # persist-tun makes ping-restart fail because it leaves a
+ # broken routing table
+ ##############################################################
+ # '--persist-tun',
+ '--persist-key',
+ '--tls-client',
+ '--remote-cert-tls',
+ 'server'
+ ]
+
+ openvpn_configuration = eipconfig.get_openvpn_configuration()
+ for key, value in openvpn_configuration.items():
+ args += ['--%s' % (key,), value]
+
+ user = getpass.getuser()
+
+ ##############################################################
+ # The down-root plugin fails in some situations, so we don't
+ # drop privs for the time being
+ ##############################################################
+ # args += [
+ # '--user', user,
+ # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name
+ # ]
+
+ if socket_port == "unix":
+ args += [
+ '--management-client-user', user
+ ]
+
+ args += [
+ '--management-signal',
+ '--management', socket_host, socket_port,
+ '--script-security', '2'
+ ]
+
+ if _has_updown_scripts(self.UP_SCRIPT):
+ args += [
+ '--up', '\"%s\"' % (self.UP_SCRIPT,),
+ ]
+
+ if _has_updown_scripts(self.DOWN_SCRIPT):
+ args += [
+ '--down', '\"%s\"' % (self.DOWN_SCRIPT,)
+ ]
+
+ # should have the down script too
+ if _has_updown_scripts(self.OPENVPN_DOWN_PLUGIN):
+ args += [
+ ###########################################################
+ # For the time being we are disabling the usage of the
+ # down-root plugin, because it doesn't quite work as
+ # expected (i.e. it doesn't run route -del as root
+ # when finishing, so it fails to properly
+ # restart/quit)
+ ###########################################################
+ # '--plugin', self.OPENVPN_DOWN_PLUGIN,
+ # '\'%s\'' % self.DOWN_SCRIPT
+ ]
+
+ # we set user to be passed to the up/down scripts
+ args += [
+ '--setenv', "LEAPUSER", "%s" % (user,)]
+
+ args += [
+ '--cert', eipconfig.get_client_cert_path(providerconfig),
+ '--key', eipconfig.get_client_cert_path(providerconfig),
+ '--ca', providerconfig.get_ca_cert_path()
+ ]
+
+ command, cargs = self.get_cocoasudo_ovpn_cmd()
+ cmd_args = cargs + args
+
+ logger.debug("Running VPN with command:")
+ logger.debug("%s %s" % (command, " ".join(cmd_args)))
+
+ return [command] + cmd_args
+
+ def get_vpn_env(self, providerconfig):
+ """
+ Returns a dictionary with the custom env for the platform.
+ This is mainly used for setting LD_LIBRARY_PATH to the correct
+ path when distributing a standalone client
+
+ :param providerconfig: provider specific configuration
+ :type providerconfig: ProviderConfig
+
+ :rtype: dict
+ """
+ return {"DYLD_LIBRARY_PATH": os.path.join(
+ providerconfig.get_path_prefix(),
+ "..", "lib")}
+
+
+class WindowsVPNLauncher(VPNLauncher):
+ """
+ VPN launcher for the Windows platform
+ """
+
+ OPENVPN_BIN = 'openvpn_leap.exe'
+
+ # XXX UPDOWN_FILES ... we do not have updown files defined yet!
+ # (and maybe we won't)
+
+ def get_vpn_command(self, eipconfig=None, providerconfig=None,
+ socket_host=None, socket_port="9876", openvpn_verb=1):
+ """
+ Returns the platform dependant vpn launching command. It will
+ look for openvpn in the regular paths and algo in
+ path_prefix/apps/eip/ (in case standalone is set)
+
+ Might raise VPNException.
+
+ :param eipconfig: eip configuration object
+ :type eipconfig: EIPConfig
+
+ :param providerconfig: provider specific configuration
+ :type providerconfig: ProviderConfig
+
+ :param socket_host: either socket path (unix) or socket IP
+ :type socket_host: str
+
+ :param socket_port: either string "unix" if it's a unix
+ socket, or port otherwise
+ :type socket_port: str
+
+ :param openvpn_verb: the openvpn verbosity wanted
+ :type openvpn_verb: int
+
+ :return: A VPN command ready to be launched
+ :rtype: list
+ """
+ leap_assert(eipconfig, "We need an eip config")
+ leap_assert_type(eipconfig, EIPConfig)
+ leap_assert(providerconfig, "We need a provider config")
+ leap_assert_type(providerconfig, ProviderConfig)
+ leap_assert(socket_host, "We need a socket host!")
+ leap_assert(socket_port, "We need a socket port!")
+ leap_assert(socket_port != "unix",
+ "We cannot use unix sockets in windows!")
+
+ openvpn_possibilities = which(
+ self.OPENVPN_BIN,
+ path_extension=os.path.join(providerconfig.get_path_prefix(),
+ "..", "apps", "eip"))
+
+ if len(openvpn_possibilities) == 0:
+ raise OpenVPNNotFoundException()
+
+ openvpn = first(openvpn_possibilities)
+ args = []
+ if openvpn_verb is not None:
+ args += ['--verb', '%d' % (openvpn_verb,)]
+
+ gateway_selector = VPNGatewaySelector(eipconfig)
+ gateways = gateway_selector.get_gateways()
+
+ logger.debug("Using gateways ips: {}".format(', '.join(gateways)))
+
+ for gw in gateways:
+ args += ['--remote', gw, '1194', 'udp']
+
+ args += [
+ '--client',
+ '--dev', 'tun',
+ ##############################################################
+ # persist-tun makes ping-restart fail because it leaves a
+ # broken routing table
+ ##############################################################
+ # '--persist-tun',
+ '--persist-key',
+ '--tls-client',
+ # We make it log to a file because we cannot attach to the
+ # openvpn process' stdout since it's a process with more
+ # privileges than we are
+ '--log-append', 'eip.log',
+ '--remote-cert-tls',
+ 'server'
+ ]
+
+ openvpn_configuration = eipconfig.get_openvpn_configuration()
+ for key, value in openvpn_configuration.items():
+ args += ['--%s' % (key,), value]
+
+ ##############################################################
+ # The down-root plugin fails in some situations, so we don't
+ # drop privs for the time being
+ ##############################################################
+ # args += [
+ # '--user', getpass.getuser(),
+ # #'--group', grp.getgrgid(os.getgroups()[-1]).gr_name
+ # ]
+
+ args += [
+ '--management-signal',
+ '--management', socket_host, socket_port,
+ '--script-security', '2'
+ ]
+
+ args += [
+ '--cert', eipconfig.get_client_cert_path(providerconfig),
+ '--key', eipconfig.get_client_cert_path(providerconfig),
+ '--ca', providerconfig.get_ca_cert_path()
+ ]
+
+ logger.debug("Running VPN with command:")
+ logger.debug("%s %s" % (openvpn, " ".join(args)))
+
+ return [openvpn] + args
+
+ def get_vpn_env(self, providerconfig):
+ """
+ Returns a dictionary with the custom env for the platform.
+ This is mainly used for setting LD_LIBRARY_PATH to the correct
+ path when distributing a standalone client
+
+ :param providerconfig: provider specific configuration
+ :type providerconfig: ProviderConfig
+
+ :rtype: dict
+ """
+ return {}
+
+
+if __name__ == "__main__":
+ logger = logging.getLogger(name='leap')
+ logger.setLevel(logging.DEBUG)
+ console = logging.StreamHandler()
+ console.setLevel(logging.DEBUG)
+ formatter = logging.Formatter(
+ '%(asctime)s '
+ '- %(name)s - %(levelname)s - %(message)s')
+ console.setFormatter(formatter)
+ logger.addHandler(console)
+
+ try:
+ abs_launcher = VPNLauncher()
+ except Exception as e:
+ assert isinstance(e, TypeError), "Something went wrong"
+ print "Abstract Prefixer class is working as expected"
+
+ vpnlauncher = get_platform_launcher()
+
+ eipconfig = EIPConfig()
+ eipconfig.set_api_version('1')
+ if eipconfig.load("leap/providers/bitmask.net/eip-service.json"):
+ provider = ProviderConfig()
+ if provider.load("leap/providers/bitmask.net/provider.json"):
+ vpnlauncher.get_vpn_command(eipconfig=eipconfig,
+ providerconfig=provider,
+ socket_host="/blah")
diff --git a/src/leap/services/eip/vpnprocess.py b/src/leap/services/eip/vpnprocess.py
new file mode 100644
index 00000000..5b07a3cf
--- /dev/null
+++ b/src/leap/services/eip/vpnprocess.py
@@ -0,0 +1,791 @@
+# -*- coding: utf-8 -*-
+# vpnprocess.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+VPN Manager, spawned in a custom processProtocol.
+"""
+import logging
+import os
+import psutil
+import psutil.error
+import shutil
+import socket
+
+from PySide import QtCore
+
+from leap.common.check import leap_assert, leap_assert_type
+from leap.config.providerconfig import ProviderConfig
+from leap.services.eip.vpnlaunchers import get_platform_launcher
+from leap.services.eip.eipconfig import EIPConfig
+from leap.services.eip.udstelnet import UDSTelnet
+from leap.util import first
+
+logger = logging.getLogger(__name__)
+vpnlog = logging.getLogger('leap.openvpn')
+
+from twisted.internet import protocol
+from twisted.internet import defer
+from twisted.internet.task import LoopingCall
+from twisted.internet import error as internet_error
+
+
+class VPNSignals(QtCore.QObject):
+ """
+ These are the signals that we use to let the UI know
+ about the events we are polling.
+ They are instantiated in the VPN object and passed along
+ till the VPNProcess.
+ """
+ state_changed = QtCore.Signal(dict)
+ status_changed = QtCore.Signal(dict)
+ process_finished = QtCore.Signal(int)
+
+ def __init__(self):
+ QtCore.QObject.__init__(self)
+
+
+class OpenVPNAlreadyRunning(Exception):
+ message = ("Another openvpn instance is already running, and could "
+ "not be stopped.")
+
+
+class AlienOpenVPNAlreadyRunning(Exception):
+ message = ("Another openvpn instance is already running, and could "
+ "not be stopped because it was not launched by LEAP.")
+
+
+class VPN(object):
+ """
+ This is the high-level object that the GUI is dealing with.
+ It exposes the start and terminate methods.
+
+ On start, it spawns a VPNProcess instance that will use a vpnlauncher
+ suited for the running platform and connect to the management interface
+ opened by the openvpn process, executing commands over that interface on
+ demand.
+ """
+ TERMINATE_MAXTRIES = 10
+ TERMINATE_WAIT = 1 # secs
+
+ OPENVPN_VERB = "openvpn_verb"
+
+ def __init__(self, **kwargs):
+ """
+ Instantiate empty attributes and get a copy
+ of a QObject containing the QSignals that we will pass along
+ to the VPNManager.
+ """
+ from twisted.internet import reactor
+ self._vpnproc = None
+ self._pollers = []
+ self._reactor = reactor
+ self._qtsigs = VPNSignals()
+
+ self._openvpn_verb = kwargs.get(self.OPENVPN_VERB, None)
+
+ @property
+ def qtsigs(self):
+ return self._qtsigs
+
+ def start(self, *args, **kwargs):
+ """
+ Starts the openvpn subprocess.
+
+ :param args: args to be passed to the VPNProcess
+ :type args: tuple
+
+ :param kwargs: kwargs to be passed to the VPNProcess
+ :type kwargs: dict
+ """
+ self._stop_pollers()
+ kwargs['qtsigs'] = self.qtsigs
+ kwargs['openvpn_verb'] = self._openvpn_verb
+
+ # start the main vpn subprocess
+ vpnproc = VPNProcess(*args, **kwargs)
+ #qtsigs=self.qtsigs,
+ #openvpn_verb=self._openvpn_verb)
+
+ if vpnproc.get_openvpn_process():
+ logger.info("Another vpn process is running. Will try to stop it.")
+ vpnproc.stop_if_already_running()
+
+ cmd = vpnproc.getCommand()
+ env = os.environ
+ for key, val in vpnproc.vpn_env.items():
+ env[key] = val
+
+ self._reactor.spawnProcess(vpnproc, cmd[0], cmd, env)
+ self._vpnproc = vpnproc
+
+ # add pollers for status and state
+ # this could be extended to a collection of
+ # generic watchers
+
+ poll_list = [LoopingCall(vpnproc.pollStatus),
+ LoopingCall(vpnproc.pollState)]
+ self._pollers.extend(poll_list)
+ self._start_pollers()
+
+ def _kill_if_left_alive(self, tries=0):
+ """
+ Check if the process is still alive, and sends a
+ SIGKILL after a timeout period.
+
+ :param tries: counter of tries, used in recursion
+ :type tries: int
+ """
+ from twisted.internet import reactor
+ while tries < self.TERMINATE_MAXTRIES:
+ if self._vpnproc.transport.pid is None:
+ logger.debug("Process has been happily terminated.")
+ return
+ else:
+ logger.debug("Process did not die, waiting...")
+ tries += 1
+ reactor.callLater(self.TERMINATE_WAIT,
+ self._kill_if_left_alive, tries)
+
+ # after running out of patience, we try a killProcess
+ logger.debug("Process did not died. Sending a SIGKILL.")
+ self.killit()
+
+ def killit(self):
+ """
+ Sends a kill signal to the process.
+ """
+ self._stop_pollers()
+ self._vpnproc.aborted = True
+ self._vpnproc.killProcess()
+
+ def terminate(self, shutdown=False):
+ """
+ Stops the openvpn subprocess.
+
+ Attempts to send a SIGTERM first, and after a timeout
+ it sends a SIGKILL.
+ """
+ from twisted.internet import reactor
+ self._stop_pollers()
+
+ # First we try to be polite and send a SIGTERM...
+ if self._vpnproc:
+ self._sentterm = True
+ self._vpnproc.terminate_openvpn(shutdown=shutdown)
+
+ # ...but we also trigger a countdown to be unpolite
+ # if strictly needed.
+
+ # XXX Watch out! This will fail NOW since we are running
+ # openvpn as root as a workaround for some connection issues.
+ reactor.callLater(
+ self.TERMINATE_WAIT, self._kill_if_left_alive)
+
+ def _start_pollers(self):
+ """
+ Iterate through the registered observers
+ and start the looping call for them.
+ """
+ for poller in self._pollers:
+ poller.start(VPNManager.POLL_TIME)
+
+ def _stop_pollers(self):
+ """
+ Iterate through the registered observers
+ and stop the looping calls if they are running.
+ """
+ for poller in self._pollers:
+ if poller.running:
+ poller.stop()
+ self._pollers = []
+
+
+class VPNManager(object):
+ """
+ This is a mixin that we use in the VPNProcess class.
+ Here we get together all methods related with the openvpn management
+ interface.
+
+ A copy of a QObject containing signals as attributes is passed along
+ upon initialization, and we use that object to emit signals to qt-land.
+
+ For more info about management methods::
+
+ zcat `dpkg -L openvpn | grep management`
+ """
+
+ # Timers, in secs
+ POLL_TIME = 0.5
+ CONNECTION_RETRY_TIME = 1
+
+ TS_KEY = "ts"
+ STATUS_STEP_KEY = "status_step"
+ OK_KEY = "ok"
+ IP_KEY = "ip"
+ REMOTE_KEY = "remote"
+
+ TUNTAP_READ_KEY = "tun_tap_read"
+ TUNTAP_WRITE_KEY = "tun_tap_write"
+ TCPUDP_READ_KEY = "tcp_udp_read"
+ TCPUDP_WRITE_KEY = "tcp_udp_write"
+ AUTH_READ_KEY = "auth_read"
+
+ def __init__(self, qtsigs=None):
+ """
+ Initializes the VPNManager.
+
+ :param qtsigs: a QObject containing the Qt signals used by the UI
+ to give feedback about state changes.
+ :type qtsigs: QObject
+ """
+ from twisted.internet import reactor
+ self._reactor = reactor
+ self._tn = None
+ self._qtsigs = qtsigs
+ self._aborted = False
+
+ @property
+ def qtsigs(self):
+ return self._qtsigs
+
+ @property
+ def aborted(self):
+ return self._aborted
+
+ @aborted.setter
+ def aborted(self, value):
+ self._aborted = value
+
+ def _seek_to_eof(self):
+ """
+ Read as much as available. Position seek pointer to end of stream
+ """
+ try:
+ self._tn.read_eager()
+ except EOFError:
+ logger.debug("Could not read from socket. Assuming it died.")
+ return
+
+ def _send_command(self, command, until=b"END"):
+ """
+ Sends a command to the telnet connection and reads until END
+ is reached.
+
+ :param command: command to send
+ :type command: str
+
+ :param until: byte delimiter string for reading command output
+ :type until: byte str
+
+ :return: response read
+ :rtype: list
+ """
+ leap_assert(self._tn, "We need a tn connection!")
+
+ try:
+ self._tn.write("%s\n" % (command,))
+ buf = self._tn.read_until(until, 2)
+ self._seek_to_eof()
+ blist = buf.split('\r\n')
+ if blist[-1].startswith(until):
+ del blist[-1]
+ return blist
+ else:
+ return []
+
+ except socket.error:
+ # XXX should get a counter and repeat only
+ # after mod X times.
+ logger.warning('socket error (command was: "%s")' % (command,))
+ self._close_management_socket(announce=False)
+ logger.debug('trying to connect to management again')
+ self.try_to_connect_to_management(max_retries=5)
+ return []
+
+ # XXX should move this to a errBack!
+ except Exception as e:
+ logger.warning("Error sending command %s: %r" %
+ (command, e))
+ return []
+
+ def _close_management_socket(self, announce=True):
+ """
+ Close connection to openvpn management interface.
+ """
+ logger.debug('closing socket')
+ if announce:
+ self._tn.write("quit\n")
+ self._tn.read_all()
+ self._tn.get_socket().close()
+ self._tn = None
+
+ def _connect_management(self, socket_host, socket_port):
+ """
+ Connects to the management interface on the specified
+ socket_host socket_port.
+
+ :param socket_host: either socket path (unix) or socket IP
+ :type socket_host: str
+
+ :param socket_port: either string "unix" if it's a unix
+ socket, or port otherwise
+ :type socket_port: str
+ """
+ if self.is_connected():
+ self._close_management_socket()
+
+ try:
+ self._tn = UDSTelnet(socket_host, socket_port)
+
+ # XXX make password optional
+ # specially for win. we should generate
+ # the pass on the fly when invoking manager
+ # from conductor
+
+ # self.tn.read_until('ENTER PASSWORD:', 2)
+ # self.tn.write(self.password + '\n')
+ # self.tn.read_until('SUCCESS:', 2)
+ if self._tn:
+ self._tn.read_eager()
+
+ # XXX move this to the Errback
+ except Exception as e:
+ logger.warning("Could not connect to OpenVPN yet: %r" % (e,))
+ self._tn = None
+
+ def _connectCb(self, *args):
+ """
+ Callback for connection.
+
+ :param args: not used
+ """
+ if self._tn:
+ logger.info('Connected to management')
+ else:
+ logger.debug('Cannot connect to management...')
+
+ def _connectErr(self, failure):
+ """
+ Errorback for connection.
+
+ :param failure: Failure
+ """
+ logger.warning(failure)
+
+ def connect_to_management(self, host, port):
+ """
+ Connect to a management interface.
+
+ :param host: the host of the management interface
+ :type host: str
+
+ :param port: the port of the management interface
+ :type port: str
+
+ :returns: a deferred
+ """
+ self.connectd = defer.maybeDeferred(
+ self._connect_management, host, port)
+ self.connectd.addCallbacks(self._connectCb, self._connectErr)
+ return self.connectd
+
+ def is_connected(self):
+ """
+ Returns the status of the management interface.
+
+ :returns: True if connected, False otherwise
+ :rtype: bool
+ """
+ return True if self._tn else False
+
+ def try_to_connect_to_management(self, retry=0, max_retries=None):
+ """
+ Attempts to connect to a management interface, and retries
+ after CONNECTION_RETRY_TIME if not successful.
+
+ :param retry: number of the retry
+ :type retry: int
+ """
+ if max_retries and retry > max_retries:
+ logger.warning("Max retries reached while attempting to connect "
+ "to management. Aborting.")
+ self.aborted = True
+ return
+
+ # _alive flag is set in the VPNProcess class.
+ if not self._alive:
+ logger.debug('Tried to connect to management but process is '
+ 'not alive.')
+ return
+ logger.debug('trying to connect to management')
+ if not self.aborted and not self.is_connected():
+ self.connect_to_management(self._socket_host, self._socket_port)
+ self._reactor.callLater(
+ self.CONNECTION_RETRY_TIME,
+ self.try_to_connect_to_management, retry + 1)
+
+ def _parse_state_and_notify(self, output):
+ """
+ Parses the output of the state command and emits state_changed
+ signal when the state changes.
+
+ :param output: list of lines that the state command printed as
+ its output
+ :type output: list
+ """
+ for line in output:
+ stripped = line.strip()
+ if stripped == "END":
+ continue
+ parts = stripped.split(",")
+ if len(parts) < 5:
+ continue
+ ts, status_step, ok, ip, remote = parts
+
+ state_dict = {
+ self.TS_KEY: ts,
+ self.STATUS_STEP_KEY: status_step,
+ self.OK_KEY: ok,
+ self.IP_KEY: ip,
+ self.REMOTE_KEY: remote
+ }
+
+ if state_dict != self._last_state:
+ self.qtsigs.state_changed.emit(state_dict)
+ self._last_state = state_dict
+
+ def _parse_status_and_notify(self, output):
+ """
+ Parses the output of the status command and emits
+ status_changed signal when the status changes.
+
+ :param output: list of lines that the status command printed
+ as its output
+ :type output: list
+ """
+ tun_tap_read = ""
+ tun_tap_write = ""
+ tcp_udp_read = ""
+ tcp_udp_write = ""
+ auth_read = ""
+ for line in output:
+ stripped = line.strip()
+ if stripped.endswith("STATISTICS") or stripped == "END":
+ continue
+ parts = stripped.split(",")
+ if len(parts) < 2:
+ continue
+ if parts[0].strip() == "TUN/TAP read bytes":
+ tun_tap_read = parts[1]
+ elif parts[0].strip() == "TUN/TAP write bytes":
+ tun_tap_write = parts[1]
+ elif parts[0].strip() == "TCP/UDP read bytes":
+ tcp_udp_read = parts[1]
+ elif parts[0].strip() == "TCP/UDP write bytes":
+ tcp_udp_write = parts[1]
+ elif parts[0].strip() == "Auth read bytes":
+ auth_read = parts[1]
+
+ status_dict = {
+ self.TUNTAP_READ_KEY: tun_tap_read,
+ self.TUNTAP_WRITE_KEY: tun_tap_write,
+ self.TCPUDP_READ_KEY: tcp_udp_read,
+ self.TCPUDP_WRITE_KEY: tcp_udp_write,
+ self.AUTH_READ_KEY: auth_read
+ }
+
+ if status_dict != self._last_status:
+ self.qtsigs.status_changed.emit(status_dict)
+ self._last_status = status_dict
+
+ def get_state(self):
+ """
+ Notifies the gui of the output of the state command over
+ the openvpn management interface.
+ """
+ if self.is_connected():
+ return self._parse_state_and_notify(self._send_command("state"))
+
+ def get_status(self):
+ """
+ Notifies the gui of the output of the status command over
+ the openvpn management interface.
+ """
+ if self.is_connected():
+ return self._parse_status_and_notify(self._send_command("status"))
+
+ @property
+ def vpn_env(self):
+ """
+ Return a dict containing the vpn environment to be used.
+ """
+ return self._launcher.get_vpn_env(self._providerconfig)
+
+ def terminate_openvpn(self, shutdown=False):
+ """
+ Attempts to terminate openvpn by sending a SIGTERM.
+ """
+ if self.is_connected():
+ self._send_command("signal SIGTERM")
+ if shutdown:
+ self._cleanup_tempfiles()
+
+ def _cleanup_tempfiles(self):
+ """
+ Remove all temporal files we might have left behind.
+
+ Iif self.port is 'unix', we have created a temporal socket path that,
+ under normal circumstances, we should be able to delete.
+ """
+ if self._socket_port == "unix":
+ logger.debug('cleaning socket file temp folder')
+ tempfolder = first(os.path.split(self._socket_host))
+ if tempfolder and os.path.isdir(tempfolder):
+ try:
+ shutil.rmtree(tempfolder)
+ except OSError:
+ logger.error('could not delete tmpfolder %s' % tempfolder)
+
+ def get_openvpn_process(self):
+ """
+ Looks for openvpn instances running.
+
+ :rtype: process
+ """
+ openvpn_process = None
+ for p in psutil.process_iter():
+ try:
+ # XXX Not exact!
+ # Will give false positives.
+ # we should check that cmdline BEGINS
+ # with openvpn or with our wrapper
+ # (pkexec / osascript / whatever)
+
+ # This needs more work, see #3268, but for the moment
+ # we need to be able to filter out arguments in the form
+ # --openvpn-foo, since otherwise we are shooting ourselves
+ # in the feet.
+ if any(map(lambda s: s.startswith("openvpn"), p.cmdline)):
+ openvpn_process = p
+ break
+ except psutil.error.AccessDenied:
+ pass
+ return openvpn_process
+
+ def stop_if_already_running(self):
+ """
+ Checks if VPN is already running and tries to stop it.
+
+ Might raise OpenVPNAlreadyRunning.
+
+ :return: True if stopped, False otherwise
+
+ """
+ process = self.get_openvpn_process()
+ if not process:
+ logger.debug('Could not find openvpn process while '
+ 'trying to stop it.')
+ return
+
+ logger.debug("OpenVPN is already running, trying to stop it...")
+ cmdline = process.cmdline
+
+ manag_flag = "--management"
+ if isinstance(cmdline, list) and manag_flag in cmdline:
+ # we know that our invocation has this distinctive fragment, so
+ # we use this fingerprint to tell other invocations apart.
+ # this might break if we change the configuration path in the
+ # launchers
+ smellslikeleap = lambda s: "leap" in s and "providers" in s
+
+ if not any(map(smellslikeleap, cmdline)):
+ logger.debug("We cannot stop this instance since we do not "
+ "recognise it as a leap invocation.")
+ raise AlienOpenVPNAlreadyRunning
+
+ try:
+ index = cmdline.index(manag_flag)
+ host = cmdline[index + 1]
+ port = cmdline[index + 2]
+ logger.debug("Trying to connect to %s:%s"
+ % (host, port))
+ self.connect_to_management(host, port)
+
+ # XXX this has a problem with connections to different
+ # remotes. So the reconnection will only work when we are
+ # terminating instances left running for the same provider.
+ # If we are killing an openvpn instance configured for another
+ # provider, we will get:
+ # TLS Error: local/remote TLS keys are out of sync
+ # However, that should be a rare case right now.
+ self._send_command("signal SIGTERM")
+ self._close_management_socket(announce=True)
+ except Exception as e:
+ logger.warning("Problem trying to terminate OpenVPN: %r"
+ % (e,))
+ else:
+ logger.debug("Could not find the expected openvpn command line.")
+
+ process = self.get_openvpn_process()
+ if process is None:
+ logger.debug("Successfully finished already running "
+ "openvpn process.")
+ return True
+ else:
+ logger.warning("Unable to terminate OpenVPN")
+ raise OpenVPNAlreadyRunning
+
+
+class VPNProcess(protocol.ProcessProtocol, VPNManager):
+ """
+ A ProcessProtocol class that can be used to spawn a process that will
+ launch openvpn and connect to its management interface to control it
+ programmatically.
+ """
+
+ def __init__(self, eipconfig, providerconfig, socket_host, socket_port,
+ qtsigs, openvpn_verb):
+ """
+ :param eipconfig: eip configuration object
+ :type eipconfig: EIPConfig
+
+ :param providerconfig: provider specific configuration
+ :type providerconfig: ProviderConfig
+
+ :param socket_host: either socket path (unix) or socket IP
+ :type socket_host: str
+
+ :param socket_port: either string "unix" if it's a unix
+ socket, or port otherwise
+ :type socket_port: str
+
+ :param qtsigs: a QObject containing the Qt signals used to notify the
+ UI.
+ :type qtsigs: QObject
+
+ :param openvpn_verb: the desired level of verbosity in the
+ openvpn invocation
+ :type openvpn_verb: int
+ """
+ VPNManager.__init__(self, qtsigs=qtsigs)
+ leap_assert_type(eipconfig, EIPConfig)
+ leap_assert_type(providerconfig, ProviderConfig)
+ leap_assert_type(qtsigs, QtCore.QObject)
+
+ #leap_assert(not self.isRunning(), "Starting process more than once!")
+
+ self._eipconfig = eipconfig
+ self._providerconfig = providerconfig
+ self._socket_host = socket_host
+ self._socket_port = socket_port
+
+ self._launcher = get_platform_launcher()
+
+ self._last_state = None
+ self._last_status = None
+ self._alive = False
+
+ self._openvpn_verb = openvpn_verb
+
+ # processProtocol methods
+
+ def connectionMade(self):
+ """
+ Called when the connection is made.
+
+ .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa
+ """
+ self._alive = True
+ self.aborted = False
+ self.try_to_connect_to_management(max_retries=10)
+
+ def outReceived(self, data):
+ """
+ Called when new data is available on stdout.
+
+ :param data: the data read on stdout
+
+ .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa
+ """
+ # truncate the newline
+ # should send this to the logging window
+ vpnlog.info(data[:-1])
+
+ def processExited(self, reason):
+ """
+ Called when the child process exits.
+
+ .. 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,))
+ self.qtsigs.process_finished.emit(exit_code)
+ self._alive = False
+
+ def processEnded(self, reason):
+ """
+ Called when the child process exits and all file descriptors associated
+ with it have been closed.
+
+ .. 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("processEnded, status %d" % (exit_code,))
+
+ # polling
+
+ def pollStatus(self):
+ """
+ Polls connection status.
+ """
+ if self._alive:
+ self.get_status()
+
+ def pollState(self):
+ """
+ Polls connection state.
+ """
+ if self._alive:
+ self.get_state()
+
+ # launcher
+
+ def getCommand(self):
+ """
+ Gets the vpn command from the aproppriate launcher.
+
+ Might throw: VPNLauncherException, OpenVPNNotFoundException.
+ """
+ cmd = self._launcher.get_vpn_command(
+ eipconfig=self._eipconfig,
+ providerconfig=self._providerconfig,
+ socket_host=self._socket_host,
+ socket_port=self._socket_port,
+ openvpn_verb=self._openvpn_verb)
+ return map(str, cmd)
+
+ # shutdown
+
+ def killProcess(self):
+ """
+ Sends the KILL signal to the running process.
+ """
+ try:
+ self.transport.signalProcess('KILL')
+ except internet_error.ProcessExitedAlready:
+ logger.debug('Process Exited Already')
diff --git a/src/leap/eip/tests/__init__.py b/src/leap/services/mail/__init__.py
index e69de29b..e69de29b 100644
--- a/src/leap/eip/tests/__init__.py
+++ b/src/leap/services/mail/__init__.py
diff --git a/src/leap/services/mail/imap.py b/src/leap/services/mail/imap.py
new file mode 100644
index 00000000..4dceb2ad
--- /dev/null
+++ b/src/leap/services/mail/imap.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# imap.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Initialization of imap service
+"""
+import logging
+import sys
+
+from leap.mail.imap.service import imap
+from twisted.python import log
+
+logger = logging.getLogger(__name__)
+
+
+def start_imap_service(*args, **kwargs):
+ """
+ Initializes and run imap service.
+
+ :returns: twisted.internet.task.LoopingCall instance
+ """
+ logger.debug('Launching imap service')
+
+ # Uncomment the next two lines to get a separate debugging log
+ # TODO handle this by a separate flag.
+ #log.startLogging(open('/tmp/leap-imap.log', 'w'))
+ #log.startLogging(sys.stdout)
+
+ return imap.run_service(*args, **kwargs)
diff --git a/src/leap/services/mail/smtpbootstrapper.py b/src/leap/services/mail/smtpbootstrapper.py
new file mode 100644
index 00000000..48040035
--- /dev/null
+++ b/src/leap/services/mail/smtpbootstrapper.py
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+# smtpbootstrapper.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+SMTP bootstrapping
+"""
+
+import logging
+import os
+
+from PySide import QtCore
+
+from leap.common.check import leap_assert, leap_assert_type
+from leap.common.files import get_mtime
+from leap.config.providerconfig import ProviderConfig
+from leap.crypto.srpauth import SRPAuth
+from leap.util.request_helpers import get_content
+from leap.services.abstractbootstrapper import AbstractBootstrapper
+
+logger = logging.getLogger(__name__)
+
+
+class SMTPBootstrapper(AbstractBootstrapper):
+ """
+ SMTP init procedure
+ """
+
+ # All dicts returned are of the form
+ # {"passed": bool, "error": str}
+ download_config = QtCore.Signal(dict)
+
+ def __init__(self):
+ AbstractBootstrapper.__init__(self)
+
+ self._provider_config = None
+ self._smtp_config = None
+ self._download_if_needed = False
+
+ def _download_config(self, *args):
+ """
+ Downloads the SMTP config for the given provider
+ """
+
+ leap_assert(self._provider_config,
+ "We need a provider configuration!")
+
+ logger.debug("Downloading SMTP config for %s" %
+ (self._provider_config.get_domain(),))
+
+ headers = {}
+ mtime = get_mtime(os.path.join(self._smtp_config
+ .get_path_prefix(),
+ "leap",
+ "providers",
+ self._provider_config.get_domain(),
+ "smtp-service.json"))
+
+ if self._download_if_needed and mtime:
+ headers['if-modified-since'] = mtime
+
+ api_version = self._provider_config.get_api_version()
+
+ # there is some confusion with this uri,
+ config_uri = "%s/%s/config/smtp-service.json" % (
+ self._provider_config.get_api_uri(), api_version)
+
+ logger.debug('Downloading SMTP config from: %s' % config_uri)
+
+ srp_auth = SRPAuth(self._provider_config)
+ session_id = srp_auth.get_session_id()
+ cookies = None
+ if session_id:
+ cookies = {"_session_id": session_id}
+
+ res = self._session.get(config_uri,
+ verify=self._provider_config
+ .get_ca_cert_path(),
+ headers=headers,
+ cookies=cookies)
+ res.raise_for_status()
+
+ self._smtp_config.set_api_version(api_version)
+
+ # Not modified
+ if res.status_code == 304:
+ logger.debug("SMTP definition has not been modified")
+ self._smtp_config.load(os.path.join(
+ "leap", "providers",
+ self._provider_config.get_domain(),
+ "smtp-service.json"))
+ else:
+ smtp_definition, mtime = get_content(res)
+
+ self._smtp_config.load(data=smtp_definition, mtime=mtime)
+ self._smtp_config.save(["leap",
+ "providers",
+ self._provider_config.get_domain(),
+ "smtp-service.json"])
+
+ def run_smtp_setup_checks(self,
+ provider_config,
+ smtp_config,
+ download_if_needed=False):
+ """
+ Starts the checks needed for a new smtp setup
+
+ :param provider_config: Provider configuration
+ :type provider_config: ProviderConfig
+ :param smtp_config: SMTP configuration to populate
+ :type smtp_config: SMTPConfig
+ :param download_if_needed: True if it should check for mtime
+ for the file
+ :type download_if_needed: bool
+ """
+ leap_assert_type(provider_config, ProviderConfig)
+
+ self._provider_config = provider_config
+ self._smtp_config = smtp_config
+ self._download_if_needed = download_if_needed
+
+ cb_chain = [
+ (self._download_config, self.download_config),
+ ]
+
+ self.addCallbackChain(cb_chain)
diff --git a/src/leap/services/mail/smtpconfig.py b/src/leap/services/mail/smtpconfig.py
new file mode 100644
index 00000000..ea0f9c37
--- /dev/null
+++ b/src/leap/services/mail/smtpconfig.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+# smtpconfig.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+SMTP configuration
+"""
+import logging
+
+from leap.common.config.baseconfig import BaseConfig
+from leap.services.mail.smtpspec import get_schema
+
+logger = logging.getLogger(__name__)
+
+
+class SMTPConfig(BaseConfig):
+ """
+ SMTP configuration abstraction class
+ """
+
+ def __init__(self):
+ BaseConfig.__init__(self)
+
+ def _get_schema(self):
+ """
+ Returns the schema corresponding to the version given.
+
+ :rtype: dict or None if the version is not supported.
+ """
+ return get_schema(self._api_version)
+
+ def get_hosts(self):
+ return self._safe_get_value("hosts")
+
+ def get_locations(self):
+ return self._safe_get_value("locations")
diff --git a/src/leap/services/mail/smtpspec.py b/src/leap/services/mail/smtpspec.py
new file mode 100644
index 00000000..ff9d1bf8
--- /dev/null
+++ b/src/leap/services/mail/smtpspec.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+# smtpspec.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Schemas dict
+# To add a schema for a version you should follow the form:
+# { '1': schema_v1, '2': schema_v2, ... etc }
+# so for instance, to add the '2' version, you should do:
+# smtp_config_spec['2'] = schema_v2
+smtp_config_spec = {}
+
+smtp_config_spec['1'] = {
+ 'description': 'sample smtp service config',
+ 'type': 'object',
+ 'properties': {
+ 'serial': {
+ 'type': int,
+ 'default': 1,
+ 'required': ["True"]
+ },
+ 'version': {
+ 'type': int,
+ 'default': 1,
+ 'required': ["True"]
+ },
+ 'hosts': {
+ 'type': dict,
+ 'default': {
+ "walrus": {
+ "hostname": "someprovider",
+ "ip_address": "1.1.1.1",
+ "port": 1111
+ },
+ },
+ },
+ 'locations': {
+ 'type': dict,
+ 'default': {
+ "locations": {
+
+ }
+ }
+ }
+ }
+}
+
+
+def get_schema(version):
+ """
+ Returns the schema corresponding to the version given.
+
+ :param version: the version of the schema to get.
+ :type version: str
+ :rtype: dict or None if the version is not supported.
+ """
+ schema = smtp_config_spec.get(version, None)
+ return schema
diff --git a/src/leap/gui/tests/__init__.py b/src/leap/services/soledad/__init__.py
index e69de29b..e69de29b 100644
--- a/src/leap/gui/tests/__init__.py
+++ b/src/leap/services/soledad/__init__.py
diff --git a/src/leap/services/soledad/soledadbootstrapper.py b/src/leap/services/soledad/soledadbootstrapper.py
new file mode 100644
index 00000000..c67bc004
--- /dev/null
+++ b/src/leap/services/soledad/soledadbootstrapper.py
@@ -0,0 +1,265 @@
+# -*- coding: utf-8 -*-
+# soledadbootstrapper.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Soledad bootstrapping
+"""
+
+import logging
+import os
+
+from PySide import QtCore
+from u1db import errors as u1db_errors
+
+from leap.common.check import leap_assert, leap_assert_type
+from leap.common.files import get_mtime
+from leap.keymanager import KeyManager, openpgp
+from leap.keymanager.errors import KeyNotFound
+from leap.config.providerconfig import ProviderConfig
+from leap.crypto.srpauth import SRPAuth
+from leap.services.soledad.soledadconfig import SoledadConfig
+from leap.util.request_helpers import get_content
+from leap.soledad import Soledad
+from leap.services.abstractbootstrapper import AbstractBootstrapper
+
+logger = logging.getLogger(__name__)
+
+
+class SoledadBootstrapper(AbstractBootstrapper):
+ """
+ Soledad init procedure
+ """
+
+ SOLEDAD_KEY = "soledad"
+ KEYMANAGER_KEY = "keymanager"
+
+ PUBKEY_KEY = "user[public_key]"
+
+ # All dicts returned are of the form
+ # {"passed": bool, "error": str}
+ download_config = QtCore.Signal(dict)
+ gen_key = QtCore.Signal(dict)
+
+ def __init__(self):
+ AbstractBootstrapper.__init__(self)
+
+ self._provider_config = None
+ self._soledad_config = None
+ self._keymanager = None
+ self._download_if_needed = False
+ self._user = ""
+ self._password = ""
+ self._soledad = None
+
+ @property
+ def keymanager(self):
+ return self._keymanager
+
+ @property
+ def soledad(self):
+ return self._soledad
+
+ def _load_and_sync_soledad(self, srp_auth):
+ """
+ Once everthing is in the right place, we instantiate and sync
+ Soledad
+
+ :param srp_auth: SRPAuth object used
+ :type srp_auth: SRPAuth
+ """
+ uuid = srp_auth.get_uid()
+
+ prefix = os.path.join(self._soledad_config.get_path_prefix(),
+ "leap", "soledad")
+ secrets_path = "%s/%s.secret" % (prefix, uuid)
+ local_db_path = "%s/%s.db" % (prefix, uuid)
+
+ # TODO: Select server based on timezone (issue #3308)
+ server_dict = self._soledad_config.get_hosts()
+
+ if server_dict.keys():
+ selected_server = server_dict[server_dict.keys()[0]]
+ server_url = "https://%s:%s/user-%s" % (
+ selected_server["hostname"],
+ selected_server["port"],
+ uuid)
+
+ logger.debug("Using soledad server url: %s" % (server_url,))
+
+ cert_file = self._provider_config.get_ca_cert_path()
+
+ # TODO: If selected server fails, retry with another host
+ # (issue #3309)
+ try:
+ self._soledad = Soledad(
+ uuid,
+ self._password.encode("utf-8"),
+ secrets_path=secrets_path,
+ local_db_path=local_db_path,
+ server_url=server_url,
+ cert_file=cert_file,
+ auth_token=srp_auth.get_token())
+ self._soledad.sync()
+ except u1db_errors.Unauthorized:
+ logger.error("Error while initializing soledad.")
+ else:
+ raise Exception("No soledad server found")
+
+ def _download_config(self):
+ """
+ Downloads the Soledad config for the given provider
+ """
+
+ leap_assert(self._provider_config,
+ "We need a provider configuration!")
+
+ logger.debug("Downloading Soledad config for %s" %
+ (self._provider_config.get_domain(),))
+
+ self._soledad_config = SoledadConfig()
+
+ headers = {}
+ mtime = get_mtime(
+ os.path.join(
+ self._soledad_config.get_path_prefix(),
+ "leap", "providers",
+ self._provider_config.get_domain(),
+ "soledad-service.json"))
+
+ if self._download_if_needed and mtime:
+ headers['if-modified-since'] = mtime
+
+ api_version = self._provider_config.get_api_version()
+
+ # there is some confusion with this uri,
+ config_uri = "%s/%s/config/soledad-service.json" % (
+ self._provider_config.get_api_uri(),
+ api_version)
+ logger.debug('Downloading soledad config from: %s' % config_uri)
+
+ srp_auth = SRPAuth(self._provider_config)
+ session_id = srp_auth.get_session_id()
+ cookies = None
+ if session_id:
+ cookies = {"_session_id": session_id}
+
+ res = self._session.get(config_uri,
+ verify=self._provider_config
+ .get_ca_cert_path(),
+ headers=headers,
+ cookies=cookies)
+ res.raise_for_status()
+
+ self._soledad_config.set_api_version(api_version)
+
+ # Not modified
+ if res.status_code == 304:
+ logger.debug("Soledad definition has not been modified")
+ self._soledad_config.load(
+ os.path.join(
+ "leap", "providers",
+ self._provider_config.get_domain(),
+ "soledad-service.json"))
+ else:
+ soledad_definition, mtime = get_content(res)
+
+ self._soledad_config.load(data=soledad_definition, mtime=mtime)
+ self._soledad_config.save(["leap",
+ "providers",
+ self._provider_config.get_domain(),
+ "soledad-service.json"])
+
+ self._load_and_sync_soledad(srp_auth)
+
+ def _gen_key(self, _):
+ """
+ Generates the key pair if needed, uploads it to the webapp and
+ nickserver
+ """
+ leap_assert(self._provider_config,
+ "We need a provider configuration!")
+
+ address = "%s@%s" % (self._user, self._provider_config.get_domain())
+
+ logger.debug("Retrieving key for %s" % (address,))
+
+ srp_auth = SRPAuth(self._provider_config)
+
+ # TODO: Fix for Windows
+ gpgbin = "/usr/bin/gpg"
+
+ if self._standalone:
+ gpgbin = os.path.join(self._provider_config.get_path_prefix(),
+ "..", "apps", "mail", "gpg")
+
+ self._keymanager = KeyManager(
+ address,
+ "https://nicknym.%s:6425" % (self._provider_config.get_domain(),),
+ self._soledad,
+ #token=srp_auth.get_token(), # TODO: enable token usage
+ session_id=srp_auth.get_session_id(),
+ ca_cert_path=self._provider_config.get_ca_cert_path(),
+ api_uri=self._provider_config.get_api_uri(),
+ api_version=self._provider_config.get_api_version(),
+ uid=srp_auth.get_uid(),
+ gpgbinary=gpgbin)
+ try:
+ self._keymanager.get_key(address, openpgp.OpenPGPKey,
+ private=True, fetch_remote=False)
+ except KeyNotFound:
+ logger.debug("Key not found. Generating key for %s" % (address,))
+ self._keymanager.gen_key(openpgp.OpenPGPKey)
+ self._keymanager.send_key(openpgp.OpenPGPKey)
+ logger.debug("Key generated successfully.")
+
+ def run_soledad_setup_checks(self,
+ provider_config,
+ user,
+ password,
+ download_if_needed=False,
+ standalone=False):
+ """
+ Starts the checks needed for a new soledad setup
+
+ :param provider_config: Provider configuration
+ :type provider_config: ProviderConfig
+ :param user: User's login
+ :type user: str
+ :param password: User's password
+ :type password: str
+ :param download_if_needed: If True, it will only download
+ files if the have changed since the
+ time it was previously downloaded.
+ :type download_if_needed: bool
+ :param standalone: If True, it'll look for paths inside the
+ bundle (like for gpg)
+ :type standalone: bool
+ """
+ leap_assert_type(provider_config, ProviderConfig)
+
+ self._provider_config = provider_config
+ self._download_if_needed = download_if_needed
+ self._user = user
+ self._password = password
+ self._standalone = standalone
+
+ cb_chain = [
+ (self._download_config, self.download_config),
+ (self._gen_key, self.gen_key)
+ ]
+
+ self.addCallbackChain(cb_chain)
diff --git a/src/leap/services/soledad/soledadconfig.py b/src/leap/services/soledad/soledadconfig.py
new file mode 100644
index 00000000..a2367692
--- /dev/null
+++ b/src/leap/services/soledad/soledadconfig.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+# soledadconfig.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Soledad configuration
+"""
+import logging
+
+from leap.common.config.baseconfig import BaseConfig
+from leap.services.soledad.soledadspec import get_schema
+
+logger = logging.getLogger(__name__)
+
+
+class SoledadConfig(BaseConfig):
+ """
+ Soledad configuration abstraction class
+ """
+
+ def __init__(self):
+ BaseConfig.__init__(self)
+
+ def _get_schema(self):
+ """
+ Returns the schema corresponding to the version given.
+
+ :rtype: dict or None if the version is not supported.
+ """
+ return get_schema(self._api_version)
+
+ def get_hosts(self):
+ return self._safe_get_value("hosts")
+
+ def get_locations(self):
+ return self._safe_get_value("locations")
diff --git a/src/leap/services/soledad/soledadspec.py b/src/leap/services/soledad/soledadspec.py
new file mode 100644
index 00000000..111175dd
--- /dev/null
+++ b/src/leap/services/soledad/soledadspec.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+# soledadspec.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Schemas dict
+# To add a schema for a version you should follow the form:
+# { '1': schema_v1, '2': schema_v2, ... etc }
+# so for instance, to add the '2' version, you should do:
+# soledad_config_spec['2'] = schema_v2
+soledad_config_spec = {}
+
+soledad_config_spec['1'] = {
+ 'description': 'sample soledad service config',
+ 'type': 'object',
+ 'properties': {
+ 'serial': {
+ 'type': int,
+ 'default': 1,
+ 'required': ["True"]
+ },
+ 'version': {
+ 'type': int,
+ 'default': 1,
+ 'required': ["True"]
+ },
+ 'hosts': {
+ 'type': dict,
+ 'default': {
+ "python": {
+ "hostname": "someprovider",
+ "ip_address": "1.1.1.1",
+ "location": "loc",
+ "port": 1111
+ },
+ },
+ },
+ 'locations': {
+ 'type': dict,
+ 'default': {
+ "locations": {
+ "ankara": {
+ "country_code": "TR",
+ "hemisphere": "N",
+ "name": "loc",
+ "timezone": "+0"
+ }
+ }
+ }
+ }
+ }
+}
+
+
+def get_schema(version):
+ """
+ Returns the schema corresponding to the version given.
+
+ :param version: the version of the schema to get.
+ :type version: str
+ :rtype: dict or None if the version is not supported.
+ """
+ schema = soledad_config_spec.get(version, None)
+ return schema
diff --git a/src/leap/testing/__init__.py b/src/leap/services/tests/__init__.py
index e69de29b..e69de29b 100644
--- a/src/leap/testing/__init__.py
+++ b/src/leap/services/tests/__init__.py
diff --git a/src/leap/services/tests/test_abstractbootstrapper.py b/src/leap/services/tests/test_abstractbootstrapper.py
new file mode 100644
index 00000000..a9ee220f
--- /dev/null
+++ b/src/leap/services/tests/test_abstractbootstrapper.py
@@ -0,0 +1,196 @@
+## -*- coding: utf-8 -*-
+# test_abstrctbootstrapper.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+"""
+Tests for the Abstract Boostrapper functionality
+"""
+
+import mock
+
+from PySide import QtCore
+
+from nose.twistedtools import deferred
+
+from leap.services.abstractbootstrapper import AbstractBootstrapper
+from leap.util.pyside_tests_helper import UsesQApplication, BasicPySlotCase
+
+
+class TesterBootstrapper(AbstractBootstrapper):
+ test_signal1 = QtCore.Signal(dict)
+ test_signal2 = QtCore.Signal(dict)
+ test_signal3 = QtCore.Signal(dict)
+
+ ERROR_MSG = "This is a test error msg"
+
+ def _check_that_passes(self, *args):
+ pass
+
+ def _second_check_that_passes(self, *args):
+ pass
+
+ def _check_that_fails(self, *args):
+ raise Exception(self.ERROR_MSG)
+
+ def run_checks_pass(self):
+ cb_chain = [
+ (self._check_that_passes, self.test_signal1),
+ (self._second_check_that_passes, self.test_signal2),
+ ]
+ return self.addCallbackChain(cb_chain)
+
+ def run_second_checks_pass(self):
+ cb_chain = [
+ (self._check_that_passes, None),
+ ]
+ return self.addCallbackChain(cb_chain)
+
+ def run_checks_fail(self):
+ cb_chain = [
+ (self._check_that_passes, self.test_signal1),
+ (self._check_that_fails, self.test_signal2)
+ ]
+ return self.addCallbackChain(cb_chain)
+
+ def run_second_checks_fail(self):
+ cb_chain = [
+ (self._check_that_passes, self.test_signal1),
+ (self._check_that_fails, self.test_signal2),
+ (self._second_check_that_passes, self.test_signal1)
+ ]
+ return self.addCallbackChain(cb_chain)
+
+ def run_third_checks_fail(self):
+ cb_chain = [
+ (self._check_that_passes, self.test_signal1),
+ (self._check_that_fails, None)
+ ]
+ return self.addCallbackChain(cb_chain)
+
+
+class AbstractBootstrapperTest(UsesQApplication, BasicPySlotCase):
+ def setUp(self):
+ UsesQApplication.setUp(self)
+ BasicPySlotCase.setUp(self)
+
+ self.tbt = TesterBootstrapper()
+ self.called1 = self.called2 = 0
+
+ @deferred()
+ def test_all_checks_executed_once(self):
+ self.tbt._check_that_passes = mock.MagicMock()
+ self.tbt._second_check_that_passes = mock.MagicMock()
+
+ d = self.tbt.run_checks_pass()
+
+ def check(*args):
+ self.tbt._check_that_passes.assert_called_once_with()
+ self.tbt._second_check_that_passes.\
+ assert_called_once_with(None)
+
+ d.addCallback(check)
+ return d
+
+ #######################################################################
+ # Dummy callbacks that test the arguments expected from a certain
+ # signal and only allow being called once
+
+ def cb1(self, *args):
+ if tuple(self.args1) == args:
+ self.called1 += 1
+ else:
+ raise ValueError('Invalid arguments for callback')
+
+ def cb2(self, *args):
+ if tuple(self.args2) == args:
+ self.called2 += 1
+ else:
+ raise ValueError('Invalid arguments for callback')
+
+ #
+ #######################################################################
+
+ def _check_cb12_once(self, *args):
+ self.assertEquals(self.called1, 1)
+ self.assertEquals(self.called2, 1)
+
+ @deferred()
+ def test_emits_correct(self):
+ self.tbt.test_signal1.connect(self.cb1)
+ self.tbt.test_signal2.connect(self.cb2)
+ d = self.tbt.run_checks_pass()
+
+ self.args1 = [{
+ AbstractBootstrapper.PASSED_KEY: True,
+ AbstractBootstrapper.ERROR_KEY: ""
+ }]
+
+ self.args2 = self.args1
+
+ d.addCallback(self._check_cb12_once)
+ return d
+
+ @deferred()
+ def test_emits_failed(self):
+ self.tbt.test_signal1.connect(self.cb1)
+ self.tbt.test_signal2.connect(self.cb2)
+ d = self.tbt.run_checks_fail()
+
+ self.args1 = [{
+ AbstractBootstrapper.PASSED_KEY: True,
+ AbstractBootstrapper.ERROR_KEY: ""
+ }]
+
+ self.args2 = [{
+ AbstractBootstrapper.PASSED_KEY: False,
+ AbstractBootstrapper.ERROR_KEY:
+ TesterBootstrapper.ERROR_MSG
+ }]
+
+ d.addCallback(self._check_cb12_once)
+ return d
+
+ @deferred()
+ def test_emits_failed_and_stops(self):
+ self.tbt.test_signal1.connect(self.cb1)
+ self.tbt.test_signal2.connect(self.cb2)
+ self.tbt.test_signal3.connect(self.cb1)
+ d = self.tbt.run_second_checks_fail()
+
+ self.args1 = [{
+ AbstractBootstrapper.PASSED_KEY: True,
+ AbstractBootstrapper.ERROR_KEY: ""
+ }]
+
+ self.args2 = [{
+ AbstractBootstrapper.PASSED_KEY: False,
+ AbstractBootstrapper.ERROR_KEY:
+ TesterBootstrapper.ERROR_MSG
+ }]
+
+ d.addCallback(self._check_cb12_once)
+ return d
+
+ @deferred()
+ def test_failed_without_signal(self):
+ d = self.tbt.run_third_checks_fail()
+ return d
+
+ @deferred()
+ def test_sucess_without_signal(self):
+ d = self.tbt.run_second_checks_pass()
+ return d
diff --git a/src/leap/services/tx.py b/src/leap/services/tx.py
new file mode 100644
index 00000000..7da1cb01
--- /dev/null
+++ b/src/leap/services/tx.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# twisted.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Twisted services launched by the client
+"""
+import logging
+
+from twisted.application.service import Application
+from twisted.internet.task import LoopingCall
+
+logger = logging.getLogger(__name__)
+
+
+def task():
+ """
+ stub periodic task, mainly for tests.
+ DELETE-ME when there's real meat here :)
+ """
+ from datetime import datetime
+ logger.debug("hi there %s", datetime.now())
+
+
+def leap_services():
+ """
+ Check which twisted services are enabled and
+ register them.
+ """
+ logger.debug('starting leap services')
+ application = Application("Bitmask Local Services")
+ #lc = LoopingCall(task)
+ #lc.start(5)
+ return application
diff --git a/src/leap/testing/basetest.py b/src/leap/testing/basetest.py
deleted file mode 100644
index 3186e1eb..00000000
--- a/src/leap/testing/basetest.py
+++ /dev/null
@@ -1,85 +0,0 @@
-import os
-import platform
-import shutil
-import tempfile
-
-try:
- import unittest2 as unittest
-except ImportError:
- import unittest
-
-from leap.base.config import get_username, get_groupname
-from leap.util.fileutil import mkdir_p, check_and_fix_urw_only
-
-_system = platform.system()
-
-
-class BaseLeapTest(unittest.TestCase):
-
- __name__ = "leap_test"
-
- @classmethod
- def setUpClass(cls):
- cls.old_path = os.environ['PATH']
- cls.old_home = os.environ['HOME']
- cls.tempdir = tempfile.mkdtemp(prefix="leap_tests-")
- cls.home = cls.tempdir
- bin_tdir = os.path.join(
- cls.tempdir,
- 'bin')
- os.environ["PATH"] = bin_tdir
- os.environ["HOME"] = cls.tempdir
-
- @classmethod
- def tearDownClass(cls):
- os.environ["PATH"] = cls.old_path
- os.environ["HOME"] = cls.old_home
- # safety check
- assert cls.tempdir.startswith('/tmp/leap_tests-')
- shutil.rmtree(cls.tempdir)
-
- # you have to override these methods
- # this way we ensure we did not put anything
- # here that you can forget to call.
-
- def setUp(self):
- raise NotImplementedError("abstract base class")
-
- def tearDown(self):
- raise NotImplementedError("abstract base class")
-
- #
- # helper methods
- #
-
- def get_tempfile(self, filename):
- return os.path.join(self.tempdir, filename)
-
- def get_username(self):
- return get_username()
-
- def get_groupname(self):
- return get_groupname()
-
- def _missing_test_for_plat(self, do_raise=False):
- if do_raise:
- raise NotImplementedError(
- "This test is not implemented "
- "for the running platform: %s" %
- _system)
-
- def touch(self, filepath):
- folder, filename = os.path.split(filepath)
- if not os.path.isdir(folder):
- mkdir_p(folder)
- # XXX should move to test_basetest
- self.assertTrue(os.path.isdir(folder))
-
- with open(filepath, 'w') as fp:
- fp.write(' ')
-
- # XXX should move to test_basetest
- self.assertTrue(os.path.isfile(filepath))
-
- def chmod600(self, filepath):
- check_and_fix_urw_only(filepath)
diff --git a/src/leap/testing/cacert.pem b/src/leap/testing/cacert.pem
deleted file mode 100644
index 6989c480..00000000
--- a/src/leap/testing/cacert.pem
+++ /dev/null
@@ -1,23 +0,0 @@
------BEGIN CERTIFICATE-----
-MIID1TCCAr2gAwIBAgIJAOv0BS09D8byMA0GCSqGSIb3DQEBBQUAMIGAMQswCQYD
-VQQGEwJVUzETMBEGA1UECAwKY3liZXJzcGFjZTEnMCUGA1UECgweTEVBUCBFbmNy
-eXB0aW9uIEFjY2VzcyBQcm9qZWN0MRYwFAYDVQQDDA10ZXN0cy1sZWFwLnNlMRsw
-GQYJKoZIhvcNAQkBFgxpbmZvQGxlYXAuc2UwHhcNMTIwODMxMTYyNjMwWhcNMTUw
-ODMxMTYyNjMwWjCBgDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCmN5YmVyc3BhY2Ux
-JzAlBgNVBAoMHkxFQVAgRW5jcnlwdGlvbiBBY2Nlc3MgUHJvamVjdDEWMBQGA1UE
-AwwNdGVzdHMtbGVhcC5zZTEbMBkGCSqGSIb3DQEJARYMaW5mb0BsZWFwLnNlMIIB
-IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1pU7OU+abrUXFZwp6X0LlF0f
-xQvC1Nmr5sFH7N9RTu3bdwY2t57ECP2TPkH6+x7oOvCTgAMxIE1scWEEkfgKViqW
-FH/Om1UW1PMaiDYGtFuqEuxM95FvaYxp2K6rzA37WNsedA28sCYzhRD+/5HqbCNT
-3rRS2cPaVO8kXI/5bgd8bUk3009pWTg4SvTtOW/9MWJbBH5f5JWmMn7Ayt6hIdT/
-E6npofEK/UCqAlEscARYFXSB/F8nK1whjo9mGFjMUd7d/25UbFHqOk4K7ishD4DH
-F7LaS84rS+Sjwn3YtDdDQblGghJfz8X1AfPSGivGnvLVdkmMF9Y2hJlSQ7+C5wID
-AQABo1AwTjAdBgNVHQ4EFgQUnpJEv4FnlqKbfm7mprudKdrnOAowHwYDVR0jBBgw
-FoAUnpJEv4FnlqKbfm7mprudKdrnOAowDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B
-AQUFAAOCAQEAGW66qwdK/ATRVZkTpI2sgi+2dWD5tY4VyZuJIrRwfXsGPeVvmdsa
-zDmwW5dMkth1Of5yO6o7ijvUvfnw/UCLNLNICKZhH5G0DHstfBeFc0jnP2MqOZCp
-puRGPBlO2nxUCvoGcPRUKGQK9XSYmxcmaSFyzKVDMLnmH+Lakj5vaY9a8ZAcZTz7
-T5qePxKAxg+RIlH8Ftc485QP3fhqPYPrRsL3g6peiqCvIRshoP1MSoh19boI+1uX
-wHQ/NyDkL5ErKC5JCSpaeF8VG1ek570kKWQLuQAbnlXZw+Sqfu35CIdizHaYGEcx
-xA8oXH4L2JaT2x9GKDSpCmB2xXy/NVamUg==
------END CERTIFICATE-----
diff --git a/src/leap/testing/https_server.py b/src/leap/testing/https_server.py
deleted file mode 100644
index 21191c32..00000000
--- a/src/leap/testing/https_server.py
+++ /dev/null
@@ -1,68 +0,0 @@
-from BaseHTTPServer import HTTPServer
-import os
-import ssl
-import SocketServer
-import threading
-import unittest
-
-_where = os.path.split(__file__)[0]
-
-
-def where(filename):
- return os.path.join(_where, filename)
-
-
-class HTTPSServer(HTTPServer):
- def server_bind(self):
- SocketServer.TCPServer.server_bind(self)
- self.socket = ssl.wrap_socket(
- self.socket, server_side=True,
- certfile=where("leaptestscert.pem"),
- keyfile=where("leaptestskey.pem"),
- ca_certs=where("cacert.pem"),
- ssl_version=ssl.PROTOCOL_SSLv23)
-
-
-class TestServerThread(threading.Thread):
- def __init__(self, test_object, request_handler):
- threading.Thread.__init__(self)
- self.request_handler = request_handler
- self.test_object = test_object
-
- def run(self):
- self.server = HTTPSServer(('localhost', 0), self.request_handler)
- host, port = self.server.socket.getsockname()
- self.test_object.HOST, self.test_object.PORT = host, port
- self.test_object.server_started.set()
- self.test_object = None
- try:
- self.server.serve_forever(0.05)
- finally:
- self.server.server_close()
-
- def stop(self):
- self.server.shutdown()
-
-
-class BaseHTTPSServerTestCase(unittest.TestCase):
- """
- derived classes need to implement a request_handler
- """
- def setUp(self):
- self.server_started = threading.Event()
- self.thread = TestServerThread(self, self.request_handler)
- self.thread.start()
- self.server_started.wait()
-
- def tearDown(self):
- self.thread.stop()
-
- def get_server(self):
- host, port = self.HOST, self.PORT
- if host == "127.0.0.1":
- host = "localhost"
- return "%s:%s" % (host, port)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/testing/leaptestscert.pem b/src/leap/testing/leaptestscert.pem
deleted file mode 100644
index 65596b1a..00000000
--- a/src/leap/testing/leaptestscert.pem
+++ /dev/null
@@ -1,84 +0,0 @@
-Certificate:
- Data:
- Version: 3 (0x2)
- Serial Number:
- eb:f4:05:2d:3d:0f:c6:f3
- Signature Algorithm: sha1WithRSAEncryption
- Issuer: C=US, ST=cyberspace, O=LEAP Encryption Access Project, CN=tests-leap.se/emailAddress=info@leap.se
- Validity
- Not Before: Aug 31 16:30:17 2012 GMT
- Not After : Aug 31 16:30:17 2013 GMT
- Subject: C=US, ST=cyberspace, L=net, O=LEAP Encryption Access Project, CN=localhost/emailAddress=info@leap.se
- Subject Public Key Info:
- Public Key Algorithm: rsaEncryption
- Public-Key: (2048 bit)
- Modulus:
- 00:bc:f1:c4:05:ce:4b:d5:9b:9a:fa:c1:a5:0c:89:
- 15:7e:05:69:b6:a4:62:38:3a:d6:14:4a:36:aa:3c:
- 31:70:54:2e:bf:7d:05:19:ad:7b:0c:a9:a6:7d:46:
- be:83:62:cb:ea:b9:48:6c:7d:78:a0:10:0b:ad:8a:
- 74:7a:b8:ff:32:85:64:36:90:dc:38:dd:90:6e:07:
- 82:70:ae:5f:4e:1f:f4:46:98:f3:98:b4:fa:08:65:
- bf:d6:ec:a9:ba:7e:a8:f0:40:a2:d0:1a:cb:e6:fc:
- 95:c5:54:63:92:5b:b8:0a:36:cc:26:d3:2b:ad:16:
- ff:49:53:f4:65:7c:64:27:9a:f5:12:75:11:a5:0c:
- 5a:ea:1e:e4:31:f3:a6:2b:db:0e:4a:5d:aa:47:3a:
- f0:5e:2a:d5:6f:74:b6:f8:bc:9a:73:d0:fa:8a:be:
- a8:69:47:9b:07:45:d9:b5:cd:1c:9b:c5:41:9a:65:
- cc:99:a0:bd:bf:b5:e8:9f:66:5f:69:c9:6d:c8:68:
- 50:68:74:ae:8e:12:7e:9c:24:4f:dc:05:61:b7:8a:
- 6d:2a:95:43:d9:3f:fe:d8:c9:a7:ae:63:cd:30:d5:
- 95:84:18:2d:12:b5:2d:a6:fe:37:dd:74:b8:f8:a5:
- 59:18:8f:ca:f7:ae:63:0d:9d:66:51:7d:9c:40:48:
- 9b:a1
- Exponent: 65537 (0x10001)
- X509v3 extensions:
- X509v3 Basic Constraints:
- CA:FALSE
- Netscape Comment:
- OpenSSL Generated Certificate
- X509v3 Subject Key Identifier:
- B2:50:B4:C6:38:8F:BA:C4:3B:69:4C:6B:45:7C:CF:08:48:36:02:E0
- X509v3 Authority Key Identifier:
- keyid:9E:92:44:BF:81:67:96:A2:9B:7E:6E:E6:A6:BB:9D:29:DA:E7:38:0A
-
- Signature Algorithm: sha1WithRSAEncryption
- aa:ab:d4:27:e3:cb:42:05:55:fd:24:b3:e5:55:7d:fb:ce:6c:
- ff:c7:96:f0:7d:30:a1:53:4a:04:eb:a4:24:5e:96:ee:65:ef:
- e5:aa:08:47:9d:aa:95:2a:bb:6a:28:9f:51:62:63:d9:7d:1a:
- 81:a0:72:f7:9f:33:6b:3b:f4:dc:85:cd:2a:ee:83:a9:93:3d:
- 75:53:91:fa:0b:1b:10:83:11:2c:03:4e:ac:bf:c3:e6:25:74:
- 9f:14:13:4a:43:66:c2:d7:1c:6c:94:3e:a6:f3:a5:bd:01:2c:
- 9f:20:29:2e:62:82:12:d8:8b:70:1b:88:2b:18:68:5a:45:80:
- 46:2a:6a:d5:df:1f:d3:e8:57:39:0a:be:1a:d8:b0:3e:e5:b6:
- c3:69:b7:5e:c0:7b:b3:a8:a6:78:ee:0a:3d:a0:74:40:fb:42:
- 9f:f4:98:7f:47:cc:15:28:eb:b1:95:77:82:a8:65:9b:46:c3:
- 4f:f9:f4:72:be:bd:24:28:5c:0d:b3:89:e4:13:71:c8:a7:54:
- 1b:26:15:f3:c1:b2:a9:13:77:54:c2:b9:b0:c7:24:39:00:4c:
- 1a:a7:9b:e7:ad:4a:3a:32:c2:81:0d:13:2d:27:ea:98:00:a9:
- 0e:9e:38:3b:8f:80:34:17:17:3d:49:7e:f4:a5:19:05:28:08:
- 7d:de:d3:1f
------BEGIN CERTIFICATE-----
-MIIECjCCAvKgAwIBAgIJAOv0BS09D8bzMA0GCSqGSIb3DQEBBQUAMIGAMQswCQYD
-VQQGEwJVUzETMBEGA1UECAwKY3liZXJzcGFjZTEnMCUGA1UECgweTEVBUCBFbmNy
-eXB0aW9uIEFjY2VzcyBQcm9qZWN0MRYwFAYDVQQDDA10ZXN0cy1sZWFwLnNlMRsw
-GQYJKoZIhvcNAQkBFgxpbmZvQGxlYXAuc2UwHhcNMTIwODMxMTYzMDE3WhcNMTMw
-ODMxMTYzMDE3WjCBijELMAkGA1UEBhMCVVMxEzARBgNVBAgMCmN5YmVyc3BhY2Ux
-DDAKBgNVBAcMA25ldDEnMCUGA1UECgweTEVBUCBFbmNyeXB0aW9uIEFjY2VzcyBQ
-cm9qZWN0MRIwEAYDVQQDDAlsb2NhbGhvc3QxGzAZBgkqhkiG9w0BCQEWDGluZm9A
-bGVhcC5zZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALzxxAXOS9Wb
-mvrBpQyJFX4FabakYjg61hRKNqo8MXBULr99BRmtewyppn1GvoNiy+q5SGx9eKAQ
-C62KdHq4/zKFZDaQ3DjdkG4HgnCuX04f9EaY85i0+ghlv9bsqbp+qPBAotAay+b8
-lcVUY5JbuAo2zCbTK60W/0lT9GV8ZCea9RJ1EaUMWuoe5DHzpivbDkpdqkc68F4q
-1W90tvi8mnPQ+oq+qGlHmwdF2bXNHJvFQZplzJmgvb+16J9mX2nJbchoUGh0ro4S
-fpwkT9wFYbeKbSqVQ9k//tjJp65jzTDVlYQYLRK1Lab+N910uPilWRiPyveuYw2d
-ZlF9nEBIm6ECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3Bl
-blNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFLJQtMY4j7rEO2lM
-a0V8zwhINgLgMB8GA1UdIwQYMBaAFJ6SRL+BZ5aim35u5qa7nSna5zgKMA0GCSqG
-SIb3DQEBBQUAA4IBAQCqq9Qn48tCBVX9JLPlVX37zmz/x5bwfTChU0oE66QkXpbu
-Ze/lqghHnaqVKrtqKJ9RYmPZfRqBoHL3nzNrO/Tchc0q7oOpkz11U5H6CxsQgxEs
-A06sv8PmJXSfFBNKQ2bC1xxslD6m86W9ASyfICkuYoIS2ItwG4grGGhaRYBGKmrV
-3x/T6Fc5Cr4a2LA+5bbDabdewHuzqKZ47go9oHRA+0Kf9Jh/R8wVKOuxlXeCqGWb
-RsNP+fRyvr0kKFwNs4nkE3HIp1QbJhXzwbKpE3dUwrmwxyQ5AEwap5vnrUo6MsKB
-DRMtJ+qYAKkOnjg7j4A0Fxc9SX70pRkFKAh93tMf
------END CERTIFICATE-----
diff --git a/src/leap/testing/leaptestskey.pem b/src/leap/testing/leaptestskey.pem
deleted file mode 100644
index fe6291a1..00000000
--- a/src/leap/testing/leaptestskey.pem
+++ /dev/null
@@ -1,27 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-MIIEpQIBAAKCAQEAvPHEBc5L1Zua+sGlDIkVfgVptqRiODrWFEo2qjwxcFQuv30F
-Ga17DKmmfUa+g2LL6rlIbH14oBALrYp0erj/MoVkNpDcON2QbgeCcK5fTh/0Rpjz
-mLT6CGW/1uypun6o8ECi0BrL5vyVxVRjklu4CjbMJtMrrRb/SVP0ZXxkJ5r1EnUR
-pQxa6h7kMfOmK9sOSl2qRzrwXirVb3S2+Lyac9D6ir6oaUebB0XZtc0cm8VBmmXM
-maC9v7Xon2ZfacltyGhQaHSujhJ+nCRP3AVht4ptKpVD2T/+2MmnrmPNMNWVhBgt
-ErUtpv433XS4+KVZGI/K965jDZ1mUX2cQEiboQIDAQABAoIBAQCh/+yhSbrtoCgm
-PegEsnix/3QfPBxWt+Obq/HozglZlWQrnMbFuF+bgM4V9ZUdU5UhYNF+66mEG53X
-orGyE3IDYCmHO3cGbroKDPhDIs7mTjGEYlniIbGLh6oPXgU8uKKis9ik84TGPOUx
-NuTUtT07zLYHx+FX3DLwLUKLzTaWWSRgA7nxNwCY8aPqDxCkXEyZHvSlm9KYZnhe
-nVevycoHR+chxL6X/ebbBt2FKR7tl4328mlDXvMXr0vahPH94CuXEvfTj+f6ZxZF
-OctdikyRfd8O3ebrUw0XjafPYyTsDMH0/rQovEBVlecEHqh6Z9dBFlogRq5DSun9
-jem4bBXRAoGBAPGPi4g21pTQPqTFxpqea8TsPqIfo3csfMDPdzT246MxzALHqCfG
-yZi4g2JYJrReSWHulZDORO5skSKNEb5VTA/3xFhKLt8CULZOakKBDLkzRXlnDFXg
-Jsu9vtjDWjQcJsdsRx1tc5V6s+hmel70aaUu/maUlEYZnyIXaTe+1SB1AoGBAMg9
-EMEO5YN52pOI5qPH8j7uyVKtZWKRiR6jb5KA5TxWqZalSdPV6YwDqV/e+HjWrZNw
-kSEFONY0seKpIHwXchx91aym7rDHUgOoBQfCWufRMYvRXLhfOTBu4X+U52++i8wt
-FvKgh6eSmc7VayAaDfHp7yfrIfS03IiN0T35mGj9AoGAPCoXg7a83VW8tId5/trE
-VsjMlM6yhSU0cUV7GFsBuYzWlj6qODX/0iTqvFzeTwBI4LZu1CE78/Jgd62RJMnT
-5wo8Ag1//RVziuSe/K9tvtbxT9qFrQHmR8qbtRt65Q257uOeFstDBZEJLDIR+oJ/
-qZ+5x0zsXUVWaERSdYr3RF0CgYEApKDgN3oB5Ti4Jnh1984aMver+heptYKmU9RX
-lQH4dsVhpQO8UTgcTgtso+/0JZWLHB9+ksFyW1rzrcETfjLglOA4XzzYHeuiWHM5
-v4lhqBpsO+Ij80oHAPUI3RYVud/VnEauCUlGftWfM1hwPPJu6KhHAnDleAWDE5pV
-oDinwBkCgYEAnn/OceaqA2fNYp1IRegbFzpewjUlHLq3bXiCIVhO7W/HqsdfUxjE
-VVdjEno/pAG7ZCO5j8u+rLkG2ZIVY3qsUENUiXz52Q08qEltgM8nfirK7vIQkfd9
-YISRE3QHYJd+ArY4v+7rNeF1O5eIEyzPAbvG5raeZFcZ6POxy66uWKo=
------END RSA PRIVATE KEY-----
diff --git a/src/leap/testing/pyqt.py b/src/leap/testing/pyqt.py
deleted file mode 100644
index 6edaf059..00000000
--- a/src/leap/testing/pyqt.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from PyQt4 import QtCore
-
-_oldConnect = QtCore.QObject.connect
-_oldDisconnect = QtCore.QObject.disconnect
-_oldEmit = QtCore.QObject.emit
-
-
-def _wrapConnect(callableObject):
- """
- Returns a wrapped call to the old version of QtCore.QObject.connect
- """
- @staticmethod
- def call(*args):
- callableObject(*args)
- _oldConnect(*args)
- return call
-
-
-def _wrapDisconnect(callableObject):
- """
- Returns a wrapped call to the old version of QtCore.QObject.disconnect
- """
- @staticmethod
- def call(*args):
- callableObject(*args)
- _oldDisconnect(*args)
- return call
-
-
-def enableSignalDebugging(**kwargs):
- """
- Call this to enable Qt Signal debugging. This will trap all
- connect, and disconnect calls.
- """
-
- f = lambda *args: None
- connectCall = kwargs.get('connectCall', f)
- disconnectCall = kwargs.get('disconnectCall', f)
- emitCall = kwargs.get('emitCall', f)
-
- def printIt(msg):
- def call(*args):
- print msg, args
- return call
- QtCore.QObject.connect = _wrapConnect(connectCall)
- QtCore.QObject.disconnect = _wrapDisconnect(disconnectCall)
-
- def new_emit(self, *args):
- emitCall(self, *args)
- _oldEmit(self, *args)
-
- QtCore.QObject.emit = new_emit
diff --git a/src/leap/testing/qunittest.py b/src/leap/testing/qunittest.py
deleted file mode 100644
index b89ccec3..00000000
--- a/src/leap/testing/qunittest.py
+++ /dev/null
@@ -1,302 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# **qunittest** is an standard Python `unittest` enhancement for PyQt4,
-# allowing
-# you to test asynchronous code using standard synchronous testing facility.
-#
-# The source for `qunittest` is available on [GitHub][gh], and released under
-# the MIT license.
-#
-# Slightly modified by The Leap Project.
-
-### Prerequisites
-
-# Import unittest2 or unittest
-try:
- import unittest2 as unittest
-except ImportError:
- import unittest
-
-# ... and some standard Python libraries
-import sys
-import functools
-import contextlib
-import re
-
-# ... and several PyQt classes
-from PyQt4.QtCore import QTimer
-from PyQt4.QtTest import QTest
-from PyQt4 import QtGui
-
-### The code
-
-
-# Override standard main method, by invoking it inside PyQt event loop
-
-def main(*args, **kwargs):
- qapplication = QtGui.QApplication(sys.argv)
-
- QTimer.singleShot(0, unittest.main(*args, **kwargs))
- qapplication.exec_()
-
-"""
-This main substitute does not integrate with unittest.
-
-Note about mixing the event loop and unittests:
-
-Unittest will fail if we keep more than one reference to a QApplication.
-(pyqt expects to be and only one).
-So, for the things that need a QApplication to exist, do something like:
-
- self.app = QApplication()
- QtGui.qApp = self.app
-
-in the class setUp, and::
-
- QtGui.qApp = None
- self.app = None
-
-in the class tearDown.
-
-For some explanation about this, see
- http://stuvel.eu/blog/127/multiple-instances-of-qapplication-in-one-process
-and
- http://www.riverbankcomputing.com/pipermail/pyqt/2010-September/027705.html
-"""
-
-
-# Helper returning the name of a given signal
-
-def _signal_name(signal):
- s = repr(signal)
- name_re = "signal (\w+) of (\w+)"
- match = re.search(name_re, s, re.I)
- if not match:
- return "??"
- return "%s#%s" % (match.group(2), match.group(1))
-
-
-class _SignalConnector(object):
- """ Encapsulates signal assertion testing """
- def __init__(self, test, signal, callable_):
- self.test = test
- self.callable_ = callable_
- self.called_with = None
- self.emited = False
- self.signal = signal
- self._asserted = False
-
- signal.connect(self.on_signal_emited)
-
- # Store given parameters and mark signal as `emited`
- def on_signal_emited(self, *args, **kwargs):
- self.called_with = (args, kwargs)
- self.emited = True
-
- def assertEmission(self):
- # Assert once wheter signal was emited or not
- was_asserted = self._asserted
- self._asserted = True
-
- if not was_asserted:
- if not self.emited:
- self.test.fail(
- "signal %s not emited" % (_signal_name(self.signal)))
-
- # Call given callable is necessary
- if self.callable_:
- args, kwargs = self.called_with
- self.callable_(*args, **kwargs)
-
- def __enter__(self):
- # Assert emission when context is entered
- self.assertEmission()
- return self.called_with
-
- def __exit__(self, *_):
- return False
-
-### Unit Testing
-
-# `qunittest` does not force much abould how test should look - it just adds
-# several helpers for asynchronous code testing.
-#
-# Common test case may look like this:
-#
-# import qunittest
-# from calculator import Calculator
-#
-# class TestCalculator(qunittest.TestCase):
-# def setUp(self):
-# self.calc = Calculator()
-#
-# def test_should_add_two_numbers_synchronously(self):
-# # given
-# a, b = 2, 3
-#
-# # when
-# r = self.calc.add(a, b)
-#
-# # then
-# self.assertEqual(5, r)
-#
-# def test_should_calculate_factorial_in_background(self):
-# # given
-#
-# # when
-# self.calc.factorial(20)
-#
-# # then
-# self.assertEmited(self.calc.done) with (args, kwargs):
-# self.assertEqual([2432902008176640000], args)
-#
-# if __name__ == "__main__":
-# main()
-#
-# Test can be run by typing:
-#
-# python test_calculator.py
-#
-# Automatic test discovery is not supported now, because testing PyQt needs
-# an instance of `QApplication` and its `exec_` method is blocking.
-#
-
-
-### TestCase class
-
-class TestCase(unittest.TestCase):
- """
- Extends standard `unittest.TestCase` with several PyQt4 testing features
- useful for asynchronous testing.
- """
- def __init__(self, *args, **kwargs):
- super(TestCase, self).__init__(*args, **kwargs)
-
- self._clearSignalConnectors()
- self._succeeded = False
- self.addCleanup(self._clearSignalConnectors)
- self.tearDown = self._decorateTearDown(self.tearDown)
-
- ### Protected methods
-
- def _clearSignalConnectors(self):
- self._connectedSignals = []
-
- def _decorateTearDown(self, tearDown):
- @functools.wraps(tearDown)
- def decorator():
- self._ensureEmitedSignals()
- return tearDown()
- return decorator
-
- def _ensureEmitedSignals(self):
- """
- Checks if signals were acually emited. Raises AssertionError if no.
- """
- # TODO: add information about line
- for signal in self._connectedSignals:
- signal.assertEmission()
-
- ### Assertions
-
- def assertEmited(self, signal, callable_=None, timeout=1):
- """
- Asserts if given `signal` was emited. Waits 1 second by default,
- before asserts signal emission.
-
- If `callable_` is given, it should be a function which takes two
- arguments: `args` and `kwargs`. It will be called after blocking
- operation or when assertion about signal emission is made and
- signal was emited.
-
- When timeout is not `False`, method call is blocking, and ends
- after `timeout` seconds. After that time, it validates wether
- signal was emited.
-
- When timeout is `False`, method is non blocking, and test should wait
- for signals afterwards. Otherwise, at the end of the test, all
- signal emissions are checked if appeared.
-
- Function returns context, which yields to list of parameters given
- to signal. It can be useful for testing given parameters. Following
- code:
-
- with self.assertEmited(widget.signal) as (args, kwargs):
- self.assertEqual(1, len(args))
- self.assertEqual("Hello World!", args[0])
-
- will wait 1 second and test for correct parameters, is signal was
- emtied.
-
- Note that code:
-
- with self.assertEmited(widget.signal, timeout=False) as (a, k):
- # Will not be invoked
-
- will always fail since signal cannot be emited in the time of its
- connection - code inside the context will not be invoked at all.
- """
-
- connector = _SignalConnector(self, signal, callable_)
- self._connectedSignals.append(connector)
- if timeout:
- self.waitFor(timeout)
- connector.assertEmission()
-
- return connector
-
- ### Helper methods
-
- @contextlib.contextmanager
- def invokeAfter(self, seconds, callable_=None):
- """
- Waits given amount of time and executes the context.
-
- If `callable_` is given, executes it, instead of context.
- """
- self.waitFor(seconds)
- if callable_:
- callable_()
- else:
- yield
-
- def waitFor(self, seconds):
- """
- Waits given amount of time.
-
- self.widget.loadImage(url)
- self.waitFor(seconds=10)
- """
- QTest.qWait(seconds * 1000)
-
- def succeed(self, bool_=True):
- """ Marks test as suceeded for next `failAfter()` invocation. """
- self._succeeded = self._succeeded or bool_
-
- def failAfter(self, seconds, message=None):
- """
- Waits given amount of time, and fails the test if `succeed(bool)`
- is not called - in most common case, `succeed(bool)` should be called
- asynchronously (in signal handler):
-
- self.widget.signal.connect(lambda: self.succeed())
- self.failAfter(1, "signal not emited?")
-
- After invocation, test is no longer consider as succeeded.
- """
- self.waitFor(seconds)
- if not self._succeeded:
- self.fail(message)
-
- self._succeeded = False
-
-### Credits
-#
-# * **Who is responsible:** [Dawid Fatyga][df]
-# * **Source:** [GitHub][gh]
-# * **Doc. generator:** [rocco][ro]
-#
-# [gh]: https://www.github.com/dejw/qunittest
-# [df]: https://github.com/dejw
-# [ro]: http://rtomayko.github.com/rocco/
-#
diff --git a/src/leap/testing/test_basetest.py b/src/leap/testing/test_basetest.py
deleted file mode 100644
index 14d8f8a3..00000000
--- a/src/leap/testing/test_basetest.py
+++ /dev/null
@@ -1,91 +0,0 @@
-"""becase it's oh so meta"""
-try:
- import unittest2 as unittest
-except ImportError:
- import unittest
-
-import os
-import StringIO
-
-from leap.testing.basetest import BaseLeapTest
-
-# global for tempdir checking
-_tempdir = None
-
-
-class _TestCaseRunner(object):
- def run_testcase(self, testcase=None):
- if not testcase:
- return None
- loader = unittest.TestLoader()
- suite = loader.loadTestsFromTestCase(testcase)
-
- # Create runner, and run testcase
- io = StringIO.StringIO()
- runner = unittest.TextTestRunner(stream=io)
- results = runner.run(suite)
- return results
-
-
-class TestAbstractBaseLeapTest(unittest.TestCase, _TestCaseRunner):
-
- def test_abstract_base_class(self):
- class _BaseTest(BaseLeapTest):
- def test_dummy_method(self):
- pass
-
- def test_tautology(self):
- assert True
-
- results = self.run_testcase(_BaseTest)
-
- # should be 2 errors: NotImplemented
- # raised for setUp/tearDown
- self.assertEquals(results.testsRun, 2)
- self.assertEquals(len(results.failures), 0)
- self.assertEquals(len(results.errors), 2)
-
-
-class TestInitBaseLeapTest(BaseLeapTest):
-
- def setUp(self):
- pass
-
- def tearDown(self):
- pass
-
- def test_path_is_changed(self):
- os_path = os.environ['PATH']
- self.assertTrue(os_path.startswith(self.tempdir))
-
- def test_old_path_is_saved(self):
- self.assertTrue(len(self.old_path) > 1)
-
-
-class TestCleanedBaseLeapTest(unittest.TestCase, _TestCaseRunner):
-
- def test_tempdir_is_cleaned_after_tests(self):
- class _BaseTest(BaseLeapTest):
- def setUp(self):
- global _tempdir
- _tempdir = self.tempdir
-
- def tearDown(self):
- pass
-
- def test_tempdir_created(self):
- self.assertTrue(os.path.isdir(self.tempdir))
-
- def test_tempdir_created_on_setupclass(self):
- self.assertEqual(_tempdir, self.tempdir)
-
- results = self.run_testcase(_BaseTest)
- self.assertEquals(results.testsRun, 2)
- self.assertEquals(len(results.failures), 0)
- self.assertEquals(len(results.errors), 0)
-
- # did we cleaned the tempdir?
- self.assertFalse(os.path.isdir(_tempdir))
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/util/__init__.py b/src/leap/util/__init__.py
index a70a9a8b..f39b52e9 100644
--- a/src/leap/util/__init__.py
+++ b/src/leap/util/__init__.py
@@ -1,9 +1,105 @@
-import logging
-logger = logging.getLogger(__name__)
+# -*- coding: utf-8 -*-
+# __init__.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Initializes version and app info, plus some small and handy functions.
+"""
+import datetime
+import os
+from pkg_resources import parse_version
+
+
+def _is_release_version(version):
+ """
+ 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
+ and dots.
+
+ :param version: the version string
+ :type version: str
+ :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
+
+try:
+ from leap._version import get_versions
+ __version__ = get_versions()['version']
+ 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:
- import pygeoip
- HAS_GEOIP = True
+ from leap._appname import __appname__
except ImportError:
- logger.debug('PyGeoIP not found. Disabled Geo support.')
- HAS_GEOIP = False
+ #running on a tree that has not run
+ #the setup.py setver
+ pass
+
+__full_version__ = __appname__ + '/' + str(__version__)
+
+
+def first(things):
+ """
+ Return the head of a collection.
+ """
+ try:
+ return things[0]
+ except TypeError:
+ return None
+
+
+def get_modification_ts(path):
+ """
+ Gets modification time of a file.
+
+ :param path: the path to get ts from
+ :type path: str
+ :returns: modification time
+ :rtype: datetime object
+ """
+ ts = os.path.getmtime(path)
+ return datetime.datetime.fromtimestamp(ts)
+
+
+def update_modification_ts(path):
+ """
+ Sets modification time of a file to current time.
+
+ :param path: the path to set ts to.
+ :type path: str
+ :returns: modification time
+ :rtype: datetime object
+ """
+ os.utime(path, None)
+ return get_modification_ts(path)
diff --git a/src/leap/util/certs.py b/src/leap/util/certs.py
deleted file mode 100644
index f0f790e9..00000000
--- a/src/leap/util/certs.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import os
-import logging
-
-logger = logging.getLogger(__name__)
-
-
-def get_mac_cabundle():
- # hackaround bundle error
- # XXX this needs a better fix!
- f = os.path.split(__file__)[0]
- sep = os.path.sep
- f_ = sep.join(f.split(sep)[:-2])
- verify = os.path.join(f_, 'cacert.pem')
- #logger.error('VERIFY PATH = %s' % verify)
- exists = os.path.isfile(verify)
- #logger.error('do exist? %s', exists)
- if exists:
- return verify
diff --git a/src/leap/util/constants.py b/src/leap/util/constants.py
new file mode 100644
index 00000000..63f6b1f7
--- /dev/null
+++ b/src/leap/util/constants.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# constants.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+SIGNUP_TIMEOUT = 5
+REQUEST_TIMEOUT = 10
diff --git a/src/leap/util/coroutines.py b/src/leap/util/coroutines.py
deleted file mode 100644
index 0657fc04..00000000
--- a/src/leap/util/coroutines.py
+++ /dev/null
@@ -1,109 +0,0 @@
-# the problem of watching a stdout pipe from
-# openvpn binary: using subprocess and coroutines
-# acting as event consumers
-
-from __future__ import division, print_function
-
-import logging
-from subprocess import PIPE, Popen
-import sys
-from threading import Thread
-
-logger = logging.getLogger(__name__)
-
-ON_POSIX = 'posix' in sys.builtin_module_names
-
-
-#
-# Coroutines goodies
-#
-
-def coroutine(func):
- def start(*args, **kwargs):
- cr = func(*args, **kwargs)
- cr.next()
- return cr
- return start
-
-
-@coroutine
-def process_events(callback):
- """
- coroutine loop that receives
- events sent and dispatch the callback.
- :param callback: callback to be called\
-for each event
- :type callback: callable
- """
- try:
- while True:
- m = (yield)
- if callable(callback):
- callback(m)
- else:
- logger.debug('not a callable passed')
- except GeneratorExit:
- return
-
-#
-# Threads
-#
-
-
-def launch_thread(target, args):
- """
- launch and demonize thread.
- :param target: target function that will run in thread
- :type target: function
- :param args: args to be passed to thread
- :type args: list
- """
- t = Thread(target=target,
- args=args)
- t.daemon = True
- t.start()
- return t
-
-
-def watch_output(out, observers):
- """
- initializes dict of observer coroutines
- and pushes lines to each of them as they are received
- from the watched output.
- :param out: stdout of a process.
- :type out: fd
- :param observers: tuple of coroutines to send data\
-for each event
- :type observers: tuple
- """
- observer_dict = dict(((observer, process_events(observer))
- for observer in observers))
- for line in iter(out.readline, b''):
- for obs in observer_dict:
- observer_dict[obs].send(line)
- out.close()
-
-
-def spawn_and_watch_process(command, args, observers=None):
- """
- spawns a subprocess with command, args, and launch
- a watcher thread.
- :param command: command to be executed in the subprocess
- :type command: str
- :param args: arguments
- :type args: list
- :param observers: tuple of observer functions to be called \
-for each line in the subprocess output.
- :type observers: tuple
- :return: a tuple containing the child process instance, and watcher_thread,
- :rtype: (Subprocess, Thread)
- """
- subp = Popen([command] + args,
- stdout=PIPE,
- stderr=PIPE,
- bufsize=1,
- close_fds=ON_POSIX)
- watcher = launch_thread(
- watch_output,
- (subp.stdout, observers))
- return subp, watcher
diff --git a/src/leap/util/dicts.py b/src/leap/util/dicts.py
deleted file mode 100644
index 001ca96b..00000000
--- a/src/leap/util/dicts.py
+++ /dev/null
@@ -1,268 +0,0 @@
-# Backport of OrderedDict() class that runs
-# on Python 2.4, 2.5, 2.6, 2.7 and pypy.
-# Passes Python2.7's test suite and incorporates all the latest updates.
-
-try:
- from thread import get_ident as _get_ident
-except ImportError:
- from dummy_thread import get_ident as _get_ident
-
-try:
- from _abcoll import KeysView, ValuesView, ItemsView
-except ImportError:
- pass
-
-
-class OrderedDict(dict):
- 'Dictionary that remembers insertion order'
- # An inherited dict maps keys to values.
- # The inherited dict provides __getitem__, __len__, __contains__, and get.
- # The remaining methods are order-aware.
- # Big-O running times for all methods are the same as for regular
- # dictionaries.
-
- # The internal self.__map dictionary maps keys to links in a doubly
- # linked list.
- # The circular doubly linked list starts and ends with a sentinel element.
- # The sentinel element never gets deleted (this simplifies the algorithm).
- # Each link is stored as a list of length three: [PREV, NEXT, KEY].
-
- def __init__(self, *args, **kwds):
- '''Initialize an ordered dictionary. Signature is the same as for
- regular dictionaries, but keyword arguments are not recommended
- because their insertion order is arbitrary.
-
- '''
- if len(args) > 1:
- raise TypeError('expected at most 1 arguments, got %d' % len(args))
- try:
- self.__root
- except AttributeError:
- self.__root = root = [] # sentinel node
- root[:] = [root, root, None]
- self.__map = {}
- self.__update(*args, **kwds)
-
- def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
- 'od.__setitem__(i, y) <==> od[i]=y'
- # Setting a new item creates a new link which goes at the end
- # of the linked list, and the inherited dictionary is updated
- # with the new key/value pair.
- if key not in self:
- root = self.__root
- last = root[0]
- last[1] = root[0] = self.__map[key] = [last, root, key]
- dict_setitem(self, key, value)
-
- def __delitem__(self, key, dict_delitem=dict.__delitem__):
- 'od.__delitem__(y) <==> del od[y]'
- # Deleting an existing item uses self.__map to find the link which is
- # then removed by updating the links in the predecessor and successor
- # nodes.
- dict_delitem(self, key)
- link_prev, link_next, key = self.__map.pop(key)
- link_prev[1] = link_next
- link_next[0] = link_prev
-
- def __iter__(self):
- 'od.__iter__() <==> iter(od)'
- root = self.__root
- curr = root[1]
- while curr is not root:
- yield curr[2]
- curr = curr[1]
-
- def __reversed__(self):
- 'od.__reversed__() <==> reversed(od)'
- root = self.__root
- curr = root[0]
- while curr is not root:
- yield curr[2]
- curr = curr[0]
-
- def clear(self):
- 'od.clear() -> None. Remove all items from od.'
- try:
- for node in self.__map.itervalues():
- del node[:]
- root = self.__root
- root[:] = [root, root, None]
- self.__map.clear()
- except AttributeError:
- pass
- dict.clear(self)
-
- def popitem(self, last=True):
- '''od.popitem() -> (k, v), return and remove a (key, value) pair.
- Pairs are returned in LIFO order if last is true or FIFO order if
- false.
- '''
- if not self:
- raise KeyError('dictionary is empty')
- root = self.__root
- if last:
- link = root[0]
- link_prev = link[0]
- link_prev[1] = root
- root[0] = link_prev
- else:
- link = root[1]
- link_next = link[1]
- root[1] = link_next
- link_next[0] = root
- key = link[2]
- del self.__map[key]
- value = dict.pop(self, key)
- return key, value
-
- # -- the following methods do not depend on the internal structure --
-
- def keys(self):
- 'od.keys() -> list of keys in od'
- return list(self)
-
- def values(self):
- 'od.values() -> list of values in od'
- return [self[key] for key in self]
-
- def items(self):
- 'od.items() -> list of (key, value) pairs in od'
- return [(key, self[key]) for key in self]
-
- def iterkeys(self):
- 'od.iterkeys() -> an iterator over the keys in od'
- return iter(self)
-
- def itervalues(self):
- 'od.itervalues -> an iterator over the values in od'
- for k in self:
- yield self[k]
-
- def iteritems(self):
- 'od.iteritems -> an iterator over the (key, value) items in od'
- for k in self:
- yield (k, self[k])
-
- def update(*args, **kwds):
- '''od.update(E, **F) -> None. Update od from dict/iterable E and F.
-
- If E is a dict instance, does: for k in E: od[k] = E[k]
- If E has a .keys() method, does: for k in E.keys():
- od[k] = E[k]
- Or if E is an iterable of items, does: for k, v in E: od[k] = v
- In either case, this is followed by: for k, v in F.items():
- od[k] = v
- '''
-
- if len(args) > 2:
- raise TypeError('update() takes at most 2 positional '
- 'arguments (%d given)' % (len(args),))
- elif not args:
- raise TypeError('update() takes at least 1 argument (0 given)')
- self = args[0]
- # Make progressively weaker assumptions about "other"
- other = ()
- if len(args) == 2:
- other = args[1]
- if isinstance(other, dict):
- for key in other:
- self[key] = other[key]
- elif hasattr(other, 'keys'):
- for key in other.keys():
- self[key] = other[key]
- else:
- for key, value in other:
- self[key] = value
- for key, value in kwds.items():
- self[key] = value
-
- __update = update # let subclasses override update
- # without breaking __init__
-
- __marker = object()
-
- def pop(self, key, default=__marker):
- '''od.pop(k[,d]) -> v
- remove specified key and return the corresponding value.
- If key is not found, d is returned if given,
- otherwise KeyError is raised.
-
- '''
- if key in self:
- result = self[key]
- del self[key]
- return result
- if default is self.__marker:
- raise KeyError(key)
- return default
-
- def setdefault(self, key, default=None):
- 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
- if key in self:
- return self[key]
- self[key] = default
- return default
-
- def __repr__(self, _repr_running={}):
- 'od.__repr__() <==> repr(od)'
- call_key = id(self), _get_ident()
- if call_key in _repr_running:
- return '...'
- _repr_running[call_key] = 1
- try:
- if not self:
- return '%s()' % (self.__class__.__name__,)
- return '%s(%r)' % (self.__class__.__name__, self.items())
- finally:
- del _repr_running[call_key]
-
- def __reduce__(self):
- 'Return state information for pickling'
- items = [[k, self[k]] for k in self]
- inst_dict = vars(self).copy()
- for k in vars(OrderedDict()):
- inst_dict.pop(k, None)
- if inst_dict:
- return (self.__class__, (items,), inst_dict)
- return self.__class__, (items,)
-
- def copy(self):
- 'od.copy() -> a shallow copy of od'
- return self.__class__(self)
-
- @classmethod
- def fromkeys(cls, iterable, value=None):
- '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
- and values equal to v (which defaults to None).
-
- '''
- d = cls()
- for key in iterable:
- d[key] = value
- return d
-
- def __eq__(self, other):
- '''od.__eq__(y) <==> od==y.
- Comparison to another OD is order-sensitive
- while comparison to a regular mapping is order-insensitive.
- '''
- if isinstance(other, OrderedDict):
- return len(self) == len(other) and self.items() == other.items()
- return dict.__eq__(self, other)
-
- def __ne__(self, other):
- return not self == other
-
- # -- the following methods are only used in Python 2.7 --
-
- def viewkeys(self):
- "od.viewkeys() -> a set-like object providing a view on od's keys"
- return KeysView(self)
-
- def viewvalues(self):
- "od.viewvalues() -> an object providing a view on od's values"
- return ValuesView(self)
-
- def viewitems(self):
- "od.viewitems() -> a set-like object providing a view on od's items"
- return ItemsView(self)
diff --git a/src/leap/util/fileutil.py b/src/leap/util/fileutil.py
deleted file mode 100644
index 820ffe46..00000000
--- a/src/leap/util/fileutil.py
+++ /dev/null
@@ -1,120 +0,0 @@
-import errno
-from itertools import chain
-import logging
-import os
-import platform
-import stat
-
-
-logger = logging.getLogger()
-
-
-def is_user_executable(fpath):
- st = os.stat(fpath)
- return bool(st.st_mode & stat.S_IXUSR)
-
-
-def extend_path():
- ourplatform = platform.system()
- if ourplatform == "Linux":
- return "/usr/local/sbin:/usr/sbin"
- # XXX add mac / win extended search paths?
-
-
-def which(program, path=None):
- """
- an implementation of which
- that extends the path with
- other locations, like sbin
- (f.i., openvpn binary is likely to be there)
- @param program: a string representing the binary we're looking for.
- """
- def is_exe(fpath):
- """
- check that path exists,
- it's a file,
- and is executable by the owner
- """
- # we would check for access,
- # but it's likely that we're
- # using uid 0 + polkitd
-
- return os.path.isfile(fpath)\
- and is_user_executable(fpath)
-
- def ext_candidates(fpath):
- yield fpath
- for ext in os.environ.get("PATHEXT", "").split(os.pathsep):
- yield fpath + ext
-
- def iter_path(pathset):
- """
- returns iterator with
- full path for a given path list
- and the current target bin.
- """
- for path in pathset.split(os.pathsep):
- exe_file = os.path.join(path, program)
- #print 'file=%s' % exe_file
- for candidate in ext_candidates(exe_file):
- if is_exe(candidate):
- yield candidate
-
- fpath, fname = os.path.split(program)
- if fpath:
- if is_exe(program):
- return program
- else:
- # extended iterator
- # with extra path
- if path is None:
- path = os.environ['PATH']
- extended_path = chain(
- iter_path(path),
- iter_path(extend_path()))
- for candidate in extended_path:
- if candidate is not None:
- return candidate
-
- # sorry bro.
- return None
-
-
-def mkdir_p(path):
- """
- implements mkdir -p functionality
- """
- try:
- os.makedirs(path)
- except OSError as exc:
- if exc.errno == errno.EEXIST:
- pass
- else:
- raise
-
-
-def mkdir_f(path):
- folder, fname = os.path.split(path)
- mkdir_p(folder)
-
-
-def check_and_fix_urw_only(_file):
- """
- test for 600 mode and try
- to set it if anything different found
- """
- mode = stat.S_IMODE(
- os.stat(_file).st_mode)
-
- if mode != int('600', 8):
- try:
- logger.warning(
- 'bad permission on %s '
- 'attempting to set 600',
- _file)
- os.chmod(_file, stat.S_IRUSR | stat.S_IWUSR)
- except OSError:
- logger.error(
- 'error while trying to chmod 600 %s',
- _file)
- raise
diff --git a/src/leap/util/geo.py b/src/leap/util/geo.py
deleted file mode 100644
index 54b29596..00000000
--- a/src/leap/util/geo.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""
-experimental geo support.
-not yet a feature.
-in debian, we rely on the (optional) geoip-database
-"""
-import os
-import platform
-
-from leap.util import HAS_GEOIP
-
-GEOIP = None
-
-if HAS_GEOIP:
- import pygeoip # we know we can :)
-
- GEOIP_PATH = None
-
- if platform.system() == "Linux":
- PATH = "/usr/share/GeoIP/GeoIP.dat"
- if os.path.isfile(PATH):
- GEOIP_PATH = PATH
- GEOIP = pygeoip.GeoIP(GEOIP_PATH, pygeoip.MEMORY_CACHE)
-
-
-def get_country_name(ip):
- if not GEOIP:
- return
- try:
- country = GEOIP.country_name_by_addr(ip)
- except pygeoip.GeoIPError:
- country = None
- return country if country else "-"
diff --git a/src/leap/util/keyring_helpers.py b/src/leap/util/keyring_helpers.py
new file mode 100644
index 00000000..8f354f28
--- /dev/null
+++ b/src/leap/util/keyring_helpers.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# keyring_helpers.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Keyring helpers.
+"""
+
+import keyring
+
+OBSOLETE_KEYRINGS = [
+ keyring.backends.file.EncryptedKeyring,
+ keyring.backends.file.PlaintextKeyring
+]
+
+
+def has_keyring():
+ """
+ Returns whether we have an useful keyring to use.
+
+ :rtype: bool
+ """
+ kr = keyring.get_keyring()
+ return kr is not None and kr.__class__ not in OBSOLETE_KEYRINGS
diff --git a/src/leap/util/leap_argparse.py b/src/leap/util/leap_argparse.py
index 3412a72c..f60c4e10 100644
--- a/src/leap/util/leap_argparse.py
+++ b/src/leap/util/leap_argparse.py
@@ -1,17 +1,41 @@
+# -*- coding: utf-8 -*-
+# leap_argparse.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
import argparse
+from leap.util import IS_RELEASE_VERSION
+
def build_parser():
"""
- all the options for the leap arg parser
+ All the options for the leap arg parser
Some of these could be switched on only if debug flag is present!
"""
epilog = "Copyright 2012 The LEAP Encryption Access Project"
parser = argparse.ArgumentParser(description="""
-Launches the LEAP Client""", epilog=epilog)
+Launches Bitmask""", epilog=epilog)
parser.add_argument('-d', '--debug', action="store_true",
- help=("Launches client in debug mode, writing debug"
+ help=("Launches Bitmask in debug mode, writing debug"
"info to stdout"))
+ # TODO: when we are ready to disable the --danger flag remove 'True or '
+ if True or not IS_RELEASE_VERSION:
+ help_text = "Bypasses the certificate check for bootstrap"
+ parser.add_argument('--danger', action="store_true", help=help_text)
+
parser.add_argument('-l', '--logfile', metavar="LOG FILE", nargs='?',
action="store", dest="log_file",
#type=argparse.FileType('w'),
@@ -20,6 +44,10 @@ Launches the LEAP Client""", epilog=epilog)
type=int,
action="store", dest="openvpn_verb",
help='verbosity level for openvpn logs [1-6]')
+ parser.add_argument('-s', '--standalone', action="store_true",
+ help='Makes Bitmask use standalone'
+ 'directories for configuration and binary'
+ 'searching')
# Not in use, we might want to reintroduce them.
#parser.add_argument('-i', '--no-provider-checks',
diff --git a/src/leap/util/leap_log_handler.py b/src/leap/util/leap_log_handler.py
new file mode 100644
index 00000000..9adb21a5
--- /dev/null
+++ b/src/leap/util/leap_log_handler.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+# leap_log_handler.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Custom handler for the logger window.
+"""
+import logging
+
+from PySide import QtCore
+
+
+class LogHandler(logging.Handler):
+ """
+ This is the custom handler that implements our desired formatting
+ and also keeps a history of all the logged events.
+ """
+
+ MESSAGE_KEY = 'message'
+ RECORD_KEY = 'record'
+
+ def __init__(self, qtsignal):
+ """
+ LogHander initialization.
+ Calls parent method and keeps a reference to the qtsignal
+ that will be used to fire the gui update.
+ """
+ # TODO This is going to eat lots of memory after some time.
+ # Should be pruned at some moment.
+ self._log_history = []
+
+ logging.Handler.__init__(self)
+ self._qtsignal = qtsignal
+
+ def _get_format(self, logging_level):
+ """
+ Sets the log format depending on the parameter.
+ It uses html and css to set the colors for the logs.
+
+ :param logging_level: the debug level to define the color.
+ :type logging_level: str.
+ """
+ log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+ formatter = logging.Formatter(log_format)
+
+ return formatter
+
+ def emit(self, logRecord):
+ """
+ This method is fired every time that a record is logged by the
+ logging module.
+ This method reimplements logging.Handler.emit that is fired
+ in every logged message.
+
+ :param logRecord: the record emitted by the logging module.
+ :type logRecord: logging.LogRecord.
+ """
+ self.setFormatter(self._get_format(logRecord.levelname))
+ log = self.format(logRecord)
+ log_item = {self.RECORD_KEY: logRecord, self.MESSAGE_KEY: log}
+ self._log_history.append(log_item)
+ self._qtsignal(log_item)
+
+
+class HandlerAdapter(object):
+ """
+ New style class that accesses all attributes from the LogHandler.
+
+ Used as a workaround for a problem with multiple inheritance with Pyside
+ that surfaced under OSX with pyside 1.1.0.
+ """
+ MESSAGE_KEY = 'message'
+ RECORD_KEY = 'record'
+
+ def __init__(self, qtsignal):
+ self._handler = LogHandler(qtsignal=qtsignal)
+
+ def setLevel(self, *args, **kwargs):
+ return self._handler.setLevel(*args, **kwargs)
+
+ def handle(self, *args, **kwargs):
+ return self._handler.handle(*args, **kwargs)
+
+ @property
+ def level(self):
+ return self._handler.level
+
+
+class LeapLogHandler(QtCore.QObject, HandlerAdapter):
+ """
+ Custom logging handler. It emits Qt signals so it can be plugged to a gui.
+
+ Its inner handler also stores an history of logs that can be fetched after
+ having been connected to a gui.
+ """
+ # All dicts returned are of the form
+ # {'record': LogRecord, 'message': str}
+ new_log = QtCore.Signal(dict)
+
+ def __init__(self):
+ """
+ LeapLogHandler initialization.
+ Initializes parent classes.
+ """
+ QtCore.QObject.__init__(self)
+ HandlerAdapter.__init__(self, qtsignal=self.qtsignal)
+
+ def qtsignal(self, log_item):
+ # WARNING: the new-style connection does NOT work because PySide
+ # translates the emit method to self.emit, and that collides with
+ # the emit method for logging.Handler
+ # self.new_log.emit(log_item)
+ QtCore.QObject.emit(
+ self,
+ QtCore.SIGNAL('new_log(PyObject)'), log_item)
+
+ @property
+ def log_history(self):
+ """
+ Returns the history of the logged messages.
+ """
+ return self._handler._log_history
diff --git a/src/leap/util/misc.py b/src/leap/util/misc.py
deleted file mode 100644
index d869a1ba..00000000
--- a/src/leap/util/misc.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""
-misc utils
-"""
-import psutil
-
-from leap.base.constants import OPENVPN_BIN
-
-
-class ImproperlyConfigured(Exception):
- """
- """
-
-
-def null_check(value, value_name):
- try:
- assert value is not None
- except AssertionError:
- raise ImproperlyConfigured(
- "%s parameter cannot be None" % value_name)
-
-
-def get_openvpn_pids():
- # binary name might change
-
- openvpn_pids = []
- for p in psutil.process_iter():
- try:
- # XXX Not exact!
- # Will give false positives.
- # we should check that cmdline BEGINS
- # with openvpn or with our wrapper
- # (pkexec / osascript / whatever)
- if OPENVPN_BIN in ' '.join(p.cmdline):
- openvpn_pids.append(p.pid)
- except psutil.error.AccessDenied:
- pass
- return openvpn_pids
diff --git a/src/leap/util/polkit.py b/src/leap/util/polkit.py
deleted file mode 100644
index 8bb87b02..00000000
--- a/src/leap/util/polkit.py
+++ /dev/null
@@ -1,36 +0,0 @@
-import logging
-
-import sh
-
-logger = logging.getLogger(__name__)
-
-
-def run_polkit_auth_agent():
- """
- launches polkit authentication agent
- """
- logger.debug('launching policykit authentication agent in background...')
- polkit = sh.Command('/usr/lib/policykit-1-gnome/'
- 'polkit-gnome-authentication-agent-1')
- polkit(_bg=True)
-
-
-def check_if_running_polkit_auth():
- """
- check if polkit authentication agent is running
- and launch it if it is not
- """
- from sh import grep
- from sh import ps
-
- # for some reason, sh is getting us
- # limited line width, so the grep fails
- #grep_pk = lambda: grep(ps('a'), 'polkit-gnome-authentication-agent-1')
- grep_pk = lambda: grep(ps('a'), '[p]olkit-gnome-auth')
- try:
- grep_pk()
- except sh.ErrorReturnCode_1:
- logger.debug('polkit auth agent not found, trying to launch it...')
- run_polkit_auth_agent()
- else:
- logger.debug('polkit auth agent is running, no need to be launched')
diff --git a/src/leap/util/privilege_policies.py b/src/leap/util/privilege_policies.py
new file mode 100644
index 00000000..72442553
--- /dev/null
+++ b/src/leap/util/privilege_policies.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+# privilege_policies.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Helpers to determine if the needed policies for privilege escalation
+are operative under this client run.
+"""
+import logging
+import os
+import platform
+
+from abc import ABCMeta, abstractmethod
+
+logger = logging.getLogger(__name__)
+
+
+POLICY_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE policyconfig PUBLIC
+ "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
+<policyconfig>
+
+ <vendor>LEAP Project</vendor>
+ <vendor_url>https://leap.se/</vendor_url>
+
+ <action id="net.openvpn.gui.leap.run-openvpn">
+ <description>Runs the openvpn binary</description>
+ <description xml:lang="es">Ejecuta el binario openvpn</description>
+ <message>OpenVPN needs that you authenticate to start</message>
+ <message xml:lang="es">
+ OpenVPN necesita autorizacion para comenzar
+ </message>
+ <icon_name>package-x-generic</icon_name>
+ <defaults>
+ <allow_any>yes</allow_any>
+ <allow_inactive>yes</allow_inactive>
+ <allow_active>yes</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.exec.path">{path}</annotate>
+ <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
+ </action>
+</policyconfig>
+"""
+
+
+def is_missing_policy_permissions():
+ """
+ Returns True if we do not have implemented a policy checker for this
+ platform, or if the policy checker exists but it cannot find the
+ appropriate policy mechanisms in place.
+
+ :rtype: bool
+ """
+ _system = platform.system()
+ platform_checker = _system + "PolicyChecker"
+ policy_checker = globals().get(platform_checker, None)
+ if not policy_checker:
+ # it is true that we miss permission to escalate
+ # privileges without asking for password each time.
+ logger.debug("we could not find a policy checker implementation "
+ "for %s" % (_system,))
+ return True
+ return policy_checker().is_missing_policy_permissions()
+
+
+def get_policy_contents(openvpn_path):
+ """
+ Returns the contents that the policy file should have.
+
+ :param openvpn_path: the openvpn path to use in the polkit file
+ :type openvpn_path: str
+ :rtype: str
+ """
+ return POLICY_TEMPLATE.format(path=openvpn_path)
+
+
+def is_policy_outdated(path):
+ """
+ Returns if the existing polkit file is outdated, comparing if the path
+ is correct.
+
+ :param path: the path that should have the polkit file.
+ :type path: str.
+ :rtype: bool
+ """
+ _system = platform.system()
+ platform_checker = _system + "PolicyChecker"
+ policy_checker = globals().get(platform_checker, None)
+ if policy_checker is None:
+ logger.debug("we could not find a policy checker implementation "
+ "for %s" % (_system,))
+ return False
+ return policy_checker().is_outdated(path)
+
+
+class PolicyChecker:
+ """
+ Abstract PolicyChecker class
+ """
+
+ __metaclass__ = ABCMeta
+
+ @abstractmethod
+ def is_missing_policy_permissions(self):
+ """
+ Returns True if we could not find any policy mechanisms that
+ are defined to be in used for this particular platform.
+
+ :rtype: bool
+ """
+ return True
+
+
+class LinuxPolicyChecker(PolicyChecker):
+ """
+ PolicyChecker for Linux
+ """
+ LINUX_POLKIT_FILE = ("/usr/share/polkit-1/actions/"
+ "net.openvpn.gui.leap.policy")
+
+ @classmethod
+ def get_polkit_path(self):
+ """
+ Returns the polkit file path.
+
+ :rtype: str
+ """
+ return self.LINUX_POLKIT_FILE
+
+ def is_missing_policy_permissions(self):
+ """
+ Returns True if we could not find the appropriate policykit file
+ in place
+
+ :rtype: bool
+ """
+ return not os.path.isfile(self.LINUX_POLKIT_FILE)
+
+ def is_outdated(self, path):
+ """
+ Returns if the existing polkit file is outdated, comparing if the path
+ is correct.
+
+ :param path: the path that should have the polkit file.
+ :type path: str.
+ :rtype: bool
+ """
+ polkit = None
+ try:
+ with open(self.LINUX_POLKIT_FILE) as f:
+ polkit = f.read()
+ except IOError, e:
+ logger.error("Error reading polkit file(%s): %r" % (
+ self.LINUX_POLKIT_FILE, e))
+
+ return get_policy_contents(path) != polkit
diff --git a/src/leap/util/pyside_tests_helper.py b/src/leap/util/pyside_tests_helper.py
new file mode 100644
index 00000000..5c0eb8d6
--- /dev/null
+++ b/src/leap/util/pyside_tests_helper.py
@@ -0,0 +1,136 @@
+
+'''Helper classes and functions'''
+
+import os
+import unittest
+
+from random import randint
+
+from PySide.QtCore import QCoreApplication, QTimer
+
+try:
+ from PySide.QtGui import QApplication
+except ImportError:
+ has_gui = False
+else:
+ has_gui = True
+
+
+def adjust_filename(filename, orig_mod_filename):
+ dirpath = os.path.dirname(os.path.abspath(orig_mod_filename))
+ return os.path.join(dirpath, filename)
+
+
+class NoQtGuiError(Exception):
+ def __init__(self):
+ Exception.__init__(self, 'No QtGui found')
+
+
+class BasicPySlotCase(object):
+ '''Base class that tests python slots and signal emissions.
+
+ Python slots are defined as any callable passed to QObject.connect().
+ '''
+ def setUp(self):
+ self.called = False
+
+ def tearDown(self):
+ try:
+ del self.args
+ except:
+ pass
+
+ def cb(self, *args):
+ '''Simple callback with arbitrary arguments.
+
+ The test function must setup the 'args' attribute with a sequence
+ containing the arguments expected to be received by this slot.
+ Currently only a single connection is supported.
+ '''
+ if tuple(self.args) == args:
+ self.called = True
+ else:
+ raise ValueError('Invalid arguments for callback')
+
+
+_instance = None
+_timed_instance = None
+
+if has_gui:
+ class UsesQApplication(unittest.TestCase):
+ '''Helper class to provide QApplication instances'''
+
+ qapplication = True
+
+ def setUp(self):
+ '''Creates the QApplication instance'''
+
+ # Simple way of making instance a singleton
+ super(UsesQApplication, self).setUp()
+ global _instance
+ if _instance is None:
+ _instance = QApplication([])
+
+ self.app = _instance
+
+ def tearDown(self):
+ '''Deletes the reference owned by self'''
+ del self.app
+ super(UsesQApplication, self).tearDown()
+
+ class TimedQApplication(unittest.TestCase):
+ '''Helper class with timed QApplication exec loop'''
+
+ def setUp(self, timeout=100):
+ '''Setups this Application.
+
+ timeout - timeout in milisseconds'''
+ global _timed_instance
+ if _timed_instance is None:
+ _timed_instance = QApplication([])
+
+ self.app = _timed_instance
+ QTimer.singleShot(timeout, self.app.quit)
+
+ def tearDown(self):
+ '''Delete resources'''
+ del self.app
+else:
+ class UsesQApplication(unittest.TestCase):
+ def setUp(self):
+ raise NoQtGuiError()
+
+ class TimedQapplication(unittest.TestCase):
+ def setUp(self):
+ raise NoQtGuiError()
+
+_core_instance = None
+
+
+class UsesQCoreApplication(unittest.TestCase):
+ '''Helper class for test cases that require an QCoreApplication
+ Just connect or call self.exit_app_cb. When called, will ask
+ self.app to exit.
+ '''
+
+ def setUp(self):
+ '''Set up resources'''
+
+ global _core_instance
+ if _core_instance is None:
+ _core_instance = QCoreApplication([])
+
+ self.app = _core_instance
+
+ def tearDown(self):
+ '''Release resources'''
+ del self.app
+
+ def exit_app_cb(self):
+ '''Quits the application'''
+ self.app.exit(0)
+
+
+def random_string(size=5):
+ '''Generate random string with the given size'''
+ return ''.join(map(chr, [randint(33, 126) for x in range(size)]))
diff --git a/src/leap/util/request_helpers.py b/src/leap/util/request_helpers.py
new file mode 100644
index 00000000..74aaa06b
--- /dev/null
+++ b/src/leap/util/request_helpers.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+# request_helpers.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Request helpers for backward compatible "parsing" of requests
+"""
+
+import time
+
+import json
+
+from dateutil import parser as dateparser
+
+
+def get_content(request):
+ """
+ Returns the content by trying to get it from the json
+ property/function or from content, in that order.
+ Also returns the mtime for that content if available
+
+ :param request: request as it is given by requests
+ :type request: Response
+
+ :rtype: tuple (contents, mtime)
+ """
+
+ contents = ""
+ mtime = None
+
+ if request and request.content and request.json:
+ if callable(request.json):
+ contents = json.dumps(request.json())
+ else:
+ contents = json.dumps(request.json)
+ else:
+ contents = request.content
+
+ mtime = None
+ last_modified = request.headers.get('last-modified', None)
+ if last_modified:
+ dt = dateparser.parse(unicode(last_modified))
+ mtime = int(time.mktime(dt.timetuple()) + dt.microsecond / 1000000.0)
+
+ return contents, mtime
diff --git a/src/leap/util/requirement_checker.py b/src/leap/util/requirement_checker.py
new file mode 100644
index 00000000..1d9b9923
--- /dev/null
+++ b/src/leap/util/requirement_checker.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+# requirement_checker.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Utility to check the needed requirements.
+"""
+
+import os
+import logging
+
+from pkg_resources import (DistributionNotFound,
+ get_distribution,
+ Requirement,
+ resource_stream,
+ VersionConflict)
+
+logger = logging.getLogger(__name__)
+
+
+def get_requirements():
+ """
+ This function returns a list with requirements.
+ It checks either if its running from the source or if its installed.
+
+ :returns: a list with packages names, required for the app.
+ :return type: list of str.
+ """
+ develop = True
+ requirements = []
+
+ try:
+ # if we are running from the source
+ from pkg import util
+ requirements = util.parse_requirements()
+ except ImportError:
+ develop = False
+
+ # if we are running from the package
+ if not develop:
+ requires_file_name = os.path.join('leap', 'util', 'reqs.txt')
+ dist_name = Requirement.parse('leap-client')
+
+ try:
+ with resource_stream(dist_name, requires_file_name) as stream:
+ requirements = [line.strip() for line in stream]
+ except Exception, e:
+ logger.error("Requirements file not found. %r" % (e, ))
+
+ return requirements
+
+
+def check_requirements():
+ """
+ This function check the dependencies declared in the
+ requirement(s) file(s) and logs the results.
+ """
+ logger.debug("Checking requirements...")
+ requirements = get_requirements()
+
+ for package in requirements:
+ try:
+ get_distribution(package)
+ except VersionConflict:
+ required_package = Requirement.parse(package)
+ required_version = required_package.specs[0]
+ required_name = required_package.key
+
+ installed_package = get_distribution(required_name)
+ installed_version = installed_package.version
+ installed_location = installed_package.location
+
+ msg = "Error: version not satisfied. "
+ msg += "Expected %s, installed %s (path: %s)." % (
+ required_version, installed_version, installed_location)
+
+ result = "%s ... %s" % (package, msg)
+ logger.error(result)
+ except DistributionNotFound:
+ msg = "Error: package not found!"
+ result = "%s ... %s" % (package, msg)
+ logger.error(result)
+ else:
+ msg = "OK"
+ result = "%s ... %s" % (package, msg)
+ logger.debug(result)
+
+ logger.debug('Done')
diff --git a/src/leap/util/streamtologger.py b/src/leap/util/streamtologger.py
new file mode 100644
index 00000000..25a06718
--- /dev/null
+++ b/src/leap/util/streamtologger.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# streamtologger.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Stream object that redirects writes to a logger instance.
+"""
+import logging
+
+
+class StreamToLogger(object):
+ """
+ Fake file-like stream object that redirects writes to a logger instance.
+
+ Credits to:
+ http://www.electricmonk.nl/log/2011/08/14/\
+ redirect-stdout-and-stderr-to-a-logger-in-python/
+ """
+ def __init__(self, logger, log_level=logging.INFO):
+ """
+ Constructor, defines the logger and level to use to log messages.
+
+ :param logger: logger object to log messages.
+ :type logger: logging.Handler
+ :param log_level: the level to use to log messages through the logger.
+ :type log_level: int
+ look at logging-levels in 'logging' docs.
+ """
+ self._logger = logger
+ self._log_level = log_level
+
+ def write(self, data):
+ """
+ Simulates the 'write' method in a file object.
+ It writes the data receibed in buf to the logger 'self._logger'.
+
+ :param data: data to write to the 'file'
+ :type data: str
+ """
+ for line in data.rstrip().splitlines():
+ self._logger.log(self._log_level, line.rstrip())
+
+ def flush(self):
+ """
+ Dummy method. Needed to replace the twisted.log output.
+ """
+ pass
diff --git a/src/leap/util/tests/test_fileutil.py b/src/leap/util/tests/test_fileutil.py
deleted file mode 100644
index f5131b3d..00000000
--- a/src/leap/util/tests/test_fileutil.py
+++ /dev/null
@@ -1,100 +0,0 @@
-import os
-import platform
-import shutil
-import stat
-import tempfile
-import unittest
-
-from leap.util import fileutil
-
-
-class FileUtilTest(unittest.TestCase):
- """
- test our file utils
- """
-
- def setUp(self):
- self.system = platform.system()
- self.create_temp_dir()
-
- def tearDown(self):
- self.remove_temp_dir()
-
- #
- # helpers
- #
-
- def create_temp_dir(self):
- self.tmpdir = tempfile.mkdtemp()
-
- def remove_temp_dir(self):
- shutil.rmtree(self.tmpdir)
-
- def get_file_path(self, filename):
- return os.path.join(
- self.tmpdir,
- filename)
-
- def touch_exec_file(self):
- fp = self.get_file_path('testexec')
- open(fp, 'w').close()
- os.chmod(
- fp,
- stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
- return fp
-
- def get_mode(self, fp):
- return stat.S_IMODE(os.stat(fp).st_mode)
-
- #
- # tests
- #
-
- def test_is_user_executable(self):
- """
- touch_exec_file creates in mode 700?
- """
- # XXX could check access X_OK
-
- fp = self.touch_exec_file()
- mode = self.get_mode(fp)
- self.assertEqual(mode, int('700', 8))
-
- def test_which(self):
- """
- which implementation ok?
- not a very reliable test,
- but I cannot think of anything smarter now
- I guess it's highly improbable that copy
- """
- # XXX yep, we can change the syspath
- # for the test... !
-
- if self.system == "Linux":
- self.assertEqual(
- fileutil.which('cp'),
- '/bin/cp')
-
- def test_mkdir_p(self):
- """
- our own mkdir -p implementation ok?
- """
- testdir = self.get_file_path(
- os.path.join('test', 'foo', 'bar'))
- self.assertEqual(os.path.isdir(testdir), False)
- fileutil.mkdir_p(testdir)
- self.assertEqual(os.path.isdir(testdir), True)
-
- def test_check_and_fix_urw_only(self):
- """
- ensure check_and_fix_urx_only ok?
- """
- fp = self.touch_exec_file()
- mode = self.get_mode(fp)
- self.assertEqual(mode, int('700', 8))
- fileutil.check_and_fix_urw_only(fp)
- mode = self.get_mode(fp)
- self.assertEqual(mode, int('600', 8))
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/util/tests/test_is_release_version.py b/src/leap/util/tests/test_is_release_version.py
new file mode 100644
index 00000000..4199f603
--- /dev/null
+++ b/src/leap/util/tests/test_is_release_version.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# test_is_release_version.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+tests for _is_release_version function
+"""
+
+import unittest
+from leap.common.testing.basetest import BaseLeapTest
+from leap.util import _is_release_version as is_release_version
+
+
+class TestIsReleaseVersion(BaseLeapTest):
+ """Tests for release version check."""
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ def test_git_version(self):
+ version = '0.2.3-12-ge5b50a1'
+ self.assertFalse(is_release_version(version))
+
+ def test_release(self):
+ version = '0.2.4'
+ self.assertTrue(is_release_version(version))
+
+ def test_release_candidate(self):
+ version = '0.2.4-rc1'
+ self.assertFalse(is_release_version(version))
+
+ def test_complex_version(self):
+ version = '12.5.2.4-rc12.dev.alpha1'
+ self.assertFalse(is_release_version(version))
+
+ def test_super_high_version(self):
+ version = '12.5.2.4.45'
+ self.assertTrue(is_release_version(version))
+
+
+if __name__ == "__main__":
+ unittest.main(verbosity=2)
diff --git a/src/leap/util/tests/test_leap_argparse.py b/src/leap/util/tests/test_leap_argparse.py
deleted file mode 100644
index 4e2b811f..00000000
--- a/src/leap/util/tests/test_leap_argparse.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from argparse import Namespace
-import unittest
-
-from leap.util import leap_argparse
-
-
-class LeapArgParseTest(unittest.TestCase):
- """
- Test argparse options for eip client
- """
-
- def setUp(self):
- """
- get the parser
- """
- self.parser = leap_argparse.build_parser()
-
- def test_debug_mode(self):
- """
- test debug mode option
- """
- opts = self.parser.parse_args(
- ['--debug'])
- self.assertEqual(
- opts,
- Namespace(
- debug=True,
- log_file=None,
- #config_file=None,
- #no_provider_checks=False,
- #no_ca_verify=False,
- openvpn_verb=None))
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/util/tests/test_leap_log_handler.py b/src/leap/util/tests/test_leap_log_handler.py
new file mode 100644
index 00000000..ea509ea8
--- /dev/null
+++ b/src/leap/util/tests/test_leap_log_handler.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+# test_leap_log_handler.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+tests for leap_log_handler
+"""
+
+import unittest
+
+import logging
+
+from leap.util.leap_log_handler import LeapLogHandler
+from leap.common.testing.basetest import BaseLeapTest
+from leap.util.pyside_tests_helper import BasicPySlotCase
+
+from mock import Mock
+
+
+class LeapLogHandlerTest(BaseLeapTest, BasicPySlotCase):
+ """
+ LeapLogHandlerTest's tests.
+ """
+ def _callback(self, *args):
+ """
+ Simple callback to track if a signal was emitted.
+ """
+ self.called = True
+ self.emitted_msg = args[0][LeapLogHandler.MESSAGE_KEY]
+
+ def setUp(self):
+ BasicPySlotCase.setUp(self)
+
+ # Create the logger
+ level = logging.DEBUG
+ self.logger = logging.getLogger(name='test')
+ self.logger.setLevel(level)
+
+ # Create the handler
+ self.leap_handler = LeapLogHandler()
+ self.leap_handler.setLevel(level)
+ self.logger.addHandler(self.leap_handler)
+
+ def tearDown(self):
+ BasicPySlotCase.tearDown(self)
+ try:
+ self.leap_handler.new_log.disconnect()
+ except Exception:
+ pass
+
+ def test_history_starts_empty(self):
+ self.assertEqual(self.leap_handler.log_history, [])
+
+ def test_one_log_captured(self):
+ self.logger.debug('test')
+ self.assertEqual(len(self.leap_handler.log_history), 1)
+
+ def test_history_records_order(self):
+ self.logger.debug('test 01')
+ self.logger.debug('test 02')
+ self.logger.debug('test 03')
+
+ logs = []
+ for message in self.leap_handler.log_history:
+ logs.append(message[LeapLogHandler.RECORD_KEY].msg)
+
+ self.assertIn('test 01', logs)
+ self.assertIn('test 02', logs)
+ self.assertIn('test 03', logs)
+
+ def test_history_messages_order(self):
+ self.logger.debug('test 01')
+ self.logger.debug('test 02')
+ self.logger.debug('test 03')
+
+ logs = []
+ for message in self.leap_handler.log_history:
+ logs.append(message[LeapLogHandler.MESSAGE_KEY])
+
+ self.assertIn('test 01', logs[0])
+ self.assertIn('test 02', logs[1])
+ self.assertIn('test 03', logs[2])
+
+ def test_emits_signal(self):
+ log_format = '%(name)s - %(levelname)s - %(message)s'
+ formatter = logging.Formatter(log_format)
+ get_format = Mock(return_value=formatter)
+ self.leap_handler._handler._get_format = get_format
+
+ self.leap_handler.new_log.connect(self._callback)
+ self.logger.debug('test')
+
+ expected_log_msg = "test - DEBUG - test"
+
+ # signal emitted
+ self.assertTrue(self.called)
+
+ # emitted message
+ self.assertEqual(self.emitted_msg, expected_log_msg)
+
+ # Mock called
+ self.assertTrue(get_format.called)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/leap/util/tests/test_streamtologger.py b/src/leap/util/tests/test_streamtologger.py
new file mode 100644
index 00000000..4c98e562
--- /dev/null
+++ b/src/leap/util/tests/test_streamtologger.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+# test_streamtologger.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+tests for streamtologger
+"""
+
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+import logging
+import sys
+
+from leap.util.streamtologger import StreamToLogger
+from leap.common.testing.basetest import BaseLeapTest
+
+
+class SimpleLogHandler(logging.Handler):
+ """
+ The simplest log handler that allows to check if the log was
+ delivered to the handler correctly.
+ """
+ def __init__(self):
+ logging.Handler.__init__(self)
+ self._last_log = ""
+ self._last_log_level = ""
+
+ def emit(self, record):
+ self._last_log = record.getMessage()
+ self._last_log_level = record.levelno
+
+ def get_last_log(self):
+ """
+ Returns the last logged message by this handler.
+
+ :return: the last logged message.
+ :rtype: str
+ """
+ return self._last_log
+
+ def get_last_log_level(self):
+ """
+ Returns the level of the last logged message by this handler.
+
+ :return: the last logged level.
+ :rtype: str
+ """
+ return self._last_log_level
+
+
+class StreamToLoggerTest(BaseLeapTest):
+ """
+ StreamToLogger's tests.
+
+ NOTE: we may need to find a way to test the use case that an exception
+ is raised. I couldn't catch the output of an exception because the
+ test failed if some exception is raised.
+ """
+ def setUp(self):
+ # Create the logger
+ level = logging.DEBUG
+ self.logger = logging.getLogger(name='test')
+ self.logger.setLevel(level)
+
+ # Simple log handler
+ self.handler = SimpleLogHandler()
+ self.logger.addHandler(self.handler)
+
+ # Preserve original values
+ self._sys_stdout = sys.stdout
+ self._sys_stderr = sys.stderr
+
+ # Create the handler
+ sys.stdout = StreamToLogger(self.logger, logging.DEBUG)
+ sys.stderr = StreamToLogger(self.logger, logging.ERROR)
+
+ def tearDown(self):
+ # Restore original values
+ sys.stdout = self._sys_stdout
+ sys.stderr = self._sys_stderr
+
+ def test_logger_starts_empty(self):
+ self.assertEqual(self.handler.get_last_log(), '')
+
+ def test_standard_output(self):
+ message = 'Test string'
+ print message
+
+ log = self.handler.get_last_log()
+ log_level = self.handler.get_last_log_level()
+
+ self.assertEqual(log, message)
+ self.assertEqual(log_level, logging.DEBUG)
+
+ def test_standard_error(self):
+ message = 'Test string'
+ sys.stderr.write(message)
+
+ log_level = self.handler.get_last_log_level()
+ log = self.handler.get_last_log()
+
+ self.assertEqual(log, message)
+ self.assertEqual(log_level, logging.ERROR)
+
+
+if __name__ == "__main__":
+ unittest.main(verbosity=2)
diff --git a/src/leap/util/tests/test_translations.py b/src/leap/util/tests/test_translations.py
deleted file mode 100644
index 794daeba..00000000
--- a/src/leap/util/tests/test_translations.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import unittest
-
-from leap.util import translations
-
-
-class TrasnlationsTestCase(unittest.TestCase):
- """
- tests for translation functions and classes
- """
-
- def setUp(self):
- self.trClass = translations.LEAPTranslatable
-
- def test_trasnlatable(self):
- tr = self.trClass({"en": "house", "es": "casa"})
- eq = self.assertEqual
- eq(tr.tr(to="es"), "casa")
- eq(tr.tr(to="en"), "house")
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/src/leap/util/translations.py b/src/leap/util/translations.py
deleted file mode 100644
index f55c8fba..00000000
--- a/src/leap/util/translations.py
+++ /dev/null
@@ -1,82 +0,0 @@
-import inspect
-import logging
-
-from PyQt4.QtCore import QCoreApplication
-from PyQt4.QtCore import QLocale
-
-logger = logging.getLogger(__name__)
-
-"""
-here I could not do all that I wanted.
-the context is not getting passed to the xml file.
-Looks like pylupdate4 is somehow a hack that does not
-parse too well the python ast.
-I guess we could generate the xml for ourselves as a last recourse.
-"""
-
-# XXX BIG NOTE:
-# RESIST the temptation to get the translate function
-# more compact, or have the Context argument passed as a variable
-# Its name HAS to be explicit due to how the pylupdate parser
-# works.
-
-
-qtTranslate = QCoreApplication.translate
-
-
-def translate(*args, **kwargs):
- """
- our magic function.
- translate(Context, text, comment)
- """
- if len(args) == 1:
- obj = args[0]
- if isinstance(obj, LEAPTranslatable) and hasattr(obj, 'tr'):
- return obj.tr()
-
- klsname = None
- try:
- # get class value from instance
- # using live object inspection
- prev_frame = inspect.stack()[1][0]
- locals_ = inspect.getargvalues(prev_frame).locals
- self = locals_.get('self')
- if self:
-
- # Trying to get the class name
- # but this is useless, the parser
- # has already got the context.
- klsname = self.__class__.__name__
- #print 'KLSNAME -- ', klsname
- except:
- logger.error('error getting stack frame')
-
- if klsname and len(args) == 1:
- nargs = (klsname,) + args
- return qtTranslate(*nargs)
-
- else:
- return qtTranslate(*args)
-
-
-class LEAPTranslatable(dict):
- """
- An extended dict that implements a .tr method
- so it can be translated on the fly by our
- magic translate method
- """
-
- try:
- locale = str(QLocale.system().name()).split('_')[0]
- except:
- logger.warning("could not get system locale!")
- print "could not get system locale!"
- locale = "en"
-
- def tr(self, to=None):
- if not to:
- to = self.locale
- _tr = self.get(to, None)
- if not _tr:
- _tr = self.get("en", None)
- return _tr
diff --git a/src/leap/util/web.py b/src/leap/util/web.py
deleted file mode 100644
index 15de0561..00000000
--- a/src/leap/util/web.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""
-web related utilities
-"""
-
-
-class UsageError(Exception):
- """ """
-
-
-def get_https_domain_and_port(full_domain):
- """
- returns a tuple with domain and port
- from a full_domain string that can
- contain a colon
- """
- full_domain = unicode(full_domain)
- if full_domain is None:
- return None, None
-
- https_sch = "https://"
- http_sch = "http://"
-
- if full_domain.startswith(https_sch):
- full_domain = full_domain.lstrip(https_sch)
- elif full_domain.startswith(http_sch):
- raise UsageError(
- "cannot be called with a domain "
- "that begins with 'http://'")
-
- domain_split = full_domain.split(':')
- _len = len(domain_split)
- if _len == 1:
- domain, port = full_domain, 443
- elif _len == 2:
- domain, port = domain_split
- else:
- raise UsageError(
- "must be called with one only parameter"
- "in the form domain[:port]")
- return domain, port
diff --git a/tests/README b/tests/README
deleted file mode 100644
index 8745f03c..00000000
--- a/tests/README
+++ /dev/null
@@ -1 +0,0 @@
-write here general, integration tests.
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/tests/__init__.py
+++ /dev/null
diff --git a/tests/test_qt_environment.py b/tests/test_qt_environment.py
deleted file mode 100644
index 08fccf4b..00000000
--- a/tests/test_qt_environment.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import sys
-import unittest
-
-import sip
-sip.setapi('QVariant', 2)
-
-from PyQt4 import QtGui
-
-
-class TestWin(QtGui.QMainWindow):
- """
- a _really_ minimal test window,
- with only one tray icon
- """
- def __init__(self):
- super(TestWin, self).__init__()
- self.trayIcon = QtGui.QSystemTrayIcon(self)
-
-
-class QtEnvironTest(unittest.TestCase):
- """
- Test we're running a proper qt environment
- """
-
- def setUp(self):
- self.app = QtGui.QApplication(sys.argv)
- self.win = TestWin()
-
- def tearDown(self):
- del(self.win)
- del(self.app)
-
- def test_system_has_systray(self):
- """
- does system have systray available?
- """
- self.assertEqual(
- self.win.trayIcon.isSystemTrayAvailable(),
- True)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..e041515d
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,12 @@
+[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