summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst165
-rw-r--r--README.txt111
-rw-r--r--pkg/install_venv.py21
-rw-r--r--pkg/requirements.pip8
-rw-r--r--src/leap/app.py13
-rw-r--r--src/leap/base/config.py97
-rw-r--r--src/leap/base/constants.py2
-rw-r--r--src/leap/base/pluggableconfig.py421
-rw-r--r--src/leap/base/specs.py104
-rw-r--r--src/leap/base/tests/test_config.py4
-rw-r--r--src/leap/base/tests/test_providers.py48
-rw-r--r--src/leap/base/tests/test_validation.py92
-rw-r--r--src/leap/baseapp/eip.py24
-rw-r--r--src/leap/baseapp/leap_app.py58
-rw-r--r--src/leap/baseapp/mainwindow.py72
-rw-r--r--src/leap/baseapp/systray.py1
-rw-r--r--src/leap/crypto/__init__.py0
-rw-r--r--src/leap/crypto/leapkeyring.py64
-rw-r--r--src/leap/eip/checks.py17
-rw-r--r--src/leap/eip/config.py4
-rw-r--r--src/leap/eip/exceptions.py7
-rw-r--r--src/leap/eip/openvpnconnection.py15
-rw-r--r--src/leap/eip/specs.py148
-rw-r--r--src/leap/eip/tests/data.py9
-rw-r--r--src/leap/eip/tests/test_checks.py30
-rw-r--r--src/leap/eip/tests/test_config.py14
-rw-r--r--src/leap/eip/tests/test_openvpnconnection.py12
-rwxr-xr-xsrc/leap/gui/firstrunwizard.py489
-rw-r--r--src/leap/gui/tests/integration/fake_user_signup.py80
29 files changed, 1762 insertions, 368 deletions
diff --git a/README.rst b/README.rst
new file mode 100644
index 00000000..183229fe
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,165 @@
+LEAP
+The LEAP Encryption Access Project
+your internet encryption toolkit
+
+Installation
+=============
+
+Dependencies
+--------------
+Listed in pkg/requirements.pip and pkg/test-requirements.pip
+
+* python 2.6 or 2.7
+* python setuptools
+* qt4 libraries
+* python-qt4
+* python-gnutls == 1.1.9
+* python-keyring
+* python-nose, python-mock, python-coverage (if you want to run tests)
+
+If you are on a debian-based system, you can run:
+
+ apt-get install python-qt4 python-qt4-doc pyqt4-dev-tools python-gnutls libgnutls-dev python-setuptools python-nose
+
+# **note**: I think setuptools is needed for build process only.
+# we should separate what's needed as a lib dependency, and what's a dependency that has been debianized but
+# still can be retrieved using pip.
+
+If you are installing in a virtualenv:
+
+ pip install -r pkg/requirements.pip
+
+# **note**: I _think_ setuptools is needed for build process only.
+# we should separate what's needed as a global lib dependency, and what's a dependency that
+# still can be retrieved using pip.
+
+If you are installing in a virtualenv:
+ pip install -r pkg/requirements.pip
+
+
+Install PyQt
+------------
+pip install PyQt will fail because PyQt4 does not use the standard setup.py mechanism.
+Instead, they use configure.py which generates a Makefile.
+
+python configure.py
+make && make install
+
+You can:
+
+* (recommended) run pkg/postmkvenv.sh after creating your virtualenv. It will symlink to your global PyQt installation.
+* install PyQt globally and make a virtualenv with --site-packages
+* run pkg/install_pyqt.sh inside your virtualenv (with --no-site-packages)
+
+
+Install
+---------------
+
+# 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
+
+python setup.py install # as root if installing globally.
+
+
+
+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
+
+% leap-springbok-client
+
+In order to run in debub mode:
+
+% leap-springbok-client --debug --logfile /tmp/leap.log
+
+To see all options:
+
+% leap-springbok-client --help
+
+
+Development
+==============
+
+Hack
+--------------
+
+Some steps to be run when setting a development environment for the first time.
+
+# recommended: enable a virtualenv to isolate your libraries.
+
+% virtualenv . # ensure your .gitignore knows about it
+% source bin/activate
+
+# make sure you're in the development branch
+
+(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,
+it's recommended to run the app like this during development cycle:
+
+(leap_client)% cd src/leap
+(leap_client)% python app.py --debug
+
+Install testing dependencies
+----------------------------
+
+have a look at setup/test-requires
+The ./run_tests.sh command should install all of them in your virtualenv for you.
+
+Running tests
+-------------
+
+./run_tests.sh
+
+force no virtualenv and create coverage reports:
+./run_tests.sh -N -c
+
+if you want to run specific tests, pass the (sub)module to nose:
+
+nosetests leap.util
+
+or
+
+nosetests leap.util.test_leap_argparse
+
+Colorized output
+----------------
+Install rednose locally and activate it.
+
+ (leap_client)% pip install rednose
+ (leap_client)% export NOSE_REDNOSE=1
+
+enjoy :)
+
+Tox
+---
+For running testsuite against all the supported python versions (currently 2.6 and 2.7), run:
+
+ tox -v
+
+
+Compiling resource/ui files
+-----------------------------
+
+You should refresh resource/ui files every time you
+change an image or a resource/ui (.ui / .qc). From
+the root folder:
+
+make ui
+make resources
+
+As there are some tests to guard against unwanted resource updates,
+you will have to update the resource hash in those failing tests.
diff --git a/README.txt b/README.txt
deleted file mode 100644
index 238cd1e3..00000000
--- a/README.txt
+++ /dev/null
@@ -1,111 +0,0 @@
-========================================
-= LEAP =
-= The LEAP Encryption Access Project =
-= your internet encryption toolkit =
-========================================
-
-Installation
-=============
-
-Dependencies
---------------
-
-* python <= 2.7
-* python setuptools
-* qt4 libraries
-* python-qt4
-* python-gnutls == 1.1.9
-* python-nose, python-mock, python-coverage (if you want to run tests)
-
-If you are on a debian-based system, you can run:
-
-apt-get install python-qt4 python-qt4-doc pyqt4-dev-tools python-gnutls libgnutls-dev python-setuptools python-nose
-
-Install
----------------
-
-Global install:
-sudo python setup.py install
-
-If using virtualenv:
-python setup.py install
-
-Install PyQt
-------------
-pip install PyQt will fail because PyQt4 does not use the standard setup.py mechanism.
-Instead, they use configure.py which generates a Makefile.
-
-python configure.py
-make && make install
-
-You can:
-
-* install PyQt globally and make a virtualenv with --site-packages
-* run pkg/install_pyqt.sh inside your virtualenv (with --no-site-packages)
-* run pkg/postmkvenv.sh after creating your virtualenv, for making symlinks to your global PyQt installation.
-
-
-Running the App
------------------
-
-leap-client --debug --logfile /tmp/leap.log
-
-If you're running a branded build, the script name will have a suffix that
-depends on your build flavor:
-
-leap-client-springbok
-
-(or python app.py --debug if you run it from the src/leap folder).
-
-Development
-==============
-
-Hack
---------------
-
-(recommended)
-virtualenv . # ensure your .gitignore knows about it
-source bin/activate
-git checkout develop
-pkg/postmkvenv.sh
-
-python setup.py branding
-python setup.py develop
-
-Running tests
--------------
-
-./run_tests.sh
-
-force no virtualenv and create coverage reports:
-./run_tests.sh -N -c
-
-if you want to run specific tests, pass the (sub)module to nose:
-
-nosetests leap.util
-
-or
-
-nosetests leap.util.test_leap_argparse
-
-Tox
----
-For running testsuite against all the supported python versions (currently 2.6 and 2.7), run:
-
-tox -v
-
-Test-deps
----------
-
-have a look at setup/test-requires
-
-
-Compiling resource/ui files
------------------------------
-
-You should refresh resource/ui files every time you
-change an image or a resource/ui (.ui / .qc). From
-the root folder:
-
-make ui
-make resources
diff --git a/pkg/install_venv.py b/pkg/install_venv.py
index 15385beb..17dfb984 100644
--- a/pkg/install_venv.py
+++ b/pkg/install_venv.py
@@ -29,8 +29,8 @@ import sys
ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
VENV = os.path.join(ROOT, '.venv')
-PIP_REQUIRES = os.path.join(ROOT, 'setup', 'requirements.pip')
-TEST_REQUIRES = os.path.join(ROOT, 'setup', 'test-requirements.pip')
+PIP_REQUIRES = os.path.join(ROOT, 'pkg', 'requirements.pip')
+TEST_REQUIRES = os.path.join(ROOT, 'pkg', 'test-requirements.pip')
PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
@@ -108,6 +108,10 @@ class Debian(Distro):
def apply_patch(self, originalfile, patchfile):
run_command(['patch', originalfile, patchfile])
+ def post_process(self):
+ #symlink qt in virtualenv
+ run_command(['pkg/tools/with_venv.sh', 'pkg/postmkvenv.sh'])
+
def install_virtualenv(self):
if self.check_cmd('virtualenv'):
return
@@ -163,19 +167,22 @@ def create_virtualenv(venv=VENV, no_site_packages=True):
"""
print 'Creating venv...',
if no_site_packages:
- run_command(['virtualenv', '-q', '--no-site-packages', VENV])
+ #setuptools and virtualenv don't play nicely together,
+ #so we create the virtualenv with the distribute package instead.
+ #See: view-source:http://pypi.python.org/pypi/distribute
+ run_command(['virtualenv', '-q', '--distribute', '--no-site-packages', VENV])
else:
- run_command(['virtualenv', '-q', VENV])
+ run_command(['virtualenv', '-q', '--distribute', VENV])
print 'done.'
print 'Installing pip in virtualenv...',
- if not run_command(['setup/tools/with_venv.sh', 'easy_install',
+ if not run_command(['pkg/tools/with_venv.sh', 'easy_install',
'pip>1.0']).strip():
die("Failed to install pip.")
print 'done.'
def pip_install(*args):
- run_command(['setup/tools/with_venv.sh',
+ run_command(['pkg/tools/with_venv.sh',
'pip', 'install', '--upgrade'] + list(args),
redirect_output=False)
@@ -211,7 +218,7 @@ def print_help():
Or, if you prefer, you can run commands in the virtualenv on a case by case
basis by running:
- $ setup/tools/with_venv.sh <your command>
+ $ pkg/tools/with_venv.sh <your command>
Also, make test will automatically use the virtualenv.
"""
diff --git a/pkg/requirements.pip b/pkg/requirements.pip
index 5eeabf5c..2406884d 100644
--- a/pkg/requirements.pip
+++ b/pkg/requirements.pip
@@ -1,6 +1,10 @@
-argparse
-configuration
+argparse # only for python 2.6
requests
ping
+psutil
netifaces
python-gnutls==1.1.9 # see https://bugs.launchpad.net/ubuntu/+source/python-gnutls/+bug/1027129
+jsonschema
+srp
+pycrypto
+keyring
diff --git a/src/leap/app.py b/src/leap/app.py
index 52ebcaea..341f6a6e 100644
--- a/src/leap/app.py
+++ b/src/leap/app.py
@@ -3,6 +3,7 @@ import logging
# This is only needed for Python v2 but is harmless for Python v3.
import sip
sip.setapi('QVariant', 2)
+sip.setapi('QString', 2)
from PyQt4.QtGui import (QApplication, QSystemTrayIcon, QMessageBox)
from leap import __version__ as VERSION
@@ -50,6 +51,14 @@ def main():
logger.info('Starting app')
app = QApplication(sys.argv)
+ # needed for initializing qsettings
+ # it will write .config/leap/leap.conf
+ # top level app settings
+ # in a platform independent way
+ app.setOrganizationName("leap")
+ app.setApplicationName("leap")
+ app.setOrganizationDomain("leap.se")
+
if not QSystemTrayIcon.isSystemTrayAvailable():
QMessageBox.critical(None, "Systray",
"I couldn't detect"
@@ -60,6 +69,10 @@ def main():
window = LeapWindow(opts)
if debug:
+ # we only show the main window
+ # if debug mode active.
+ # if not, it will be set visible
+ # from the systray menu.
window.show()
sys.exit(app.exec_())
diff --git a/src/leap/base/config.py b/src/leap/base/config.py
index 76fbee3c..57f9f1b7 100644
--- a/src/leap/base/config.py
+++ b/src/leap/base/config.py
@@ -9,13 +9,12 @@ import tempfile
import os
logger = logging.getLogger(name=__name__)
-logger.setLevel('DEBUG')
-import configuration
import requests
from leap.base import exceptions
from leap.base import constants
+from leap.base.pluggableconfig import PluggableConfig
from leap.util.fileutil import (mkdir_p)
# move to base!
@@ -38,13 +37,9 @@ class BaseLeapConfig(object):
def get_config(self, *kwargs):
raise NotImplementedError("abstract base class")
- #XXX todo: enable this property after
- #fixing name clash with "config" in use at
- #vpnconnection
-
- #@property
- #def config(self):
- #return self.get_config()
+ @property
+ def config(self):
+ return self.get_config()
def get_value(self, *kwargs):
raise NotImplementedError("abstract base class")
@@ -54,55 +49,51 @@ class MetaConfigWithSpec(type):
"""
metaclass for JSONLeapConfig classes.
It creates a configuration spec out of
- the `spec` dictionary.
+ the `spec` dictionary. The `properties` attribute
+ of the spec dict is turn into the `schema` attribute
+ of the new class (which will be used to validate against).
"""
# XXX in the near future, this is the
# place where we want to enforce
- # singletons, read-only and stuff.
-
- # TODO:
- # - add a error handler for missing options that
- # we can act easily upon (sys.exit is ugly, for $deity's sake)
+ # singletons, read-only and similar stuff.
def __new__(meta, classname, bases, classDict):
- spec_options = classDict.get('spec', None)
+ schema_obj = classDict.get('spec', None)
+
# not quite happy with this workaround.
# I want to raise if missing spec dict, but only
# for grand-children of this metaclass.
# maybe should use abc module for this.
abcderived = ("JSONLeapConfig",)
- if spec_options is None and classname not in abcderived:
+ if schema_obj is None and classname not in abcderived:
raise exceptions.ImproperlyConfigured(
- "missing spec dict on your derived class")
+ "missing spec dict on your derived class (%s)" % classname)
- # we create a configuration spec attribute from the spec dict
+ # we create a configuration spec attribute
+ # from the spec dict
config_class = type(
classname + "Spec",
- (configuration.Configuration, object),
- {'options': spec_options})
+ (PluggableConfig, object),
+ {'options': schema_obj})
classDict['spec'] = config_class
return type.__new__(meta, classname, bases, classDict)
##########################################################
-# hacking in progress:
+# some hacking still in progress:
# Configs have:
+
# - a slug (from where a filename/folder is derived)
# - a spec (for validation and defaults).
-# this spec is basically a dict that will be used
+# this spec is conformant to the json-schema.
+# basically a dict that will be used
# for type casting and validation, and defaults settings.
# all config objects, since they are derived from BaseConfig, implement basic
# useful methods:
# - save
# - load
-# - get_config (returns a optparse.OptionParser object)
-
-# TODO:
-# - have a good type cast repertory (uris, version, hashes...)
-# - raise validation errors
-# - multilingual objects
##########################################################
@@ -125,10 +116,10 @@ class JSONLeapConfig(BaseLeapConfig):
raise exceptions.ImproperlyConfigured(
"missing spec on JSONLeapConfig"
" derived class")
- assert issubclass(self.spec, configuration.Configuration)
+ assert issubclass(self.spec, PluggableConfig)
- self._config = self.spec()
- self._config.parse_args(list(args))
+ self._config = self.spec(format="json")
+ self._config.load()
self.fetcher = kwargs.pop('fetcher', requests)
# mandatory baseconfig interface
@@ -139,13 +130,6 @@ class JSONLeapConfig(BaseLeapConfig):
folder, filename = os.path.split(to)
if folder and not os.path.isdir(folder):
mkdir_p(folder)
- # lazy evaluation until first level of nesting
- # to allow lambdas with context-dependant info
- # like os.path.expanduser
- config = self.get_config()
- for k, v in config.iteritems():
- if callable(v):
- config[k] = v()
self._config.serialize(to)
def load(self, fromfile=None, from_uri=None, fetcher=None, verify=False):
@@ -155,28 +139,37 @@ class JSONLeapConfig(BaseLeapConfig):
return
if fromfile is None:
fromfile = self.filename
- newconfig = self._config.deserialize(fromfile)
- # XXX check for no errors, etc
- self._config.config = newconfig
+ if os.path.isfile(fromfile):
+ self._config.load(fromfile=fromfile)
+ else:
+ logger.error('tried to load config from non-existent path')
+ logger.error('Not Found: %s', fromfile)
def fetch(self, uri, fetcher=None, verify=True):
if not fetcher:
fetcher = self.fetcher
logger.debug('verify: %s', verify)
+ logger.debug('uri: %s', uri)
request = fetcher.get(uri, verify=verify)
+ # XXX should send a if-modified-since header
# XXX get 404, ...
# and raise a UnableToFetch...
request.raise_for_status()
fd, fname = tempfile.mkstemp(suffix=".json")
- if not request.json:
+
+ if request.json:
+ self._config.load(json.dumps(request.json))
+
+ else:
+ # not request.json
+ # might be server did not announce content properly,
+ # let's try deserializing all the same.
try:
- json.loads(request.content)
+ self._config.load(request.content)
except ValueError:
raise eipexceptions.LeapBadConfigFetchedError
- with open(fname, 'w') as tmp:
- tmp.write(json.dumps(request.json))
- self._loadtemp(fname)
+
return True
def get_config(self):
@@ -191,16 +184,16 @@ class JSONLeapConfig(BaseLeapConfig):
def filename(self):
return self.get_filename()
- # private
+ def validate(self, data):
+ logger.debug('validating schema')
+ self._config.validate(data)
+ return True
- def _loadtemp(self, filename):
- self.load(fromfile=filename)
- os.remove(filename)
+ # private
def _slug_to_filename(self):
# is this going to work in winland if slug is "foo/bar" ?
folder, filename = os.path.split(self.slug)
- # XXX fix import
config_file = get_config_file(filename, folder)
return config_file
diff --git a/src/leap/base/constants.py b/src/leap/base/constants.py
index 3f32176f..f7be8d98 100644
--- a/src/leap/base/constants.py
+++ b/src/leap/base/constants.py
@@ -16,7 +16,7 @@ DEFINITION_EXPECTED_PATH = "provider.json"
DEFAULT_PROVIDER_DEFINITION = {
u'api_uri': u'https://api.%s/' % DEFAULT_PROVIDER,
u'api_version': u'0.1.0',
- u'ca_cert': u'8aab80ae4326fd30721689db813733783fe0bd7e',
+ u'ca_cert_fingerprint': u'8aab80ae4326fd30721689db813733783fe0bd7e',
u'ca_cert_uri': u'https://%s/cacert.pem' % DEFAULT_PROVIDER,
u'description': {u'en': u'This is a test provider'},
u'display_name': {u'en': u'Test Provider'},
diff --git a/src/leap/base/pluggableconfig.py b/src/leap/base/pluggableconfig.py
new file mode 100644
index 00000000..b8615ad8
--- /dev/null
+++ b/src/leap/base/pluggableconfig.py
@@ -0,0 +1,421 @@
+"""
+generic configuration handlers
+"""
+import copy
+import json
+import logging
+import os
+import time
+import urlparse
+
+import jsonschema
+
+logger = logging.getLogger(__name__)
+
+
+__all__ = ['PluggableConfig',
+ 'adaptors',
+ 'types',
+ 'UnknownOptionException',
+ 'MissingValueException',
+ 'ConfigurationProviderException',
+ 'TypeCastException']
+
+# exceptions
+
+
+class UnknownOptionException(Exception):
+ """exception raised when a non-configuration
+ value is present in the configuration"""
+
+
+class MissingValueException(Exception):
+ """exception raised when a required value is missing"""
+
+
+class ConfigurationProviderException(Exception):
+ """exception raised when a configuration provider is missing, etc"""
+
+
+class TypeCastException(Exception):
+ """exception raised when a
+ configuration item cannot be coerced to a type"""
+
+
+class ConfigAdaptor(object):
+ """
+ abstract base class for config adaotors for
+ serialization/deserialization and custom validation
+ and type casting.
+ """
+ def read(self, filename):
+ raise NotImplementedError("abstract base class")
+
+ def write(self, config, filename):
+ with open(filename, 'w') as f:
+ self._write(f, config)
+
+ def _write(self, fp, config):
+ raise NotImplementedError("abstract base class")
+
+ def validate(self, config, schema):
+ raise NotImplementedError("abstract base class")
+
+
+adaptors = {}
+
+
+class JSONSchemaEncoder(json.JSONEncoder):
+ """
+ custom default encoder that
+ casts python objects to json objects for
+ the schema validation
+ """
+ def default(self, obj):
+ if obj is str:
+ return 'string'
+ if obj is unicode:
+ return 'string'
+ if obj is int:
+ return 'integer'
+ if obj is list:
+ return 'array'
+ if obj is dict:
+ return 'object'
+ if obj is bool:
+ return 'boolean'
+
+
+class JSONAdaptor(ConfigAdaptor):
+ indent = 2
+ extensions = ['json']
+
+ def read(self, _from):
+ if isinstance(_from, file):
+ _from_string = _from.read()
+ if isinstance(_from, str):
+ _from_string = _from
+ return json.loads(_from_string)
+
+ def _write(self, fp, config):
+ fp.write(json.dumps(config,
+ indent=self.indent,
+ sort_keys=True))
+
+ def validate(self, config, schema_obj):
+ schema_json = JSONSchemaEncoder().encode(schema_obj)
+ schema = json.loads(schema_json)
+ jsonschema.validate(config, schema)
+
+
+adaptors['json'] = JSONAdaptor()
+
+#
+# Adaptors
+#
+# Allow to apply a predefined set of types to the
+# specs, so it checks the validity of formats and cast it
+# to proper python types.
+
+# TODO:
+# - multilingual object.
+# - HTTPS uri
+
+
+class DateType(object):
+ fmt = '%Y-%m-%d'
+
+ def to_python(self, data):
+ return time.strptime(data, self.fmt)
+
+ def get_prep_value(self, data):
+ return time.strftime(self.fmt, data)
+
+
+class URIType(object):
+
+ def to_python(self, data):
+ parsed = urlparse.urlparse(data)
+ if not parsed.scheme:
+ raise TypeCastException("uri %s has no schema" % data)
+ return parsed
+
+ def get_prep_value(self, data):
+ return data.geturl()
+
+
+class HTTPSURIType(object):
+
+ def to_python(self, data):
+ parsed = urlparse.urlparse(data)
+ if not parsed.scheme:
+ raise TypeCastException("uri %s has no schema" % data)
+ if parsed.scheme != "https":
+ raise TypeCastException(
+ "uri %s does not has "
+ "https schema" % data)
+ return parsed
+
+ def get_prep_value(self, data):
+ return data.geturl()
+
+
+types = {
+ 'date': DateType(),
+ 'uri': URIType(),
+ 'https-uri': HTTPSURIType(),
+}
+
+
+class PluggableConfig(object):
+
+ options = {}
+
+ def __init__(self,
+ adaptors=adaptors,
+ types=types,
+ format=None):
+
+ self.config = {}
+ self.adaptors = adaptors
+ self.types = types
+ self._format = format
+
+ @property
+ def option_dict(self):
+ if hasattr(self, 'options') and isinstance(self.options, dict):
+ return self.options.get('properties', None)
+
+ def items(self):
+ """
+ act like an iterator
+ """
+ if isinstance(self.option_dict, dict):
+ return self.option_dict.items()
+ return self.options
+
+ def validate(self, config, format=None):
+ """
+ validate config
+ """
+ schema = self.options
+ if format is None:
+ format = self._format
+
+ if format:
+ adaptor = self.get_adaptor(self._format)
+ adaptor.validate(config, schema)
+ else:
+ # we really should make format mandatory...
+ logger.error('no format passed to validate')
+
+ # first round of validation is ok.
+ # now we proceed to cast types if any specified.
+ self.to_python(config)
+
+ def to_python(self, config):
+ """
+ cast types following first type and then format indications.
+ """
+ unseen_options = [i for i in config if i not in self.option_dict]
+ if unseen_options:
+ raise UnknownOptionException(
+ "Unknown options: %s" % ', '.join(unseen_options))
+
+ for key, value in config.items():
+ _type = self.option_dict[key].get('type')
+ if _type is None and 'default' in self.option_dict[key]:
+ _type = type(self.option_dict[key]['default'])
+ if _type is not None:
+ tocast = True
+ if not callable(_type) and isinstance(value, _type):
+ tocast = False
+ if tocast:
+ try:
+ config[key] = _type(value)
+ except BaseException, e:
+ raise TypeCastException(
+ "Could not coerce %s, %s, "
+ "to type %s: %s" % (key, value, _type.__name__, e))
+ _format = self.option_dict[key].get('format', None)
+ _ftype = self.types.get(_format, None)
+ if _ftype:
+ try:
+ config[key] = _ftype.to_python(value)
+ except BaseException, e:
+ raise TypeCastException(
+ "Could not coerce %s, %s, "
+ "to format %s: %s" % (key, value,
+ _ftype.__class__.__name__,
+ e))
+
+ return config
+
+ def prep_value(self, config):
+ """
+ the inverse of to_python method,
+ called just before serialization
+ """
+ for key, value in config.items():
+ _format = self.option_dict[key].get('format', None)
+ _ftype = self.types.get(_format, None)
+ if _ftype and hasattr(_ftype, 'get_prep_value'):
+ try:
+ config[key] = _ftype.get_prep_value(value)
+ except BaseException, e:
+ raise TypeCastException(
+ "Could not serialize %s, %s, "
+ "by format %s: %s" % (key, value,
+ _ftype.__class__.__name__,
+ e))
+ else:
+ config[key] = value
+ return config
+
+ # methods for adding configuration
+
+ def get_default_values(self):
+ """
+ return a config options from configuration defaults
+ """
+ defaults = {}
+ for key, value in self.items():
+ if 'default' in value:
+ defaults[key] = value['default']
+ return copy.deepcopy(defaults)
+
+ def get_adaptor(self, format):
+ """
+ get specified format adaptor or
+ guess for a given filename
+ """
+ adaptor = self.adaptors.get(format, None)
+ if adaptor:
+ return adaptor
+
+ # not registered in adaptors dict, let's try all
+ for adaptor in self.adaptors.values():
+ if format in adaptor.extensions:
+ return adaptor
+
+ def filename2format(self, filename):
+ extension = os.path.splitext(filename)[-1]
+ return extension.lstrip('.') or None
+
+ def serialize(self, filename, format=None, full=False):
+ if not format:
+ format = self._format
+ if not format:
+ format = self.filename2format(filename)
+ if not format:
+ raise Exception('Please specify a format')
+ # TODO: more specific exception type
+
+ adaptor = self.get_adaptor(format)
+ if not adaptor:
+ raise Exception("Adaptor not found for format: %s" % format)
+
+ config = copy.deepcopy(self.config)
+ serializable = self.prep_value(config)
+ adaptor.write(serializable, filename)
+
+ def deserialize(self, string=None, fromfile=None, format=None):
+ """
+ load configuration from a file or string
+ """
+
+ def _try_deserialize():
+ if fromfile:
+ with open(fromfile, 'r') as f:
+ content = adaptor.read(f)
+ elif string:
+ content = adaptor.read(string)
+ return content
+
+ # XXX cleanup this!
+
+ if fromfile:
+ assert os.path.exists(fromfile)
+ if not format:
+ format = self.filename2format(fromfile)
+
+ if not format:
+ format = self._format
+ if format:
+ adaptor = self.get_adaptor(format)
+ else:
+ adaptor = None
+
+ if adaptor:
+ content = _try_deserialize()
+ return content
+
+ # no adaptor, let's try rest of adaptors
+
+ adaptors = self.adaptors[:]
+
+ if format:
+ adaptors.sort(
+ key=lambda x: int(
+ format in x.extensions),
+ reverse=True)
+
+ for adaptor in adaptors:
+ content = _try_deserialize()
+ return content
+
+ def load(self, *args, **kwargs):
+ """
+ load from string or file
+ if no string of fromfile option is given,
+ it will attempt to load from defaults
+ defined in the schema.
+ """
+ string = args[0] if args else None
+ fromfile = kwargs.get("fromfile", None)
+ content = None
+
+ # start with defaults, so we can
+ # have partial values applied.
+ content = self.get_default_values()
+ if string and isinstance(string, str):
+ content = self.deserialize(string)
+
+ if not string and fromfile is not None:
+ #import ipdb;ipdb.set_trace()
+ content = self.deserialize(fromfile=fromfile)
+
+ if not content:
+ logger.error('no content could be loaded')
+ # XXX raise!
+ return
+
+ # lazy evaluation until first level of nesting
+ # to allow lambdas with context-dependant info
+ # like os.path.expanduser
+ for k, v in content.iteritems():
+ if callable(v):
+ content[k] = v()
+
+ self.validate(content)
+ self.config = content
+ return True
+
+
+def testmain():
+ from tests import test_validation as t
+ import pprint
+
+ config = PluggableConfig(_format="json")
+ properties = copy.deepcopy(t.sample_spec)
+
+ config.options = properties
+ config.load(fromfile='data.json')
+
+ print 'config'
+ pprint.pprint(config.config)
+
+ config.serialize('/tmp/testserial.json')
+
+if __name__ == "__main__":
+ testmain()
diff --git a/src/leap/base/specs.py b/src/leap/base/specs.py
index d88dc63f..b4bb8dcf 100644
--- a/src/leap/base/specs.py
+++ b/src/leap/base/specs.py
@@ -1,49 +1,59 @@
leap_provider_spec = {
- 'serial': {
- 'type': int,
- 'default': 1,
- 'required': True,
- },
- 'version': {
- 'type': unicode,
- 'default': '0.1.0'
- #'required': True
- },
- 'domain': {
- 'type': unicode, # XXX define uri type
- 'default': 'testprovider.example.org'
- #'required': True,
- },
- 'display_name': {
- 'type': unicode, # XXX multilingual object?
- 'default': 'test provider'
- #'required': True
- },
- 'description': {
- 'default': 'test provider'
- },
- 'enrollment_policy': {
- 'type': unicode, # oneof ??
- 'default': 'open'
- },
- 'services': {
- 'type': list, # oneof ??
- 'default': ['eip']
- },
- 'api_version': {
- 'type': unicode,
- 'default': '0.1.0' # version regexp
- },
- 'api_uri': {
- 'type': unicode # uri
- },
- 'public_key': {
- 'type': unicode # fingerprint
- },
- 'ca_cert': {
- 'type': unicode
- },
- 'ca_cert_uri': {
- 'type': unicode
- },
+ 'description': 'provider definition',
+ 'type': 'object',
+ 'properties': {
+ 'serial': {
+ 'type': int,
+ 'default': 1,
+ 'required': True,
+ },
+ 'version': {
+ 'type': unicode,
+ 'default': '0.1.0'
+ #'required': True
+ },
+ 'domain': {
+ 'type': unicode, # XXX define uri type
+ 'default': 'testprovider.example.org'
+ #'required': True,
+ },
+ 'display_name': {
+ 'type': dict, # XXX multilingual object?
+ 'default': {u'en': u'Test Provider'}
+ #'required': True
+ },
+ 'description': {
+ 'type': dict,
+ 'default': {u'en': u'Test provider'}
+ },
+ 'enrollment_policy': {
+ 'type': unicode, # oneof ??
+ 'default': 'open'
+ },
+ 'services': {
+ 'type': list, # oneof ??
+ 'default': ['eip']
+ },
+ 'api_version': {
+ 'type': unicode,
+ 'default': '0.1.0' # version regexp
+ },
+ 'api_uri': {
+ 'type': unicode # uri
+ },
+ 'public_key': {
+ 'type': unicode # fingerprint
+ },
+ 'ca_cert_fingerprint': {
+ 'type': unicode,
+ },
+ 'ca_cert_uri': {
+ 'type': unicode,
+ 'format': 'https-uri'
+ },
+ 'languages': {
+ 'type': list,
+ 'default': ['en']
+ }
+ }
}
diff --git a/src/leap/base/tests/test_config.py b/src/leap/base/tests/test_config.py
index bede5ea1..d03149b2 100644
--- a/src/leap/base/tests/test_config.py
+++ b/src/leap/base/tests/test_config.py
@@ -38,14 +38,14 @@ class JSONLeapConfigTest(BaseLeapTest):
class DummyTestConfig(config.JSONLeapConfig):
__metaclass__ = config.MetaConfigWithSpec
- spec = {}
+ spec = {'properties': {}}
with self.assertRaises(exceptions.ImproperlyConfigured) as exc:
DummyTestConfig()
exc.startswith("missing slug")
class DummyTestConfig(config.JSONLeapConfig):
__metaclass__ = config.MetaConfigWithSpec
- spec = {}
+ spec = {'properties': {}}
slug = "foo"
DummyTestConfig()
diff --git a/src/leap/base/tests/test_providers.py b/src/leap/base/tests/test_providers.py
index 9e0ff90c..8d3b8847 100644
--- a/src/leap/base/tests/test_providers.py
+++ b/src/leap/base/tests/test_providers.py
@@ -1,35 +1,39 @@
+import copy
import json
try:
import unittest2 as unittest
except ImportError:
import unittest
-
import os
+import jsonschema
+
from leap import __branding as BRANDING
from leap.testing.basetest import BaseLeapTest
from leap.base import providers
EXPECTED_DEFAULT_CONFIG = {
- "api_version": "0.1.0",
- "description": "test provider",
- "display_name": "test provider",
- "domain": "testprovider.example.org",
- "enrollment_policy": "open",
- "serial": 1,
- "services": [
- "eip"
+ u"api_version": u"0.1.0",
+ u"description": {u'en': u"Test provider"},
+ u"display_name": {u'en': u"Test Provider"},
+ u"domain": u"testprovider.example.org",
+ u"enrollment_policy": u"open",
+ u"serial": 1,
+ u"services": [
+ u"eip"
],
- "version": "0.1.0"
+ u"languages": [u"en"],
+ u"version": u"0.1.0"
}
class TestLeapProviderDefinition(BaseLeapTest):
def setUp(self):
self.definition = providers.LeapProviderDefinition()
- #XXX change to self.definition.config when property is fixed
- self.config = self.definition.get_config()
+ self.definition.save()
+ self.definition.load()
+ self.config = self.definition.config
def tearDown(self):
if hasattr(self, 'testfile') and os.path.isfile(self.testfile):
@@ -57,6 +61,7 @@ class TestLeapProviderDefinition(BaseLeapTest):
self.testfile = self.get_tempfile('test.json')
self.definition.save(to=self.testfile)
deserialized = json.load(open(self.testfile, 'rb'))
+ self.maxDiff = None
self.assertEqual(deserialized, EXPECTED_DEFAULT_CONFIG)
def test_provider_dump_to_slug(self):
@@ -78,6 +83,13 @@ class TestLeapProviderDefinition(BaseLeapTest):
self.assertDictEqual(self.config,
EXPECTED_DEFAULT_CONFIG)
+ def test_provider_validation(self):
+ self.definition.validate(self.config)
+ _config = copy.deepcopy(self.config)
+ _config['serial'] = 'aaa'
+ with self.assertRaises(jsonschema.ValidationError):
+ self.definition.validate(_config)
+
@unittest.skip
def test_load_malformed_json_definition(self):
raise NotImplementedError
@@ -89,18 +101,6 @@ class TestLeapProviderDefinition(BaseLeapTest):
raise NotImplementedError
-class TestLeapProvider(BaseLeapTest):
- def setUp(self):
- pass
-
- def tearDown(self):
- pass
-
- ###
-
- # XXX ??
-
-
class TestLeapProviderSet(BaseLeapTest):
def setUp(self):
diff --git a/src/leap/base/tests/test_validation.py b/src/leap/base/tests/test_validation.py
new file mode 100644
index 00000000..87e99648
--- /dev/null
+++ b/src/leap/base/tests/test_validation.py
@@ -0,0 +1,92 @@
+import copy
+import datetime
+#import json
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+import os
+
+import jsonschema
+
+from leap.base.config import JSONLeapConfig
+from leap.base import pluggableconfig
+from leap.testing.basetest import BaseLeapTest
+
+SAMPLE_CONFIG_DICT = {
+ 'prop_one': 1,
+ 'prop_uri': "http://example.org",
+ 'prop_date': '2012-12-12',
+}
+
+EXPECTED_CONFIG = {
+ 'prop_one': 1,
+ 'prop_uri': "http://example.org",
+ 'prop_date': datetime.datetime(2012, 12, 12)
+}
+
+sample_spec = {
+ 'description': 'sample schema definition',
+ 'type': 'object',
+ 'properties': {
+ 'prop_one': {
+ 'type': int,
+ 'default': 1,
+ 'required': True
+ },
+ 'prop_uri': {
+ 'type': str,
+ 'default': 'http://example.org',
+ 'required': True,
+ 'format': 'uri'
+ },
+ 'prop_date': {
+ 'type': str,
+ 'default': '2012-12-12',
+ 'format': 'date'
+ }
+ }
+}
+
+
+class SampleConfig(JSONLeapConfig):
+ spec = sample_spec
+
+ @property
+ def slug(self):
+ return os.path.expanduser('~/sampleconfig.json')
+
+
+class TestJSONLeapConfigValidation(BaseLeapTest):
+ def setUp(self):
+ self.sampleconfig = SampleConfig()
+ self.sampleconfig.save()
+ self.sampleconfig.load()
+ self.config = self.sampleconfig.config
+
+ def tearDown(self):
+ if hasattr(self, 'testfile') and os.path.isfile(self.testfile):
+ os.remove(self.testfile)
+
+ # tests
+
+ def test_good_validation(self):
+ self.sampleconfig.validate(SAMPLE_CONFIG_DICT)
+
+ def test_broken_int(self):
+ _config = copy.deepcopy(SAMPLE_CONFIG_DICT)
+ _config['prop_one'] = '1'
+ with self.assertRaises(jsonschema.ValidationError):
+ self.sampleconfig.validate(_config)
+
+ def test_format_property(self):
+ # JsonSchema Validator does not check the format property.
+ # We should have to extend the Configuration class
+ blah = copy.deepcopy(SAMPLE_CONFIG_DICT)
+ blah['prop_uri'] = 'xxx'
+ with self.assertRaises(pluggableconfig.TypeCastException):
+ self.sampleconfig.validate(blah)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/leap/baseapp/eip.py b/src/leap/baseapp/eip.py
index ad074abc..e291de34 100644
--- a/src/leap/baseapp/eip.py
+++ b/src/leap/baseapp/eip.py
@@ -40,22 +40,28 @@ class EIPConductorAppMixin(object):
debug=self.debugmode,
ovpn_verbosity=opts.openvpn_verb)
- skip_download = opts.no_provider_checks
- skip_verify = opts.no_ca_verify
+ self.skip_download = opts.no_provider_checks
+ self.skip_verify = opts.no_ca_verify
+
+ def run_eip_checks(self):
+ """
+ runs eip checks and
+ the error checking loop
+ """
+ logger.debug('running EIP CHECKS')
self.conductor.run_checks(
- skip_download=skip_download,
- skip_verify=skip_verify)
+ skip_download=self.skip_download,
+ skip_verify=self.skip_verify)
self.error_check()
- # XXX should receive "ready" signal
- # it is called from LeapWindow now.
- #if self.conductor.autostart:
- #self.start_or_stopVPN()
-
if self.debugmode:
self.startStopButton.clicked.connect(
lambda: self.start_or_stopVPN())
+ # XXX should send ready signal instead
+ if self.conductor.autostart:
+ self.start_or_stopVPN()
+
def error_check(self):
"""
consumes the conductor error queue.
diff --git a/src/leap/baseapp/leap_app.py b/src/leap/baseapp/leap_app.py
index 208c4e7c..460d1269 100644
--- a/src/leap/baseapp/leap_app.py
+++ b/src/leap/baseapp/leap_app.py
@@ -1,5 +1,9 @@
import logging
+import sip
+sip.setapi('QVariant', 2)
+
+from PyQt4 import QtCore
from PyQt4 import QtGui
from leap.gui import mainwindow_rc
@@ -23,22 +27,62 @@ class MainWindowMixin(object):
widget = QtGui.QWidget()
self.setCentralWidget(widget)
- self.createWindowHeader()
-
- # add widgets to layout
mainLayout = QtGui.QVBoxLayout()
- mainLayout.addWidget(self.headerBox)
+ # add widgets to layout
+ #self.createWindowHeader()
+ #mainLayout.addWidget(self.headerBox)
mainLayout.addWidget(self.statusIconBox)
if self.debugmode:
mainLayout.addWidget(self.statusBox)
mainLayout.addWidget(self.loggerBox)
widget.setLayout(mainLayout)
+ self.createMainActions()
+ self.createMainMenus()
+
self.setWindowTitle("LEAP Client")
self.set_app_icon()
- self.resize(400, 300)
self.set_statusbarMessage('ready')
+ def createMainActions(self):
+ #self.openAct = QtGui.QAction("&Open...", self, shortcut="Ctrl+O",
+ #triggered=self.open)
+
+ self.firstRunWizardAct = QtGui.QAction(
+ "&First run wizard...", self,
+ triggered=self.launch_first_run_wizard)
+ self.aboutAct = QtGui.QAction("&About", self, triggered=self.about)
+
+ #self.aboutQtAct = QtGui.QAction("About &Qt", self,
+ #triggered=QtGui.qApp.aboutQt)
+
+ def createMainMenus(self):
+ self.connMenu = QtGui.QMenu("&Connections", self)
+ #self.viewMenu.addSeparator()
+ self.connMenu.addAction(self.quitAction)
+
+ self.settingsMenu = QtGui.QMenu("&Settings", self)
+ self.settingsMenu.addAction(self.firstRunWizardAct)
+
+ self.helpMenu = QtGui.QMenu("&Help", self)
+ self.helpMenu.addAction(self.aboutAct)
+ #self.helpMenu.addAction(self.aboutQtAct)
+
+ self.menuBar().addMenu(self.connMenu)
+ self.menuBar().addMenu(self.settingsMenu)
+ self.menuBar().addMenu(self.helpMenu)
+
+ def 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()
+
def set_app_icon(self):
icon = QtGui.QIcon(APP_LOGO)
self.setWindowIcon(icon)
@@ -88,6 +132,10 @@ class MainWindowMixin(object):
"""
cleans state before shutting down app.
"""
+ # save geometry for restoring
+ settings = QtCore.QSettings()
+ settings.setValue("Geometry", self.saveGeometry())
+
# TODO:make sure to shutdown all child process / threads
# in conductor
# XXX send signal instead?
diff --git a/src/leap/baseapp/mainwindow.py b/src/leap/baseapp/mainwindow.py
index fdbaf693..09e0c0bb 100644
--- a/src/leap/baseapp/mainwindow.py
+++ b/src/leap/baseapp/mainwindow.py
@@ -29,22 +29,33 @@ class LeapWindow(QtGui.QMainWindow,
newLogLine = QtCore.pyqtSignal([str])
statusChange = QtCore.pyqtSignal([object])
+ mainappReady = QtCore.pyqtSignal([])
+ initReady = QtCore.pyqtSignal([])
networkError = QtCore.pyqtSignal([object])
def __init__(self, opts):
logger.debug('init leap window')
self.debugmode = getattr(opts, 'debug', False)
-
super(LeapWindow, self).__init__()
if self.debugmode:
self.createLogBrowser()
+
EIPConductorAppMixin.__init__(self, opts=opts)
StatusAwareTrayIconMixin.__init__(self)
NetworkCheckerAppMixin.__init__(self)
MainWindowMixin.__init__(self)
+ settings = QtCore.QSettings()
+ geom = settings.value("Geometry")
+ if geom:
+ self.restoreGeometry(geom)
+ self.wizard_done = settings.value("FirstRunWizardDone")
+
+ self.initchecks = InitChecksThread(self.run_eip_checks)
+
# bind signals
- # XXX move to parent classes init??
+ self.initchecks.finished.connect(
+ lambda: logger.debug('Initial checks finished'))
self.trayIcon.activated.connect(self.iconActivated)
self.newLogLine.connect(
lambda line: self.onLoggerNewLine(line))
@@ -55,22 +66,59 @@ class LeapWindow(QtGui.QMainWindow,
self.networkError.connect(
lambda exc: self.onNetworkError(exc))
+ # do frwizard 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
+ self.mainappReady.emit()
+
+ def do_first_run_wizard_check(self):
+ logger.debug('first run wizard check...')
+ if self.wizard_done:
+ self.initReady.emit()
+ else:
+ # need to run first-run-wizard
+ logger.debug('running first run wizard')
+ from leap.gui.firstrunwizard import FirstRunWizard
+ wizard = FirstRunWizard(
+ parent=self,
+ success_cb=self.initReady.emit)
+ wizard.show()
+
+ 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()
+
+#<<<<<<< HEAD
+ def begin(self):
+ self.start()
+#=======
# could send "ready" signal instead
# eipapp should catch that
- if self.conductor.autostart:
- self.start_or_stopVPN()
-
+ #if self.conductor.autostart:
+ #self.start_or_stopVPN()
+#
#TODO: Put all Dialogs in one place
- @QtCore.pyqtSlot()
- def raise_Network_Error(self, exc):
- message = exc.message
-
+ #@QtCore.pyqtSlot()
+ #def raise_Network_Error(self, exc):
+ #message = exc.message
+#
# XXX
# check headless = False before
# launching dialog.
# (so Qt tests can assert stuff)
-
- dialog = dialogs.ErrorDialog()
- dialog.warningMessage(message, 'error')
+#
+ #dialog = dialogs.ErrorDialog()
+ #dialog.warningMessage(message, 'error')
+#>>>>>>> feature/network_check
diff --git a/src/leap/baseapp/systray.py b/src/leap/baseapp/systray.py
index adcfe9b9..1939bc09 100644
--- a/src/leap/baseapp/systray.py
+++ b/src/leap/baseapp/systray.py
@@ -41,6 +41,7 @@ class StatusAwareTrayIconMixin(object):
self.createIconGroupBox()
self.createActions()
self.createTrayIcon()
+ logger.debug('showing tray icon................')
self.trayIcon.show()
# not sure if this really belongs here, but...
diff --git a/src/leap/crypto/__init__.py b/src/leap/crypto/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/leap/crypto/__init__.py
diff --git a/src/leap/crypto/leapkeyring.py b/src/leap/crypto/leapkeyring.py
new file mode 100644
index 00000000..bb0ca147
--- /dev/null
+++ b/src/leap/crypto/leapkeyring.py
@@ -0,0 +1,64 @@
+import os
+
+import keyring
+
+#############
+# Disclaimer
+#############
+# This currently is not a keyring, it's more like a joke.
+# No, seriously.
+# We're affected by this **bug**
+
+# https://bitbucket.org/kang/python-keyring-lib/
+# issue/65/dbusexception-method-opensession-with
+
+# so using the gnome keyring does not seem feasible right now.
+# I thought this was the next best option to store secrets in plain sight.
+
+# in the future we should move to use the gnome/kde/macosx/win keyrings.
+
+
+class LeapCryptedFileKeyring(keyring.backend.CryptedFileKeyring):
+
+ filename = os.path.expanduser("~/.config/leap/.secrets")
+
+ def __init__(self, seed=None):
+ self.seed = seed
+
+ def _get_new_password(self):
+ # XXX every time this method is called,
+ # $deity kills a kitten.
+ return "secret%s" % self.seed
+
+ def _init_file(self):
+ self.keyring_key = self._get_new_password()
+ self.set_password('keyring_setting', 'pass_ref', 'pass_ref_value')
+
+ def _unlock(self):
+ self.keyring_key = self._get_new_password()
+ print 'keyring key ', self.keyring_key
+ try:
+ ref_pw = self.get_password(
+ 'keyring_setting',
+ 'pass_ref')
+ print 'ref pw ', ref_pw
+ assert ref_pw == "pass_ref_value"
+ except AssertionError:
+ self._lock()
+ raise ValueError('Incorrect password')
+
+
+def leap_set_password(key, value, seed="xxx"):
+ keyring.set_keyring(LeapCryptedFileKeyring(seed=seed))
+ keyring.set_password('leap', key, value)
+
+
+def leap_get_password(key, seed="xxx"):
+ keyring.set_keyring(LeapCryptedFileKeyring(seed=seed))
+ return keyring.get_password('leap', key)
+
+
+if __name__ == "__main__":
+ leap_set_password('test', 'bar')
+ passwd = leap_get_password('test')
+ assert passwd == 'bar'
diff --git a/src/leap/eip/checks.py b/src/leap/eip/checks.py
index b68ee23a..f739c3e8 100644
--- a/src/leap/eip/checks.py
+++ b/src/leap/eip/checks.py
@@ -155,6 +155,9 @@ class ProviderCertChecker(object):
# 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.raise_for_status()
except requests.exceptions.SSLError:
@@ -180,7 +183,7 @@ class ProviderCertChecker(object):
valid = exists() and valid_pemfile() and not_expired()
if not valid:
if do_raise:
- raise Exception('missing cert')
+ raise Exception('missing valid cert')
else:
return False
return True
@@ -196,7 +199,9 @@ class ProviderCertChecker(object):
with open(certfile) as cf:
cert_s = cf.read()
cert = crypto.X509Certificate(cert_s)
- return cert.activation_time < now() < cert.expiration_time
+ from_ = time.gmtime(cert.activation_time)
+ to_ = time.gmtime(cert.expiration_time)
+ return from_ < now() < to_
def is_valid_pemfile(self, cert_s=None):
"""
@@ -316,7 +321,7 @@ class EIPConfigChecker(object):
This is catched by ui and runs FirstRunWizard (MVS+)
"""
if config is None:
- config = self.eipconfig.get_config()
+ config = self.eipconfig.config
logger.debug('checking default provider')
provider = config.get('provider', None)
if provider is None:
@@ -340,7 +345,7 @@ class EIPConfigChecker(object):
logger.debug('(fetching def skipped)')
return True
if config is None:
- config = self.defaultprovider.get_config()
+ config = self.defaultprovider.config
if uri is None:
domain = config.get('provider', None)
uri = self._get_provider_definition_uri(domain=domain)
@@ -357,7 +362,7 @@ class EIPConfigChecker(object):
if skip_download:
return True
if config is None:
- config = self.eipserviceconfig.get_config()
+ config = self.eipserviceconfig.config
if uri is None:
domain = config.get('provider', None)
uri = self._get_eip_service_uri(domain=domain)
@@ -368,7 +373,7 @@ class EIPConfigChecker(object):
def check_complete_eip_config(self, config=None):
# TODO check for gateway
if config is None:
- config = self.eipconfig.get_config()
+ config = self.eipconfig.config
try:
'trying assertions'
assert 'provider' in config
diff --git a/src/leap/eip/config.py b/src/leap/eip/config.py
index 082cc24d..ef0f52b4 100644
--- a/src/leap/eip/config.py
+++ b/src/leap/eip/config.py
@@ -61,8 +61,10 @@ def get_eip_gateway():
"""
placeholder = "testprovider.example.org"
eipconfig = EIPConfig()
+ #import ipdb;ipdb.set_trace()
eipconfig.load()
- conf = eipconfig.get_config()
+ conf = eipconfig.config
+
primary_gateway = conf.get('primary_gateway', None)
if not primary_gateway:
return placeholder
diff --git a/src/leap/eip/exceptions.py b/src/leap/eip/exceptions.py
index 24c9bfe8..11bfd620 100644
--- a/src/leap/eip/exceptions.py
+++ b/src/leap/eip/exceptions.py
@@ -92,6 +92,13 @@ class LeapBadConfigFetchedError(Warning):
message = "provider sent a malformed json file"
usermessage = "an error occurred during configuratio of leap services"
+
+class OpenVPNAlreadyRunning(EIPClientError):
+ message = "Another OpenVPN Process is already running."
+ usermessage = ("Another OpenVPN Process has been detected."
+ "Please close it before starting leap-client")
+
+
#
# errors still needing some love
#
diff --git a/src/leap/eip/openvpnconnection.py b/src/leap/eip/openvpnconnection.py
index f4d1c449..a835ead9 100644
--- a/src/leap/eip/openvpnconnection.py
+++ b/src/leap/eip/openvpnconnection.py
@@ -3,6 +3,7 @@ OpenVPN Connection
"""
from __future__ import (print_function)
import logging
+import psutil
import socket
import time
from functools import partial
@@ -87,6 +88,7 @@ to be triggered for each one of them.
def run_openvpn_checks(self):
logger.debug('running openvpn checks')
+ self._check_if_running_instance()
self._set_ovpn_command()
self._check_vpn_keys()
@@ -156,9 +158,20 @@ to be triggered for each one of them.
raise eip_exceptions.EIPNoCommandError
if self.subp is not None:
logger.debug('cowardly refusing to launch subprocess again')
- return
+
self._launch_openvpn()
+ def _check_if_running_instance(self):
+ """
+ check if openvpn is already running
+ """
+ for process in psutil.get_process_list():
+ if process.name == "openvpn":
+ logger.debug('an openvpn instance is already running.')
+ raise eip_exceptions.OpenVPNAlreadyRunning
+
+ logger.debug('no openvpn instance found.')
+
def cleanup(self):
"""
terminates child subprocess
diff --git a/src/leap/eip/specs.py b/src/leap/eip/specs.py
index 2391e919..1a670b0e 100644
--- a/src/leap/eip/specs.py
+++ b/src/leap/eip/specs.py
@@ -8,7 +8,7 @@ PROVIDER_CA_CERT = __branding.get(
'provider_ca_file',
'testprovider-ca-cert.pem')
-provider_ca_path = lambda: unicode(os.path.join(
+provider_ca_path = lambda: str(os.path.join(
baseconfig.get_default_provider_path(),
'keys', 'ca',
PROVIDER_CA_CERT
@@ -24,78 +24,86 @@ client_cert_path = lambda: unicode(os.path.join(
))
eipconfig_spec = {
- 'provider': {
- 'type': unicode,
- 'default': u"%s" % PROVIDER_DOMAIN,
- 'required': True,
- },
- 'transport': {
- 'type': unicode,
- 'default': u"openvpn",
- },
- 'openvpn_protocol': {
- 'type': unicode,
- 'default': u"tcp"
- },
- 'openvpn_port': {
- 'type': int,
- 'default': 80
- },
- 'openvpn_ca_certificate': {
- 'type': unicode, # path
- 'default': provider_ca_path
- },
- 'openvpn_client_certificate': {
- 'type': unicode, # path
- 'default': client_cert_path
- },
- 'connect_on_login': {
- 'type': bool,
- 'default': True
- },
- 'block_cleartext_traffic': {
- 'type': bool,
- 'default': True
- },
- 'primary_gateway': {
- 'type': unicode,
- 'default': u"turkey",
- 'required': True
- },
- 'secondary_gateway': {
- 'type': unicode,
- 'default': u"france"
- },
- 'management_password': {
- 'type': unicode
+ 'description': 'sample eipconfig',
+ 'type': 'object',
+ 'properties': {
+ 'provider': {
+ 'type': unicode,
+ 'default': u"%s" % PROVIDER_DOMAIN,
+ 'required': True,
+ },
+ 'transport': {
+ 'type': unicode,
+ 'default': u"openvpn",
+ },
+ 'openvpn_protocol': {
+ 'type': unicode,
+ 'default': u"tcp"
+ },
+ 'openvpn_port': {
+ 'type': int,
+ 'default': 80
+ },
+ 'openvpn_ca_certificate': {
+ 'type': unicode, # path
+ 'default': provider_ca_path
+ },
+ 'openvpn_client_certificate': {
+ 'type': unicode, # path
+ 'default': client_cert_path
+ },
+ 'connect_on_login': {
+ 'type': bool,
+ 'default': True
+ },
+ 'block_cleartext_traffic': {
+ 'type': bool,
+ 'default': True
+ },
+ 'primary_gateway': {
+ 'type': unicode,
+ 'default': u"turkey",
+ #'required': True
+ },
+ 'secondary_gateway': {
+ 'type': unicode,
+ 'default': u"france"
+ },
+ 'management_password': {
+ 'type': unicode
+ }
}
}
eipservice_config_spec = {
- 'serial': {
- 'type': int,
- 'required': True,
- 'default': 1
- },
- 'version': {
- 'type': unicode,
- 'required': True,
- 'default': "0.1.0"
- },
- 'capabilities': {
- 'type': dict,
- 'default': {
- "transport": ["openvpn"],
- "ports": ["80", "53"],
- "protocols": ["udp", "tcp"],
- "static_ips": True,
- "adblock": True}
- },
- 'gateways': {
- 'type': list,
- 'default': [{"country_code": "us",
- "label": {"en":"west"},
- "capabilities": {},
- "hosts": ["1.2.3.4", "1.2.3.5"]}]
+ 'description': 'sample eip service config',
+ 'type': 'object',
+ 'properties': {
+ 'serial': {
+ 'type': int,
+ 'required': True,
+ 'default': 1
+ },
+ 'version': {
+ 'type': unicode,
+ 'required': True,
+ 'default': "0.1.0"
+ },
+ 'capabilities': {
+ 'type': dict,
+ 'default': {
+ "transport": ["openvpn"],
+ "ports": ["80", "53"],
+ "protocols": ["udp", "tcp"],
+ "static_ips": True,
+ "adblock": True}
+ },
+ 'gateways': {
+ 'type': list,
+ 'default': [{"country_code": "us",
+ "label": {"en":"west"},
+ "capabilities": {},
+ "hosts": ["1.2.3.4", "1.2.3.5"]}]
+ }
}
}
diff --git a/src/leap/eip/tests/data.py b/src/leap/eip/tests/data.py
index 9bf86540..43df2013 100644
--- a/src/leap/eip/tests/data.py
+++ b/src/leap/eip/tests/data.py
@@ -7,7 +7,7 @@ from leap import __branding
PROVIDER = __branding.get('provider_domain')
-EIP_SAMPLE_JSON = {
+EIP_SAMPLE_CONFIG = {
"provider": "%s" % PROVIDER,
"transport": "openvpn",
"openvpn_protocol": "tcp",
@@ -38,9 +38,10 @@ EIP_SAMPLE_SERVICE = {
"adblock": True
},
"gateways": [
- {"country_code": "us",
- "label": {"en":"west"},
+ {"country_code": "tr",
+ "name": "turkey",
+ "label": {"en":"Ankara, Turkey"},
"capabilities": {},
- "hosts": ["1.2.3.4", "1.2.3.5"]},
+ "hosts": ["94.103.43.4"]}
]
}
diff --git a/src/leap/eip/tests/test_checks.py b/src/leap/eip/tests/test_checks.py
index 06133825..58ce473f 100644
--- a/src/leap/eip/tests/test_checks.py
+++ b/src/leap/eip/tests/test_checks.py
@@ -11,6 +11,8 @@ import urlparse
from mock import (patch, Mock)
+import jsonschema
+#import ping
import requests
from leap.base import config as baseconfig
@@ -89,12 +91,12 @@ class EIPCheckTest(BaseLeapTest):
# force re-evaluation of the paths
# small workaround for evaluating home dirs correctly
- EIP_SAMPLE_JSON = copy.copy(testdata.EIP_SAMPLE_JSON)
- EIP_SAMPLE_JSON['openvpn_client_certificate'] = \
+ EIP_SAMPLE_CONFIG = copy.copy(testdata.EIP_SAMPLE_CONFIG)
+ EIP_SAMPLE_CONFIG['openvpn_client_certificate'] = \
eipspecs.client_cert_path()
- EIP_SAMPLE_JSON['openvpn_ca_certificate'] = \
+ EIP_SAMPLE_CONFIG['openvpn_ca_certificate'] = \
eipspecs.provider_ca_path()
- self.assertEqual(deserialized, EIP_SAMPLE_JSON)
+ self.assertEqual(deserialized, EIP_SAMPLE_CONFIG)
# TODO: shold ALSO run validation methods.
@@ -111,16 +113,20 @@ class EIPCheckTest(BaseLeapTest):
# ok. now, messing with real files...
# blank out default_provider
- sampleconfig = copy.copy(testdata.EIP_SAMPLE_JSON)
+ sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG)
sampleconfig['provider'] = None
eipcfg_path = checker.eipconfig.filename
with open(eipcfg_path, 'w') as fp:
json.dump(sampleconfig, fp)
- with self.assertRaises(eipexceptions.EIPMissingDefaultProvider):
+ #with self.assertRaises(eipexceptions.EIPMissingDefaultProvider):
+ # XXX we should catch this as one of our errors, but do not
+ # see how to do it quickly.
+ with self.assertRaises(jsonschema.ValidationError):
+ #import ipdb;ipdb.set_trace()
checker.eipconfig.load(fromfile=eipcfg_path)
checker.check_is_there_default_provider()
- sampleconfig = testdata.EIP_SAMPLE_JSON
+ sampleconfig = testdata.EIP_SAMPLE_CONFIG
#eipcfg_path = checker._get_default_eipconfig_path()
with open(eipcfg_path, 'w') as fp:
json.dump(sampleconfig, fp)
@@ -132,7 +138,7 @@ class EIPCheckTest(BaseLeapTest):
mocked_get.return_value.status_code = 200
mocked_get.return_value.json = DEFAULT_PROVIDER_DEFINITION
checker = eipchecks.EIPConfigChecker(fetcher=requests)
- sampleconfig = testdata.EIP_SAMPLE_JSON
+ sampleconfig = testdata.EIP_SAMPLE_CONFIG
checker.fetch_definition(config=sampleconfig)
fn = os.path.join(baseconfig.get_default_provider_path(),
@@ -150,22 +156,22 @@ class EIPCheckTest(BaseLeapTest):
mocked_get.return_value.status_code = 200
mocked_get.return_value.json = testdata.EIP_SAMPLE_SERVICE
checker = eipchecks.EIPConfigChecker(fetcher=requests)
- sampleconfig = testdata.EIP_SAMPLE_JSON
+ sampleconfig = testdata.EIP_SAMPLE_CONFIG
checker.fetch_eip_service_config(config=sampleconfig)
def test_check_complete_eip_config(self):
checker = eipchecks.EIPConfigChecker()
with self.assertRaises(eipexceptions.EIPConfigurationError):
- sampleconfig = copy.copy(testdata.EIP_SAMPLE_JSON)
+ sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG)
sampleconfig['provider'] = None
checker.check_complete_eip_config(config=sampleconfig)
with self.assertRaises(eipexceptions.EIPConfigurationError):
- sampleconfig = copy.copy(testdata.EIP_SAMPLE_JSON)
+ sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG)
del sampleconfig['provider']
checker.check_complete_eip_config(config=sampleconfig)
# normal case
- sampleconfig = copy.copy(testdata.EIP_SAMPLE_JSON)
+ sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG)
checker.check_complete_eip_config(config=sampleconfig)
diff --git a/src/leap/eip/tests/test_config.py b/src/leap/eip/tests/test_config.py
index f9f963dc..6759b522 100644
--- a/src/leap/eip/tests/test_config.py
+++ b/src/leap/eip/tests/test_config.py
@@ -12,7 +12,7 @@ except ImportError:
#from leap.eip import config as eip_config
from leap import __branding as BRANDING
from leap.eip import config as eipconfig
-from leap.eip.tests.data import EIP_SAMPLE_SERVICE
+from leap.eip.tests.data import EIP_SAMPLE_CONFIG, EIP_SAMPLE_SERVICE
from leap.testing.basetest import BaseLeapTest
from leap.util.fileutil import mkdir_p
@@ -47,13 +47,21 @@ class EIPConfigTest(BaseLeapTest):
os.chmod(tfile, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
def write_sample_eipservice(self):
- conf = eipconfig.EIPConfig()
+ conf = eipconfig.EIPServiceConfig()
folder, f = os.path.split(conf.filename)
if not os.path.isdir(folder):
mkdir_p(folder)
with open(conf.filename, 'w') as fd:
fd.write(json.dumps(EIP_SAMPLE_SERVICE))
+ def write_sample_eipconfig(self):
+ conf = eipconfig.EIPConfig()
+ folder, f = os.path.split(conf.filename)
+ if not os.path.isdir(folder):
+ mkdir_p(folder)
+ with open(conf.filename, 'w') as fd:
+ fd.write(json.dumps(EIP_SAMPLE_CONFIG))
+
def get_expected_openvpn_args(self):
args = []
username = self.get_username()
@@ -123,6 +131,8 @@ class EIPConfigTest(BaseLeapTest):
def test_build_ovpn_command_empty_config(self):
self.touch_exec()
self.write_sample_eipservice()
+ self.write_sample_eipconfig()
+
from leap.eip import config as eipconfig
from leap.util.fileutil import which
path = os.environ['PATH']
diff --git a/src/leap/eip/tests/test_openvpnconnection.py b/src/leap/eip/tests/test_openvpnconnection.py
index 885c80b3..61769f04 100644
--- a/src/leap/eip/tests/test_openvpnconnection.py
+++ b/src/leap/eip/tests/test_openvpnconnection.py
@@ -1,6 +1,7 @@
import logging
import os
import platform
+import psutil
import shutil
#import socket
@@ -16,6 +17,7 @@ from mock import Mock, patch # MagicMock
from leap.eip import config as eipconfig
from leap.eip import openvpnconnection
+from leap.eip import exceptions as eipexceptions
from leap.eip.udstelnet import UDSTelnet
from leap.testing.basetest import BaseLeapTest
@@ -73,6 +75,16 @@ class OpenVPNConnectionTest(BaseLeapTest):
# tests
#
+ def test_detect_vpn(self):
+ openvpn_connection = openvpnconnection.OpenVPNConnection()
+ with patch.object(psutil, "get_process_list") as mocked_psutil:
+ 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")
def test_lin_mac_default_init(self):
"""
diff --git a/src/leap/gui/firstrunwizard.py b/src/leap/gui/firstrunwizard.py
new file mode 100755
index 00000000..abdff7cf
--- /dev/null
+++ b/src/leap/gui/firstrunwizard.py
@@ -0,0 +1,489 @@
+#!/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)
+
+ 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_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",
+
+ #provider="localhost",
+ #register_path="timeout",
+ #port=8000
+ )
+ try:
+ ok, req = signup.register_user(username, password)
+ except socket.timeout:
+ self.set_status_timeout()
+ 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/tests/integration/fake_user_signup.py b/src/leap/gui/tests/integration/fake_user_signup.py
new file mode 100644
index 00000000..12f18966
--- /dev/null
+++ b/src/leap/gui/tests/integration/fake_user_signup.py
@@ -0,0 +1,80 @@
+"""
+simple server to test registration and
+authentication
+
+To test:
+
+curl -d login=python_test_user -d password_salt=54321\
+ -d password_verifier=12341234 \
+ http://localhost:8000/users.json
+
+"""
+from BaseHTTPServer import HTTPServer
+from BaseHTTPServer import BaseHTTPRequestHandler
+import cgi
+import urlparse
+
+HOST = "localhost"
+PORT = 8000
+
+LOGIN_ERROR = """{"errors":{"login":["has already been taken"]}}"""
+
+
+class request_handler(BaseHTTPRequestHandler):
+ responses = {
+ '/': ['ok\n'],
+ '/users.json': ['ok\n'],
+ '/timeout': ['ok\n']
+ }
+
+ def do_GET(self):
+ path = urlparse.urlparse(self.path)
+ message = '\n'.join(
+ self.responses.get(
+ path.path, None))
+ self.send_response(200)
+ self.end_headers()
+ self.wfile.write(message)
+
+ def do_POST(self):
+ form = cgi.FieldStorage(
+ fp=self.rfile,
+ headers=self.headers,
+ environ={'REQUEST_METHOD': 'POST',
+ 'CONTENT_TYPE': self.headers['Content-Type'],
+ })
+ data = dict(
+ (key, form[key].value) for key in form.keys())
+ path = urlparse.urlparse(self.path)
+ message = '\n'.join(
+ self.responses.get(
+ path.path, ''))
+
+ login = data.get('login', None)
+ #password_salt = data.get('password_salt', None)
+ #password_verifier = data.get('password_verifier', None)
+
+ if path.geturl() == "/timeout":
+ print 'timeout'
+ self.send_response(200)
+ self.end_headers()
+ self.wfile.write(message)
+ import time
+ time.sleep(10)
+ return
+
+ ok = True if (login == "python_test_user") else False
+ if ok:
+ self.send_response(200)
+ self.end_headers()
+ self.wfile.write(message)
+
+ else:
+ self.send_response(500)
+ self.end_headers()
+ self.wfile.write(LOGIN_ERROR)
+
+
+if __name__ == "__main__":
+ server = HTTPServer((HOST, PORT), request_handler)
+ server.serve_forever()