summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkali <kali@leap.se>2012-11-14 00:38:20 +0900
committerkali <kali@leap.se>2012-11-14 00:38:20 +0900
commit21875404282522a9c83bfb9c85d6a24fa59d20f8 (patch)
treeae0409bd742ce3a6f994ae9bb31fc5ab7225f1c6
parentf6e900f024074435349eb778a2d89baed55e1e6c (diff)
parentd24c7328fa845737dbb83d512e4b3f287634c4cc (diff)
Merge branch 'feature/generic-wizard' into develop
The generic wizard (big) branch is now stabilised. A bunch of refactors have gone together with this topic branch: - client does not have any info included for default service providers. - user has to run the first-run wizard and manually entry domain for sample provider. - remove all remains of the older branding strategy for default provider. - srp registration + authentication are integrated with the signup process.
-rw-r--r--MANIFEST.in1
-rw-r--r--README.rst29
-rw-r--r--data/branding/cacert.pem24
-rw-r--r--data/images/checked.pngbin0 -> 1375 bytes
-rw-r--r--data/resources/mainwindow.qrc1
-rw-r--r--pkg/branding/config.py8
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py10
-rw-r--r--src/leap/app.py3
-rw-r--r--src/leap/base/auth.py376
-rw-r--r--src/leap/base/checks.py17
-rw-r--r--src/leap/base/config.py10
-rw-r--r--src/leap/base/connection.py10
-rw-r--r--src/leap/base/exceptions.py5
-rw-r--r--src/leap/base/network.py2
-rw-r--r--src/leap/base/providers.py14
-rw-r--r--src/leap/base/tests/__init__.py0
-rw-r--r--src/leap/base/tests/test_checks.py7
-rw-r--r--src/leap/base/tests/test_providers.py6
-rw-r--r--src/leap/baseapp/eip.py14
-rw-r--r--src/leap/baseapp/leap_app.py23
-rw-r--r--src/leap/baseapp/mainwindow.py113
-rw-r--r--src/leap/baseapp/systray.py38
-rw-r--r--src/leap/crypto/certs.py71
-rw-r--r--src/leap/crypto/leapkeyring.py1
-rw-r--r--src/leap/eip/checks.py198
-rw-r--r--src/leap/eip/config.py49
-rw-r--r--src/leap/eip/eipconnection.py42
-rw-r--r--src/leap/eip/exceptions.py11
-rw-r--r--src/leap/eip/openvpnconnection.py101
-rw-r--r--src/leap/eip/specs.py25
-rw-r--r--src/leap/eip/tests/data.py9
-rw-r--r--src/leap/eip/tests/test_checks.py37
-rw-r--r--src/leap/eip/tests/test_config.py19
-rw-r--r--src/leap/eip/tests/test_eipconnection.py12
-rw-r--r--src/leap/eip/tests/test_openvpnconnection.py10
-rw-r--r--src/leap/gui/__init__.py10
-rw-r--r--src/leap/gui/constants.py13
-rw-r--r--src/leap/gui/firstrun/__init__.py29
-rw-r--r--src/leap/gui/firstrun/connect.py231
-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.py90
-rw-r--r--src/leap/gui/firstrun/login.py192
-rw-r--r--src/leap/gui/firstrun/mixins.py18
-rw-r--r--src/leap/gui/firstrun/providerinfo.py230
-rw-r--r--src/leap/gui/firstrun/providerselect.py211
-rw-r--r--src/leap/gui/firstrun/providersetup.py171
-rw-r--r--src/leap/gui/firstrun/register.py188
-rw-r--r--src/leap/gui/firstrun/regvalidation.py265
-rwxr-xr-xsrc/leap/gui/firstrun/tests/integration/fake_provider.py295
-rwxr-xr-xsrc/leap/gui/firstrun/wizard.py277
-rwxr-xr-xsrc/leap/gui/firstrunwizard.py507
-rw-r--r--src/leap/gui/mainwindow_rc.py97
-rw-r--r--src/leap/gui/progress.py287
-rw-r--r--src/leap/gui/styles.py4
-rw-r--r--src/leap/gui/test_mainwindow_rc.py9
-rw-r--r--src/leap/gui/tests/integration/fake_user_signup.py6
-rw-r--r--src/leap/gui/threads.py15
-rw-r--r--src/leap/gui/utils.py10
-rw-r--r--src/leap/util/dicts.py268
-rw-r--r--src/leap/util/web.py39
62 files changed, 4025 insertions, 803 deletions
diff --git a/MANIFEST.in b/MANIFEST.in
index 685cee16..d7a5201e 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,4 +2,3 @@ include pkg/*
include pkg/branding/*
include docs/*
include versioneer.py
-include src/leap/certs/*.pem
diff --git a/README.rst b/README.rst
index d1f91c4b..cc30c544 100644
--- a/README.rst
+++ b/README.rst
@@ -20,17 +20,21 @@ Python packages are listed in ``pkg/requirements.pip`` and ``pkg/test-requiremen
Debian systems
--------------
+# XXX TODO: move to packaging doc.
* python-qt4
-* python-gnutls == 1.1.9
-* python-keyring
* python-crypto
* python setuptools
* python-nose, python-mock, python-coverage (if you want to run tests)
+Note: these two need a version that is not found in the current debian stable or in ubuntu 12.04.
+
+* python-gnutls == 1.1.9
+* python-keyring
+
Under a debian-based system, you can run::
- # apt-get install openvpn python-qt4 python-keyring python-crypto
+ # apt-get install openvpn python-qt4 python-crypto
For testing:
@@ -43,7 +47,9 @@ For _building_ the package you will need to install also::
Install python dependencies with pip
-------------------------------------
-Use pip (preferrable inside a virtualenv) to install all the required python packages::
+# XXX TODO: move to developers doc.
+
+Use pip (preferrable inside a virtualenv) to install the required python packages::
# apt-get install python-pip python-dev libgnutls-dev
% pip install -r pkg/requirements.pip
@@ -54,11 +60,6 @@ Install leap-client
After getting the source and installing all the dependencies, proceed to install ``leap-client`` package:
-# need to run this if you are installing from the git source tree
-# not needed if installing from a tarball::
-
- python setup.py branding
-
# run this if you have installed previous versions before::
python setup.py clean
@@ -71,18 +72,17 @@ And finally, build and install leap-client::
Running the App
-----------------
-If you're running a branded build, the script name will have a infix that
-depends on your build flavor. Look for it in ``/usr/local/bin``::
+After a successful installation, there should be a launcher called leap-client somewhere in your path::
- % leap-springbok-client
+ % leap-client
In order to run the client in debug mode::
- % leap-springbok-client --debug --logfile /tmp/leap.log
+ % leap-client --debug --logfile /tmp/leap.log
To see all the available command line options::
- % leap-springbok-client --help
+ % leap-client --help
Development
@@ -120,7 +120,6 @@ Some steps need to be run when setting a development environment for the first t
(leap_client)% git checkout develop
(leap_client)% pkg/postmkvenv.sh
- (leap_client)% python setup.py branding
(leap_client)% python setup.py develop
to avoid messing with the entry point and global versions installed,
diff --git a/data/branding/cacert.pem b/data/branding/cacert.pem
deleted file mode 100644
index ed12e159..00000000
--- a/data/branding/cacert.pem
+++ /dev/null
@@ -1,24 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIECzCCAl2gAwIBAgIEUFDp9TANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDEwRU
-RVNUMB4XDTEyMDkxMjIwMDA1M1oXDTEzMDkxMjIwMDA1M1owDzENMAsGA1UEAxME
-VEVTVDCCAbgwDQYJKoZIhvcNAQEBBQADggGlADCCAaACggGXANsoS1m9wj9iv+UV
-BXfeq14SR94gSot96eJu7PZVRrcGlGe/PRfbmfxF3j/gXM9B8sIkyM2L46OMtOKw
-1iOTKtYYdMhtnUSd3FRshWGtYeuy+OCe9umU0jfZDBZ2pXlUmSqCNqfD0OPkksYL
-GDjQUKjaEd1oURwpCG8uEU+3tjBNCMuEwhcMEoUYmI8t4vss2hdFb+LKefVMPTzz
-oiNM/o8Z/ANzWCC0qSW5FsB4wGhUS5HKLDOr4tACgdxaJSWtAqFFAnyMeG9g8aqe
-PTM+URlqVnzzGckrJwBbd4y0zEpv/R7SAiSAP725cnB1GKptwdrcNIIHnQjOdAOl
-uNg6JlRXrv6fV1gApka4INfJAf1yMf+fA0WdZ22UJQ9Up7tdzi8lL+3HsEpEx4Pz
-NyzuqzEw9LJ6SUmMcE/VP00t4RjTOVoncwcLjvURY8jt2DQ9E36JEPwUoyALq/De
-bGBjeK2KGzBZcOu1HZAwWLLWR2++WKuCEXbRbahwSIlbMfmAe8xGx4bbHol0D1A+
-wmu0uxjAze6FvUkCAwEAAaNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8E
-BQMDBwQAMB0GA1UdDgQWBBT/PX8XZ0Y2jDkppz6PHs23IgzQEDANBgkqhkiG9w0B
-AQsFAAOCAZcAMfi+HLbcFaB0/Mv8/GkIdjpThUBVEeFrIiDy9GmGWUDOXgP1Skld
-5H4eY5inE5lFfB69yacHIGS4OiZIBuBKfKNl5d6XO+ztJEJpG3yrbF4MtGV/aHEp
-OlbJCncnk3fspBk6tFGrv4Inak4gza6SQPfBEZj29ciwfwrqrtuWZ7km+og0Clcd
-pIB0g+DK0K//NtaDZDK0havQw2AFJKyXlNfI8XZ2jsNmQYR1wtiMci+UfGQr7bjn
-Kw9yyVCf0ohXvnSK4ortz/bDQbcMWkK0m/VCCEK8PSldk+XFzPWFWn5ndKCczcvd
-1BQc392n12ZstEuzm6+d9A0D3kCxralJUXUC+4kThq4Rtjey/gBjyZQnZ+5tIxMF
-5ZFAStEglNxqm6HB17q7owJqTvIg9Cf9GATsvoFFQDJrBXewRX7cWVeSr0zNSQB4
-ydIlSUOkyE3AyfLN+lx8NVS/I7gp4fWDuHrh27NKKDtMxalxPL5pTGO7l4uTybLY
-4aVzQYGvzA5HVS++VAtcTQ6TP9p4HURL2cllEU9u9A==
------END CERTIFICATE-----
diff --git a/data/images/checked.png b/data/images/checked.png
new file mode 100644
index 00000000..fbaf90f2
--- /dev/null
+++ b/data/images/checked.png
Binary files differ
diff --git a/data/resources/mainwindow.qrc b/data/resources/mainwindow.qrc
index f62c531e..2fc43b13 100644
--- a/data/resources/mainwindow.qrc
+++ b/data/resources/mainwindow.qrc
@@ -5,5 +5,6 @@
<file>../images/conn_connected.png</file>
<file>../images/leapfrog.jpg</file>
<file>../images/leap-color-small.png</file>
+ <file>../images/checked.png</file>
</qresource>
</RCC>
diff --git a/pkg/branding/config.py b/pkg/branding/config.py
index 665cfbda..bcacc3bc 100644
--- a/pkg/branding/config.py
+++ b/pkg/branding/config.py
@@ -1,11 +1,11 @@
# Configuration file for branding
-BRANDED_BUILD = True
+BRANDED_BUILD = False
APP_BASE_NAME = "leap-client"
APP_PREFIX = "%s-" % APP_BASE_NAME
BRANDED_OPTS = {
- 'short_name': "springbok",
- 'provider_domain': "springbok",
- 'provider_ca_path': "data/branding/cacert.pem"}
+ 'short_name': "",
+ 'provider_domain': "",
+ 'provider_ca_path': ""}
diff --git a/setup.cfg b/setup.cfg
index 3c2fdf51..4b049f97 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,2 @@
[egg_info]
-tag_build = dev
+#tag_build = dev
diff --git a/setup.py b/setup.py
index e1566752..c280860d 100755
--- a/setup.py
+++ b/setup.py
@@ -187,8 +187,12 @@ class cmd_post_install(_install_data):
cmdclass = versioneer.get_cmdclass()
cmdclass["branding"] = DoBranding
-cmdclass["build"] = cmd_build
-cmdclass["sdist"] = cmd_sdist
+
+# Uncomment this to have the branding command run automatically
+# on the build and sdist commands.
+#cmdclass["build"] = cmd_build
+#cmdclass["sdist"] = cmd_sdist
+
cmdclass["install_data"] = cmd_post_install
@@ -196,7 +200,7 @@ launcher_name = branding.get_shortname()
if launcher_name:
leap_launcher = 'leap-%s-client=leap.app:main' % launcher_name
else:
- leap_launcher = 'leap=leap.app:main'
+ leap_launcher = 'leap-client=leap.app:main'
setup(
name=branding.get_name(),
diff --git a/src/leap/app.py b/src/leap/app.py
index a1251ca8..4dd93600 100644
--- a/src/leap/app.py
+++ b/src/leap/app.py
@@ -1,5 +1,6 @@
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
import logging
+
# This is only needed for Python v2 but is harmless for Python v3.
import sip
sip.setapi('QVariant', 2)
@@ -74,6 +75,8 @@ def main():
# if not, it will be set visible
# from the systray menu.
window.show()
+
+ # run main loop
sys.exit(app.exec_())
if __name__ == "__main__":
diff --git a/src/leap/base/auth.py b/src/leap/base/auth.py
new file mode 100644
index 00000000..50533278
--- /dev/null
+++ b/src/leap/base/auth.py
@@ -0,0 +1,376 @@
+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.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 ImproperlyConfigured(Exception):
+ """
+ """
+
+
+class SRPAuthenticationError(Exception):
+ """
+ exception raised
+ for authentication errors
+ """
+
+
+def null_check(value, value_name):
+ try:
+ assert value is not None
+ except AssertionError:
+ raise ImproperlyConfigured(
+ "%s parameter cannot be None" % value_name)
+
+
+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,
+ port=None,
+ verify=True,
+ register_path="1/users.json",
+ method="POST",
+ fetcher=requests,
+ srp=srp,
+ hashfun=srp.SHA256,
+ ng_constant=srp.NG_1024):
+
+ null_check(provider, provider)
+
+ self.schema = schema
+
+ # XXX FIXME
+ self.provider = provider
+ self.port = port
+ # XXX splitting server,port
+ # deprecate port call.
+ 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)
+ logger.debug(req)
+ logger.debug('user_data: %s', user_data)
+ #logger.debug('response: %s', req.text)
+ # 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
+
+ self.init_data = None
+ self.session = requests.session()
+
+ self.init_srp()
+
+ def get_json_data(self, response):
+ return json.loads(response.content)
+
+ 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.json/',
+ data=self.get_auth_data(),
+ verify=self.verify)
+ except requests.exceptions.ConnectionError:
+ raise SRPAuthenticationError(
+ "No connection made (salt).")
+ if init_session.status_code not in (200, ):
+ raise SRPAuthenticationError(
+ "No valid response (salt).")
+
+ # XXX should get auth_result.json instead
+ self.init_data = self.get_json_data(init_session)
+ 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).")
+
+ # XXX should get auth_result.json instead
+ try:
+ self.auth_data = self.get_json_data(auth_result)
+ except ValueError:
+ raise SRPAuthenticationError(
+ "No valid data sent (HAMK)")
+
+ 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.session = self.session
+ 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
+ return fn(*args, **kwargs)
+ return wrapper
+ return srpauth
+
+
+def get_leap_credentials():
+ settings = QtCore.QSettings()
+ full_username = settings.value('eip_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')
+ print 'cert :', req.content[:200] + "..."
+ sys.exit(0)
+
+ if action == "add":
+ auth = LeapSRPRegister(provider=SERVER, verify=False)
+ auth.register_user(user, passwd)
diff --git a/src/leap/base/checks.py b/src/leap/base/checks.py
index 7285e74f..23446f4a 100644
--- a/src/leap/base/checks.py
+++ b/src/leap/base/checks.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import logging
import platform
+import socket
import netifaces
import ping
@@ -23,7 +24,7 @@ class LeapNetworkChecker(object):
def run_all(self, checker=None):
if not checker:
checker = self
- self.error = None # ?
+ #self.error = None # ?
# for MVS
checker.check_tunnel_default_interface()
@@ -118,11 +119,9 @@ class LeapNetworkChecker(object):
if packet_loss > constants.MAX_ICMP_PACKET_LOSS:
raise exceptions.NoConnectionToGateway
- # XXX check for name resolution servers
- # dunno what's the best way to do this...
- # check for etc/resolv entries or similar?
- # just try to resolve?
- # is there something in psutil?
-
- # def check_name_resolution(self):
- # pass
+ def check_name_resolution(self, domain_name):
+ try:
+ socket.gethostbyname(domain_name)
+ return True
+ except socket.gaierror:
+ raise exceptions.CannotResolveDomainError
diff --git a/src/leap/base/config.py b/src/leap/base/config.py
index cf01d1aa..0255fbab 100644
--- a/src/leap/base/config.py
+++ b/src/leap/base/config.py
@@ -118,6 +118,7 @@ class JSONLeapConfig(BaseLeapConfig):
" 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)
@@ -252,6 +253,15 @@ def get_default_provider_path():
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
diff --git a/src/leap/base/connection.py b/src/leap/base/connection.py
index e478538d..41d13935 100644
--- a/src/leap/base/connection.py
+++ b/src/leap/base/connection.py
@@ -37,11 +37,11 @@ class Connection(Authentication):
"""
pass
- def shutdown(self):
- """
- shutdown and quit
- """
- self.desired_con_state = self.status.DISCONNECTED
+ #def shutdown(self):
+ #"""
+ #shutdown and quit
+ #"""
+ #self.desired_con_state = self.status.DISCONNECTED
def connection_state(self):
"""
diff --git a/src/leap/base/exceptions.py b/src/leap/base/exceptions.py
index f12a49d5..227da953 100644
--- a/src/leap/base/exceptions.py
+++ b/src/leap/base/exceptions.py
@@ -67,6 +67,11 @@ class NoInternetConnection(CriticalError):
# and now we try to connect to our web to troubleshoot LOL :P
+class CannotResolveDomainError(LeapException):
+ message = "Cannot resolve domain"
+ usermessage = "Domain cannot be found"
+
+
class TunnelNotDefaultRouteError(CriticalError):
message = "Tunnel connection dissapeared. VPN down?"
usermessage = "The Encrypted Connection was lost. Shutting down..."
diff --git a/src/leap/base/network.py b/src/leap/base/network.py
index 3891b00a..3aba3f61 100644
--- a/src/leap/base/network.py
+++ b/src/leap/base/network.py
@@ -31,7 +31,7 @@ class NetworkCheckerThread(object):
# see in eip.config for function
# #718
self.checker = LeapNetworkChecker(
- provider_gw = get_eip_gateway())
+ provider_gw=get_eip_gateway())
def start(self):
self.process_handle = self._launch_recurrent_network_checks(
diff --git a/src/leap/base/providers.py b/src/leap/base/providers.py
index 7b219cc7..d41f3695 100644
--- a/src/leap/base/providers.py
+++ b/src/leap/base/providers.py
@@ -7,20 +7,20 @@ class LeapProviderDefinition(baseconfig.JSONLeapConfig):
spec = specs.leap_provider_spec
def _get_slug(self):
- provider_path = baseconfig.get_default_provider_path()
+ 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=provider_path)
+ 'provider.json', folder=path)
def _set_slug(self, *args, **kwargs):
raise AttributeError("you cannot set slug")
slug = property(_get_slug, _set_slug)
- # TODO (MVS+)
- # we will construct slug from providers/%s/definition.json
- # where %s is domain name. we can get that on __init__
-
class LeapProviderSet(object):
# we gather them from the filesystem
diff --git a/src/leap/base/tests/__init__.py b/src/leap/base/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/leap/base/tests/__init__.py
diff --git a/src/leap/base/tests/test_checks.py b/src/leap/base/tests/test_checks.py
index bec09ce6..8d573b1e 100644
--- a/src/leap/base/tests/test_checks.py
+++ b/src/leap/base/tests/test_checks.py
@@ -40,7 +40,14 @@ class LeapNetworkCheckTest(BaseLeapTest):
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")
+ # 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")
diff --git a/src/leap/base/tests/test_providers.py b/src/leap/base/tests/test_providers.py
index 8d3b8847..15c4ed58 100644
--- a/src/leap/base/tests/test_providers.py
+++ b/src/leap/base/tests/test_providers.py
@@ -30,7 +30,9 @@ EXPECTED_DEFAULT_CONFIG = {
class TestLeapProviderDefinition(BaseLeapTest):
def setUp(self):
- self.definition = providers.LeapProviderDefinition()
+ self.domain = "testprovider.example.org"
+ self.definition = providers.LeapProviderDefinition(
+ domain=self.domain)
self.definition.save()
self.definition.load()
self.config = self.definition.config
@@ -51,7 +53,7 @@ class TestLeapProviderDefinition(BaseLeapTest):
os.path.join(
self.home,
'.config', 'leap', 'providers',
- '%s' % BRANDING.get('provider_domain'),
+ '%s' % self.domain,
'provider.json'))
with self.assertRaises(AttributeError):
self.definition.slug = 23
diff --git a/src/leap/baseapp/eip.py b/src/leap/baseapp/eip.py
index 93dce3ac..54acbc0e 100644
--- a/src/leap/baseapp/eip.py
+++ b/src/leap/baseapp/eip.py
@@ -25,6 +25,7 @@ class EIPConductorAppMixin(object):
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
@@ -36,10 +37,11 @@ class EIPConductorAppMixin(object):
self.conductor = EIPConnection(
watcher_cb=self.newLogLine.emit,
config_file=config_file,
- checker_signals=(self.changeLeapStatus.emit, ),
- status_signals=(self.statusChange.emit, ),
+ checker_signals=(self.eipStatusChange.emit, ),
+ status_signals=(self.openvpnStatusChange.emit, ),
debug=self.debugmode,
- ovpn_verbosity=opts.openvpn_verb)
+ ovpn_verbosity=opts.openvpn_verb,
+ provider=provider)
self.skip_download = opts.no_provider_checks
self.skip_verify = opts.no_ca_verify
@@ -137,14 +139,14 @@ class EIPConductorAppMixin(object):
# is not ready yet.
return
- if self.conductor.with_errors:
+ #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
+ #logger.error('errors. disconnect')
+ #self.start_or_stopVPN() # is stop
state = self.conductor.poll_connection_state()
if not state:
diff --git a/src/leap/baseapp/leap_app.py b/src/leap/baseapp/leap_app.py
index 6ffb08a8..4b63dd2f 100644
--- a/src/leap/baseapp/leap_app.py
+++ b/src/leap/baseapp/leap_app.py
@@ -52,7 +52,7 @@ class MainWindowMixin(object):
self.firstRunWizardAct = QtGui.QAction(
"&First run wizard...", self,
- triggered=self.launch_first_run_wizard)
+ 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,
@@ -74,16 +74,21 @@ class MainWindowMixin(object):
self.menuBar().addMenu(self.settingsMenu)
self.menuBar().addMenu(self.helpMenu)
- def launch_first_run_wizard(self):
+ def stop_connection_and_launch_first_run_wizard(self):
settings = QtCore.QSettings()
settings.setValue('FirstRunWizardDone', False)
logger.debug('should run first run wizard again...')
- from leap.gui.firstrunwizard import FirstRunWizard
- wizard = FirstRunWizard(
- parent=self,
- success_cb=self.initReady.emit)
- wizard.show()
+ 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)
@@ -127,8 +132,8 @@ class MainWindowMixin(object):
"context menu of the system tray entry.")
self.hide()
event.ignore()
- if self.debugmode:
- self.cleanupAndQuit()
+ return
+ self.cleanupAndQuit()
def cleanupAndQuit(self):
"""
diff --git a/src/leap/baseapp/mainwindow.py b/src/leap/baseapp/mainwindow.py
index 3b6cb544..918f1568 100644
--- a/src/leap/baseapp/mainwindow.py
+++ b/src/leap/baseapp/mainwindow.py
@@ -2,6 +2,10 @@
#!/usr/bin/env python
import logging
+import sip
+sip.setapi('QString', 2)
+sip.setapi('QVariant', 2)
+
from PyQt4 import QtCore
from PyQt4 import QtGui
@@ -10,6 +14,7 @@ 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.gui.threads import FunThread
logger = logging.getLogger(name=__name__)
@@ -35,11 +40,10 @@ class LeapWindow(QtGui.QMainWindow,
triggerEIPError = QtCore.pyqtSignal([object])
start_eipconnection = QtCore.pyqtSignal([])
- # XXX fix nomenclature here
- # this is eip status change got from vpn management
- statusChange = QtCore.pyqtSignal([object])
- # this is global leap status
- changeLeapStatus = QtCore.pyqtSignal([str])
+ # 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')
@@ -48,26 +52,32 @@ class LeapWindow(QtGui.QMainWindow,
if self.debugmode:
self.createLogBrowser()
- EIPConductorAppMixin.__init__(self, opts=opts)
+ settings = QtCore.QSettings()
+ self.provider_domain = settings.value("provider_domain", None)
+ self.eip_username = settings.value("eip_username", None)
+
+ logger.debug('provider: %s', self.provider_domain)
+ logger.debug('eip_username: %s', self.eip_username)
+
+ EIPConductorAppMixin.__init__(
+ self, opts=opts, provider=self.provider_domain)
StatusAwareTrayIconMixin.__init__(self)
NetworkCheckerAppMixin.__init__(self)
MainWindowMixin.__init__(self)
- settings = QtCore.QSettings()
-
geom_key = "DebugGeometry" if self.debugmode else "Geometry"
geom = settings.value(geom_key)
-
- geom = settings.value("Geometry")
if geom:
self.restoreGeometry(geom)
+
+ # XXX check for wizard
self.wizard_done = settings.value("FirstRunWizardDone")
- self.initchecks = InitChecksThread(self.run_eip_checks)
+ self.initchecks = FunThread(self.run_eip_checks)
# bind signals
self.initchecks.finished.connect(
- lambda: logger.debug('Initial checks finished'))
+ lambda: logger.debug('Initial checks thread finished'))
self.trayIcon.activated.connect(self.iconActivated)
self.newLogLine.connect(
lambda line: self.onLoggerNewLine(line))
@@ -86,44 +96,65 @@ class LeapWindow(QtGui.QMainWindow,
# status change.
# TODO unify
- self.statusChange.connect(
- lambda status: self.onStatusChange(status))
- self.changeLeapStatus.connect(
- lambda newstatus: self.onChangeLeapConnStatus(newstatus))
-
- # do frwizard and init signals
+ self.openvpnStatusChange.connect(
+ lambda status: self.onOpenVPNStatusChange(status))
+ self.eipStatusChange.connect(
+ lambda newstatus: self.onEIPConnStatusChange(newstatus))
+ # can I connect 2 signals?
+ 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!
- # calls do_first_run_wizard_check
+ # 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...')
- if self.wizard_done:
- self.initReady.emit()
- else:
- # need to run first-run-wizard
+ 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
+
+ # launch wizard if needed
+ if need_wizard:
+ self.launch_first_run_wizard()
+ else: # no wizard needed
logger.debug('running first run wizard')
- from leap.gui.firstrunwizard import FirstRunWizard
- wizard = FirstRunWizard(
- parent=self,
- success_cb=self.initReady.emit)
- wizard.show()
+ 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,
+ eip_username=self.eip_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):
self.initchecks.begin()
-
-
-class InitChecksThread(QtCore.QThread):
-
- def __init__(self, fun, parent=None):
- QtCore.QThread.__init__(self, parent)
- self.fun = fun
-
- def run(self):
- self.fun()
-
- def begin(self):
- self.start()
diff --git a/src/leap/baseapp/systray.py b/src/leap/baseapp/systray.py
index cc5d89df..94a7a8f2 100644
--- a/src/leap/baseapp/systray.py
+++ b/src/leap/baseapp/systray.py
@@ -1,4 +1,7 @@
import logging
+import sip
+sip.setapi('QString', 2)
+sip.setapi('QVariant', 2)
from PyQt4 import QtCore
from PyQt4 import QtGui
@@ -92,7 +95,9 @@ class StatusAwareTrayIconMixin(object):
self.trayIconMenu.addAction(self.detailsAct)
self.trayIconMenu.addSeparator()
self.trayIconMenu.addAction(self.aboutAct)
- self.trayIconMenu.addAction(self.aboutQtAct)
+ # 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)
@@ -127,12 +132,21 @@ class StatusAwareTrayIconMixin(object):
def toggleEIPAct(self):
# this is too simple by now.
- # XXX We need to get the REAL info for Encryption state.
- # (now is ON as soon as vpn launched)
- if self.eip_service_started is True:
+ # XXX get STATUS CONSTANTS INSTEAD
+
+ icon_status = self.conductor.get_icon_name()
+ if icon_status == "connected":
+ self.connAct.setEnabled(True)
self.connAct.setText('Encryption ON turn o&ff')
- else:
+ return
+ if icon_status == "disconnected":
+ self.connAct.setEnabled(True)
self.connAct.setText('Encryption OFF turn &on')
+ return
+ if icon_status == "connecting":
+ self.connAct.setDisabled(True)
+ self.connAct.setText('connecting...')
+ return
def detailsWin(self):
visible = self.isVisible()
@@ -196,31 +210,31 @@ class StatusAwareTrayIconMixin(object):
self.statusUpdate()
@QtCore.pyqtSlot(object)
- def onStatusChange(self, status):
+ def onOpenVPNStatusChange(self, status):
"""
- updates icon
+ updates icon, according to the openvpn status change.
"""
icon_name = self.conductor.get_icon_name()
# XXX refactor. Use QStateMachine
if icon_name in ("disconnected", "connected"):
- self.changeLeapStatus.emit(icon_name)
+ 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.changeLeapStatus.emit(leap_status_name)
+ self.eipStatusChange.emit(leap_status_name)
self.setIcon(icon_name)
# change connection pixmap widget
self.setConnWidget(icon_name)
@QtCore.pyqtSlot(str)
- def onChangeLeapConnStatus(self, newstatus):
+ def onEIPConnStatusChange(self, newstatus):
"""
- slot for LEAP status changes
- not to be confused with onStatusChange.
+ 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.
"""
diff --git a/src/leap/crypto/certs.py b/src/leap/crypto/certs.py
new file mode 100644
index 00000000..8908865d
--- /dev/null
+++ b/src/leap/crypto/certs.py
@@ -0,0 +1,71 @@
+import ctypes
+import socket
+
+import gnutls.connection
+import gnutls.crypto
+import gnutls.library
+
+
+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(filepath):
+ with open(filepath) as f:
+ cert = gnutls.crypto.X509Certificate(f.read())
+ return cert
+
+
+def get_cert_fingerprint(domain=None, filepath=None,
+ hash_type="SHA256", sep=":"):
+ """
+ @param domain: a domain name to get a fingerprint from
+ @type domain: str
+ @param filepath: path to a file containing a PEM file
+ @type filepath: str
+ @param hash_type: the hash function to be used in the fingerprint.
+ must be one of SHA1, SHA224, SHA256, SHA384, SHA512
+ @type hash_type: str
+ @rparam: hex_fpr, a hexadecimal representation of a bytestring
+ containing the fingerprint.
+ @rtype: string
+ """
+ if domain:
+ cert = get_https_cert_from_domain(domain)
+ if filepath:
+ cert = get_cert_from_file(filepath)
+
+ _buffer = ctypes.create_string_buffer(64)
+ buffer_length = ctypes.c_size_t(64)
+
+ SUPPORTED_DIGEST_FUN = ("SHA1", "SHA224", "SHA256", "SHA384", "SHA512")
+ if hash_type in SUPPORTED_DIGEST_FUN:
+ digestfunction = getattr(
+ gnutls.library.constants,
+ "GNUTLS_DIG_%s" % hash_type)
+ else:
+ # XXX improperlyconfigured or something
+ raise Exception("digest function not supported")
+
+ gnutls.library.functions.gnutls_x509_crt_get_fingerprint(
+ cert._c_object, digestfunction,
+ ctypes.byref(_buffer), ctypes.byref(buffer_length))
+
+ # deinit
+ #server_cert._X509Certificate__deinit(server_cert._c_object)
+ # needed? is segfaulting
+
+ fpr = ctypes.string_at(_buffer, buffer_length.value)
+ hex_fpr = sep.join(u"%02X" % ord(char) for char in fpr)
+
+ return hex_fpr
diff --git a/src/leap/crypto/leapkeyring.py b/src/leap/crypto/leapkeyring.py
index bceadc75..d4be7bf9 100644
--- a/src/leap/crypto/leapkeyring.py
+++ b/src/leap/crypto/leapkeyring.py
@@ -59,6 +59,7 @@ def leap_set_password(key, value, seed="xxx"):
def leap_get_password(key, seed="xxx"):
keyring.set_keyring(LeapCryptedFileKeyring(seed=seed))
+ #import ipdb;ipdb.set_trace()
return keyring.get_password('leap', key)
diff --git a/src/leap/eip/checks.py b/src/leap/eip/checks.py
index f739c3e8..116c535e 100644
--- a/src/leap/eip/checks.py
+++ b/src/leap/eip/checks.py
@@ -4,15 +4,18 @@ import ssl
import time
import os
-from gnutls import crypto
+import gnutls.crypto
#import netifaces
#import ping
import requests
from leap import __branding as BRANDING
-from leap import certs
+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
@@ -42,10 +45,11 @@ reachable and testable as a whole.
"""
-def get_ca_cert():
+def get_branding_ca_cert(domain):
+ # XXX deprecated
ca_file = BRANDING.get('provider_ca_file')
if ca_file:
- return certs.where(ca_file)
+ return leapcerts.where(ca_file)
class ProviderCertChecker(object):
@@ -54,18 +58,25 @@ class ProviderCertChecker(object):
client certs and checking tls connection
with provider.
"""
- def __init__(self, fetcher=requests):
+ def __init__(self, fetcher=requests,
+ domain=None):
+
self.fetcher = fetcher
- self.cacert = get_ca_cert()
+ self.domain = domain
+ self.cacert = eipspecs.provider_ca_path(domain)
+
+ def run_all(
+ self, checker=None,
+ skip_download=False, skip_verify=False):
- 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)
- # For MVS+
# checker.download_ca_cert()
+
+ # For MVS+
# checker.download_ca_signature()
# checker.get_ca_signatures()
# checker.is_there_trust_path()
@@ -74,12 +85,44 @@ class ProviderCertChecker(object):
checker.is_there_provider_ca()
# XXX FAKE IT!!!
- checker.is_https_working(verify=do_verify)
+ checker.is_https_working(verify=do_verify, autocacert=True)
checker.check_new_cert_needed(verify=do_verify)
- def download_ca_cert(self):
- # MVS+
- raise NotImplementedError
+ 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 and cacert or True
+ req = self.fetcher.get(uri, verify=verify)
+ req.raise_for_status()
+ return True
def download_ca_signature(self):
# MVS+
@@ -94,36 +137,47 @@ class ProviderCertChecker(object):
raise NotImplementedError
def is_there_provider_ca(self):
- from leap import certs
- logger.debug('do we have provider_ca?')
- cacert_path = BRANDING.get('provider_ca_file', None)
- if not cacert_path:
- logger.debug('False')
+ if not self.cacert:
return False
- self.cacert = certs.where(cacert_path)
- logger.debug('True')
- return True
+ 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):
+ 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
- assert uri.startswith('https')
- if verify is True and self.cacert is not None:
+ 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
+ #import pdb4qt; pdb4qt.set_trace()
logger.debug('is https working?')
logger.debug('uri: %s (verify:%s)', uri, verify)
try:
self.fetcher.get(uri, verify=verify)
+
except requests.exceptions.SSLError as exc:
- logger.warning('False! CERT VERIFICATION FAILED! '
+ logger.error("SSLError")
+ # XXX RAISE! See #638
+ #raise eipexceptions.HttpsBadCertError
+ logger.warning('BUG #638 CERT VERIFICATION FAILED! '
'(this should be CRITICAL)')
logger.warning('SSLError: %s', exc.message)
- # XXX RAISE! See #638
- #raise eipexceptions.EIPBadCertError
- # XXX get requests.exceptions.ConnectionError Errno 110
- # Connection timed out, and raise ours.
+
+ except requests.exceptions.ConnectionError:
+ logger.error('ConnectionError')
+ raise eipexceptions.HttpsNotSupported
+
else:
logger.debug('True')
return True
@@ -140,7 +194,8 @@ class ProviderCertChecker(object):
return False
def download_new_client_cert(self, uri=None, verify=True,
- skip_download=False):
+ skip_download=False,
+ credentials=None):
logger.debug('download new client cert')
if skip_download:
return True
@@ -148,18 +203,38 @@ class ProviderCertChecker(object):
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
+
+ fgetfn = self.fetcher.get
+
+ if credentials:
+ user, passwd = credentials
+
+ logger.debug('domain = %s', self.domain)
+
+ @srpauth_protected(user, passwd,
+ server="https://%s" % self.domain,
+ 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:
+
# XXX FIXME!!!!
# verify=verify
# Workaround for #638. return to verification
# when That's done!!!
-
- # XXX HOOK SRP here...
- # will have to be more generic in the future.
- req = self.fetcher.get(uri, verify=False)
+ #req = self.fetcher.get(uri, verify=False)
+ req = getfn(uri, verify=False)
req.raise_for_status()
+
except requests.exceptions.SSLError:
logger.warning('SSLError while fetching cert. '
'Look below for stack trace.')
@@ -198,7 +273,7 @@ class ProviderCertChecker(object):
certfile = self._get_client_cert_path()
with open(certfile) as cf:
cert_s = cf.read()
- cert = crypto.X509Certificate(cert_s)
+ cert = gnutls.crypto.X509Certificate(cert_s)
from_ = time.gmtime(cert.activation_time)
to_ = time.gmtime(cert.expiration_time)
return from_ < now() < to_
@@ -233,16 +308,34 @@ class ProviderCertChecker(object):
raise
return True
+ @property
+ def ca_cert_path(self):
+ return self._get_ca_cert_path(self.domain)
+
def _get_root_uri(self):
- return u"https://%s/" % baseconstants.DEFAULT_PROVIDER
+ return u"https://%s/" % self.domain
def _get_client_cert_uri(self):
# XXX get the whole thing from constants
- return "https://%s/1/cert" % (baseconstants.DEFAULT_PROVIDER)
+ return "https://%s/1/cert" % self.domain
def _get_client_cert_path(self):
- # MVS+ : get provider path
- return eipspecs.client_cert_path()
+ 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)
@@ -260,16 +353,20 @@ class EIPConfigChecker(object):
use run_all to run all checks.
"""
- def __init__(self, fetcher=requests):
+ 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
- self.eipconfig = eipconfig.EIPConfig()
- self.defaultprovider = providers.LeapProviderDefinition()
- self.eipserviceconfig = eipconfig.EIPServiceConfig()
+ # if not domain, get from config
+ self.domain = domain
+
+ self.eipconfig = eipconfig.EIPConfig(domain=domain)
+ self.defaultprovider = providers.LeapProviderDefinition(domain=domain)
+ self.eipserviceconfig = eipconfig.EIPServiceConfig(domain=domain)
def run_all(self, checker=None, skip_download=False):
"""
@@ -330,7 +427,8 @@ class EIPConfigChecker(object):
return True
def fetch_definition(self, skip_download=False,
- config=None, uri=None):
+ config=None, uri=None,
+ domain=None):
"""
fetches a definition file from server
"""
@@ -347,10 +445,13 @@ class EIPConfigChecker(object):
if config is None:
config = self.defaultprovider.config
if uri is None:
- domain = config.get('provider', None)
+ if not domain:
+ domain = config.get('provider', None)
uri = self._get_provider_definition_uri(domain=domain)
# FIXME! Pass ca path verify!!!
+ # BUG #638
+ # FIXME FIXME FIXME
self.defaultprovider.load(
from_uri=uri,
fetcher=self.fetcher,
@@ -358,13 +459,14 @@ class EIPConfigChecker(object):
self.defaultprovider.save()
def fetch_eip_service_config(self, skip_download=False,
- config=None, uri=None):
+ config=None, uri=None, domain=None):
if skip_download:
return True
if config is None:
config = self.eipserviceconfig.config
if uri is None:
- domain = config.get('provider', None)
+ if not domain:
+ domain = self.domain or config.get('provider', None)
uri = self._get_eip_service_uri(domain=domain)
self.eipserviceconfig.load(from_uri=uri, fetcher=self.fetcher)
@@ -399,7 +501,7 @@ class EIPConfigChecker(object):
def _get_provider_definition_uri(self, domain=None, path=None):
if domain is None:
- domain = baseconstants.DEFAULT_PROVIDER
+ domain = self.domain or baseconstants.DEFAULT_PROVIDER
if path is None:
path = baseconstants.DEFINITION_EXPECTED_PATH
uri = u"https://%s/%s" % (domain, path)
@@ -408,7 +510,7 @@ class EIPConfigChecker(object):
def _get_eip_service_uri(self, domain=None, path=None):
if domain is None:
- domain = baseconstants.DEFAULT_PROVIDER
+ domain = self.domain or baseconstants.DEFAULT_PROVIDER
if path is None:
path = eipconstants.EIP_SERVICE_EXPECTED_PATH
uri = "https://%s/%s" % (domain, path)
diff --git a/src/leap/eip/config.py b/src/leap/eip/config.py
index ef0f52b4..42c00380 100644
--- a/src/leap/eip/config.py
+++ b/src/leap/eip/config.py
@@ -35,9 +35,13 @@ 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=baseconfig.get_default_provider_path())
+ 'eip-service.json', folder=path)
def _set_slug(self):
raise AttributeError("you cannot set slug")
@@ -53,15 +57,16 @@ def get_socket_path():
return socket_path
-def get_eip_gateway():
+def get_eip_gateway(provider=None):
"""
return the first host in eip service config
that matches the name defined in the eip.json config
file.
"""
placeholder = "testprovider.example.org"
- eipconfig = EIPConfig()
- #import ipdb;ipdb.set_trace()
+ # XXX check for null on provider??
+
+ eipconfig = EIPConfig(domain=provider)
eipconfig.load()
conf = eipconfig.config
@@ -69,7 +74,7 @@ def get_eip_gateway():
if not primary_gateway:
return placeholder
- eipserviceconfig = EIPServiceConfig()
+ eipserviceconfig = EIPServiceConfig(domain=provider)
eipserviceconfig.load()
eipsconf = eipserviceconfig.get_config()
gateways = eipsconf.get('gateways', None)
@@ -78,8 +83,15 @@ def get_eip_gateway():
return placeholder
if len(gateways) > 0:
for gw in gateways:
- if gw['name'] == primary_gateway:
- hosts = gw['hosts']
+ name = gw.get('name', None)
+ if not name:
+ return
+
+ if name == primary_gateway:
+ hosts = gw.get('hosts', None)
+ if not hosts:
+ logger.error('no hosts')
+ return
if len(hosts) > 0:
return hosts[0]
else:
@@ -103,6 +115,8 @@ def build_ovpn_options(daemon=False, socket_path=None, **kwargs):
# since we will need to take some
# things from there if present.
+ provider = kwargs.pop('provider', None)
+
# get user/group name
# also from config.
user = baseconfig.get_username()
@@ -125,10 +139,11 @@ def build_ovpn_options(daemon=False, socket_path=None, **kwargs):
# remote
opts.append('--remote')
- gw = get_eip_gateway()
+ gw = get_eip_gateway(provider=provider)
logger.debug('setting eip gateway to %s', gw)
opts.append(str(gw))
opts.append('1194')
+ #opts.append('80')
opts.append('udp')
opts.append('--tls-client')
@@ -165,12 +180,15 @@ def build_ovpn_options(daemon=False, socket_path=None, **kwargs):
opts.append('7777')
# certs
+ client_cert_path = eipspecs.client_cert_path(provider)
+ ca_cert_path = eipspecs.provider_ca_path(provider)
+
opts.append('--cert')
- opts.append(eipspecs.client_cert_path())
+ opts.append(client_cert_path)
opts.append('--key')
- opts.append(eipspecs.client_cert_path())
+ opts.append(client_cert_path)
opts.append('--ca')
- opts.append(eipspecs.provider_ca_path())
+ opts.append(ca_cert_path)
# we cannot run in daemon mode
# with the current subp setting.
@@ -238,7 +256,7 @@ def build_ovpn_command(debug=False, do_pkexec_check=True, vpnbin=None,
return [command[0], command[1:]]
-def check_vpn_keys():
+def check_vpn_keys(provider=None):
"""
performs an existance and permission check
over the openvpn keys file.
@@ -246,8 +264,9 @@ def check_vpn_keys():
per provider, containing the CA cert,
the provider key, and our client certificate
"""
- provider_ca = eipspecs.provider_ca_path()
- client_cert = eipspecs.client_cert_path()
+ 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)
diff --git a/src/leap/eip/eipconnection.py b/src/leap/eip/eipconnection.py
index f0e7861e..7828c864 100644
--- a/src/leap/eip/eipconnection.py
+++ b/src/leap/eip/eipconnection.py
@@ -29,6 +29,9 @@ class EIPConnection(OpenVPNConnection):
*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()
@@ -38,8 +41,7 @@ class EIPConnection(OpenVPNConnection):
checker_signals = kwargs.pop('checker_signals', None)
self.checker_signals = checker_signals
- self.provider_cert_checker = provider_cert_checker()
- self.config_checker = config_checker()
+ self.init_checkers()
host = eipconfig.get_socket_path()
kwargs['host'] = host
@@ -49,6 +51,25 @@ class EIPConnection(OpenVPNConnection):
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
@@ -95,11 +116,11 @@ class EIPConnection(OpenVPNConnection):
logger.debug("disconnect: clicked.")
self.status.change_to(self.status.DISCONNECTED)
- def shutdown(self):
- """
- shutdown and quit
- """
- self.desired_con_state = self.status.DISCONNECTED
+ #def shutdown(self):
+ #"""
+ #shutdown and quit
+ #"""
+ #self.desired_con_state = self.status.DISCONNECTED
def connection_state(self):
"""
@@ -110,10 +131,6 @@ class EIPConnection(OpenVPNConnection):
def poll_connection_state(self):
"""
"""
- # XXX this separation does not
- # make sense anymore after having
- # merged Connection and Manager classes.
- # XXX GET RID OF THIS FUNCTION HERE!
try:
state = self.get_connection_state()
except eip_exceptions.ConnectionRefusedError:
@@ -121,7 +138,7 @@ class EIPConnection(OpenVPNConnection):
logger.warning('connection refused')
return
if not state:
- #logger.debug('no state')
+ logger.debug('no state')
return
(ts, status_step,
ok, ip, remote) = state
@@ -247,6 +264,7 @@ class EIPConnectionStatus(object):
def get_leap_status(self):
# XXX improve nomenclature
leap_status = {
+ 0: 'disconnected',
1: 'connecting to gateway',
2: 'connecting to gateway',
3: 'authenticating',
diff --git a/src/leap/eip/exceptions.py b/src/leap/eip/exceptions.py
index 11bfd620..41eed77a 100644
--- a/src/leap/eip/exceptions.py
+++ b/src/leap/eip/exceptions.py
@@ -32,8 +32,10 @@ TODO:
* gettext / i18n for user messages.
"""
+from leap.base.exceptions import LeapException
+# This should inherit from LeapException
class EIPClientError(Exception):
"""
base EIPClient exception
@@ -99,6 +101,15 @@ class OpenVPNAlreadyRunning(EIPClientError):
"Please close it before starting leap-client")
+class HttpsNotSupported(LeapException):
+ message = "connection refused while accessing via https"
+ usermessage = "Server does not allow secure connections."
+
+
+class HttpsBadCertError(LeapException):
+ message = "verification error on cert"
+ usermessage = "Server certificate could not be verified."
+
#
# errors still needing some love
#
diff --git a/src/leap/eip/openvpnconnection.py b/src/leap/eip/openvpnconnection.py
index d93bc40f..4104bd0e 100644
--- a/src/leap/eip/openvpnconnection.py
+++ b/src/leap/eip/openvpnconnection.py
@@ -25,7 +25,6 @@ class OpenVPNConnection(Connection):
"""
def __init__(self,
- #config_file=None,
watcher_cb=None,
debug=False,
host=None,
@@ -64,7 +63,7 @@ to be triggered for each one of them.
#XXX workaround for signaling
#the ui that we don't know how to
#manage a connection error
- self.with_errors = False
+ #self.with_errors = False
self.command = None
self.args = None
@@ -96,6 +95,7 @@ to be triggered for each one of them.
# XXX check also for command-line --command flag
try:
command, args = eip_config.build_ovpn_command(
+ provider=self.provider,
debug=self.debug,
socket_path=self.host,
ovpn_verbosity=self.ovpn_verbosity)
@@ -115,7 +115,7 @@ to be triggered for each one of them.
checks for correct permissions on vpn keys
"""
try:
- eip_config.check_vpn_keys()
+ eip_config.check_vpn_keys(provider=self.provider)
except eip_exceptions.EIPInitBadKeyFilePermError:
logger.error('Bad VPN Keys permission!')
# do nothing now
@@ -179,42 +179,28 @@ to be triggered for each one of them.
terminates openvpn child subprocess
"""
if self.subp:
- self._stop()
+ try:
+ self._stop()
+ except eip_exceptions.ConnectionRefusedError:
+ logger.warning(
+ 'unable to send sigterm signal to openvpn: '
+ 'connection refused.')
+
+ # XXX kali --
+ # 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! '
- '(maybe openvpn still running?)')
-
- def _stop(self):
- """
- stop openvpn process
- """
- logger.debug("disconnecting...")
- self._send_command("signal SIGTERM\n")
-
- if self.subp:
- return True
-
- #shutting openvpn failured
- #try patching in old openvpn host and trying again
- process = self._get_openvpn_process()
- if process:
- self.host = \
- process.cmdline[process.cmdline.index("--management") + 1]
- self._send_command("signal SIGTERM\n")
-
- #make sure the process was terminated
- process = self._get_openvpn_process()
- if not process:
- logger.debug("Exisiting OpenVPN Process Terminated")
- return True
- else:
- logger.error("Unable to terminate exisiting OpenVPN Process.")
- return False
-
- return True
+ logger.error(
+ 'cannot terminate subprocess! Retcode %s'
+ '(We might have left openvpn running)' % RETCODE)
def _get_openvpn_process(self):
+ # plist = [p for p in psutil.get_process_list() if p.name == "openvpn"]
+ # return plist[0] if plist else None
for process in psutil.get_process_list():
if process.name == "openvpn":
return process
@@ -247,8 +233,8 @@ to be triggered for each one of them.
#self.tn.read_until('ENTER PASSWORD:', 2)
#self.tn.write(self.password + '\n')
#self.tn.read_until('SUCCESS:', 2)
-
- self._seek_to_eof()
+ if self.tn:
+ self._seek_to_eof()
return True
def _seek_to_eof(self):
@@ -293,12 +279,7 @@ to be triggered for each one of them.
self.connect_to_management()
except eip_exceptions.MissingSocketError:
logger.warning('missing management socket')
- # This should only happen briefly during
- # the first invocation. Race condition make
- # the polling begin before management socket
- # is ready
return []
- #return self.make_error()
try:
if hasattr(self, 'tn'):
self.tn.write(cmd + "\n")
@@ -376,6 +357,42 @@ to be triggered for each one of them.
"""
return self._send_command("status 2")
+ def _stop(self):
+ """
+ stop openvpn process
+ by sending SIGTERM to the management
+ interface
+ """
+ logger.debug("disconnecting...")
+ if self.connected():
+ self._send_command("signal SIGTERM\n")
+
+ if self.subp:
+ return True
+
+ #shutting openvpn failured
+ #try patching in old openvpn host and trying again
+ process = self._get_openvpn_process()
+ if process:
+ logger.debug('process :%s' % process)
+ cmdline = process.cmdline
+
+ if isinstance(cmdline, list):
+ _index = cmdline.index("--management")
+ 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
+
#
# parse info
#
diff --git a/src/leap/eip/specs.py b/src/leap/eip/specs.py
index 1a670b0e..57e7537b 100644
--- a/src/leap/eip/specs.py
+++ b/src/leap/eip/specs.py
@@ -4,11 +4,20 @@ 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',
- 'testprovider-ca-cert.pem')
+ '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
-provider_ca_path = lambda: str(os.path.join(
+default_provider_ca_path = lambda: str(os.path.join(
baseconfig.get_default_provider_path(),
'keys', 'ca',
PROVIDER_CA_CERT
@@ -17,7 +26,13 @@ provider_ca_path = lambda: str(os.path.join(
PROVIDER_DOMAIN = __branding.get('provider_domain', 'testprovider.example.org')
-client_cert_path = lambda: unicode(os.path.join(
+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'
@@ -46,11 +61,11 @@ eipconfig_spec = {
},
'openvpn_ca_certificate': {
'type': unicode, # path
- 'default': provider_ca_path
+ 'default': default_provider_ca_path
},
'openvpn_client_certificate': {
'type': unicode, # path
- 'default': client_cert_path
+ 'default': default_client_cert_path
},
'connect_on_login': {
'type': bool,
diff --git a/src/leap/eip/tests/data.py b/src/leap/eip/tests/data.py
index 43df2013..cadf720e 100644
--- a/src/leap/eip/tests/data.py
+++ b/src/leap/eip/tests/data.py
@@ -1,11 +1,12 @@
from __future__ import unicode_literals
import os
-from leap import __branding
+#from leap import __branding
# sample data used in tests
-PROVIDER = __branding.get('provider_domain')
+#PROVIDER = __branding.get('provider_domain')
+PROVIDER = "testprovider.example.org"
EIP_SAMPLE_CONFIG = {
"provider": "%s" % PROVIDER,
@@ -15,7 +16,7 @@ EIP_SAMPLE_CONFIG = {
"openvpn_ca_certificate": os.path.expanduser(
"~/.config/leap/providers/"
"%s/"
- "keys/ca/testprovider-ca-cert.pem" % PROVIDER),
+ "keys/ca/cacert.pem" % PROVIDER),
"openvpn_client_certificate": os.path.expanduser(
"~/.config/leap/providers/"
"%s/"
@@ -42,6 +43,6 @@ EIP_SAMPLE_SERVICE = {
"name": "turkey",
"label": {"en":"Ankara, Turkey"},
"capabilities": {},
- "hosts": ["94.103.43.4"]}
+ "hosts": ["192.0.43.10"]}
]
}
diff --git a/src/leap/eip/tests/test_checks.py b/src/leap/eip/tests/test_checks.py
index 58ce473f..1d7bfc17 100644
--- a/src/leap/eip/tests/test_checks.py
+++ b/src/leap/eip/tests/test_checks.py
@@ -39,6 +39,8 @@ class NoLogRequestHandler:
class EIPCheckTest(BaseLeapTest):
__name__ = "eip_check_tests"
+ provider = "testprovider.example.org"
+ maxDiff = None
def setUp(self):
pass
@@ -49,7 +51,7 @@ class EIPCheckTest(BaseLeapTest):
# test methods are there, and can be called from run_all
def test_checker_should_implement_check_methods(self):
- checker = eipchecks.EIPConfigChecker()
+ checker = eipchecks.EIPConfigChecker(domain=self.provider)
self.assertTrue(hasattr(checker, "check_default_eipconfig"),
"missing meth")
@@ -62,7 +64,7 @@ class EIPCheckTest(BaseLeapTest):
"missing meth")
def test_checker_should_actually_call_all_tests(self):
- checker = eipchecks.EIPConfigChecker()
+ checker = eipchecks.EIPConfigChecker(domain=self.provider)
mc = Mock()
checker.run_all(checker=mc)
@@ -79,7 +81,7 @@ class EIPCheckTest(BaseLeapTest):
# test individual check methods
def test_check_default_eipconfig(self):
- checker = eipchecks.EIPConfigChecker()
+ checker = eipchecks.EIPConfigChecker(domain=self.provider)
# no eip config (empty home)
eipconfig_path = checker.eipconfig.filename
self.assertFalse(os.path.isfile(eipconfig_path))
@@ -93,15 +95,15 @@ class EIPCheckTest(BaseLeapTest):
# 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()
+ eipspecs.client_cert_path(self.provider)
EIP_SAMPLE_CONFIG['openvpn_ca_certificate'] = \
- eipspecs.provider_ca_path()
+ 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()
+ 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
@@ -178,6 +180,7 @@ class EIPCheckTest(BaseLeapTest):
class ProviderCertCheckerTest(BaseLeapTest):
__name__ = "provider_cert_checker_tests"
+ provider = "testprovider.example.org"
def setUp(self):
pass
@@ -226,13 +229,20 @@ class ProviderCertCheckerTest(BaseLeapTest):
# 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', ''],
@@ -292,12 +302,19 @@ class ProviderCertCheckerHTTPSTests(BaseHTTPSServerTestCase, BaseLeapTest):
# 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()
+ checker = eipchecks.ProviderCertChecker(domain=self.provider)
+ credentials = "testuser", "testpassword"
self.assertTrue(checker.download_new_client_cert(
- uri=uri, verify=cacert))
+ credentials=credentials, uri=uri, verify=cacert))
# now download a malformed cert
uri = "https://%s/badclient.cert" % (self.get_server())
@@ -305,7 +322,7 @@ class ProviderCertCheckerHTTPSTests(BaseHTTPSServerTestCase, BaseLeapTest):
checker = eipchecks.ProviderCertChecker()
with self.assertRaises(ValueError):
self.assertTrue(checker.download_new_client_cert(
- uri=uri, verify=cacert))
+ credentials=credentials, uri=uri, verify=cacert))
# did we write cert to its path?
clientcertfile = eipspecs.client_cert_path()
@@ -339,7 +356,7 @@ class ProviderCertCheckerHTTPSTests(BaseHTTPSServerTestCase, BaseLeapTest):
def test_check_new_cert_needed(self):
# check: missing cert
- checker = eipchecks.ProviderCertChecker()
+ checker = eipchecks.ProviderCertChecker(domain=self.provider)
self.assertTrue(checker.check_new_cert_needed(skip_download=True))
# TODO check: malformed cert
# TODO check: expired cert
diff --git a/src/leap/eip/tests/test_config.py b/src/leap/eip/tests/test_config.py
index 6759b522..50538240 100644
--- a/src/leap/eip/tests/test_config.py
+++ b/src/leap/eip/tests/test_config.py
@@ -18,13 +18,14 @@ from leap.util.fileutil import mkdir_p
_system = platform.system()
-PROVIDER = BRANDING.get('provider_domain')
-PROVIDER_SHORTNAME = BRANDING.get('short_name')
+#PROVIDER = BRANDING.get('provider_domain')
+#PROVIDER_SHORTNAME = BRANDING.get('short_name')
class EIPConfigTest(BaseLeapTest):
__name__ = "eip_config_tests"
+ provider = "testprovider.example.org"
def setUp(self):
pass
@@ -74,7 +75,8 @@ class EIPConfigTest(BaseLeapTest):
args.append('--persist-tun')
args.append('--persist-key')
args.append('--remote')
- args.append('%s' % eipconfig.get_eip_gateway())
+ args.append('%s' % eipconfig.get_eip_gateway(
+ provider=self.provider))
# XXX get port!?
args.append('1194')
# XXX get proto
@@ -103,23 +105,23 @@ class EIPConfigTest(BaseLeapTest):
args.append(os.path.join(
self.home,
'.config', 'leap', 'providers',
- '%s' % PROVIDER,
+ '%s' % self.provider,
'keys', 'client',
'openvpn.pem'))
args.append('--key')
args.append(os.path.join(
self.home,
'.config', 'leap', 'providers',
- '%s' % PROVIDER,
+ '%s' % self.provider,
'keys', 'client',
'openvpn.pem'))
args.append('--ca')
args.append(os.path.join(
self.home,
'.config', 'leap', 'providers',
- '%s' % PROVIDER,
+ '%s' % self.provider,
'keys', 'ca',
- '%s-cacert.pem' % PROVIDER_SHORTNAME))
+ 'cacert.pem'))
return args
# build command string
@@ -141,7 +143,8 @@ class EIPConfigTest(BaseLeapTest):
print 'vpnbin = ', vpnbin
command, args = eipconfig.build_ovpn_command(
do_pkexec_check=False, vpnbin=vpnbin,
- socket_path="/tmp/test.socket")
+ socket_path="/tmp/test.socket",
+ provider=self.provider)
self.assertEqual(command, self.home + '/bin/openvpn')
self.assertEqual(args, self.get_expected_openvpn_args())
diff --git a/src/leap/eip/tests/test_eipconnection.py b/src/leap/eip/tests/test_eipconnection.py
index bb643ae0..aefca36f 100644
--- a/src/leap/eip/tests/test_eipconnection.py
+++ b/src/leap/eip/tests/test_eipconnection.py
@@ -19,6 +19,8 @@ from leap.testing.basetest import BaseLeapTest
_system = platform.system()
+PROVIDER = "testprovider.example.org"
+
class NotImplementedError(Exception):
pass
@@ -27,6 +29,7 @@ class NotImplementedError(Exception):
@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]
@@ -35,6 +38,7 @@ class MockedEIPConnection(EIPConnection):
class EIPConductorTest(BaseLeapTest):
__name__ = "eip_conductor_tests"
+ provider = PROVIDER
def setUp(self):
# XXX there's a conceptual/design
@@ -51,8 +55,8 @@ class EIPConductorTest(BaseLeapTest):
# XXX change to keys_checker invocation
# (see config_checker)
- keyfiles = (eipspecs.provider_ca_path(),
- eipspecs.client_cert_path())
+ 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)
@@ -61,6 +65,7 @@ class EIPConductorTest(BaseLeapTest):
# some methods mocked
self.manager = Mock(name="openvpnmanager_mock")
self.con = MockedEIPConnection()
+ self.con.provider = self.provider
self.con.run_openvpn_checks()
def tearDown(self):
@@ -118,8 +123,9 @@ class EIPConductorTest(BaseLeapTest):
self.con.status.CONNECTED)
# disconnect
+ self.con.cleanup = Mock()
self.con.disconnect()
- self.con._disconnect.assert_called_once_with()
+ self.con.cleanup.assert_called_once_with()
# new status should be disconnected
# XXX this should evolve and check no errors
diff --git a/src/leap/eip/tests/test_openvpnconnection.py b/src/leap/eip/tests/test_openvpnconnection.py
index 61769f04..0f27facf 100644
--- a/src/leap/eip/tests/test_openvpnconnection.py
+++ b/src/leap/eip/tests/test_openvpnconnection.py
@@ -76,13 +76,17 @@ class OpenVPNConnectionTest(BaseLeapTest):
#
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, "get_process_list") as mocked_psutil:
+ mocked_process = Mock()
+ mocked_process.name = "openvpn"
+ mocked_psutil.return_value = [mocked_process]
with self.assertRaises(eipexceptions.OpenVPNAlreadyRunning):
- mocked_process = Mock()
- mocked_process.name = "openvpn"
- mocked_psutil.return_value = [mocked_process]
openvpn_connection._check_if_running_instance()
+
openvpn_connection._check_if_running_instance()
@unittest.skipIf(_system == "Windows", "lin/mac only")
diff --git a/src/leap/gui/__init__.py b/src/leap/gui/__init__.py
index e69de29b..9b8f8746 100644
--- a/src/leap/gui/__init__.py
+++ b/src/leap/gui/__init__.py
@@ -0,0 +1,10 @@
+try:
+ import sip
+ sip.setapi('QString', 2)
+ sip.setapi('QVariant', 2)
+except ValueError:
+ pass
+
+import firstrun
+
+__all__ = ['firstrun']
diff --git a/src/leap/gui/constants.py b/src/leap/gui/constants.py
new file mode 100644
index 00000000..277f3540
--- /dev/null
+++ b/src/leap/gui/constants.py
@@ -0,0 +1,13 @@
+import time
+
+APP_LOGO = ':/images/leap-color-small.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
new file mode 100644
index 00000000..8a70d90e
--- /dev/null
+++ b/src/leap/gui/firstrun/__init__.py
@@ -0,0 +1,29 @@
+try:
+ import sip
+ sip.setapi('QString', 2)
+ sip.setapi('QVariant', 2)
+except ValueError:
+ pass
+
+import connect
+import intro
+import last
+import login
+import mixins
+import providerinfo
+import providerselect
+import providersetup
+import register
+import regvalidation
+
+__all__ = [
+ 'connect',
+ 'intro',
+ 'last',
+ 'login',
+ 'mixins',
+ 'providerinfo',
+ 'providerselect',
+ 'providersetup',
+ 'register',
+ 'regvalidation']
diff --git a/src/leap/gui/firstrun/connect.py b/src/leap/gui/firstrun/connect.py
new file mode 100644
index 00000000..a0fe021c
--- /dev/null
+++ b/src/leap/gui/firstrun/connect.py
@@ -0,0 +1,231 @@
+"""
+Connecting Page, used in First Run Wizard
+"""
+# XXX FIXME
+# DEPRECATED. All functionality moved to regvalidation
+# This file should be removed after checking that one is ok.
+# XXX
+
+import logging
+
+from PyQt4 import QtGui
+
+logger = logging.getLogger(__name__)
+
+from leap.base import auth
+
+from leap.gui.constants import APP_LOGO
+from leap.gui.styles import ErrorLabelStyleSheet
+
+
+class ConnectingPage(QtGui.QWizardPage):
+
+ # XXX change to a ValidationPage
+
+ def __init__(self, parent=None):
+ super(ConnectingPage, self).__init__(parent)
+
+ self.setTitle("Connecting")
+ self.setSubTitle('Connecting to provider.')
+
+ self.setPixmap(
+ QtGui.QWizard.LogoPixmap,
+ QtGui.QPixmap(APP_LOGO))
+
+ self.status = QtGui.QLabel("")
+ self.status.setWordWrap(True)
+ self.progress = QtGui.QProgressBar()
+ self.progress.setMaximum(100)
+ self.progress.hide()
+
+ # for pre-checks
+ self.status_line_1 = QtGui.QLabel()
+ self.status_line_2 = QtGui.QLabel()
+ self.status_line_3 = QtGui.QLabel()
+ self.status_line_4 = QtGui.QLabel()
+
+ # for connecting signals...
+ self.status_line_5 = QtGui.QLabel()
+
+ layout = QtGui.QGridLayout()
+ layout.addWidget(self.status, 0, 1)
+ layout.addWidget(self.progress, 5, 1)
+ layout.addWidget(self.status_line_1, 8, 1)
+ layout.addWidget(self.status_line_2, 9, 1)
+ layout.addWidget(self.status_line_3, 10, 1)
+ layout.addWidget(self.status_line_4, 11, 1)
+
+ # XXX to be used?
+ #self.validation_status = QtGui.QLabel("")
+ #self.validation_status.setStyleSheet(
+ #ErrorLabelStyleSheet)
+ #self.validation_msg = QtGui.QLabel("")
+
+ self.setLayout(layout)
+
+ self.goto_login_again = False
+
+ def set_status(self, status):
+ self.status.setText(status)
+ self.status.setWordWrap(True)
+
+ def set_status_line(self, line, status):
+ line = getattr(self, 'status_line_%s' % line)
+ if line:
+ line.setText(status)
+
+ def set_validation_status(self, status):
+ # Do not remember if we're using
+ # status lines > 3 now...
+ # if we are, move below
+ self.status_line_3.setStyleSheet(
+ ErrorLabelStyleSheet)
+ self.status_line_3.setText(status)
+
+ def set_validation_message(self, message):
+ self.status_line_4.setText(message)
+ self.status_line_4.setWordWrap(True)
+
+ def get_donemsg(self, msg):
+ return "%s ... done" % msg
+
+ 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)
+ conductor.run_checks()
+ self.conductor = conductor
+ errors = self.eip_error_check()
+ if not errors and 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")
+
+ 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
+ """
+ logger.debug('eip error check from connecting page')
+ errq = self.conductor.error_queue
+ # XXX missing!
+
+ def fetch_and_validate(self):
+ # XXX MOVE TO validate function in register-validation
+ import time
+ domain = self.field('provider_domain')
+ wizard = self.wizard()
+ #pconfig = wizard.providerconfig
+ eipconfigchecker = wizard.eipconfigchecker()
+ pCertChecker = wizard.providercertchecker(
+ domain=domain)
+
+ # username and password are in different fields
+ # if they were stored in log_in or sign_up pages.
+ from_login = self.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
+
+ self.progress.show()
+
+ fetching_eip_conf_msg = 'Fetching eip service configuration'
+ self.set_status(fetching_eip_conf_msg)
+ self.progress.setValue(30)
+
+ # Fetching eip service
+ eipconfigchecker.fetch_eip_service_config(
+ domain=domain)
+
+ self.status_line_1.setText(
+ self.get_donemsg(fetching_eip_conf_msg))
+
+ getting_client_cert_msg = 'Getting client certificate'
+ self.set_status(getting_client_cert_msg)
+ self.progress.setValue(66)
+
+ # Download cert
+ try:
+ pCertChecker.download_new_client_cert(
+ credentials=credentials,
+ # FIXME FIXME FIXME
+ # XXX FIX THIS!!!!!
+ # BUG #638. remove verify
+ # FIXME FIXME FIXME
+ verify=False)
+ except auth.SRPAuthenticationError as exc:
+ self.set_validation_status(
+ "Authentication error: %s" % exc.message)
+ return False
+
+ time.sleep(2)
+ self.status_line_2.setText(
+ self.get_donemsg(getting_client_cert_msg))
+
+ validating_clientcert_msg = 'Validating client certificate'
+ self.set_status(validating_clientcert_msg)
+ self.progress.setValue(90)
+ time.sleep(2)
+ self.status_line_3.setText(
+ self.get_donemsg(validating_clientcert_msg))
+
+ self.progress.setValue(100)
+ time.sleep(3)
+
+ # here we go! :)
+ self.run_eip_checks_for_provider_and_connect(domain)
+
+ #self.validation_block = self.wait_for_validation_block()
+
+ # XXX signal timeout!
+ return True
+
+ #
+ # wizardpage methods
+ #
+
+ def nextId(self):
+ wizard = self.wizard()
+ # XXX this does not work because
+ # page login has already been met
+ #if self.goto_login_again:
+ #next_ = "login"
+ #else:
+ #next_ = "lastpage"
+ next_ = "lastpage"
+ return wizard.get_page_index(next_)
+
+ def initializePage(self):
+ # XXX if we're coming from signup page
+ # we could say something like
+ # 'registration successful!'
+ self.status.setText(
+ "We have "
+ "all we need to connect with the provider.<br><br> "
+ "Click <i>next</i> to continue. ")
+ self.progress.setValue(0)
+ self.progress.hide()
+ self.status_line_1.setText('')
+ self.status_line_2.setText('')
+ self.status_line_3.setText('')
+
+ def validatePage(self):
+ # XXX remove
+ validated = self.fetch_and_validate()
+ return validated
diff --git a/src/leap/gui/firstrun/constants.py b/src/leap/gui/firstrun/constants.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/leap/gui/firstrun/constants.py
diff --git a/src/leap/gui/firstrun/intro.py b/src/leap/gui/firstrun/intro.py
new file mode 100644
index 00000000..4bb008c7
--- /dev/null
+++ b/src/leap/gui/firstrun/intro.py
@@ -0,0 +1,68 @@
+"""
+Intro page used in first run wizard
+"""
+
+from PyQt4 import QtGui
+
+from leap.gui.constants import APP_LOGO
+
+
+class IntroPage(QtGui.QWizardPage):
+ def __init__(self, parent=None):
+ super(IntroPage, self).__init__(parent)
+
+ self.setTitle("First run wizard.")
+
+ #self.setPixmap(
+ #QtGui.QWizard.WatermarkPixmap,
+ #QtGui.QPixmap(':/images/watermark1.png'))
+
+ self.setPixmap(
+ QtGui.QWizard.LogoPixmap,
+ QtGui.QPixmap(APP_LOGO))
+
+ label = QtGui.QLabel(
+ "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(
+ "Sign up for a new account.")
+ self.sign_up.setChecked(True)
+ self.log_in = QtGui.QRadioButton(
+ "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
new file mode 100644
index 00000000..13b2f548
--- /dev/null
+++ b/src/leap/gui/firstrun/last.py
@@ -0,0 +1,90 @@
+"""
+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
+
+logger = logging.getLogger(__name__)
+
+
+class LastPage(QtGui.QWizardPage):
+ def __init__(self, parent=None):
+ super(LastPage, self).__init__(parent)
+
+ self.setTitle("Connecting to Encrypted Internet Proxy service...")
+
+ self.setPixmap(
+ QtGui.QWizard.LogoPixmap,
+ QtGui.QPixmap(APP_LOGO))
+
+ #self.setPixmap(
+ #QtGui.QWizard.WatermarkPixmap,
+ #QtGui.QPixmap(':/images/watermark2.png'))
+
+ self.label = QtGui.QLabel()
+ self.label.setWordWrap(True)
+
+ # 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()
+
+ 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)
+
+ self.setLayout(layout)
+
+ 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('You are now using an encrypted connection!')
+ finishText = self.wizard().buttonText(
+ QtGui.QWizard.FinishButton)
+ finishText = finishText.replace('&', '')
+ self.label.setText(
+ "Click '<i>%s</i>' to end the wizard and "
+ "save your settings." % finishText)
+
+ @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 = 0
+ 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()
+ break
+ except GeneratorExit:
+ pass
+
+ def initializePage(self):
+ wizard = self.wizard()
+ if not wizard:
+ return
+ eip_status_handler = self.eip_status_handler()
+ eip_statuschange_signal = wizard.eip_statuschange_signal
+ if eip_statuschange_signal:
+ eip_statuschange_signal.connect(
+ lambda status: eip_status_handler.send(status))
diff --git a/src/leap/gui/firstrun/login.py b/src/leap/gui/firstrun/login.py
new file mode 100644
index 00000000..4271c774
--- /dev/null
+++ b/src/leap/gui/firstrun/login.py
@@ -0,0 +1,192 @@
+"""
+LogIn Page, used inf First Run Wizard
+"""
+from PyQt4 import QtCore
+from PyQt4 import QtGui
+
+#import requests
+
+from leap.gui.firstrun.mixins import UserFormMixIn
+
+from leap.gui.constants import APP_LOGO, FULL_USERNAME_REGEX
+from leap.gui.styles import ErrorLabelStyleSheet
+
+
+class LogInPage(QtGui.QWizardPage, UserFormMixIn):
+ def __init__(self, parent=None):
+ super(LogInPage, self).__init__(parent)
+
+ self.setTitle("Log In")
+ self.setSubTitle("Log in with your credentials.")
+ self.current_page = "login"
+
+ self.setPixmap(
+ QtGui.QWizard.LogoPixmap,
+ QtGui.QPixmap(APP_LOGO))
+
+ userNameLabel = QtGui.QLabel("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))
+ self.userNameLineEdit = userNameLineEdit
+
+ userPasswordLabel = QtGui.QLabel("&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(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)
+
+ self.setLayout(layout)
+
+ #self.registerField('is_login_wizard')
+
+ def onUserNameEdit(self, *args):
+ if self.initial_username_sample:
+ self.userNameLineEdit.setText('')
+ self.initial_username_sample = None
+
+ # pagewizard methods
+
+ #### begin possible refactor
+
+ def populateErrors(self):
+ # XXX could move this to ValidationMixin
+ # used in providerselect and register too
+
+ errors = self.wizard().get_validation_error(
+ self.current_page)
+ prev_er = getattr(self, 'prevalidation_error', None)
+ showerr = self.validationMsg.setText
+
+ if not errors and prev_er:
+ showerr(prev_er)
+ return
+
+ 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:
+ if prev_er:
+ showerr(prev_er)
+ return
+ # not the first time
+ if cur_str == bad_str:
+ showerr(errors)
+ else:
+ 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
+
+ #### end possible refactor
+
+ 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()
+ self.userNameLineEdit.setText('username@provider.example.org')
+ self.userNameLineEdit.cursorPositionChanged.connect(
+ self.onUserNameEdit)
+ self.initial_username_sample = True
+
+ def validatePage(self):
+ #wizard = self.wizard()
+ #eipconfigchecker = wizard.eipconfigchecker()
+
+ full_username = self.userNameLineEdit.text()
+ password = self.userPasswordLineEdit.text()
+ if full_username.count('@') != 1:
+ self.set_prevalidation_error(
+ "Username must be in the username@provider form.")
+ return False
+
+ username, domain = full_username.split('@')
+ self.setField('provider_domain', domain)
+ self.setField('login_userName', username)
+ self.setField('login_userPassword', password)
+
+ ####################################################
+ # Validation logic:
+ # move to provider setup page
+ ####################################################
+ # Able to contact domain?
+ # can get definition?
+ # two-by-one
+ #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:
+ #self.set_validation_status(exc.message[1])
+ #return False
+ #except requests.exceptions.HTTPError as exc:
+ #self.set_validation_status(exc.message)
+ #return False
+ #wizard.set_providerconfig(
+ #eipconfigchecker.defaultprovider.config)
+ ####################################################
+
+ # XXX I think this is not needed
+ # since we're also checking for the is_signup field.
+ self.wizard().from_login = True
+
+ # some cleanup before we leave the page
+ self.cleanup_errormsg()
+
+ return True
diff --git a/src/leap/gui/firstrun/mixins.py b/src/leap/gui/firstrun/mixins.py
new file mode 100644
index 00000000..c4731893
--- /dev/null
+++ b/src/leap/gui/firstrun/mixins.py
@@ -0,0 +1,18 @@
+"""
+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
new file mode 100644
index 00000000..e642fcd0
--- /dev/null
+++ b/src/leap/gui/firstrun/providerinfo.py
@@ -0,0 +1,230 @@
+"""
+Provider Info Page, used in First run Wizard
+"""
+import logging
+
+from PyQt4 import QtCore
+from PyQt4 import QtGui
+
+import requests
+
+from leap.base import exceptions as baseexceptions
+#from leap.crypto import certs
+from leap.eip import exceptions as eipexceptions
+
+from leap.gui.progress import ValidationPage
+from leap.util.web import get_https_domain_and_port
+
+from leap.gui.constants import APP_LOGO, pause_for_user
+
+logger = logging.getLogger(__name__)
+
+
+class ProviderInfoPage(ValidationPage):
+ def __init__(self, parent=None):
+ super(ProviderInfoPage, self).__init__(parent)
+
+ self.setTitle("Provider Info")
+ #self.setSubTitle("Available information about chosen provider.")
+
+ self.setPixmap(
+ QtGui.QWizard.LogoPixmap,
+ QtGui.QPixmap(APP_LOGO))
+
+ self.prev_page = "providerselection"
+ #self.current_page = "providerinfo"
+
+ def create_info_panel(self):
+ # Use stacked widget instead
+ # of reparenting the layout.
+
+ self.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...
+
+ layout.addWidget(displayName)
+ layout.addWidget(description)
+ layout.addWidget(enrollment_policy)
+ layout.addStretch(1)
+
+ info.setLayout(layout)
+ self.infoWidget.addWidget(info)
+
+ self.layout.addWidget(self.infoWidget)
+
+ # add refs to self to allow for
+ # updates.
+ self.displayName = displayName
+ self.description = description
+ 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('display_name')
+ display_name = dn[lang] if dn else ''
+ self.displayName.setText(
+ "<b>%s</b>" % display_name)
+
+ desc = pconfig.get('description')
+ description_text = desc[lang] if desc else ''
+ self.description.setText(
+ "<i>%s</i>" % description_text)
+
+ enroll = pconfig.get('enrollment_policy')
+ if enroll:
+ self.enrollment_policy.setText(
+ 'enrollment policy: %s' % enroll)
+
+ def _do_checks(self, update_signal=None):
+ """
+ executes actual checks in a separate thread
+ """
+ def pause_and_finish():
+ update_signal.emit("end_sentinel", 100)
+ pause_for_user()
+
+ wizard = self.wizard()
+ prevpage = "providerselection"
+
+ full_domain = self.field('provider_domain')
+
+ # 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)
+
+ update_signal.emit("head_sentinel", 0)
+ pause_for_user()
+
+ ########################
+ # 1) try name resolution
+ ########################
+ update_signal.emit("Checking that server is reachable", 20)
+ logger.debug('checking name resolution')
+ try:
+ netchecker.check_name_resolution(
+ domain)
+
+ except baseexceptions.LeapException as exc:
+ logger.error(exc.message)
+ wizard.set_validation_error(
+ prevpage, exc.usermessage)
+ pause_and_finish()
+ return False
+
+ #########################
+ # 2) try https connection
+ #########################
+ update_signal.emit("Checking secure connection to provider", 40)
+ logger.debug('checking https connection')
+ try:
+ providercertchecker.is_https_working(
+ "https://%s" % _domain,
+ verify=True)
+
+ except eipexceptions.HttpsBadCertError as exc:
+ logger.debug('exception')
+ # XXX skipping for now...
+ ##############################################
+ # We had this validation logic
+ # in the provider selection page before
+ ##############################################
+ #if self.trustProviderCertCheckBox.isChecked():
+ #pass
+ #else:
+ wizard.set_validation_error(
+ prevpage, exc.usermessage)
+ #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
+ pause_and_finish()
+ return False
+
+ except baseexceptions.LeapException as exc:
+ wizard.set_validation_error(
+ prevpage, exc.usermessage)
+ pause_and_finish()
+ return False
+
+ ##################################
+ # 3) try download provider info...
+ ##################################
+
+ update_signal.emit("Downloading provider info", 70)
+ 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:
+ # XXX we should have catched this before.
+ # but cert checking is broken.
+ wizard.set_validation_error(
+ prevpage,
+ "Could not get info from provider.")
+ pause_and_finish()
+ return False
+ except requests.exceptions.ConnectionError:
+ wizard.set_validation_error(
+ prevpage,
+ "Could not download provider info "
+ "(refused conn.).")
+ pause_and_finish()
+ return False
+ # XXX catch more errors...
+
+ # We're done!
+ pause_and_finish()
+
+ def _do_validation(self):
+ """
+ called after _do_checks has finished
+ (connected to checker thread finished signal)
+ """
+ print 'validation...'
+ prevpage = "providerselection"
+ errors = self.wizard().get_validation_error(prevpage)
+
+ if not errors:
+ self.progress.hide()
+ self.stepsTableWidget.hide()
+ self.create_info_panel()
+ self.show_provider_info()
+
+ else:
+ logger.debug('going back with errors')
+ logger.debug('ERRORS: %s' % errors)
+ self.go_back()
+
+ def nextId(self):
+ wizard = self.wizard()
+ next_ = "providersetupvalidation"
+ return wizard.get_page_index(next_)
diff --git a/src/leap/gui/firstrun/providerselect.py b/src/leap/gui/firstrun/providerselect.py
new file mode 100644
index 00000000..8d1aa869
--- /dev/null
+++ b/src/leap/gui/firstrun/providerselect.py
@@ -0,0 +1,211 @@
+"""
+Select Provider Page, used in First Run Wizard
+"""
+import logging
+
+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.constants import APP_LOGO
+from leap.gui.styles import ErrorLabelStyleSheet
+
+logger = logging.getLogger(__name__)
+
+
+class SelectProviderPage(QtGui.QWizardPage):
+ def __init__(self, parent=None, providers=None):
+ super(SelectProviderPage, self).__init__(parent)
+
+ self.setTitle("Enter Provider")
+ self.setSubTitle(
+ "Please enter the domain of the provider you want "
+ "to use for your connection."
+ )
+ self.setPixmap(
+ QtGui.QWizard.LogoPixmap,
+ QtGui.QPixmap(APP_LOGO))
+
+ self.did_cert_check = False
+ self.current_page = 'providerselection'
+
+ 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-z\d_-.]+$")
+ 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(ErrorLabelStyleSheet)
+ self.validationMsg = validationMsg
+
+ # 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 = "Server certificate could not be verified."
+
+ self.certInfo = QtGui.QLabel("")
+ self.certInfo.setWordWrap(True)
+ self.certWarning = QtGui.QLabel("")
+ self.trustProviderCertCheckBox = QtGui.QCheckBox(
+ "&Trust this provider certificate.")
+
+ self.trustProviderCertCheckBox.stateChanged.connect(
+ self.onTrustCheckChanged)
+ self.providerNameEdit.textChanged.connect(
+ self.onProviderChanged)
+
+ layout = QtGui.QGridLayout()
+ layout.addWidget(validationMsg, 0, 2)
+ layout.addWidget(providerNameLabel, 1, 1)
+ layout.addWidget(providerNameEdit, 1, 2)
+
+ # XXX get a groupbox or something....
+ certinfoGroup = QtGui.QGroupBox("Certificate validation")
+ certinfoLayout = QtGui.QVBoxLayout()
+ certinfoLayout.addWidget(self.certInfo)
+ certinfoLayout.addWidget(self.certWarning)
+ certinfoLayout.addWidget(self.trustProviderCertCheckBox)
+ certinfoGroup.setLayout(certinfoLayout)
+
+ layout.addWidget(certinfoGroup, 4, 1, 4, 2)
+ self.certinfoGroup = certinfoGroup
+ self.certinfoGroup.hide()
+
+ self.setLayout(layout)
+
+ def is_insecure_cert_trusted(self):
+ return self.trustProviderCertCheckBox.isChecked()
+
+ def onTrustCheckChanged(self, state):
+ 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 onProviderChanged(self, text):
+ self.completeChanged.emit()
+
+ def reset_validation_status(self):
+ """
+ empty the validation msg
+ """
+ self.validationMsg.setText('')
+
+ #def set_validation_status(selF, STATUS):
+ #self.validationMsg.setText(status)
+
+ def add_cert_info(self, certinfo):
+ self.certWarning.setText(
+ "Do you want to <b>trust this provider certificate?</b>")
+ self.certInfo.setText(
+ 'SHA-256 fingerprint: <i>%s</i><br>' % certinfo)
+ self.certInfo.setWordWrap(True)
+ self.certinfoGroup.show()
+
+ # pagewizard methods
+
+ def isComplete(self):
+ provider = self.providerNameEdit.text()
+
+ if not provider:
+ return False
+ else:
+ if self.is_insecure_cert_trusted():
+ return True
+ if not self.did_cert_check:
+ 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
+ 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:
+ 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(SelectProviderPage, self).paintEvent(event)
+ self.populateErrors()
+
+ def initializePage(self):
+ self.validationMsg.setText('')
+ self.certinfoGroup.hide()
+
+ 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
new file mode 100644
index 00000000..3fb9a19b
--- /dev/null
+++ b/src/leap/gui/firstrun/providersetup.py
@@ -0,0 +1,171 @@
+"""
+Provider Setup Validation Page,
+used if First Run Wizard
+"""
+import logging
+
+from PyQt4 import QtGui
+
+from leap.base import auth
+from leap.gui.progress import ValidationPage
+
+from leap.gui.constants import APP_LOGO, pause_for_user
+
+logger = logging.getLogger(__name__)
+
+
+class ProviderSetupValidationPage(ValidationPage):
+ def __init__(self, parent=None):
+ super(ProviderSetupValidationPage, self).__init__(parent)
+ is_signup = self.field("is_signup")
+ self.is_signup = is_signup
+
+ self.setTitle("Setting up provider")
+ #self.setSubTitle(
+ #"auto configuring provider...")
+
+ self.setPixmap(
+ QtGui.QWizard.LogoPixmap,
+ QtGui.QPixmap(APP_LOGO))
+
+ def _do_checks(self, update_signal=None):
+ """
+ executes actual checks 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)
+
+ update_signal.emit('head_sentinel', 0)
+
+ ######################################
+ if not self.is_signup:
+ # We come from login page.
+ # We try a call to an authenticated
+ # page here as a mean to catch
+ # srp authentication errors while
+ # we are still at one page's reach
+ # of the login credentials input page.
+ # (so we're able to go back an correct)
+
+ step = "fetch_eipcert"
+ update_signal.emit('validating credentials', 20)
+
+ unamek = 'login_userName'
+ passwk = 'login_userPassword'
+
+ username = self.field(unamek)
+ password = self.field(passwk)
+ credentials = username, password
+
+ #################
+ # FIXME #BUG #638
+ verify = False
+
+ try:
+ pCertChecker.download_new_client_cert(
+ credentials=credentials,
+ verify=verify)
+
+ except auth.SRPAuthenticationError as exc:
+ self.set_error(
+ step,
+ "Authentication error: %s" % exc.message)
+ return False
+
+ pause_for_user()
+
+ #######################################
+
+ update_signal.emit('Fetching CA certificate', 30)
+ pause_for_user()
+
+ 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)
+
+ pCertChecker.download_ca_cert(
+ uri=ca_cert_uri,
+ verify=False)
+ pause_for_user()
+
+ update_signal.emit('Checking CA fingerprint', 66)
+ #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
+
+ update_signal.emit('Validating api certificate', 90)
+
+ #api_uri = pconfig.get('api_uri', None)
+ #try:
+ #api_cert_verified = pCertChecker.verify_api_https(api_uri)
+ #except requests.exceptions.SSLError as exc:
+ #logger.error('BUG #638. %s' % exc.message)
+ # XXX RAISE! See #638
+ # bypassing until the hostname is fixed.
+ # We probably should raise yet-another-warning
+ # here saying user that the hostname "XX.XX.XX.XX' does not
+ # match 'foo.bar.baz'
+ #api_cert_verified = True
+
+ #if not api_cert_verified:
+ # XXX update validationMsg
+ # should catch exception
+ #return False
+ pause_for_user()
+ #ca_cert_path = checker.ca_cert_path
+
+ update_signal.emit('end_sentinel', 100)
+ pause_for_user()
+
+ def _do_validation(self):
+ """
+ called after _do_checks has finished
+ (connected to checker thread finished signal)
+ """
+ prevpage = "providerselection" if self.is_signup else "login"
+ wizard = self.wizard()
+
+ if self.errors:
+ logger.debug('going back with errors')
+ name, first_error = self.pop_first_error()
+ wizard.set_validation_error(
+ prevpage,
+ first_error)
+ self.go_back()
+ else:
+ logger.debug('going next')
+ self.go_next()
+
+ def nextId(self):
+ wizard = self.wizard()
+ if not wizard:
+ return
+ is_signup = self.field('is_signup')
+ if is_signup is True:
+ next_ = 'signup'
+ if is_signup is False:
+ # XXX bad name. change to connect again.
+ next_ = 'signupvalidation'
+ return wizard.get_page_index(next_)
diff --git a/src/leap/gui/firstrun/register.py b/src/leap/gui/firstrun/register.py
new file mode 100644
index 00000000..b46dd4cd
--- /dev/null
+++ b/src/leap/gui/firstrun/register.py
@@ -0,0 +1,188 @@
+"""
+Register User Page, used in First Run Wizard
+"""
+import logging
+
+
+from PyQt4 import QtCore
+from PyQt4 import QtGui
+
+from leap.gui.firstrun.mixins import UserFormMixIn
+
+logger = logging.getLogger(__name__)
+
+from leap.gui.constants import APP_LOGO, BARE_USERNAME_REGEX
+from leap.gui.styles import ErrorLabelStyleSheet
+
+
+class RegisterUserPage(QtGui.QWizardPage, UserFormMixIn):
+
+ def __init__(self, parent=None):
+
+ super(RegisterUserPage, self).__init__(parent)
+
+ self.setTitle("Sign Up")
+
+ self.setPixmap(
+ QtGui.QWizard.LogoPixmap,
+ QtGui.QPixmap(APP_LOGO))
+
+ self.current_page = "signup"
+
+ userNameLabel = QtGui.QLabel("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("&Password:")
+ self.userPasswordLineEdit = QtGui.QLineEdit()
+ self.userPasswordLineEdit.setEchoMode(
+ QtGui.QLineEdit.Password)
+ userPasswordLabel.setBuddy(self.userPasswordLineEdit)
+
+ userPassword2Label = QtGui.QLabel("Password (again):")
+ self.userPassword2LineEdit = QtGui.QLineEdit()
+ self.userPassword2LineEdit.setEchoMode(
+ QtGui.QLineEdit.Password)
+ userPassword2Label.setBuddy(self.userPassword2LineEdit)
+
+ rememberPasswordCheckBox = QtGui.QCheckBox(
+ "&Remember username and password.")
+ rememberPasswordCheckBox.setChecked(True)
+
+ self.registerField('userName*', self.userNameLineEdit)
+ self.registerField('userPassword*', self.userPasswordLineEdit)
+
+ # 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)
+ self.setLayout(layout)
+
+ # pagewizard methods
+
+ def populateErrors(self):
+ # XXX could move this to ValidationMixin
+ # used in providerselect too
+
+ errors = self.wizard().get_validation_error(
+ self.current_page)
+ if errors:
+ bad_str = getattr(self, 'bad_string', None)
+ cur_str = self.userNameLineEdit.text()
+ showerr = self.validationMsg.setText
+ 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:
+ 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(RegisterUserPage, self).paintEvent(event)
+ self.populateErrors()
+
+ def set_prevalidation_error(self, error):
+ self.prevalidation_error = error
+
+ def validatePage(self):
+ """
+ we only pre-validate here password weakness
+ stuff, or any other client side validation
+ that we think of.
+ real server validation is made on next page,
+ and if any errors are thrown there we come back
+ and re-display the validation label.
+ """
+
+ #username = self.userNameLineEdit.text()
+ password = self.userPasswordLineEdit.text()
+ password2 = self.userPassword2LineEdit.text()
+
+ # we better have here
+ # some call to a password checker...
+ # to assess strenght and avoid silly stuff.
+
+ if password != password2:
+ self.set_prevalidation_error('Password does not match.')
+ return False
+
+ if len(password) < 6:
+ self.set_prevalidation_error('Password too short.')
+ return False
+
+ if password == "123456":
+ # joking, but not too much.
+ self.set_prevalidation_error('Password too obvious.')
+ return False
+
+ # some cleanup before we leave the page
+ self.cleanup_errormsg()
+
+ # go
+ return True
+
+ def initializePage(self):
+ """
+ inits wizard page
+ """
+ provider = self.field('provider_domain')
+ self.setSubTitle(
+ "Register a new user with provider %s." %
+ provider)
+ self.validationMsg.setText('')
+ self.userPassword2LineEdit.setText('')
+
+ def nextId(self):
+ wizard = self.wizard()
+ if not wizard:
+ return
+ return wizard.get_page_index('signupvalidation')
diff --git a/src/leap/gui/firstrun/regvalidation.py b/src/leap/gui/firstrun/regvalidation.py
new file mode 100644
index 00000000..dbe30d3c
--- /dev/null
+++ b/src/leap/gui/firstrun/regvalidation.py
@@ -0,0 +1,265 @@
+"""
+Provider Setup Validation Page,
+used in First Run Wizard
+"""
+# XXX This page is called regvalidation
+# but it's implementing functionality in the former
+# connect page.
+# We should remame it to connect again, when we integrate
+# the login branch of the wizard.
+
+import logging
+import json
+import socket
+
+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, pause_for_user
+
+logger = logging.getLogger(__name__)
+
+
+class RegisterUserValidationPage(ValidationPage):
+
+ def __init__(self, parent=None):
+ super(RegisterUserValidationPage, self).__init__(parent)
+ is_signup = self.field("is_signup")
+ self.is_signup = is_signup
+
+ if is_signup:
+ title = "User Creation"
+ subtitle = "Registering account with provider."
+ else:
+ title = "Connecting..."
+ # XXX uh... really?
+ subtitle = "Checking connection with provider."
+
+ self.setTitle(title)
+ self.setSubTitle(subtitle)
+
+ 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)
+ _domain = u"%s:%s" % (domain, port) if port != 443 else unicode(domain)
+
+ # FIXME #BUG 638 FIXME FIXME FIXME
+ verify = False # !!!!!!!!!!!!!!!!
+ # FIXME #BUG 638 FIXME FIXME FIXME
+
+ ###########################################
+ # Set Credentials.
+ # username and password are in different fields
+ # if they were stored in log_in or sign_up pages.
+ is_signup = self.is_signup
+
+ unamek_base = 'userName'
+ passwk_base = 'userPassword'
+ unamek = 'login_%s' % unamek_base if not is_signup else unamek_base
+ passwk = 'login_%s' % passwk_base if not is_signup else passwk_base
+
+ username = self.field(unamek)
+ password = self.field(passwk)
+ credentials = username, password
+
+ eipconfigchecker = wizard.eipconfigchecker(domain=_domain)
+ #XXX change for _domain (sanitized)
+ pCertChecker = wizard.providercertchecker(
+ domain=full_domain)
+
+ ###########################################
+ # only if from signup
+ if is_signup:
+ signup = auth.LeapSRPRegister(
+ schema="https",
+ provider=full_domain,
+ verify=verify)
+
+ update_signal.emit("head_sentinel", 0)
+
+ ##################################################
+ # 1) register user
+ ##################################################
+ # only if from signup.
+
+ if is_signup:
+
+ step = "register"
+ update_signal.emit("checking availability", 20)
+ update_signal.emit("registering with provider", 40)
+ logger.debug('registering user')
+
+ try:
+ ok, req = signup.register_user(
+ username, password)
+
+ except socket.timeout:
+ self.set_error(
+ step,
+ "Error connecting to provider (timeout)")
+ pause_for_user()
+ return False
+
+ except requests.exceptions.ConnectionError as exc:
+ logger.error(exc.message)
+ self.set_error(
+ step,
+ "Error connecting to provider "
+ "(connection error)")
+ # XXX we should signal a BAD step
+ pause_for_user()
+ update_signal.emit("connection error!", 50)
+ pause_for_user()
+ return False
+
+ # XXX check for != OK instead???
+
+ if req.status_code in (404, 500):
+ self.set_error(
+ step,
+ "Error during registration (%s)" % req.status_code)
+ pause_for_user()
+ return False
+
+ validation_msgs = json.loads(req.content)
+ errors = validation_msgs.get('errors', None)
+ logger.debug('validation errors: %s' % validation_msgs)
+
+ if errors and errors.get('login', None):
+ # XXX this sometimes catch the blank username
+ # but we're not allowing that (soon)
+ self.set_error(
+ step,
+ 'Username not available.')
+ pause_for_user()
+ return False
+
+ pause_for_user()
+
+ ##################################################
+ # 2) fetching eip service config
+ ##################################################
+
+ step = "fetch_eipconf"
+ fetching_eipconf_msg = "Fetching eip service configuration"
+ update_signal.emit(fetching_eipconf_msg, 60)
+ try:
+ eipconfigchecker.fetch_eip_service_config(
+ domain=full_domain)
+
+ # XXX get specific exception
+ except:
+ self.set_error(
+ step,
+ 'Could not download eip config.')
+ pause_for_user()
+ return False
+ pause_for_user()
+
+ ##################################################
+ # 3) getting client certificate
+ ##################################################
+ # XXX maybe only do this if we come from signup
+
+ step = "fetch_eipcert"
+ fetching_clientcert_msg = "Fetching eip certificate"
+ update_signal.emit(fetching_clientcert_msg, 80)
+
+ try:
+ pCertChecker.download_new_client_cert(
+ credentials=credentials,
+ verify=verify)
+
+ except auth.SRPAuthenticationError as exc:
+ self.set_error(
+ step,
+ "Authentication error: %s" % exc.message)
+ return False
+
+ pause_for_user()
+
+ ################
+ # end !
+ ################
+
+ update_signal.emit("end_sentinel", 100)
+ pause_for_user()
+
+ # here we go! :)
+ 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)
+ conductor.run_checks()
+ self.conductor = conductor
+ errors = self.eip_error_check()
+ if not errors and 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.")
+
+ 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
+ """
+ logger.debug('eip error check from connecting page')
+ errq = self.conductor.error_queue
+ # XXX missing!
+
+ def _do_validation(self):
+ """
+ called after _do_checks has finished
+ (connected to checker thread finished signal)
+ """
+ prevpage = "signup" if self.is_signup else "login"
+
+ 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()
+ else:
+ logger.debug('going next')
+ # check if this "next" interferes
+ # with the eip signal.
+ self.go_next()
+
+ def nextId(self):
+ wizard = self.wizard()
+ if not wizard:
+ return
+ return wizard.get_page_index('lastpage')
diff --git a/src/leap/gui/firstrun/tests/integration/fake_provider.py b/src/leap/gui/firstrun/tests/integration/fake_provider.py
new file mode 100755
index 00000000..33ee0ee6
--- /dev/null
+++ b/src/leap/gui/firstrun/tests/integration/fake_provider.py
@@ -0,0 +1,295 @@
+#!/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
+
+# 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!
+ certs_path = get_certs_path()
+
+ cert = crypto.X509Certificate(
+ open(certs_path + '/leaptestscert.pem').read())
+ key = crypto.X509PrivateKey(
+ open(certs_path + '/leaptestskey.pem').read())
+ ca = crypto.X509Certificate(
+ open(certs_path + '/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'."""
+ certs_path = get_certs_path()
+
+ ctx = SSL.Context(SSL.SSLv23_METHOD)
+ ctx.use_certificate_file(certs_path + '/leaptestscert.pem')
+ ctx.use_privatekey_file(certs_path + '/leaptestskey.pem')
+ return ctx
+
+
+if __name__ == "__main__":
+
+ from twisted.python import log
+ log.startLogging(sys.stdout)
+
+ 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()
diff --git a/src/leap/gui/firstrun/wizard.py b/src/leap/gui/firstrun/wizard.py
new file mode 100755
index 00000000..bbb48149
--- /dev/null
+++ b/src/leap/gui/firstrun/wizard.py
@@ -0,0 +1,277 @@
+#!/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.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.
+[ ] Refactor widgets out.
+[ ] Follow more MVC style.
+[ ] Maybe separate "first run wizard" into different wizards
+ that share some of the pages?
+"""
+
+
+class FirstRunWizard(QtGui.QWizard):
+
+ def __init__(
+ self,
+ conductor_instance,
+ parent=None,
+ eip_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.eip_username = eip_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.eip_username)
+ self.from_login = False
+
+ pages_dict = 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),
+ ('signupvalidation',
+ firstrun.regvalidation.RegisterUserValidationPage),
+ ('connecting', firstrun.connect.ConnectingPage),
+ ('lastpage', firstrun.last.LastPage)
+ ))
+ self.add_pages_from_dict(pages_dict)
+
+ self.validation_errors = {}
+
+ self.setPixmap(
+ QtGui.QWizard.BannerPixmap,
+ QtGui.QPixmap(':/images/banner.png'))
+ self.setPixmap(
+ QtGui.QWizard.BackgroundPixmap,
+ QtGui.QPixmap(':/images/background.png'))
+
+ self.setWindowTitle("First Run Wizard")
+
+ # TODO: set style for MAC / windows ...
+ #self.setWizardStyle()
+
+ 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)
+
+ def set_validation_error(self, pagename, error):
+ self.validation_errors[pagename] = error
+
+ def get_validation_error(self, pagename):
+ return self.validation_errors.get(pagename, None)
+
+ def set_providerconfig(self, providerconfig):
+ self.providerconfig = providerconfig
+
+ def setWindowFlags(self, flags):
+ logger.debug('setting window flags')
+ QtGui.QWizard.setWindowFlags(self, flags)
+
+ def focusOutEvent(self, event):
+ # needed ?
+ self.setFocus(True)
+ self.activateWindow()
+ self.raise_()
+ self.show()
+
+ 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("eip_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()
+
+ def get_provider_by_index(self):
+ provider = self.field('provider_index')
+ return self.providers[provider]
+
+ def get_random_str(self, n):
+ 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))
+
+
+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/firstrunwizard.py b/src/leap/gui/firstrunwizard.py
deleted file mode 100755
index a76865fd..00000000
--- a/src/leap/gui/firstrunwizard.py
+++ /dev/null
@@ -1,507 +0,0 @@
-#!/usr/bin/env python
-import logging
-import json
-import socket
-
-import sip
-sip.setapi('QString', 2)
-sip.setapi('QVariant', 2)
-
-from PyQt4 import QtCore
-from PyQt4 import QtGui
-
-from leap.crypto import leapkeyring
-from leap.gui import mainwindow_rc
-
-logger = logging.getLogger(__name__)
-
-APP_LOGO = ':/images/leap-color-small.png'
-
-# registration ######################
-# move to base/
-import binascii
-
-import requests
-import srp
-
-from leap.base import constants as baseconstants
-
-SIGNUP_TIMEOUT = getattr(baseconstants, 'SIGNUP_TIMEOUT', 5)
-
-
-class LeapSRPRegister(object):
-
- def __init__(self,
- schema="https",
- provider=None,
- port=None,
- register_path="1/users.json",
- method="POST",
- fetcher=requests,
- srp=srp,
- hashfun=srp.SHA256,
- ng_constant=srp.NG_1024):
-
- self.schema = schema
- self.provider = provider
- self.port = port
- 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
- @rvalue: (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)
- logger.debug(req)
- logger.debug('user_data: %s', user_data)
- #logger.debug('response: %s', req.text)
- # we catch it in the form
- #req.raise_for_status()
- return (req.ok, req)
-
-######################################
-
-ErrorLabelStyleSheet = """
-QLabel { color: red;
- font-weight: bold}
-"""
-
-
-class FirstRunWizard(QtGui.QWizard):
-
- def __init__(
- self, parent=None, providers=None,
- success_cb=None):
- super(FirstRunWizard, self).__init__(
- parent,
- QtCore.Qt.WindowStaysOnTopHint)
-
- # XXX hardcoded for tests
- if not providers:
- providers = ('springbok',)
- self.providers = providers
-
- # success callback
- self.success_cb = success_cb
-
- self.addPage(IntroPage())
- self.addPage(SelectProviderPage(providers=providers))
-
- self.addPage(RegisterUserPage(wizard=self))
- #self.addPage(GlobalEIPSettings())
- self.addPage(LastPage())
-
- self.setPixmap(
- QtGui.QWizard.BannerPixmap,
- QtGui.QPixmap(':/images/banner.png'))
- self.setPixmap(
- QtGui.QWizard.BackgroundPixmap,
- QtGui.QPixmap(':/images/background.png'))
-
- self.setWindowTitle("First Run Wizard")
-
- # TODO: set style for MAC / windows ...
- #self.setWizardStyle()
-
- def setWindowFlags(self, flags):
- logger.debug('setting window flags')
- QtGui.QWizard.setWindowFlags(self, flags)
-
- def focusOutEvent(self, event):
- # needed ?
- self.setFocus(True)
- self.activateWindow()
- self.raise_()
- self.show()
-
- def accept(self):
- """
- final step in the wizard.
- gather the info, update settings
- and call the success callback.
- """
- provider = self.get_provider()
- username = self.field('userName')
- #password = self.field('userPassword')
- remember_pass = self.field('rememberPassword')
-
- logger.debug('chosen provider: %s', provider)
- logger.debug('username: %s', username)
- logger.debug('remember password: %s', remember_pass)
- super(FirstRunWizard, self).accept()
-
- settings = QtCore.QSettings()
- settings.setValue("FirstRunWizardDone", True)
- settings.setValue(
- "eip_%s_username" % provider,
- username)
- settings.setValue("%s_remember_pass" % provider, remember_pass)
-
- seed = self.get_random_str(10)
- settings.setValue("%s_seed" % provider, seed)
-
- # Commenting out for 0.2.0 release
- # since we did not fix #744 on time.
-
- #leapkeyring.leap_set_password(username, password, seed=seed)
-
- logger.debug('First Run Wizard Done.')
- cb = self.success_cb
- if cb and callable(cb):
- self.success_cb()
-
- def get_provider(self):
- provider = self.field('provider_index')
- return self.providers[provider]
-
- def get_random_str(self, n):
- 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))
-
-
-class IntroPage(QtGui.QWizardPage):
- def __init__(self, parent=None):
- super(IntroPage, self).__init__(parent)
-
- self.setTitle("First run wizard.")
-
- #self.setPixmap(
- #QtGui.QWizard.WatermarkPixmap,
- #QtGui.QPixmap(':/images/watermark1.png'))
-
- label = QtGui.QLabel(
- "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 of the Leap App.")
-
- label.setWordWrap(True)
-
- layout = QtGui.QVBoxLayout()
- layout.addWidget(label)
- self.setLayout(layout)
-
-
-class SelectProviderPage(QtGui.QWizardPage):
- def __init__(self, parent=None, providers=None):
- super(SelectProviderPage, self).__init__(parent)
-
- self.setTitle("Select Provider")
- self.setSubTitle(
- "Please select which provider do you want "
- "to use for your connection."
- )
- self.setPixmap(
- QtGui.QWizard.LogoPixmap,
- QtGui.QPixmap(APP_LOGO))
-
- providerNameLabel = QtGui.QLabel("&Provider:")
-
- providercombo = QtGui.QComboBox()
- if providers:
- for provider in providers:
- providercombo.addItem(provider)
- providerNameSelect = providercombo
-
- providerNameLabel.setBuddy(providerNameSelect)
-
- self.registerField('provider_index', providerNameSelect)
-
- layout = QtGui.QGridLayout()
- layout.addWidget(providerNameLabel, 0, 0)
- layout.addWidget(providerNameSelect, 0, 1)
- self.setLayout(layout)
-
-
-class RegisterUserPage(QtGui.QWizardPage):
- setSigningUpStatus = QtCore.pyqtSignal([])
-
- def __init__(self, parent=None, wizard=None):
- super(RegisterUserPage, self).__init__(parent)
-
- # bind wizard page signals
- self.setSigningUpStatus.connect(
- self.set_status_validating)
-
- # XXX check for no wizard pased
- # getting provider from previous step
- provider = wizard.get_provider()
-
- self.setTitle("User registration")
- self.setSubTitle(
- "Register a new user with provider %s." %
- provider)
- self.setPixmap(
- QtGui.QWizard.LogoPixmap,
- QtGui.QPixmap(APP_LOGO))
-
- rememberPasswordCheckBox = QtGui.QCheckBox(
- "&Remember password.")
- rememberPasswordCheckBox.setChecked(True)
-
- userNameLabel = QtGui.QLabel("User &name:")
- userNameLineEdit = QtGui.QLineEdit()
- userNameLineEdit.cursorPositionChanged.connect(
- self.reset_validation_status)
- userNameLabel.setBuddy(userNameLineEdit)
-
- # add regex validator
- usernameRe = QtCore.QRegExp(r"^[A-Za-z\d_]+$")
- userNameLineEdit.setValidator(
- QtGui.QRegExpValidator(usernameRe, self))
- self.userNameLineEdit = userNameLineEdit
-
- userPasswordLabel = QtGui.QLabel("&Password:")
- self.userPasswordLineEdit = QtGui.QLineEdit()
- self.userPasswordLineEdit.setEchoMode(
- QtGui.QLineEdit.Password)
-
- userPasswordLabel.setBuddy(self.userPasswordLineEdit)
-
- self.registerField('userName', self.userNameLineEdit)
- self.registerField('userPassword', self.userPasswordLineEdit)
- 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(self.userPasswordLineEdit, 2, 3)
-
- layout.addWidget(rememberPasswordCheckBox, 3, 3, 3, 4)
- self.setLayout(layout)
-
- def reset_validation_status(self):
- """
- empty the validation msg
- """
- self.validationMsg.setText('')
-
- def set_status_validating(self):
- """
- set validation msg to 'registering...'
- """
- # XXX this is NOT WORKING.
- # My guess is that, even if we are using
- # signals to trigger this, it does
- # not show until the validate function
- # returns.
- # I guess it is because there is no delay...
- logger.debug('registering........')
- self.validationMsg.setText('registering...')
- # need to call update somehow???
-
- def set_status_invalid_username(self):
- """
- set validation msg to
- not available user
- """
- self.validationMsg.setText('Username not available.')
-
- def set_status_server_500(self):
- """
- set validation msg to
- internal server error
- """
- self.validationMsg.setText("Error during registration (500)")
-
- def set_status_timeout(self):
- """
- set validation msg to
- timeout
- """
- self.validationMsg.setText("Error connecting to provider (timeout)")
-
- def set_status_connerror(self):
- """
- set validation msg to
- connection refused
- """
- self.validationMsg.setText(
- "Error connecting to provider "
- "(connection error)")
-
- def set_status_unknown_error(self):
- """
- set validation msg to
- unknown error
- """
- self.validationMsg.setText("Error during signup")
-
- # overwritten methods
-
- def initializePage(self):
- """
- inits wizard page
- """
- self.validationMsg.setText('')
-
- def validatePage(self):
- """
- validation
- we initialize the srp protocol register
- and try to register user. if error
- returned we write validation error msg
- above the form.
- """
- # the slot for this signal is not doing
- # what's expected. Investigate why,
- # right now we're not giving any feedback
- # to the user re. what's going on. The only
- # thing I can see as a workaround is setting
- # a low timeout.
- self.setSigningUpStatus.emit()
-
- username = self.userNameLineEdit.text()
- password = self.userPasswordLineEdit.text()
-
- # XXX TODO -- remove debug info
- # XXX get from provider info
- # XXX enforce https
- # and pass a verify value
-
- signup = LeapSRPRegister(
- schema="http",
- provider="springbok",
-
- # debug -----
- #provider="localhost",
- #register_path="timeout",
- #port=8000
- )
- try:
- ok, req = signup.register_user(username, password)
- except socket.timeout:
- self.set_status_timeout()
- return False
-
- except requests.exceptions.ConnectionError as exc:
- logger.error(exc)
- self.set_status_connerror()
- return False
-
- if ok:
- return True
-
- # something went wrong.
- # not registered, let's catch what.
- # get timeout
- # ...
- if req.status_code == 500:
- self.set_status_server_500()
- return False
-
- validation_msgs = json.loads(req.content)
- logger.debug('validation errors: %s' % validation_msgs)
- errors = validation_msgs.get('errors', None)
- if errors and errors.get('login', None):
- self.set_status_invalid_username()
- else:
- self.set_status_unknown_error()
- return False
-
-
-class GlobalEIPSettings(QtGui.QWizardPage):
- def __init__(self, parent=None):
- super(GlobalEIPSettings, self).__init__(parent)
-
-
-class LastPage(QtGui.QWizardPage):
- def __init__(self, parent=None):
- super(LastPage, self).__init__(parent)
-
- self.setTitle("Ready to go!")
-
- #self.setPixmap(
- #QtGui.QWizard.WatermarkPixmap,
- #QtGui.QPixmap(':/images/watermark2.png'))
-
- self.label = QtGui.QLabel()
- self.label.setWordWrap(True)
-
- layout = QtGui.QVBoxLayout()
- layout.addWidget(self.label)
- self.setLayout(layout)
-
- def initializePage(self):
- finishText = self.wizard().buttonText(
- QtGui.QWizard.FinishButton)
- finishText = finishText.replace('&', '')
- self.label.setText(
- "Click '<i>%s</i>' to end the wizard and start "
- "encrypting your connection." % finishText)
-
-
-if __name__ == '__main__':
- # standalone test
- import sys
- import logging
- logging.basicConfig()
- logger = logging.getLogger()
- logger.setLevel(logging.DEBUG)
-
- app = QtGui.QApplication(sys.argv)
- wizard = FirstRunWizard()
- wizard.show()
- sys.exit(app.exec_())
diff --git a/src/leap/gui/mainwindow_rc.py b/src/leap/gui/mainwindow_rc.py
index be575159..63e9f6be 100644
--- a/src/leap/gui/mainwindow_rc.py
+++ b/src/leap/gui/mainwindow_rc.py
@@ -2,7 +2,7 @@
# Resource object code
#
-# Created: Thu Sep 13 16:12:58 2012
+# Created: Tue Nov 6 01:22:11 2012
# by: The Resource Compiler for PyQt (Qt v4.8.2)
#
# WARNING! All changes made in this file will be lost!
@@ -1491,6 +1491,94 @@ qt_resource_data = "\
\xc3\x25\x0d\x25\x35\x01\xd7\x0f\x5b\xb5\x7e\x8e\x93\x83\xff\x0f\
\x92\x04\x28\x92\xfd\x58\xc9\xac\x00\x00\x00\x00\x49\x45\x4e\x44\
\xae\x42\x60\x82\
+\x00\x00\x05\x5f\
+\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\x00\x8e\x00\x00\x00\x8e\
+\x01\x6b\xdf\xd6\xc9\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\xdc\x49\x44\
+\x41\x54\x48\x89\x8d\x95\x79\x6c\x54\x55\x14\xc6\x7f\xf7\xce\x4c\
+\x3b\xa5\xed\x14\x6b\x3b\x25\x42\x17\x18\x28\x65\xba\x90\x6e\x18\
+\xa3\xc5\x05\xa4\x86\x20\x21\xa0\x0d\x88\x24\x26\x98\x10\x28\x68\
+\x42\x40\x8c\x20\x1a\xa3\x16\x43\x48\x8c\x9a\x50\x08\xc6\x14\xad\
+\x9a\x22\x8d\xa2\xa6\x01\x42\x13\x20\x44\x49\x3b\x2c\x5d\x87\xd2\
+\x05\x64\xb1\xad\x85\x76\xa6\xed\x74\x9b\xf7\xae\x7f\xcc\x62\x9b\
+\x69\x2b\xe7\x9f\x97\x73\x5e\xce\xf7\x9d\x7b\xce\x77\xcf\x15\x4a\
+\x29\xa6\xb3\x95\x55\x0b\x2c\xe1\x26\x73\x8e\x80\x3c\x14\xb9\x20\
+\x24\x4a\xd4\x02\x0e\xa3\x34\x39\x2a\x96\xd7\xba\xa6\xcb\x17\x53\
+\x11\xac\x3b\x97\x91\xaf\x94\x2c\x45\x91\x0d\x88\x29\xf2\x15\xd0\
+\xa0\xeb\xa2\xf8\xe7\xc2\xeb\x17\x1f\x89\xa0\xe8\x44\x46\x98\x77\
+\xa6\xf8\x00\xe4\x1e\x83\x30\x18\x72\xe3\x0a\x48\x8d\x49\xc7\x66\
+\x49\xe7\x71\x73\x02\x12\xc9\x83\x91\x2e\x5a\x5d\x8d\xdc\x74\x37\
+\x52\xdb\x73\x01\xaf\x3e\xa6\x03\x9f\xc7\x98\xfa\xf7\x7e\xf3\x5c\
+\xc7\xf0\x94\x04\xaf\x9c\xc9\x4a\xd3\x05\x15\x40\x66\x72\x54\x2a\
+\xc5\xf6\xfd\xb4\xba\xae\xd2\xe6\xba\xc6\xbd\x81\x16\x06\x46\x7b\
+\x30\x00\xb1\x66\x2b\x89\x51\x69\x24\x5b\x32\x48\xb6\x64\x73\xd8\
+\xf9\x09\x6d\xee\x66\x80\x66\x29\xd5\xfa\x9f\x96\xd5\xd7\x85\x10\
+\xf8\x2a\x97\xb5\x52\xc8\xcc\x75\x29\x6f\x92\x1f\xbf\x94\x32\xe7\
+\xc7\x74\x79\xda\x31\x08\x81\x44\x60\x10\x02\x03\x4c\xf0\xe3\xcc\
+\x4f\xb0\x7a\xfe\x1e\xea\xfa\x1c\xfc\xd8\x76\x04\x4d\xd7\xda\xbc\
+\xa3\x61\x59\xa7\x56\xd5\x7a\x00\x64\x80\xc9\xd7\x16\x32\xd7\xa6\
+\x6c\x26\xde\x6c\xa5\xe4\xca\x66\xee\x0f\xb6\x4d\xd1\xfa\xff\xac\
+\x77\xf8\x6f\xca\x1b\x77\x32\x2b\x2c\x9a\x8d\xf3\x8b\x41\x60\x33\
+\x86\x8f\x95\x04\xfe\x4b\xf0\x0d\x14\xe4\x9e\xe4\xa8\x54\xf2\xe3\
+\x9f\xa5\xbc\xe5\x20\x9a\xd2\xfe\x17\x1c\x40\x20\x88\x09\x4f\xe0\
+\xcf\xfb\x15\x64\xc7\x2e\x21\x35\x26\x13\x50\x3b\xd6\x9c\x5e\x5c\
+\x00\x60\x04\x50\x4a\x96\x1a\x84\xc1\xb0\xd5\xbe\x9f\x63\xcd\x1f\
+\xa1\x29\x0d\x39\x95\x6e\xc6\xd9\x9c\xa8\x34\x5e\xb6\xed\x24\xc9\
+\x92\xc1\xe0\xd8\x43\xca\xea\xb7\xb3\xdd\xfe\x21\xbb\x2e\x6f\x14\
+\xa3\x8c\x94\x02\xe9\x72\x65\xd5\x02\x0b\x8a\xec\xdc\xb8\x02\x9c\
+\xbd\x0e\xee\x3e\x42\x5b\x00\x56\xcd\xdb\xc1\xb6\xec\x63\x24\x59\
+\x32\x00\xb8\xe3\x6e\xe0\xe1\xd0\x1d\x3a\x7a\x2f\xf1\x94\xf5\x05\
+\x00\x7b\x51\x55\x4e\xbc\x0c\x37\x99\x73\x00\x31\xdf\x92\x4e\x73\
+\xdf\x95\x10\xa0\xe4\xe8\x45\x21\xb1\xe5\x49\x6f\xb0\x74\xce\x6b\
+\x08\xff\x08\x3b\x07\x5b\x39\xd5\x7a\xc0\x4f\x74\x8d\x45\x33\xb3\
+\x00\xf0\x86\x69\xb9\x52\x40\x1e\xc0\x3c\x8b\x9d\x5b\xfd\xce\x09\
+\x40\x85\x49\x9b\x78\x2f\xef\x38\x6b\x6d\x6f\x05\x63\xb9\xd6\x97\
+\x28\x4c\xd9\x12\xf4\xbb\x06\xdb\x29\x6b\x78\x9b\x21\xaf\xdb\x47\
+\x36\xd0\xc2\xbc\x68\x3b\x00\x4a\x53\x79\x46\xdf\xf5\x87\x38\x73\
+\x02\xbd\x23\xff\xf8\x24\x28\x24\x9b\x52\xdf\x61\xd9\x9c\x22\x00\
+\x5e\x4c\xdc\x84\xa3\xeb\x0c\x46\x69\x60\x43\xda\x3e\x84\xff\x62\
+\x0f\x8e\xf5\xf1\x6d\xe3\x6e\x3c\x63\xae\xe0\xcc\x46\xb4\x41\x66\
+\x18\x23\x91\x42\xa2\x2b\x3d\xcf\x08\x42\x02\x08\x11\x54\x2c\x11\
+\xc6\x68\x22\x4d\xd1\x13\x4e\xf3\x42\xe2\x06\x52\x2c\x19\x18\x84\
+\x09\x00\x5d\x69\x7c\xdf\xbc\x8f\xbe\x91\x4e\x0c\x62\xa2\x22\x02\
+\x58\x42\x28\x19\x58\x5c\xf4\x0c\x77\x12\x1b\x6e\xf5\x57\xe6\xa2\
+\xb4\x71\x2f\xe5\x2d\x9f\xa1\xfb\xe5\xba\x24\x61\x25\xd6\x88\xa4\
+\x20\x48\x55\xc7\x61\xda\x5d\xa1\x33\x33\x1b\xa3\xf0\x78\x07\xd0\
+\x95\x0e\x88\x1a\x09\x38\x00\xda\xdd\x4d\xcc\xb5\x4c\x1c\x68\xf5\
+\xdd\x0a\xbe\xbb\x51\x12\x02\x72\x7f\xa0\x85\x8b\xf7\x7e\x08\x89\
+\x03\xcc\x8a\x5c\x48\x9b\xbb\xc9\x77\x02\x70\x48\xa3\x34\x39\x00\
+\xd5\xea\x6e\x62\x81\x25\x33\x24\xe1\x52\xe7\x29\x3a\x3d\x1d\x41\
+\x5f\xa1\xa8\x6c\x3d\xe8\xaf\x30\xd4\x66\x47\x67\xd0\xec\xaa\x07\
+\x40\x57\xa2\x56\xfa\xf7\x79\x83\xa3\xe7\x02\xf6\xd8\x7c\xe2\x23\
+\x66\x4f\x48\x50\x4a\xe7\xcc\x5f\xc7\x83\x7e\x4d\xe7\xef\xdc\x72\
+\xd7\x4f\x0a\x6e\x09\x8b\x67\xee\x63\x4f\x73\xb9\xbb\x1a\x50\x37\
+\x2b\x57\x5c\xef\x96\x00\xba\x2e\x8a\xbd\xfa\x98\x7e\xd4\xf9\x29\
+\x9b\xd3\xde\x0f\xaa\x24\x60\x8d\x0f\xff\xe0\xfc\xbd\x0a\xaa\xef\
+\x94\xf3\x6b\xfb\x57\x93\x82\x03\x14\xda\x76\x73\xc4\x59\xc2\xb0\
+\x36\x04\x8a\x62\x18\xb7\x4d\xd7\x9e\xcd\x3a\x04\xec\x5c\x6f\xdb\
+\x46\xa4\x21\x82\x93\xed\x5f\x22\x94\xf2\x6d\xd0\x69\xb6\xa9\xc1\
+\xff\x2d\x48\x7c\x9d\x11\x22\xf8\xba\xe5\x10\x42\x70\xf4\xe4\xf2\
+\xba\x2d\x30\x6e\x9b\xc6\x98\xfa\xf7\x02\xcd\x27\xda\x8f\x30\xac\
+\x8f\xf2\x6e\xf6\x31\x12\x66\x24\x4f\x59\x6d\xb0\x2d\xe1\x56\x36\
+\xd8\x0f\x32\x4c\x04\x65\xad\x5f\x00\xe2\xb6\xd7\x33\xba\x2b\xf0\
+\x7f\xe2\x83\x73\x2e\x33\x4b\xd7\x44\x25\x02\x5b\x6a\x4c\x26\x5b\
+\x17\xed\xa7\xee\xc1\x79\x5a\xfb\xae\x72\x77\xc0\xc9\x98\xe6\xc1\
+\x00\x44\x99\x2c\x24\x46\xa5\x31\x37\x26\x8b\x85\xb1\xcf\x70\xd4\
+\x79\x80\xa6\xbe\xab\x80\xb8\x2d\xa4\xf6\xea\xc9\x65\x0d\x35\x93\
+\x12\x00\xac\xfe\x2d\x6f\x86\x6f\x9f\xab\x1d\x61\x32\x5c\x3c\x69\
+\x7d\x9e\x85\x31\x59\xd8\x2c\x76\x22\x8c\x91\x48\x04\x43\x5e\x0f\
+\x6d\xfd\x8d\xdc\x70\x35\x70\xb9\xbb\x9a\x61\x6d\x08\x21\x38\xea\
+\xf5\x8c\xee\xfa\x65\xb5\xb3\x7f\x3c\xde\x94\x8f\xfe\x9a\xd3\x8b\
+\x0b\xa4\x54\xa5\x80\x3d\x10\x93\xfe\x1b\x3a\x51\xa2\xea\x26\x8a\
+\xe2\xca\x15\xf5\x67\x27\xc3\x99\x92\x20\x60\x45\x55\x39\xf1\xde\
+\x30\x2d\x57\x69\x2a\x4f\x40\x9e\x10\x4a\x82\xa8\x11\xe0\xd0\x95\
+\xa8\xad\x5c\x71\xbd\x7b\xba\xfc\x7f\x01\xe3\xf6\xed\xcb\x2c\x97\
+\xd8\xbf\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
"
qt_resource_name = "\
@@ -1521,12 +1609,17 @@ qt_resource_name = "\
\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\x0b\
+\x01\x64\x80\x07\
+\x00\x63\
+\x00\x68\x00\x65\x00\x63\x00\x6b\x00\x65\x00\x64\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\x05\x00\x00\x00\x02\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x06\x00\x00\x00\x02\
\x00\x00\x00\xa8\x00\x00\x00\x00\x00\x01\x00\x00\x2d\x4e\
+\x00\x00\x00\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x5b\xd7\
\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xf7\
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x00\x5e\x00\x00\x00\x00\x00\x01\x00\x00\x19\xd2\
diff --git a/src/leap/gui/progress.py b/src/leap/gui/progress.py
new file mode 100644
index 00000000..6e8abc1f
--- /dev/null
+++ b/src/leap/gui/progress.py
@@ -0,0 +1,287 @@
+"""
+classes used in progress pages
+from first run wizard
+"""
+try:
+ from collections import OrderedDict
+except ImportError:
+ # 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
+
+CHECKMARK_IMG = ":/images/checked.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.step.get(identity)
+
+ def addStep(self, step):
+ self.steps[step.index] = step
+
+ def removeStep(self, step):
+ 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)
+
+ # 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... :(
+
+ # XXX do we need this initial?
+ logger.debug('init table. width=%s' % width)
+ 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)
+
+ # 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 ValidationPage(QtGui.QWizardPage):
+ """
+ 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.steps = ProgressStepContainer()
+ self.progress = QtGui.QProgressBar(self)
+
+ # steps table widget
+ self.stepsTableWidget = StepsTableWidget(self)
+
+ layout = QtGui.QVBoxLayout()
+ layout.addWidget(self.progress)
+ layout.addWidget(self.stepsTableWidget)
+
+ self.setLayout(layout)
+ self.layout = layout
+
+ self.timer = QtCore.QTimer()
+
+ # connect the new step status
+ # signal to status handler
+ self.stepChanged.connect(
+ self.onStepStatusChanged)
+
+ self.errors = OrderedDict()
+
+ def set_error(self, name, error):
+ self.errors[name] = error
+
+ def pop_first_error(self):
+ return list(reversed(self.errors.items())).pop()
+
+ def clean_errors(self):
+ self.errors = OrderedDict()
+
+ def clean_wizard_errors(self, pagename=None):
+ if pagename is None:
+ pagename = getattr(self, 'prev_page', None)
+ if pagename is None:
+ 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.75
+ width = table.width()
+ logger.debug('populate table. width=%s' % width)
+ table.horizontalHeader().resizeSection(0, width * FIRST_COLUMN_PERCENT)
+
+ def onStepStatusChanged(self, status, progress=None):
+ if status not in ("head_sentinel", "end_sentinel"):
+ self.add_status_line(status)
+ if progress:
+ self.progress.setValue(progress)
+ self.progress.update()
+
+ def add_status_line(self, message):
+ index = len(self.steps)
+ step = ProgressStep(message, False, index=index)
+ self.steps.addStep(step)
+ self.populateStepsTable()
+ table = self.stepsTableWidget
+
+ # setting cell widget.
+ # see note on StepsTableWidget about plans to
+ # change this for a better solution.
+
+ table.setCellWidget(
+ index - 1,
+ ProgressStep.DONE,
+ ImgWidget(img=CHECKMARK_IMG))
+ table.update()
+
+ def go_back(self):
+ self.wizard().back()
+
+ def go_next(self):
+ self.wizard().next()
+
+ def initializePage(self):
+ self.clean_errors()
+ self.clean_wizard_errors()
+ self.steps.removeAllSteps()
+ self.clearTable()
+ self.resizeTable()
+ self.timer.singleShot(0, self.do_checks)
+
+ def do_checks(self):
+ """
+ launches a thread to do the checks
+ """
+ signal = self.stepChanged
+ self.checks = FunThread(
+ self._do_checks(update_signal=signal))
+ self.checks.finished.connect(self._do_validation)
+ self.checks.begin()
+ #logger.debug('check thread started!')
+ #logger.debug('waiting for it to terminate...')
+ self.checks.wait()
diff --git a/src/leap/gui/styles.py b/src/leap/gui/styles.py
new file mode 100644
index 00000000..759817ce
--- /dev/null
+++ b/src/leap/gui/styles.py
@@ -0,0 +1,4 @@
+ErrorLabelStyleSheet = """
+QLabel { color: red;
+ font-weight: bold}
+"""
diff --git a/src/leap/gui/test_mainwindow_rc.py b/src/leap/gui/test_mainwindow_rc.py
index 88ae5854..c2fb3f78 100644
--- a/src/leap/gui/test_mainwindow_rc.py
+++ b/src/leap/gui/test_mainwindow_rc.py
@@ -1,8 +1,11 @@
import unittest
import hashlib
-import sip
-sip.setapi('QVariant', 2)
+try:
+ import sip
+ sip.setapi('QVariant', 2)
+except ValueError:
+ pass
from leap.gui import mainwindow_rc
@@ -23,4 +26,4 @@ class MainWindowResourcesTest(unittest.TestCase):
def test_mainwindow_resources_hash(self):
self.assertEqual(
hashlib.md5(mainwindow_rc.qt_resource_data).hexdigest(),
- 'd74eb99247b9d5cd2f00b2f695ca6b59')
+ 'cc7f55e551df55e39c7dbedc1f7de4c2')
diff --git a/src/leap/gui/tests/integration/fake_user_signup.py b/src/leap/gui/tests/integration/fake_user_signup.py
index 12f18966..78873749 100644
--- a/src/leap/gui/tests/integration/fake_user_signup.py
+++ b/src/leap/gui/tests/integration/fake_user_signup.py
@@ -12,6 +12,7 @@ curl -d login=python_test_user -d password_salt=54321\
from BaseHTTPServer import HTTPServer
from BaseHTTPServer import BaseHTTPRequestHandler
import cgi
+import json
import urlparse
HOST = "localhost"
@@ -19,12 +20,15 @@ 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']
+ '/timeout': ['ok\n'],
+ '/provider.json': ['%s\n' % json.dumps(EXPECTED_DEFAULT_CONFIG)]
}
def do_GET(self):
diff --git a/src/leap/gui/threads.py b/src/leap/gui/threads.py
new file mode 100644
index 00000000..176c19b1
--- /dev/null
+++ b/src/leap/gui/threads.py
@@ -0,0 +1,15 @@
+from PyQt4 import QtCore
+
+
+class FunThread(QtCore.QThread):
+
+ def __init__(self, fun, parent=None):
+ QtCore.QThread.__init__(self, parent)
+ self.fun = fun
+
+ def run(self):
+ if self.fun:
+ self.fun()
+
+ def begin(self):
+ self.start()
diff --git a/src/leap/gui/utils.py b/src/leap/gui/utils.py
new file mode 100644
index 00000000..8b1e3630
--- /dev/null
+++ b/src/leap/gui/utils.py
@@ -0,0 +1,10 @@
+"""
+utility functions to work with gui objects
+"""
+
+
+def layout_widgets(layout):
+ """
+ return a generator with all widgets in a layout
+ """
+ return (layout.itemAt(i) for i in range(layout.count()))
diff --git a/src/leap/util/dicts.py b/src/leap/util/dicts.py
new file mode 100644
index 00000000..001ca96b
--- /dev/null
+++ b/src/leap/util/dicts.py
@@ -0,0 +1,268 @@
+# 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/web.py b/src/leap/util/web.py
new file mode 100644
index 00000000..b2aef058
--- /dev/null
+++ b/src/leap/util/web.py
@@ -0,0 +1,39 @@
+"""
+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
+ """
+ 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