summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTomás Touceda <chiiph@leap.se>2013-06-28 14:25:19 -0300
committerTomás Touceda <chiiph@leap.se>2013-06-28 14:25:19 -0300
commit5b975799ce9b7a6e0a88be4bcb48bdfb90800bb3 (patch)
treebda674a79b1aeccb37b67609517bc4761db7ae07
parent9cea9c8a34343f8792d65b96f93ae22bd8685878 (diff)
parentc088a1544a5f7a51359d2802019c0740aab0cc5b (diff)
Merge branch 'release-0.2.2'0.2.2
-rw-r--r--.gitignore7
-rw-r--r--CHANGELOG109
-rw-r--r--LICENSE (renamed from COPYING)24
-rw-r--r--MANIFEST.in6
-rw-r--r--Makefile44
-rw-r--r--NEWS.rst8
-rw-r--r--README.rst73
-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/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/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.pro39
-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.qrc31
-rw-r--r--data/translations/de.qmbin6036 -> 0 bytes
-rw-r--r--data/translations/de.ts218
-rw-r--r--data/translations/es.qmbin6194 -> 12207 bytes
-rw-r--r--data/translations/es.ts542
-rw-r--r--data/ts/en_US.ts434
-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.wiki5
-rw-r--r--docs/conf.py26
-rw-r--r--docs/dev/environment.rst49
-rw-r--r--docs/dev/internationalization.rst22
-rw-r--r--docs/dev/resources.rst4
-rw-r--r--docs/dev/workflow.rst8
-rw-r--r--docs/index.rst7
-rw-r--r--docs/pkg/debian.rst4
-rw-r--r--docs/testers/howto.rst4
-rw-r--r--docs/user/install.rst46
-rw-r--r--docs/user/intro.rst2
-rw-r--r--docs/user/running.rst20
-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.pip32
-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
-rwxr-xr-xrun_tests.sh32
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py190
-rw-r--r--src/leap/__init__.py37
-rw-r--r--src/leap/app.py177
-rw-r--r--src/leap/base/auth.py355
-rw-r--r--src/leap/base/authentication.py11
-rw-r--r--src/leap/base/checks.py213
-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/jsonschema.py791
-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.py177
-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.py177
-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/constants.py18
-rw-r--r--src/leap/crypto/leapkeyring.py70
-rw-r--r--src/leap/crypto/srpauth.py496
-rw-r--r--src/leap/crypto/srpregister.py163
-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_srpregister.py202
-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.py336
-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.py813
-rw-r--r--src/leap/gui/loggerwindow.py124
-rw-r--r--src/leap/gui/login.py214
-rw-r--r--src/leap/gui/mainwindow.py1336
-rw-r--r--src/leap/gui/mainwindow_rc.py2564
-rw-r--r--src/leap/gui/progress.py488
-rw-r--r--src/leap/gui/statuspanel.py279
-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.ui129
-rw-r--r--src/leap/gui/ui/mainwindow.ui315
-rw-r--r--src/leap/gui/ui/statuspanel.ui244
-rw-r--r--src/leap/gui/ui/wizard.ui846
-rw-r--r--src/leap/gui/utils.py34
-rw-r--r--src/leap/gui/wizard.py620
-rw-r--r--src/leap/gui/wizardpage.py40
-rw-r--r--src/leap/keymanager/__init__.py341
-rw-r--r--src/leap/keymanager/errors.py86
-rw-r--r--src/leap/keymanager/gpg.py397
-rw-r--r--src/leap/keymanager/keys.py285
-rw-r--r--src/leap/keymanager/openpgp.py636
-rw-r--r--src/leap/keymanager/tests/__init__.py (renamed from src/leap/base/tests/__init__.py)0
-rw-r--r--src/leap/keymanager/tests/test_keymanager.py686
-rw-r--r--src/leap/platform_init/__init__.py28
-rw-r--r--src/leap/platform_init/initializers.py383
-rw-r--r--src/leap/platform_init/locks.py312
-rw-r--r--src/leap/provider/__init__.py (renamed from src/leap/baseapp/__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.py160
-rw-r--r--src/leap/services/eip/__init__.py (renamed from src/leap/eip/__init__.py)0
-rw-r--r--src/leap/services/eip/eipbootstrapper.py178
-rw-r--r--src/leap/services/eip/eipconfig.py260
-rw-r--r--src/leap/services/eip/eipspec.py65
-rw-r--r--src/leap/services/eip/providerbootstrapper.py311
-rw-r--r--src/leap/services/eip/tests/__init__.py (renamed from src/leap/eip/tests/__init__.py)0
-rw-r--r--src/leap/services/eip/tests/test_eipbootstrapper.py347
-rw-r--r--src/leap/services/eip/tests/test_eipconfig.py313
-rw-r--r--src/leap/services/eip/tests/test_providerbootstrapper.py504
-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.py774
-rw-r--r--src/leap/services/eip/vpnprocess.py718
-rw-r--r--src/leap/services/mail/__init__.py (renamed from src/leap/gui/tests/__init__.py)0
-rw-r--r--src/leap/services/mail/smtpbootstrapper.py135
-rw-r--r--src/leap/services/mail/smtpconfig.py47
-rw-r--r--src/leap/services/mail/smtpspec.py51
-rw-r--r--src/leap/services/soledad/__init__.py (renamed from src/leap/testing/__init__.py)0
-rw-r--r--src/leap/services/soledad/soledadbootstrapper.py220
-rw-r--r--src/leap/services/soledad/soledadconfig.py47
-rw-r--r--src/leap/services/soledad/soledadspec.py57
-rw-r--r--src/leap/services/tests/__init__.py (renamed from tests/__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__.py52
-rw-r--r--src/leap/util/certs.py18
-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.py35
-rw-r--r--src/leap/util/leap_argparse.py25
-rw-r--r--src/leap/util/leap_log_handler.py149
-rw-r--r--src/leap/util/misc.py37
-rw-r--r--src/leap/util/polkit.py26
-rw-r--r--src/leap/util/privilege_policies.py82
-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/tests/test_fileutil.py100
-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_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/test_qt_environment.py43
-rw-r--r--tox.ini4
277 files changed, 17066 insertions, 20645 deletions
diff --git a/.gitignore b/.gitignore
index a9b7c1c8..b0f9a137 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,10 @@
*.swo
*.pyc
*.log
+*.*~
.*
+*_rc.py
+ui_*.py
!.coveragerc
!.tx
bin/
@@ -16,12 +19,16 @@ include/
lib/
local/
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/CHANGELOG b/CHANGELOG
new file mode 100644
index 00000000..c3809d2b
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,109 @@
+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 d7a5201e..a4d18e0b 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,8 @@
include pkg/*
-include pkg/branding/*
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 8d63232f..81db6886 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,3 @@
-SHELL := /bin/zsh
# ################################
# Makefile for compiling resources
# files.
@@ -9,7 +8,8 @@ SHELL := /bin/zsh
#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
@@ -18,52 +18,50 @@ TRANSLAT_DIR = data/translations
#Project file, used for translations
PROJFILE = data/leap_client.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 locale.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 = pylupdate4
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)
translations:
$(PYLUP) $(PROJFILE)
$(LRELE) $(TRANSLAT_DIR)/*.ts
-
-$(COMPILED_DIR)/ui_%.py : $(RESOURCE_DIR)/%.ui
+
+$(COMPILED_DIR)/ui_%.py : $(UI_DIR)/%.ui
$(PYUIC) $< -o $@
-
+
$(COMPILED_DIR)/%_rc.py : $(RESOURCE_DIR)/%.qrc
$(PYRCC) $< -o $@
manpages:
- rst2man docs/man/leap-client.1.rst docs/man/leap-client.1
+ rst2man docs/man/leap.1.rst docs/man/leap.1
apidocs:
@sphinx-apidoc -o docs/api src/leap
-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
index 45f1012f..00350cbb 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -2,10 +2,10 @@
User-facing changes in Leap Client
==================================
-Release 0.2.0 (2013-1-XX)
+Release 0.2.0 (2012-10-XX)
--------------------------
-This release is a functionally working version in Debian Wheezy and Ubuntu 12.04.
+This release is a functionally working version in Debian Squeeze and Ubuntu 12.04.
It is able to connect to a preconfigured leap provider and autoconfigures a EIP connection.
Python Support
@@ -14,6 +14,7 @@ This release supports Python2.6 and Python2.7
New Features
''''''''''''
+- Branded build: the final package includes branding info needed to connect to a default provider.
- 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
@@ -39,14 +40,15 @@ for building the package dependencies, you will need also:
Leap-Client depends on the following python packages:
-- pyopenssl
- requests
+- ping
- psutil
- netifaces
- jsonschema
- srp
- pycrypto
- keyring
+- python-gnutls==1.1.9
We are freezing the python-gnutls library dependency for this release due to a bug in ubuntu, see:
https://bugs.launchpad.net/ubuntu/+source/python-gnutls/+bug/1027129
diff --git a/README.rst b/README.rst
index 08c8e38d..7b4924ab 100644
--- a/README.rst
+++ b/README.rst
@@ -1,21 +1,10 @@
-=========================================
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/leap-client/badge.png
+ :target: https://crate.io/packages/leap.client
Dependencies
------------------
@@ -24,6 +13,7 @@ LEAP Client 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,35 +21,17 @@ 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 leap-client 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
-
+ $ make
+ $ sudo LEAP_VENV_SKIP_PYSIDE=1 python setup.py install
Running
-------
@@ -68,15 +40,22 @@ After a successful installation, there should be a launcher called ``leap-client
$ leap-client
+If you are testing a new provider and do not have a CA certificate chain tied to your SSL certificate, you should execute leap-client in the following way::
+
+ $ leap-client --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::
- git://leap.se/leap_client
+ git://leap.se/leap_client
Some steps need to be run when setting a development environment for the first time.
@@ -89,16 +68,26 @@ Make sure you are in the development branch::
(leap_client)$ git checkout develop
-Symlink your global pyqt libraries::
+Symlink your global pyside libraries::
(leap_client)$ pkg/postmkvenv.sh
And make your working tree available to your pythonpath::
- (leap_client)$ python setup.py develop
+ (leap_client)$ python setup.py develop
+Run the client::
-Testing
+ (leap_client)$ 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,7 +95,7 @@ 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
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/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/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
index 57764a23..e91497c0 100644
--- a/data/leap_client.pro
+++ b/data/leap_client.pro
@@ -2,20 +2,31 @@
# 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
+SOURCES += ../src/leap/gui/mainwindow.py \
+ ../src/leap/gui/wizardpage.py \
+ ../src/leap/gui/wizard.py \
+ ../src/leap/config/provider_spec.py \
+ ../src/leap/config/pluggableconfig.py \
+ ../src/leap/config/providerconfig.py \
+ ../src/leap/config/prefixers.py \
+ ../src/leap/config/baseconfig.py \
+ ../src/leap/app.py \
+ ../src/leap/util/checkerthread.py \
+ ../src/leap/util/leap_argparse.py \
+ ../src/leap/util/check.py \
+ ../src/leap/crypto/constants.py \
+ ../src/leap/crypto/srpauth.py \
+ ../src/leap/crypto/srpregister.py \
+ ../src/leap/services/eip/eipbootstrapper.py \
+ ../src/leap/services/eip/udstelnet.py \
+ ../src/leap/services/eip/eipspec.py \
+ ../src/leap/services/eip/vpn.py \
+ ../src/leap/services/eip/vpnlaunchers.py \
+ ../src/leap/services/eip/providerbootstrapper.py \
+ ../src/leap/services/eip/eipconfig.py
+
+FORMS += ../src/leap/gui/ui/mainwindow.ui \
+ ../src/leap/gui/ui/wizard.ui
# where to generate ts files -- tx will pick from here
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..655f2812 100644
--- a/data/resources/mainwindow.qrc
+++ b/data/resources/mainwindow.qrc
@@ -1,12 +1,21 @@
-<!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/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 8daa2037..9b32cdcf 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 84aa6f0a..f6102bfa 100644
--- a/data/translations/es.ts
+++ b/data/translations/es.ts
@@ -1,218 +1,496 @@
<?xml version="1.0" ?><!DOCTYPE TS><TS language="es" version="2.0">
<context>
- <name>IntroPage</name>
+ <name>MainWindow</name>
<message>
- <location filename="../src/leap/gui/firstrun/intro.py" line="14"/>
- <source>First run wizard.</source>
- <translation>Primera Conexion.</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/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/mainwindow.py" line="155"/>
+ <source>Stop</source>
+ <translation>Detener</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/intro.py" line="37"/>
- <source>Sign up for a new account.</source>
- <translation>Registrar una cuenta nueva.</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="257"/>
+ <source>Hide</source>
+ <translation>Ocultar</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/intro.py" line="40"/>
- <source>Log In with my credentials.</source>
- <translation>Loguearme con un usuario que ya tengo.</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="259"/>
+ <source>Show</source>
+ <translation>Mostrar</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>El usuario tiene que ser usuario@tu.proveedor</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="288"/>
+ <source>About LEAP</source>
+ <translation>Acerca de LEAP</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="140"/>
- <source>Resolving domain name</source>
- <translation>Resolviendo nombre de dominio</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/gui/firstrun/login.py" line="163"/>
- <source>Authentication error: %s</source>
- <translation>Error de autenticacion: %s</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>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="179"/>
- <source>Credentials validated.</source>
- <translation>Credenciales validadas.</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>
-</context>
-<context>
- <name>ProviderInfoPage</name>
<message>
- <location filename="../src/leap/gui/firstrun/providerinfo.py" line="18"/>
- <source>Provider Info</source>
- <translation>Info del Proveedor</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/gui/firstrun/providerinfo.py" line="19"/>
- <source>This is what provider says.</source>
- <translation>Esto es lo que dice el proveedor.</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>
-</context>
-<context>
- <name>ProviderSetupValidationPage</name>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="26"/>
- <source>Provider setup</source>
- <translation>Configuracion 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/gui/firstrun/providersetup.py" line="27"/>
- <source>Doing autoconfig.</source>
- <translation>Autoconfigurando.</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="549"/>
+ <source>Stop EIP</source>
+ <translation>Detener EIP</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="83"/>
- <source>Fetching CA certificate</source>
- <translation>Obteniendo certificado de la CA</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/gui/firstrun/providersetup.py" line="105"/>
- <source>Checking CA fingerprint</source>
- <translation>Comprobando el fingerprint de la CA</translation>
+ <location filename="../src/leap/gui/mainwindow.py" line="560"/>
+ <source>Start EIP</source>
+ <translation>Comenzar EIP</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="134"/>
- <source>Validating api certificate</source>
- <translation>Validando certificado de la api</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/gui/mainwindow.py" line="581"/>
+ <source>%s does not support EIP</source>
+ <translation>%s no soporta EIP</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="596"/>
+ <source>Encryption is ON</source>
+ <translation>Cifrado ENCENDIDO</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="614"/>
+ <source>VPN: Authenticating...</source>
+ <translation>VPN: Autorizando...</translation>
+ </message>
+ <message>
+ <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/gui/mainwindow.py" line="618"/>
+ <source>VPN: Connected!</source>
+ <translation>VPN: Conectado!</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/mainwindow.py" line="672"/>
+ <source>Signing out...</source>
+ <translation>Saliendo...</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="14"/>
+ <source>LEAP</source>
+ <translation>LEAP</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="80"/>
+ <source>Remember</source>
+ <translation>Recordar</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="103"/>
+ <source>Login</source>
+ <translation>Acceder</translation>
+ </message>
+ <message>
+ <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/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>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/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/register.py" line="208"/>
- <source>Registration succeeded!</source>
- <translation>Cuenta creada con exito!</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/register.py" line="238"/>
- <source>Password does not match..</source>
- <translation>Las contrasenas no son iguales..</translation>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="272"/>
+ <source>Disconnected</source>
+ <translation>Desconectado</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="242"/>
- <source>Password too short.</source>
- <translation>Contrasena demasiado corta.</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/register.py" line="247"/>
- <source>Password too obvious.</source>
- <translation>Contrasena demasiado obvia.</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/register.py" line="279"/>
- <source>Error connecting to provider (timeout)</source>
- <translation>Error conectandose al proveedor (timeout)</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/register.py" line="284"/>
- <source>Error Connecting to provider (connerr).</source>
- <translation>Error conectandose al proveedor (connerr).</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/register.py" line="292"/>
- <source>Error during registration (%s)</source>
- <translation>Error durante el registro (%s)</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/firstrun/register.py" line="302"/>
- <source>Could not register (bad response)</source>
- <translation>No se pudo registrar (bad response)</translation>
+ <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/register.py" line="309"/>
- <source>Username not available.</source>
- <translation>Usuario no disponible.</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/register.py" line="375"/>
- <source>Register a new user with provider %s.</source>
- <translation>Registrar un nuevo usuario con el proveedor %s.</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/mainwindow.py" line="620"/>
+ <source>VPN: Waiting to start...</source>
+ <translation>VPN: Esperando para comenzar...</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="176"/>
+ <source>Automatically login</source>
+ <translation>Ingresar automáticamente</translation>
</message>
</context>
<context>
- <name>RegisterUserValidationPage</name>
+ <name>Wizard</name>
+ <message>
+ <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/gui/wizard.py" line="155"/>
+ <source>Password too short</source>
+ <translation>Contraseña muy corta</translation>
+ </message>
+ <message>
+ <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/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/wizard.py" line="189"/>
+ <source>Starting registration...</source>
+ <translation>Comenzando el registro...</translation>
+ </message>
+ <message>
+ <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/wizard.py" line="202"/>
+ <source>Unknown error</source>
+ <translation>Error desconocido</translation>
+ </message>
+ <message>
+ <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/ui/wizard.ui" line="14"/>
+ <source>LEAP First run</source>
+ <translation>Primera ejecución de LEAP</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="31"/>
+ <source>Welcome</source>
+ <translation>Bienvenido</translation>
+ </message>
+ <message>
+ <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/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/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/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/regvalidation.py" line="95"/>
- <source>Fetching provider config...</source>
- <translation>Obteniendo configuracion del proveedor...</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/regvalidation.py" line="112"/>
- <source>Authentication error: %s</source>
- <translation>Error de autenticacion: %s</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/regvalidation.py" line="117"/>
- <source>Fetching eip certificate</source>
- <translation>Obteniendo certificado eip</translation>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="128"/>
+ <source>https://</source>
+ <translation>https://</translation>
+ </message>
+ <message>
+ <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/ui/wizard.ui" line="147"/>
+ <source>Download provider information</source>
+ <translation>Descargando información del proveedor</translation>
+ </message>
+ <message>
+ <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/ui/wizard.ui" line="209"/>
+ <source>Name resolution</source>
+ <translation>Resolución de nombre</translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="232"/>
+ <source>Check</source>
+ <translation>Corroborar</translation>
+ </message>
+ <message>
+ <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/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/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>
+ <message>
+ <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/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/ui/wizard.ui" line="420"/>
+ <source>Check API Certificate</source>
+ <translation>Corroborando certificado de API</translation>
+ </message>
+ <message>
+ <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/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/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/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/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/ui/wizard.ui" line="563"/>
+ <source>Register</source>
+ <translation>Registrar</translation>
+ </message>
+ <message>
+ <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/ui/wizard.ui" line="587"/>
+ <source>Setting up Encrypted Internet</source>
+ <translation>Configurando Internet Cifrado</translation>
+ </message>
+ <message>
+ <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/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>SelectProviderPage</name>
+ <name>__impl</name>
+ <message>
+ <location filename="../src/leap/crypto/srpauth.py" line="161"/>
+ <source>Unknown user</source>
+ <translation>Usuario desconocido</translation>
+ </message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="32"/>
- <source>Enter Provider</source>
- <translation>Entra tu Proveedor</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/gui/firstrun/providerselect.py" line="33"/>
- <source>Please enter the domain of the provider you want to use for your connection.</source>
- <translation>Por favor, rellena el dominio del proveedor que quieras usar para tu conexion.</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/gui/firstrun/providerselect.py" line="87"/>
- <source>chec&amp;k!</source>
- <translation>compro&amp;bar!</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/gui/firstrun/providerselect.py" line="97"/>
- <source>Server certificate could not be verified.</source>
- <translation>No se pudo verificar el certificado del servidor.</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/gui/firstrun/providerselect.py" line="136"/>
- <source>Certificate validation</source>
- <translation>Validacion del certificado</translation>
+ <location filename="../src/leap/crypto/srpauth.py" line="228"/>
+ <source>Wrong password</source>
+ <translation>Password incorrecto</translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="222"/>
- <source>checking domain name</source>
- <translation>comprobando nombre de dominio</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/gui/firstrun/providerselect.py" line="276"/>
- <source>checking https connection</source>
- <translation>comprobando conexion https</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/gui/firstrun/providerselect.py" line="292"/>
- <source>Could not get info from provider.</source>
- <translation>no se pudo obtener info del proveedor</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/gui/firstrun/providerselect.py" line="295"/>
- <source>Could not download provider info (refused conn.).</source>
- <translation>no se pudo obtener info del proveedor (refused conn.).</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/gui/firstrun/providerselect.py" line="305"/>
- <source>fetching provider info</source>
- <translation>obteniendo info del preveedor</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 d2cba837..426acfbc 100644
--- a/data/ts/en_US.ts
+++ b/data/ts/en_US.ts
@@ -1,476 +1,496 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS><TS version="2.0">
<context>
- <name>ConnectionPage</name>
+ <name>MainWindow</name>
<message>
- <location filename="../src/leap/gui/firstrun/connect.py" line="26"/>
- <source>Connecting...</source>
+ <location filename="../src/leap/gui/mainwindow.py" line="592"/>
+ <source>Encryption is OFF</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/mainwindow.py" line="155"/>
+ <source>Stop</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/connect.py" line="85"/>
- <source>Getting EIP configuration files</source>
+ <location filename="../src/leap/gui/mainwindow.py" line="257"/>
+ <source>Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/connect.py" line="101"/>
- <source>Authentication error: %s</source>
+ <location filename="../src/leap/gui/mainwindow.py" line="259"/>
+ <source>Show</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/connect.py" line="109"/>
- <source>Getting EIP certificate</source>
+ <location filename="../src/leap/gui/mainwindow.py" line="288"/>
+ <source>About LEAP</source>
<translation type="unfinished"></translation>
</message>
-</context>
-<context>
- <name>EIPConductorAppMixin</name>
<message>
- <location filename="../src/leap/baseapp/eip.py" line="221"/>
- <source>&amp;Disconnect</source>
+ <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 type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/eip.py" line="235"/>
- <source>&amp;Connect</source>
+ <location filename="../src/leap/gui/mainwindow.py" line="433"/>
+ <source>Could not load provider configuration</source>
<translation type="unfinished"></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>
+ <location filename="../src/leap/gui/mainwindow.py" line="459"/>
+ <source>Please select a valid provider</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="463"/>
+ <source>Please provide a valid username</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="467"/>
+ <source>Please provide a valid Password</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="470"/>
+ <source>Logging in...</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="549"/>
+ <source>Stop EIP</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="558"/>
+ <source>EIP has stopped</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="560"/>
+ <source>Start EIP</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="573"/>
+ <source>Checking configuration, please wait...</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="581"/>
+ <source>%s does not support EIP</source>
<translation type="unfinished"></translation>
</message>
-</context>
-<context>
- <name>Errors</name>
<message>
- <location filename="../src/leap/base/exceptions.py" line="57"/>
- <source>Interface not found</source>
+ <location filename="../src/leap/gui/mainwindow.py" line="596"/>
+ <source>Encryption is ON</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/gui/mainwindow.py" line="614"/>
+ <source>VPN: Authenticating...</source>
<translation type="unfinished"></translation>
</message>
<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/gui/mainwindow.py" line="616"/>
+ <source>VPN: Retrieving configuration...</source>
<translation type="unfinished"></translation>
</message>
<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/mainwindow.py" line="618"/>
+ <source>VPN: Connected!</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/mainwindow.py" line="672"/>
+ <source>Signing out...</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/mainwindow.ui" line="14"/>
+ <source>LEAP</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/mainwindow.ui" line="80"/>
+ <source>Remember</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/mainwindow.ui" line="103"/>
+ <source>Login</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/mainwindow.ui" line="123"/>
+ <source>&lt;b&gt;Provider:&lt;/b&gt;</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/mainwindow.ui" line="133"/>
+ <source>&lt;b&gt;Password:&lt;/b&gt;</source>
<translation type="unfinished"></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>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="153"/>
+ <source>&lt;b&gt;User:&lt;/b&gt;</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/ui/mainwindow.ui" line="219"/>
+ <source>0.0 Kb</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/ui/mainwindow.ui" line="272"/>
+ <source>Disconnected</source>
<translation type="unfinished"></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>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="332"/>
+ <source>&amp;Session</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="140"/>
- <source>Resolving domain name</source>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="340"/>
+ <source>Help</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="163"/>
- <source>Authentication error: %s</source>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="348"/>
+ <source>S&amp;ettings</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="179"/>
- <source>Credentials validated.</source>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="359"/>
+ <source>&amp;Sign out</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="24"/>
- <source>Log In</source>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="364"/>
+ <source>&amp;Quit</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="38"/>
- <source>User &amp;name:</source>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="369"/>
+ <source>About &amp;LEAP</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="53"/>
- <source>&amp;Password:</source>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="374"/>
+ <source>&amp;Help</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="80"/>
- <source>Log in</source>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="379"/>
+ <source>&amp;Wizard</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/login.py" line="25"/>
- <source>Log in with your credentials</source>
+ <location filename="../src/leap/gui/mainwindow.py" line="620"/>
+ <source>VPN: Waiting to start...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/mainwindow.ui" line="176"/>
+ <source>Automatically login</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>LogPaneMixin</name>
+ <name>Wizard</name>
<message>
- <location filename="../src/leap/baseapp/log.py" line="25"/>
- <source>&amp;Connect</source>
+ <location filename="../src/leap/gui/wizard.py" line="152"/>
+ <source>Passwords don&apos;t match</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/log.py" line="38"/>
- <source>Disconnected</source>
+ <location filename="../src/leap/gui/wizard.py" line="155"/>
+ <source>Password too short</source>
<translation type="unfinished"></translation>
</message>
-</context>
-<context>
- <name>ProviderInfoPage</name>
<message>
- <location filename="../src/leap/gui/firstrun/providerinfo.py" line="19"/>
- <source>Provider Information</source>
+ <location filename="../src/leap/gui/wizard.py" line="158"/>
+ <source>Password too easy</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/wizard.py" line="161"/>
+ <source>Password equal to username</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerinfo.py" line="95"/>
- <source>enrollment policy</source>
+ <location filename="../src/leap/gui/wizard.py" line="189"/>
+ <source>Starting registration...</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/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 type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="85"/>
- <source>Fetching CA certificate</source>
+ <location filename="../src/leap/gui/wizard.py" line="202"/>
+ <source>Unknown error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="107"/>
- <source>Checking CA fingerprint</source>
+ <location filename="../src/leap/gui/wizard.py" line="303"/>
+ <source>Unable to load provider configuration</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providersetup.py" line="125"/>
- <source>Validating api certificate</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/providersetup.py" line="29"/>
- <source>Gathering configuration options for this provider</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/ui/wizard.ui" line="34"/>
+ <source>This is the LEAP Client first run wizard</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="43"/>
+ <source>Log In with my credentials</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="208"/>
- <source>Registration succeeded!</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/register.py" line="244"/>
- <source>Password does not match..</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/register.py" line="248"/>
- <source>Password too short.</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/register.py" line="253"/>
- <source>Password too obvious.</source>
+ <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 type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="282"/>
- <source>Error connecting to provider (timeout)</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="128"/>
+ <source>https://</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="287"/>
- <source>Error Connecting to provider (connerr).</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="368"/>
+ <source>Checking provider</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="295"/>
- <source>Error during registration (%s)</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="147"/>
+ <source>Download provider information</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="305"/>
- <source>Could not register (bad response)</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="154"/>
+ <source>HTTPS Connection</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="312"/>
- <source>Username not available.</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="209"/>
+ <source>Name resolution</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="48"/>
- <source>User &amp;name:</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="232"/>
+ <source>Check</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="60"/>
- <source>&amp;Password:</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="253"/>
+ <source>Provider Information</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="66"/>
- <source>Password (again):</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="256"/>
+ <source>Services offered by this provider</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="72"/>
- <source>&amp;Remember username and password.</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="278"/>
+ <source>&lt;b&gt;Enrollment policy:&lt;/b&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/register.py" line="378"/>
- <source>Register a new user with provider &lt;em&gt;%s&lt;/em&gt;</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="285"/>
+ <source>URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="292"/>
+ <source>Name</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="325"/>
+ <source>policy</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="87"/>
- <source>chec&amp;k!</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="332"/>
+ <source>Desc</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="356"/>
+ <source>Provider setup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="359"/>
+ <source>Gathering configuration options for this provider</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="97"/>
- <source>Server certificate could not be verified.</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="406"/>
+ <source>Download CA Certificate</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="136"/>
- <source>Certificate validation</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="413"/>
+ <source>Check CA Certificate Fingerprint</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="290"/>
- <source>Could not get info from provider.</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="420"/>
+ <source>Check API Certificate</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="293"/>
- <source>Could not download provider info (refused conn.).</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="486"/>
+ <source>Register new user</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="103"/>
- <source>&amp;Trust this provider certificate.</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="489"/>
+ <source>Register a new user with provider</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="344"/>
- <source>Do you want to &lt;b&gt;trust this provider certificate?&lt;/b&gt;</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="524"/>
+ <source>&lt;b&gt;User:&lt;/b&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="347"/>
- <source>SHA-256 fingerprint: &lt;i&gt;%s&lt;/i&gt;&lt;br&gt;</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="534"/>
+ <source>&lt;b&gt;Password:&lt;/b&gt;</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/ui/wizard.ui" line="553"/>
+ <source>&lt;b&gt;Re-enter password:&lt;/b&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="222"/>
- <source>Checking if it is a valid provider</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="563"/>
+ <source>Register</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="276"/>
- <source>Checking for a secure connection</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="584"/>
+ <source>EIP Setup</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/gui/firstrun/providerselect.py" line="303"/>
- <source>Getting info from the provider</source>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="587"/>
+ <source>Setting up Encrypted Internet</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="609"/>
+ <source>Checking EIP</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="615"/>
+ <source>Download EIP configuration</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="622"/>
+ <source>Download client certificate</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="691"/>
+ <source>Congratulations!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/leap/gui/ui/wizard.ui" line="694"/>
+ <source>You have successfully configured the LEAP client.</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="161"/>
+ <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="167"/>
+ <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="171"/>
+ <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="199"/>
+ <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="220"/>
+ <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="228"/>
+ <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="234"/>
+ <source>Unknown error (%s)</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/crypto/srpauth.py" line="243"/>
+ <source>Problem getting data from server</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/crypto/srpauth.py" line="264"/>
+ <source>Bad data from server</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="162"/>
- <source>connecting...</source>
+ <location filename="../src/leap/crypto/srpauth.py" line="270"/>
+ <source>Auth verification failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../src/leap/baseapp/systray.py" line="185"/>
- <source>About</source>
+ <location filename="../src/leap/crypto/srpauth.py" line="400"/>
+ <source>Succeeded</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 c61b258c..d3bdf1ee 100644
--- a/docs/checklist_for_leap_client_release.wiki
+++ b/docs/checklist_for_leap_client_release.wiki
@@ -4,7 +4,7 @@
* [ ] all rc-critical closed!
* [ ] all bbots green
* [ ] uploaded translations: make translations
- * [ ] re-generate pyqt resources
+ * [ ] re-generate pyside resources
* [ ] update docs
* [ ] CREDITS
@@ -34,9 +34,6 @@
* [ ] 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
diff --git a/docs/conf.py b/docs/conf.py
index 05c8cf5b..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.1.0'
+version = '0.2.1-dev1'
# The full version, including alpha/beta/rc tags.
-release = '0.1.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/dev/environment.rst b/docs/dev/environment.rst
index c3868b81..010ccc83 100644
--- a/docs/dev/environment.rst
+++ b/docs/dev/environment.rst
@@ -8,19 +8,20 @@ This document covers how to get an enviroment ready to contribute code to the LE
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 checkout develop
Base Dependencies
------------------
Leap client 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,7 +100,7 @@ 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 `leap-client` 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
diff --git a/docs/dev/internationalization.rst b/docs/dev/internationalization.rst
index 1a9af0be..8c584fdd 100644
--- a/docs/dev/internationalization.rst
+++ b/docs/dev/internationalization.rst
@@ -6,7 +6,7 @@ Internationalization
This part of the documentation covers the localization and translation of LEAP Client.
Because we want to *bring fire to the people*, in as many countries and languages as possible.
-Translating the LEAP Client PyQt Application
+Translating the LEAP Client PySide Application
--------------------------------------------
.. raw:: html
@@ -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,9 +55,9 @@ 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::
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/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/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..a3f8da09 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::
diff --git a/docs/user/install.rst b/docs/user/install.rst
index 1f0fd831..0467ba8b 100644
--- a/docs/user/install.rst
+++ b/docs/user/install.rst
@@ -6,42 +6,56 @@ Installation
This part of the documentation covers the installation of the LEAP Client.
We assume that you want to get it properly installed before being able to use it.
-Debian package
---------------
-
-.. warning::
-
- No updated debian package yet.
+.. note::
-Once we have a release candidate, probably the easiest way of having the LEAP Client installed will be to install a .deb package under debian or ubuntu systems.
+ 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>`.
Distribute & Pip
----------------
-.. warning::
-
- This does not work yet, since we have not released an initial version yet to the cheese shop.
+.. 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.
-Installing LEAP Client will be as simple as using `pip <http://www.pip-installer.org/>`_ once we have a release candidate::
+Installing LEAP Client is as simple as using `pip <http://www.pip-installer.org/>`_ for the already released versions ::
$ pip install leap-client
-Get the code
-------------
+Debian package
+--------------
.. warning::
- This... won't work either, as-is. This should be the third optional way to install stable releases from master branch. Right now that does not work because there is *nothing* updated in the master branch. Leaving this here since this is what we will be doing, but if you really intend to have a working tree, refer to the sections :ref:`setting up a working environment <environment>` or :ref:`fetching latest code for testing <fetchinglatest>`.
+ 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,
+
+First, you need to bootstrap your apt-key::
+
+ # gpg --recv-key 0x1E34A1828E207901 0x485B12FA218E81EB
+ # gpg --list-sigs 0x1E34A1828E207901
+ # gpg --list-sigs 0x485B12FA218E81EB
+ # gpg -a --export 0x1E34A1828E207901 | sudo apt-key add -
+
+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 you can happily install leap-client::
+
+ apt-get install leap-client
+
+Show me the code!
+-----------------
You can get the code from LEAP public git repository ::
- git clone git://leap.se/leap_client
+ $ git clone git://leap.se/leap_client
Or from the github mirror ::
- git clone git://github.com/leapcode/leap_client.git
+ $ git clone git://github.com/leapcode/leap_client.git
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..9461d5f2 100644
--- a/docs/user/intro.rst
+++ b/docs/user/intro.rst
@@ -6,7 +6,7 @@ Introduction
The LEAP Client
---------------
.. 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.
+The **LEAP 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.
Features
^^^^^^^^
diff --git a/docs/user/running.rst b/docs/user/running.rst
index 35ec1f6d..43f3e87c 100644
--- a/docs/user/running.rst
+++ b/docs/user/running.rst
@@ -3,14 +3,19 @@
Running
==================
-This document dovers how to launch the LEAP Client.
+This document covers how to launch the LEAP Client.
Launching the client
--------------------
-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 `leap-client` somewhere in your path::
% leap-client
+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:
@@ -24,14 +29,13 @@ If you ask for it, you can also have all that debug info in a beautiful file rea
$ leap-client --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::
+.. $ leap-client --openvpn-verbosity 4
Options
------------
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..ec3c2834 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"
+# LEAP CLient 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 LEAP Client 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..2f0cba45 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 )
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 839722de..1b72ed6a 100644
--- a/pkg/requirements.pip
+++ b/pkg/requirements.pip
@@ -1,17 +1,27 @@
# 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
+pyopenssl
keyring
python-dateutil
-sh
-pyxdg
+psutil
+ipaddr
+twisted
+qt4reactor
+python-gnupg
+
+leap.common>=0.2.5
+leap.soledad>=0.1.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/run_tests.sh b/run_tests.sh
index 7cbed018..500cf53c 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -16,6 +16,7 @@ function usage {
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,"
@@ -35,6 +36,7 @@ function process_option {
-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
@@ -54,6 +56,7 @@ just_pep8=0
no_pep8=0
coverage=0
alltests=0
+progressive=0
for arg in "$@"; do
process_option $arg
@@ -61,7 +64,7 @@ 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"
fi
if [ $no_site_packages -eq 1 ]; then
@@ -70,11 +73,20 @@ fi
# If alltests flag is not set, let's exclude some dirs that are troublesome.
if [ $alltests -eq 0 ]; then
- noseopts="$noseopts --exclude-dir=src/leap/soledad"
+ 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=$?
@@ -83,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,_version.py --repeat"
+ pep8_opts="--ignore=E202,W602 --exclude=*_rc.py,ui_*,_version.py --repeat"
${wrapper} pep8 ${pep8_opts} ${srcfiles}
}
@@ -93,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
@@ -140,9 +154,11 @@ if [ -z "$noseargs" ]; then
fi
function run_coverage {
- cov_opts="--omit=`pwd`/src/leap/base/tests/*,`pwd`/src/leap/eip/tests/*,`pwd`/src/leap/gui/tests/*"
- cov_opts="$cov_opts,`pwd`/src/leap/util/tests/* "
- cov_opts="$cov_opts --include=`pwd`/src/leap/*" #,`pwd`/src/leap/eip/*"
+ cov_opts="--include=`pwd`/src/leap/*" #,`pwd`/src/leap/eip/*"
+ cov_opts="$cov_opts --omit=`pwd`/src/leap/gui/ui_*,`pwd`/src/leap/gui/*_rc.py*"
+ #cov_opts="--omit=`pwd`/src/leap/base/tests/*,`pwd`/src/leap/eip/tests/*,`pwd`/src/leap/gui/tests/*"
+ #cov_opts="$cov_opts,`pwd`/src/leap/util/tests/* "
+ #cov_opts="$cov_opts --include=`pwd`/src/leap/*" #,`pwd`/src/leap/eip/*"
${wrapper} coverage html -d docs/covhtml -i $cov_opts
echo "now point your browser at docs/covhtml/index.html"
}
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 4b049f97..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[egg_info]
-#tag_build = dev
diff --git a/setup.py b/setup.py
index a7016b94..4b39189c 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 = 'leap_client-'
+
+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,142 +48,72 @@ 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')
+parsed_reqs = utils.parse_requirements()
-from setuptools import Command
+cmdclass = versioneer.get_cmdclass()
+leap_launcher = 'leap-client=leap.app:main'
+from setuptools.command.develop import develop as _develop
-class DoBranding(Command):
- description = "copy the branding info the the top level package"
- user_options = []
- def initialize_options(self):
- pass
+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_develop(_develop):
+ def run(self):
+ # versioneer:
+ versions = versioneer.get_versions(verbose=True)
+ self._versioneer_generated_versions = versions
+ # unless we update this, the command will keep using the old version
+ self.distribution.metadata.version = versions["version"]
- def finalize_options(self):
- pass
+ _develop.run(self)
+ copy_reqs(self.egg_path)
- def run(self):
- do_branding()
+cmdclass["develop"] = cmd_develop
-from distutils.command.build import build as _build
-from distutils.command.sdist import sdist as _sdist
+# next two classes need to augment the versioneer modified ones
+versioneer_build = cmdclass['build']
+versioneer_sdist = cmdclass['sdist']
-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_build(versioneer_build):
def run(self):
- # versioneer:
- versions = versioneer.get_versions(verbose=True)
- self._versioneer_generated_versions = versions
- # unless we update this, the command will keep using the old version
- self.distribution.metadata.version = versions["version"]
+ versioneer_build.run(self)
+ copy_reqs(self.build_lib)
+
- # branding:
- do_branding()
- return _sdist.run(self)
+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="leap-client",
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 +127,11 @@ 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='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 +139,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.
+ # 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/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/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/app.py b/src/leap/app.py
index 1b2ccd61..cb9951c1 100644
--- a/src/leap/app.py
+++ b/src/leap/app.py
@@ -1,72 +1,125 @@
-# 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
+
+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.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 install_qtreactor(logger):
+ import qt4reactor
+ qt4reactor.install()
+ logger.debug("Qt4 reactor installed")
def main():
"""
- launches the main event loop
- long live to the (hidden) leap window!
+ Starts the main event loop and launches the main window.
"""
- import sys
- from leap.util import leap_argparse
- parser, opts = leap_argparse.init_leapc_args()
- debug = getattr(opts, 'debug', False)
+ event_server.ensure_server(event_server.SERVER_PORT)
+
+ _, opts = leap_argparse.init_leapc_args()
+ debug = opts.debug
+ standalone = opts.standalone
+ bypass_checks = opts.danger
- # XXX get severity from command line args
+ # TODO: get severity from command line args
if debug:
level = logging.DEBUG
else:
level = logging.WARNING
+ # Console logger
logger = logging.getLogger(name='leap')
logger.setLevel(level)
console = logging.StreamHandler()
console.setLevel(level)
- formatter = logging.Formatter(
- '%(asctime)s '
- '- %(name)s - %(levelname)s - %(message)s')
+ log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+ formatter = logging.Formatter(log_format)
console.setFormatter(formatter)
logger.addHandler(console)
+ # LEAP custom handler
+ leap_handler = LeapLogHandler()
+ leap_handler.setLevel(level)
+ logger.addHandler(leap_handler)
+
+ logger.debug('Leap handler plugged!')
+
+ if not we_are_the_one_and_only():
+ # leap-client is already running
+ logger.warning("Tried to launch more than one instance "
+ "of leap-client. Raising the existing "
+ "one instead.")
+ sys.exit(1)
+
+ check_requirements()
+
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)
+ logfile = opts.log_file
+ 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.info('Starting app')
- app = QApplication(sys.argv)
+ app = QtGui.QApplication(sys.argv)
- # launch polkit-auth agent if needed
- if platform.system() == "Linux":
- polkit.check_if_running_polkit_auth()
+ # install the qt4reactor.
+ install_qtreactor(logger)
# To test:
# $ LANG=es ./app.py
@@ -75,52 +128,42 @@ 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,
+ bypass_checks=bypass_checks)
+ window.show()
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_()
+
+ 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 0bf44f59..00000000
--- a/src/leap/base/checks.py
+++ /dev/null
@@ -1,213 +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:
- 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 2e31b33b..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(CriticalError):
- message = "no connection to gateway"
- usermessage = translate(
- "Errors",
- "Looks like there are problems "
- "with your internet connection")
-
-
-class NoInternetConnection(CriticalError):
- 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/jsonschema.py b/src/leap/base/jsonschema.py
deleted file mode 100644
index 56689b08..00000000
--- a/src/leap/base/jsonschema.py
+++ /dev/null
@@ -1,791 +0,0 @@
-"""
-An implementation of JSON Schema for Python
-
-The main functionality is provided by the validator classes for each of the
-supported JSON Schema versions.
-
-Most commonly, :func:`validate` is the quickest way to simply validate a given
-instance under a schema, and will create a validator for you.
-
-"""
-
-from __future__ import division, unicode_literals
-
-import collections
-import json
-import itertools
-import operator
-import re
-import sys
-
-
-__version__ = "0.8.0"
-
-PY3 = sys.version_info[0] >= 3
-
-if PY3:
- from urllib import parse as urlparse
- from urllib.parse import unquote
- from urllib.request import urlopen
- basestring = unicode = str
- iteritems = operator.methodcaller("items")
-else:
- from itertools import izip as zip
- from urllib import unquote
- from urllib2 import urlopen
- import urlparse
- iteritems = operator.methodcaller("iteritems")
-
-
-FLOAT_TOLERANCE = 10 ** -15
-validators = {}
-
-
-def validates(version):
- """
- Register the decorated validator for a ``version`` of the specification.
-
- Registered validators and their meta schemas will be considered when
- parsing ``$schema`` properties' URIs.
-
- :argument str version: an identifier to use as the version's name
- :returns: a class decorator to decorate the validator with the version
-
- """
-
- def _validates(cls):
- validators[version] = cls
- return cls
- return _validates
-
-
-class UnknownType(Exception):
- """
- An attempt was made to check if an instance was of an unknown type.
-
- """
-
-
-class RefResolutionError(Exception):
- """
- A JSON reference failed to resolve.
-
- """
-
-
-class SchemaError(Exception):
- """
- The provided schema is malformed.
-
- The same attributes are present as for :exc:`ValidationError`\s.
-
- """
-
- def __init__(self, message, validator=None, path=()):
- super(SchemaError, self).__init__(message, validator, path)
- self.message = message
- self.path = list(path)
- self.validator = validator
-
- def __str__(self):
- return self.message
-
-
-class ValidationError(Exception):
- """
- The instance didn't properly validate under the provided schema.
-
- Relevant attributes are:
- * ``message`` : a human readable message explaining the error
- * ``path`` : a list containing the path to the offending element (or []
- if the error happened globally) in *reverse* order (i.e.
- deepest index first).
-
- """
-
- def __init__(self, message, validator=None, path=()):
- # Any validator that recurses (e.g. properties and items) must append
- # to the ValidationError's path to properly maintain where in the
- # instance the error occurred
- super(ValidationError, self).__init__(message, validator, path)
- self.message = message
- self.path = list(path)
- self.validator = validator
-
- def __str__(self):
- return self.message
-
-
-@validates("draft3")
-class Draft3Validator(object):
- """
- A validator for JSON Schema draft 3.
-
- """
-
- DEFAULT_TYPES = {
- "array": list, "boolean": bool, "integer": int, "null": type(None),
- "number": (int, float), "object": dict, "string": basestring,
- }
-
- def __init__(self, schema, types=(), resolver=None):
- self._types = dict(self.DEFAULT_TYPES)
- self._types.update(types)
-
- if resolver is None:
- resolver = RefResolver.from_schema(schema)
-
- self.resolver = resolver
- self.schema = schema
-
- def is_type(self, instance, type):
- if type == "any":
- return True
- elif type not in self._types:
- raise UnknownType(type)
- type = self._types[type]
-
- # bool inherits from int, so ensure bools aren't reported as integers
- if isinstance(instance, bool):
- type = _flatten(type)
- if int in type and bool not in type:
- return False
- return isinstance(instance, type)
-
- def is_valid(self, instance, _schema=None):
- error = next(self.iter_errors(instance, _schema), None)
- return error is None
-
- @classmethod
- def check_schema(cls, schema):
- for error in cls(cls.META_SCHEMA).iter_errors(schema):
- raise SchemaError(
- error.message, validator=error.validator, path=error.path,
- )
-
- def iter_errors(self, instance, _schema=None):
- if _schema is None:
- _schema = self.schema
-
- for k, v in iteritems(_schema):
- validator = getattr(self, "validate_%s" % (k.lstrip("$"),), None)
-
- if validator is None:
- continue
-
- errors = validator(v, instance, _schema) or ()
- for error in errors:
- # set the validator if it wasn't already set by the called fn
- if error.validator is None:
- error.validator = k
- yield error
-
- def validate(self, *args, **kwargs):
- for error in self.iter_errors(*args, **kwargs):
- raise error
-
- def validate_type(self, types, instance, schema):
- types = _list(types)
-
- for type in types:
- if self.is_type(type, "object"):
- if self.is_valid(instance, type):
- return
- elif self.is_type(type, "string"):
- if self.is_type(instance, type):
- return
- else:
- yield ValidationError(_types_msg(instance, types))
-
- def validate_properties(self, properties, instance, schema):
- if not self.is_type(instance, "object"):
- return
-
- for property, subschema in iteritems(properties):
- if property in instance:
- for error in self.iter_errors(instance[property], subschema):
- error.path.append(property)
- yield error
- elif subschema.get("required", False):
- yield ValidationError(
- "%r is a required property" % (property,),
- validator="required",
- path=[property],
- )
-
- def validate_patternProperties(self, patternProperties, instance, schema):
- if not self.is_type(instance, "object"):
- return
-
- for pattern, subschema in iteritems(patternProperties):
- for k, v in iteritems(instance):
- if re.match(pattern, k):
- for error in self.iter_errors(v, subschema):
- yield error
-
- def validate_additionalProperties(self, aP, instance, schema):
- if not self.is_type(instance, "object"):
- return
-
- extras = set(_find_additional_properties(instance, schema))
-
- if self.is_type(aP, "object"):
- for extra in extras:
- for error in self.iter_errors(instance[extra], aP):
- yield error
- elif not aP and extras:
- error = "Additional properties are not allowed (%s %s unexpected)"
- yield ValidationError(error % _extras_msg(extras))
-
- def validate_dependencies(self, dependencies, instance, schema):
- if not self.is_type(instance, "object"):
- return
-
- for property, dependency in iteritems(dependencies):
- if property not in instance:
- continue
-
- if self.is_type(dependency, "object"):
- for error in self.iter_errors(instance, dependency):
- yield error
- else:
- dependencies = _list(dependency)
- for dependency in dependencies:
- if dependency not in instance:
- yield ValidationError(
- "%r is a dependency of %r" % (dependency, property)
- )
-
- def validate_items(self, items, instance, schema):
- if not self.is_type(instance, "array"):
- return
-
- if self.is_type(items, "object"):
- for index, item in enumerate(instance):
- for error in self.iter_errors(item, items):
- error.path.append(index)
- yield error
- else:
- for (index, item), subschema in zip(enumerate(instance), items):
- for error in self.iter_errors(item, subschema):
- error.path.append(index)
- yield error
-
- def validate_additionalItems(self, aI, instance, schema):
- if (
- not self.is_type(instance, "array") or
- not self.is_type(schema.get("items"), "array")
- ):
- return
-
- if self.is_type(aI, "object"):
- for item in instance[len(schema):]:
- for error in self.iter_errors(item, aI):
- yield error
- elif not aI and len(instance) > len(schema.get("items", [])):
- error = "Additional items are not allowed (%s %s unexpected)"
- yield ValidationError(
- error % _extras_msg(instance[len(schema.get("items", [])):])
- )
-
- def validate_minimum(self, minimum, instance, schema):
- if not self.is_type(instance, "number"):
- return
-
- instance = float(instance)
- if schema.get("exclusiveMinimum", False):
- failed = instance <= minimum
- cmp = "less than or equal to"
- else:
- failed = instance < minimum
- cmp = "less than"
-
- if failed:
- yield ValidationError(
- "%r is %s the minimum of %r" % (instance, cmp, minimum)
- )
-
- def validate_maximum(self, maximum, instance, schema):
- if not self.is_type(instance, "number"):
- return
-
- instance = float(instance)
- if schema.get("exclusiveMaximum", False):
- failed = instance >= maximum
- cmp = "greater than or equal to"
- else:
- failed = instance > maximum
- cmp = "greater than"
-
- if failed:
- yield ValidationError(
- "%r is %s the maximum of %r" % (instance, cmp, maximum)
- )
-
- def validate_minItems(self, mI, instance, schema):
- if self.is_type(instance, "array") and len(instance) < mI:
- yield ValidationError("%r is too short" % (instance,))
-
- def validate_maxItems(self, mI, instance, schema):
- if self.is_type(instance, "array") and len(instance) > mI:
- yield ValidationError("%r is too long" % (instance,))
-
- def validate_uniqueItems(self, uI, instance, schema):
- if uI and self.is_type(instance, "array") and not _uniq(instance):
- yield ValidationError("%r has non-unique elements" % instance)
-
- def validate_pattern(self, patrn, instance, schema):
- if self.is_type(instance, "string") and not re.match(patrn, instance):
- yield ValidationError("%r does not match %r" % (instance, patrn))
-
- def validate_minLength(self, mL, instance, schema):
- if self.is_type(instance, "string") and len(instance) < mL:
- yield ValidationError("%r is too short" % (instance,))
-
- def validate_maxLength(self, mL, instance, schema):
- if self.is_type(instance, "string") and len(instance) > mL:
- yield ValidationError("%r is too long" % (instance,))
-
- def validate_enum(self, enums, instance, schema):
- if instance not in enums:
- yield ValidationError("%r is not one of %r" % (instance, enums))
-
- def validate_divisibleBy(self, dB, instance, schema):
- if not self.is_type(instance, "number"):
- return
-
- if isinstance(dB, float):
- mod = instance % dB
- failed = (mod > FLOAT_TOLERANCE) and (dB - mod) > FLOAT_TOLERANCE
- else:
- failed = instance % dB
-
- if failed:
- yield ValidationError("%r is not divisible by %r" % (instance, dB))
-
- def validate_disallow(self, disallow, instance, schema):
- for disallowed in _list(disallow):
- if self.is_valid(instance, {"type": [disallowed]}):
- yield ValidationError(
- "%r is disallowed for %r" % (disallowed, instance)
- )
-
- def validate_extends(self, extends, instance, schema):
- if self.is_type(extends, "object"):
- extends = [extends]
- for subschema in extends:
- for error in self.iter_errors(instance, subschema):
- yield error
-
- def validate_ref(self, ref, instance, schema):
- resolved = self.resolver.resolve(ref)
- for error in self.iter_errors(instance, resolved):
- yield error
-
-
-Draft3Validator.META_SCHEMA = {
- "$schema": "http://json-schema.org/draft-03/schema#",
- "id": "http://json-schema.org/draft-03/schema#",
- "type": "object",
-
- "properties": {
- "type": {
- "type": ["string", "array"],
- "items": {"type": ["string", {"$ref": "#"}]},
- "uniqueItems": True,
- "default": "any"
- },
- "properties": {
- "type": "object",
- "additionalProperties": {"$ref": "#", "type": "object"},
- "default": {}
- },
- "patternProperties": {
- "type": "object",
- "additionalProperties": {"$ref": "#"},
- "default": {}
- },
- "additionalProperties": {
- "type": [{"$ref": "#"}, "boolean"], "default": {}
- },
- "items": {
- "type": [{"$ref": "#"}, "array"],
- "items": {"$ref": "#"},
- "default": {}
- },
- "additionalItems": {
- "type": [{"$ref": "#"}, "boolean"], "default": {}
- },
- "required": {"type": "boolean", "default": False},
- "dependencies": {
- "type": ["string", "array", "object"],
- "additionalProperties": {
- "type": ["string", "array", {"$ref": "#"}],
- "items": {"type": "string"}
- },
- "default": {}
- },
- "minimum": {"type": "number"},
- "maximum": {"type": "number"},
- "exclusiveMinimum": {"type": "boolean", "default": False},
- "exclusiveMaximum": {"type": "boolean", "default": False},
- "minItems": {"type": "integer", "minimum": 0, "default": 0},
- "maxItems": {"type": "integer", "minimum": 0},
- "uniqueItems": {"type": "boolean", "default": False},
- "pattern": {"type": "string", "format": "regex"},
- "minLength": {"type": "integer", "minimum": 0, "default": 0},
- "maxLength": {"type": "integer"},
- "enum": {"type": "array", "minItems": 1, "uniqueItems": True},
- "default": {"type": "any"},
- "title": {"type": "string"},
- "description": {"type": "string"},
- "format": {"type": "string"},
- "maxDecimal": {"type": "number", "minimum": 0},
- "divisibleBy": {
- "type": "number",
- "minimum": 0,
- "exclusiveMinimum": True,
- "default": 1
- },
- "disallow": {
- "type": ["string", "array"],
- "items": {"type": ["string", {"$ref": "#"}]},
- "uniqueItems": True
- },
- "extends": {
- "type": [{"$ref": "#"}, "array"],
- "items": {"$ref": "#"},
- "default": {}
- },
- "id": {"type": "string", "format": "uri"},
- "$ref": {"type": "string", "format": "uri"},
- "$schema": {"type": "string", "format": "uri"},
- },
- "dependencies": {
- "exclusiveMinimum": "minimum", "exclusiveMaximum": "maximum"
- },
-}
-
-
-class RefResolver(object):
- """
- Resolve JSON References.
-
- :argument str base_uri: URI of the referring document
- :argument referrer: the actual referring document
- :argument dict store: a mapping from URIs to documents to cache
-
- """
-
- def __init__(self, base_uri, referrer, store=()):
- self.base_uri = base_uri
- self.referrer = referrer
- self.store = dict(store, **_meta_schemas())
-
- @classmethod
- def from_schema(cls, schema, *args, **kwargs):
- """
- Construct a resolver from a JSON schema object.
-
- :argument schema schema: the referring schema
- :rtype: :class:`RefResolver`
-
- """
-
- return cls(schema.get("id", ""), schema, *args, **kwargs)
-
- def resolve(self, ref):
- """
- Resolve a JSON ``ref``.
-
- :argument str ref: reference to resolve
- :returns: the referrant document
-
- """
-
- base_uri = self.base_uri
- uri, fragment = urlparse.urldefrag(urlparse.urljoin(base_uri, ref))
-
- if uri in self.store:
- document = self.store[uri]
- elif not uri or uri == self.base_uri:
- document = self.referrer
- else:
- document = self.resolve_remote(uri)
-
- return self.resolve_fragment(document, fragment.lstrip("/"))
-
- def resolve_fragment(self, document, fragment):
- """
- Resolve a ``fragment`` within the referenced ``document``.
-
- :argument document: the referrant document
- :argument str fragment: a URI fragment to resolve within it
-
- """
-
- parts = unquote(fragment).split("/") if fragment else []
-
- for part in parts:
- part = part.replace("~1", "/").replace("~0", "~")
-
- if part not in document:
- raise RefResolutionError(
- "Unresolvable JSON pointer: %r" % fragment
- )
-
- document = document[part]
-
- return document
-
- def resolve_remote(self, uri):
- """
- Resolve a remote ``uri``.
-
- Does not check the store first.
-
- :argument str uri: the URI to resolve
- :returns: the retrieved document
-
- """
-
- return json.load(urlopen(uri))
-
-
-class ErrorTree(object):
- """
- ErrorTrees make it easier to check which validations failed.
-
- """
-
- def __init__(self, errors=()):
- self.errors = {}
- self._contents = collections.defaultdict(self.__class__)
-
- for error in errors:
- container = self
- for element in reversed(error.path):
- container = container[element]
- container.errors[error.validator] = error
-
- def __contains__(self, k):
- return k in self._contents
-
- def __getitem__(self, k):
- """
- Retrieve the child tree with key ``k``.
-
- """
-
- return self._contents[k]
-
- def __setitem__(self, k, v):
- self._contents[k] = v
-
- def __iter__(self):
- return iter(self._contents)
-
- def __len__(self):
- return self.total_errors
-
- def __repr__(self):
- return "<%s (%s total errors)>" % (self.__class__.__name__, len(self))
-
- @property
- def total_errors(self):
- """
- The total number of errors in the entire tree, including children.
-
- """
-
- child_errors = sum(len(tree) for _, tree in iteritems(self._contents))
- return len(self.errors) + child_errors
-
-
-def _meta_schemas():
- """
- Collect the urls and meta schemas from each known validator.
-
- """
-
- meta_schemas = (v.META_SCHEMA for v in validators.values())
- return dict((urlparse.urldefrag(m["id"])[0], m) for m in meta_schemas)
-
-
-def _find_additional_properties(instance, schema):
- """
- Return the set of additional properties for the given ``instance``.
-
- Weeds out properties that should have been validated by ``properties`` and
- / or ``patternProperties``.
-
- Assumes ``instance`` is dict-like already.
-
- """
-
- properties = schema.get("properties", {})
- patterns = "|".join(schema.get("patternProperties", {}))
- for property in instance:
- if property not in properties:
- if patterns and re.search(patterns, property):
- continue
- yield property
-
-
-def _extras_msg(extras):
- """
- Create an error message for extra items or properties.
-
- """
-
- if len(extras) == 1:
- verb = "was"
- else:
- verb = "were"
- return ", ".join(repr(extra) for extra in extras), verb
-
-
-def _types_msg(instance, types):
- """
- Create an error message for a failure to match the given types.
-
- If the ``instance`` is an object and contains a ``name`` property, it will
- be considered to be a description of that object and used as its type.
-
- Otherwise the message is simply the reprs of the given ``types``.
-
- """
-
- reprs = []
- for type in types:
- try:
- reprs.append(repr(type["name"]))
- except Exception:
- reprs.append(repr(type))
- return "%r is not of type %s" % (instance, ", ".join(reprs))
-
-
-def _flatten(suitable_for_isinstance):
- """
- isinstance() can accept a bunch of really annoying different types:
- * a single type
- * a tuple of types
- * an arbitrary nested tree of tuples
-
- Return a flattened tuple of the given argument.
-
- """
-
- types = set()
-
- if not isinstance(suitable_for_isinstance, tuple):
- suitable_for_isinstance = (suitable_for_isinstance,)
- for thing in suitable_for_isinstance:
- if isinstance(thing, tuple):
- types.update(_flatten(thing))
- else:
- types.add(thing)
- return tuple(types)
-
-
-def _list(thing):
- """
- Wrap ``thing`` in a list if it's a single str.
-
- Otherwise, return it unchanged.
-
- """
-
- if isinstance(thing, basestring):
- return [thing]
- return thing
-
-
-def _delist(thing):
- """
- Unwrap ``thing`` to a single element if its a single str in a list.
-
- Otherwise, return it unchanged.
-
- """
-
- if (
- isinstance(thing, list) and
- len(thing) == 1
- and isinstance(thing[0], basestring)
- ):
- return thing[0]
- return thing
-
-
-def _unbool(element, true=object(), false=object()):
- """
- A hack to make True and 1 and False and 0 unique for _uniq.
-
- """
-
- if element is True:
- return true
- elif element is False:
- return false
- return element
-
-
-def _uniq(container):
- """
- Check if all of a container's elements are unique.
-
- Successively tries first to rely that the elements are hashable, then
- falls back on them being sortable, and finally falls back on brute
- force.
-
- """
-
- try:
- return len(set(_unbool(i) for i in container)) == len(container)
- except TypeError:
- try:
- sort = sorted(_unbool(i) for i in container)
- sliced = itertools.islice(sort, 1, None)
- for i, j in zip(sort, sliced):
- if i == j:
- return False
- except (NotImplementedError, TypeError):
- seen = []
- for e in container:
- e = _unbool(e)
- if e in seen:
- return False
- seen.append(e)
- return True
-
-
-def validate(instance, schema, cls=Draft3Validator, *args, **kwargs):
- """
- Validate an ``instance`` under the given ``schema``.
-
- >>> validate([2, 3, 4], {"maxItems" : 2})
- Traceback (most recent call last):
- ...
- ValidationError: [2, 3, 4] is too long
-
- :func:`validate` will first verify that the provided schema is itself
- valid, since not doing so can lead to less obvious error messages and fail
- in less obvious or consistent ways. If you know you have a valid schema
- already or don't care, you might prefer using the ``validate`` method
- directly on a specific validator (e.g. :meth:`Draft3Validator.validate`).
-
- ``cls`` is a validator class that will be used to validate the instance.
- By default this is a draft 3 validator. Any other provided positional and
- keyword arguments will be provided to this class when constructing a
- validator.
-
- :raises:
- :exc:`ValidationError` if the instance is invalid
-
- :exc:`SchemaError` if the schema itself is invalid
-
- """
-
- cls.check_schema(schema)
- cls(schema, *args, **kwargs).validate(instance)
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 8126755b..00000000
--- a/src/leap/base/tests/test_checks.py
+++ /dev/null
@@ -1,177 +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()
-
- def test_ping_gateway_fail(self):
- 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")
-
- def test_ping_gateway(self):
- 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")
-
- def test_check_internet_connection_failures(self):
- 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..8b72153a
--- /dev/null
+++ b/src/leap/config/providerconfig.py
@@ -0,0 +1,177 @@
+# -*- 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_assert
+from leap.common.config.baseconfig import BaseConfig, LocalizedKey
+from leap.config.provider_spec import leap_provider_spec
+
+logger = logging.getLogger(__name__)
+
+
+class ProviderConfig(BaseConfig):
+ """
+ Provider configuration abstraction class
+ """
+ def __init__(self):
+ BaseConfig.__init__(self)
+
+ def _get_spec(self):
+ """
+ Returns the spec object for the specific configuration
+ """
+ return leap_provider_spec
+
+ 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")
+
+ def get_domain(self):
+ return 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.
+
+ :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:
+ leap_assert(os.path.exists(cert_path),
+ "You need to download the certificate first")
+ 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..4e86a5f7
--- /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
+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(AssertionError):
+ 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/constants.py b/src/leap/crypto/constants.py
new file mode 100644
index 00000000..c5eaef1f
--- /dev/null
+++ b/src/leap/crypto/constants.py
@@ -0,0 +1,18 @@
+# -*- 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
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..0e95ae64
--- /dev/null
+++ b/src/leap/crypto/srpauth.py
@@ -0,0 +1,496 @@
+# -*- 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.request_helpers import get_content
+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 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, password):
+ """
+ Sends the first request for authentication to retrieve the
+ salt and B parameter
+
+ Might raise SRPAuthenticationError
+
+ :param _: IGNORED, output from the previous callback (None)
+ :type _: IGNORED
+ :param username: username to login
+ :type username: str
+ :param password: password for the username
+ :type password: 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())
+ except requests.exceptions.ConnectionError as e:
+ logger.error("No connection made (salt): %r" %
+ (e,))
+ raise SRPAuthenticationError("Could not establish a "
+ "connection")
+ except Exception as e:
+ logger.error("Unknown error: %r" % (e,))
+ raise SRPAuthenticationError("Unknown error: %r" %
+ (e,))
+
+ content, mtime = 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 SRPAuthenticationError(self.tr("Unknown user"))
+
+ 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 SRPAuthenticationError(self.tr("The server did not send "
+ "the salt parameter"))
+ if B is None:
+ logger.error("No B parameter sent")
+ raise SRPAuthenticationError(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 throw SRPAuthenticationError
+
+ :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 as e:
+ logger.error("Bad data from server: %r" % (e,))
+ raise SRPAuthenticationError(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())
+ except requests.exceptions.ConnectionError as e:
+ logger.error("No connection made (HAMK): %r" % (e,))
+ raise SRPAuthenticationError(self.tr("Could not connect to "
+ "the server"))
+
+ try:
+ content, mtime = get_content(auth_result)
+ except JSONDecodeError:
+ raise SRPAuthenticationError("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 SRPAuthenticationError(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 SRPAuthenticationError(self.tr("Unknown error (%s)") %
+ (auth_result.status_code,))
+
+ json_content = json.loads(content)
+
+ 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 Exception("Something went wrong with the login")
+
+ events_signal(proto.CLIENT_UID, content=uid)
+
+ 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" %
+ (content,))
+ raise SRPAuthenticationError(self.tr("Problem getting data "
+ "from server"))
+
+ 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 throw SRPAuthenticationError
+
+ :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 SRPAuthenticationError(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 SRPAuthenticationError(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 SRPAuthenticationError(self.tr("Session cookie "
+ "verification "
+ "failed"))
+
+ events_signal(proto.CLIENT_SESSION_ID, content=session_id)
+
+ 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,
+ password=password)
+ d.addCallback(
+ partial(self._threader,
+ self._process_challenge),
+ username=username)
+ 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())
+ 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
+
+ self._username = None
+ self._password = None
+
+ 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
+ """
+
+ 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..07b3c917
--- /dev/null
+++ b/src/leap/crypto/srpregister.py
@@ -0,0 +1,163 @@
+# -*- 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.crypto.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,))
+ logger.debug("user_data => %r" % (user_data,))
+
+ ok = None
+ try:
+ req = self._session.post(uri,
+ data=user_data,
+ timeout=SIGNUP_TIMEOUT,
+ verify=self._provider_config.
+ get_ca_cert_path())
+
+ except requests.exceptions.SSLError as exc:
+ logger.error("SSLError: %s" % exc.message)
+ req = None
+ 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_srpregister.py b/src/leap/crypto/tests/test_srpregister.py
new file mode 100644
index 00000000..6d2b52e8
--- /dev/null
+++ b/src/leap/crypto/tests/test_srpregister.py
@@ -0,0 +1,202 @@
+# -*- 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
+ * leap/crypto/srpauth.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 SRP Register and Auth classes
+ """
+ __name__ = "SRPRegister and SRPAuth 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 af824c57..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):
-
- 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 = None
- 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.')
- # XXX raise better exception
- return self.fail("SSLError")
- except Exception as exc:
- return self.fail(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 1efceaa9..00000000
--- a/src/leap/gui/firstrun/login.py
+++ /dev/null
@@ -1,336 +0,0 @@
-"""
-LogIn Page, used inf First Run Wizard
-"""
-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
-
-
-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()
- eipconfigchecker = wizard.eipconfigchecker(domain=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:
- eipconfigchecker.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(
- eipconfigchecker.defaultprovider.config)
-
- ########################
- # 2) do authentication
- ########################
- credentials = username, password
- pCertChecker = wizard.providercertchecker(
- domain=domain)
-
- def validate_credentials():
- #################
- # FIXME #BUG #638
- verify = False
-
- try:
- pCertChecker.download_new_client_cert(
- credentials=credentials,
- verify=verify)
-
- 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(('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 8c383709..00000000
--- a/src/leap/gui/locale_rc.py
+++ /dev/null
@@ -1,813 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Resource object code
-#
-# Created: Fri Jan 25 18:19:04 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\x18\x32\
-\x3c\
-\xb8\x64\x18\xca\xef\x9c\x95\xcd\x21\x1c\xbf\x60\xa1\xbd\xdd\x42\
-\x00\x00\x01\x38\x00\x8f\x9b\xbe\x00\x00\x14\x83\x01\x23\x92\xe5\
-\x00\x00\x10\x3d\x01\x87\x64\x8e\x00\x00\x08\x7a\x01\xa8\xbe\x7e\
-\x00\x00\x0e\x02\x02\x2c\xac\xe9\x00\x00\x0b\x8a\x02\x3a\xce\xbf\
-\x00\x00\x16\x62\x02\x6e\x0f\xe5\x00\x00\x08\xdd\x02\x87\x60\x9e\
-\x00\x00\x06\x6e\x02\xaa\x52\x6e\x00\x00\x07\x6b\x02\xf2\xe0\x59\
-\x00\x00\x0a\x5e\x03\xec\x70\x0e\x00\x00\x10\xb8\x04\xd4\x45\xee\
-\x00\x00\x0d\x24\x05\xb7\x8f\x59\x00\x00\x0c\x27\x06\x3e\x6a\x9e\
-\x00\x00\x05\x9f\x06\x40\xa8\x7e\x00\x00\x0a\xea\x06\xee\xff\x6e\
-\x00\x00\x13\x74\x08\x13\xe8\xae\x00\x00\x0c\xa6\x08\x7a\x64\xee\
-\x00\x00\x11\xc5\x08\xe6\x98\x33\x00\x00\x05\x35\x08\xe6\x98\x33\
-\x00\x00\x0f\xc2\x09\x5c\x35\xe1\x00\x00\x0e\xaa\x09\x74\x75\x4e\
-\x00\x00\x0d\x94\x09\x98\x34\x0e\x00\x00\x12\x89\x09\xd8\x1f\x95\
-\x00\x00\x15\x79\x09\xeb\x5c\xb1\x00\x00\x15\x35\x09\xfc\x2c\x8e\
-\x00\x00\x04\xc7\x09\xfe\x05\x90\x00\x00\x0f\x16\x0a\x74\xb8\x1e\
-\x00\x00\x00\xd6\x0a\xfd\x99\xfe\x00\x00\x00\x51\x0b\xd2\x4b\x3f\
-\x00\x00\x07\x15\x0c\x44\x41\xbe\x00\x00\x00\x00\x0c\xc0\x94\x05\
-\x00\x00\x09\xd6\x0d\x0d\x9d\xc5\x00\x00\x06\x01\x0d\x15\x34\x70\
-\x00\x00\x09\x62\x0e\x36\x15\x54\x00\x00\x07\xed\x0e\x7e\xf5\xee\
-\x00\x00\x0f\x5a\x0e\x91\x50\x3e\x00\x00\x15\xee\x0e\xc0\xbb\x72\
-\x00\x00\x13\x1b\x0f\x27\x0d\x6e\x00\x00\x11\x54\x69\x00\x00\x16\
-\xd9\x03\x00\x00\x00\x22\x00\x50\x00\x72\x00\x69\x00\x6d\x00\x65\
-\x00\x72\x00\x61\x00\x20\x00\x43\x00\x6f\x00\x6e\x00\x65\x00\x78\
-\x00\x69\x00\x6f\x00\x6e\x00\x2e\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\x4c\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\x75\x00\x6e\x00\x20\x00\x75\x00\x73\x00\x75\x00\
-\x61\x00\x72\x00\x69\x00\x6f\x00\x20\x00\x71\x00\x75\x00\x65\x00\
-\x20\x00\x79\x00\x61\x00\x20\x00\x74\x00\x65\x00\x6e\x00\x67\x00\
-\x6f\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\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\x36\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\x63\x00\x75\x00\x65\x00\
-\x6e\x00\x74\x00\x61\x00\x20\x00\x6e\x00\x75\x00\x65\x00\x76\x00\
-\x61\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\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\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\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\x24\x00\x49\x00\x6e\x00\
-\x66\x00\x6f\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\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\
-\x42\x00\x45\x00\x73\x00\x74\x00\x6f\x00\x20\x00\x65\x00\x73\x00\
-\x20\x00\x6c\x00\x6f\x00\x20\x00\x71\x00\x75\x00\x65\x00\x20\x00\
-\x64\x00\x69\x00\x63\x00\x65\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\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\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\x22\
-\x00\x41\x00\x75\x00\x74\x00\x6f\x00\x63\x00\x6f\x00\x6e\x00\x66\
-\x00\x69\x00\x67\x00\x75\x00\x72\x00\x61\x00\x6e\x00\x64\x00\x6f\
-\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\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\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\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\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\x5e\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\x25\x00\x73\x00\x2e\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\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\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\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\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\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\x65\x00\x69\x00\x70\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\x52\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\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\x70\x00\x72\x00\x6f\x00\x76\
-\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\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\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\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\
-\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\x9c\x00\x50\x00\x6f\x00\x72\
-\x00\x20\x00\x66\x00\x61\x00\x76\x00\x6f\x00\x72\x00\x2c\x00\x20\
-\x00\x72\x00\x65\x00\x6c\x00\x6c\x00\x65\x00\x6e\x00\x61\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\x61\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\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\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\x3a\x00\
-\x63\x00\x6f\x00\x6d\x00\x70\x00\x72\x00\x6f\x00\x62\x00\x61\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\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\x34\x00\x63\x00\x6f\x00\x6d\x00\x70\x00\x72\x00\x6f\
-\x00\x62\x00\x61\x00\x6e\x00\x64\x00\x6f\x00\x20\x00\x63\x00\x6f\
-\x00\x6e\x00\x65\x00\x78\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\x68\
-\x00\x74\x00\x74\x00\x70\x00\x73\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\x3a\x00\x6f\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\x65\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\
-\x00\x72\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\
-"
-
-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..dd724ac7
--- /dev/null
+++ b/src/leap/gui/loggerwindow.py
@@ -0,0 +1,124 @@
+# -*- 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.QWidget):
+ """
+ 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.QWidget.__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.
+ """
+ level = log[LeapLogHandler.RECORD_KEY].levelname
+ message = log[LeapLogHandler.MESSAGE_KEY]
+
+ if self._logs_to_display[level]:
+ 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 = {
+ 'DEBUG': self.ui.btnDebug.isChecked(),
+ 'INFO': self.ui.btnInfo.isChecked(),
+ 'WARNING': self.ui.btnWarning.isChecked(),
+ 'ERROR': self.ui.btnError.isChecked(),
+ '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..8fc6ec24
--- /dev/null
+++ b/src/leap/gui/login.py
@@ -0,0 +1,214 @@
+# -*- 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
+import keyring
+
+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()
+ # Emitted when the user selects "Other..." in the provider
+ # combobox or click "Create Account"
+ show_wizard = QtCore.Signal()
+
+ 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)
+
+ 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 + ["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 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.btnLogin.setEnabled(enabled)
+ self.ui.chkRemember.setEnabled(enabled)
+ self.ui.cmbProviders.setEnabled(enabled)
+
+ 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..94343292
--- /dev/null
+++ b/src/leap/gui/mainwindow.py
@@ -0,0 +1,1336 @@
+# -*- 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.platform_init import IS_MAC
+from leap.services.eip.eipbootstrapper import EIPBootstrapper
+from leap.services.eip.eipconfig import EIPConfig
+from leap.services.eip.providerbootstrapper import ProviderBootstrapper
+# XXX: comment out soledad temporarily to avoid problem in Windows, issue #2932
+# from leap.services.soledad.soledadbootstrapper import SoledadBootstrapper
+from leap.services.mail.smtpbootstrapper import SMTPBootstrapper
+from leap.platform_init import IS_WIN
+from leap.platform_init.initializers import init_platform
+from leap.services.eip.vpnprocess import VPN
+
+from leap.services.eip.vpnlaunchers import (VPNLauncherException,
+ OpenVPNNotFoundException,
+ EIPNoPkexecAvailable,
+ EIPNoPolkitAuthAgentAvailable)
+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 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 = "leap_client"
+
+ # SMTP
+ PORT_KEY = "port"
+ IP_KEY = "ip_address"
+
+ OPENVPN_SERVICE = "openvpn"
+ MX_SERVICE = "mx"
+
+ # Signals
+ new_updates = QtCore.Signal(object)
+ raise_window = QtCore.Signal([])
+
+ # We use this flag to detect abnormal terminations
+ user_stopped_eip = False
+
+ def __init__(self, quit_callback,
+ standalone=False, 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
+
+ # XXX ------- this is blocking -----------
+ register(signal=proto.UPDATER_NEW_UPDATES,
+ callback=self._new_updates_available)
+ register(signal=proto.RAISE_WINDOW,
+ callback=self._on_raise_window_event)
+ # XXX ------- this is blocking -----------
+
+ 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)
+
+ self._login_widget.login.connect(self._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()
+ 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_sign_out.setEnabled(False)
+ self.ui.action_sign_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 ON"), self)
+ self._action_eip_startstop.triggered.connect(
+ self._stop_eip)
+
+ 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)
+
+ 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._login_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.setVisible(False)
+ self._wizard.exec_()
+ # We need this to process any wizard related event
+ QtCore.QCoreApplication.processEvents()
+ self._wizard = None
+ 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())
+
+ 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 LEAPClient app is ready to update, please"
+ " restart the application.")
+
+ # We assume that if there is nothing in the contents, then
+ # the LEAPClient 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 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_widget.set_remember(has_keyring())
+ 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
+ systrayMenu = QtGui.QMenu(self)
+ systrayMenu.addAction(self._action_visible)
+ systrayMenu.addAction(self.ui.action_sign_out)
+ systrayMenu.addSeparator()
+ systrayMenu.addAction(self.ui.action_quit)
+ systrayMenu.addSeparator()
+ systrayMenu.addAction(self._action_eip_provider)
+ systrayMenu.addAction(self._action_eip_status)
+ systrayMenu.addAction(self._action_eip_startstop)
+ self._systray = QtGui.QSystemTrayIcon(self)
+ self._systray.setContextMenu(systrayMenu)
+ self._systray.setIcon(self._status_panel.ERROR_ICON)
+ 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
+ """
+ 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))
+
+ 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 _toggle_visible(self):
+ """
+ SLOT
+ TRIGGER: self._action_visible.triggered
+
+ Toggles the window visibility
+ """
+ self.setVisible(not self.isVisible())
+
+ 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 LEAP dialog
+ """
+ QtGui.QMessageBox.about(
+ self, self.tr("About LEAP - %s") % (VERSION,),
+ self.tr("version: <b>%s</b><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. "
+ "<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()
+
+ self._provider_bootstrapper.run_provider_select_checks(
+ provider,
+ download_if_needed=True)
+
+ 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 self._provider_config.loaded() or \
+ self._provider_config.load(os.path.join("leap",
+ "providers",
+ provider,
+ "provider.json")):
+ self._provider_bootstrapper.run_provider_setup_checks(
+ self._provider_config,
+ download_if_needed=True)
+ else:
+ self._login_widget.set_status(
+ self.tr("Could not load provider configuration."))
+ self._login_widget.set_enabled(True)
+ else:
+ self._login_widget.set_status(
+ data[self._provider_bootstrapper.ERROR_KEY])
+ self._login_widget.set_enabled(True)
+
+ def _login(self):
+ """
+ SLOT
+ TRIGGERS:
+ self.ui.btnLogin.clicked
+ self.ui.lnPassword.returnPressed
+
+ 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 _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(
+ 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
+ """
+ self._login_widget.set_status(message, error=not ok)
+ if ok:
+ self._logged_user = self._login_widget.get_user()
+ self.ui.action_sign_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)
+
+ # XXX disabling soledad for now
+ #self._soledad_bootstrapper.run_soledad_setup_checks(
+ #self._provider_config,
+ #self._login_widget.get_user(),
+ #self._login_widget.get_password(),
+ #download_if_needed=True)
+
+ 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
+ logger.error("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])
+ else:
+ logger.debug("Done bootstrapping Soledad")
+
+ self._soledad = self._soledad_bootstrapper.soledad
+ self._keymanager = self._soledad_bootstrapper.keymanager
+
+ 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"))
+
+ 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])
+ else:
+ 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
+ setup_smtp_relay(port=1234,
+ keymanager=self._keymanager,
+ smtp_host=host,
+ smtp_port=port,
+ smtp_username=".",
+ smtp_password=".",
+ encrypted_only=False)
+
+ 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.ui.btnEipStartStop.clicked
+ 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()
+
+ 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 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 VPNLauncherException as e:
+ self._status_panel.set_gloal_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_startstop_enabled(True)
+
+ def _stop_eip(self, abnormal=False):
+ """
+ Stops vpn process and makes gui adjustments to reflect
+ the change of state.
+
+ :param abnormal: whether this was an abnormal termination.
+ :type abnormal: bool
+ ""
+ """
+ self.user_stopped_eip = True
+ self._vpn.terminate()
+
+ self._status_panel.set_eip_status(self.tr("OFF"))
+ self._status_panel.set_eip_status_icon("error")
+ self._status_panel.eip_stopped()
+ self._action_eip_startstop.setText(self.tr("Turn ON"))
+ self._action_eip_startstop.disconnect(self)
+ self._action_eip_startstop.triggered.connect(
+ self._start_eip)
+ 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()))
+ if abnormal:
+ self._status_panel.set_startstop_enabled(True)
+
+ 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.ui.btnEipStartStop.setEnabled(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!")
+
+ provider_config = self._get_best_provider_config()
+
+ domain = provider_config.get_domain()
+
+ if data[self._eip_bootstrapper.PASSED_KEY] and \
+ (self._eip_config.loaded() or
+ self._eip_config.load(os.path.join("leap",
+ "providers",
+ domain,
+ "eip-service.json"))):
+ self._start_eip()
+ else:
+ if data[self._eip_bootstrapper.PASSED_KEY]:
+ self._status_panel.set_eip_status(
+ self.tr("Could not load Encrypted Internet "
+ "Configuration."),
+ error=True)
+ else:
+ self._status_panel.set_eip_status(
+ data[self._eip_bootstrapper.ERROR_KEY],
+ error=True)
+ self._already_started_eip = False
+
+ def _logout(self):
+ """
+ SLOT
+ TRIGGER: self.ui.action_sign_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_sign_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(
+ 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(
+ 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:
+ # 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
+ """
+ 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()
+
+ def _cleanup_pidfiles(self):
+ """
+ Removes lockfiles on a clean shutdown.
+
+ Triggered after aboutToQuit signal.
+ """
+ if IS_WIN:
+ lockfile = WindowsLock()
+ lockfile.release_lock()
+
+ 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...')
+
+ logger.debug('Cleaning pidfiles')
+ self._cleanup_pidfiles()
+
+ logger.debug('Terminating vpn')
+ self._vpn.terminate(shutdown=True)
+
+ def quit(self):
+ """
+ Cleanup and tidely close the main window before quitting.
+ """
+ self._cleanup_and_quit()
+
+ self._really_quit = True
+ if self._wizard:
+ self._wizard.close()
+
+ if self._logger_window:
+ self._logger_window.close()
+
+ if self._login_defer:
+ self._login_defer.cancel()
+
+ 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..3e5a5093
--- /dev/null
+++ b/src/leap/gui/statuspanel.py
@@ -0,0 +1,279 @@
+# -*- 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 functools import partial
+from PySide import QtCore, QtGui
+
+from ui_statuspanel import Ui_StatusPanel
+from leap.services.eip.vpnprocess import VPNManager
+from leap.platform_init import IS_WIN, IS_LINUX
+from leap.common.check import leap_assert_type
+
+logger = logging.getLogger(__name__)
+
+
+class StatusPanelWidget(QtGui.QWidget):
+ """
+ Status widget that displays the current state of the LEAP services
+ """
+
+ start_eip = QtCore.Signal()
+ stop_eip = QtCore.Signal()
+
+ 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()
+
+ 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_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 based on value
+
+ :param value: True for enabled, False otherwise
+ :type value: bool
+ """
+ leap_assert_type(value, bool)
+ self.ui.btnEipStartStop.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.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
+ """
+ upload = float(data[VPNManager.TUNTAP_WRITE_KEY] or "0")
+ upload = upload / 1000.0
+ upload_str = "%12.2f Kb" % (upload,)
+ self.ui.lblUpload.setText(upload_str)
+ download = float(data[VPNManager.TUNTAP_READ_KEY] or "0")
+ download = download / 1000.0
+ download_str = "%12.2f Kb" % (download,)
+ self.ui.lblDownload.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..28325cdf
--- /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/leap-color-big.png</normaloff>:/images/leap-color-big.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..88c9ef44
--- /dev/null
+++ b/src/leap/gui/ui/login.ui
@@ -0,0 +1,129 @@
+<?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>219</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>
+ </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..58827fe0
--- /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>LEAP</string>
+ </property>
+ <property name="windowIcon">
+ <iconset resource="../../../../data/resources/mainwindow.qrc">
+ <normaloff>:/images/leap-color-big.png</normaloff>:/images/leap-color-big.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/leap-color-big.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>25</height>
+ </rect>
+ </property>
+ <widget class="QMenu" name="menuSession">
+ <property name="title">
+ <string>&amp;Session</string>
+ </property>
+ <addaction name="action_sign_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_sign_out">
+ <property name="text">
+ <string>&amp;Sign 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;LEAP</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..1a2c77ad
--- /dev/null
+++ b/src/leap/gui/ui/statuspanel.ui
@@ -0,0 +1,244 @@
+<?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>542</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="QLabel" name="lblUpload">
+ <property name="text">
+ <string>0.0 Kb</string>
+ </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>
+ <widget class="QLabel" name="lblDownload">
+ <property name="text">
+ <string>0.0 Kb</string>
+ </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>
+ <zorder>lblProvider</zorder>
+ <zorder>status_rows</zorder>
+ <zorder>globalStatusBox</zorder>
+ </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..4b9cab1c
--- /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>LEAP First run</string>
+ </property>
+ <property name="windowIcon">
+ <iconset resource="../../../../data/resources/mainwindow.qrc">
+ <normaloff>:/images/leap-color-big.png</normaloff>:/images/leap-color-big.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 LEAP Client 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;User:&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 the LEAP Client.</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..b29250c8
--- /dev/null
+++ b/src/leap/gui/wizard.py
@@ -0,0 +1,620 @@
+# -*- 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()
+
+ 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)
+ self._set_register_status(error_msg, error=True)
+ except:
+ logger.error("Unknown error: %r" % (req.content,))
+ 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_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_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/keymanager/__init__.py b/src/leap/keymanager/__init__.py
new file mode 100644
index 00000000..38e23d0e
--- /dev/null
+++ b/src/leap/keymanager/__init__.py
@@ -0,0 +1,341 @@
+# -*- 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/>.
+
+
+"""
+Key Manager is a Nicknym agent for LEAP client.
+"""
+
+import requests
+
+try:
+ import simplejson as json
+except ImportError:
+ import json # noqa
+
+from leap.common.check import leap_assert
+from leap.keymanager.errors import (
+ KeyNotFound,
+ NoPasswordGiven,
+)
+from leap.keymanager.keys import (
+ build_key_from_dict,
+ KEYMANAGER_KEY_TAG,
+ TAGS_PRIVATE_INDEX,
+)
+from leap.keymanager.openpgp import (
+ OpenPGPKey,
+ OpenPGPScheme,
+)
+
+
+#
+# The Key Manager
+#
+
+class KeyManager(object):
+
+ #
+ # server's key storage constants
+ #
+
+ OPENPGP_KEY = 'openpgp'
+ PUBKEY_KEY = "user[public_key]"
+
+ def __init__(self, address, nickserver_uri, soledad, session_id=None,
+ ca_cert_path=None, api_uri=None, api_version=None, uid=None):
+ """
+ Initialize a Key Manager for user's C{address} with provider's
+ nickserver reachable in C{url}.
+
+ :param address: The address of the user of this Key Manager.
+ :type address: str
+ :param url: The URL of the nickserver.
+ :type url: str
+ :param soledad: A Soledad instance for local storage of keys.
+ :type soledad: leap.soledad.Soledad
+ :param session_id: The session ID for interacting with the webapp API.
+ :type session_id: str
+ :param ca_cert_path: The path to the CA certificate.
+ :type ca_cert_path: str
+ :param api_uri: The URI of the webapp API.
+ :type api_uri: str
+ :param api_version: The version of the webapp API.
+ :type api_version: str
+ :param uid: The users' UID.
+ :type uid: str
+ """
+ self._address = address
+ self._nickserver_uri = nickserver_uri
+ self._soledad = soledad
+ self._session_id = session_id
+ self.ca_cert_path = ca_cert_path
+ self.api_uri = api_uri
+ self.api_version = api_version
+ self.uid = uid
+ # a dict to map key types to their handlers
+ self._wrapper_map = {
+ OpenPGPKey: OpenPGPScheme(soledad),
+ # other types of key will be added to this mapper.
+ }
+ # the following are used to perform https requests
+ self._fetcher = requests
+ self._session = self._fetcher.session()
+
+ #
+ # utilities
+ #
+
+ def _key_class_from_type(self, ktype):
+ """
+ Return key class from string representation of key type.
+ """
+ return filter(
+ lambda klass: str(klass) == ktype,
+ self._wrapper_map).pop()
+
+ def _get(self, uri, data=None):
+ """
+ Send a GET request to C{uri} containing C{data}.
+
+ :param uri: The URI of the request.
+ :type uri: str
+ :param data: The body of the request.
+ :type data: dict, str or file
+
+ :return: The response to the request.
+ :rtype: requests.Response
+ """
+ leap_assert(
+ self._ca_cert_path is not None,
+ 'We need the CA certificate path!')
+ res = self._fetcher.get(uri, data=data, verify=self._ca_cert_path)
+ # assert that the response is valid
+ res.raise_for_status()
+ leap_assert(
+ res.headers['content-type'].startswith('application/json'),
+ 'Content-type is not JSON.')
+ return res
+
+ def _put(self, uri, data=None):
+ """
+ Send a PUT request to C{uri} containing C{data}.
+
+ The request will be sent using the configured CA certificate path to
+ verify the server certificate and the configured session id for
+ authentication.
+
+ :param uri: The URI of the request.
+ :type uri: str
+ :param data: The body of the request.
+ :type data: dict, str or file
+
+ :return: The response to the request.
+ :rtype: requests.Response
+ """
+ leap_assert(
+ self._ca_cert_path is not None,
+ 'We need the CA certificate path!')
+ leap_assert(
+ self._session_id is not None,
+ 'We need a session_id to interact with webapp!')
+ res = self._fetcher.put(
+ uri, data=data, verify=self._ca_cert_path,
+ cookies={'_session_id': self._session_id})
+ # assert that the response is valid
+ res.raise_for_status()
+ return res
+
+ def _fetch_keys_from_server(self, address):
+ """
+ Fetch keys bound to C{address} from nickserver and insert them in
+ local database.
+
+ :param address: The address bound to the keys.
+ :type address: str
+
+ @raise KeyNotFound: If the key was not found on nickserver.
+ """
+ # request keys from the nickserver
+ server_keys = self._get(
+ self._nickserver_uri, {'address': address}).json()
+ # insert keys in local database
+ if self.OPENPGP_KEY in server_keys:
+ self._wrapper_map[OpenPGPKey].put_ascii_key(
+ server_keys['openpgp'])
+
+ #
+ # key management
+ #
+
+ def send_key(self, ktype):
+ """
+ Send user's key of type C{ktype} to provider.
+
+ Public key bound to user's is sent to provider, which will sign it and
+ replace any prior keys for the same address in its database.
+
+ If C{send_private} is True, then the private key is encrypted with
+ C{password} and sent to server in the same request, together with a
+ hash string of user's address and password. The encrypted private key
+ will be saved in the server in a way it is publicly retrievable
+ through the hash string.
+
+ :param ktype: The type of the key.
+ :type ktype: KeyType
+
+ @raise KeyNotFound: If the key was not found in local database.
+ """
+ leap_assert(
+ ktype is OpenPGPKey,
+ 'For now we only know how to send OpenPGP public keys.')
+ # prepare the public key bound to address
+ pubkey = self.get_key(
+ self._address, ktype, private=False, fetch_remote=False)
+ data = {
+ self.PUBKEY_KEY: pubkey.key_data
+ }
+ uri = "%s/%s/users/%s.json" % (
+ self._api_uri,
+ self._api_version,
+ self._uid)
+ self._put(uri, data)
+
+ def get_key(self, address, ktype, private=False, fetch_remote=True):
+ """
+ Return a key of type C{ktype} bound to C{address}.
+
+ First, search for the key in local storage. If it is not available,
+ then try to fetch from nickserver.
+
+ :param address: The address bound to the key.
+ :type address: str
+ :param ktype: The type of the key.
+ :type ktype: KeyType
+ :param private: Look for a private key instead of a public one?
+ :type private: bool
+
+ :return: A key of type C{ktype} bound to C{address}.
+ :rtype: EncryptionKey
+ @raise KeyNotFound: If the key was not found both locally and in
+ keyserver.
+ """
+ leap_assert(
+ ktype in self._wrapper_map,
+ 'Unkown key type: %s.' % str(ktype))
+ try:
+ # return key if it exists in local database
+ return self._wrapper_map[ktype].get_key(address, private=private)
+ except KeyNotFound:
+ # we will only try to fetch a key from nickserver if fetch_remote
+ # is True and the key is not private.
+ if fetch_remote is False or private is True:
+ raise
+ self._fetch_keys_from_server(address)
+ return self._wrapper_map[ktype].get_key(address, private=False)
+
+ def get_all_keys_in_local_db(self, private=False):
+ """
+ Return all keys stored in local database.
+
+ :return: A list with all keys in local db.
+ :rtype: list
+ """
+ return map(
+ lambda doc: build_key_from_dict(
+ self._key_class_from_type(doc.content['type']),
+ doc.content['address'],
+ doc.content),
+ self._soledad.get_from_index(
+ TAGS_PRIVATE_INDEX,
+ KEYMANAGER_KEY_TAG,
+ '1' if private else '0'))
+
+ def refresh_keys(self):
+ """
+ Fetch keys from nickserver and update them locally.
+ """
+ addresses = set(map(
+ lambda doc: doc.address,
+ self.get_all_keys_in_local_db(private=False)))
+ for address in addresses:
+ # do not attempt to refresh our own key
+ if address == self._address:
+ continue
+ self._fetch_keys_from_server(address)
+
+ def gen_key(self, ktype):
+ """
+ Generate a key of type C{ktype} bound to the user's address.
+
+ :param ktype: The type of the key.
+ :type ktype: KeyType
+
+ :return: The generated key.
+ :rtype: EncryptionKey
+ """
+ return self._wrapper_map[ktype].gen_key(self._address)
+
+ #
+ # Setters/getters
+ #
+
+ def _get_session_id(self):
+ return self._session_id
+
+ def _set_session_id(self, session_id):
+ self._session_id = session_id
+
+ session_id = property(
+ _get_session_id, _set_session_id, doc='The session id.')
+
+ def _get_ca_cert_path(self):
+ return self._ca_cert_path
+
+ def _set_ca_cert_path(self, ca_cert_path):
+ self._ca_cert_path = ca_cert_path
+
+ ca_cert_path = property(
+ _get_ca_cert_path, _set_ca_cert_path,
+ doc='The path to the CA certificate.')
+
+ def _get_api_uri(self):
+ return self._api_uri
+
+ def _set_api_uri(self, api_uri):
+ self._api_uri = api_uri
+
+ api_uri = property(
+ _get_api_uri, _set_api_uri, doc='The webapp API URI.')
+
+ def _get_api_version(self):
+ return self._api_version
+
+ def _set_api_version(self, api_version):
+ self._api_version = api_version
+
+ api_version = property(
+ _get_api_version, _set_api_version, doc='The webapp API version.')
+
+ def _get_uid(self):
+ return self._uid
+
+ def _set_uid(self, uid):
+ self._uid = uid
+
+ uid = property(
+ _get_uid, _set_uid, doc='The uid of the user.')
diff --git a/src/leap/keymanager/errors.py b/src/leap/keymanager/errors.py
new file mode 100644
index 00000000..89949d29
--- /dev/null
+++ b/src/leap/keymanager/errors.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+# errors.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/>.
+
+
+"""
+Errors and exceptions used by the Key Manager.
+"""
+
+
+class KeyNotFound(Exception):
+ """
+ Raised when key was no found on keyserver.
+ """
+ pass
+
+
+class KeyAlreadyExists(Exception):
+ """
+ Raised when attempted to create a key that already exists.
+ """
+ pass
+
+
+class KeyAttributesDiffer(Exception):
+ """
+ Raised when trying to delete a key but the stored key differs from the key
+ passed to the delete_key() method.
+ """
+ pass
+
+
+class NoPasswordGiven(Exception):
+ """
+ Raised when trying to perform some action that needs a password without
+ providing one.
+ """
+ pass
+
+
+class InvalidSignature(Exception):
+ """
+ Raised when signature could not be verified.
+ """
+ pass
+
+
+class EncryptionFailed(Exception):
+ """
+ Raised upon failures of encryption.
+ """
+ pass
+
+
+class DecryptionFailed(Exception):
+ """
+ Raised upon failures of decryption.
+ """
+ pass
+
+
+class EncryptionDecryptionFailed(Exception):
+ """
+ Raised upon failures of encryption/decryption.
+ """
+ pass
+
+
+class SignFailed(Exception):
+ """
+ Raised when failed to sign.
+ """
+ pass
diff --git a/src/leap/keymanager/gpg.py b/src/leap/keymanager/gpg.py
new file mode 100644
index 00000000..15c1d9f6
--- /dev/null
+++ b/src/leap/keymanager/gpg.py
@@ -0,0 +1,397 @@
+# -*- coding: utf-8 -*-
+# gpgwrapper.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 GPG wrapper used to handle OpenPGP keys.
+
+This is a temporary class that will be superseded by the a revised version of
+python-gnupg.
+"""
+
+
+import os
+import gnupg
+import re
+from gnupg import (
+ logger,
+ _is_sequence,
+ _make_binary_stream,
+)
+
+
+class ListPackets():
+ """
+ Handle status messages for --list-packets.
+ """
+
+ def __init__(self, gpg):
+ """
+ Initialize the packet listing handling class.
+
+ :param gpg: GPG object instance.
+ :type gpg: gnupg.GPG
+ """
+ self.gpg = gpg
+ self.nodata = None
+ self.key = None
+ self.need_passphrase = None
+ self.need_passphrase_sym = None
+ self.userid_hint = None
+
+ def handle_status(self, key, value):
+ """
+ Handle one line of the --list-packets status message.
+
+ :param key: The status message key.
+ :type key: str
+ :param value: The status message value.
+ :type value: str
+ """
+ # TODO: write tests for handle_status
+ if key == 'NODATA':
+ self.nodata = True
+ if key == 'ENC_TO':
+ # This will only capture keys in our keyring. In the future we
+ # may want to include multiple unknown keys in this list.
+ self.key, _, _ = value.split()
+ if key == 'NEED_PASSPHRASE':
+ self.need_passphrase = True
+ if key == 'NEED_PASSPHRASE_SYM':
+ self.need_passphrase_sym = True
+ if key == 'USERID_HINT':
+ self.userid_hint = value.strip().split()
+
+
+class GPGWrapper(gnupg.GPG):
+ """
+ This is a temporary class for handling GPG requests, and should be
+ replaced by a more general class used throughout the project.
+ """
+
+ GNUPG_HOME = os.environ['HOME'] + "/.config/leap/gnupg"
+ GNUPG_BINARY = "/usr/bin/gpg" # this has to be changed based on OS
+
+ def __init__(self, gpgbinary=GNUPG_BINARY, gnupghome=GNUPG_HOME,
+ verbose=False, use_agent=False, keyring=None, options=None):
+ """
+ Initialize a GnuPG process wrapper.
+
+ :param gpgbinary: Name for GnuPG binary executable.
+ :type gpgbinary: C{str}
+ :param gpghome: Full pathname to directory containing the public and
+ private keyrings.
+ :type gpghome: C{str}
+ :param keyring: Name of alternative keyring file to use. If specified,
+ the default keyring is not used.
+ :param verbose: Should some verbose info be output?
+ :type verbose: bool
+ :param use_agent: Should pass `--use-agent` to GPG binary?
+ :type use_agent: bool
+ :param keyring: Path for the keyring to use.
+ :type keyring: str
+ @options: A list of additional options to pass to the GPG binary.
+ :type options: list
+
+ @raise: RuntimeError with explanation message if there is a problem
+ invoking gpg.
+ """
+ gnupg.GPG.__init__(self, gnupghome=gnupghome, gpgbinary=gpgbinary,
+ verbose=verbose, use_agent=use_agent,
+ keyring=keyring, options=options)
+ self.result_map['list-packets'] = ListPackets
+
+ def find_key_by_email(self, email, secret=False):
+ """
+ Find user's key based on their email.
+
+ :param email: Email address of key being searched for.
+ :type email: str
+ :param secret: Should we search for a secret key?
+ :type secret: bool
+
+ :return: The fingerprint of the found key.
+ :rtype: str
+ """
+ for key in self.list_keys(secret=secret):
+ for uid in key['uids']:
+ if re.search(email, uid):
+ return key
+ raise LookupError("GnuPG public key for email %s not found!" % email)
+
+ def find_key_by_subkey(self, subkey, secret=False):
+ """
+ Find user's key based on a subkey fingerprint.
+
+ :param email: Subkey fingerprint of the key being searched for.
+ :type email: str
+ :param secret: Should we search for a secret key?
+ :type secret: bool
+
+ :return: The fingerprint of the found key.
+ :rtype: str
+ """
+ for key in self.list_keys(secret=secret):
+ for sub in key['subkeys']:
+ if sub[0] == subkey:
+ return key
+ raise LookupError(
+ "GnuPG public key for subkey %s not found!" % subkey)
+
+ def find_key_by_keyid(self, keyid, secret=False):
+ """
+ Find user's key based on the key ID.
+
+ :param email: The key ID of the key being searched for.
+ :type email: str
+ :param secret: Should we search for a secret key?
+ :type secret: bool
+
+ :return: The fingerprint of the found key.
+ :rtype: str
+ """
+ for key in self.list_keys(secret=secret):
+ if keyid == key['keyid']:
+ return key
+ raise LookupError(
+ "GnuPG public key for keyid %s not found!" % keyid)
+
+ def find_key_by_fingerprint(self, fingerprint, secret=False):
+ """
+ Find user's key based on the key fingerprint.
+
+ :param email: The fingerprint of the key being searched for.
+ :type email: str
+ :param secret: Should we search for a secret key?
+ :type secret: bool
+
+ :return: The fingerprint of the found key.
+ :rtype: str
+ """
+ for key in self.list_keys(secret=secret):
+ if fingerprint == key['fingerprint']:
+ return key
+ raise LookupError(
+ "GnuPG public key for fingerprint %s not found!" % fingerprint)
+
+ def encrypt(self, data, recipient, sign=None, always_trust=True,
+ passphrase=None, symmetric=False):
+ """
+ Encrypt data using GPG.
+
+ :param data: The data to be encrypted.
+ :type data: str
+ :param recipient: The address of the public key to be used.
+ :type recipient: str
+ :param sign: Should the encrypted content be signed?
+ :type sign: bool
+ :param always_trust: Skip key validation and assume that used keys
+ are always fully trusted?
+ :type always_trust: bool
+ :param passphrase: The passphrase to be used if symmetric encryption
+ is desired.
+ :type passphrase: str
+ :param symmetric: Should we encrypt to a password?
+ :type symmetric: bool
+
+ :return: An object with encrypted result in the `data` field.
+ :rtype: gnupg.Crypt
+ """
+ # TODO: devise a way so we don't need to "always trust".
+ return gnupg.GPG.encrypt(self, data, recipient, sign=sign,
+ always_trust=always_trust,
+ passphrase=passphrase,
+ symmetric=symmetric,
+ cipher_algo='AES256')
+
+ def decrypt(self, data, always_trust=True, passphrase=None):
+ """
+ Decrypt data using GPG.
+
+ :param data: The data to be decrypted.
+ :type data: str
+ :param always_trust: Skip key validation and assume that used keys
+ are always fully trusted?
+ :type always_trust: bool
+ :param passphrase: The passphrase to be used if symmetric encryption
+ is desired.
+ :type passphrase: str
+
+ :return: An object with decrypted result in the `data` field.
+ :rtype: gnupg.Crypt
+ """
+ # TODO: devise a way so we don't need to "always trust".
+ return gnupg.GPG.decrypt(self, data, always_trust=always_trust,
+ passphrase=passphrase)
+
+ def send_keys(self, keyserver, *keyids):
+ """
+ Send keys to a keyserver
+
+ :param keyserver: The keyserver to send the keys to.
+ :type keyserver: str
+ :param keyids: The key ids to send.
+ :type keyids: list
+
+ :return: A list of keys sent to server.
+ :rtype: gnupg.ListKeys
+ """
+ # TODO: write tests for this.
+ # TODO: write a SendKeys class to handle status for this.
+ result = self.result_map['list'](self)
+ gnupg.logger.debug('send_keys: %r', keyids)
+ data = gnupg._make_binary_stream("", self.encoding)
+ args = ['--keyserver', keyserver, '--send-keys']
+ args.extend(keyids)
+ self._handle_io(args, data, result, binary=True)
+ gnupg.logger.debug('send_keys result: %r', result.__dict__)
+ data.close()
+ return result
+
+ def encrypt_file(self, file, recipients, sign=None,
+ always_trust=False, passphrase=None,
+ armor=True, output=None, symmetric=False,
+ cipher_algo=None):
+ """
+ Encrypt the message read from the file-like object 'file'.
+
+ :param file: The file to be encrypted.
+ :type data: file
+ :param recipient: The address of the public key to be used.
+ :type recipient: str
+ :param sign: Should the encrypted content be signed?
+ :type sign: bool
+ :param always_trust: Skip key validation and assume that used keys
+ are always fully trusted?
+ :type always_trust: bool
+ :param passphrase: The passphrase to be used if symmetric encryption
+ is desired.
+ :type passphrase: str
+ :param armor: Create ASCII armored output?
+ :type armor: bool
+ :param output: Path of file to write results in.
+ :type output: str
+ :param symmetric: Should we encrypt to a password?
+ :type symmetric: bool
+ :param cipher_algo: Algorithm to use.
+ :type cipher_algo: str
+
+ :return: An object with encrypted result in the `data` field.
+ :rtype: gnupg.Crypt
+ """
+ args = ['--encrypt']
+ if symmetric:
+ args = ['--symmetric']
+ if cipher_algo:
+ args.append('--cipher-algo %s' % cipher_algo)
+ else:
+ args = ['--encrypt']
+ if not _is_sequence(recipients):
+ recipients = (recipients,)
+ for recipient in recipients:
+ args.append('--recipient "%s"' % recipient)
+ if armor: # create ascii-armored output - set to False for binary
+ args.append('--armor')
+ if output: # write the output to a file with the specified name
+ if os.path.exists(output):
+ os.remove(output) # to avoid overwrite confirmation message
+ args.append('--output "%s"' % output)
+ if sign:
+ args.append('--sign --default-key "%s"' % sign)
+ if always_trust:
+ args.append("--always-trust")
+ result = self.result_map['crypt'](self)
+ self._handle_io(args, file, result, passphrase=passphrase, binary=True)
+ logger.debug('encrypt result: %r', result.data)
+ return result
+
+ def list_packets(self, data):
+ """
+ List the sequence of packets.
+
+ :param data: The data to extract packets from.
+ :type data: str
+
+ :return: An object with packet info.
+ :rtype ListPackets
+ """
+ args = ["--list-packets"]
+ result = self.result_map['list-packets'](self)
+ self._handle_io(
+ args,
+ _make_binary_stream(data, self.encoding),
+ result,
+ )
+ return result
+
+ def encrypted_to(self, data):
+ """
+ Return the key to which data is encrypted to.
+
+ :param data: The data to be examined.
+ :type data: str
+
+ :return: The fingerprint of the key to which data is encrypted to.
+ :rtype: str
+ """
+ # TODO: make this support multiple keys.
+ result = self.list_packets(data)
+ if not result.key:
+ raise LookupError(
+ "Content is not encrypted to a GnuPG key!")
+ try:
+ return self.find_key_by_keyid(result.key)
+ except:
+ return self.find_key_by_subkey(result.key)
+
+ def is_encrypted_sym(self, data):
+ """
+ Say whether some chunk of data is encrypted to a symmetric key.
+
+ :param data: The data to be examined.
+ :type data: str
+
+ :return: Whether data is encrypted to a symmetric key.
+ :rtype: bool
+ """
+ result = self.list_packets(data)
+ return bool(result.need_passphrase_sym)
+
+ def is_encrypted_asym(self, data):
+ """
+ Say whether some chunk of data is encrypted to a private key.
+
+ :param data: The data to be examined.
+ :type data: str
+
+ :return: Whether data is encrypted to a private key.
+ :rtype: bool
+ """
+ result = self.list_packets(data)
+ return bool(result.key)
+
+ def is_encrypted(self, data):
+ """
+ Say whether some chunk of data is encrypted to a key.
+
+ :param data: The data to be examined.
+ :type data: str
+
+ :return: Whether data is encrypted to a key.
+ :rtype: bool
+ """
+ return self.is_encrypted_asym(data) or self.is_encrypted_sym(data)
diff --git a/src/leap/keymanager/keys.py b/src/leap/keymanager/keys.py
new file mode 100644
index 00000000..44bd587b
--- /dev/null
+++ b/src/leap/keymanager/keys.py
@@ -0,0 +1,285 @@
+# -*- coding: utf-8 -*-
+# keys.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/>.
+
+
+"""
+Abstact key type and encryption scheme representations.
+"""
+
+
+try:
+ import simplejson as json
+except ImportError:
+ import json # noqa
+import re
+
+
+from abc import ABCMeta, abstractmethod
+from leap.common.check import leap_assert
+
+
+#
+# Dictionary keys used for storing cryptographic keys.
+#
+
+KEY_ADDRESS_KEY = 'address'
+KEY_TYPE_KEY = 'type'
+KEY_ID_KEY = 'key_id'
+KEY_FINGERPRINT_KEY = 'fingerprint'
+KEY_DATA_KEY = 'key_data'
+KEY_PRIVATE_KEY = 'private'
+KEY_LENGTH_KEY = 'length'
+KEY_EXPIRY_DATE_KEY = 'expiry_date'
+KEY_FIRST_SEEN_AT_KEY = 'first_seen_at'
+KEY_LAST_AUDITED_AT_KEY = 'last_audited_at'
+KEY_VALIDATION_KEY = 'validation'
+KEY_TAGS_KEY = 'tags'
+
+
+#
+# Key storage constants
+#
+
+KEYMANAGER_KEY_TAG = 'keymanager-key'
+
+
+#
+# key indexing constants.
+#
+
+TAGS_PRIVATE_INDEX = 'by-tags-private'
+TAGS_ADDRESS_PRIVATE_INDEX = 'by-tags-address-private'
+INDEXES = {
+ TAGS_PRIVATE_INDEX: [
+ KEY_TAGS_KEY,
+ 'bool(%s)' % KEY_PRIVATE_KEY,
+ ],
+ TAGS_ADDRESS_PRIVATE_INDEX: [
+ KEY_TAGS_KEY,
+ KEY_ADDRESS_KEY,
+ 'bool(%s)' % KEY_PRIVATE_KEY,
+ ]
+}
+
+
+#
+# Key handling utilities
+#
+
+def is_address(address):
+ """
+ Return whether the given C{address} is in the form user@provider.
+
+ :param address: The address to be tested.
+ :type address: str
+ :return: Whether C{address} is in the form user@provider.
+ :rtype: bool
+ """
+ return bool(re.match('[\w.-]+@[\w.-]+', address))
+
+
+def build_key_from_dict(kClass, address, kdict):
+ """
+ Build an C{kClass} key bound to C{address} based on info in C{kdict}.
+
+ :param address: The address bound to the key.
+ :type address: str
+ :param kdict: Dictionary with key data.
+ :type kdict: dict
+ :return: An instance of the key.
+ :rtype: C{kClass}
+ """
+ leap_assert(
+ address == kdict[KEY_ADDRESS_KEY],
+ 'Wrong address in key data.')
+ return kClass(
+ address,
+ key_id=kdict[KEY_ID_KEY],
+ fingerprint=kdict[KEY_FINGERPRINT_KEY],
+ key_data=kdict[KEY_DATA_KEY],
+ private=kdict[KEY_PRIVATE_KEY],
+ length=kdict[KEY_LENGTH_KEY],
+ expiry_date=kdict[KEY_EXPIRY_DATE_KEY],
+ first_seen_at=kdict[KEY_FIRST_SEEN_AT_KEY],
+ last_audited_at=kdict[KEY_LAST_AUDITED_AT_KEY],
+ validation=kdict[KEY_VALIDATION_KEY], # TODO: verify for validation.
+ )
+
+
+#
+# Abstraction for encryption keys
+#
+
+class EncryptionKey(object):
+ """
+ Abstract class for encryption keys.
+
+ A key is "validated" if the nicknym agent has bound the user address to a
+ public key. Nicknym supports three different levels of key validation:
+
+ * Level 3 - path trusted: A path of cryptographic signatures can be traced
+ from a trusted key to the key under evaluation. By default, only the
+ provider key from the user's provider is a "trusted key".
+ * level 2 - provider signed: The key has been signed by a provider key for
+ the same domain, but the provider key is not validated using a trust
+ path (i.e. it is only registered)
+ * level 1 - registered: The key has been encountered and saved, it has no
+ signatures (that are meaningful to the nicknym agent).
+ """
+
+ __metaclass__ = ABCMeta
+
+ def __init__(self, address, key_id=None, fingerprint=None,
+ key_data=None, private=None, length=None, expiry_date=None,
+ validation=None, first_seen_at=None, last_audited_at=None):
+ self.address = address
+ self.key_id = key_id
+ self.fingerprint = fingerprint
+ self.key_data = key_data
+ self.private = private
+ self.length = length
+ self.expiry_date = expiry_date
+ self.validation = validation
+ self.first_seen_at = first_seen_at
+ self.last_audited_at = last_audited_at
+
+ def get_json(self):
+ """
+ Return a JSON string describing this key.
+
+ :return: The JSON string describing this key.
+ :rtype: str
+ """
+ return json.dumps({
+ KEY_ADDRESS_KEY: self.address,
+ KEY_TYPE_KEY: str(self.__class__),
+ KEY_ID_KEY: self.key_id,
+ KEY_FINGERPRINT_KEY: self.fingerprint,
+ KEY_DATA_KEY: self.key_data,
+ KEY_PRIVATE_KEY: self.private,
+ KEY_LENGTH_KEY: self.length,
+ KEY_EXPIRY_DATE_KEY: self.expiry_date,
+ KEY_VALIDATION_KEY: self.validation,
+ KEY_FIRST_SEEN_AT_KEY: self.first_seen_at,
+ KEY_LAST_AUDITED_AT_KEY: self.last_audited_at,
+ KEY_TAGS_KEY: [KEYMANAGER_KEY_TAG],
+ })
+
+ def __repr__(self):
+ """
+ Representation of this class
+ """
+ return u"<%s 0x%s (%s - %s)>" % (
+ self.__class__.__name__,
+ self.key_id,
+ self.address,
+ "priv" if self.private else "publ")
+
+
+#
+# Encryption schemes
+#
+
+class EncryptionScheme(object):
+ """
+ Abstract class for Encryption Schemes.
+
+ A wrapper for a certain encryption schemes should know how to get and put
+ keys in local storage using Soledad, how to generate new keys and how to
+ find out about possibly encrypted content.
+ """
+
+ __metaclass__ = ABCMeta
+
+ def __init__(self, soledad):
+ """
+ Initialize this Encryption Scheme.
+
+ :param soledad: A Soledad instance for local storage of keys.
+ :type soledad: leap.soledad.Soledad
+ """
+ self._soledad = soledad
+ self._init_indexes()
+
+ def _init_indexes(self):
+ """
+ Initialize the database indexes.
+ """
+ # Ask the database for currently existing indexes.
+ db_indexes = dict(self._soledad.list_indexes())
+ # Loop through the indexes we expect to find.
+ for name, expression in INDEXES.items():
+ if name not in db_indexes:
+ # The index does not yet exist.
+ self._soledad.create_index(name, *expression)
+ continue
+ if expression == db_indexes[name]:
+ # The index exists and is up to date.
+ continue
+ # The index exists but the definition is not what expected, so we
+ # delete it and add the proper index expression.
+ self._soledad.delete_index(name)
+ self._soledad.create_index(name, *expression)
+
+ @abstractmethod
+ def get_key(self, address, private=False):
+ """
+ Get key from local storage.
+
+ :param address: The address bound to the key.
+ :type address: str
+ :param private: Look for a private key instead of a public one?
+ :type private: bool
+
+ :return: The key bound to C{address}.
+ :rtype: EncryptionKey
+ @raise KeyNotFound: If the key was not found on local storage.
+ """
+ pass
+
+ @abstractmethod
+ def put_key(self, key):
+ """
+ Put a key in local storage.
+
+ :param key: The key to be stored.
+ :type key: EncryptionKey
+ """
+ pass
+
+ @abstractmethod
+ def gen_key(self, address):
+ """
+ Generate a new key.
+
+ :param address: The address bound to the key.
+ :type address: str
+
+ :return: The key bound to C{address}.
+ :rtype: EncryptionKey
+ """
+ pass
+
+ @abstractmethod
+ def delete_key(self, key):
+ """
+ Remove C{key} from storage.
+
+ :param key: The key to be removed.
+ :type key: EncryptionKey
+ """
+ pass
diff --git a/src/leap/keymanager/openpgp.py b/src/leap/keymanager/openpgp.py
new file mode 100644
index 00000000..89323117
--- /dev/null
+++ b/src/leap/keymanager/openpgp.py
@@ -0,0 +1,636 @@
+# -*- coding: utf-8 -*-
+# openpgp.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/>.
+
+"""
+Infrastructure for using OpenPGP keys in Key Manager.
+"""
+import logging
+import os
+import re
+import shutil
+import tempfile
+
+from leap.common.check import leap_assert, leap_assert_type
+from leap.keymanager import errors
+from leap.keymanager.keys import (
+ EncryptionKey,
+ EncryptionScheme,
+ is_address,
+ build_key_from_dict,
+ KEYMANAGER_KEY_TAG,
+ TAGS_ADDRESS_PRIVATE_INDEX,
+)
+from leap.keymanager.gpg import GPGWrapper
+
+logger = logging.getLogger(__name__)
+
+
+#
+# gpg wrapper and decorator
+#
+
+def temporary_gpgwrapper(keys=None):
+ """
+ Returns a unitary gpg wrapper that implements context manager
+ protocol.
+
+ :param key_data: ASCII armored key data.
+ :type key_data: str
+
+ :return: a GPGWrapper instance
+ :rtype: GPGWrapper
+ """
+ # TODO do here checks on key_data
+ return TempGPGWrapper(keys=keys)
+
+
+def with_temporary_gpg(fun):
+ """
+ Decorator to add a temporary gpg wrapper as context
+ to gpg related functions.
+
+ Decorated functions are expected to return a function whose only
+ argument is a gpgwrapper instance.
+ """
+ def wrapped(*args, **kwargs):
+ """
+ We extract the arguments passed to the wrapped function,
+ run the function and do validations.
+ We expect that the positional arguments are `data`,
+ and an optional `key`.
+ All the rest of arguments should be passed as named arguments
+ to allow for a correct unpacking.
+ """
+ if len(args) == 2:
+ keys = args[1] if isinstance(args[1], OpenPGPKey) else None
+ else:
+ keys = None
+
+ # sign/verify keys passed as arguments
+ sign = kwargs.get('sign', None)
+ if sign:
+ keys = [keys, sign]
+
+ verify = kwargs.get('verify', None)
+ if verify:
+ keys = [keys, verify]
+
+ # is the wrapped function sign or verify?
+ fun_name = fun.__name__
+ is_sign_function = True if fun_name == "sign" else False
+ is_verify_function = True if fun_name == "verify" else False
+
+ result = None
+
+ with temporary_gpgwrapper(keys) as gpg:
+ result = fun(*args, **kwargs)(gpg)
+
+ # TODO: cleanup a little bit the
+ # validation. maybe delegate to other
+ # auxiliary functions for clarity.
+
+ ok = getattr(result, 'ok', None)
+
+ stderr = getattr(result, 'stderr', None)
+ if stderr:
+ logger.debug("%s" % (stderr,))
+
+ if ok is False:
+ raise errors.EncryptionDecryptionFailed(
+ 'Failed to encrypt/decrypt in %s: %s' % (
+ fun.__name__,
+ stderr))
+
+ if verify is not None:
+ # A verify key has been passed
+ if result.valid is False or \
+ verify.fingerprint != result.pubkey_fingerprint:
+ raise errors.InvalidSignature(
+ 'Failed to verify signature with key %s: %s' %
+ (verify.key_id, stderr))
+
+ if is_sign_function:
+ # Specific validation for sign function
+ privkey = gpg.list_keys(secret=True).pop()
+ rfprint = result.fingerprint
+ kfprint = privkey['fingerprint']
+ if result.fingerprint is None:
+ raise errors.SignFailed(
+ 'Failed to sign with key %s: %s' %
+ (privkey['keyid'], stderr))
+ leap_assert(
+ result.fingerprint == kfprint,
+ 'Signature and private key fingerprints mismatch: '
+ '%s != %s' %
+ (rfprint, kfprint))
+
+ if is_verify_function:
+ # Specific validation for verify function
+ pubkey = gpg.list_keys().pop()
+ valid = result.valid
+ rfprint = result.fingerprint
+ kfprint = pubkey['fingerprint']
+ if valid is False or rfprint != kfprint:
+ raise errors.InvalidSignature(
+ 'Failed to verify signature '
+ 'with key %s.' % pubkey['keyid'])
+ result = result.valid
+
+ # ok, enough checks. let's return data if available
+ if hasattr(result, 'data'):
+ result = result.data
+ return result
+ return wrapped
+
+
+class TempGPGWrapper(object):
+ """
+ A context manager returning a temporary GPG wrapper keyring, which
+ contains exactly zero or one pubkeys, and zero or one privkeys.
+
+ Temporary unitary keyrings allow the to use GPG's facilities for exactly
+ one key. This function creates an empty temporary keyring and imports
+ C{keys} if it is not None.
+ """
+ def __init__(self, keys=None):
+ """
+ :param keys: OpenPGP key, or list of.
+ :type keys: OpenPGPKey or list of OpenPGPKeys
+ """
+ self._gpg = None
+ if not keys:
+ keys = list()
+ if not isinstance(keys, list):
+ keys = [keys]
+ self._keys = keys
+ for key in filter(None, keys):
+ leap_assert_type(key, OpenPGPKey)
+
+ def __enter__(self):
+ """
+ Calls the unitary gpgwrapper initializer
+
+ :return: A GPG wrapper with a unitary keyring.
+ :rtype: gnupg.GPG
+ """
+ self._build_keyring()
+ return self._gpg
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ """
+ Ensures the gpgwrapper is properly destroyed.
+ """
+ # TODO handle exceptions and log here
+ self._destroy_keyring()
+
+ def _build_keyring(self):
+ """
+ Create an empty GPG keyring and import C{keys} into it.
+
+ :param keys: List of keys to add to the keyring.
+ :type keys: list of OpenPGPKey
+
+ :return: A GPG wrapper with a unitary keyring.
+ :rtype: gnupg.GPG
+ """
+ privkeys = [key for key in self._keys if key and key.private is True]
+ publkeys = [key for key in self._keys if key and key.private is False]
+ # here we filter out public keys that have a correspondent
+ # private key in the list because the private key_data by
+ # itself is enough to also have the public key in the keyring,
+ # and we want to count the keys afterwards.
+
+ privaddrs = map(lambda privkey: privkey.address, privkeys)
+ publkeys = filter(
+ lambda pubkey: pubkey.address not in privaddrs, publkeys)
+
+ listkeys = lambda: self._gpg.list_keys()
+ listsecretkeys = lambda: self._gpg.list_keys(secret=True)
+
+ self._gpg = GPGWrapper(gnupghome=tempfile.mkdtemp())
+ leap_assert(len(listkeys()) is 0, 'Keyring not empty.')
+
+ # import keys into the keyring:
+ # concatenating ascii-armored keys, which is correctly
+ # understood by the GPGWrapper.
+
+ self._gpg.import_keys("".join(
+ [x.key_data for x in publkeys + privkeys]))
+
+ # assert the number of keys in the keyring
+ leap_assert(
+ len(listkeys()) == len(publkeys) + len(privkeys),
+ 'Wrong number of public keys in keyring: %d, should be %d)' %
+ (len(listkeys()), len(publkeys) + len(privkeys)))
+ leap_assert(
+ len(listsecretkeys()) == len(privkeys),
+ 'Wrong number of private keys in keyring: %d, should be %d)' %
+ (len(listsecretkeys()), len(privkeys)))
+
+ def _destroy_keyring(self):
+ """
+ Securely erase a unitary keyring.
+ """
+ # TODO: implement some kind of wiping of data or a more
+ # secure way that
+ # does not write to disk.
+
+ try:
+ for secret in [True, False]:
+ for key in self._gpg.list_keys(secret=secret):
+ self._gpg.delete_keys(
+ key['fingerprint'],
+ secret=secret)
+ leap_assert(len(self._gpg.list_keys()) is 0, 'Keyring not empty!')
+
+ except:
+ raise
+
+ finally:
+ leap_assert(self._gpg.gnupghome != os.path.expanduser('~/.gnupg'),
+ "watch out! Tried to remove default gnupg home!")
+ shutil.rmtree(self._gpg.gnupghome)
+
+
+#
+# API functions
+#
+
+@with_temporary_gpg
+def encrypt_asym(data, key, passphrase=None, sign=None):
+ """
+ Encrypt C{data} using public @{key} and sign with C{sign} key.
+
+ :param data: The data to be encrypted.
+ :type data: str
+ :param pubkey: The key used to encrypt.
+ :type pubkey: OpenPGPKey
+ :param sign: The key used for signing.
+ :type sign: OpenPGPKey
+
+ :return: The encrypted data.
+ :rtype: str
+ """
+ leap_assert_type(key, OpenPGPKey)
+ leap_assert(key.private is False, 'Key is not public.')
+ if sign is not None:
+ leap_assert_type(sign, OpenPGPKey)
+ leap_assert(sign.private is True)
+
+ # Here we cannot assert for correctness of sig because the sig is in
+ # the ciphertext.
+ # result.ok - (bool) indicates if the operation succeeded
+ # result.data - (bool) contains the result of the operation
+
+ return lambda gpg: gpg.encrypt(
+ data, key.fingerprint,
+ sign=sign.key_id if sign else None,
+ passphrase=passphrase, symmetric=False)
+
+
+@with_temporary_gpg
+def decrypt_asym(data, key, passphrase=None, verify=None):
+ """
+ Decrypt C{data} using private @{key} and verify with C{verify} key.
+
+ :param data: The data to be decrypted.
+ :type data: str
+ :param privkey: The key used to decrypt.
+ :type privkey: OpenPGPKey
+ :param verify: The key used to verify a signature.
+ :type verify: OpenPGPKey
+
+ :return: The decrypted data.
+ :rtype: str
+
+ @raise InvalidSignature: Raised if unable to verify the signature with
+ C{verify} key.
+ """
+ leap_assert(key.private is True, 'Key is not private.')
+ if verify is not None:
+ leap_assert_type(verify, OpenPGPKey)
+ leap_assert(verify.private is False)
+
+ return lambda gpg: gpg.decrypt(
+ data, passphrase=passphrase)
+
+
+@with_temporary_gpg
+def is_encrypted(data):
+ """
+ Return whether C{data} was encrypted using OpenPGP.
+
+ :param data: The data we want to know about.
+ :type data: str
+
+ :return: Whether C{data} was encrypted using this wrapper.
+ :rtype: bool
+ """
+ return lambda gpg: gpg.is_encrypted(data)
+
+
+@with_temporary_gpg
+def is_encrypted_asym(data):
+ """
+ Return whether C{data} was asymmetrically encrypted using OpenPGP.
+
+ :param data: The data we want to know about.
+ :type data: str
+
+ :return: Whether C{data} was encrypted using this wrapper.
+ :rtype: bool
+ """
+ return lambda gpg: gpg.is_encrypted_asym(data)
+
+
+@with_temporary_gpg
+def sign(data, privkey):
+ """
+ Sign C{data} with C{privkey}.
+
+ :param data: The data to be signed.
+ :type data: str
+
+ :param privkey: The private key to be used to sign.
+ :type privkey: OpenPGPKey
+
+ :return: The ascii-armored signed data.
+ :rtype: str
+ """
+ leap_assert_type(privkey, OpenPGPKey)
+ leap_assert(privkey.private is True)
+
+ # result.fingerprint - contains the fingerprint of the key used to
+ # sign.
+ return lambda gpg: gpg.sign(data, keyid=privkey.key_id)
+
+
+@with_temporary_gpg
+def verify(data, key):
+ """
+ Verify signed C{data} with C{pubkey}.
+
+ :param data: The data to be verified.
+ :type data: str
+
+ :param pubkey: The public key to be used on verification.
+ :type pubkey: OpenPGPKey
+
+ :return: The ascii-armored signed data.
+ :rtype: str
+ """
+ leap_assert_type(key, OpenPGPKey)
+ leap_assert(key.private is False)
+
+ return lambda gpg: gpg.verify(data)
+
+
+#
+# Helper functions
+#
+
+
+def _build_key_from_gpg(address, key, key_data):
+ """
+ Build an OpenPGPKey for C{address} based on C{key} from
+ local gpg storage.
+
+ ASCII armored GPG key data has to be queried independently in this
+ wrapper, so we receive it in C{key_data}.
+
+ :param address: The address bound to the key.
+ :type address: str
+ :param key: Key obtained from GPG storage.
+ :type key: dict
+ :param key_data: Key data obtained from GPG storage.
+ :type key_data: str
+ :return: An instance of the key.
+ :rtype: OpenPGPKey
+ """
+ return OpenPGPKey(
+ address,
+ key_id=key['keyid'],
+ fingerprint=key['fingerprint'],
+ key_data=key_data,
+ private=True if key['type'] == 'sec' else False,
+ length=key['length'],
+ expiry_date=key['expires'],
+ validation=None, # TODO: verify for validation.
+ )
+
+
+#
+# The OpenPGP wrapper
+#
+
+class OpenPGPKey(EncryptionKey):
+ """
+ Base class for OpenPGP keys.
+ """
+
+
+class OpenPGPScheme(EncryptionScheme):
+ """
+ A wrapper for OpenPGP keys.
+ """
+
+ def __init__(self, soledad):
+ """
+ Initialize the OpenPGP wrapper.
+
+ :param soledad: A Soledad instance for key storage.
+ :type soledad: leap.soledad.Soledad
+ """
+ EncryptionScheme.__init__(self, soledad)
+
+ def gen_key(self, address):
+ """
+ Generate an OpenPGP keypair bound to C{address}.
+
+ :param address: The address bound to the key.
+ :type address: str
+ :return: The key bound to C{address}.
+ :rtype: OpenPGPKey
+ @raise KeyAlreadyExists: If key already exists in local database.
+ """
+ # make sure the key does not already exist
+ leap_assert(is_address(address), 'Not an user address: %s' % address)
+ try:
+ self.get_key(address)
+ raise errors.KeyAlreadyExists(address)
+ except errors.KeyNotFound:
+ pass
+
+ def _gen_key(gpg):
+ params = gpg.gen_key_input(
+ key_type='RSA',
+ key_length=4096,
+ name_real=address,
+ name_email=address,
+ name_comment='Generated by LEAP Key Manager.')
+ gpg.gen_key(params)
+ pubkeys = gpg.list_keys()
+ # assert for new key characteristics
+ leap_assert(
+ len(pubkeys) is 1, # a unitary keyring!
+ 'Keyring has wrong number of keys: %d.' % len(pubkeys))
+ key = gpg.list_keys(secret=True).pop()
+ leap_assert(
+ len(key['uids']) is 1, # with just one uid!
+ 'Wrong number of uids for key: %d.' % len(key['uids']))
+ leap_assert(
+ re.match('.*<%s>$' % address, key['uids'][0]) is not None,
+ 'Key not correctly bound to address.')
+ # insert both public and private keys in storage
+ for secret in [True, False]:
+ key = gpg.list_keys(secret=secret).pop()
+ openpgp_key = _build_key_from_gpg(
+ address, key,
+ gpg.export_keys(key['fingerprint'], secret=secret))
+ self.put_key(openpgp_key)
+
+ with temporary_gpgwrapper() as gpg:
+ # TODO: inspect result, or use decorator
+ _gen_key(gpg)
+
+ return self.get_key(address, private=True)
+
+ def get_key(self, address, private=False):
+ """
+ Get key bound to C{address} from local storage.
+
+ :param address: The address bound to the key.
+ :type address: str
+ :param private: Look for a private key instead of a public one?
+ :type private: bool
+
+ :return: The key bound to C{address}.
+ :rtype: OpenPGPKey
+ @raise KeyNotFound: If the key was not found on local storage.
+ """
+ leap_assert(is_address(address), 'Not an user address: %s' % address)
+ doc = self._get_key_doc(address, private)
+ if doc is None:
+ raise errors.KeyNotFound(address)
+ return build_key_from_dict(OpenPGPKey, address, doc.content)
+
+ def put_ascii_key(self, key_data):
+ """
+ Put key contained in ascii-armored C{key_data} in local storage.
+
+ :param key_data: The key data to be stored.
+ :type key_data: str
+ """
+ leap_assert_type(key_data, str)
+ # TODO: add more checks for correct key data.
+ leap_assert(key_data is not None, 'Data does not represent a key.')
+
+ def _put_ascii_key(gpg):
+ gpg.import_keys(key_data)
+ privkey = None
+ pubkey = None
+
+ try:
+ privkey = gpg.list_keys(secret=True).pop()
+ except IndexError:
+ pass
+ pubkey = gpg.list_keys(secret=False).pop() # unitary keyring
+ # extract adress from first uid on key
+ match = re.match('.*<([\w.-]+@[\w.-]+)>.*', pubkey['uids'].pop())
+ leap_assert(match is not None, 'No user address in key data.')
+ address = match.group(1)
+ if privkey is not None:
+ match = re.match(
+ '.*<([\w.-]+@[\w.-]+)>.*', privkey['uids'].pop())
+ leap_assert(match is not None, 'No user address in key data.')
+ privaddress = match.group(1)
+ leap_assert(
+ address == privaddress,
+ 'Addresses in pub and priv key differ.')
+ leap_assert(
+ pubkey['fingerprint'] == privkey['fingerprint'],
+ 'Fingerprints for pub and priv key differ.')
+ # insert private key in storage
+ openpgp_privkey = _build_key_from_gpg(
+ address, privkey,
+ gpg.export_keys(privkey['fingerprint'], secret=True))
+ self.put_key(openpgp_privkey)
+ # insert public key in storage
+ openpgp_pubkey = _build_key_from_gpg(
+ address, pubkey,
+ gpg.export_keys(pubkey['fingerprint'], secret=False))
+ self.put_key(openpgp_pubkey)
+
+ with temporary_gpgwrapper() as gpg:
+ # TODO: inspect result, or use decorator
+ _put_ascii_key(gpg)
+
+ def put_key(self, key):
+ """
+ Put C{key} in local storage.
+
+ :param key: The key to be stored.
+ :type key: OpenPGPKey
+ """
+ doc = self._get_key_doc(key.address, private=key.private)
+ if doc is None:
+ self._soledad.create_doc_from_json(key.get_json())
+ else:
+ doc.set_json(key.get_json())
+ self._soledad.put_doc(doc)
+
+ def _get_key_doc(self, address, private=False):
+ """
+ Get the document with a key (public, by default) bound to C{address}.
+
+ If C{private} is True, looks for a private key instead of a public.
+
+ :param address: The address bound to the key.
+ :type address: str
+ :param private: Whether to look for a private key.
+ :type private: bool
+ :return: The document with the key or None if it does not exist.
+ :rtype: leap.soledad.backends.leap_backend.LeapDocument
+ """
+ doclist = self._soledad.get_from_index(
+ TAGS_ADDRESS_PRIVATE_INDEX,
+ KEYMANAGER_KEY_TAG,
+ address,
+ '1' if private else '0')
+ if len(doclist) is 0:
+ return None
+ leap_assert(
+ len(doclist) is 1,
+ 'Found more than one %s key for address!' %
+ 'private' if private else 'public')
+ return doclist.pop()
+
+ def delete_key(self, key):
+ """
+ Remove C{key} from storage.
+
+ :param key: The key to be removed.
+ :type key: EncryptionKey
+ """
+ leap_assert(key.__class__ is OpenPGPKey, 'Wrong key type.')
+ stored_key = self.get_key(key.address, private=key.private)
+ if stored_key is None:
+ raise errors.KeyNotFound(key)
+ if stored_key.__dict__ != key.__dict__:
+ raise errors.KeyAttributesDiffer(key)
+ doc = self._get_key_doc(key.address, key.private)
+ self._soledad.delete_doc(doc)
diff --git a/src/leap/base/tests/__init__.py b/src/leap/keymanager/tests/__init__.py
index e69de29b..e69de29b 100644
--- a/src/leap/base/tests/__init__.py
+++ b/src/leap/keymanager/tests/__init__.py
diff --git a/src/leap/keymanager/tests/test_keymanager.py b/src/leap/keymanager/tests/test_keymanager.py
new file mode 100644
index 00000000..a36406a6
--- /dev/null
+++ b/src/leap/keymanager/tests/test_keymanager.py
@@ -0,0 +1,686 @@
+## -*- coding: utf-8 -*-
+# test_keymanager.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 Key Manager.
+"""
+
+
+from mock import Mock
+try:
+ import simplejson as json
+except ImportError:
+ import json # noqa
+
+
+from leap.common.testing.basetest import BaseLeapTest
+from leap.soledad import Soledad
+#from leap.soledad.crypto import SoledadCrypto
+
+from leap.keymanager import (
+ KeyManager,
+ openpgp,
+ KeyNotFound,
+ NoPasswordGiven,
+ #TAGS_INDEX,
+ #TAGS_AND_PRIVATE_INDEX,
+)
+from leap.keymanager.openpgp import OpenPGPKey
+from leap.keymanager.keys import (
+ is_address,
+ build_key_from_dict,
+)
+from leap.keymanager import errors
+
+
+ADDRESS = 'leap@leap.se'
+ADDRESS_2 = 'anotheruser@leap.se'
+
+
+class KeyManagerUtilTestCase(BaseLeapTest):
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ def test_is_address(self):
+ self.assertTrue(
+ is_address('user@leap.se'),
+ 'Incorrect address detection.')
+ self.assertFalse(
+ is_address('userleap.se'),
+ 'Incorrect address detection.')
+ self.assertFalse(
+ is_address('user@'),
+ 'Incorrect address detection.')
+ self.assertFalse(
+ is_address('@leap.se'),
+ 'Incorrect address detection.')
+
+ def test_build_key_from_dict(self):
+ kdict = {
+ 'address': ADDRESS,
+ 'key_id': 'key_id',
+ 'fingerprint': 'fingerprint',
+ 'key_data': 'key_data',
+ 'private': 'private',
+ 'length': 'length',
+ 'expiry_date': 'expiry_date',
+ 'first_seen_at': 'first_seen_at',
+ 'last_audited_at': 'last_audited_at',
+ 'validation': 'validation',
+ }
+ key = build_key_from_dict(OpenPGPKey, ADDRESS, kdict)
+ self.assertEqual(
+ kdict['address'], key.address,
+ 'Wrong data in key.')
+ self.assertEqual(
+ kdict['key_id'], key.key_id,
+ 'Wrong data in key.')
+ self.assertEqual(
+ kdict['fingerprint'], key.fingerprint,
+ 'Wrong data in key.')
+ self.assertEqual(
+ kdict['key_data'], key.key_data,
+ 'Wrong data in key.')
+ self.assertEqual(
+ kdict['private'], key.private,
+ 'Wrong data in key.')
+ self.assertEqual(
+ kdict['length'], key.length,
+ 'Wrong data in key.')
+ self.assertEqual(
+ kdict['expiry_date'], key.expiry_date,
+ 'Wrong data in key.')
+ self.assertEqual(
+ kdict['first_seen_at'], key.first_seen_at,
+ 'Wrong data in key.')
+ self.assertEqual(
+ kdict['last_audited_at'], key.last_audited_at,
+ 'Wrong data in key.')
+ self.assertEqual(
+ kdict['validation'], key.validation,
+ 'Wrong data in key.')
+
+
+class KeyManagerWithSoledadTestCase(BaseLeapTest):
+
+ def setUp(self):
+ # mock key fetching and storing so Soledad doesn't fail when trying to
+ # reach the server.
+ Soledad._get_secrets_from_shared_db = Mock(return_value=None)
+ Soledad._put_secrets_in_shared_db = Mock(return_value=None)
+
+ self._soledad = Soledad(
+ "leap@leap.se",
+ "123456",
+ self.tempdir + "/secret.gpg",
+ self.tempdir + "/soledad.u1db",
+ '',
+ None,
+ auth_token=None,
+ )
+
+ def tearDown(self):
+ km = self._key_manager()
+ for key in km.get_all_keys_in_local_db():
+ km._wrapper_map[key.__class__].delete_key(key)
+ for key in km.get_all_keys_in_local_db(private=True):
+ km._wrapper_map[key.__class__].delete_key(key)
+
+ def _key_manager(self, user=ADDRESS, url=''):
+ return KeyManager(user, url, self._soledad)
+
+
+class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase):
+
+ def _test_openpgp_gen_key(self):
+ pgp = openpgp.OpenPGPScheme(self._soledad)
+ self.assertRaises(KeyNotFound, pgp.get_key, 'user@leap.se')
+ key = pgp.gen_key('user@leap.se')
+ self.assertIsInstance(key, openpgp.OpenPGPKey)
+ self.assertEqual(
+ 'user@leap.se', key.address, 'Wrong address bound to key.')
+ self.assertEqual(
+ '4096', key.length, 'Wrong key length.')
+
+ def test_openpgp_put_delete_key(self):
+ pgp = openpgp.OpenPGPScheme(self._soledad)
+ self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS)
+ pgp.put_ascii_key(PUBLIC_KEY)
+ key = pgp.get_key(ADDRESS, private=False)
+ pgp.delete_key(key)
+ self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS)
+
+ def test_openpgp_put_ascii_key(self):
+ pgp = openpgp.OpenPGPScheme(self._soledad)
+ self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS)
+ pgp.put_ascii_key(PUBLIC_KEY)
+ key = pgp.get_key(ADDRESS, private=False)
+ self.assertIsInstance(key, openpgp.OpenPGPKey)
+ self.assertEqual(
+ ADDRESS, key.address, 'Wrong address bound to key.')
+ self.assertEqual(
+ '4096', key.length, 'Wrong key length.')
+ pgp.delete_key(key)
+ self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS)
+
+ def test_get_public_key(self):
+ pgp = openpgp.OpenPGPScheme(self._soledad)
+ self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS)
+ pgp.put_ascii_key(PUBLIC_KEY)
+ self.assertRaises(
+ KeyNotFound, pgp.get_key, ADDRESS, private=True)
+ key = pgp.get_key(ADDRESS, private=False)
+ self.assertEqual(ADDRESS, key.address)
+ self.assertFalse(key.private)
+ self.assertEqual(KEY_FINGERPRINT, key.fingerprint)
+ pgp.delete_key(key)
+ self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS)
+
+ def test_openpgp_encrypt_decrypt_asym(self):
+ # encrypt
+ pgp = openpgp.OpenPGPScheme(self._soledad)
+ pgp.put_ascii_key(PUBLIC_KEY)
+ pubkey = pgp.get_key(ADDRESS, private=False)
+ cyphertext = openpgp.encrypt_asym('data', pubkey)
+ # assert
+ self.assertTrue(cyphertext is not None)
+ self.assertTrue(cyphertext != '')
+ self.assertTrue(cyphertext != 'data')
+ self.assertTrue(openpgp.is_encrypted_asym(cyphertext))
+ self.assertTrue(openpgp.is_encrypted(cyphertext))
+ # decrypt
+ self.assertRaises(
+ KeyNotFound, pgp.get_key, ADDRESS, private=True)
+ pgp.put_ascii_key(PRIVATE_KEY)
+ privkey = pgp.get_key(ADDRESS, private=True)
+ plaintext = openpgp.decrypt_asym(cyphertext, privkey)
+ pgp.delete_key(pubkey)
+ pgp.delete_key(privkey)
+ self.assertRaises(
+ KeyNotFound, pgp.get_key, ADDRESS, private=False)
+ self.assertRaises(
+ KeyNotFound, pgp.get_key, ADDRESS, private=True)
+
+ def test_verify_with_private_raises(self):
+ pgp = openpgp.OpenPGPScheme(self._soledad)
+ pgp.put_ascii_key(PRIVATE_KEY)
+ data = 'data'
+ privkey = pgp.get_key(ADDRESS, private=True)
+ signed = openpgp.sign(data, privkey)
+ self.assertRaises(
+ AssertionError,
+ openpgp.verify, signed, privkey)
+
+ def test_sign_with_public_raises(self):
+ pgp = openpgp.OpenPGPScheme(self._soledad)
+ pgp.put_ascii_key(PUBLIC_KEY)
+ data = 'data'
+ pubkey = pgp.get_key(ADDRESS, private=False)
+ self.assertRaises(
+ AssertionError,
+ openpgp.sign, data, pubkey)
+
+ def test_verify_with_wrong_key_raises(self):
+ pgp = openpgp.OpenPGPScheme(self._soledad)
+ pgp.put_ascii_key(PRIVATE_KEY)
+ data = 'data'
+ privkey = pgp.get_key(ADDRESS, private=True)
+ signed = openpgp.sign(data, privkey)
+ pgp.put_ascii_key(PUBLIC_KEY_2)
+ wrongkey = pgp.get_key(ADDRESS_2)
+ self.assertRaises(
+ errors.InvalidSignature,
+ openpgp.verify, signed, wrongkey)
+
+ def test_encrypt_asym_sign_with_public_raises(self):
+ pgp = openpgp.OpenPGPScheme(self._soledad)
+ pgp.put_ascii_key(PRIVATE_KEY)
+ data = 'data'
+ privkey = pgp.get_key(ADDRESS, private=True)
+ pubkey = pgp.get_key(ADDRESS, private=False)
+ self.assertRaises(
+ AssertionError,
+ openpgp.encrypt_asym, data, privkey, sign=pubkey)
+
+ def test_decrypt_asym_verify_with_private_raises(self):
+ pgp = openpgp.OpenPGPScheme(self._soledad)
+ pgp.put_ascii_key(PRIVATE_KEY)
+ data = 'data'
+ privkey = pgp.get_key(ADDRESS, private=True)
+ pubkey = pgp.get_key(ADDRESS, private=False)
+ encrypted_and_signed = openpgp.encrypt_asym(
+ data, pubkey, sign=privkey)
+ self.assertRaises(
+ AssertionError,
+ openpgp.decrypt_asym,
+ encrypted_and_signed, privkey, verify=privkey)
+
+ def test_decrypt_asym_verify_with_wrong_key_raises(self):
+ pgp = openpgp.OpenPGPScheme(self._soledad)
+ pgp.put_ascii_key(PRIVATE_KEY)
+ data = 'data'
+ privkey = pgp.get_key(ADDRESS, private=True)
+ pubkey = pgp.get_key(ADDRESS, private=False)
+ encrypted_and_signed = openpgp.encrypt_asym(data, pubkey, sign=privkey)
+ pgp.put_ascii_key(PUBLIC_KEY_2)
+ wrongkey = pgp.get_key(ADDRESS_2)
+ self.assertRaises(
+ errors.InvalidSignature,
+ openpgp.verify, encrypted_and_signed, wrongkey)
+
+ def test_sign_verify(self):
+ pgp = openpgp.OpenPGPScheme(self._soledad)
+ pgp.put_ascii_key(PRIVATE_KEY)
+ data = 'data'
+ privkey = pgp.get_key(ADDRESS, private=True)
+ signed = openpgp.sign(data, privkey)
+ pubkey = pgp.get_key(ADDRESS, private=False)
+ self.assertTrue(openpgp.verify(signed, pubkey))
+
+ def test_encrypt_asym_sign_decrypt_verify(self):
+ pgp = openpgp.OpenPGPScheme(self._soledad)
+ pgp.put_ascii_key(PRIVATE_KEY)
+ pubkey = pgp.get_key(ADDRESS, private=False)
+ privkey = pgp.get_key(ADDRESS, private=True)
+ pgp.put_ascii_key(PRIVATE_KEY_2)
+ pubkey2 = pgp.get_key(ADDRESS_2, private=False)
+ privkey2 = pgp.get_key(ADDRESS_2, private=True)
+ data = 'data'
+ encrypted_and_signed = openpgp.encrypt_asym(
+ data, pubkey2, sign=privkey)
+ res = openpgp.decrypt_asym(
+ encrypted_and_signed, privkey2, verify=pubkey)
+ self.assertTrue(data, res)
+
+
+class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase):
+
+ def test_get_all_keys_in_db(self):
+ km = self._key_manager()
+ km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY)
+ # get public keys
+ keys = km.get_all_keys_in_local_db(False)
+ self.assertEqual(len(keys), 1, 'Wrong number of keys')
+ self.assertEqual(ADDRESS, keys[0].address)
+ self.assertFalse(keys[0].private)
+ # get private keys
+ keys = km.get_all_keys_in_local_db(True)
+ self.assertEqual(len(keys), 1, 'Wrong number of keys')
+ self.assertEqual(ADDRESS, keys[0].address)
+ self.assertTrue(keys[0].private)
+
+ def test_get_public_key(self):
+ km = self._key_manager()
+ km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY)
+ # get the key
+ key = km.get_key(ADDRESS, OpenPGPKey, private=False,
+ fetch_remote=False)
+ self.assertTrue(key is not None)
+ self.assertEqual(key.address, ADDRESS)
+ self.assertEqual(
+ key.fingerprint.lower(), KEY_FINGERPRINT.lower())
+ self.assertFalse(key.private)
+
+ def test_get_private_key(self):
+ km = self._key_manager()
+ km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY)
+ # get the key
+ key = km.get_key(ADDRESS, OpenPGPKey, private=True,
+ fetch_remote=False)
+ self.assertTrue(key is not None)
+ self.assertEqual(key.address, ADDRESS)
+ self.assertEqual(
+ key.fingerprint.lower(), KEY_FINGERPRINT.lower())
+ self.assertTrue(key.private)
+
+ def test_send_key_raises_key_not_found(self):
+ km = self._key_manager()
+ self.assertRaises(
+ KeyNotFound,
+ km.send_key, OpenPGPKey)
+
+ def test_send_key(self):
+ """
+ Test that request is well formed when sending keys to server.
+ """
+ km = self._key_manager()
+ km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY)
+ km._fetcher.put = Mock()
+ # the following data will be used on the send
+ km.ca_cert_path = 'capath'
+ km.session_id = 'sessionid'
+ km.uid = 'myuid'
+ km.api_uri = 'apiuri'
+ km.api_version = 'apiver'
+ km.send_key(OpenPGPKey)
+ # setup expected args
+ data = {
+ km.PUBKEY_KEY: km.get_key(km._address, OpenPGPKey).key_data,
+ }
+ url = '%s/%s/users/%s.json' % ('apiuri', 'apiver', 'myuid')
+ km._fetcher.put.assert_called_once_with(
+ url, data=data, verify='capath',
+ cookies={'_session_id': 'sessionid'},
+ )
+
+ def test__fetch_keys_from_server(self):
+ """
+ Test that the request is well formed when fetching keys from server.
+ """
+ km = self._key_manager(url='http://nickserver.domain')
+
+ class Response(object):
+ status_code = 200
+ headers = {'content-type': 'application/json'}
+
+ def json(self):
+ return {'address': ADDRESS_2, 'openpgp': PUBLIC_KEY_2}
+
+ def raise_for_status(self):
+ pass
+
+ # mock the fetcher so it returns the key for ADDRESS_2
+ km._fetcher.get = Mock(
+ return_value=Response())
+ km.ca_cert_path = 'cacertpath'
+ # do the fetch
+ km._fetch_keys_from_server(ADDRESS_2)
+ # and verify the call
+ km._fetcher.get.assert_called_once_with(
+ 'http://nickserver.domain',
+ data={'address': ADDRESS_2},
+ verify='cacertpath',
+ )
+
+ def test_refresh_keys_does_not_refresh_own_key(self):
+ """
+ Test that refreshing keys will not attempt to refresh our own key.
+ """
+ km = self._key_manager()
+ # we add 2 keys but we expect it to only refresh the second one.
+ km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY)
+ km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY_2)
+ # mock the key fetching
+ km._fetch_keys_from_server = Mock(return_value=[])
+ km.ca_cert_path = '' # some bogus path so the km does not complain.
+ # do the refreshing
+ km.refresh_keys()
+ km._fetch_keys_from_server.assert_called_once_with(
+ ADDRESS_2
+ )
+
+ def test_get_key_fetches_from_server(self):
+ """
+ Test that getting a key successfuly fetches from server.
+ """
+ km = self._key_manager(url='http://nickserver.domain')
+
+ class Response(object):
+ status_code = 200
+ headers = {'content-type': 'application/json'}
+
+ def json(self):
+ return {'address': ADDRESS_2, 'openpgp': PUBLIC_KEY_2}
+
+ def raise_for_status(self):
+ pass
+
+ # mock the fetcher so it returns the key for ADDRESS_2
+ km._fetcher.get = Mock(return_value=Response())
+ km.ca_cert_path = 'cacertpath'
+ # try to key get without fetching from server
+ self.assertRaises(
+ KeyNotFound, km.get_key, ADDRESS_2, OpenPGPKey,
+ fetch_remote=False
+ )
+ # try to get key fetching from server.
+ key = km.get_key(ADDRESS_2, OpenPGPKey)
+ self.assertIsInstance(key, OpenPGPKey)
+ self.assertEqual(ADDRESS_2, key.address)
+
+
+# Key material for testing
+
+# key 24D18DDF: public key "Leap Test Key <leap@leap.se>"
+KEY_FINGERPRINT = "E36E738D69173C13D709E44F2F455E2824D18DDF"
+PUBLIC_KEY = """
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.10 (GNU/Linux)
+
+mQINBFC9+dkBEADNRfwV23TWEoGc/x0wWH1P7PlXt8MnC2Z1kKaKKmfnglVrpOiz
+iLWoiU58sfZ0L5vHkzXHXCBf6Eiy/EtUIvdiWAn+yASJ1mk5jZTBKO/WMAHD8wTO
+zpMsFmWyg3xc4DkmFa9KQ5EVU0o/nqPeyQxNMQN7px5pPwrJtJFmPxnxm+aDkPYx
+irDmz/4DeDNqXliazGJKw7efqBdlwTHkl9Akw2gwy178pmsKwHHEMOBOFFvX61AT
+huKqHYmlCGSliwbrJppTG7jc1/ls3itrK+CWTg4txREkSpEVmfcASvw/ZqLbjgfs
+d/INMwXnR9U81O8+7LT6yw/ca4ppcFoJD7/XJbkRiML6+bJ4Dakiy6i727BzV17g
+wI1zqNvm5rAhtALKfACha6YO43aJzairO4II1wxVHvRDHZn2IuKDDephQ3Ii7/vb
+hUOf6XCSmchkAcpKXUOvbxm1yfB1LRa64mMc2RcZxf4mW7KQkulBsdV5QG2276lv
+U2UUy2IutXcGP5nXC+f6sJJGJeEToKJ57yiO/VWJFjKN8SvP+7AYsQSqINUuEf6H
+T5gCPCraGMkTUTPXrREvu7NOohU78q6zZNaL3GW8ai7eSeANSuQ8Vzffx7Wd8Y7i
+Pw9sYj0SMFs1UgjbuL6pO5ueHh+qyumbtAq2K0Bci0kqOcU4E9fNtdiovQARAQAB
+tBxMZWFwIFRlc3QgS2V5IDxsZWFwQGxlYXAuc2U+iQI3BBMBCAAhBQJQvfnZAhsD
+BQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEC9FXigk0Y3fT7EQAKH3IuRniOpb
+T/DDIgwwjz3oxB/W0DDMyPXowlhSOuM0rgGfntBpBb3boezEXwL86NPQxNGGruF5
+hkmecSiuPSvOmQlqlS95NGQp6hNG0YaKColh+Q5NTspFXCAkFch9oqUje0LdxfSP
+QfV9UpeEvGyPmk1I9EJV/YDmZ4+Djge1d7qhVZInz4Rx1NrSyF/Tc2EC0VpjQFsU
+Y9Kb2YBBR7ivG6DBc8ty0jJXi7B4WjkFcUEJviQpMF2dCLdonCehYs1PqsN1N7j+
+eFjQd+hqVMJgYuSGKjvuAEfClM6MQw7+FmFwMyLgK/Ew/DttHEDCri77SPSkOGSI
+txCzhTg6798f6mJr7WcXmHX1w1Vcib5FfZ8vTDFVhz/XgAgArdhPo9V6/1dgSSiB
+KPQ/spsco6u5imdOhckERE0lnAYvVT6KE81TKuhF/b23u7x+Wdew6kK0EQhYA7wy
+7LmlaNXc7rMBQJ9Z60CJ4JDtatBWZ0kNrt2VfdDHVdqBTOpl0CraNUjWE5YMDasr
+K2dF5IX8D3uuYtpZnxqg0KzyLg0tzL0tvOL1C2iudgZUISZNPKbS0z0v+afuAAnx
+2pTC3uezbh2Jt8SWTLhll4i0P4Ps5kZ6HQUO56O+/Z1cWovX+mQekYFmERySDR9n
+3k1uAwLilJmRmepGmvYbB8HloV8HqwgguQINBFC9+dkBEAC0I/xn1uborMgDvBtf
+H0sEhwnXBC849/32zic6udB6/3Efk9nzbSpL3FSOuXITZsZgCHPkKarnoQ2ztMcS
+sh1ke1C5gQGms75UVmM/nS+2YI4vY8OX/GC/on2vUyncqdH+bR6xH5hx4NbWpfTs
+iQHmz5C6zzS/kuabGdZyKRaZHt23WQ7JX/4zpjqbC99DjHcP9BSk7tJ8wI4bkMYD
+uFVQdT9O6HwyKGYwUU4sAQRAj7XCTGvVbT0dpgJwH4RmrEtJoHAx4Whg8mJ710E0
+GCmzf2jqkNuOw76ivgk27Kge+Hw00jmJjQhHY0yVbiaoJwcRrPKzaSjEVNgrpgP3
+lXPRGQArgESsIOTeVVHQ8fhK2YtTeCY9rIiO+L0OX2xo9HK7hfHZZWL6rqymXdyS
+fhzh/f6IPyHFWnvj7Brl7DR8heMikygcJqv+ed2yx7iLyCUJ10g12I48+aEj1aLe
+dP7lna32iY8/Z0SHQLNH6PXO9SlPcq2aFUgKqE75A/0FMk7CunzU1OWr2ZtTLNO1
+WT/13LfOhhuEq9jTyTosn0WxBjJKq18lnhzCXlaw6EAtbA7CUwsD3CTPR56aAXFK
+3I7KXOVAqggrvMe5Tpdg5drfYpI8hZovL5aAgb+7Y5ta10TcJdUhS5K3kFAWe/td
+U0cmWUMDP1UMSQ5Jg6JIQVWhSwARAQABiQIfBBgBCAAJBQJQvfnZAhsMAAoJEC9F
+Xigk0Y3fRwsP/i0ElYCyxeLpWJTwo1iCLkMKz2yX1lFVa9nT1BVTPOQwr/IAc5OX
+NdtbJ14fUsKL5pWgW8OmrXtwZm1y4euI1RPWWubG01ouzwnGzv26UcuHeqC5orZj
+cOnKtL40y8VGMm8LoicVkRJH8blPORCnaLjdOtmA3rx/v2EXrJpSa3AhOy0ZSRXk
+ZSrK68AVNwamHRoBSYyo0AtaXnkPX4+tmO8X8BPfj125IljubvwZPIW9VWR9UqCE
+VPfDR1XKegVb6VStIywF7kmrknM1C5qUY28rdZYWgKorw01hBGV4jTW0cqde3N51
+XT1jnIAa+NoXUM9uQoGYMiwrL7vNsLlyyiW5ayDyV92H/rIuiqhFgbJsHTlsm7I8
+oGheR784BagAA1NIKD1qEO9T6Kz9lzlDaeWS5AUKeXrb7ZJLI1TTCIZx5/DxjLqM
+Tt/RFBpVo9geZQrvLUqLAMwdaUvDXC2c6DaCPXTh65oCZj/hqzlJHH+RoTWWzKI+
+BjXxgUWF9EmZUBrg68DSmI+9wuDFsjZ51BcqvJwxyfxtTaWhdoYqH/UQS+D1FP3/
+diZHHlzwVwPICzM9ooNTgbrcDzyxRkIVqsVwBq7EtzcvgYUyX53yG25Giy6YQaQ2
+ZtQ/VymwFL3XdUWV6B/hU4PVAFvO3qlOtdJ6TpE+nEWgcWjCv5g7RjXX
+=MuOY
+-----END PGP PUBLIC KEY BLOCK-----
+"""
+PRIVATE_KEY = """
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1.4.10 (GNU/Linux)
+
+lQcYBFC9+dkBEADNRfwV23TWEoGc/x0wWH1P7PlXt8MnC2Z1kKaKKmfnglVrpOiz
+iLWoiU58sfZ0L5vHkzXHXCBf6Eiy/EtUIvdiWAn+yASJ1mk5jZTBKO/WMAHD8wTO
+zpMsFmWyg3xc4DkmFa9KQ5EVU0o/nqPeyQxNMQN7px5pPwrJtJFmPxnxm+aDkPYx
+irDmz/4DeDNqXliazGJKw7efqBdlwTHkl9Akw2gwy178pmsKwHHEMOBOFFvX61AT
+huKqHYmlCGSliwbrJppTG7jc1/ls3itrK+CWTg4txREkSpEVmfcASvw/ZqLbjgfs
+d/INMwXnR9U81O8+7LT6yw/ca4ppcFoJD7/XJbkRiML6+bJ4Dakiy6i727BzV17g
+wI1zqNvm5rAhtALKfACha6YO43aJzairO4II1wxVHvRDHZn2IuKDDephQ3Ii7/vb
+hUOf6XCSmchkAcpKXUOvbxm1yfB1LRa64mMc2RcZxf4mW7KQkulBsdV5QG2276lv
+U2UUy2IutXcGP5nXC+f6sJJGJeEToKJ57yiO/VWJFjKN8SvP+7AYsQSqINUuEf6H
+T5gCPCraGMkTUTPXrREvu7NOohU78q6zZNaL3GW8ai7eSeANSuQ8Vzffx7Wd8Y7i
+Pw9sYj0SMFs1UgjbuL6pO5ueHh+qyumbtAq2K0Bci0kqOcU4E9fNtdiovQARAQAB
+AA/+JHtlL39G1wsH9R6UEfUQJGXR9MiIiwZoKcnRB2o8+DS+OLjg0JOh8XehtuCs
+E/8oGQKtQqa5bEIstX7IZoYmYFiUQi9LOzIblmp2vxOm+HKkxa4JszWci2/ZmC3t
+KtaA4adl9XVnshoQ7pijuCMUKB3naBEOAxd8s9d/JeReGIYkJErdrnVfNk5N71Ds
+FmH5Ll3XtEDvgBUQP3nkA6QFjpsaB94FHjL3gDwum/cxzj6pCglcvHOzEhfY0Ddb
+J967FozQTaf2JW3O+w3LOqtcKWpq87B7+O61tVidQPSSuzPjCtFF0D2LC9R/Hpky
+KTMQ6CaKja4MPhjwywd4QPcHGYSqjMpflvJqi+kYIt8psUK/YswWjnr3r4fbuqVY
+VhtiHvnBHQjz135lUqWvEz4hM3Xpnxydx7aRlv5NlevK8+YIO5oFbWbGNTWsPZI5
+jpoFBpSsnR1Q5tnvtNHauvoWV+XN2qAOBTG+/nEbDYH6Ak3aaE9jrpTdYh0CotYF
+q7csANsDy3JvkAzeU6WnYpsHHaAjqOGyiZGsLej1UcXPFMosE/aUo4WQhiS8Zx2c
+zOVKOi/X5vQ2GdNT9Qolz8AriwzsvFR+bxPzyd8V6ALwDsoXvwEYinYBKK8j0OPv
+OOihSR6HVsuP9NUZNU9ewiGzte/+/r6pNXHvR7wTQ8EWLcEIAN6Zyrb0bHZTIlxt
+VWur/Ht2mIZrBaO50qmM5RD3T5oXzWXi/pjLrIpBMfeZR9DWfwQwjYzwqi7pxtYx
+nJvbMuY505rfnMoYxb4J+cpRXV8MS7Dr1vjjLVUC9KiwSbM3gg6emfd2yuA93ihv
+Pe3mffzLIiQa4mRE3wtGcioC43nWuV2K2e1KjxeFg07JhrezA/1Cak505ab/tmvP
+4YmjR5c44+yL/YcQ3HdFgs4mV+nVbptRXvRcPpolJsgxPccGNdvHhsoR4gwXMS3F
+RRPD2z6x8xeN73Q4KH3bm01swQdwFBZbWVfmUGLxvN7leCdfs9+iFJyqHiCIB6Iv
+mQfp8F0IAOwSo8JhWN+V1dwML4EkIrM8wUb4yecNLkyR6TpPH/qXx4PxVMC+vy6x
+sCtjeHIwKE+9vqnlhd5zOYh7qYXEJtYwdeDDmDbL8oks1LFfd+FyAuZXY33DLwn0
+cRYsr2OEZmaajqUB3NVmj3H4uJBN9+paFHyFSXrH68K1Fk2o3n+RSf2EiX+eICwI
+L6rqoF5sSVUghBWdNegV7qfy4anwTQwrIMGjgU5S6PKW0Dr/3iO5z3qQpGPAj5OW
+ATqPWkDICLbObPxD5cJlyyNE2wCA9VVc6/1d6w4EVwSq9h3/WTpATEreXXxTGptd
+LNiTA1nmakBYNO2Iyo3djhaqBdWjk+EIAKtVEnJH9FAVwWOvaj1RoZMA5DnDMo7e
+SnhrCXl8AL7Z1WInEaybasTJXn1uQ8xY52Ua4b8cbuEKRKzw/70NesFRoMLYoHTO
+dyeszvhoDHberpGRTciVmpMu7Hyi33rM31K9epA4ib6QbbCHnxkWOZB+Bhgj1hJ8
+xb4RBYWiWpAYcg0+DAC3w9gfxQhtUlZPIbmbrBmrVkO2GVGUj8kH6k4UV6kUHEGY
+HQWQR0HcbKcXW81ZXCCD0l7ROuEWQtTe5Jw7dJ4/QFuqZnPutXVRNOZqpl6eRShw
+7X2/a29VXBpmHA95a88rSQsL+qm7Fb3prqRmuMCtrUZgFz7HLSTuUMR867QcTGVh
+cCBUZXN0IEtleSA8bGVhcEBsZWFwLnNlPokCNwQTAQgAIQUCUL352QIbAwULCQgH
+AwUVCgkICwUWAgMBAAIeAQIXgAAKCRAvRV4oJNGN30+xEACh9yLkZ4jqW0/wwyIM
+MI896MQf1tAwzMj16MJYUjrjNK4Bn57QaQW926HsxF8C/OjT0MTRhq7heYZJnnEo
+rj0rzpkJapUveTRkKeoTRtGGigqJYfkOTU7KRVwgJBXIfaKlI3tC3cX0j0H1fVKX
+hLxsj5pNSPRCVf2A5mePg44HtXe6oVWSJ8+EcdTa0shf03NhAtFaY0BbFGPSm9mA
+QUe4rxugwXPLctIyV4uweFo5BXFBCb4kKTBdnQi3aJwnoWLNT6rDdTe4/nhY0Hfo
+alTCYGLkhio77gBHwpTOjEMO/hZhcDMi4CvxMPw7bRxAwq4u+0j0pDhkiLcQs4U4
+Ou/fH+pia+1nF5h19cNVXIm+RX2fL0wxVYc/14AIAK3YT6PVev9XYEkogSj0P7Kb
+HKOruYpnToXJBERNJZwGL1U+ihPNUyroRf29t7u8flnXsOpCtBEIWAO8Muy5pWjV
+3O6zAUCfWetAieCQ7WrQVmdJDa7dlX3Qx1XagUzqZdAq2jVI1hOWDA2rKytnReSF
+/A97rmLaWZ8aoNCs8i4NLcy9Lbzi9QtornYGVCEmTTym0tM9L/mn7gAJ8dqUwt7n
+s24dibfElky4ZZeItD+D7OZGeh0FDuejvv2dXFqL1/pkHpGBZhEckg0fZ95NbgMC
+4pSZkZnqRpr2GwfB5aFfB6sIIJ0HGARQvfnZARAAtCP8Z9bm6KzIA7wbXx9LBIcJ
+1wQvOPf99s4nOrnQev9xH5PZ820qS9xUjrlyE2bGYAhz5Cmq56ENs7THErIdZHtQ
+uYEBprO+VFZjP50vtmCOL2PDl/xgv6J9r1Mp3KnR/m0esR+YceDW1qX07IkB5s+Q
+us80v5LmmxnWcikWmR7dt1kOyV/+M6Y6mwvfQ4x3D/QUpO7SfMCOG5DGA7hVUHU/
+Tuh8MihmMFFOLAEEQI+1wkxr1W09HaYCcB+EZqxLSaBwMeFoYPJie9dBNBgps39o
+6pDbjsO+or4JNuyoHvh8NNI5iY0IR2NMlW4mqCcHEazys2koxFTYK6YD95Vz0RkA
+K4BErCDk3lVR0PH4StmLU3gmPayIjvi9Dl9saPRyu4Xx2WVi+q6spl3ckn4c4f3+
+iD8hxVp74+wa5ew0fIXjIpMoHCar/nndsse4i8glCddINdiOPPmhI9Wi3nT+5Z2t
+9omPP2dEh0CzR+j1zvUpT3KtmhVICqhO+QP9BTJOwrp81NTlq9mbUyzTtVk/9dy3
+zoYbhKvY08k6LJ9FsQYySqtfJZ4cwl5WsOhALWwOwlMLA9wkz0eemgFxStyOylzl
+QKoIK7zHuU6XYOXa32KSPIWaLy+WgIG/u2ObWtdE3CXVIUuSt5BQFnv7XVNHJllD
+Az9VDEkOSYOiSEFVoUsAEQEAAQAP/1AagnZQZyzHDEgw4QELAspYHCWLXE5aZInX
+wTUJhK31IgIXNn9bJ0hFiSpQR2xeMs9oYtRuPOu0P8oOFMn4/z374fkjZy8QVY3e
+PlL+3EUeqYtkMwlGNmVw5a/NbNuNfm5Darb7pEfbYd1gPcni4MAYw7R2SG/57GbC
+9gucvspHIfOSfBNLBthDzmK8xEKe1yD2eimfc2T7IRYb6hmkYfeds5GsqvGI6mwI
+85h4uUHWRc5JOlhVM6yX8hSWx0L60Z3DZLChmc8maWnFXd7C8eQ6P1azJJbW71Ih
+7CoK0XW4LE82vlQurSRFgTwfl7wFYszW2bOzCuhHDDtYnwH86Nsu0DC78ZVRnvxn
+E8Ke/AJgrdhIOo4UAyR+aZD2+2mKd7/waOUTUrUtTzc7i8N3YXGi/EIaNReBXaq+
+ZNOp24BlFzRp+FCF/pptDW9HjPdiV09x0DgICmeZS4Gq/4vFFIahWctg52NGebT0
+Idxngjj+xDtLaZlLQoOz0n5ByjO/Wi0ANmMv1sMKCHhGvdaSws2/PbMR2r4caj8m
+KXpIgdinM/wUzHJ5pZyF2U/qejsRj8Kw8KH/tfX4JCLhiaP/mgeTuWGDHeZQERAT
+xPmRFHaLP9/ZhvGNh6okIYtrKjWTLGoXvKLHcrKNisBLSq+P2WeFrlme1vjvJMo/
+jPwLT5o9CADQmcbKZ+QQ1ZM9v99iDZol7SAMZX43JC019sx6GK0u6xouJBcLfeB4
+OXacTgmSYdTa9RM9fbfVpti01tJ84LV2SyL/VJq/enJF4XQPSynT/tFTn1PAor6o
+tEAAd8fjKdJ6LnD5wb92SPHfQfXqI84rFEO8rUNIE/1ErT6DYifDzVCbfD2KZdoF
+cOSp7TpD77sY1bs74ocBX5ejKtd+aH99D78bJSMM4pSDZsIEwnomkBHTziubPwJb
+OwnATy0LmSMAWOw5rKbsh5nfwCiUTM20xp0t5JeXd+wPVWbpWqI2EnkCEN+RJr9i
+7dp/ymDQ+Yt5wrsN3NwoyiexPOG91WQVCADdErHsnglVZZq9Z8Wx7KwecGCUurJ2
+H6lKudv5YOxPnAzqZS5HbpZd/nRTMZh2rdXCr5m2YOuewyYjvM757AkmUpM09zJX
+MQ1S67/UX2y8/74TcRF97Ncx9HeELs92innBRXoFitnNguvcO6Esx4BTe1OdU6qR
+ER3zAmVf22Le9ciXbu24DN4mleOH+OmBx7X2PqJSYW9GAMTsRB081R6EWKH7romQ
+waxFrZ4DJzZ9ltyosEJn5F32StyLrFxpcrdLUoEaclZCv2qka7sZvi0EvovDVEBU
+e10jOx9AOwf8Gj2ufhquQ6qgVYCzbP+YrodtkFrXRS3IsljIchj1M2ffB/0bfoUs
+rtER9pLvYzCjBPg8IfGLw0o754Qbhh/ReplCRTusP/fQMybvCvfxreS3oyEriu/G
+GufRomjewZ8EMHDIgUsLcYo2UHZsfF7tcazgxMGmMvazp4r8vpgrvW/8fIN/6Adu
+tF+WjWDTvJLFJCe6O+BFJOWrssNrrra1zGtLC1s8s+Wfpe+bGPL5zpHeebGTwH1U
+22eqgJArlEKxrfarz7W5+uHZJHSjF/K9ZvunLGD0n9GOPMpji3UO3zeM8IYoWn7E
+/EWK1XbjnssNemeeTZ+sDh+qrD7BOi+vCX1IyBxbfqnQfJZvmcPWpruy1UsO+aIC
+0GY8Jr3OL69dDQ21jueJAh8EGAEIAAkFAlC9+dkCGwwACgkQL0VeKCTRjd9HCw/+
+LQSVgLLF4ulYlPCjWIIuQwrPbJfWUVVr2dPUFVM85DCv8gBzk5c121snXh9Swovm
+laBbw6ate3BmbXLh64jVE9Za5sbTWi7PCcbO/bpRy4d6oLmitmNw6cq0vjTLxUYy
+bwuiJxWREkfxuU85EKdouN062YDevH+/YResmlJrcCE7LRlJFeRlKsrrwBU3BqYd
+GgFJjKjQC1peeQ9fj62Y7xfwE9+PXbkiWO5u/Bk8hb1VZH1SoIRU98NHVcp6BVvp
+VK0jLAXuSauSczULmpRjbyt1lhaAqivDTWEEZXiNNbRyp17c3nVdPWOcgBr42hdQ
+z25CgZgyLCsvu82wuXLKJblrIPJX3Yf+si6KqEWBsmwdOWybsjygaF5HvzgFqAAD
+U0goPWoQ71PorP2XOUNp5ZLkBQp5etvtkksjVNMIhnHn8PGMuoxO39EUGlWj2B5l
+Cu8tSosAzB1pS8NcLZzoNoI9dOHrmgJmP+GrOUkcf5GhNZbMoj4GNfGBRYX0SZlQ
+GuDrwNKYj73C4MWyNnnUFyq8nDHJ/G1NpaF2hiof9RBL4PUU/f92JkceXPBXA8gL
+Mz2ig1OButwPPLFGQhWqxXAGrsS3Ny+BhTJfnfIbbkaLLphBpDZm1D9XKbAUvdd1
+RZXoH+FTg9UAW87eqU610npOkT6cRaBxaMK/mDtGNdc=
+=JTFu
+-----END PGP PRIVATE KEY BLOCK-----
+"""
+
+# key 7FEE575A: public key "anotheruser <anotheruser@leap.se>"
+PUBLIC_KEY_2 = """
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.10 (GNU/Linux)
+
+mI0EUYwJXgEEAMbTKHuPJ5/Gk34l9Z06f+0WCXTDXdte1UBoDtZ1erAbudgC4MOR
+gquKqoj3Hhw0/ILqJ88GcOJmKK/bEoIAuKaqlzDF7UAYpOsPZZYmtRfPC2pTCnXq
+Z1vdeqLwTbUspqXflkCkFtfhGKMq5rH8GV5a3tXZkRWZhdNwhVXZagC3ABEBAAG0
+IWFub3RoZXJ1c2VyIDxhbm90aGVydXNlckBsZWFwLnNlPoi4BBMBAgAiBQJRjAle
+AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRB/nfpof+5XWotuA/4tLN4E
+gUr7IfLy2HkHAxzw7A4rqfMN92DIM9mZrDGaWRrOn3aVF7VU1UG7MDkHfPvp/cFw
+ezoCw4s4IoHVc/pVlOkcHSyt4/Rfh248tYEJmFCJXGHpkK83VIKYJAithNccJ6Q4
+JE/o06Mtf4uh/cA1HUL4a4ceqUhtpLJULLeKo7iNBFGMCV4BBADsyQI7GR0wSAxz
+VayLjuPzgT+bjbFeymIhjuxKIEwnIKwYkovztW+4bbOcQs785k3Lp6RzvigTpQQt
+Z/hwcLOqZbZw8t/24+D+Pq9mMP2uUvCFFqLlVvA6D3vKSQ/XNN+YB919WQ04jh63
+yuRe94WenT1RJd6xU1aaUff4rKizuQARAQABiJ8EGAECAAkFAlGMCV4CGwwACgkQ
+f536aH/uV1rPZQQAqCzRysOlu8ez7PuiBD4SebgRqWlxa1TF1ujzfLmuPivROZ2X
+Kw5aQstxgGSjoB7tac49s0huh4X8XK+BtJBfU84JS8Jc2satlfwoyZ35LH6sDZck
+I+RS/3we6zpMfHs3vvp9xgca6ZupQxivGtxlJs294TpJorx+mFFqbV17AzQ=
+=Thdu
+-----END PGP PUBLIC KEY BLOCK-----
+"""
+
+PRIVATE_KEY_2 = """
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1.4.10 (GNU/Linux)
+
+lQHYBFGMCV4BBADG0yh7jyefxpN+JfWdOn/tFgl0w13bXtVAaA7WdXqwG7nYAuDD
+kYKriqqI9x4cNPyC6ifPBnDiZiiv2xKCALimqpcwxe1AGKTrD2WWJrUXzwtqUwp1
+6mdb3Xqi8E21LKal35ZApBbX4RijKuax/BleWt7V2ZEVmYXTcIVV2WoAtwARAQAB
+AAP7BLuSAx7tOohnimEs74ks8l/L6dOcsFQZj2bqs4AoY3jFe7bV0tHr4llypb/8
+H3/DYvpf6DWnCjyUS1tTnXSW8JXtx01BUKaAufSmMNg9blKV6GGHlT/Whe9uVyks
+7XHk/+9mebVMNJ/kNlqq2k+uWqJohzC8WWLRK+d1tBeqDsECANZmzltPaqUsGV5X
+C3zszE3tUBgptV/mKnBtopKi+VH+t7K6fudGcG+bAcZDUoH/QVde52mIIjjIdLje
+uajJuHUCAO1mqh+vPoGv4eBLV7iBo3XrunyGXiys4a39eomhxTy3YktQanjjx+ty
+GltAGCs5PbWGO6/IRjjvd46wh53kzvsCAO0J97gsWhzLuFnkxFAJSPk7RRlyl7lI
+1XS/x0Og6j9XHCyY1OYkfBm0to3UlCfkgirzCYlTYObCofzdKFIPDmSqHbQhYW5v
+dGhlcnVzZXIgPGFub3RoZXJ1c2VyQGxlYXAuc2U+iLgEEwECACIFAlGMCV4CGwMG
+CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEH+d+mh/7ldai24D/i0s3gSBSvsh
+8vLYeQcDHPDsDiup8w33YMgz2ZmsMZpZGs6fdpUXtVTVQbswOQd8++n9wXB7OgLD
+izgigdVz+lWU6RwdLK3j9F+Hbjy1gQmYUIlcYemQrzdUgpgkCK2E1xwnpDgkT+jT
+oy1/i6H9wDUdQvhrhx6pSG2kslQst4qjnQHYBFGMCV4BBADsyQI7GR0wSAxzVayL
+juPzgT+bjbFeymIhjuxKIEwnIKwYkovztW+4bbOcQs785k3Lp6RzvigTpQQtZ/hw
+cLOqZbZw8t/24+D+Pq9mMP2uUvCFFqLlVvA6D3vKSQ/XNN+YB919WQ04jh63yuRe
+94WenT1RJd6xU1aaUff4rKizuQARAQABAAP9EyElqJ3dq3EErXwwT4mMnbd1SrVC
+rUJrNWQZL59mm5oigS00uIyR0SvusOr+UzTtd8ysRuwHy5d/LAZsbjQStaOMBILx
+77TJveOel0a1QK0YSMF2ywZMCKvquvjli4hAtWYz/EwfuzQN3t23jc5ny+GqmqD2
+3FUxLJosFUfLNmECAO9KhVmJi+L9dswIs+2Dkjd1eiRQzNOEVffvYkGYZyKxNiXF
+UA5kvyZcB4iAN9sWCybE4WHZ9jd4myGB0MPDGxkCAP1RsXJbbuD6zS7BXe5gwunO
+2q4q7ptdSl/sJYQuTe1KNP5d/uGsvlcFfsYjpsopasPjFBIncc/2QThMKlhoEaEB
+/0mVAxpT6SrEvUbJ18z7kna24SgMPr3OnPMxPGfvNLJY/Xv/A17YfoqjmByCvsKE
+JCDjopXtmbcrZyoEZbEht9mko4ifBBgBAgAJBQJRjAleAhsMAAoJEH+d+mh/7lda
+z2UEAKgs0crDpbvHs+z7ogQ+Enm4EalpcWtUxdbo83y5rj4r0TmdlysOWkLLcYBk
+o6Ae7WnOPbNIboeF/FyvgbSQX1POCUvCXNrGrZX8KMmd+Sx+rA2XJCPkUv98Hus6
+THx7N776fcYHGumbqUMYrxrcZSbNveE6SaK8fphRam1dewM0
+=a5gs
+-----END PGP PRIVATE KEY BLOCK-----
+"""
+import unittest
+if __name__ == "__main__":
+ unittest.main()
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..5345f11a
--- /dev/null
+++ b/src/leap/platform_init/initializers.py
@@ -0,0 +1,383 @@
+# -*- 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
+
+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, "
+ "LEAP 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
+ install_missing_fun(
+ # XXX maybe move constants to fun
+ UPDOWN_BADEXEC_MSG,
+ UPDOWN_NOTFOUND_MSG)
+
+ 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("LEAPClient 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
+ """
+ # We expect to execute this from some way of bundle, since
+ # the up/down scripts should be put in place by the installer.
+ installer_path = os.path.join(
+ os.getcwd(),
+ "..",
+ "Resources",
+ "openvpn")
+ launcher = vpnlaunchers.DarwinVPNLauncher
+
+ # TODO should change osascript by use of the proper
+ # os authorization api.
+ if os.path.isdir(installer_path):
+ fd, tempscript = tempfile.mkstemp(prefix="leap_installer-")
+ try:
+ cmd = launcher.OSASCRIPT_BIN
+ 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)
+
+ osascript = launcher.OSX_ASADMIN % ("/bin/sh %s" % (tempscript,),)
+ cmdline = ["%s -e '%s'" % (cmd, osascript)]
+ ret = subprocess.call(
+ cmdline, stdout=subprocess.PIPE,
+ shell=True)
+ assert([ret]) # happy flakes
+ 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,))
+
+
+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("LEAPClient 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.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
+ """
+ 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-")
+ try:
+ pkexec = first(launcher.maybe_pkexec())
+ 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)
+ cmdline = ["%s %s" % (pkexec, tempscript)]
+ ret = subprocess.call(
+ cmdline, stdout=subprocess.PIPE,
+ shell=True)
+ assert([ret]) # happy flakes
+ 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,))
+
+
+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..c40c31d0
--- /dev/null
+++ b/src/leap/platform_init/locks.py
@@ -0,0 +1,312 @@
+# -*- 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:
+ import glob
+ import shutil
+
+ from tempfile import gettempdir
+
+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:
+
+ 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 release_lock(self):
+ """
+ Releases the pidfile dir for this process, by removing it.
+ """
+ try:
+ shutil.rmtree(self.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
+
+ @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 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 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
+
+ :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()
+ signal_event(proto.RAISE_WINDOW)
+ 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/baseapp/__init__.py b/src/leap/provider/__init__.py
index e69de29b..e69de29b 100644
--- a/src/leap/baseapp/__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..fc4aa416
--- /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"] # for 0.2.2 release
+
+
+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..633d818d
--- /dev/null
+++ b/src/leap/services/abstractbootstrapper.py
@@ -0,0 +1,160 @@
+# -*- 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))
+ """
+ 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/eip/__init__.py b/src/leap/services/eip/__init__.py
index e69de29b..e69de29b 100644
--- a/src/leap/eip/__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..b2af0aea
--- /dev/null
+++ b/src/leap/services/eip/eipbootstrapper.py
@@ -0,0 +1,178 @@
+# -*- 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.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(),))
+
+ self._eip_config = EIPConfig()
+
+ 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(),
+ self._provider_config.get_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)
+ 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)
+ 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..9e3a9b29
--- /dev/null
+++ b/src/leap/services/eip/eipconfig.py
@@ -0,0 +1,260 @@
+# -*- 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 eipservice_config_spec
+
+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)
+
+ def _get_spec(self):
+ """
+ Returns the spec object for the specific configuration
+ """
+ return eipservice_config_spec
+
+ 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()
+
+ 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..94ba674f
--- /dev/null
+++ b/src/leap/services/eip/eipspec.py
@@ -0,0 +1,65 @@
+# -*- 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/>.
+
+eipservice_config_spec = {
+ '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}
+ }
+ }
+}
diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py
new file mode 100644
index 00000000..754d0643
--- /dev/null
+++ b/src/leap/services/eip/providerbootstrapper.py
@@ -0,0 +1,311 @@
+# -*- 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
+from leap.config.providerconfig import ProviderConfig
+from leap.util.request_helpers import get_content
+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 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)
+ 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 = {}
+ mtime = get_mtime(os.path.join(ProviderConfig()
+ .get_path_prefix(),
+ "leap",
+ "providers",
+ self._domain,
+ "provider.json"))
+ if self._download_if_needed and mtime:
+ headers['if-modified-since'] = mtime
+
+ res = self._session.get("https://%s/%s" % (self._domain,
+ "provider.json"),
+ headers=headers,
+ verify=not self._bypass_checks)
+ res.raise_for_status()
+
+ # 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 = 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)
+ 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(":")
+ leap_assert(len(parts) == 2, "Wrong fingerprint format")
+
+ 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)
+ leap_assert(digest == fingerprint,
+ "Downloaded certificate has a different fingerprint!")
+
+ 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())
+ 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/tests/__init__.py b/src/leap/services/eip/tests/__init__.py
index e69de29b..e69de29b 100644
--- a/src/leap/eip/tests/__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..8b746b78
--- /dev/null
+++ b/src/leap/services/eip/tests/test_eipconfig.py
@@ -0,0 +1,313 @@
+# -*- 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):
+ """
+ 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
+ :fromfile type: bool
+ :param data: sets the data to be used to load in the EIPConfig object
+ :data type: dict (valid json)
+ :rtype: EIPConfig
+ """
+ config = EIPConfig()
+
+ 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)
+
+
+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..cd740793
--- /dev/null
+++ b/src/leap/services/eip/tests/test_providerbootstrapper.py
@@ -0,0 +1,504 @@
+# -*- 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.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(AssertionError):
+ 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(AssertionError):
+ 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_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()))
+
+ self.pb._download_provider_info()
+ # we check that it doesn't do anything with the provider
+ # config, because it's new enough
+ self.assertFalse(ProviderConfig.load.called)
+ self.assertFalse(ProviderConfig.save.called)
+
+ 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))
+
+ self.pb._download_provider_info()
+ self.assertTrue(ProviderConfig.load.called)
+ self.assertTrue(ProviderConfig.save.called)
+
+ 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 self.assertRaises(UnsupportedProviderAPI):
+ self.pb._download_provider_info()
+
+ 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()
+
+ self.pb._download_provider_info()
+
+ def test_check_api_certificate_skips(self):
+ self.pb._provider_config = ProviderConfig()
+ self.pb._provider_config.get_api_uri = mock.MagicMock(
+ return_value="api.uri")
+ self.pb._provider_config.get_ca_cert_path = mock.MagicMock(
+ return_value="/cert/path")
+ 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..570a7893
--- /dev/null
+++ b/src/leap/services/eip/vpnlaunchers.py
@@ -0,0 +1,774 @@
+# -*- 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
+
+logger = logging.getLogger(__name__)
+
+
+class VPNLauncherException(Exception):
+ pass
+
+
+class OpenVPNNotFoundException(VPNLauncherException):
+ pass
+
+
+class EIPNoPolkitAuthAgentAvailable(VPNLauncherException):
+ pass
+
+
+class EIPNoPkexecAvailable(VPNLauncherException):
+ pass
+
+
+class VPNLauncher:
+ """
+ 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'
+ 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)
+
+ POLKIT_BASE = "/usr/share/polkit-1/actions"
+ POLKIT_FILE = "net.openvpn.gui.leap.policy"
+ POLKIT_PATH = "%s/%s" % (POLKIT_BASE, POLKIT_FILE)
+
+ UPDOWN_FILES = (UP_DOWN_PATH,)
+ OTHER_FILES = (POLKIT_PATH,)
+
+ @classmethod
+ def cmd_for_missing_scripts(kls, frompath):
+ """
+ Returns a command that can copy the missing scripts.
+ :rtype: str
+ """
+ to = kls.SYSTEM_CONFIG
+ cmd = "#!/bin/sh\nset -e\nmkdir -p %s\ncp %s/%s %s\ncp %s/%s %s" % (
+ to,
+ frompath, kls.UP_DOWN_FILE, to,
+ frompath, kls.POLKIT_FILE, 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"):
+ """
+ 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
+
+ :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)
+
+ # TODO: handle verbosity
+
+ 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',
+ '--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]
+
+ 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,
+ '--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 magic translate for this string
+ SUDO_MSG = ("LEAP needs administrative privileges to run "
+ "Encrypted Internet.")
+
+ INSTALL_PATH = "/Applications/LEAP\ Client.app"
+ OPENVPN_BIN = 'openvpn.leap'
+ OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,)
+
+ 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)
+
+ @classmethod
+ def cmd_for_missing_scripts(kls, frompath):
+ """
+ Returns a command that can copy the missing scripts.
+ :rtype: str
+ """
+ to = kls.OPENVPN_PATH
+ cmd = "#!/bin/sh\nmkdir -p %s\ncp \"%s/\"* %s" % (to, frompath, to)
+ return cmd
+
+ def get_cocoasudo_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 = os.path.abspath(os.path.join(
+ os.getcwd(),
+ "../../../Resources/leap-client.tiff"))
+ 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_vpn_command(self, eipconfig=None, providerconfig=None,
+ socket_host=None, socket_port="unix"):
+ """
+ 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
+
+ :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 = [openvpn]
+
+ # TODO: handle verbosity
+
+ 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',
+ '--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()
+ 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', self.UP_SCRIPT,
+ ]
+
+ if _has_updown_scripts(self.DOWN_SCRIPT):
+ args += [
+ '--down', self.DOWN_SCRIPT]
+
+ # should have the down script too
+ if _has_updown_scripts(self.OPENVPN_DOWN_PLUGIN):
+ args += [
+ '--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_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!
+
+ def get_vpn_command(self, eipconfig=None, providerconfig=None,
+ socket_host=None, socket_port="9876"):
+ """
+ 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
+
+ :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 = []
+
+ # TODO: handle verbosity
+
+ 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',
+ '--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]
+
+ 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()
+ 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..0ec56ae7
--- /dev/null
+++ b/src/leap/services/eip/vpnprocess.py
@@ -0,0 +1,718 @@
+# -*- 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
+
+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 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
+
+ def __init__(self):
+ """
+ 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()
+
+ @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
+ """
+ kwargs['qtsigs'] = self.qtsigs
+
+ # start the main vpn subprocess
+ vpnproc = VPNProcess(*args, **kwargs)
+
+ # XXX Should stop if already running -------
+ if vpnproc.get_openvpn_process():
+ logger.warning("Another vpnprocess is 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.
+ 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')
+ self._close_management_socket(announce=False)
+ 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')
+
+ 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):
+ """
+ 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
+ """
+ # TODO decide about putting a max_lim to retries and signaling
+ # an error.
+ 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 = os.path.split(self._socket_host)[0] # XXX use `first`
+ if os.path.isdir(tempfolder):
+ try:
+ shutil.rmtree(tempfolder)
+ except OSError:
+ logger.error('could not delete tmpfolder %s' % tempfolder)
+
+ # ---------------------------------------------------
+ # XXX old methods, not adapted to twisted process yet
+
+ 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)
+ if "openvpn" in ' '.join(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.
+
+ :return: True if stopped, False otherwise
+ """
+ # TODO cleanup this
+ process = self._get_openvpn_process()
+ if process:
+ 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:
+ 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)
+ self._send_command("signal SIGTERM")
+ self._tn.close()
+ self._tn = None
+ #self._disconnect_management()
+ except Exception as e:
+ logger.warning("Problem trying to terminate OpenVPN: %r"
+ % (e,))
+
+ process = self._get_openvpn_process()
+ if process is None:
+ logger.warning("Unabled to terminate OpenVPN")
+ return True
+ else:
+ return False
+ return True
+
+
+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):
+ """
+ :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
+ """
+ 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
+
+ # 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()
+
+ 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.
+ """
+ cmd = self._launcher.get_vpn_command(
+ eipconfig=self._eipconfig,
+ providerconfig=self._providerconfig,
+ socket_host=self._socket_host,
+ socket_port=self._socket_port)
+ 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/gui/tests/__init__.py b/src/leap/services/mail/__init__.py
index e69de29b..e69de29b 100644
--- a/src/leap/gui/tests/__init__.py
+++ b/src/leap/services/mail/__init__.py
diff --git a/src/leap/services/mail/smtpbootstrapper.py b/src/leap/services/mail/smtpbootstrapper.py
new file mode 100644
index 00000000..e8af5349
--- /dev/null
+++ b/src/leap/services/mail/smtpbootstrapper.py
@@ -0,0 +1,135 @@
+# -*- 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
+
+ # there is some confusion with this uri,
+ config_uri = "%s/%s/config/smtp-service.json" % (
+ self._provider_config.get_api_uri(),
+ self._provider_config.get_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()
+
+ # 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..30371005
--- /dev/null
+++ b/src/leap/services/mail/smtpconfig.py
@@ -0,0 +1,47 @@
+# -*- 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 smtp_config_spec
+
+logger = logging.getLogger(__name__)
+
+
+class SMTPConfig(BaseConfig):
+ """
+ SMTP configuration abstraction class
+ """
+
+ def __init__(self):
+ BaseConfig.__init__(self)
+
+ def _get_spec(self):
+ """
+ Returns the spec object for the specific configuration
+ """
+ return smtp_config_spec
+
+ 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..270dfb76
--- /dev/null
+++ b/src/leap/services/mail/smtpspec.py
@@ -0,0 +1,51 @@
+# -*- 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/>.
+
+smtp_config_spec = {
+ '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": {
+
+ }
+ }
+ }
+ }
+}
diff --git a/src/leap/testing/__init__.py b/src/leap/services/soledad/__init__.py
index e69de29b..e69de29b 100644
--- a/src/leap/testing/__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..2635a7e6
--- /dev/null
+++ b/src/leap/services/soledad/soledadbootstrapper.py
@@ -0,0 +1,220 @@
+# -*- 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 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 = ""
+
+ @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: use the proper URL
+ #server_url = 'https://mole.dev.bitmask.net:2424/user-%s' % (uuid,)
+ server_url = 'https://gadwall.dev.bitmask.net:1111/user-%s' % (uuid,)
+ # server_url = self._soledad_config.get_hosts(...)
+
+ cert_file = self._provider_config.get_ca_cert_path()
+
+ self._soledad = Soledad(uuid,
+ self._password.encode("utf-8"),
+ secrets_path,
+ local_db_path,
+ server_url,
+ cert_file,
+ srp_auth.get_token())
+ self._soledad.sync()
+
+ 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
+
+ # there is some confusion with this uri,
+ config_uri = "%s/%s/config/soledad-service.json" % (
+ self._provider_config.get_api_uri(),
+ self._provider_config.get_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()
+
+ # Not modified
+ if res.status_code == 304:
+ logger.debug("Soledad definition has not been modified")
+ 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)
+ self._keymanager = KeyManager(
+ address,
+ "https://%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())
+ 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)
+ logger.debug("Key generated successfully.")
+
+ def run_soledad_setup_checks(self,
+ provider_config,
+ user,
+ password,
+ download_if_needed=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
+ """
+ leap_assert_type(provider_config, ProviderConfig)
+
+ self._provider_config = provider_config
+ self._download_if_needed = download_if_needed
+ self._user = user
+ self._password = password
+
+ 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..80a82d11
--- /dev/null
+++ b/src/leap/services/soledad/soledadconfig.py
@@ -0,0 +1,47 @@
+# -*- 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 soledad_config_spec
+
+logger = logging.getLogger(__name__)
+
+
+class SoledadConfig(BaseConfig):
+ """
+ Soledad configuration abstraction class
+ """
+
+ def __init__(self):
+ BaseConfig.__init__(self)
+
+ def _get_spec(self):
+ """
+ Returns the spec object for the specific configuration
+ """
+ return soledad_config_spec
+
+ 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..8233d6a0
--- /dev/null
+++ b/src/leap/services/soledad/soledadspec.py
@@ -0,0 +1,57 @@
+# -*- 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/>.
+
+soledad_config_spec = {
+ '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"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/__init__.py b/src/leap/services/tests/__init__.py
index e69de29b..e69de29b 100644
--- a/tests/__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..ef08fcc6
--- /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("LEAP Client 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..5ceaede5 100644
--- a/src/leap/util/__init__.py
+++ b/src/leap/util/__init__.py
@@ -1,9 +1,49 @@
-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
+"""
+__version__ = "unknown"
try:
- import pygeoip
- HAS_GEOIP = True
+ from leap._version import get_versions
+ __version__ = get_versions()['version']
+ del get_versions
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
+
+__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__)
+
+
+def first(things):
+ """
+ Return the head of a collection.
+ """
+ try:
+ return things[0]
+ except TypeError:
+ return None
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/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..b815d385
--- /dev/null
+++ b/src/leap/util/keyring_helpers.py
@@ -0,0 +1,35 @@
+# -*- 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/>.
+
+"""
+
+"""
+
+import keyring
+
+OBSOLETE_KEYRINGS = [
+ keyring.backends.file.EncryptedKeyring,
+ keyring.backends.file.PlaintextKeyring
+]
+
+
+def has_keyring():
+ """
+
+ """
+ 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..8300e4d8 100644
--- a/src/leap/util/leap_argparse.py
+++ b/src/leap/util/leap_argparse.py
@@ -1,9 +1,26 @@
+# -*- 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
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"
@@ -12,6 +29,8 @@ Launches the LEAP Client""", epilog=epilog)
parser.add_argument('-d', '--debug', action="store_true",
help=("Launches client in debug mode, writing debug"
"info to stdout"))
+ parser.add_argument('--danger', action="store_true",
+ help=("Bypasses the certificate check for bootstrap"))
parser.add_argument('-l', '--logfile', metavar="LOG FILE", nargs='?',
action="store", dest="log_file",
#type=argparse.FileType('w'),
@@ -20,6 +39,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 the client 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..271096d3
--- /dev/null
+++ b/src/leap/util/leap_log_handler.py
@@ -0,0 +1,149 @@
+# -*- 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.
+ """
+ html_style = {
+ 'DEBUG': "color: blue",
+ 'INFO': "color: black",
+ 'WARNING': "color: black; background: yellow;",
+ 'ERROR': "color: red",
+ 'CRITICAL': "color: red; font-weight: bold;"
+ }
+
+ style_open = "<span style='" + html_style[logging_level] + "'>"
+ style_close = "</span>"
+ time = "%(asctime)s"
+ name = style_open + "%(name)s"
+ level = "%(levelname)s"
+ message = "%(message)s" + style_close
+ format_attrs = [time, name, level, message]
+ log_format = ' - '.join(format_attrs)
+ 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 70671124..00000000
--- a/src/leap/util/polkit.py
+++ /dev/null
@@ -1,26 +0,0 @@
-import logging
-
-import sh
-from sh import grep
-from sh import ps
-
-logger = logging.getLogger(__name__)
-
-
-def run_polkit_auth_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
- """
- try:
- grep(ps('aux'), '[p]olkit-gnome-authentication-agent-1')
- except sh.ErrorReturnCode_1:
- logger.debug('polkit auth agent not found, trying to launch it...')
- run_polkit_auth_agent()
diff --git a/src/leap/util/privilege_policies.py b/src/leap/util/privilege_policies.py
new file mode 100644
index 00000000..10224bcd
--- /dev/null
+++ b/src/leap/util/privilege_policies.py
@@ -0,0 +1,82 @@
+# -*- 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__)
+
+
+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()
+
+
+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")
+
+ 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)
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..e06dabb8
--- /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.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/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_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_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/test_qt_environment.py b/tests/test_qt_environment.py
deleted file mode 100644
index e90d527f..00000000
--- a/tests/test_qt_environment.py
+++ /dev/null
@@ -1,43 +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)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tox.ini b/tox.ini
index a9107c71..e041515d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,9 +3,9 @@ envlist = py26,py27
[testenv]
deps = -r{toxinidir}/pkg/requirements.pip
- -r{toxinidir}/pkg/test-requirements.pip
+ -r{toxinidir}/pkg/requirements-testing.pip
sitepackages = True
-commands = xvfb-run nosetests leap --exclude-dir=src/leap/soledad
+commands = xvfb-run nosetests leap --first-package-wins --exclude=soledad*
[testenv:pep8]
deps = pep8==1.1