diff options
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 @@ -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.pngBinary files differ new file mode 100644 index 00000000..fbaf90f2 --- /dev/null +++ b/data/images/checked.png 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': ""} @@ -1,2 +1,2 @@  [egg_info] -tag_build = dev +#tag_build = dev @@ -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 | 
